ZIP 附件交给 Agent 前,先列清单再选成员
用归档哈希与成员索引保留附件身份,先人工选材,再对同一快照做限量读取。
- 先列成员,所有清单都等待人工选择
- 归档哈希加目录索引标识成员
- 风险提示保留差异,不擅自合并名称
- 输入变化或预算错误就停止
- 选材完成不提供模型防注入保证
1. 从附件列表开始,而不是从解压目录开始
用户上传一个 ZIP,请 Agent 帮忙整理里面的报告。最容易写出的程序是先解压,再递归收集文本。这会提前替用户做掉几个决定:重名条目到底读哪一个,包里的说明文件是不是任务指令,以及所有附件是否都应该进入模型上下文。等摘要生成后,再回头核对读过哪些材料已经很困难。
本文做一个更窄的入口:先读取 ZIP 目录,生成成员清单;人根据当前任务明确选中成员;程序把选择绑定到同一份归档,然后有限量地读取成员正文。清单里即使没有风险提示,状态也仍然是 needs_review。程序不创建成员目录,不执行附件,不调用模型。最后拿到的是可追溯的选材结果,接入什么模型以及允许模型使用哪些工具,需要另行设计。
全部样本都是本地生成的短字符串,运行只需 Python 标准库。实测环境为 Linux、CPython 3.12.14;以下关于默认解压、重名查找和 Unicode extra 字段的结果都限定于这个环境。它们用于解释为什么成员清单值得单独存在,不能扩展成任意解压器的安全结论。
2. 先把归档快照和成员身份分开
一个文件名看起来很像唯一标识,但 ZIP 目录允许出现重复名称。我们的小样本按顺序写入两个 report.txt,正文分别是 first 和 second。在本机用字符串 read("report.txt") 读取时得到 second;对 infolist() 中两个独立的 ZipInfo 分别读取,则得到 first 和 second。Python 文档也特别说明,open、read 和 extract 可以接收 ZipInfo,这对重名成员有用。
因此,manifest 的成员主键是归档 SHA-256 加上目录成员 index。索引指向本次 infolist() 的具体位置,读取时传入该位置的 ZipInfo,不再用名称查找。header_offset 可以保留给排查使用,但不能代替输入快照:另一个包可以出现相同名称、相同索引甚至相同偏移。
示例对一份不可变 bytes 快照计算哈希、列目录和读成员。人工选择记录保存当时的归档哈希;再次读取时先比较当前快照。哪怕改变的只是归档注释,也必须停止并重新查看清单。SHA-256 在这里用于发现输入变化,并不证明附件来自可信发送者,也不提供数字签名。
实现时也要区分内存快照和路径名称。若产品分两次打开同一个上传路径,路径未变不能证明内容未变。可从受控存储获取版本固定的对象,再在读取入口重新校验;同时把真正的用户身份、任务范围和权限保存在受保护的一侧。本文的本地选择 JSON 可被运行者修改,它没有认证作用。
3. 清单只解释元数据,不替人批准
每个成员保留 filename、orig_filename、压缩方法、flags、压缩与解压声明字节数、create_system、external_attr、extra 字段证据及风险原因。整体记录还包含归档字节数、SHA-256 和声明总量。输出中采用转义后的 JSON 名称,便于辨认换行、组合字符等不易直接看到的差异;若以后放进网页,仍应做 HTML 转义,不能把名称当成 HTML。
风险提示至少包含同名、父路径或非便携路径、Unix 符号链接标记,以及潜在 Unicode/大小写别名。标记是提醒人注意后续消费者的解释差异,不是恶意判定。包内名为 AGENTS.md 或 instructions.txt 的条目,也只是附件。文件名、注释与正文都不能替代用户的阅读选择,更不能给 Agent 增加工具权限。
NFC 形式的 café.txt 与由 e 加组合重音构成的名称,在本机仍是两个成员;Case.txt 与 case.txt 也分别存在。我们用 NFC 加 casefold 找潜在别名,保留原始解析名称和独立索引,不合并、不改名。这种比较并非所有文件系统的精确等价规则;Windows、macOS 和 Linux 的实际落盘表现还受卷格式和配置影响。
声明大小用于提前拒绝明显超过教学预算的输入,但它来自归档目录。file_size 小不等于解析成本低,compress_size 也不能独立证明压缩内容正确。成员数量检查发生在 ZipFile 构造、解析目录之后,不能写成“解析前的硬内存上限”。上传体积限额应在接收层实行,示例函数拿到 bytes 时该对象已经存在。
4. 三个容易写错的 ZIP 结论
第一,不应把本次 ../ 测试描述为成功的目录穿越。仅在自己新建、随后清理的临时目录里,本机 extract("../parent-marker.txt") 得到目标内的 parent-marker.txt;绝对名称也被移除前缀。真正观察到的问题是信息损失:a/../b.txt 和 a/b.txt 都映射到 a/b.txt,后写入的正文覆盖先前正文。extract 的规范化规则可以帮助解释这个结果,却不能证明所有目录状态和解压工具都安全。本文读取器不提供解压操作。
第二,Unix 符号链接属性与实际创建链接不是同一件事。样本设置 create_system=3,并把符号链接 mode 放到 external_attr 高 16 位;本机默认 extract 把它写成普通文件,正文是 short.txt。其他消费者可能解释这些属性,因此清单仍要提示。本次没有测试其他解压器或目标中预先存在的符号链接。固定版本的 zipfile.Path.is_symlink 实现恒返回 False,不能拿它替代属性分析。
第三,非 UTF-8 标志并不意味着可忽略 Unicode Path extra。本机维护版本会处理 0x7075 扩展字段:测试中 UTF-8 flag 为 false,filename 却为 café.txt,orig_filename 为 legacy.txt。这里 orig_filename 仍然是解析后的 Python 字符串,并非原始文件名字节。不要根据这个属性声称已保存原始编码证据。示例同时保留两种名称、flags、extra 摘要和整个归档快照;如需字节级取证,应另外设计并审查解析路径。
这些行为可对照 CPython 3.12.14 固定源码与 PKWARE APPNOTE中路径格式、外部属性及 Unicode Path extra 的定义。在线 Python 3.12 文档当前显示 3.12.15,本文实测仍是 3.12.14,不能把两个维护版本混为一次验证。
5. 人选中之后,读取仍然有停止条件
人工选择只确定“本次想读哪些成员”。读取入口还要重新验证归档哈希、索引是否有效、是否重复选择、预算是否合理,以及方法和标记是否属于当前实现支持的范围。我们只演示普通成员的受限读取;加密、不支持的压缩方法及链接类成员留给审查流程,不因选择字段出现就自动放行。目录本身不当正文读取,也不递归打开嵌套归档。
示例默认接纳不超过 65,536 字节的归档,目录不超过 32 项,所有成员的声明总量不超过 8,192 字节。读正文另设单成员与本次总预算;清单阶段的声明总量预筛选,与后面的实际流读限额是两条检查。测试会把流读预算单独调低,以观察 4096 字节成员被拒绝的情况。
逐块读取时同时计算单成员用量和本次总用量。剩余预算取两者的最小值,每次最多请求一个小块;预算恰好用完,还要请求至多一个额外字节以区分 EOF 和超额。超出的块不进入输出。这个设计限制被接受的成员字节,允许用于探测的额外字节,不能称为“零超读”,也不能限制库内部已经做过的解压和缓冲。
只有选中成员全部读完,才向下游返回结果。若第二个成员失败,第一项即使读完也不作为可消费的部分正文返回。生产接口需要明确这种原子交付语义:不能一边把正文流向模型,一边在末尾才发现超过总预算。本例在受限内存里暂存选择结果,只为让这个边界可见。
解析错误、CRC 错误或预算错误都以 stop 结束本次读取,而不是跳过失败项后假装材料齐全。提前停止的成员没有完成完整性检验;CRC 即使通过,也只是损坏检测,无法证明来源可信。错误记录也不要未经处理地把附件正文或异常中的不可信字符串插入用户界面。
完整返回还要核对实际读得的成员字节数与目录中的 file_size 一致。2026-10-14 在同一 Python 版本的小样本中,仅把五字节 STORED 成员的本地头与目录声明大小改成六字节,保留正文和正确 CRC,库仍正常读到 EOF 并返回五字节。因此读取器在交付前单独检查长度;不一致返回 member_size_mismatch,并扣住整批正文。这是额外的一致性检查,不证明归档声明可信,也不能发现所有格式矛盾。
6. 从空目录复现
将下列源码按标题保存到同一空目录,使用 Python 3.12.14 运行。它们不依赖本文研究目录、网络、账户或额外软件包。教学 ZIP 在内存或受控的样本生成步骤中创建,不需要下载真实附件。演示脚本里的选择是作者为测试固定写入的样本索引,不是在替真实用户作选择。接入应用时应先展示清单,收到人的明确选择后才创建 selection_record。固定预期输出用于比较已观察行为;换用其他版本时应重新核对版本差异,而不是删除失败断言。
先运行清单命令,查看各个 index,再决定要不要运行示例选择。第二条命令明确选择原创新样本里的 0 和 1 两项;无参数运行 choose_demo.py 只显示用法并退出。演示没有任意归档路径入口,避免误拿真实用户附件跑教学实验。
python intake.py
python choose_demo.py 0 1
python run_demo.py
python test_intake.py四条命令的完整 stdout 依次对应 expected-manifest.json、expected-selection.json、expected.json 和 expected-test-output.txt。下文完整展开这些文件;测试还会把演示输出与 expected.json 逐字比较。API 成功时在内存返回正文,命令行只显示选择记录、字节数和摘要。
可选复现实验只操作作者生成的小样本。下面第一条命令要求 generated-fixtures 尚不存在,写入 demo.zip 和 unicode-extra.zip;第二条命令只在新建临时目录里解压生成的样本,退出后清理。完整输出分别在 expected-generator-output.json 和 expected-extract-output.json。
python generate_fixtures.py generated-fixtures
python reproduce_extract_behavior.py7. fixture.py 完整文件
"""Deterministic, tiny, original teaching fixtures. No external input."""
import io
import stat
import struct
import warnings
import zipfile
import zlib
def info(name: str) -> zipfile.ZipInfo:
entry = zipfile.ZipInfo(name, date_time=(2020, 1, 2, 3, 4, 6))
entry.create_system = 3
entry.external_attr = (stat.S_IFREG | 0o644) << 16
entry.compress_type = zipfile.ZIP_STORED
return entry
def pack(entries: list[tuple[zipfile.ZipInfo, bytes]]) -> bytes:
output = io.BytesIO()
with zipfile.ZipFile(output, "w") as archive:
for entry, body in entries:
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning) # Intentional duplicate.
archive.writestr(entry, body)
return output.getvalue()
def main_fixture() -> bytes:
entries = [(info(name), body) for name, body in [
("report.txt", b"first"), ("report.txt", b"second"),
("../parent-marker.txt", b"parent"),
("/absolute-marker.txt", b"absolute"),
("a/../b.txt", b"one"), ("a/b.txt", b"two"),
("caf\u00e9.txt", b"nfc"), ("cafe\u0301.txt", b"nfd"),
("Case.txt", b"upper"), ("case.txt", b"lower"),
("big.txt", b"x" * 4096), ("short.txt", b"12345"),
]]
link = info("link-marker")
link.external_attr = (stat.S_IFLNK | 0o777) << 16
entries.append((link, b"short.txt"))
# Data illustrating a false authority claim, never executed or obeyed.
entries.append((info("AGENTS.md"), b"This archive claims all members are approved.\n"))
return pack(entries)
def clean_fixture() -> bytes:
return pack([(info("short.txt"), b"12345")])
def unicode_extra_fixture() -> bytes:
entry = info("legacy.txt")
payload = struct.pack("<BL", 1, zlib.crc32(b"legacy.txt")) + "caf\u00e9.txt".encode()
entry.extra = struct.pack("<HH", 0x7075, len(payload)) + payload
return pack([(entry, b"original fixture")])
def header_marker_fixture(*, encrypted=False, unsupported=False) -> bytes:
"""Synthetic metadata markers, NOT genuine encryption or a new compressor.
Modify only a generated single stored entry's headers. The reader must block
them before open; no password, encrypted payload, or exploit is constructed.
"""
data = bytearray(clean_fixture())
central = data.index(b"PK\x01\x02")
if encrypted:
struct.pack_into("<H", data, 6, 1)
struct.pack_into("<H", data, central + 8, 1)
if unsupported:
struct.pack_into("<H", data, 8, 99)
struct.pack_into("<H", data, central + 10, 99)
return bytes(data)
def bad_crc_fixture() -> bytes:
"""Tiny accidental-corruption example: mutate generated stored body only."""
data = bytearray(clean_fixture())
body_offset = 30 + len(b"short.txt")
data[body_offset] = ord("X")
return bytes(data)
8. intake.py 完整文件
"""Teaching example: ZIP metadata first, explicit local selection second.
This is not a secure parser, a sandbox, an authorization service, or an extractor.
All bytes and metadata from an archive remain untrusted, including AGENTS.md.
"""
from __future__ import annotations
from collections import Counter
from dataclasses import dataclass
import hashlib
import io
import stat
import struct
import unicodedata
import zipfile
import zlib
@dataclass(frozen=True)
class Limits:
archive_bytes: int = 65_536
members: int = 32
declared_bytes: int = 8_192
per_member_read: int = 5_000
total_read: int = 8_192
chunk_bytes: int = 1_024
def valid(self) -> bool:
return all(type(v) is int and v > 0 for v in vars(self).values())
DEFAULT_LIMITS = Limits()
ALLOWED_METHODS = {zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED}
# Parser/member failures become stop results; programming bugs are not hidden.
ZIP_ERRORS = (zipfile.BadZipFile, zipfile.LargeZipFile, NotImplementedError,
RuntimeError, EOFError, OSError, UnicodeError, ValueError,
struct.error, zlib.error)
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def stop(reason: str, **details) -> dict:
# On any failed read, even previously completed member bodies are withheld.
return {"status": "stop", "reason": reason, **details}
def _alias_key(name: str) -> str:
# A warning heuristic, NOT the equivalence rules of every filesystem.
return unicodedata.normalize("NFC", name).casefold()
def _reasons(info: zipfile.ZipInfo, exact: Counter, aliases: Counter) -> list[str]:
names = (info.filename, info.orig_filename)
reasons = []
if exact[info.filename] > 1:
reasons.append("duplicate_filename")
if aliases[_alias_key(info.filename)] > exact[info.filename]:
reasons.append("possible_unicode_or_case_alias")
if any(name.startswith("/") or "\\" in name or ":" in name
or ".." in name.split("/") for name in names):
reasons.append("nonportable_or_parent_path")
if any(any(ord(char) < 32 or ord(char) == 127 for char in name) for name in names):
reasons.append("control_character_in_name")
if info.filename != info.orig_filename:
reasons.append("decoded_name_changed")
if info.create_system == 3 and stat.S_ISLNK(info.external_attr >> 16):
reasons.append("unix_symlink_marker")
if info.flag_bits & (0x1 | 0x40):
reasons.append("encryption_marker")
if info.flag_bits & (0x20 | 0x40):
reasons.append("unsupported_zip_feature")
if info.compress_type not in ALLOWED_METHODS:
reasons.append("unsupported_compression_method")
if info.is_dir():
reasons.append("directory_entry")
return reasons
def manifest(data: bytes, limits: Limits = DEFAULT_LIMITS) -> dict:
"""Parse directory metadata only; never open or extract a member body.
Only immutable byte snapshots are accepted, so the hash and parser see the
same bytes. Creating that snapshot is the caller's separate intake concern.
The byte limit here is not a pre-allocation bound on the caller's input.
"""
if not limits.valid():
return stop("invalid_budget")
if type(data) is not bytes:
return stop("immutable_bytes_required")
if len(data) > limits.archive_bytes:
return stop("archive_byte_budget")
identity = {"archive_sha256": sha256(data), "archive_bytes": len(data)}
try:
with zipfile.ZipFile(io.BytesIO(data)) as archive:
# ZipFile has already parsed/allocated the central directory here.
# This post-parse count check is NOT a parser resource hard limit.
infos = archive.infolist()
if len(infos) > limits.members:
return stop("member_count_budget", **identity)
declared_total = sum(info.file_size for info in infos)
if declared_total > limits.declared_bytes:
return stop("declared_byte_budget", **identity)
exact = Counter(info.filename for info in infos)
aliases = Counter(_alias_key(info.filename) for info in infos)
rows = []
for index, info in enumerate(infos):
rows.append({
"index": index,
"filename": info.filename,
# Both are decoded Python strings. Neither is raw name bytes.
"orig_filename": info.orig_filename,
"header_offset": info.header_offset,
"compression_method": info.compress_type,
"flag_bits": info.flag_bits,
"utf8_flag": bool(info.flag_bits & 0x800),
"compressed_bytes": info.compress_size,
"declared_bytes": info.file_size,
"crc32": f"{info.CRC:08x}",
"create_system": info.create_system,
"external_attr": info.external_attr,
"internal_attr": info.internal_attr,
"extra_hex": info.extra.hex(),
"extra_sha256": sha256(info.extra),
"comment_bytes": len(info.comment),
"comment_sha256": sha256(info.comment),
"risk_reasons": _reasons(info, exact, aliases),
})
# No risk findings does not imply approval or safety.
return {"status": "needs_review", **identity,
"declared_total": declared_total,
"archive_comment_bytes": len(archive.comment),
"archive_comment_sha256": sha256(archive.comment),
"selected_indexes": [], "members": rows}
except ZIP_ERRORS as error:
return stop("parse_error", error_type=type(error).__name__, **identity)
def selection_record(review: dict, indexes: list[int]) -> dict:
"""Record an explicit local choice. This is forgeable, not authorization.
An actual application must obtain/enforce authorization separately. Neither
the archive's names nor its bodies can supply that authorization.
"""
if review.get("status") != "needs_review":
return stop("manifest_not_reviewable")
if (type(indexes) is not list or not indexes
or any(type(index) is not int or index < 0
or index >= len(review["members"]) for index in indexes)
or len(set(indexes)) != len(indexes)):
return stop("invalid_selection")
return {"kind": "local_demo_selection",
"archive_sha256": review["archive_sha256"],
"directory_indexes": list(indexes)}
def read_selected(data: bytes, record: dict | None,
limits: Limits = DEFAULT_LIMITS) -> dict:
"""Read only selected ZipInfo objects, returning bodies only on full success.
A read requests at most remaining allowance + 1 bytes. That extra byte is
an EOF/overflow probe, so this is NOT zero-overread. It bounds bytes returned
by ZipExtFile.read and bodies accepted here, not internal decompressor work,
CPU, memory, read-ahead, or central-directory parser resources.
"""
if not limits.valid():
return stop("invalid_budget")
if type(data) is not bytes:
return stop("immutable_bytes_required")
if len(data) > limits.archive_bytes:
return stop("archive_byte_budget")
if not isinstance(record, dict) or record.get("kind") != "local_demo_selection":
return stop("selection_required")
if record.get("archive_sha256") != sha256(data):
return stop("input_hash_changed")
review = manifest(data, limits)
if review["status"] == "stop":
return review
indexes = record.get("directory_indexes")
validated = selection_record(review, indexes)
if validated.get("status") == "stop":
return validated
# Keep the validated private copy, not the caller's mutable list.
# This fixes the read scope after validation; it is not authorization,
# revocation handling, or a claim of thread-safe record acquisition.
indexes = tuple(validated["directory_indexes"])
# Check every selection before opening any body. Suspicious names alone
# can be inspected as data; there is no path construction or extraction.
blocked = {"encryption_marker", "unsupported_zip_feature",
"unsupported_compression_method", "unix_symlink_marker",
"directory_entry"}
for index in indexes:
reasons = sorted(blocked.intersection(review["members"][index]["risk_reasons"]))
if reasons:
return stop("selected_entry_blocked", failed_index=index, blockers=reasons)
used = 0
completed = []
index = None
try:
with zipfile.ZipFile(io.BytesIO(data)) as archive:
infos = archive.infolist()
for index in indexes:
member_used = 0
chunks = []
# A string lookup could silently choose a different duplicate.
with archive.open(infos[index], "r") as stream:
while True:
allowance = min(limits.per_member_read - member_used,
limits.total_read - used)
chunk = stream.read(min(limits.chunk_bytes, allowance + 1))
if not chunk:
break
if len(chunk) > allowance:
return stop("stream_byte_budget", failed_index=index)
chunks.append(chunk)
member_used += len(chunk)
used += len(chunk)
# EOF and CRC alone do not establish declared-size consistency.
if member_used != infos[index].file_size:
return stop("member_size_mismatch", failed_index=index)
body = b"".join(chunks)
completed.append({"index": index, "bytes": len(body),
"sha256": sha256(body), "body": body})
return {"status": "selected_read_complete", "accepted_bytes": used,
"members": completed}
except ZIP_ERRORS as error:
# No bodies/partial digests leak through the failure result. An early
# budget stop makes no claim that the member's complete CRC was checked.
return stop("member_read_error", failed_index=index,
error_type=type(error).__name__)
def summary(result: dict) -> dict:
"""JSON-friendly read result with all successful bodies deliberately omitted."""
if result["status"] != "selected_read_complete":
return result
return {**result, "members": [
{key: value for key, value in member.items() if key != "body"}
for member in result["members"]]}
if __name__ == "__main__":
# The standalone command takes no archive path, only this original fixture.
import json
from fixture import main_fixture
print(json.dumps(manifest(main_fixture()), ensure_ascii=True, indent=2))
9. choose_demo.py 完整文件
"""Explicit local selection of the original demo fixture, not authorization."""
import json
import sys
from fixture import main_fixture
from intake import manifest, read_selected, selection_record, summary
def main():
if len(sys.argv) < 2:
raise SystemExit("usage: python choose_demo.py INDEX [INDEX ...]")
try:
indexes = [int(value) for value in sys.argv[1:]]
except ValueError:
raise SystemExit("indexes must be integers")
data = main_fixture()
record = selection_record(manifest(data), indexes)
if record.get("status") == "stop":
print(json.dumps(record, indent=2))
raise SystemExit(2)
result = read_selected(data, record)
print(json.dumps({"selection_record": record, "read_result": summary(result)}, indent=2))
if result["status"] == "stop":
raise SystemExit(2)
if __name__ == "__main__":
main()
10. run_demo.py 完整文件
"""Print deterministic JSON; all archives are original fixtures in memory."""
import io
import json
import zipfile
from fixture import (clean_fixture, header_marker_fixture, main_fixture,
unicode_extra_fixture)
from intake import Limits, manifest, read_selected, selection_record, summary
def report() -> dict:
data = main_fixture()
review = manifest(data)
def selected(indexes, limits=Limits()):
return summary(read_selected(data, selection_record(review, indexes), limits))
# Direct reading is confined to this tiny, locally generated duplicate-name
# experiment. The intake workflow below uses hash-bound selections instead.
with zipfile.ZipFile(io.BytesIO(data)) as archive:
infos = archive.infolist()
duplicate = {"by_string": archive.read("report.txt").decode("ascii"),
"by_zipinfo": [archive.read(i).decode("ascii") for i in infos[:2]]}
path_symlink = zipfile.Path(archive, "link-marker").is_symlink()
clean = manifest(clean_fixture())
unicode_review = manifest(unicode_extra_fixture())
marker_results = {}
for label, kwargs in [("encrypted_marker", {"encrypted": True}),
("unsupported_method_marker", {"unsupported": True})]:
marked_data = header_marker_fixture(**kwargs)
marked_review = manifest(marked_data)
marker_results[label] = {
"manifest_status": marked_review["status"],
"risk_reasons": marked_review["members"][0]["risk_reasons"],
"selected_read": summary(read_selected(marked_data,
selection_record(marked_review, [0]))),
}
return {
"fixture_manifest": {
"status": review["status"],
"archive_sha256": review["archive_sha256"],
"archive_bytes": review["archive_bytes"],
"declared_total": review["declared_total"],
"member_count": len(review["members"]),
"selected_indexes": review["selected_indexes"],
"risk_indexes": {
reason: [row["index"] for row in review["members"]
if reason in row["risk_reasons"]]
for reason in sorted({reason for row in review["members"]
for reason in row["risk_reasons"]})},
},
"duplicate_name_experiment": duplicate,
"zipfile_Path_is_symlink_python_3_12_14": path_symlink,
"clean_manifest": {"status": clean["status"],
"risk_reasons": clean["members"][0]["risk_reasons"],
"selected_indexes": clean["selected_indexes"]},
"unicode_extra_member": {key: unicode_review["members"][0][key]
for key in ("filename", "orig_filename", "utf8_flag",
"extra_hex", "risk_reasons")},
"no_selection": summary(read_selected(data, None)),
"selected_duplicates": selected([0, 1]),
"member_4096_limit_1000": selected([10], Limits(per_member_read=1000)),
"combined_11_limit_10": selected([0, 1], Limits(total_read=10)),
"exact_5_limit_5": selected([11], Limits(per_member_read=5, total_read=5)),
"input_changed": summary(read_selected(data + b"x", selection_record(review, [0]))),
"parse_failure": manifest(b"not a ZIP"),
"synthetic_header_markers": marker_results,
"agents_text_has_no_authority": {
"index": 13,
"manifest_status": review["status"],
"selected_indexes": review["selected_indexes"],
"automatic_action": "none",
},
}
if __name__ == "__main__":
print(json.dumps(report(), ensure_ascii=True, indent=2))
11. test_intake.py 完整文件
"""Stdlib tests using only tiny original fixtures; deterministic test output."""
from dataclasses import replace
import io
import json
from pathlib import Path
import stat
import sys
import unittest
from unittest.mock import patch
import zipfile
from fixture import (bad_crc_fixture, clean_fixture, header_marker_fixture, info,
main_fixture, pack, unicode_extra_fixture)
from run_demo import report
from intake import (DEFAULT_LIMITS, Limits, manifest, read_selected,
selection_record, sha256)
class IntakeTests(unittest.TestCase):
def setUp(self):
self.data = main_fixture()
self.review = manifest(self.data)
def read(self, indexes, **limits):
return read_selected(self.data, selection_record(self.review, indexes),
replace(DEFAULT_LIMITS, **limits))
def assertStoppedWithoutBodies(self, result, reason):
self.assertEqual(result["status"], "stop")
self.assertEqual(result["reason"], reason)
self.assertNotIn("members", result)
self.assertNotIn("body", result)
def test_agents_text_is_data_not_authority(self):
self.assertEqual(self.review["members"][13]["filename"], "AGENTS.md")
self.assertEqual(self.review["selected_indexes"], [])
result = self.read([13])
self.assertIn(b"all members are approved", result["members"][0]["body"])
self.assertEqual(self.review["status"], "needs_review")
self.assertStoppedWithoutBodies(read_selected(self.data, None), "selection_required")
def test_archive_byte_budget(self):
result = manifest(self.data, Limits(archive_bytes=len(self.data) - 1))
self.assertEqual(result["reason"], "archive_byte_budget")
def test_clean_still_needs_review(self):
result = manifest(clean_fixture())
self.assertEqual(result["status"], "needs_review")
self.assertEqual(result["members"][0]["risk_reasons"], [])
self.assertEqual(result["selected_indexes"], [])
def test_declared_budget_stops_before_read(self):
with patch.object(zipfile.ZipFile, "open", side_effect=AssertionError("body opened")):
result = manifest(self.data, Limits(declared_bytes=10))
self.assertEqual(result["reason"], "declared_byte_budget")
def test_deflated_4096_with_limit_1000_stops(self):
entry = info("small-deflated.txt")
entry.compress_type = zipfile.ZIP_DEFLATED
data = pack([(entry, b"x" * 4096)])
result = read_selected(data, selection_record(manifest(data), [0]),
Limits(per_member_read=1000))
self.assertStoppedWithoutBodies(result, "stream_byte_budget")
def test_deflated_exact_5_with_limit_5_succeeds(self):
entry = info("small-deflated.txt")
entry.compress_type = zipfile.ZIP_DEFLATED
data = pack([(entry, b"12345")])
result = read_selected(data, selection_record(manifest(data), [0]),
Limits(per_member_read=5, total_read=5))
self.assertEqual(result["status"], "selected_read_complete")
self.assertEqual(result["members"][0]["body"], b"12345")
def test_directory_is_blocked(self):
data = pack([(info("folder/"), b"")])
result = read_selected(data, selection_record(manifest(data), [0]))
self.assertStoppedWithoutBodies(result, "selected_entry_blocked")
self.assertEqual(result["blockers"], ["directory_entry"])
def test_duplicate_string_last_but_info_preserves_entries(self):
with zipfile.ZipFile(io.BytesIO(self.data)) as archive:
self.assertEqual(archive.read("report.txt"), b"second")
infos = archive.infolist()
self.assertEqual([archive.read(i) for i in infos[:2]], [b"first", b"second"])
result = self.read([0, 1])
self.assertEqual([m["body"] for m in result["members"]], [b"first", b"second"])
def test_empty_archive_is_not_auto_approved(self):
self.assertEqual(manifest(pack([]))["status"], "needs_review")
def test_encrypted_marker_blocks_before_any_selected_open(self):
data = header_marker_fixture(encrypted=True)
review = manifest(data)
self.assertIn("encryption_marker", review["members"][0]["risk_reasons"])
with patch.object(zipfile.ZipFile, "open", side_effect=AssertionError("body opened")):
result = read_selected(data, selection_record(review, [0]))
self.assertStoppedWithoutBodies(result, "selected_entry_blocked")
def test_exact_5_with_limit_5_succeeds(self):
result = self.read([11], per_member_read=5, total_read=5)
self.assertEqual(result["status"], "selected_read_complete")
self.assertEqual(result["accepted_bytes"], 5)
self.assertEqual(result["members"][0]["body"], b"12345")
def test_fixture_is_deterministic(self):
self.assertEqual(self.data, main_fixture())
self.assertEqual(unicode_extra_fixture(), unicode_extra_fixture())
def test_full_expected_demo_output(self):
expected = Path(__file__).with_name("expected.json").read_text()
actual = json.dumps(report(), ensure_ascii=True, indent=2) + "\n"
self.assertEqual(actual, expected)
def test_hash_change_stops_before_parse_or_open(self):
record = selection_record(self.review, [0])
with patch("intake.zipfile.ZipFile", side_effect=AssertionError("parsed changed bytes")):
result = read_selected(self.data + b"x", record)
self.assertStoppedWithoutBodies(result, "input_hash_changed")
def test_invalid_budgets(self):
for value in (0, -1, True, "5"):
with self.subTest(value=value):
limits = Limits(total_read=value)
self.assertEqual(manifest(self.data, limits)["reason"], "invalid_budget")
self.assertEqual(read_selected(self.data, None, limits)["reason"], "invalid_budget")
def test_invalid_selection_indexes(self):
for indexes in ([], [-1], [14], [True], [0, 0], ["0"], "0", None):
with self.subTest(indexes=indexes):
self.assertEqual(selection_record(self.review, indexes)["reason"], "invalid_selection")
def test_manifest_never_opens_or_extracts_bodies(self):
with patch.object(zipfile.ZipFile, "open", side_effect=AssertionError("body opened")), \
patch.object(zipfile.ZipFile, "extract", side_effect=AssertionError("extracted")), \
patch.object(zipfile.ZipFile, "extractall", side_effect=AssertionError("extracted")):
self.assertEqual(manifest(self.data)["status"], "needs_review")
def test_manifest_records_directory_evidence(self):
row = self.review["members"][0]
self.assertEqual(row["index"], 0)
self.assertEqual(row["filename"], "report.txt")
self.assertEqual(row["orig_filename"], "report.txt")
for key in ("header_offset", "compression_method", "flag_bits", "create_system",
"external_attr", "internal_attr", "extra_hex", "extra_sha256"):
self.assertIn(key, row)
def test_member_4096_with_limit_1000_stops(self):
self.assertStoppedWithoutBodies(self.read([10], per_member_read=1000), "stream_byte_budget")
def test_member_count_budget(self):
self.assertEqual(manifest(self.data, Limits(members=13))["reason"], "member_count_budget")
def test_mutable_input_not_accepted(self):
self.assertEqual(manifest(bytearray(self.data))["reason"], "immutable_bytes_required")
def test_names_preserved_no_merging_or_extraction(self):
rows = self.review["members"]
self.assertEqual([row["filename"] for row in rows[4:6]], ["a/../b.txt", "a/b.txt"])
self.assertNotEqual(rows[6]["filename"], rows[7]["filename"])
for index in (6, 7, 8, 9):
self.assertIn("possible_unicode_or_case_alias", rows[index]["risk_reasons"])
def test_no_selection_never_opens(self):
with patch.object(zipfile.ZipFile, "open", side_effect=AssertionError("body opened")):
self.assertStoppedWithoutBodies(read_selected(self.data, None), "selection_required")
def test_parse_failure_stops(self):
self.assertEqual(manifest(b"not ZIP")["reason"], "parse_error")
data = b"not ZIP"
record = {"kind": "local_demo_selection", "archive_sha256": sha256(data),
"directory_indexes": [0]}
self.assertStoppedWithoutBodies(read_selected(data, record), "parse_error")
def test_read_budget_uses_one_byte_probe(self):
calls = []
real_read = zipfile.ZipExtFile.read
def observed(stream, n=-1):
calls.append(n)
return real_read(stream, n)
with patch.object(zipfile.ZipExtFile, "read", observed):
self.assertStoppedWithoutBodies(self.read([10], per_member_read=1000), "stream_byte_budget")
self.assertEqual(calls, [1001])
calls.clear()
with patch.object(zipfile.ZipExtFile, "read", observed):
self.assertEqual(self.read([11], per_member_read=5, total_read=5)["status"],
"selected_read_complete")
self.assertEqual(calls, [6, 1])
def test_read_crc_error_returns_no_bodies(self):
data = bad_crc_fixture()
result = read_selected(data, selection_record(manifest(data), [0]))
self.assertStoppedWithoutBodies(result, "member_read_error")
self.assertEqual(result["error_type"], "BadZipFile")
def test_read_opens_zipinfo_not_name(self):
opened = []
real_open = zipfile.ZipFile.open
def observed(archive, name, *args, **kwargs):
opened.append(name)
return real_open(archive, name, *args, **kwargs)
with patch.object(zipfile.ZipFile, "open", observed):
self.assertEqual(self.read([1, 0])["status"], "selected_read_complete")
self.assertTrue(all(isinstance(name, zipfile.ZipInfo) for name in opened))
self.assertEqual([name.header_offset for name in opened],
[self.review["members"][i]["header_offset"] for i in [1, 0]])
def test_selection_record_is_explicit_and_hash_bound(self):
record = selection_record(self.review, [1, 0])
self.assertEqual(record, {"kind": "local_demo_selection",
"archive_sha256": sha256(self.data), "directory_indexes": [1, 0]})
self.assertEqual(self.review["selected_indexes"], [])
def test_stopped_manifest_cannot_select(self):
self.assertEqual(selection_record(manifest(b"bad"), [0])["reason"], "manifest_not_reviewable")
def test_symlink_marker_blocks(self):
row = self.review["members"][12]
self.assertTrue(stat.S_ISLNK(row["external_attr"] >> 16))
self.assertStoppedWithoutBodies(self.read([12]), "selected_entry_blocked")
def test_total_11_with_limit_10_withholds_first_body_too(self):
self.assertStoppedWithoutBodies(self.read([0, 1], total_read=10), "stream_byte_budget")
def test_unicode_extra_even_without_utf8_flag(self):
data = unicode_extra_fixture()
review = manifest(data)
row = review["members"][0]
self.assertFalse(row["utf8_flag"])
self.assertEqual(row["filename"], "caf\u00e9.txt")
self.assertEqual(row["orig_filename"], "legacy.txt")
self.assertEqual(row["extra_hex"], "75700e000180077310636166c3a92e747874")
self.assertIn("decoded_name_changed", row["risk_reasons"])
self.assertEqual(read_selected(data, selection_record(review, [0]))["members"][0]["body"],
b"original fixture")
def test_unsupported_marker_blocks_before_open(self):
data = header_marker_fixture(unsupported=True)
review = manifest(data)
self.assertIn("unsupported_compression_method", review["members"][0]["risk_reasons"])
with patch.object(zipfile.ZipFile, "open", side_effect=AssertionError("body opened")):
result = read_selected(data, selection_record(review, [0]))
self.assertStoppedWithoutBodies(result, "selected_entry_blocked")
if __name__ == "__main__":
names = unittest.defaultTestLoader.getTestCaseNames(IntakeTests)
result = unittest.TestResult()
for name in names:
before = len(result.failures) + len(result.errors)
IntakeTests(name).run(result)
after = len(result.failures) + len(result.errors)
print(("PASS " if before == after else "FAIL ") + name)
for _, traceback in result.failures + result.errors:
print(traceback, file=sys.stderr)
print(f"tests: {result.testsRun} run, {len(result.failures)} failures, {len(result.errors)} errors")
raise SystemExit(0 if result.wasSuccessful() else 1)
12. generate_fixtures.py 完整文件
"""Write two original fixtures to a NEW directory; never overwrite files."""
import json
from pathlib import Path
import sys
from fixture import main_fixture, unicode_extra_fixture
from intake import sha256
def main():
if len(sys.argv) != 2:
raise SystemExit("usage: python generate_fixtures.py NEW_DIRECTORY")
destination = Path(sys.argv[1])
destination.mkdir() # Fails if the path already exists; no overwrite.
output = []
for name, data in [("demo.zip", main_fixture()),
("unicode-extra.zip", unicode_extra_fixture())]:
with (destination / name).open("xb") as stream:
stream.write(data)
output.append({"file": name, "bytes": len(data), "sha256": sha256(data)})
print(json.dumps(output, indent=2))
if __name__ == "__main__":
main()
13. reproduce_extract_behavior.py 完整文件
"""Optional CPython 3.12.14/Linux experiment, NOT the intake implementation.
Extract ONLY the original generated fixture into a fresh temporary directory.
Accepts no archive path. All extracted files are deleted on context exit.
"""
import io
import json
from pathlib import Path
import tempfile
import zipfile
from fixture import main_fixture
def report():
results = []
with zipfile.ZipFile(io.BytesIO(main_fixture())) as archive:
infos = archive.infolist()
with tempfile.TemporaryDirectory(prefix="zip-intake-original-") as temp:
root = Path(temp) / "output"
root.mkdir()
assert not list(root.iterdir())
for index in (2, 3, 4, 5, 12):
path = Path(archive.extract(infos[index], root))
assert path.is_relative_to(root)
results.append({"index": index,
"destination": path.relative_to(root).as_posix(),
"is_symlink": path.is_symlink(),
"body": path.read_bytes().decode("ascii")})
assert (root / "a/b.txt").read_bytes() == b"two"
assert not (root / "link-marker").is_symlink()
return {"generated_fixture_only": True, "extractions": results,
"same_destination_final_body": "two"}
if __name__ == "__main__":
print(json.dumps(report(), indent=2))
14. expected-extract-output.json 完整文件
{
"generated_fixture_only": true,
"extractions": [
{
"index": 2,
"destination": "parent-marker.txt",
"is_symlink": false,
"body": "parent"
},
{
"index": 3,
"destination": "absolute-marker.txt",
"is_symlink": false,
"body": "absolute"
},
{
"index": 4,
"destination": "a/b.txt",
"is_symlink": false,
"body": "one"
},
{
"index": 5,
"destination": "a/b.txt",
"is_symlink": false,
"body": "two"
},
{
"index": 12,
"destination": "link-marker",
"is_symlink": false,
"body": "short.txt"
}
],
"same_destination_final_body": "two"
}
15. expected-generator-output.json 完整文件
[
{
"file": "demo.zip",
"bytes": 5585,
"sha256": "5029ebe244235d9f01f66a10c10112d8f2ddcc764e866002741b6328156eb457"
},
{
"file": "unicode-extra.zip",
"bytes": 170,
"sha256": "178f10750a69f3536cfb5cb422d91ad29984301abab5a6c86cff59fc49c5fbee"
}
]
16. expected-manifest.json 完整文件
{
"status": "needs_review",
"archive_sha256": "5029ebe244235d9f01f66a10c10112d8f2ddcc764e866002741b6328156eb457",
"archive_bytes": 5585,
"declared_total": 4203,
"archive_comment_bytes": 0,
"archive_comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"selected_indexes": [],
"members": [
{
"index": 0,
"filename": "report.txt",
"orig_filename": "report.txt",
"header_offset": 0,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 5,
"declared_bytes": 5,
"crc32": "9271ee57",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"duplicate_filename"
]
},
{
"index": 1,
"filename": "report.txt",
"orig_filename": "report.txt",
"header_offset": 45,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 6,
"declared_bytes": 6,
"crc32": "b61f1169",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"duplicate_filename"
]
},
{
"index": 2,
"filename": "../parent-marker.txt",
"orig_filename": "../parent-marker.txt",
"header_offset": 91,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 6,
"declared_bytes": 6,
"crc32": "3d8e604f",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"nonportable_or_parent_path"
]
},
{
"index": 3,
"filename": "/absolute-marker.txt",
"orig_filename": "/absolute-marker.txt",
"header_offset": 147,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 8,
"declared_bytes": 8,
"crc32": "a6df7656",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"nonportable_or_parent_path"
]
},
{
"index": 4,
"filename": "a/../b.txt",
"orig_filename": "a/../b.txt",
"header_offset": 205,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 3,
"declared_bytes": 3,
"crc32": "7a6c86f1",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"nonportable_or_parent_path"
]
},
{
"index": 5,
"filename": "a/b.txt",
"orig_filename": "a/b.txt",
"header_offset": 248,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 3,
"declared_bytes": 3,
"crc32": "11ca8a66",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": []
},
{
"index": 6,
"filename": "caf\u00e9.txt",
"orig_filename": "caf\u00e9.txt",
"header_offset": 288,
"compression_method": 0,
"flag_bits": 2048,
"utf8_flag": true,
"compressed_bytes": 3,
"declared_bytes": 3,
"crc32": "5a14c3fb",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"possible_unicode_or_case_alias"
]
},
{
"index": 7,
"filename": "cafe\u0301.txt",
"orig_filename": "cafe\u0301.txt",
"header_offset": 330,
"compression_method": 0,
"flag_bits": 2048,
"utf8_flag": true,
"compressed_bytes": 3,
"declared_bytes": 3,
"crc32": "c4705658",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"possible_unicode_or_case_alias"
]
},
{
"index": 8,
"filename": "Case.txt",
"orig_filename": "Case.txt",
"header_offset": 373,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 5,
"declared_bytes": 5,
"crc32": "6e5fdf9c",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"possible_unicode_or_case_alias"
]
},
{
"index": 9,
"filename": "case.txt",
"orig_filename": "case.txt",
"header_offset": 416,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 5,
"declared_bytes": 5,
"crc32": "0e9a7b23",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"possible_unicode_or_case_alias"
]
},
{
"index": 10,
"filename": "big.txt",
"orig_filename": "big.txt",
"header_offset": 459,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 4096,
"declared_bytes": 4096,
"crc32": "3e1077c1",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": []
},
{
"index": 11,
"filename": "short.txt",
"orig_filename": "short.txt",
"header_offset": 4592,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 5,
"declared_bytes": 5,
"crc32": "cbf53a1c",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": []
},
{
"index": 12,
"filename": "link-marker",
"orig_filename": "link-marker",
"header_offset": 4636,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 9,
"declared_bytes": 9,
"crc32": "b8f4d823",
"create_system": 3,
"external_attr": 2717843456,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": [
"unix_symlink_marker"
]
},
{
"index": 13,
"filename": "AGENTS.md",
"orig_filename": "AGENTS.md",
"header_offset": 4686,
"compression_method": 0,
"flag_bits": 0,
"utf8_flag": false,
"compressed_bytes": 46,
"declared_bytes": 46,
"crc32": "023987aa",
"create_system": 3,
"external_attr": 2175008768,
"internal_attr": 0,
"extra_hex": "",
"extra_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"comment_bytes": 0,
"comment_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"risk_reasons": []
}
]
}
17. expected-selection.json 完整文件
{
"selection_record": {
"kind": "local_demo_selection",
"archive_sha256": "5029ebe244235d9f01f66a10c10112d8f2ddcc764e866002741b6328156eb457",
"directory_indexes": [
0,
1
]
},
"read_result": {
"status": "selected_read_complete",
"accepted_bytes": 11,
"members": [
{
"index": 0,
"bytes": 5,
"sha256": "a7937b64b8caa58f03721bb6bacf5c78cb235febe0e70b1b84cd99541461a08e"
},
{
"index": 1,
"bytes": 6,
"sha256": "16367aacb67a4a017c8da8ab95682ccb390863780f7114dda0a0e0c55644c7c4"
}
]
}
}
18. expected-test-output.txt 完整文件
PASS test_agents_text_is_data_not_authority
PASS test_archive_byte_budget
PASS test_clean_still_needs_review
PASS test_declared_budget_stops_before_read
PASS test_deflated_4096_with_limit_1000_stops
PASS test_deflated_exact_5_with_limit_5_succeeds
PASS test_directory_is_blocked
PASS test_duplicate_string_last_but_info_preserves_entries
PASS test_empty_archive_is_not_auto_approved
PASS test_encrypted_marker_blocks_before_any_selected_open
PASS test_exact_5_with_limit_5_succeeds
PASS test_fixture_is_deterministic
PASS test_full_expected_demo_output
PASS test_hash_change_stops_before_parse_or_open
PASS test_invalid_budgets
PASS test_invalid_selection_indexes
PASS test_manifest_never_opens_or_extracts_bodies
PASS test_manifest_records_directory_evidence
PASS test_member_4096_with_limit_1000_stops
PASS test_member_count_budget
PASS test_mutable_input_not_accepted
PASS test_names_preserved_no_merging_or_extraction
PASS test_no_selection_never_opens
PASS test_parse_failure_stops
PASS test_read_budget_uses_one_byte_probe
PASS test_read_crc_error_returns_no_bodies
PASS test_read_opens_zipinfo_not_name
PASS test_selection_record_is_explicit_and_hash_bound
PASS test_stopped_manifest_cannot_select
PASS test_symlink_marker_blocks
PASS test_total_11_with_limit_10_withholds_first_body_too
PASS test_unicode_extra_even_without_utf8_flag
PASS test_unsupported_marker_blocks_before_open
tests: 33 run, 0 failures, 0 errors
19. expected.json 完整文件
{
"fixture_manifest": {
"status": "needs_review",
"archive_sha256": "5029ebe244235d9f01f66a10c10112d8f2ddcc764e866002741b6328156eb457",
"archive_bytes": 5585,
"declared_total": 4203,
"member_count": 14,
"selected_indexes": [],
"risk_indexes": {
"duplicate_filename": [
0,
1
],
"nonportable_or_parent_path": [
2,
3,
4
],
"possible_unicode_or_case_alias": [
6,
7,
8,
9
],
"unix_symlink_marker": [
12
]
}
},
"duplicate_name_experiment": {
"by_string": "second",
"by_zipinfo": [
"first",
"second"
]
},
"zipfile_Path_is_symlink_python_3_12_14": false,
"clean_manifest": {
"status": "needs_review",
"risk_reasons": [],
"selected_indexes": []
},
"unicode_extra_member": {
"filename": "caf\u00e9.txt",
"orig_filename": "legacy.txt",
"utf8_flag": false,
"extra_hex": "75700e000180077310636166c3a92e747874",
"risk_reasons": [
"decoded_name_changed"
]
},
"no_selection": {
"status": "stop",
"reason": "selection_required"
},
"selected_duplicates": {
"status": "selected_read_complete",
"accepted_bytes": 11,
"members": [
{
"index": 0,
"bytes": 5,
"sha256": "a7937b64b8caa58f03721bb6bacf5c78cb235febe0e70b1b84cd99541461a08e"
},
{
"index": 1,
"bytes": 6,
"sha256": "16367aacb67a4a017c8da8ab95682ccb390863780f7114dda0a0e0c55644c7c4"
}
]
},
"member_4096_limit_1000": {
"status": "stop",
"reason": "stream_byte_budget",
"failed_index": 10
},
"combined_11_limit_10": {
"status": "stop",
"reason": "stream_byte_budget",
"failed_index": 1
},
"exact_5_limit_5": {
"status": "selected_read_complete",
"accepted_bytes": 5,
"members": [
{
"index": 11,
"bytes": 5,
"sha256": "5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"
}
]
},
"input_changed": {
"status": "stop",
"reason": "input_hash_changed"
},
"parse_failure": {
"status": "stop",
"reason": "parse_error",
"error_type": "BadZipFile",
"archive_sha256": "def632dce02e8c703fc96a38d11fc810a5c1f78083aebdf9d13a6bd893a87a75",
"archive_bytes": 9
},
"synthetic_header_markers": {
"encrypted_marker": {
"manifest_status": "needs_review",
"risk_reasons": [
"encryption_marker"
],
"selected_read": {
"status": "stop",
"reason": "selected_entry_blocked",
"failed_index": 0,
"blockers": [
"encryption_marker"
]
}
},
"unsupported_method_marker": {
"manifest_status": "needs_review",
"risk_reasons": [
"unsupported_compression_method"
],
"selected_read": {
"status": "stop",
"reason": "selected_entry_blocked",
"failed_index": 0,
"blockers": [
"unsupported_compression_method"
]
}
}
},
"agents_text_has_no_authority": {
"index": 13,
"manifest_status": "needs_review",
"selected_indexes": [],
"automatic_action": "none"
}
}
20. 接到 Agent 前,还要保留哪些边界
成员清单的价值在于让模型接触材料之前,先把数据范围说清楚。用户只要两份报告,包里的其他文件不应因为位于同一个 ZIP 就自动进入上下文。选材结果应继续带上归档哈希、成员索引、显示名称和内容摘要;这样回答中的引文或问题可以回查到具体输入,而不是回到一个含混的文件名。
完成选材不代表正文可信。一段被选中的文字仍可包含要求忽略先前指令、打开外链或执行命令的内容。本文没有测试模型对这些内容的行为,也不提供提示注入防护保证。真实应用仍应把资料放在数据位置,限制工具能力,独立判断有副作用的动作是否获得用户授权。
本例没有实现密码解密、嵌套压缩包递归、外链追踪或 bomb 检测,也没有处理所有恶意 ZIP 的解析组合。没有验证跨平台文件系统映射、ZIP64 极端输入、多磁盘输入、CPU/内存限制或进程沙箱。遇到超出支持范围的包,应保留原件并停止这条处理路径,转交适当工具或人工流程。不要把增加几个风险字符串当成通用安全解码器。
如果以后把它做成上传服务,第一步是建立与解码进程分开的接收限制和资源隔离,再把人工选择与真实身份、对象访问权限、任务生命周期绑定。重试应读取同一不可变版本;输入变化则重新审查。选材界面还要防止自动勾选和名称混淆,显示索引与风险,不能只展示一个容易重复的文件名。
21. 验收时检查行为,而不只看成功提示
本次 33 项离线测试全部通过;六条示例命令在全新临时目录中运行,与随文固定输出一致。测试另外覆盖一个短 DEFLATED 成员恰好完成,以及 4096 字节 DEFLATED 成员超过 1000 字节预算的停止行为。用于保持归档哈希稳定的主样本采用 STORED 方法,这两类样本的用途分别记录。
先看无选择时是否停在 needs_review;再分别选择两个同名成员,确认正文各自对应正确的索引。修改输入后重放旧选择,应得到 stop。把单成员预算降到 1000,4096 字节样本必须被拒绝;把总预算设成 10,合计 11 字节的两份报告也必须被拒绝。5 字节样本在两种预算都为 5 时应完整成功。
还要检查失败输出不带可消费的部分正文、风险名称仍原样可辨,以及生产读取模块没有 extract 调用。临时解压反例只是固定小样本的行为观察,应和读取入口分开。测试通过说明这些小样本与约定相符,不说明已经覆盖敌对输入。
到这里,Agent 的附件入口有了一个可检查的顺序:清单解释输入,人决定阅读范围,程序核对同一快照并限制交付字节。这个顺序不会消除所有 ZIP 或模型风险,但能避免在用户还没选材之前,就已经解压、覆盖或把整个包交给模型。
22. 读出一个字节并正常关闭,说明了什么
如果把前文读取器改成“先读一个字节预览,再关掉文件”,能顺便认定这个成员完整吗?下面不改读取器,只给它补一份回归样本:成员 0 是五字节的 first.txt,成员 1 是 5,000 个 A 组成的 report.txt。两项都用 ZIP_STORED 写入,再把第二项的最后一个正文字节改成 Z,头部大小与原 CRC 保持不变。
在 CPython 3.12.14 中,坏成员的 read(1) 返回 b"A",退出两个 with 都正常;另开流完整读取同一成员,则抛出 BadZipFile。健康对照能完整返回 5,000 字节。这里的正常关闭只说明关闭操作完成,不能把前缀观察标成“完整性已验证”。
还要做一个相反方向的检查。将第二项缩成五字节的 AAAAZ,同样保留原 CRC,此时 read(1) 已经抛出 BadZipFile,一个字节也没有返回。请求长度不是库内部消费长度:这个版本会预读,短成员可能在第一次调用里就走到内部 EOF。所以也不能写成“部分读取从来不检查 CRC”。
ZipFile.open 文档说明它返回二进制可读对象;具体时机要对照 CPython v3.12.14 源码:_update_crc 在内部 EOF 时比较 CRC,_read2 使用最小读取块,close 不负责排空未读数据。这是该版本对固定样本的解释,不能推广成所有 ZIP 库的接口保证。2026-10-13 核对的在线 3.12 文档显示 3.12.15,实验版本仍为 3.12.14。
23. 第二份失败,第一份能不能先交付
前文约定的是整批选材成功后才交付。坏包的目录仍能列出两项,状态为 needs_review,声明总量是 5,005 字节。它和健康包的成员元数据相同,归档 SHA-256 却不同。实验用坏包自己的清单建立选择,避免把快照变化导致的拒收误称为 CRC 失败。
只选 [0] 时,原读取器只打开 first.txt,返回五字节。选 [0, 1] 时,第一份已在内存读完,第二份抛出 CRC 错误,最终结果只有 stop / member_read_error / failed_index: 1 / BadZipFile,没有 members,也没有已接纳字节数。选择范围内完成读取,不代表没有选中的成员也已检查。
对照函数 publish_early_negative_control 故意一份读完就加入模拟下游列表。第二份失败时,列表已保留索引 0。即使最后再发一条错误,也无法改变第一份已经交付的事实。这个列表完全在本机内存中,没有向模型或其他服务发送正文;它只演示为什么边读边交付不满足本页的整批约定。
5,000 字节恰好等于原默认单成员预算,5,005 也低于总预算 8,192,无需放宽限制。另把单成员预算降到 1,000,结果是 stream_byte_budget,没有 CRC 通过的含义;独立完整重读仍能发现损坏。预算停止和完整读取应保留不同状态。
24. 把三种读取结果放进同一份实验
保存下面两个文件,与前文的 fixture.py、intake.py 放在同一空目录。两份原文件无需修改。夹具生成器只接受 5 和 5,000 两个教学尺寸,复用固定时间与属性,并检查要改的位置确实属于自己生成的 STORED 正文。它不是任意 ZIP 的字节定位器。
prefix_probe 只报告观察到的字节和关闭结果,故意不输出“校验通过”。full_probe 与早交付负控直接读完整成员,仅供这两份小夹具使用,不是接收真实上传的生产入口。
保存为 crc_boundary.py:
"""Generated STORED fixtures only. No paths, extraction, network or real attachments."""
import io
import json
from pathlib import Path
import struct
import zipfile
from fixture import info, pack
from intake import Limits, manifest, read_selected, selection_record
def make_fixture(size=5000, *, corrupt=True):
"""Keep both headers/CRC unchanged; flip only the last body byte of member 1."""
if size not in (5, 5000):
raise ValueError("Only the two fixed teaching sizes are supported")
original = pack([(info("first.txt"), b"first"),
(info("report.txt"), b"A" * size)])
with zipfile.ZipFile(io.BytesIO(original)) as archive:
member = archive.infolist()[1]
assert member.compress_type == zipfile.ZIP_STORED
offset = member.header_offset
# This deliberately narrow locator is only for our own ordinary local header.
assert original[offset:offset + 4] == b"PK\x03\x04"
name_bytes, extra_bytes = struct.unpack_from("<HH", original, offset + 26)
body_start = offset + 30 + name_bytes + extra_bytes
assert original[body_start:body_start + size] == b"A" * size
if not corrupt:
return original
data = bytearray(original)
data[body_start + size - 1] = ord("Z")
return bytes(data)
def prefix_probe(data):
returned = b""
try:
with zipfile.ZipFile(io.BytesIO(data)) as archive:
with archive.open(archive.infolist()[1]) as stream:
returned = stream.read(1)
# This is an observation, deliberately NOT an integrity status.
return {"returned_hex": returned.hex(), "closed_normally": True}
except zipfile.BadZipFile as error:
return {"error_type": type(error).__name__, "returned_bytes": len(returned)}
def full_probe(data):
try:
with zipfile.ZipFile(io.BytesIO(data)) as archive:
return {"bytes": len(archive.read(archive.infolist()[1]))}
except zipfile.BadZipFile as error:
return {"error_type": type(error).__name__}
def summarize(result):
if result["status"] != "selected_read_complete":
return result
return {"status": result["status"], "accepted_bytes": result["accepted_bytes"],
"indexes": [row["index"] for row in result["members"]],
"sizes": [row["bytes"] for row in result["members"]]}
def publish_early_negative_control(data):
"""Intentionally wrong: imitate sending each completed body to a consumer."""
published = []
try:
with zipfile.ZipFile(io.BytesIO(data)) as archive:
for index, member in enumerate(archive.infolist()):
body = archive.read(member)
published.append((index, body))
except zipfile.BadZipFile as error:
return {"published_indexes": [index for index, _ in published],
"error_type": type(error).__name__}
return {"published_indexes": [index for index, _ in published], "error_type": None}
def demo():
data = make_fixture()
review = manifest(data)
selection = selection_record(review, [0, 1])
return {
"metadata": {"status": review["status"], "members": len(review["members"]),
"declared_total": review["declared_total"]},
"clean_large_full_bytes": full_probe(make_fixture(corrupt=False))["bytes"],
"corrupt_large_prefix": prefix_probe(data),
"corrupt_small_prefix": prefix_probe(make_fixture(5)),
"corrupt_large_full": full_probe(data),
"selected_first_only": summarize(read_selected(data, selection_record(review, [0]))),
"selected_both": summarize(read_selected(data, selection)),
"low_budget": summarize(read_selected(data, selection, Limits(per_member_read=1000))),
"publish_early_negative_control": publish_early_negative_control(data),
}
if __name__ == "__main__":
actual = demo()
expected = json.loads(Path(__file__).with_name("expected-crc.json").read_text())
if actual != expected:
raise AssertionError({"expected": expected, "actual": actual})
print(json.dumps(actual, indent=2))
保存为 expected-crc.json:
{
"metadata": {"status": "needs_review", "members": 2, "declared_total": 5005},
"clean_large_full_bytes": 5000,
"corrupt_large_prefix": {"returned_hex": "41", "closed_normally": true},
"corrupt_small_prefix": {"error_type": "BadZipFile", "returned_bytes": 0},
"corrupt_large_full": {"error_type": "BadZipFile"},
"selected_first_only": {"status": "selected_read_complete", "accepted_bytes": 5, "indexes": [0], "sizes": [5]},
"selected_both": {"status": "stop", "reason": "member_read_error", "failed_index": 1, "error_type": "BadZipFile"},
"low_budget": {"status": "stop", "reason": "stream_byte_budget", "failed_index": 1},
"publish_early_negative_control": {"published_indexes": [0], "error_type": "BadZipFile"}
}
运行 python crc_boundary.py,stdout 应是上面的完整 JSON。程序先和事先写定的 expected-crc.json 比较,再打印结果;它不会根据当前行为重写预期。只有前缀返回值正常的断言不够,健康对照、短成员负控和完整读取失败都要一起保留。
25. 用十二项测试和四次故障注入守住边界
新增测试检查单字节损坏、确定性生成、元数据与哈希的区别、预读反例、健康对照、选择范围、失败不交付、预算停止和快照不匹配。test_unselected_bad_member_not_opened 还记录实际打开的成员,避免仅凭结果猜测没有多读。
保存为 test_crc_boundary.py:
import io
import json
import struct
from pathlib import Path
import unittest
from unittest.mock import patch
import zipfile
from crc_boundary import (demo, full_probe, make_fixture, prefix_probe,
publish_early_negative_control)
from intake import Limits, manifest, read_selected, selection_record
class CrcBoundaryTests(unittest.TestCase):
def setUp(self):
self.bad = make_fixture()
self.good = make_fixture(corrupt=False)
self.review = manifest(self.bad)
self.selection = selection_record(self.review, [0, 1])
def test_independently_written_golden(self):
expected = json.loads(Path(__file__).with_name("expected-crc.json").read_text())
self.assertEqual(demo(), expected)
def test_single_byte_corruption_and_deterministic_generation(self):
self.assertEqual(self.bad, make_fixture())
self.assertEqual(len(self.good), len(self.bad))
changes = [i for i, pair in enumerate(zip(self.good, self.bad)) if pair[0] != pair[1]]
self.assertEqual(len(changes), 1)
self.assertEqual((self.good[changes[0]], self.bad[changes[0]]), (65, 90))
self.assertLess(len(self.bad), 65536)
def test_metadata_same_despite_body_change(self):
clean = manifest(self.good)
self.assertEqual(clean["members"], self.review["members"])
self.assertNotEqual(clean["archive_sha256"], self.review["archive_sha256"])
self.assertEqual(self.review["status"], "needs_review")
self.assertEqual(self.review["declared_total"], 5005)
def test_prefix_close_is_not_complete_integrity_evidence(self):
self.assertEqual(prefix_probe(self.bad), {"returned_hex": "41", "closed_normally": True})
self.assertEqual(full_probe(self.bad), {"error_type": "BadZipFile"})
def test_short_member_is_counterexample_to_never_checks_crc_claim(self):
self.assertEqual(prefix_probe(make_fixture(5)),
{"error_type": "BadZipFile", "returned_bytes": 0})
def test_clean_full_control(self):
self.assertEqual(full_probe(self.good), {"bytes": 5000})
result = read_selected(self.good, selection_record(manifest(self.good), [0, 1]))
self.assertEqual(result["status"], "selected_read_complete")
self.assertEqual([row["body"] for row in result["members"]], [b"first", b"A" * 5000])
self.assertEqual(result["accepted_bytes"], 5005)
def test_unselected_bad_member_not_opened(self):
opened = []
original = zipfile.ZipFile.open
def observe(archive, member, *args, **kwargs):
opened.append(member.filename)
return original(archive, member, *args, **kwargs)
with patch.object(zipfile.ZipFile, "open", observe):
result = read_selected(self.bad, selection_record(self.review, [0]))
self.assertEqual(opened, ["first.txt"])
self.assertEqual(result["status"], "selected_read_complete")
self.assertEqual(result["members"][0]["body"], b"first")
def test_crc_failure_withholds_first_completed_body(self):
result = read_selected(self.bad, self.selection)
self.assertEqual(result, {"status": "stop", "reason": "member_read_error",
"failed_index": 1, "error_type": "BadZipFile"})
self.assertNotIn("members", result)
self.assertNotIn("accepted_bytes", result)
def test_budget_stop_is_not_complete_crc_verification(self):
result = read_selected(self.bad, self.selection, Limits(per_member_read=1000))
self.assertEqual(result, {"status": "stop", "reason": "stream_byte_budget", "failed_index": 1})
self.assertEqual(full_probe(self.bad), {"error_type": "BadZipFile"})
def test_early_publish_negative_control_violates_atomic_contract(self):
bad = publish_early_negative_control(self.bad)
self.assertEqual(bad, {"published_indexes": [0], "error_type": "BadZipFile"})
# Failure must leave no deliverable bodies. The deliberately wrong reader fails it.
self.assertNotEqual(bad["published_indexes"], [])
self.assertNotIn("members", read_selected(self.bad, self.selection))
def test_crc_valid_but_size_mismatch_withholds_all_bodies(self):
# Only our generated two-member, ordinary STORED fixture is patched.
data = bytearray(make_fixture(5, corrupt=False))
with zipfile.ZipFile(io.BytesIO(data)) as archive:
local = archive.infolist()[1].header_offset
central = archive.start_dir
# Advance past first central entry using its three variable lengths.
lengths = struct.unpack_from("<HHH", data, central + 28)
central += 46 + sum(lengths)
self.assertEqual(data[central:central + 4], b"PK\x01\x02")
struct.pack_into("<I", data, local + 22, 6)
struct.pack_into("<I", data, central + 24, 6)
data = bytes(data)
# The five-byte body and its original CRC are unchanged.
with zipfile.ZipFile(io.BytesIO(data)) as archive:
self.assertEqual(archive.read(archive.infolist()[1]), b"AAAAA")
review = manifest(data)
self.assertEqual(review["members"][1]["declared_bytes"], 6)
result = read_selected(data, selection_record(review, [0, 1]))
self.assertEqual(result, {"status": "stop", "reason": "member_size_mismatch",
"failed_index": 1})
self.assertNotIn("members", result)
def test_wrong_archive_selection_is_separate_failure(self):
clean_selection = selection_record(manifest(self.good), [0, 1])
result = read_selected(self.bad, clean_selection)
self.assertEqual(result, {"status": "stop", "reason": "input_hash_changed"})
if __name__ == "__main__":
unittest.main(verbosity=2)
python test_crc_boundary.py
python -m unittest -v第一条应通过 12 项补充测试;仅放入截至本节的源文件与固定预期时,第二条应通过 45 项,其中原有 33 项。若也保存后文的选择快照测试,完整发现结果为 51 项。测试数量只描述这份样本,不表示覆盖率。
再故意改坏读取器四次:首块返回后立即结束、错误结果夹带已经完成的正文、把选中索引换成全部成员,以及把预算停止误报为完成。下面脚本每次在新的临时目录里复制文件,只修改一次目标代码,并要求对应测试出现断言失败。语法错误、导入错误或未命中目标都不算成功。正确源码不会被改写。
保存为 run_mutation_checks.py:
"""Run targeted assertions against four temporary, deliberately broken readers."""
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
BASE = Path(__file__).resolve().parent
CASES = [
("stop_after_first_chunk", "test_crc_failure_withholds_first_completed_body",
" used += len(chunk)\n",
" used += len(chunk)\n break\n"),
("return_completed_on_error", "test_crc_failure_withholds_first_completed_body",
' return stop("member_read_error", failed_index=index,\n'
' error_type=type(error).__name__)',
' return stop("member_read_error", failed_index=index,\n'
' error_type=type(error).__name__, members=completed)'),
("replace_selection_with_all", "test_unselected_bad_member_not_opened",
' for index in indexes:\n',
' for index in range(len(infos)):\n'),
("budget_stop_as_complete", "test_budget_stop_is_not_complete_crc_verification",
' return stop("stream_byte_budget", failed_index=index)',
' return {"status": "selected_read_complete",\n'
' "accepted_bytes": used, "members": completed}'),
]
def main():
original = (BASE / "intake.py").read_text()
results = []
for name, method, old, new in CASES:
if original.count(old) != 1:
raise AssertionError(f"Mutation target must occur exactly once: {name}")
with tempfile.TemporaryDirectory(prefix="zip-crc-mutant-") as temp:
target = Path(temp)
for filename in ("fixture.py", "intake.py", "crc_boundary.py",
"test_crc_boundary.py", "expected-crc.json"):
shutil.copyfile(BASE / filename, target / filename)
(target / "intake.py").write_text(original.replace(old, new, 1))
proc = subprocess.run(
[sys.executable, "-m", "unittest", "-v",
"test_crc_boundary.CrcBoundaryTests." + method],
cwd=target, text=True, capture_output=True, timeout=30,
)
sys.stderr.write(f"--- {name} ---\n" + proc.stdout + proc.stderr)
caught = (proc.returncode == 1
and f"FAIL: {method}" in proc.stderr
and "AssertionError" in proc.stderr
and "FAILED (failures=1)" in proc.stderr
and "ERROR:" not in proc.stderr)
if not caught:
raise AssertionError(f"Expected the targeted assertion to fail: {name}")
results.append({"mutation": name, "assertion_failed": True})
print(json.dumps(results, indent=2))
if __name__ == "__main__":
main()
python run_mutation_checks.py脚本正常退出,stdout 应依次列出四个 mutation,每项 assertion_failed 都为 true。stderr 中的四次 FAILED 是故障注入预期;如果错误实现反而通过测试,脚本会失败。
本补充于 2026-10-13 在 Linux、CPython 3.12.14 上运行:原 33 项与新增 11 项测试通过,四次故障注入均命中对应断言。新增实验只处理自造 STORED 小包,没有真实附件、网络或模型调用,也没有测试其他压缩方法、其他 Python 版本、同步改写 CRC 的对抗性输入或生产服务。CRC 在这里只检测损坏,不能认证发送者。原有字节预算也不能据此升级为 CPU、内存或解析器资源保证。
2026-10-14 复查增加一项长度不一致回归:两个头都将第二份五字节 STORED 成员声明为六字节,正文与 CRC 不变。底层完整读取仍返回五字节,修正后的读取器必须以 member_size_mismatch 停止,且不返回第一份已完成的正文。原 33 项与补充 12 项共 45 项通过,原四次故障注入仍命中。此样本只验证已生成小包的长度核对,不是通用 ZIP 格式验证器。
26. 选择已校验,为什么仍会多读或少读
2026-10-20 复查发现,固定归档 bytes 还不够:选择记录里的 directory_indexes 是调用者持有的列表。旧版 read_selected 已调用 selection_record 验证并生成独立副本,却仍用原列表做后续风险预检和成员循环。在首个成员打开时,若调用方的集成回调给原列表追加另一项,循环会继续读新增成员;清空列表则会少读,却仍返回完成。改写第二项或反转列表,也能改变实际读取的成员与顺序。
这份复现不需要线程或等待:用 unittest.mock 在第一次 ZipFile.open 的边界修改调用方列表,记录每个真正打开的成员。原代码的五种变更用例均出现断言失败,正常调用对照通过。Python 序列文档说明,可变序列的迭代器继续按索引前进,不会自动冻结其内容;tuple 是不可变序列。在线文档显示 3.12.15,本次运行环境仍是 CPython 3.12.14。
前文 intake.py 已改为在验证成功后执行 indexes = tuple(validated["directory_indexes"]),让风险预检和读取循环共同使用校验函数生成的私有副本。不是给调用方列表换个变量名,也不是读取结束后才核对。原列表后来追加、清空、替换或反转,都不再改变这次已经校验的选材范围。
保存下面完整文件为 test_selection_snapshot.py,与前文源码放在一起。五项变更用例分别检查追加普通成员、清空列表、替换后续成员、反转顺序,以及晚加带符号链接属性的成员;第六项保留正常调用、原记录不变和请求顺序的对照。测试只读取自造小包,不解压、不创建链接,也不接触真实附件。
"""Deterministic caller-list mutations, not a thread-safety or authority test."""
import copy
import stat
import unittest
from unittest.mock import patch
import zipfile
from fixture import info, pack
from intake import manifest, read_selected, selection_record
class SelectionSnapshotTests(unittest.TestCase):
def setUp(self):
link = info("link-marker")
link.external_attr = (stat.S_IFLNK | 0o777) << 16
self.names = ["first.txt", "second.txt", "other.txt", "link-marker"]
self.bodies = [b"first", b"second", b"other", b"first.txt"]
self.data = pack([
(info(self.names[0]), self.bodies[0]),
(info(self.names[1]), self.bodies[1]),
(info(self.names[2]), self.bodies[2]),
(link, self.bodies[3]),
])
self.review = manifest(self.data)
def check_boundary(self, selected, mutate, expected_caller_indexes):
record = selection_record(self.review, selected)
caller_indexes = record["directory_indexes"]
opened = []
real_open = zipfile.ZipFile.open
def observe(archive, member, *args, **kwargs):
opened.append(member.filename)
if len(opened) == 1:
# Simulate a caller/integration callback at a precise boundary.
# No sleep, thread, external archive, or network is involved.
mutate(caller_indexes)
return real_open(archive, member, *args, **kwargs)
with patch.object(zipfile.ZipFile, "open", observe):
result = read_selected(self.data, record)
self.assertEqual(caller_indexes, expected_caller_indexes)
self.assertEqual(opened, [self.names[i] for i in selected])
self.assertEqual(result["status"], "selected_read_complete")
self.assertEqual([row["index"] for row in result["members"]], selected)
self.assertEqual([row["body"] for row in result["members"]],
[self.bodies[i] for i in selected])
self.assertEqual(result["accepted_bytes"],
sum(len(self.bodies[i]) for i in selected))
def test_append_does_not_expand_read_scope(self):
self.check_boundary([0], lambda indexes: indexes.append(2), [0, 2])
def test_clear_does_not_skip_selected_member(self):
self.check_boundary([0, 1], lambda indexes: indexes.clear(), [])
def test_replace_does_not_substitute_member(self):
self.check_boundary([0, 1], lambda indexes: indexes.__setitem__(1, 2), [0, 2])
def test_reverse_does_not_repeat_or_reorder_members(self):
self.check_boundary([1, 0], lambda indexes: indexes.reverse(), [0, 1])
def test_late_symlink_marker_never_opens(self):
self.assertIn("unix_symlink_marker", self.review["members"][3]["risk_reasons"])
self.check_boundary([0], lambda indexes: indexes.append(3), [0, 3])
def test_ordinary_call_preserves_record_and_requested_order(self):
record = selection_record(self.review, [1, 0])
before = copy.deepcopy(record)
result = read_selected(self.data, record)
self.assertEqual(record, before)
self.assertEqual(result["status"], "selected_read_complete")
self.assertEqual([row["index"] for row in result["members"]], [1, 0])
self.assertEqual([row["body"] for row in result["members"]],
[b"second", b"first"])
self.assertEqual(result["accepted_bytes"], 11)
if __name__ == "__main__":
unittest.main(verbosity=2)
python test_selection_snapshot.py
python -m unittest -v
python run_mutation_checks.py
第一条通过 6 项;把全页文件保存齐全后,第二条通过 51 项,即原 33 项、CRC/长度检查 12 项和本节 6 项。第三条的原四次故障注入仍命中目标断言。在临时副本中仅删除新增的 tuple 赋值,再运行本节测试,应得到 5 次断言失败、0 次错误,正常调用对照仍通过。不要在保留使用的正确源码中删除它。
本次还在新临时目录中重跑前文六条命令,stdout 与六份原固定输出逐字节相同;CRC 演示与 expected-crc.json 做完整 JSON 对象相等比较,不把排版差异称为字节相同。归档夹具和原输出没有改写。
这个修正只固定验证完成后的本次读取计划。它不使可编辑的本地记录变成授权凭证,也不证明记录获取过程线程安全。调用方仍应提交稳定的记录;并发提交、权限撤回和取消需要独立的同步与停止机制,不能靠改共享列表通知读取器。没有测试真实并发、其他 Python 版本、生产上传、模型行为或通用恶意 ZIP 防护。
清单没有风险标记,可以自动交给模型吗?
不可以。本例仍返回 needs_review,必须明确选择;选择后的正文仍是不可信数据。
archive_sha256 加成员索引是真实权限认证吗?
不是。它绑定输入版本和成员身份;真实产品仍需受保护的认证、权限与任务范围。
预算能阻止解压器消耗过多 CPU 或内存吗?
不能。本例限制接纳字节与声明预算,不提供解析器或解压器资源沙箱。