Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check integrity of a 1C patch workspace before diff/package/apply steps."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as stream:
|
||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def issue(severity: str, code: str, message: str, *, path: str | None = None, record: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
result = {"severity": severity, "code": code, "message": message}
|
||||
if path:
|
||||
result["path"] = path
|
||||
if record:
|
||||
result["record"] = record
|
||||
return result
|
||||
|
||||
|
||||
def is_relative_safe(relative_path: str) -> bool:
|
||||
path = Path(relative_path)
|
||||
return not path.is_absolute() and ".." not in path.parts
|
||||
|
||||
|
||||
def expected_paths(workspace: Path, relative_path: str) -> tuple[Path, Path]:
|
||||
rel = Path(relative_path)
|
||||
return workspace / "original" / rel, workspace / "working" / rel
|
||||
|
||||
|
||||
def all_files(root: Path) -> list[Path]:
|
||||
if not root.exists():
|
||||
return []
|
||||
return sorted(path for path in root.rglob("*") if path.is_file())
|
||||
|
||||
|
||||
def rel_set(root: Path) -> set[str]:
|
||||
result = set()
|
||||
for path in all_files(root):
|
||||
result.add(str(path.relative_to(root)).replace("\\", "/"))
|
||||
return result
|
||||
|
||||
|
||||
def check_workspace(workspace: Path) -> dict[str, Any]:
|
||||
manifest_path = workspace / "manifest.json"
|
||||
findings = []
|
||||
if not manifest_path.exists():
|
||||
findings.append(issue("error", "missing_manifest", "Workspace manifest.json is missing.", path=str(manifest_path)))
|
||||
return result(workspace, findings, [])
|
||||
|
||||
manifest = load_json(manifest_path)
|
||||
records = manifest.get("files") or []
|
||||
manifest_relatives = set()
|
||||
file_checks = []
|
||||
for record in records:
|
||||
relative = str(record.get("relative_path") or "")
|
||||
manifest_relatives.add(relative)
|
||||
if not relative or not is_relative_safe(relative):
|
||||
findings.append(issue("error", "unsafe_relative_path", f"Unsafe relative path in manifest: {relative}", record=record))
|
||||
continue
|
||||
original, working = expected_paths(workspace, relative)
|
||||
check = {
|
||||
"relative_path": relative,
|
||||
"original_path": str(original),
|
||||
"working_path": str(working),
|
||||
"expected_sha256": record.get("sha256"),
|
||||
"original_exists": original.exists(),
|
||||
"working_exists": working.exists(),
|
||||
}
|
||||
if not original.exists():
|
||||
findings.append(issue("error", "missing_original_file", "Original file is missing.", path=str(original), record=record))
|
||||
else:
|
||||
actual = sha256_file(original)
|
||||
check["original_sha256"] = actual
|
||||
if record.get("sha256") and actual != record.get("sha256"):
|
||||
findings.append(issue("error", "original_hash_mismatch", "Original file hash differs from manifest; original/ must stay immutable.", path=str(original), record=record))
|
||||
if not working.exists():
|
||||
findings.append(issue("error", "missing_working_file", "Working file is missing.", path=str(working), record=record))
|
||||
else:
|
||||
check["working_sha256"] = sha256_file(working)
|
||||
file_checks.append(check)
|
||||
|
||||
original_extra = sorted(rel_set(workspace / "original") - manifest_relatives)
|
||||
working_extra = sorted(rel_set(workspace / "working") - manifest_relatives)
|
||||
for relative in original_extra:
|
||||
findings.append(issue("error", "extra_original_file", "Unexpected file under original/.", path=str(workspace / "original" / Path(relative))))
|
||||
for relative in working_extra:
|
||||
findings.append(issue("warning", "extra_working_file", "Unexpected file under working/; future packaging must explicitly include or reject it.", path=str(workspace / "working" / Path(relative))))
|
||||
|
||||
if not (workspace / "proposal.json").exists():
|
||||
findings.append(issue("warning", "missing_proposal_copy", "proposal.json is missing from workspace.", path=str(workspace / "proposal.json")))
|
||||
if not (workspace / "safety.json").exists():
|
||||
findings.append(issue("warning", "missing_safety_copy", "safety.json is missing from workspace.", path=str(workspace / "safety.json")))
|
||||
|
||||
return result(workspace, findings, file_checks, manifest=manifest)
|
||||
|
||||
|
||||
def result(workspace: Path, findings: list[dict[str, Any]], file_checks: list[dict[str, Any]], manifest: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
errors = [row for row in findings if row.get("severity") == "error"]
|
||||
warnings = [row for row in findings if row.get("severity") == "warning"]
|
||||
return {
|
||||
"schema": "onec_patch_workspace_integrity.v1",
|
||||
"workspace": str(workspace),
|
||||
"manifest_schema": (manifest or {}).get("schema"),
|
||||
"passed": not errors,
|
||||
"findings": findings,
|
||||
"file_checks": file_checks,
|
||||
"counts": {
|
||||
"files": len(file_checks),
|
||||
"errors": len(errors),
|
||||
"warnings": len(warnings),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Check 1C patch workspace integrity.")
|
||||
parser.add_argument("--workspace", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
check = check_workspace(args.workspace)
|
||||
output = json.dumps(check, ensure_ascii=False, indent=2) + "\n"
|
||||
if args.output:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(output, encoding="utf-8")
|
||||
print(json.dumps({"output": str(args.output) if args.output else None, "passed": check["passed"], "counts": check["counts"]}, ensure_ascii=False))
|
||||
return 0 if check["passed"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user