在 AI 开发者空间实现文件完整性检查器:SHA-256 清单与三种退出码

举报
yd_225105208 发表于 2026/11/08 07:30:34 2026/11/08
【摘要】 在华为云 AI 开发者空间用 Python 标准库实现文件完整性检查器:生成 SHA-256 清单,识别新增、缺失及同大小内容变化,区分退出码 0、1、2。实测 42 项测试通过,记录扫描错误传播、严格清单校验和并发读取等限制。

一个目录复制前后看起来完全一样,文件名没有变,文件大小也一样,内容就一定没变吗?今天在华为云 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 开发者空间的实践,最有用的部分是先写清楚命令契约,再用真实样例和故障测试检查实现。正常输出容易做到,明确区分“发现了变化”和“根本没检查完”,才让这个工具的结果便于后续脚本使用。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。