马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_hash 做增量摘要计算全解(七种算法、分块续算与未知名静默回落 SM3,PB10 基准 · PB12.5 实测)
阅读说明
① 适用版本:PB 10.0 基准编写,PB 12.5 实机逐项验证(21 组探针两版读数逐字一致,行为零版本差异);PBIDEA 运行库 2026 年 10 月后版本均适用。
② 支持数据库:本文不涉及数据库,全部计算在内存完成。
③ 操作系统与环境要求:Windows 7 及以上;需安装 PBIDEA 运行库(uo_hash 位于 websuite 库,依赖 PbIdea.dll)。
④ 难度系数:★★☆☆☆(接口只有六个声明,难点在「Final 不重置状态」「未知名静默回落 SM3」这两个反直觉行为)。
⑤ 其它阅读说明:本文结论全部来自实机探测——PB10 与 PB12.5 双轨各 21 组用例,摘要值与 Python 标准库 hashlib 逐字节对拍一致;另有 3 组负向编译探测确认不存在的 API。与既有文章的分工:《uo_blob 全解》里的 md5()、crc32() 是对整块 blob 的一次性摘要;SM3/HMAC 篇讲的是带密钥的国密与 HMAC——本篇讲的是标准增量摘要算子 uo_hash 本体。
一、这一篇讲什么
校验一个下载文件是否完整、给口令存个指纹、把大文件分块读完算个 MD5——这类活都叫「摘要计算」。PBIDEA 提供的 uo_hash 是一个标准的增量摘要算子:数据可以一段一段喂进去(update),随时取当前结果(Final),算完关闭(close)。
它一共只有六个声明:open、update(字符串/blob 两个重载)、Final(字符串/blob 两个重载)、close。接口极小,但实测下来有两组行为非常反直觉:Final 取完结果后状态不归零,继续 update 会接着算;open 传一个不认识的算法名,它不报错,静默改算 SM3。这两条踩中任何一条,产出的摘要值就是错的,而且没有任何报错提示。本篇把七个算法、两条输入路、一套状态机、全部边界行为逐一实测摊开。
二、API 全貌:六个声明与返回值语义
| 声明 | 作用 | 实测要点 | | open(algo) | 按算法名创建算子,返回 boolean | 算法名大小写不敏感;未知名也返回 true(见第八节) | | update(string) | 喂入字符串 | 字符串自动转 UTF-8 后参与计算 | | update(blob) | 喂入二进制 | 原样字节参与计算,转什么编码由调用方决定 | | Final(ref string hex) | 取十六进制摘要,返回 int | 返回值=摘要字节数(md5 是 16,不是成功标志) | | Final(ref blob hash) | 取二进制摘要,返回 int | 同上,blob 长度与返回值一致 | | close() | 关闭算子 | 关闭后 Final 返回 0;可重新 open 复用实例 |
两个容易误解的返回值语义,先钉死:
- Final 的返回值不是成功/失败标志,是摘要的字节长度:md5 返回 16、sha1 返回 20、sha256 返回 32、sha512 返回 64。它大于 0 只说明「有摘要产出」。
- 十六进制结果是全大写,长度恒为摘要字节数的两倍:md5 给 32 个字符,sha512 给 128 个字符。
三、最小示例:算一个 MD5
前置:工程已引用 PBIDEA 的 websuite 库。以下代码可放在任意按钮的 clicked 事件或函数里直接跑,无需界面控件。 - // 示例输入:待摘要的原始文本
- string ls_src
- ls_src = 'abc'
- // 创建算子并按 md5 算法打开
- uo_hash lh
- string ls_hex
- long li_ret
- lh = create uo_hash
- lh.open('md5')
- lh.update(ls_src) // 字符串自动按 UTF-8 转字节参与计算
- li_ret = lh.Final(ls_hex) // 返回值 = 摘要字节数(md5 为 16)
- lh.close()
- destroy lh
- // 实测输出:900150983CD24FB0D6963F7D28E17F72(与标准向量一致,全大写)
- MessageBox('MD5 摘要', 'ret=' + String(li_ret) + ' hex=' + ls_hex)
复制代码
'abc' 的 MD5 是密码学教材里的标准测试向量,实测输出 900150983CD24FB0D6963F7D28E17F72,与 Python 标准库 hashlib 的计算结果逐字符一致。用这个向量做第一道自检,能立刻确认环境里 PBIDEA 运行库工作正常。
四、七种算法与标准向量
用同一个 'abc' 输入,把七个算法名全部实测一遍,摘要值与 Python hashlib 逐字节对拍:
| open() 参数 | 摘要字节数 | 十六进制长度 | 'abc' 摘要实测(前 16 位) | | md5 | 16 | 32 | 900150983CD24FB0 | | sha1 | 20 | 40 | A9993E364706816A | | sha224 | 28 | 56 | 23097D223405D822 | | sha256 | 32 | 64 | BA7816BF8F01CFEA | | sha384 | 48 | 96 | CB00753F45A35E8B | | sha512 | 64 | 128 | DDAF35A193617ABA | | sm3 | 32 | 64 | 66C7F0F462EEEDD9 |
全部七组与标准值一致,其中 sm3 的 66C7F0F4…… 正是国密 SM3 标准文档里 'abc' 的官方测试向量。算法名大小写不敏感:实测 'MD5'、'SHA256' 都能正确打开并算出对应结果。但注意——open 参数是字符串写错不报错,见第八节。
五、增量的意义:分块 update 与一次性等价
增量摘要的价值在于:数据不必攒成一整块。实测分块与一次性喂养的结果完全等价:
- // 示例输入:待摘要的完整内容(演示用 12 字节,实际可任意长)
- string ls_full
- ls_full = 'abcdefghijkl'
- // 两个算子:一个一次喂完,一个分三块喂
- uo_hash lh
- string ls_hex1
- string ls_hex2
- lh = create uo_hash
- lh.open('md5')
- lh.update(ls_full)
- lh.Final(ls_hex1)
- lh.close()
- lh.open('md5')
- lh.update('abc') // 第 1 块
- lh.update('def') // 第 2 块
- lh.update('ghijkl') // 第 3 块
- lh.Final(ls_hex2)
- lh.close()
- destroy lh
- // 实测两个结果相等;换成 sha256 分四块同样相等
- MessageBox('分块等价性', 'one=' + ls_hex1 + '~r~nchunk=' + ls_hex2)
复制代码
性能上有一笔账要算清。实测对 102400 字节的数据做 MD5:一次性 update 消耗 CPU 时间 16 毫秒;分成 100 块、每块 1 KB 逐块 update,累计 234~250 毫秒——分块调用本身有十几倍的开销。所以分块的意义不是省时间,而是省内存:对大文件、网络流这类不可能整块装进一个字符串的数据,分块是唯一可行的方式;数据本来就能整块装下时,直接一次喂完更快。
六、两条输入路:字符串自动转 UTF-8,blob 是原样字节
update 有字符串和 blob 两个重载,编码语义完全不同,用同一个中文串「中文」实测:
- // 示例输入:同一个中文串,走两条输入路
- string ls_cn
- ls_cn = Char(20013) + Char(25991) // 「中文」二字
- // 路 A:字符串直喂(内部自动转 UTF-8,6 字节)
- // 路 B:blob 喂 GBK 字节(EncodingANSI!,4 字节)
- uo_hash lh
- string ls_hex_a
- string ls_hex_b
- blob lb_gbk
- lh = create uo_hash
- lh.open('md5')
- lh.update(ls_cn)
- lh.Final(ls_hex_a)
- lh.close()
- lb_gbk = Blob(ls_cn, EncodingANSI!)
- lh.open('md5')
- lh.update(lb_gbk)
- lh.Final(ls_hex_b)
- lh.close()
- destroy lh
- // 实测:A=A7BAC2239F...(与 Python 对 UTF-8 字节算的一致)
- // B=BCCE109775...(与 Python 对 GBK 字节算的一致)
- MessageBox('两条输入路', 'str=' + ls_hex_a + '~r~nblob=' + ls_hex_b)
复制代码
三条实测结论:
- 字符串路自动转 UTF-8。字符串喂进去算出的摘要,与外部工具对 UTF-8 字节算出的完全一致——不管 PB 内部字符串是什么编码,uo_hash 统一按 UTF-8 处理。
- blob 路是原样字节。喂 GBK 字节就按 GBK 字节算,喂 UTF-8 字节就按 UTF-8 字节算。要和外部系统的摘要对上,必须让两边的字节序列一致——对方用 GBK 算的,你就得喂 EncodingANSI! 的 blob;对方用 UTF-8 算的,喂字符串或 UTF-8 blob 都行。
- 两条路结果不同。同一个「中文」,字符串路 6 字节、GBK blob 路 4 字节,摘要自然不同。校验对不上号时,第一件事就是核对两边的字节序列。
七、状态机:Final 不重置,重启要 close 加 open
这是本对象最反直觉的一组行为,逐项实测:
- Final 取完结果,内部状态不归零。同一算子上连取两次 Final,结果相同;再 update 会从原状态继续累计——对已喂 'abc' 的 md5 算子 Final 之后又喂 'def',再 Final 得到的是 md5('abcdef') = E80B5017098950FC58AAD83C8C14978E,不是 md5('def')。
- 未 open 就 Final:返回 0,ref 参数不被填充(原值原样保留);未 open 就 update:静默忽略,不崩溃不报错。
- close 之后 Final:同样返回 0、不填充。close 之后可以重新 open,实例可复用,重新开就是全新状态。
于是「用同一个算子连续算多条独立摘要」的正确姿势是每条之间 close 加 open:
- // 示例输入:待摘要的字符串清单(模拟三条待校验的数据包内容)
- string ls_item1, ls_item2, ls_item3
- ls_item1 = 'user=admin&ts=20261104'
- ls_item2 = 'payload-7f3a9c'
- ls_item3 = 'log-line-088'
- // 逐条独立计算 SHA256:第二条起必须先 close 再 open,否则上一条的状态会串进来
- uo_hash lh
- string ls_hex
- string ls_out
- long li
- lh = create uo_hash
- For li = 1 To 3
- If li > 1 Then
- lh.close()
- End If
- lh.open('sha256')
- If li = 1 Then
- lh.update(ls_item1)
- ElseIf li = 2 Then
- lh.update(ls_item2)
- Else
- lh.update(ls_item3)
- End If
- lh.Final(ls_hex)
- ls_out = ls_out + String(li) + ': ' + Left(ls_hex, 24) + '~r~n'
- End For
- lh.close()
- destroy lh
- MessageBox('SHA256 清单', ls_out)
复制代码
如果嫌状态管理麻烦,也可以每条摘要都 create 一个新实例、算完 destroy——只是对象创建销毁的开销比 close 加 open 大一些,批量场景建议复用实例。
八、最大的坑:未知名静默回落 SM3
open 的算法名写错了会怎样?实测答案是:不报错、open 照样返回 true,然后按 SM3 计算。四组取证:
| open() 实传参数 | open 返回 | 实际算出 | 说明 | | 'notalgo'(胡写的名字) | true | 66C7F0F4……(SM3) | 未知名一律回落 SM3 | | 'sha'(不完整的名字) | true | 66C7F0F4……(SM3) | 没有前缀匹配,匹配不上就回落 | | 'sha3' | true | 66C7F0F4……(SM3) | 不支持 SHA-3,同样回落 | | 'sha-256'(带连字符) | true | 66C7F0F4……(SM3) | 正确写法是 'sha256',无连字符 |
也就是说,算错算法名得到的是一个看起来完全正常的 SM3 摘要——长度正常、格式正常、不报任何错。如果上游系统按 SHA256 校验,这个结果永远对不上,而且你排查时根本想不到算法名写错了。防御办法只有一条:上线前用已知向量自检——按第三节用 'abc' 核对 md5 的标准值,摘要一致才说明算法名写对了;或者干脆在代码里用常量封装算法名,禁止散落硬编码。
九、坑清单(发布前逐条实测核对)
- Final 的返回值是摘要字节数,不是成功标志;判断有没有产出要看返回值是否大于 0。
- 十六进制结果全大写;与外部小写摘要比对前先统一大小写。
- 字符串 update 自动转 UTF-8;要与按 GBK 计算的外部系统对摘要,必须走 blob 路(EncodingANSI!)。
- blob update 是原样字节,编码选择权完全在调用方。
- Final 不重置状态:取完结果继续 update 会接着累计('abc' 后接 'def' 等于算 'abcdef')。
- 连续算多条独立摘要,必须 close 再 open(或换新实例),否则上一条的状态串进下一条。
- 未 open 就 Final 返回 0 且 ref 参数不填充;判断前先确认 open 成功过。
- 未 open 就 update 静默忽略——不报错,但你的数据没被算进去。
- 未知名静默回落 SM3:'notalgo'、'sha'、'sha3'、'sha-256' 全部按 SM3 算,open 照样返回 true。
- 正确的算法名一共七个:md5、sha1、sha224、sha256、sha384、sha512、sm3;大小写随意,写法无连字符。
- 分块 update 与一次性喂养结果等价,但分块调用有开销(100 块比一次性慢十几倍),能整块喂就整块喂。
- 没有 Reset、HexDigest、GetAlgoList 这类方法(负向编译探测确认),「重置」的唯一办法是 close 加 open。
- close 之后重新 open 完全可用,实例可以长期复用,不必反复 create。
十、PB12.5 差异
21 组用例两版跑完,摘要行为零差异:七个算法的摘要值、大小写不敏感、未知名回落 SM3、Final 不重置、未 open 与 close 后的行为、分块等价性,PB10 与 PB12.5 读数逐字一致。
仅两处与行为无关的探测口径差异,如实记录:一是用「blob 切片转字符串再取 Asc」做字节窥探时,两版对高位字节的读数形态不同(PB12.5 给问号的编码值 63,PB10 给 0),该通道在中文 ANSI 环境本就不可靠,字节级验证应以十六进制摘要输出为准;二是 100 KB 分块压测的 CPU 计时两版各在 234~250 毫秒之间浮动,属机器计时噪声。
十一、实机验证情况
- PB10:21 组探针全部 PASS;--pb-version 100 全量编译 0 错误;摘要值与 Python hashlib 逐字节一致。
- PB12.5:同 21 组探针全部 PASS;--pb-version 125 全量编译 0 错误;读数与 PB10 逐字一致。
- 负向探测 3 组(Reset / HexDigest / GetAlgoList)双轨编译均报 C0051,确认方法不存在。
- 文中全部代码块即实机验证过的探针等价代码,无未实测段落。
十二、结语
uo_hash 是那种「接口五分钟、边界两小时」的对象:六个声明一眼看完,但 Final 不重置、未知名回落 SM3 这两条,文档不写、报错不出,只能靠实机把边界一个个踩出来。记住三个动作要领——算法名用常量、独立摘要要重启、跨系统对摘要先对字节编码——剩下的就是普通的状态机代码了。 |