zizi cf0f4fb985 W20–W24:保存知识方法、迁移框架、审校修订与案例行为的集成成果
接续 88dd570,保存 W20–W24 已实现的共享接口、业务入口、迁移、工作台、测试与文档。
W20/W22/W23 保持 in_progress,W21/W24 保持 verified;此提交不宣称方法或规则正式启用、多轮返修、真实角色评测完成。

W25 新增实验、标定、逐调用交付与角色执行及其迁移/测试/索引留在实施工作树,原有私人和旧实现保留项不纳入。

验证:离线 571、前端 33 通过;PG 469 项通过、2 项浏览器未启用,2 项误带入的 W25 用例已移出本提交;最终任务与交付边界 37 项通过。make 检查、最终类型、84 项资源及 diff 检查通过。未重跑浏览器或 Pi 宿主,不以合成调用认证外部模型效果。
独立整体审查四维通过;证据保存在 R2-20260909/提交W20-W24。
2026-09-13 16:24:42 +08:00

271 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""显式文件清单的字节保全与活副本核对;不导入业务或推断作品用途。"""
from __future__ import annotations
import base64
import hashlib
import os
import re
import secrets
import stat
from contextlib import contextmanager
from pathlib import Path
from 建立映射 import 稳定JSON, 读取JSON, 数据集命名空间, 旧快照, 迁移错误
单文件上限 = 8 * 1024 * 1024
快照上限 = 16 * 1024 * 1024
文件数上限 = 500
def 校验相对路径(值) -> tuple[str, ...]:
if not isinstance(值, str) or not 值 or "\\" in 值 or "\x00" in 值:
raise 迁移错误("file_path_denied", "文件路径必须是明确的相对POSIX路径")
段 = tuple(值.split("/"))
if any(s in {"", ".", ".."} or not s.strip() for s in 段):
raise 迁移错误("file_path_denied", "文件路径含空段、越界段或绝对地址")
return 段
def _哈希(内容: bytes) -> str:
return hashlib.sha256(内容).hexdigest()
def _校验哈希(值):
if not isinstance(值, str) or not re.fullmatch("[0-9a-f]{64}", 值):
raise 迁移错误("file_hash_invalid", "文件SHA必须是明确的小写64位十六进制值")
def 解码文件(源: 旧快照) -> bytes:
行 = 源.原行
if (
源.source.system != "file"
or 源.source.table != "legacy_file"
or set(行) != {"id", "revision", "relpath", "size", "sha256", "content_base64"}
):
raise 迁移错误("file_record_invalid", "文件快照字段或来源类型不符合合同")
校验相对路径(行["relpath"])
if (
行["id"] != 行["relpath"]
or 行["id"] != 源.source.id
or 行["revision"] != 源.source.revision
):
raise 迁移错误("file_record_invalid", "文件原行与源身份不一致")
大小, 编码 = 行["size"], 行["content_base64"]
if type(大小) is not int or not 0 <= 大小 <= 单文件上限 or not isinstance(编码, str):
raise 迁移错误("file_size_invalid", "文件大小或编码声明不合法")
if len(编码) > ((单文件上限 + 2) // 3) * 4:
raise 迁移错误("file_too_large", "文件超过8MiB限制")
_校验哈希(行["sha256"])
try:
内容 = base64.b64decode(编码, validate=True)
except (ValueError, UnicodeError):
raise 迁移错误("file_record_invalid", "文件base64无法严格解码") from None
if len(内容) != 大小 or _哈希(内容) != 行["sha256"]:
raise 迁移错误("file_hash_mismatch", "文件原字节与大小/SHA声明不一致")
return 内容
def _私有(信息, 模式):
if 信息.st_uid != os.geteuid() or stat.S_IMODE(信息.st_mode) != 模式:
raise 迁移错误("file_permission_denied", "保全目录或文件不符合当前用户私有权限")
@contextmanager
def _根描述符(根: Path, *, 私有=False):
fd = os.open(根, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
try:
if 私有:
_私有(os.fstat(fd), 0o700)
yield fd
finally:
os.close(fd)
@contextmanager
def _父描述符(根fd: int, 相对: str, *, 创建=False, 私有=False):
段 = 校验相对路径(相对)
fd = os.dup(根fd)
try:
for 名 in 段[:-1]:
if 创建:
try:
os.mkdir(名, 0o700, dir_fd=fd)
except FileExistsError:
pass
子 = os.open(名, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd)
os.close(fd)
fd = 子
if 私有:
_私有(os.fstat(fd), 0o700)
yield fd, 段[-1]
finally:
os.close(fd)
def _读取(根fd: int, 相对: str, 上限: int, *, 私有=False) -> bytes:
with _父描述符(根fd, 相对, 私有=私有) as (父, 名):
fd = os.open(名, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=父)
with os.fdopen(fd, "rb") as f:
信息 = os.fstat(f.fileno())
if not stat.S_ISREG(信息.st_mode):
raise 迁移错误("file_type_denied", "只允许普通文件,不读取目录、设备或管道")
if 私有:
_私有(信息, 0o600)
if 信息.st_size > 上限:
raise 迁移错误("file_too_large", "文件超过明确字节上限,未截断输出")
内容 = f.read(上限 + 1)
if len(内容) > 上限:
raise 迁移错误("file_too_large", "读取期间文件超过明确字节上限")
return 内容
def _保存(根fd: int, 相对: str, 内容: bytes):
with _父描述符(根fd, 相对, 创建=True, 私有=True) as (父, 名):
临时 = ".迁移-" + secrets.token_hex(16)
fd = os.open(临时, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, dir_fd=父)
try:
with os.fdopen(fd, "wb") as f:
os.fchmod(f.fileno(), 0o600)
f.write(内容)
f.flush()
os.fsync(f.fileno())
try:
os.link(临时, 名, src_dir_fd=父, dst_dir_fd=父, follow_symlinks=False)
except FileExistsError:
if _读取(根fd, 相对, 快照上限, 私有=True) != 内容:
raise 迁移错误("output_conflict", "保全输出存在不同内容,不能覆盖") from None
os.fsync(父)
finally:
os.unlink(临时, dir_fd=父)
def _回执(快照: list[旧快照]) -> dict:
return {
"version": 1,
"snapshot_hash": _哈希(稳定JSON([s.导出() for s in 快照]).encode()),
"entries": [
{
"source_key": s.source.记录键,
"source_hash": s.源哈希,
"relpath": s.source.id,
"size": s.原行["size"],
"sha256": s.原行["sha256"],
}
for s in 快照
],
}
def _读取保全(根fd: int) -> list[旧快照]:
原 = 读取JSON(_读取(根fd, "快照.json", 快照上限, 私有=True).decode("utf-8"))
if not isinstance(原, list) or not 1 <= len(原) <= 文件数上限:
raise 迁移错误("file_receipt_invalid", "保全快照必须是1—500项数组")
if len(稳定JSON(原).encode()) > 快照上限:
raise 迁移错误("file_too_large", "保全快照超过16MiB")
快照 = [旧快照.从载荷(s) for s in 原]
if len({s.source.id for s in 快照}) != len(快照):
raise 迁移错误("file_receipt_invalid", "保全文件位置重复")
for s in 快照:
原字节 = 解码文件(s)
if _读取(根fd, "原始文件/" + s.source.id, 单文件上限, 私有=True) != 原字节:
raise 迁移错误("file_copy_mismatch", "实际副本与原文件快照不一致")
回执 = 读取JSON(_读取(根fd, "文件回执.json", 快照上限, 私有=True).decode("utf-8"))
if 稳定JSON(回执) != 稳定JSON(_回执(快照)):
raise 迁移错误("file_receipt_invalid", "文件回执与实际快照不一致")
return 快照
def 读取文件集(源根: Path, 清单: list, *, 数据集: str, 输出目录: Path) -> list[旧快照]:
"""源只读;首次新建私有保全根,完整旧输出只核对,不接续无回执目录。"""
命名空间 = 数据集命名空间(数据集, scope="files")
if not isinstance(清单, list) or not 1 <= len(清单) <= 文件数上限:
raise 迁移错误("file_manifest_invalid", "文件清单必须为1—500项数组")
已见 = set()
for 项 in 清单:
if not isinstance(项, dict) or set(项) != {"relpath", "revision", "sha256"}:
raise 迁移错误("file_manifest_invalid", "文件清单每项必须明确路径、修订与SHA")
校验相对路径(项["relpath"])
_校验哈希(项["sha256"])
if (
not isinstance(项["revision"], str)
or not 项["revision"].strip()
or 项["relpath"] in 已见
):
raise 迁移错误("file_manifest_invalid", "源修订缺失或文件路径重复")
已见.add(项["relpath"])
try:
源根, 目标根 = 源根.resolve(strict=True), 输出目录.resolve()
if 源根.is_relative_to(目标根) or 目标根.is_relative_to(源根):
raise 迁移错误("output_is_input", "源根和保全根不能相等或互相包含")
快照 = []
快照字节 = 3 # 数组括号及输出末尾LF;逐项计量,不反复序列化整个集合。
with _根描述符(源根) as 源fd:
for 项 in 清单:
内容 = _读取(源fd, 项["relpath"], 单文件上限)
if _哈希(内容) != 项["sha256"]:
raise 迁移错误("file_hash_mismatch", "源内容不符合清单预期SHA")
快照.append(
旧快照.从载荷(
{
"source": {
"system": "file",
"database": 命名空间,
"table": "legacy_file",
"id": 项["relpath"],
"revision": 项["revision"],
},
"record": {
"id": 项["relpath"],
"revision": 项["revision"],
"relpath": 项["relpath"],
"size": len(内容),
"sha256": 项["sha256"],
"content_base64": base64.b64encode(内容).decode("ascii"),
},
}
)
)
快照字节 += len(稳定JSON(快照[-1].导出()).encode()) + (1 if len(快照) > 1 else 0)
if 快照字节 > 快照上限:
raise 迁移错误("file_too_large", "完整文件快照超过16MiB,请显式分批")
try:
os.mkdir(目标根, 0o700)
except FileExistsError:
with _根描述符(目标根, 私有=True) as 目标fd:
原 = _读取保全(目标fd)
if 稳定JSON([s.导出() for s in 原]) != 稳定JSON([s.导出() for s in 快照]):
raise 迁移错误("output_conflict", "完整保全根属于其他输入,不能覆盖") from None
return 原
with _根描述符(目标根, 私有=True) as 目标fd:
for s in 快照:
_保存(目标fd, "原始文件/" + s.source.id, 解码文件(s))
_保存(目标fd, "快照.json", (稳定JSON([s.导出() for s in 快照]) + "\n").encode())
_保存(目标fd, "文件回执.json", (稳定JSON(_回执(快照)) + "\n").encode())
os.fsync(目标fd)
return 快照
except 迁移错误:
raise
except (OSError, ValueError, TypeError, KeyError):
raise 迁移错误("file_io_denied", "文件读取或保全未完成;不回显输入内容") from None
def 核对文件保全(文件回执: Path | None, 来源: list[旧快照] | None = None) -> dict:
需核 = None if 来源 is None else [s for s in 来源 if s.source.system == "file"]
if 需核 == []:
return {"applicable": False, "passed": True, "issues": []}
try:
if 文件回执 is None:
raise 迁移错误("file_receipt_missing", "文件输入缺少显式保全回执")
if 文件回执.name != "文件回执.json" or 文件回执.is_symlink():
raise 迁移错误("file_receipt_invalid", "必须指定保全目录的固定回执")
with _根描述符(文件回执.parent, 私有=True) as fd:
保全 = _读取保全(fd)
已有 = {s.source.记录键: s.源哈希 for s in 保全}
if 需核 is not None and any(已有.get(s.source.记录键) != s.源哈希 for s in 需核):
raise 迁移错误("file_source_mismatch", "保全回执不包含本次确切文件来源")
except (OSError, ValueError, TypeError, KeyError) as 错:
代码 = 错.code if isinstance(错, 迁移错误) else "file_receipt_invalid"
return {"applicable": True, "passed": False, "issues": [{"reason_code": 代码}]}
return {"applicable": True, "passed": True, "issues": []}