祝愿大家身体健康!

 找回密码
 站点注册
查看: 42|回复: 1

[PBIDEA] PBIDEA:用 uo_vfs 做虚拟文件系统全解(目录与 zip 挂成一个盘、写入靠写目录、句柄属性崩溃边界,PB10 基准 · PB12.5 实测)

[复制链接]
发表于 5 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
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 只说了半句

先看类型清单:
  1. uo_vfs lv
  2. string ls_arc[]
  3. long ll_n, i
  4. string ls_out
  5. lv = create uo_vfs
  6. ll_n = lv.SupportedArchive(ls_arc)
  7. ls_out = '支持的归档类型数=' + String(ll_n)
  8. for i = 1 to ll_n
  9.         ls_out = ls_out + Char(13) + Char(10) + '  ' + ls_arc[i]
  10. next
  11. destroy lv
  12. MessageBox('结果', ls_out)
复制代码

实测返回 2 个:ZIP、7Z。但别被这个数字骗了——硬盘目录也能挂,只是不在这个清单里。源码注释写的是「取提供类型: ZIP 7z, 以及物理目录」,清单却只列了两个归档类型,目录是默认能力,不参与枚举。

还有一条实测结论值得记住:内容识别不看扩展名。把一个正常 ZIP 改名成 pack7.7z 再挂载,mount() 返回 true、成员照样读得出来。反过来说,指望靠扩展名拦非法包是拦不住的,要判真伪请看 mount() 的返回值和随后 read() 的长度。

二、最小可用:把目录挂成一个盘
  1. public function string of_ex1 ();
  2.         uo_vfs lv
  3.         string ls_out, ls_files[]
  4.         long ll_n, i
  5.         // 1) 建对象
  6.         lv = create uo_vfs
  7.         // 2) 把硬盘目录挂成一个虚拟目录:实体 pbvfs/res → 虚拟 /res
  8.         if not lv.mount('pbvfs/res', '/res') then
  9.                 ls_out = '挂载失败:' + lv.vfsLastError()
  10.         else
  11.                 // 3) 用虚拟路径读文件(不用再拼实体路径)
  12.                 ls_out = 'a.txt = [' + String(lv.read('/res/a.txt'), EncodingANSI!) + ']'
  13.                 // 4) 列目录:目录与文件混在同一个数组里,靠名字自己判断
  14.                 ll_n = lv.GetFiles('/res', ls_files)
  15.                 ls_out = ls_out + Char(13) + Char(10) + '条目数=' + String(ll_n)
  16.                 for i = 1 to ll_n
  17.                         ls_out = ls_out + Char(13) + Char(10) + '  ' + ls_files[i]
  18.                 next
  19.         end if
  20.         destroy lv
  21.         MessageBox('结果', ls_out)
  22. 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:不解压直接读成员
  1. public function string of_ex2 ();
  2.         uo_vfs lv
  3.         uo_json lj
  4.         string ls_out
  5.         blob lb
  6.         lv = create uo_vfs
  7.         // 直接挂一个 zip:不用先解压,成员当场可读
  8.         lv.mount('pbvfs/res/pack.zip', '/p')
  9.         lb = lv.read('/p/conf/app.json')
  10.         ls_out = 'conf/app.json 长度=' + String(Len(lb))
  11.         ls_out = ls_out + Char(13) + Char(10) + '实体位置=' + lv.GetRealDir('/p/conf/app.json')
  12.         // state 取属性:返回值恒为 false,信息全在 JSON 里
  13.         lj = create uo_json
  14.         lv.state('/p/conf/app.json', lj)
  15.         ls_out = ls_out + Char(13) + Char(10) + lj.ToString()
  16.         destroy lj
  17.         destroy lv
  18.         MessageBox('结果', ls_out)
  19. end function
复制代码

