接续 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。
271 lines
12 KiB
Python
271 lines
12 KiB
Python
"""显式文件清单的字节保全与活副本核对;不导入业务或推断作品用途。"""
|
||
|
||
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": []}
|