在 AI 开发者空间实现文件完整性检查器:SHA-256 清单与三种退出码
一个目录复制前后看起来完全一样,文件名没有变,文件大小也一样,内容就一定没变吗?今天在华为云 AI 开发者空间的免费体验环境里,我用 Python 标准库做了一个小工具,把这个问题拆成两步:先为目录生成内容清单,再把当前目录和清单进行比较。
这次练习只使用合成文件,不扫描个人目录。工具不负责修复、删除或同步文件,输出的是新增、缺失和变化的相对路径。代码与测试由 AI Shell 辅助生成,我再检查实现和实际运行结果。第一轮能跑通后,仍发现了错误处理和清单校验的问题,因此又做了一轮针对性的修正。
一、先定义输出,再写扫描逻辑
项目位于 /root/workspace/integrity-check-demo,主要文件包括 integrity_check.py、test_integrity_check.py、README.md,以及两个合成样例目录和一份基线清单。
命令行只有两个子命令:
python3 integrity_check.py snapshot sample_before > baseline.json
python3 integrity_check.py check sample_after baseline.json
snapshot 扫描指定目录,把 JSON 写到标准输出;check 重新扫描目录,与已经保存的清单比较。清单使用相对路径作为键,每个普通文件只保存实际读取到的字节数与 SHA-256 摘要。
例如,样例里的 same_size.txt 内容是 AAAA 加一个换行符,清单对应条目为:
{
"version": 1,
"files": {
"same_size.txt": {
"size": 5,
"sha256": "4677942dfa3e74b5dea7484661a2485bb73ba422eb72d311fdb39372c019c615"
}
}
}
这是清单中的一个条目示例;实际样例清单有 5 个文件。路径按排序输出,方便比较版本差异。这里没有记录空目录,也没有记录权限、所有者和修改时间,判断范围就是普通文件的内容。
还有一个容易忽略的使用约定:baseline.json 要放在被扫描的根目录外。示例把它放在项目目录,与 sample_before 平级。若把重定向的输出文件放进扫描目录,Shell 会先创建或截断它,扫描可能把正在生成的清单也算进去。当前工具没有自动排除这种情况,使用时需要遵守这个约定。
二、同样大小的文件,需要比较内容摘要
如果只比较大小,AAAA\n 和 BBBB\n 都是 5 字节,会被误判为相同。样例专门保留了这个场景。
计算摘要时使用固定 64 KiB 的读取块:
h = hashlib.sha256()
size = 0
with open(filepath, "rb") as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk:
break
h.update(chunk)
size += len(chunk)
代码不需要一次性读入整个文件。这里的 size 按实际读到的字节累加,而不是直接取某次 stat 的大小。不过,这仍然不是文件系统快照:文件在读取过程中发生变化时,摘要可能对应一次混合读取结果,后面会说明这个限制。
目录扫描得到当前字典后,比较规则很直接:当前字典独有的键放进 added,基线独有的键放进 missing,两边都有但大小或摘要不同的键放进 changed。三组结果都排序后输出。
check 不会把清单里的路径拼接起来逐个打开。它只扫描用户指定的根目录,再比较两个内存字典。这样,清单路径只是比较对象,不会成为额外的文件读取目标。清单中的一个陌生路径可能被报告为缺失,但不会因此访问那个路径。
三、检查失败,不能伪装成“没有变化”
第一轮实现使用 os.path.isdir()、os.path.isfile()、os.path.islink() 判断条目类型。这些便利函数适合很多日常场景,但它们可能把底层状态读取错误转成 False。对这个工具来说,无法确认的条目被直接跳过,就可能留下不完整的清单。
修正后的扫描先显式调用 os.lstat(),再根据 stat 模块分类:
st = os.lstat(full)
if stat.S_ISLNK(st.st_mode):
continue
if stat.S_ISDIR(st.st_mode):
_walk(full, rel)
elif stat.S_ISREG(st.st_mode):
size, digest = compute_sha256(full)
files[rel] = {"size": size, "sha256": digest}
符号链接和特殊文件会被跳过;目录列举、条目状态读取和普通文件内容读取发生 OSError 时,让异常传到命令层。扫描全部完成后才输出 JSON。对应的失败测试同时检查返回值、标准错误和标准输出,防止一边报告失败,一边留下像成功结果一样的部分 JSON。
退出码约定如下:
| 退出码 | 含义 | 输出约定 |
|---|---|---|
| 0 | 生成清单成功,或检查未发现差异 | 标准输出为 JSON |
| 1 | 检查成功执行,发现新增、缺失或变化 | 标准输出为差异 JSON |
| 2 | 参数、输入、清单格式或读取出错 | 标准错误解释原因,不输出成功 JSON |
这三个值把“文件有变化”和“无法完成检查”分开。接入 Shell 或其他脚本时,不能把所有非零退出码都当成同一种情况。
四、JSON 能解析,不代表清单格式正确
最初的校验只检查 version == 1、大小是否为非负整数、摘要是否为 64 个字符。检查代码时发现,这还不够表达我们自己的格式约定。
在 Python 里,True == 1 为真,bool 也是 int 的子类。因此 JSON 的 true 可能意外通过版本号或大小字段检查。修正后,这两个字段要求实际类型就是 int,版本必须为 1,大小不能为负。
摘要则要求恰好 64 位小写十六进制字符,与程序自身 hexdigest() 的输出保持一致。仅仅满足长度的字符串,例如 64 个 z,不再能通过校验。无效 UTF-8 清单也应返回退出码 2。
这些校验只确认当前工具所需的字段,不是通用 JSON Schema 验证器,也没有增加清单签名、文件恢复或目录同步功能。
五、用真实样例检查三类差异
sample_before 有 5 个普通文件,包含嵌套目录、空文件和 Unicode 文件名。sample_after 保留部分文件,修改两项、删除一项、增加一项。
生成基线后,检查原目录的实际结果为:
{
"added": [],
"changed": [],
"missing": []
}
退出码为 0。检查修改后的目录,实际结果为:
{
"added": ["newfile.txt"],
"changed": ["data/numbers.txt", "same_size.txt"],
"missing": ["empty.txt"]
}
退出码为 1。其中 data/numbers.txt 的大小从 6 字节变成 8 字节;same_size.txt 保持 5 字节,但内容从 AAAA\n 变成 BBBB\n,仍被识别为变化。
第一轮测试曾因为错误导入 unittest.mock.builtin_open 无法启动,修正导入后才通过。这也提醒我,AI 输出的测试文件同样要真实运行,不能根据测试函数数量就宣布验证完成。
最终在云端执行 python3 -m unittest test_integrity_check -v,实际输出为:
Ran 42 tests in 0.434s
OK
这 42 项测试包括无变化、同大小内容变化、新增、缺失、嵌套目录、空文件、Unicode 文件名、符号链接跳过和命令行退出码。补充测试验证了 lstat、目录列举、文件读取失败时返回 2、标准错误非空且标准输出为空;也验证了布尔版本号、布尔大小、非十六进制或大写摘要被拒绝,以及无效 UTF-8 清单返回 2。这里的耗时只是这次小样例测试套件的运行时间,不是扫描性能指标。
六、这个小工具能回答什么,不能回答什么
它适合在可信、相对静止的小目录中,核对一组普通文件与已有基线是否一致。使用前仍要明确几项边界。
首先,SHA-256 清单没有身份认证能力。能够同时修改文件和基线的人,可以重新生成一份匹配的清单。检查结果不能证明文件来自某个可信发布者,也不能代替数字签名。
其次,扫描没有原子性。文件逐个读取;从 lstat 分类到打开文件之间也存在时间窗口。跳过扫描时看到的符号链接,并不等于在对抗并发替换时建立了严格的目录隔离。这个练习没有实现文件系统冻结、快照或安全打开协议。
再次,内容相同不代表所有属性相同。权限、所有者、时间戳、ACL 和扩展属性的变化都不在比较范围内;硬链接也没有按 inode 去重。读取文件还可能更新访问时间,因此“工具没有写入文件内容”和“文件系统元数据完全不变”不是一回事。
最后,分块读取降低了单个文件内容的内存占用,但清单、当前结果和差异列表仍保存在内存中,每层目录也会排序。不能把整个程序描述成固定内存占用。这次没有进行大规模性能测试,也没有在 Windows 上复测;测试通过只覆盖实际运行过的场景。
这次在 AI 开发者空间的实践,最有用的部分是先写清楚命令契约,再用真实样例和故障测试检查实现。正常输出容易做到,明确区分“发现了变化”和“根本没检查完”,才让这个工具的结果便于后续脚本使用。
- 点赞
- 收藏
- 关注作者
评论(0)