读数:长度 23,实体位置 pbvfs/res/pack.zip,JSON 是
  1. {"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。
  1. public function string of_ex3 ();
  2.         uo_vfs lv
  3.         uo_vfs_reader lr
  4.         blob lb
  5.         string ls_out
  6.         lv = create uo_vfs
  7.         lv.mount('pbvfs/res', '/res')
  8.         // 句柄式读:先看大小,再分段读
  9.         lr = lv.CreateReader('/res/big.txt')
  10.         ls_out = '大小=' + String(lr.FileSize())
  11.         lb = lr.read(10)
  12.         ls_out = ls_out + Char(13) + Char(10) + '前 10 字节=[' + String(lb, EncodingANSI!) + '] 位置=' + String(lr.tell())
  13.         lr.seek(0)
  14.         lb = lr.read()
  15.         ls_out = ls_out + Char(13) + Char(10) + '整体重读长度=' + String(Len(lb)) + ' 到末尾=' + String(lr.eof())
  16.         destroy lr
  17.         destroy lv
  18.         MessageBox('结果', ls_out)
  19. end function
复制代码

读数:大小 26,前 10 字节 abcdefghij、位置 10,seek 回 0 后整体重读长度 26、已经到末尾。

分段读的边界,实测清单如下(4 字节文件上逐条验证):

操作返回值指针说明
read(2)2 字节2正常
read(0)空 blob不动不会当「读到末尾」
seek(0)true0从字节 0 重来
seek(-1)false不变负偏移直接拒绝
seek(99)true99越界 seek 不报错,只把指针摆到 99
越界后 read()空 blob99读到空
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() 用相对写目录的路径。
  1. public function string of_ex4 ();
  2.         uo_vfs lv
  3.         uo_vfs_writer lw
  4.         string ls_out
  5.         lv = create uo_vfs
  6.         // 写之前必须先指定写目录,否则 write() 一律返回 false
  7.         lv.SetWriteDir('pbvfs/out')
  8.         // 想读回自己写的文件,还得把这个目录也挂进来
  9.         lv.mount('pbvfs/out', '/out')
  10.         lw = lv.CreateWriter('log.txt')
  11.         if not lw.write('第一行' + Char(13) + Char(10)) then
  12.                 ls_out = '写入失败:' + lv.vfsLastError()
  13.         else
  14.                 ls_out = '写成功,读回=[' + String(lv.read('/out/log.txt'), EncodingANSI!) + ']'
  15.         end if
  16.         destroy lw
  17.         destroy lv
  18.         MessageBox('结果', ls_out)
  19. 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 插入到搜索最前」,听起来像「默认后挂的优先」。实测完全不是这样。
  1. public function string of_ex5 ();
  2.         uo_vfs lv
  3.         string ls_out
  4.         lv = create uo_vfs
  5.         // 1) 挂一个不存在的目录:返回 false,原因在 vfsLastError()
  6.         if lv.mount('pbvfs/nosuchdir', '/n') then
  7.                 ls_out = '意外成功'
  8.         else
  9.                 ls_out = '挂不存在目录 → false,原因=' + lv.vfsLastError()
  10.         end if
  11.         // 2) 两个目录挂到同一个虚拟目录:先挂的优先(实测,与帮助注释的 append 说法相反)
  12.         lv.mount('pbvfs/patch1', '/cfg')
  13.         lv.mount('pbvfs/patch2', '/cfg')
  14.         ls_out = ls_out + Char(13) + Char(10) + '先挂的优先 → [' + String(lv.read('/cfg/app.ini'), EncodingANSI!) + ']'
  15.         // 3) UnMount 的参数是「挂载时给的实体路径」,不是虚拟目录;卸掉先挂的,后挂的顶上
  16.         lv.UnMount('pbvfs/patch1')
  17.         ls_out = ls_out + Char(13) + Char(10) + '卸掉 patch1 → [' + String(lv.read('/cfg/app.ini'), EncodingANSI!) + ']'
  18.         destroy lv
  19.         MessageBox('结果', ls_out)
  20. 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=1append 不改变顺序
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。后者信息量大得多:
  1. public function string of_ex6 ();
  2.         uo_vfs lv
  3.         uo_json lj
  4.         long ll_n
  5.         string ls_out
  6.         lv = create uo_vfs
  7.         lv.mount('pbvfs/res/pack.zip', '/p')
  8.         // GetFiles 的 JSON 重载:一次拿到名字、大小、时间、类型、只读标志
  9.         lj = create uo_json
  10.         ll_n = lv.GetFiles('/p', lj)
  11.         ls_out = '条目数=' + String(ll_n) + Char(13) + Char(10) + lj.ToString()
  12.         destroy lj
  13.         destroy lv
  14.         MessageBox('结果', ls_out)
  15. end function
复制代码

结构化结果长这样(ZIP 根目录,两个条目):
  1. {"path":"/p","count":2,"items":[
  2.   {"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},
  3.   {"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 不代表句柄可用。

推荐的自保写法:
  1. uo_vfs lv
  2. uo_vfs_reader lr
  3. blob lb
  4. boolean lb_ok
  5. lv = create uo_vfs
  6. lv.mount('pbvfs/res', '/res')
  7. lr = lv.CreateReader('/res/maybe.txt')
  8. lb_ok = false
  9. if IsValid(lr) then
  10.         lb = lr.read()                    // 先读,读的返回值不会崩
  11.         if lv.vfsLastError() = 'no error(0)' then
  12.                 lb_ok = true
  13.         end if
  14. end if
  15. if lb_ok then
  16.         // 到这里再问 FileSize()/tell() 才安全
  17.         lb_ok = (lr.FileSize() >= 0)
  18. end if
  19. destroy lr
  20. 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_diruo_vfs.GetFiles 只给名字,不递归


一句话选型:「文件要落盘、要改」用 uo_zip / uo_file;「文件只读、想藏起来、想热替换」用 uo_vfs。

另外把代价说清楚,免得用错地方。VFS 的读不是零成本:目录挂载每次 read() 都是一次真实文件 IO,没有缓存层;ZIP 挂载按需只解压被读的那一段,首读比直接读磁盘慢一点,但省掉了整包解压的时间和磁盘占用。所以它适合「包大、一次只读几个文件」的场景(帮助文档、图标、模板、语音包),不适合「把 VFS 当数据库反复扫全包」——那种用法不如先解压到临时目录再走文件系统。写这一侧也要注意:写目录只能有一个,多个业务模块往同一个写目录写时,文件名冲突得自己用子目录隔开(子目录会自动建)。

十一、实战套路:主资源 + 补丁 + 输出三层

把前面所有规则拼起来,就是一个能直接用的结构:
  1. == 目录结构 ==
  2. app/main        主资源(图片、脚本、模板)
  3. app/patch       补丁目录(只放被改过的文件)
  4. app/out         运行时输出(日志、导出)
复制代码
  1. == 挂载顺序(注意:先挂的优先)==
  2. lv.SetWriteDir('app/out')
  3. lv.mount('app/out',   '/app/out')     // 输出:只为读回
  4. lv.mount('app/patch', '/app/res')     // 补丁先挂 → 同名文件补丁赢
  5. 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/... 换成绝对路径即可),一条命令建好目录:
  1. md pbvfs\res\sub, pbvfs\out, pbvfs\patch1, pbvfs\patch2, pbvfs\dirx
  2. "A1" | Set-Content -Encoding ascii pbvfs/res/a.txt
  3. "B2B2" | Set-Content -Encoding ascii pbvfs/res/sub/b.txt
  4. "abcdefghijklmnopqrstuvwxyz" | Set-Content -Encoding ascii pbvfs/res/big.txt
  5. "v=1" | Set-Content -Encoding ascii pbvfs/patch1/app.ini
  6. "v=2" | Set-Content -Encoding ascii pbvfs/patch2/app.ini
  7. "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 与自验证用例见本帖附件。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
 楼主| 发表于 5 小时前 | 显示全部楼层
本帖最后由 pbai 于 2026-11-10 06:26 编辑

PB10 兼容版 PBL 示例包(nvo_vfsdemo)

本帖文中 6 段示例代码已打包成 PB10 兼容版对象库,随附件发布,可直接 Import 或挂库运行。

一、包内文件

  • vfs93.pbl —— PB10 兼容版库,内含 1 个对象 nvo_vfsdemo(6 个方法 of_ex1 ~ of_ex6,与文中示例一一对应)
  • vfs93.pbt —— 配套工程文件(库路径已改为相对路径,解包即可用)
  • src/nvo_vfsdemo.sru —— 对象源码(GBK / CRLF / 无 BOM)
  • tests/t_vfsdemo.pbtest —— 自验证用例,一次跑完 6 个示例并输出期望读数
  • 说明.txt —— 环境要求、部署步骤、调用片段、常见坑与实机验证记录


二、环境要求

  • PowerBuilder 10 为基准版本构建,PB 10 / 10.5 / 11.x / 12.x 均可载入
  • 需 PBIDEA 运行库 PbIdea.dll 与 Pbidea_cs.dll,放在可执行文件同目录(32 位进程用 32 位 DLL)
  • uo_vfs / uo_vfs_reader / uo_vfs_writer / uo_json 的类型声明在 websuite.pbl 里,须把对应 PB 大版本的 websuite.pbl 挂进库搜索路径(PB10 与 PB12.5 的库格式不通用)
  • 操作系统 Windows 7 及以上;本示例不依赖数据库,因此不涉及建表 SQL


三、操作步骤

  • 解包到可写目录(路径不要带空格)
  • 把 PbIdea.dll、Pbidea_cs.dll 放到测试程序同目录
  • 把 vfs93.pbl 加进库搜索路径,或把 src/nvo_vfsdemo.sru 直接 Import 进自己的 PBL
  • 在工作目录下准备素材目录 pbvfs(res/a.txt = A1、res/big.txt = 26 个小写字母、res/sub/b.txt = B2B2、res/pack.zip 内含 conf/app.json 与 readme.txt、out/ 空目录、patch1/app.ini = v=1、patch2/app.ini = v=2),结构详见包内说明.txt
  • 调用示例:nvo_vfsdemo lnv / lnv = create nvo_vfsdemo / MessageBox('结果', lnv.of_ex1()) / destroy lnv;其余方法 of_ex2 ~ of_ex6 分别演示挂 zip 读成员、句柄分段读、写入写目录再读回、错误与挂载优先级、GetFiles 的 JSON 重载


四、自验证方法
把工作目录切到解包目录的上一级(保证 pbvfs 可见),执行 tests/t_vfsdemo.pbtest 中的 test_vfsdemo(),用例会把 6 个方法的返回值写进 g93_vfsdemo.txt。期望读数(与开发机逐字一致):
  1. EX1|a.txt = [A1] / 条目数=4(a.txt、big.txt、pack.zip、sub)
  2. EX2|conf/app.json 长度=23,realpath=pbvfs/res/pack.zip
  3. EX3|大小=26,前 10 字节=[abcdefghij] 位置=10,整体重读长度=26 到末尾=true
  4. EX4|写成功,读回=[第一行]
  5. EX5|挂不存在目录 → false,原因=not found(11);先挂的优先 → [v=1];卸掉 patch1 → [v=2]
  6. EX6|条目数=2(conf 目录项 + readme.txt 文件项)
  7. PASS
复制代码
(EX2 / EX6 里的 realpath 取决于你把 pbvfs 放在哪;modtime 是运行时刻,不要求一致。)

五、实机验证情况(如实记录)

  • PB10 轨:交付包内的 vfs93.pbl 用 PB10 全量重建 0 错误,对象齐全,6 个示例全部跑通,读数与上面期望值逐字一致
  • PB12.5 轨:用包内 src 源码在 PB 12.5 下新建库导入 + 全量重建 0 错误,6 个示例全部跑通,读数与 PB10 轨逐字一致(本对象两版无行为差异)
  • 静态检查:导入前自检 0 错 0 提醒,源码编码 GBK / CRLF / 无 BOM 校验通过
  • 未实测项:真实 7z 归档(本机无 7z 打包工具,只用「内容是 ZIP、改名 .7z」的文件验证过挂载路径);SetRoot 只验证了「设根后按原虚拟路径读到子目录文件」这一条主线


六、最容易踩的三个坑(包内说明.txt 有完整版)

  • 写文件必须先 SetWriteDir,且 CreateWriter 用「相对写目录」的路径;带挂载点前缀(如 '/out/x.txt')时 write() 恒返回 false
  • CreateReader 对不存在的文件也返回 IsValid=true 的对象,此时调 FileSize()/tell()/eof() 会直接把进程打崩(访问违例,TRY-CATCH 抓不住);先 read() 看结果,再决定要不要访问属性方法
  • 同一挂载点挂多个实体是「先挂的优先」,UnMount(实体路径) 之后后挂的才生效——做补丁覆盖要反着排


附件: vfs93_pb10.zip (11.63 KB, 下载次数: 1)
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-11-10 11:23 , Processed in 0.035487 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表