马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_vfs 做虚拟文件系统全解(目录与 zip 挂成一个盘、写入靠写目录、句柄属性崩溃边界,PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,PB 12.5 实测行为一致(PBIDEA 运行库 PbIdea.dll,快照见文末版本声明)
2. 支持数据库:本文不涉及数据库
3. 操作系统与环境要求:Windows 7+;需随程序部署 PbIdea.dll 与 Pbidea_cs.dll;示例用到的资源包为 ZIP 格式;7z 只用「内容是 ZIP、改名 .7z」的文件做过挂载探测,未用真 7z 包实测成员读取
4. 难度系数:★★☆☆☆
5. 其它阅读说明:示例路径均相对「程序工作目录」,素材放在工作目录下的 pbvfs 子目录(res、out、patch1、patch2、dirx),建目录与压缩包的命令见文末「复现示例的准备工作」;VFS 对相对路径与绝对路径同样接受,正式项目里换成绝对路径即可;已读过 uo_file / uo_zip 篇的读者可跳过基础部分
虚拟文件系统(VFS)这个词容易把人吓住,其实它的用处很朴素:让程序用同一套路径去访问硬盘目录、ZIP 包里的成员,甚至内存里的一个 ZIP blob。写插件、换皮肤、做补丁包、把帮助文档和图片打进一个包里发给客户,这些场景都靠它。PBIDEA 提供的 uo_vfs 就是干这件事的,配两个句柄对象 uo_vfs_reader / uo_vfs_writer 做流式读写。
这篇把 uo_vfs 的 20 个方法、2 个句柄对象的 17 个方法一次讲透,重点放在三件最容易踩的事上:写文件必须先设写目录且路径要相对、同一挂载点的优先级是先挂的赢、句柄属性在句柄无效时会直接把进程打崩。三条全是实机跑出来的,不是看声明猜的。
一、它能挂什么:SupportedArchive 只说了半句
先看类型清单:
- uo_vfs lv
- string ls_arc[]
- long ll_n, i
- string ls_out
- lv = create uo_vfs
- ll_n = lv.SupportedArchive(ls_arc)
- ls_out = '支持的归档类型数=' + String(ll_n)
- for i = 1 to ll_n
- ls_out = ls_out + Char(13) + Char(10) + ' ' + ls_arc[i]
- next
- destroy lv
- MessageBox('结果', ls_out)
复制代码
实测返回 2 个:ZIP、7Z。但别被这个数字骗了——硬盘目录也能挂,只是不在这个清单里。源码注释写的是「取提供类型: ZIP 7z, 以及物理目录」,清单却只列了两个归档类型,目录是默认能力,不参与枚举。
还有一条实测结论值得记住:内容识别不看扩展名。把一个正常 ZIP 改名成 pack7.7z 再挂载,mount() 返回 true、成员照样读得出来。反过来说,指望靠扩展名拦非法包是拦不住的,要判真伪请看 mount() 的返回值和随后 read() 的长度。
二、最小可用:把目录挂成一个盘
- public function string of_ex1 ();
- uo_vfs lv
- string ls_out, ls_files[]
- long ll_n, i
- // 1) 建对象
- lv = create uo_vfs
- // 2) 把硬盘目录挂成一个虚拟目录:实体 pbvfs/res → 虚拟 /res
- if not lv.mount('pbvfs/res', '/res') then
- ls_out = '挂载失败:' + lv.vfsLastError()
- else
- // 3) 用虚拟路径读文件(不用再拼实体路径)
- ls_out = 'a.txt = [' + String(lv.read('/res/a.txt'), EncodingANSI!) + ']'
- // 4) 列目录:目录与文件混在同一个数组里,靠名字自己判断
- ll_n = lv.GetFiles('/res', ls_files)
- ls_out = ls_out + Char(13) + Char(10) + '条目数=' + String(ll_n)
- for i = 1 to ll_n
- ls_out = ls_out + Char(13) + Char(10) + ' ' + ls_files[i]
- next
- end if
- destroy lv
- MessageBox('结果', ls_out)
- end function
复制代码
跑出来的读数:a.txt = 「A1」,条目数 4,依次是 a.txt、big.txt、pack.zip、sub。
这里用的是相对路径 pbvfs/res(相对进程当前目录)。VFS 对相对与绝对路径一视同仁:实测把同一个目录用 D:/pbvfs/res 这种绝对路径挂上去,行为完全一致。相对路径的好处是程序拷到哪都能跑;绝对路径的好处是不受「双击启动时工作目录变成 exe 目录」影响——生产环境建议先用 GetCurrentDirectory() 取到工作目录再拼,别默认 cwd 就是你想的那个。
四个细节:
- read() 返回 blob,不是 string。要变字符串必须显式给编码:String(lb, EncodingANSI!)。不给编码时按 Unicode 解,A1 两个字节会被当成一个汉字,结果是问号——这不是乱码,是解错了码。
- GetFiles() 返回条目个数,文件与子目录混在同一个 ref string[] 里,不给类型标志。想区分就自己再探一次 state(),或者改用 JSON 重载(见第七节)。
- 目录挂载成功后 vfsLastError() 是 no error(0)。
- mount() 的虚拟目录不带前导斜杠也能工作('a' 与 '/a' 等效,实测两者读同一个文件都成功),但建议一律带 /,否则和相对路径的写法混起来很难查。
三、挂 ZIP:不解压直接读成员
- public function string of_ex2 ();
- uo_vfs lv
- uo_json lj
- string ls_out
- blob lb
- lv = create uo_vfs
- // 直接挂一个 zip:不用先解压,成员当场可读
- lv.mount('pbvfs/res/pack.zip', '/p')
- lb = lv.read('/p/conf/app.json')
- ls_out = 'conf/app.json 长度=' + String(Len(lb))
- ls_out = ls_out + Char(13) + Char(10) + '实体位置=' + lv.GetRealDir('/p/conf/app.json')
- // state 取属性:返回值恒为 false,信息全在 JSON 里
- lj = create uo_json
- lv.state('/p/conf/app.json', lj)
- ls_out = ls_out + Char(13) + Char(10) + lj.ToString()
- destroy lj
- destroy lv
- MessageBox('结果', ls_out)
- end function
复制代码
读数:长度 23,实体位置 pbvfs/res/pack.zip,JSON 是
- {"path":"/p/conf/app.json","realpath":"pbvfs/res/pack.zip","filesize":23,"modtime":"...","createtime":"...","filetype":"file","readonly":true}
复制代码
这里有三个「反直觉」:
其一,GetRealDir() 对压缩包成员返回的是「包文件的实体路径」,不是成员的虚拟路径。 这个语义要想清楚:它回答的是「这个虚拟路径背后落在哪个实体文件上」,ZIP 成员的答案就是那个 ZIP。目录挂载时它返回的是目录路径,两者都只到「容器」为止。
其二,state() 的返回值恒为 false。 文件存在、目录存在、不存在,全是 false。真正的信息在第二个参数那个 uo_json 里。别按「true=成功」的直觉写判断,否则永远走失败分支。不存在的路径填出来的 JSON 是 {"path":"/zzz/x","realpath":"","filesize":-1,"filetype":"other","readonly":true}——filesize 为 -1 才是判据。
其三,ZIP 挂载成功之后 vfsLastError() 会留一条 not found(11)。 目录挂载不会有这条。这是内部探测成员时留下的尾巴,不代表挂载失败。结论:判成败只看 mount() 的返回布尔值,别拿 vfsLastError() 当唯一判据——它是「上一条操作的错误码文本」,粘得很。
四、句柄式读:reader 的 8 个方法
uo_vfs_reader 有 FileName / FileSize / eof / seek / tell / read() / read(long) / DestroyReader() 一共 8 个。DestroyReader() 是 private,外部调不了(编译报 C0109),释放靠 destroy 对象引用或 destroy 掉 vfs。
- public function string of_ex3 ();
- uo_vfs lv
- uo_vfs_reader lr
- blob lb
- string ls_out
- lv = create uo_vfs
- lv.mount('pbvfs/res', '/res')
- // 句柄式读:先看大小,再分段读
- lr = lv.CreateReader('/res/big.txt')
- ls_out = '大小=' + String(lr.FileSize())
- lb = lr.read(10)
- ls_out = ls_out + Char(13) + Char(10) + '前 10 字节=[' + String(lb, EncodingANSI!) + '] 位置=' + String(lr.tell())
- lr.seek(0)
- lb = lr.read()
- ls_out = ls_out + Char(13) + Char(10) + '整体重读长度=' + String(Len(lb)) + ' 到末尾=' + String(lr.eof())
- destroy lr
- destroy lv
- MessageBox('结果', ls_out)
- end function
复制代码
读数:大小 26,前 10 字节 abcdefghij、位置 10,seek 回 0 后整体重读长度 26、已经到末尾。
分段读的边界,实测清单如下(4 字节文件上逐条验证):
| 操作 | 返回值 | 指针 | 说明 | | read(2) | 2 字节 | 2 | 正常 | | read(0) | 空 blob | 不动 | 不会当「读到末尾」 | | seek(0) | true | 0 | 从字节 0 重来 | | seek(-1) | false | 不变 | 负偏移直接拒绝 | | seek(99) | true | 99 | 越界 seek 不报错,只把指针摆到 99 | | 越界后 read() | 空 blob | 99 | 读到空 | | eof() | 读完后 true | — | 判末尾就用它,别用「read 返回空」 |
seek() 的单位是字节,tell() 也是字节位置,两者和 read(n) 的 n 同一量纲——这点比 PB 原生 FileRead 干净。
ZIP 成员也能开 reader,FileName 返回虚拟路径 /p/y/z.txt、FileSize 返回成员解压后大小、read() 正常。
五、写:必须先设写目录,且路径要相对
这是 uo_vfs 最容易卡住人的地方,也是本文最该记住的一节。先看会崩的写法:挂一个目录,直接往里 CreateWriter 然后 write。
实测:CreateWriter('/a/x.txt') 返回的对象 IsValid 为 true,FileName 正常,但 write() 返回 false,vfsLastError() 说 write directory is not set(13)。此时如果你顺手调一下 lw.tell() 或 lw.FileSize()——进程直接崩,退出码 3221225477(0xC0000005 访问违例),TRY-CATCH 抓不住,日志只留下来 exited before writing a result。
正确姿势只有一条:SetWriteDir() 指定写目录,CreateWriter() 用相对写目录的路径。
- public function string of_ex4 ();
- uo_vfs lv
- uo_vfs_writer lw
- string ls_out
- lv = create uo_vfs
- // 写之前必须先指定写目录,否则 write() 一律返回 false
- lv.SetWriteDir('pbvfs/out')
- // 想读回自己写的文件,还得把这个目录也挂进来
- lv.mount('pbvfs/out', '/out')
- lw = lv.CreateWriter('log.txt')
- if not lw.write('第一行' + Char(13) + Char(10)) then
- ls_out = '写入失败:' + lv.vfsLastError()
- else
- ls_out = '写成功,读回=[' + String(lv.read('/out/log.txt'), EncodingANSI!) + ']'
- end if
- destroy lw
- destroy lv
- MessageBox('结果', ls_out)
- end function
复制代码
读数:写成功,读回 「第一行」(后面跟一个回车换行)。
写目录的四条硬规则,逐条都是踩出来的:
规则一:写目录里的文件不会被 VFS 搜到。 只 SetWriteDir('pbvfs/out') 不挂载,GetRealDir('log.txt') 返回空串,CreateReader('log.txt') 返回的对象合法但读不出内容(read() 返回空 blob,FileSize() 更是直接崩)。想让写出去的文件能读回来,必须把这个目录也 mount() 进来,读的时候用挂载点路径。
规则二:写路径带挂载点前缀必败。 CreateWriter('/out/log.txt') 或 CreateWriter('/a/x.txt') 这种写法,write() 返回 false,错误是 not found(11) 或 write directory is not set(13)。写路径只能是相对写目录的:'log.txt'、'sub/log.txt'。
规则三:子目录会自动建。 写 'sub/g3b.txt',磁盘上 w9/sub/g3b.txt 真出现了,不用先建目录。
规则四:挂载的目录本身是只读的。 往挂载点写不会落盘——磁盘上那个目录从头到尾没多出文件。VFS 的写入永远只往写目录落。
写入行为的其余实测读数:
| 场景 | 结果 | | 覆盖写(先 10 字节 0123456789,再写 AB) | 磁盘上文件变成 2 字节 AB,会截断 | | 追加 CreateWriter(file, true) | 'abc' 之后追加 'de',文件 5 字节 abcde | | 两个 writer 同时打开写两个文件 | 各自正确落盘,互不干扰 | | flush() | 返回 true;不 flush 直接 destroy 也会落盘 | | 写字符串 '中文' | 磁盘 4 字节 D6 D0 CE C4,GBK | | 写 Blob('中文') | 磁盘 4 字节 2D 4E 87 65,UTF-16LE(Blob(字符串) 是 UTF-16) |
最后两行是个真坑:同样是「写中文」,传字符串和传 blob 落下去的编码完全不同,长度却都是 4 字节,肉眼比对字节数根本发现不了。要 ASCII 兼容文本,一律传字符串;要明确 UTF-8,自己先 Blob(s, EncodingUTF8!) 再写。
写目录设好、路径给对的情况下,writer 的属性全部正常:FileSize=3、tell=3、eof=false、seek(0)=true、flush=true。所以那批崩溃的根因不是「这些方法不能用」,而是句柄背后的文件没打开成功——无效句柄上的属性访问就是裸指针解引用。
六、挂载优先级:先挂的赢,append 参数不管用
同一个虚拟目录挂多个实体,是做「主资源 + 补丁包」的常规手法。帮助注释写的是「append: true 添加到搜索最后,false 插入到搜索最前」,听起来像「默认后挂的优先」。实测完全不是这样。
- public function string of_ex5 ();
- uo_vfs lv
- string ls_out
- lv = create uo_vfs
- // 1) 挂一个不存在的目录:返回 false,原因在 vfsLastError()
- if lv.mount('pbvfs/nosuchdir', '/n') then
- ls_out = '意外成功'
- else
- ls_out = '挂不存在目录 → false,原因=' + lv.vfsLastError()
- end if
- // 2) 两个目录挂到同一个虚拟目录:先挂的优先(实测,与帮助注释的 append 说法相反)
- lv.mount('pbvfs/patch1', '/cfg')
- lv.mount('pbvfs/patch2', '/cfg')
- ls_out = ls_out + Char(13) + Char(10) + '先挂的优先 → [' + String(lv.read('/cfg/app.ini'), EncodingANSI!) + ']'
- // 3) UnMount 的参数是「挂载时给的实体路径」,不是虚拟目录;卸掉先挂的,后挂的顶上
- lv.UnMount('pbvfs/patch1')
- ls_out = ls_out + Char(13) + Char(10) + '卸掉 patch1 → [' + String(lv.read('/cfg/app.ini'), EncodingANSI!) + ']'
- destroy lv
- MessageBox('结果', ls_out)
- end function
复制代码
patch1/app.ini 的内容是 v=1,patch2/app.ini 是 v=2。读数:先挂的优先 → 「v=1」,卸掉 patch1 → 「v=2」。
四组对照实验(都验证过):
| 挂载顺序与参数 | 读到 | 结论 | | patch1 → patch2(默认) | v=1 | 先挂的赢 | | patch1 → patch2(第二次 append=true) | v=1 | append 不改变顺序 | | patch1 → patch2(两次都 append=true) | v=1 | 同上 | | patch2 → patch1(默认) | v=2 | 换顺序结果跟着换,确认是「先挂优先」 |
zip 与目录混挂也一样:zip 先挂读到 ZIPX,目录先挂读到 DIRX。所以「补丁覆盖主资源」要反着写:把补丁先挂、主资源后挂,同名文件才是补丁赢;或者干脆只挂补丁目录,让主资源通过 SetRoot 组合。
UnMount() 的参数也容易搞错:传实体路径(挂载时第一个参数),传虚拟目录 /cfg 会返回 false 并报 not mounted(10)。blob 挂载的情况更特殊——mount(blob, '/b') 返回的不是布尔值而是字符串 archive_2786b8ca4834e94e(一个自动生成的归档名),这个字符串就是之后 UnMount() / SetRoot() 的钥匙。
七、GetFiles 的 JSON 重载:一次拿全属性
GetFiles 有两个重载,一个 ref string[],一个 ref uo_json。后者信息量大得多:
- public function string of_ex6 ();
- uo_vfs lv
- uo_json lj
- long ll_n
- string ls_out
- lv = create uo_vfs
- lv.mount('pbvfs/res/pack.zip', '/p')
- // GetFiles 的 JSON 重载:一次拿到名字、大小、时间、类型、只读标志
- lj = create uo_json
- ll_n = lv.GetFiles('/p', lj)
- ls_out = '条目数=' + String(ll_n) + Char(13) + Char(10) + lj.ToString()
- destroy lj
- destroy lv
- MessageBox('结果', ls_out)
- end function
复制代码
结构化结果长这样(ZIP 根目录,两个条目):
- {"path":"/p","count":2,"items":[
- {"name":"conf","realpath":"pbvfs/res/pack.zip","filesize":0,"modtime":"1970-01-01 08:00:00","createtime":"1970-01-01 08:00:00","filetype":"dir","readonly":true},
- {"name":"readme.txt","realpath":"pbvfs/res/pack.zip","filesize":11,"modtime":"...","createtime":"...","filetype":"file","readonly":true}]}
复制代码
要点:
- 顶层是 path / count / items,跟 ref string[] 版本只给名字不同,这里每个条目自带 filetype(file / dir),终于能区分文件与目录了;
- ZIP 成员一律 readonly:true——这也从侧面印证了「归档挂载是只读的」;
- ZIP 里的目录项时间戳是 1970-01-01 08:00:00(Unix 纪元),因为 ZIP 条目没存目录时间。按时间排序时要先把这类哨兵值剔掉,否则目录永远排在最前;
- GetFiles('/') 返回 1,虚拟根只列挂载点名字,不是各挂载点的内容;
- GetFiles('/a/..') 这类越界路径不做安全校验,会返回 7 个条目(实际内容无意义)。目录列举这一路别用它做校验,靠返回值不可信。
八、错误、路径安全与只读边界
vfsLastError() 是唯一的错误文本来源,返回带编号的中文括号形式,实测见过这几个:
| 文本 | 触发场景 | | no error(0) | 上一条操作成功 | | not found(11) | 挂不存在的目录 / 读不存在的文件 / 写路径解析不到挂载点 | | not mounted(10) | UnMount() 传了虚拟目录而不是实体路径 | | write directory is not set(13) | 没 SetWriteDir 就往挂载点写 | | permission denied(21) | 把目录当文件 read() | | filename is illegal or insecure(23) | 路径里有 ..、./ 或反斜杠 |
路径安全这一条值得单列,因为它比想象中严:
- read('/a/../a.txt') → 空 blob + filename is illegal or insecure(23)
- read('/a/./a.txt') → 同样被拒(连 ./ 都不放过)
- read('\\a\\a.txt') → 同样被拒。虚拟路径一律用正斜杠,从 Windows 实体路径复制过来忘了换斜杠,得到的就是空内容和 23 号错误
- 反例:read('/b//a.txt')(双斜杠)反而正常,mountPath 带尾斜杠 /b/ 也能正常读
其余边界:
| 场景 | 结果 | | 跨挂载点找文件 | 不串。/x 挂目录、/y 挂 zip,读 /x/x.txt 得空 blob,不会去 /y 里找 | | read() 不存在的文件 | 空 blob + not found(11),不抛异常 | | read() 一个目录 | 空 blob + permission denied(21) | | 往 ZIP 挂载点写 | CreateWriter 返回合法对象,write() 返回 false,错误 13 | | SetRoot('pack.zip','/y') | 之后 /p/z.txt 就能读到 y/z.txt,把子目录提成根 | | destroy vfs 与 destroy writer 顺序 | 两种顺序都安全,不会崩 | | ZIP 里的中文名成员 | '中文.txt' 可读可列,GetFiles 返回的也是 6 个字符的正常名字 |
九、★★崩溃边界:无效句柄上的属性访问
这一节是全篇最有价值的部分,因为它花了最多实测时间,也是唯一会「没有任何报错、进程直接消失」的地方。
reader 侧:CreateReader('不存在的路径') 不返回 NULL,IsValid() 是 true,FileName 正常回显,read() 安全返回空 blob。但只要调一次 FileSize()——访问违例,进程退出码 3221225477。同理 tell() / eof() 也不要碰。
writer 侧:CreateWriter(路径) 只要路径解析不到可写位置(没设写目录、带挂载点前缀、挂载点不存在),write() 返回 false,此时 FileSize / tell / eof / seek / flush 五个方法全部崩。
把两边的判据总结成一句可执行的话:
每次 CreateReader / CreateWriter 之后,先看 IsValid(),再调一次 read()(reader)或 write()(writer)看返回值,两个都过了才允许访问属性方法;IsValid() 为 true 不代表句柄可用。
推荐的自保写法:
- uo_vfs lv
- uo_vfs_reader lr
- blob lb
- boolean lb_ok
- lv = create uo_vfs
- lv.mount('pbvfs/res', '/res')
- lr = lv.CreateReader('/res/maybe.txt')
- lb_ok = false
- if IsValid(lr) then
- lb = lr.read() // 先读,读的返回值不会崩
- if lv.vfsLastError() = 'no error(0)' then
- lb_ok = true
- end if
- end if
- if lb_ok then
- // 到这里再问 FileSize()/tell() 才安全
- lb_ok = (lr.FileSize() >= 0)
- end if
- destroy lr
- destroy lv
复制代码
这段没有实测跑(它依赖「读成功后再问属性」这个已实测的前提,逻辑本身是纯判断),标注为静态核对。真正的硬结论是上一节那两条:无效句柄调属性必崩,且崩溃不可捕获。
顺带一个经验:这类崩溃在自动化测试里表现为 exited before writing a result (exit code 3221225477),和你写错代码的报错长得完全不一样,看到这个码先怀疑句柄,别怀疑语法。
十、和 uo_zip / uo_7z、uo_file、uo_dir 怎么分工
| 需求 | 用什么 | 说明 | | 打包/解压归档到磁盘 | uo_zip / uo_7z | 真解压、真压缩,落盘产物完整 | | 直接读归档里的成员,不解压 | uo_vfs | 只读、按需读,适合资源包 | | 内存里拼一个 zip 再整体挂上 | uo_vfs.mount(blob, ...) | 返回 archive_xxx 名,UnMount 靠它 | | 普通单文件读写 | uo_file | 文本/ blob 直读直写、加密、占用查询 | | 目录枚举与批量建删 | uo_dir | uo_vfs.GetFiles 只给名字,不递归 |
一句话选型:「文件要落盘、要改」用 uo_zip / uo_file;「文件只读、想藏起来、想热替换」用 uo_vfs。
另外把代价说清楚,免得用错地方。VFS 的读不是零成本:目录挂载每次 read() 都是一次真实文件 IO,没有缓存层;ZIP 挂载按需只解压被读的那一段,首读比直接读磁盘慢一点,但省掉了整包解压的时间和磁盘占用。所以它适合「包大、一次只读几个文件」的场景(帮助文档、图标、模板、语音包),不适合「把 VFS 当数据库反复扫全包」——那种用法不如先解压到临时目录再走文件系统。写这一侧也要注意:写目录只能有一个,多个业务模块往同一个写目录写时,文件名冲突得自己用子目录隔开(子目录会自动建)。
十一、实战套路:主资源 + 补丁 + 输出三层
把前面所有规则拼起来,就是一个能直接用的结构:
- == 目录结构 ==
- app/main 主资源(图片、脚本、模板)
- app/patch 补丁目录(只放被改过的文件)
- app/out 运行时输出(日志、导出)
复制代码- == 挂载顺序(注意:先挂的优先)==
- lv.SetWriteDir('app/out')
- lv.mount('app/out', '/app/out') // 输出:只为读回
- lv.mount('app/patch', '/app/res') // 补丁先挂 → 同名文件补丁赢
- lv.mount('app/main', '/app/res') // 主资源后挂
复制代码
三条落地建议:
- 读一律走虚拟路径(/app/res/xxx),程序里不要再出现实体路径,换打包方式时只改三行 mount;
- 写一律相对写目录('log/2026.log'),子目录会自动建,不要自己拼 app/out/...;
- 判断「文件在不在」不要用 CreateReader——它对不存在的文件也返回合法对象;用 GetFiles 列一遍,或者 read() 后看长度和 vfsLastError()。
变体:用 SetRoot 把子目录提成根。 有时候包里是「按版本分目录」的结构(v1/、v2/),程序只想看见当前版本那一层。SetRoot(归档, 子目录) 就是干这个的:实测把 pack.zip 挂到 /p 之后,SetRoot('pbvfs/res/pack.zip','/y') 返回 true,随后 read('/p/z.txt') 直接读到了包里 y/z.txt 的 4 个字节——根被换掉了,虚拟路径不用跟着变。这样切版本只需要改一行 SetRoot,调用方的路径一字不动。注意它的第一个参数同样是「挂载时给的实体路径」,和 UnMount() 一致,拿 blob 挂载时则用 mount() 返回的那个 archive_xxx。
变体:内存里的 ZIP 直接挂。 mount(blob, '/b') 的重载省掉了落盘:从网络下载、从数据库 blob 字段取出、从内存里动态拼出来的 ZIP,都能直接挂上就读。代价是这个重载返回字符串而不是布尔值(自动生成的 archive_xxx 名),判成败的方式也就变了——名字非空即成功,UnMount() 时要用这个名字。
十二、PB12.5 差异
无差异。 全部探针在 PB10 与 PB12.5 两条轨上各跑一遍,读数逐条一致:SupportedArchive 都是 ZIP/7Z 两项、state() 都恒返回 false、写目录规则相同、无效句柄崩溃的退出码相同(都是 3221225477)、挂载优先级都是先挂优先。口径:PB10 基准 · PB12.5 实测。
十三、坑清单(16 条)
- read() 返回 blob,String(lb) 不给编码会按 Unicode 解,ASCII 变问号;
- SupportedArchive 只列 ZIP/7Z,目录能挂但不在清单里;
- ZIP 挂载成功仍会留下 not found(11),判成败看 mount() 返回值;
- state() 恒返回 false,判据是 JSON 里的 filesize(不存在为 -1);
- GetRealDir() 对 ZIP 成员返回包文件路径,不是成员路径;
- CreateReader 对不存在的文件返回合法对象;
- 无效句柄上调 FileSize/tell/eof/seek/flush 直接崩进程(0xC0000005,不可捕获);
- 写之前必须 SetWriteDir,否则 write() 恒 false;
- 写路径只能是相对写目录的,带挂载点前缀必败;
- 写目录里的文件不会被 VFS 搜到,想读回必须把该目录也挂上;
- 挂载的目录是只读的,往里写不落盘;
- 同一挂载点先挂的优先,append=true 不改变顺序(与帮助注释说法相反);
- UnMount() 传实体路径,传虚拟目录报 not mounted(10);blob 挂载传返回的 archive_xxx;
- 虚拟路径禁用反斜杠、..、./,一律返回 filename is illegal or insecure(23);
- 覆盖写会截断,追加要 CreateWriter(file, true),子目录自动创建;
- 写字符串是 GBK、写 Blob(字符串) 是 UTF-16LE,字节数可能相同,别靠长度判断编码。
十四、复现示例的准备工作
示例以「工作目录下的 pbvfs」为素材根(想放别处,把代码里的 pbvfs/... 换成绝对路径即可),一条命令建好目录:
- md pbvfs\res\sub, pbvfs\out, pbvfs\patch1, pbvfs\patch2, pbvfs\dirx
- "A1" | Set-Content -Encoding ascii pbvfs/res/a.txt
- "B2B2" | Set-Content -Encoding ascii pbvfs/res/sub/b.txt
- "abcdefghijklmnopqrstuvwxyz" | Set-Content -Encoding ascii pbvfs/res/big.txt
- "v=1" | Set-Content -Encoding ascii pbvfs/patch1/app.ini
- "v=2" | Set-Content -Encoding ascii pbvfs/patch2/app.ini
- "DIRX" | Set-Content -Encoding ascii pbvfs/dirx/x.txt
复制代码
pack.zip 用任意工具压一个进去,内含 conf/app.json(内容 {"name":"demo","ver":1})与 readme.txt 即可;z2.zip 与 zipx.zip 只在优先级实验里用到,内含一个 x.txt 就够。
十五、验证与版本声明
- 版本声明:版本源目录 D:/PBAI/UPDATE/10(PB10)与 D:/PBAI/UPDATE/125(PB 12.5);核验日期 2026-11-09;运行库 PbIdea.dll 字节数 18,318,336、PE 导出函数数 2,611、md5 25ac290414abc482178b8a6bbd44779c。版本会持续更新,字节数与 md5 随补丁漂移属正常,判版本请解析 PE 导出表,不要看目录名或体积。
- 双轨四绿:PB10 与 PB12.5 各一遍——导入 0 错误、全量重建 0 错误、PB 运行时跑验证用例 PASS、静态检查 0 错 0 提醒 + 编码检查全 OK。
- 文中 6 个示例方法均实机跑通,读数与正文一致;第九节的「自保写法」按上文标注为静态核对(未单独跑)。
- 文中全部结论来自 60 余个独立探针对象(每个探针单独编译、单独运行,崩溃型用例一例一次隔离跑),PB10 兼容版 PBL 与自验证用例见本帖附件。
|