mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-19 02:00:28 +02:00
1692 lines
61 KiB
Python
1692 lines
61 KiB
Python
"""Fail-closed exact-shape transfer candidate and review-packet generator.
|
|
|
|
This module never modifies repository files and never decides exactness. It
|
|
emits only an unapplied candidate patch outside the repository. Acceptance is
|
|
reserved for the hardened comparator, whole-TU regression gate, and whole-object
|
|
data/BSS gate in a separate disposable build worktree.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import difflib
|
|
import hashlib
|
|
import json
|
|
import shutil
|
|
import re
|
|
import struct
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
|
|
|
|
if __package__ in (None, ""):
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
from tools.coff_compare import (
|
|
CoffError,
|
|
load,
|
|
section_info,
|
|
section_infos_equal,
|
|
)
|
|
from tools.regression_gate import GateError, _object_fingerprint
|
|
|
|
|
|
REQUIRED_PROOFS = (
|
|
"sizes_equal",
|
|
"meaningful_equal",
|
|
"reloc_count_equal",
|
|
"reloc_addr_type_equal",
|
|
"storage_class_equal",
|
|
"prologue_equal",
|
|
"nontrivial",
|
|
"substitution_consistent",
|
|
)
|
|
FORBIDDEN_CONSTANT_PREFIXES = (
|
|
"??_C@",
|
|
"__real@",
|
|
"__xmm@",
|
|
"__mask@",
|
|
)
|
|
CODE_SYMBOL_TYPE = 0x20
|
|
RELOC_ENTRY_SIZE = 10
|
|
|
|
|
|
class Refusal(RuntimeError):
|
|
"""Terminal, non-retriable refusal for one transfer pair."""
|
|
|
|
def __init__(self, code: str, detail: str):
|
|
super().__init__(f"{code}: {detail}")
|
|
self.code = code
|
|
self.detail = detail
|
|
|
|
def as_dict(self) -> Dict[str, str]:
|
|
return {"code": self.code, "detail": self.detail}
|
|
|
|
|
|
def _sha256_bytes(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def _json_bytes(value: Any) -> bytes:
|
|
return (json.dumps(value, indent=2, sort_keys=True) + "\n").encode("utf-8")
|
|
|
|
|
|
def _inside(path: Path, root: Path) -> bool:
|
|
path = path.resolve()
|
|
root = root.resolve()
|
|
return path == root or root in path.parents
|
|
|
|
|
|
def _checked_repo_path(root: Path, relative: str, suffix: str) -> Path:
|
|
if not isinstance(relative, str) or not relative.startswith("source/"):
|
|
raise Refusal("invalid-unit-path", f"unit is not a source path: {relative!r}")
|
|
candidate = (root / (relative + suffix)).resolve()
|
|
if not _inside(candidate, root):
|
|
raise Refusal("invalid-unit-path", f"unit escapes repository: {relative!r}")
|
|
return candidate
|
|
|
|
|
|
def _read_json(path: Path) -> Any:
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
raise Refusal("invalid-json", f"cannot read {path}: {error}") from error
|
|
|
|
|
|
def _select_pair(
|
|
atlas: Any,
|
|
recipient_unit: str,
|
|
recipient_function: str,
|
|
donor_unit: Optional[str] = None,
|
|
donor_function: Optional[str] = None,
|
|
) -> Mapping[str, Any]:
|
|
if not isinstance(atlas, list):
|
|
raise Refusal("invalid-atlas", "top-level atlas value must be a list")
|
|
matches: List[Mapping[str, Any]] = []
|
|
for item in atlas:
|
|
if not isinstance(item, dict):
|
|
raise Refusal("invalid-atlas", "every atlas entry must be an object")
|
|
donor = item.get("donor")
|
|
recipient = item.get("recipient")
|
|
if not isinstance(donor, dict) or not isinstance(recipient, dict):
|
|
raise Refusal("invalid-atlas", "pair donor/recipient must be objects")
|
|
if (
|
|
recipient.get("unit") == recipient_unit
|
|
and recipient.get("function") == recipient_function
|
|
and (donor_unit is None or donor.get("unit") == donor_unit)
|
|
and (donor_function is None or donor.get("function") == donor_function)
|
|
):
|
|
matches.append(item)
|
|
if not matches:
|
|
raise Refusal(
|
|
"pair-not-found",
|
|
f"no pair targets {recipient_unit}:{recipient_function}",
|
|
)
|
|
if len(matches) != 1:
|
|
raise Refusal(
|
|
"ambiguous-pair",
|
|
f"{len(matches)} pairs target {recipient_unit}:{recipient_function}",
|
|
)
|
|
return matches[0]
|
|
|
|
|
|
def _validate_pair_policy(pair: Mapping[str, Any]) -> None:
|
|
if pair.get("safe") is not True:
|
|
raise Refusal("unsafe-pair", "atlas pair is not explicitly safe")
|
|
if pair.get("donor_source_resolved") is not True:
|
|
raise Refusal(
|
|
"donor-source-unresolved",
|
|
"atlas does not explicitly mark donor source resolved",
|
|
)
|
|
recipient = pair.get("recipient", {})
|
|
if recipient.get("status") != "ABSENT":
|
|
raise Refusal(
|
|
"recipient-not-absent",
|
|
f"recipient status is {recipient.get('status')!r}, not 'ABSENT'",
|
|
)
|
|
proof = pair.get("proof")
|
|
if not isinstance(proof, dict):
|
|
raise Refusal("incomplete-proof", "proof must be an object")
|
|
missing = [key for key in REQUIRED_PROOFS if proof.get(key) is not True]
|
|
false_or_unknown = [key for key, value in proof.items() if value is not True]
|
|
if missing or false_or_unknown:
|
|
names = sorted(set(missing + false_or_unknown))
|
|
raise Refusal("incomplete-proof", f"proofs are not true: {', '.join(names)}")
|
|
meaningful = pair.get("meaningful")
|
|
if not isinstance(meaningful, int) or isinstance(meaningful, bool):
|
|
raise Refusal("invalid-meaningful-size", "meaningful size must be an integer")
|
|
if meaningful <= 16:
|
|
raise Refusal("stub", f"meaningful size {meaningful} is at most 16 bytes")
|
|
substitutions = pair.get("substitutions")
|
|
if not isinstance(substitutions, list):
|
|
raise Refusal("invalid-atlas", "substitutions must be an ordered list")
|
|
for event in substitutions:
|
|
if (
|
|
not isinstance(event, list)
|
|
or len(event) != 2
|
|
or not all(isinstance(value, str) for value in event)
|
|
):
|
|
raise Refusal("invalid-atlas", f"invalid substitution event: {event!r}")
|
|
|
|
|
|
def _function_symbol(obj: Mapping[str, Any], name: str) -> Mapping[str, Any]:
|
|
matches = [
|
|
symbol
|
|
for symbol in obj["symbols"]
|
|
if symbol["name"] == name
|
|
and int(symbol["section"]) > 0
|
|
and int(symbol["type"]) == CODE_SYMBOL_TYPE
|
|
]
|
|
if len(matches) != 1:
|
|
raise Refusal(
|
|
"target-symbol-ambiguous",
|
|
f"expected one defined code symbol {name!r}, found {len(matches)}",
|
|
)
|
|
return matches[0]
|
|
|
|
|
|
def _function_raw(
|
|
obj: Mapping[str, Any], symbol: Mapping[str, Any]
|
|
) -> Tuple[bytes, Mapping[str, Any]]:
|
|
section_number = int(symbol["section"])
|
|
if section_number <= 0 or section_number > len(obj["sections"]):
|
|
raise Refusal("invalid-target-object", "function section number is invalid")
|
|
section = obj["sections"][section_number - 1]
|
|
start = int(section["raw"])
|
|
size = int(section["size"])
|
|
end = start + size
|
|
if start <= 0 or end > len(obj["data"]):
|
|
raise Refusal("invalid-target-object", "function bytes are unavailable")
|
|
return bytes(obj["data"][start:end]), section
|
|
|
|
|
|
def _raw_relocations(
|
|
obj: Mapping[str, Any], section: Mapping[str, Any], raw: bytes
|
|
) -> List[Dict[str, Any]]:
|
|
result: List[Dict[str, Any]] = []
|
|
for index in range(int(section["reloc_count"])):
|
|
offset = int(section["reloc"]) + index * RELOC_ENTRY_SIZE
|
|
if offset + RELOC_ENTRY_SIZE > len(obj["data"]):
|
|
raise Refusal("invalid-target-object", "relocation table is truncated")
|
|
address, target_index, relocation_type = struct.unpack_from(
|
|
"<LLH", obj["data"], offset
|
|
)
|
|
target = obj["by_index"].get(target_index)
|
|
if target is None:
|
|
raise Refusal(
|
|
"invalid-target-object",
|
|
f"relocation {index} references missing symbol {target_index}",
|
|
)
|
|
if address + 4 > len(raw):
|
|
raise Refusal(
|
|
"invalid-target-object",
|
|
f"relocation {index} lies outside function bytes",
|
|
)
|
|
result.append(
|
|
{
|
|
"index": index,
|
|
"address": int(address),
|
|
"type": int(relocation_type),
|
|
"addend": int(struct.unpack_from("<i", raw, address)[0]),
|
|
"target": target["name"],
|
|
"target_type": int(target["type"]),
|
|
"target_section": int(target["section"]),
|
|
}
|
|
)
|
|
return result
|
|
|
|
|
|
def _meaningful_size(raw: bytes) -> int:
|
|
size = len(raw)
|
|
while size and raw[size - 1] in (0x90, 0xCC):
|
|
size -= 1
|
|
return size
|
|
|
|
|
|
def _load_target_function(path: Path, symbol_name: str) -> Dict[str, Any]:
|
|
if not path.is_file():
|
|
raise Refusal("target-object-missing", f"object does not exist: {path}")
|
|
try:
|
|
obj = load(path)
|
|
info = section_info(obj, symbol_name)
|
|
except (OSError, CoffError) as error:
|
|
raise Refusal(
|
|
"invalid-target-object",
|
|
f"cannot load {path} / {symbol_name}: {error}",
|
|
) from error
|
|
symbol = _function_symbol(obj, symbol_name)
|
|
raw, section = _function_raw(obj, symbol)
|
|
relocs = _raw_relocations(obj, section, raw)
|
|
hardened_shape = [
|
|
(int(item["address"]), int(item["type"]))
|
|
for item in info["relocations"]
|
|
]
|
|
raw_shape = [(item["address"], item["type"]) for item in relocs]
|
|
if hardened_shape != raw_shape:
|
|
raise Refusal(
|
|
"invalid-target-object",
|
|
"raw relocation order differs from hardened resolver order",
|
|
)
|
|
return {
|
|
"path": str(path.resolve()),
|
|
"sha256": _sha256_bytes(path.read_bytes()),
|
|
"info": info,
|
|
"symbol": dict(symbol),
|
|
"raw": raw,
|
|
"relocations": relocs,
|
|
"meaningful": _meaningful_size(raw),
|
|
}
|
|
|
|
|
|
def _is_constant_symbol(name: str) -> bool:
|
|
return name.startswith(FORBIDDEN_CONSTANT_PREFIXES) or name.startswith("$")
|
|
|
|
|
|
def _c_identifier(symbol_name: str) -> str:
|
|
if _is_constant_symbol(symbol_name):
|
|
raise Refusal(
|
|
"constant-substitution-forbidden",
|
|
f"symbol is a compiler constant: {symbol_name!r}",
|
|
)
|
|
name = symbol_name
|
|
if name.startswith("_"):
|
|
name = name[1:]
|
|
name = re.sub(r"@\d+$", "", name)
|
|
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name):
|
|
raise Refusal(
|
|
"unsupported-symbol-spelling",
|
|
f"cannot map COFF symbol to one C identifier: {symbol_name!r}",
|
|
)
|
|
return name
|
|
|
|
|
|
def _derive_substitutions(
|
|
pair: Mapping[str, Any],
|
|
donor_target: Mapping[str, Any],
|
|
recipient_target: Mapping[str, Any],
|
|
) -> Dict[str, Any]:
|
|
donor_info = donor_target["info"]
|
|
recipient_info = recipient_target["info"]
|
|
if donor_info["size"] != recipient_info["size"]:
|
|
raise Refusal("size-mismatch", "donor/recipient padded sizes differ")
|
|
if donor_info["normalized_sha256"] != recipient_info["normalized_sha256"]:
|
|
raise Refusal(
|
|
"normalized-bytes-mismatch",
|
|
"donor/recipient masked function bytes differ",
|
|
)
|
|
if donor_target["meaningful"] != recipient_target["meaningful"]:
|
|
raise Refusal("meaningful-size-mismatch", "measured meaningful sizes differ")
|
|
if donor_target["meaningful"] != pair["meaningful"]:
|
|
raise Refusal(
|
|
"stale-atlas",
|
|
"atlas meaningful size differs from target object measurement",
|
|
)
|
|
if donor_target["meaningful"] <= 16:
|
|
raise Refusal("stub", "measured target function is a stub")
|
|
if donor_info["size"] != pair.get("size"):
|
|
raise Refusal("stale-atlas", "atlas padded size differs from target object")
|
|
if donor_target["symbol"]["storage"] != recipient_target["symbol"]["storage"]:
|
|
raise Refusal("storage-class-mismatch", "function storage classes differ")
|
|
if donor_target["raw"][:4] != recipient_target["raw"][:4]:
|
|
raise Refusal("prologue-mismatch", "first four target bytes differ")
|
|
|
|
donor_relocs = donor_target["relocations"]
|
|
recipient_relocs = recipient_target["relocations"]
|
|
if len(donor_relocs) != len(recipient_relocs):
|
|
raise Refusal("relocation-count-mismatch", "target relocation counts differ")
|
|
|
|
events: List[List[str]] = []
|
|
mapping: Dict[str, str] = {}
|
|
reverse: Dict[str, str] = {}
|
|
recipient_symbols: Dict[str, str] = {}
|
|
for donor, recipient in zip(donor_relocs, recipient_relocs):
|
|
if (
|
|
donor["address"] != recipient["address"]
|
|
or donor["type"] != recipient["type"]
|
|
):
|
|
raise Refusal(
|
|
"statement-order-mismatch",
|
|
"ordered target relocation address/type sequence differs",
|
|
)
|
|
if donor["addend"] != recipient["addend"]:
|
|
raise Refusal(
|
|
"relocation-addend-mismatch",
|
|
"relocation addends differ; constant substitution is forbidden",
|
|
)
|
|
donor_name = donor["target"]
|
|
recipient_name = recipient["target"]
|
|
kind = (
|
|
"function"
|
|
if recipient["type"] == 0x14
|
|
or recipient["target_type"] == CODE_SYMBOL_TYPE
|
|
else "data"
|
|
)
|
|
if not _is_constant_symbol(recipient_name):
|
|
recipient_symbols[recipient_name] = kind
|
|
if donor_name == recipient_name:
|
|
continue
|
|
donor_identifier = _c_identifier(donor_name)
|
|
recipient_identifier = _c_identifier(recipient_name)
|
|
if mapping.get(donor_identifier, recipient_identifier) != recipient_identifier:
|
|
raise Refusal(
|
|
"substitution-non-injective",
|
|
f"{donor_name!r} maps to more than one recipient symbol",
|
|
)
|
|
if reverse.get(recipient_identifier, donor_identifier) != donor_identifier:
|
|
raise Refusal(
|
|
"substitution-non-injective",
|
|
f"more than one donor symbol maps to {recipient_name!r}",
|
|
)
|
|
mapping[donor_identifier] = recipient_identifier
|
|
reverse[recipient_identifier] = donor_identifier
|
|
events.append([donor_name, recipient_name])
|
|
|
|
if events != pair["substitutions"]:
|
|
raise Refusal(
|
|
"wrong-callee-or-stale-atlas",
|
|
"atlas substitution events do not exactly match recipient target relocations",
|
|
)
|
|
if pair.get("class") == "IDENTICAL-INCLUDING-TARGETS" and events:
|
|
raise Refusal(
|
|
"invalid-zero-substitution-class",
|
|
"zero-substitution class produced target-derived substitutions",
|
|
)
|
|
return {
|
|
"events": events,
|
|
"identifier_map": mapping,
|
|
"recipient_symbols": recipient_symbols,
|
|
"donor_relocation_counts": {
|
|
identifier: count
|
|
for identifier, count in sorted(
|
|
{
|
|
_c_identifier(relocation["target"]): sum(
|
|
1
|
|
for candidate in donor_relocs
|
|
if not _is_constant_symbol(candidate["target"])
|
|
and _c_identifier(candidate["target"])
|
|
== _c_identifier(relocation["target"])
|
|
)
|
|
for relocation in donor_relocs
|
|
if not _is_constant_symbol(relocation["target"])
|
|
}.items()
|
|
)
|
|
},
|
|
}
|
|
|
|
|
|
def _mask_noncode(text: str) -> str:
|
|
"""Replace comments and string/character contents while preserving offsets."""
|
|
chars = list(text)
|
|
index = 0
|
|
state = "normal"
|
|
while index < len(chars):
|
|
char = chars[index]
|
|
following = chars[index + 1] if index + 1 < len(chars) else ""
|
|
if state == "normal":
|
|
if char == "/" and following == "/":
|
|
chars[index] = chars[index + 1] = " "
|
|
index += 2
|
|
state = "line"
|
|
continue
|
|
if char == "/" and following == "*":
|
|
chars[index] = chars[index + 1] = " "
|
|
index += 2
|
|
state = "block"
|
|
continue
|
|
if char == '"':
|
|
chars[index] = " "
|
|
index += 1
|
|
state = "string"
|
|
continue
|
|
if char == "'":
|
|
chars[index] = " "
|
|
index += 1
|
|
state = "character"
|
|
continue
|
|
elif state == "line":
|
|
if char == "\n":
|
|
state = "normal"
|
|
else:
|
|
chars[index] = " "
|
|
index += 1
|
|
continue
|
|
elif state == "block":
|
|
if char == "*" and following == "/":
|
|
chars[index] = chars[index + 1] = " "
|
|
index += 2
|
|
state = "normal"
|
|
continue
|
|
if char != "\n":
|
|
chars[index] = " "
|
|
index += 1
|
|
continue
|
|
else:
|
|
if char == "\\" and following:
|
|
chars[index] = " "
|
|
if following != "\n":
|
|
chars[index + 1] = " "
|
|
index += 2
|
|
continue
|
|
quote = '"' if state == "string" else "'"
|
|
if char == quote:
|
|
chars[index] = " "
|
|
state = "normal"
|
|
elif char != "\n":
|
|
chars[index] = " "
|
|
index += 1
|
|
continue
|
|
index += 1
|
|
return "".join(chars)
|
|
|
|
|
|
def _matching_delimiter(masked: str, start: int, opening: str, closing: str) -> int:
|
|
depth = 0
|
|
for index in range(start, len(masked)):
|
|
if masked[index] == opening:
|
|
depth += 1
|
|
elif masked[index] == closing:
|
|
depth -= 1
|
|
if depth == 0:
|
|
return index
|
|
raise Refusal("donor-source-ambiguous", f"unclosed {opening!r} delimiter")
|
|
|
|
|
|
def _definition_start(masked: str, identifier_start: int) -> int:
|
|
line_start = masked.rfind("\n", 0, identifier_start) + 1
|
|
prefix = masked[line_start:identifier_start]
|
|
if prefix.strip():
|
|
return line_start
|
|
previous_end = line_start - 1
|
|
while previous_end > 0:
|
|
previous_start = masked.rfind("\n", 0, previous_end) + 1
|
|
if masked[previous_start:previous_end].strip():
|
|
return previous_start
|
|
previous_end = previous_start - 1
|
|
return line_start
|
|
|
|
|
|
def _function_definitions(text: str, identifier: str) -> List[Tuple[int, int]]:
|
|
masked = _mask_noncode(text)
|
|
token = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
|
definitions: List[Tuple[int, int]] = []
|
|
for match in token.finditer(masked):
|
|
index = match.end()
|
|
while index < len(masked) and masked[index].isspace():
|
|
index += 1
|
|
if index >= len(masked) or masked[index] != "(":
|
|
continue
|
|
close_parenthesis = _matching_delimiter(masked, index, "(", ")")
|
|
after = close_parenthesis + 1
|
|
while after < len(masked) and masked[after].isspace():
|
|
after += 1
|
|
if after >= len(masked) or masked[after] != "{":
|
|
continue
|
|
start = _definition_start(masked, match.start())
|
|
prefix = masked[start:match.start()]
|
|
if "#" in prefix or not prefix.strip():
|
|
continue
|
|
close_brace = _matching_delimiter(masked, after, "{", "}")
|
|
definitions.append((start, close_brace + 1))
|
|
return definitions
|
|
|
|
|
|
def _extract_donor_source(path: Path, symbol_name: str) -> Dict[str, Any]:
|
|
if not path.is_file():
|
|
raise Refusal("donor-source-missing", f"donor TU does not exist: {path}")
|
|
try:
|
|
raw = path.read_bytes()
|
|
text = raw.decode("utf-8")
|
|
except (OSError, UnicodeError) as error:
|
|
raise Refusal("donor-source-missing", f"cannot read donor TU: {error}") from error
|
|
identifier = _c_identifier(symbol_name)
|
|
definitions = _function_definitions(text, identifier)
|
|
if not definitions:
|
|
raise Refusal(
|
|
"donor-source-missing",
|
|
f"{identifier!r} is not a function definition in {path}",
|
|
)
|
|
if len(definitions) != 1:
|
|
raise Refusal(
|
|
"donor-source-ambiguous",
|
|
f"found {len(definitions)} definitions of {identifier!r}",
|
|
)
|
|
start, end = definitions[0]
|
|
source = text[start:end]
|
|
occurrences = len(
|
|
re.findall(r"\b" + re.escape(identifier) + r"\b", _mask_noncode(source))
|
|
)
|
|
if occurrences != 1:
|
|
raise Refusal(
|
|
"donor-source-ambiguous",
|
|
"recursive or multiply-mentioned donor function name is unsupported",
|
|
)
|
|
return {
|
|
"path": str(path.resolve()),
|
|
"sha256": _sha256_bytes(raw),
|
|
"identifier": identifier,
|
|
"source": source,
|
|
"start_line": text.count("\n", 0, start) + 1,
|
|
"end_line": text.count("\n", 0, end) + 1,
|
|
}
|
|
|
|
|
|
def _canonical_c_type(text: str, *, parameter: bool) -> str:
|
|
value = _mask_noncode(text).strip()
|
|
if parameter and ("(" in value or ")" in value):
|
|
raise Refusal(
|
|
"prototype-proof-unsupported",
|
|
f"function-pointer parameter cannot be proven safely: {text!r}",
|
|
)
|
|
value = re.sub(
|
|
r"\b(static|extern|inline|__inline|__forceinline)\b",
|
|
" ",
|
|
value,
|
|
)
|
|
if parameter and "[" in value:
|
|
raise Refusal(
|
|
"prototype-proof-unsupported",
|
|
f"array parameter cannot be proven safely: {text!r}",
|
|
)
|
|
type_words = {
|
|
"void", "char", "short", "int", "long", "float", "double",
|
|
"signed", "unsigned", "const", "volatile", "struct", "union",
|
|
"enum", "boolean", "real",
|
|
}
|
|
if parameter and value != "...":
|
|
matches = list(re.finditer(r"\b[A-Za-z_][A-Za-z0-9_]*\b", value))
|
|
if len(matches) >= 2:
|
|
last = matches[-1]
|
|
suffix = value[last.end():].strip()
|
|
if last.group() not in type_words and not suffix:
|
|
value = value[:last.start()] + value[last.end():]
|
|
value = re.sub(r"\s+", " ", value).strip()
|
|
value = re.sub(r"\s*\*\s*", "*", value)
|
|
value = re.sub(r"\s*,\s*", ",", value)
|
|
if not value:
|
|
raise Refusal("prototype-proof-unsupported", "empty canonical C type")
|
|
return value
|
|
|
|
|
|
def _signature_records(
|
|
text: str, identifier: str, source: str
|
|
) -> List[Dict[str, Any]]:
|
|
masked = _mask_noncode(text)
|
|
token = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
|
records: List[Dict[str, Any]] = []
|
|
for match in token.finditer(masked):
|
|
opening = match.end()
|
|
while opening < len(masked) and masked[opening].isspace():
|
|
opening += 1
|
|
if opening >= len(masked) or masked[opening] != "(":
|
|
continue
|
|
closing = _matching_delimiter(masked, opening, "(", ")")
|
|
after = closing + 1
|
|
while after < len(masked) and masked[after].isspace():
|
|
after += 1
|
|
if after >= len(masked) or masked[after] not in ";{":
|
|
continue
|
|
start = _definition_start(masked, match.start())
|
|
return_text = text[start:match.start()].strip()
|
|
masked_return = masked[start:match.start()].strip()
|
|
if (
|
|
not masked_return
|
|
or masked_return.startswith("#")
|
|
or any(character in masked_return for character in "{}=")
|
|
or re.search(
|
|
r"\b(if|for|while|switch|return|sizeof)\s*$",
|
|
masked_return,
|
|
)
|
|
):
|
|
continue
|
|
parameter_text = text[opening + 1:closing]
|
|
parameters: List[str] = []
|
|
part_start = 0
|
|
depth = 0
|
|
for index, character in enumerate(_mask_noncode(parameter_text)):
|
|
if character in "([":
|
|
depth += 1
|
|
elif character in ")]":
|
|
depth -= 1
|
|
elif character == "," and depth == 0:
|
|
parameters.append(parameter_text[part_start:index])
|
|
part_start = index + 1
|
|
parameters.append(parameter_text[part_start:])
|
|
canonical_parameters = [
|
|
_canonical_c_type(parameter, parameter=True)
|
|
for parameter in parameters
|
|
if parameter.strip() and parameter.strip() != "void"
|
|
]
|
|
records.append(
|
|
{
|
|
"source": source,
|
|
"line": text.count("\n", 0, start) + 1,
|
|
"definition": masked[after] == "{",
|
|
"return_type": _canonical_c_type(return_text, parameter=False),
|
|
"parameter_types": canonical_parameters,
|
|
"text": text[start:closing + 1].strip(),
|
|
}
|
|
)
|
|
return records
|
|
|
|
|
|
def _prove_prototype_compatibility(
|
|
repo_root: Path,
|
|
recipient_path: Path,
|
|
donor_source: Mapping[str, Any],
|
|
donor_function: str,
|
|
recipient_function: str,
|
|
) -> Dict[str, Any]:
|
|
donor_identifier = _c_identifier(donor_function)
|
|
recipient_identifier = _c_identifier(recipient_function)
|
|
donor_records = _signature_records(
|
|
donor_source["source"],
|
|
donor_identifier,
|
|
donor_source["path"],
|
|
)
|
|
if len(donor_records) != 1:
|
|
raise Refusal(
|
|
"donor-prototype-ambiguous",
|
|
f"expected one donor signature, found {len(donor_records)}",
|
|
)
|
|
recipient_records: List[Dict[str, Any]] = []
|
|
for path in _quoted_include_closure(repo_root, recipient_path):
|
|
try:
|
|
text = path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeError) as error:
|
|
raise Refusal(
|
|
"recipient-prototype-missing",
|
|
f"cannot inspect signature context {path}: {error}",
|
|
) from error
|
|
recipient_records.extend(
|
|
_signature_records(
|
|
text,
|
|
recipient_identifier,
|
|
str(path.relative_to(repo_root)).replace("\\", "/"),
|
|
)
|
|
)
|
|
if not recipient_records:
|
|
raise Refusal(
|
|
"recipient-prototype-missing",
|
|
f"no explicit declaration or inline definition of "
|
|
f"{recipient_identifier!r} in recipient include context",
|
|
)
|
|
recipient_shapes = {
|
|
(
|
|
record["return_type"],
|
|
tuple(record["parameter_types"]),
|
|
)
|
|
for record in recipient_records
|
|
}
|
|
if len(recipient_shapes) != 1:
|
|
raise Refusal(
|
|
"recipient-prototype-ambiguous",
|
|
f"conflicting declarations of {recipient_identifier!r}",
|
|
)
|
|
donor_shape = (
|
|
donor_records[0]["return_type"],
|
|
tuple(donor_records[0]["parameter_types"]),
|
|
)
|
|
recipient_shape = next(iter(recipient_shapes))
|
|
if donor_shape != recipient_shape:
|
|
raise Refusal(
|
|
"recipient-prototype-incompatible",
|
|
f"donor {donor_shape[0]}{donor_shape[1]} is incompatible with "
|
|
f"recipient {recipient_shape[0]}{recipient_shape[1]}",
|
|
)
|
|
return {
|
|
"compatible": True,
|
|
"donor": donor_records[0],
|
|
"recipient": recipient_records,
|
|
"canonical_return_type": donor_shape[0],
|
|
"canonical_parameter_types": list(donor_shape[1]),
|
|
}
|
|
|
|
|
|
def _replace_token(text: str, old: str, new: str) -> Tuple[str, int]:
|
|
pattern = re.compile(r"\b" + re.escape(old) + r"\b")
|
|
return pattern.subn(new, text)
|
|
|
|
|
|
def _transform_source(
|
|
donor_source: Mapping[str, Any],
|
|
donor_function: str,
|
|
recipient_function: str,
|
|
derived: Mapping[str, Any],
|
|
) -> str:
|
|
source = donor_source["source"]
|
|
masked_source = _mask_noncode(source)
|
|
for identifier, expected in derived["donor_relocation_counts"].items():
|
|
actual = len(
|
|
re.findall(r"\b" + re.escape(identifier) + r"\b", masked_source)
|
|
)
|
|
if actual != expected:
|
|
raise Refusal(
|
|
"wrong-callee-or-source-mismatch",
|
|
f"{identifier!r} occurs {actual} times in source but target "
|
|
f"relocates it {expected} times",
|
|
)
|
|
donor_identifier = _c_identifier(donor_function)
|
|
recipient_identifier = _c_identifier(recipient_function)
|
|
source, count = _replace_token(source, donor_identifier, recipient_identifier)
|
|
if count != 1:
|
|
raise Refusal(
|
|
"donor-source-ambiguous",
|
|
f"donor function name replacement count is {count}, not 1",
|
|
)
|
|
for old, new in sorted(
|
|
derived["identifier_map"].items(), key=lambda item: (-len(item[0]), item[0])
|
|
):
|
|
source, count = _replace_token(source, old, new)
|
|
if count != derived["donor_relocation_counts"][old]:
|
|
raise Refusal("internal-error", f"validated token {old!r} changed count")
|
|
return source
|
|
|
|
|
|
def _quoted_include_closure(repo_root: Path, source_path: Path) -> List[Path]:
|
|
pending = [source_path.resolve()]
|
|
visited: List[Path] = []
|
|
seen = set()
|
|
while pending:
|
|
path = pending.pop()
|
|
if path in seen:
|
|
continue
|
|
seen.add(path)
|
|
if not path.is_file() or not _inside(path, repo_root):
|
|
continue
|
|
visited.append(path)
|
|
if len(visited) > 512:
|
|
raise Refusal("include-context-ambiguous", "quoted include closure exceeds 512 files")
|
|
try:
|
|
text = path.read_text(encoding="utf-8")
|
|
except (OSError, UnicodeError) as error:
|
|
raise Refusal("include-context-ambiguous", f"cannot read {path}: {error}") from error
|
|
for include in re.findall(r'^\s*#\s*include\s*"([^"]+)"', text, re.MULTILINE):
|
|
candidates = (
|
|
path.parent / include,
|
|
repo_root / include,
|
|
repo_root / "source" / include,
|
|
)
|
|
resolved = next(
|
|
(
|
|
candidate.resolve()
|
|
for candidate in candidates
|
|
if candidate.is_file() and _inside(candidate, repo_root)
|
|
),
|
|
None,
|
|
)
|
|
if resolved is not None and resolved not in seen:
|
|
pending.append(resolved)
|
|
return sorted(visited)
|
|
|
|
|
|
def _has_function_declaration(text: str, identifier: str) -> bool:
|
|
masked = _mask_noncode(text)
|
|
token = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
|
for match in token.finditer(masked):
|
|
index = match.end()
|
|
while index < len(masked) and masked[index].isspace():
|
|
index += 1
|
|
if index >= len(masked) or masked[index] != "(":
|
|
continue
|
|
close = _matching_delimiter(masked, index, "(", ")")
|
|
after = close + 1
|
|
while after < len(masked) and masked[after].isspace():
|
|
after += 1
|
|
if after >= len(masked) or masked[after] not in ";{":
|
|
continue
|
|
line_start = masked.rfind("\n", 0, match.start()) + 1
|
|
prefix = masked[line_start:match.start()].strip()
|
|
if (
|
|
prefix
|
|
and not prefix.startswith("#")
|
|
and not re.search(r"\b(if|for|while|switch|return|sizeof)\s*$", prefix)
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _has_data_declaration(text: str, identifier: str) -> bool:
|
|
masked = _mask_noncode(text)
|
|
for statement in masked.split(";")[:-1]:
|
|
if not re.search(r"\b" + re.escape(identifier) + r"\b", statement):
|
|
continue
|
|
tail = statement.rsplit("}", 1)[-1].rsplit("{", 1)[-1].strip()
|
|
if not tail or tail.startswith("#") or "(" in tail:
|
|
continue
|
|
if re.match(r"^(return|if|for|while|switch)\b", tail):
|
|
continue
|
|
before = tail.split(identifier, 1)[0]
|
|
if re.search(
|
|
r"\b(extern|static|const|volatile|struct|union|enum|void|char|short|"
|
|
r"int|long|float|double|signed|unsigned|boolean|real)\b",
|
|
before,
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _function_declaration_texts(text: str, identifier: str) -> List[str]:
|
|
masked = _mask_noncode(text)
|
|
token = re.compile(r"\b" + re.escape(identifier) + r"\b")
|
|
declarations: List[str] = []
|
|
for match in token.finditer(masked):
|
|
index = match.end()
|
|
while index < len(masked) and masked[index].isspace():
|
|
index += 1
|
|
if index >= len(masked) or masked[index] != "(":
|
|
continue
|
|
close = _matching_delimiter(masked, index, "(", ")")
|
|
after = close + 1
|
|
while after < len(masked) and masked[after].isspace():
|
|
after += 1
|
|
if after >= len(masked) or masked[after] != ";":
|
|
continue
|
|
start = _definition_start(masked, match.start())
|
|
prefix = masked[start:match.start()].strip()
|
|
if (
|
|
not prefix
|
|
or prefix.startswith("#")
|
|
or any(character in prefix for character in "{}=")
|
|
or re.search(r"\b(if|for|while|switch|return|sizeof)\s*$", prefix)
|
|
):
|
|
continue
|
|
declarations.append(text[start:after + 1].strip())
|
|
return declarations
|
|
|
|
|
|
def _data_declaration_texts(text: str, identifier: str) -> List[str]:
|
|
masked = _mask_noncode(text)
|
|
declarations: List[str] = []
|
|
statement_start = 0
|
|
for match in re.finditer(";", masked):
|
|
statement_end = match.end()
|
|
masked_statement = masked[statement_start:statement_end]
|
|
if re.search(r"\b" + re.escape(identifier) + r"\b", masked_statement):
|
|
brace = max(
|
|
masked_statement.rfind("{"),
|
|
masked_statement.rfind("}"),
|
|
)
|
|
local_start = brace + 1 if brace >= 0 else 0
|
|
candidate_mask = masked_statement[local_start:].strip()
|
|
candidate_text = text[
|
|
statement_start + local_start:statement_end
|
|
].strip()
|
|
before = candidate_mask.split(identifier, 1)[0]
|
|
if (
|
|
candidate_mask
|
|
and not candidate_mask.startswith("#")
|
|
and "(" not in candidate_mask
|
|
and "=" not in before
|
|
and re.search(
|
|
r"\b(extern|static|const|volatile|struct|union|enum|void|"
|
|
r"char|short|int|long|float|double|signed|unsigned|boolean|"
|
|
r"real)\b",
|
|
before,
|
|
)
|
|
):
|
|
declarations.append(candidate_text)
|
|
statement_start = statement_end
|
|
return declarations
|
|
|
|
|
|
def _resolve_unique_header_declaration(
|
|
repo_root: Path, symbol_name: str, kind: str
|
|
) -> Optional[Dict[str, str]]:
|
|
identifier = _c_identifier(symbol_name)
|
|
candidates: List[Dict[str, str]] = []
|
|
source_root = repo_root / "source"
|
|
if not source_root.is_dir():
|
|
return None
|
|
for header in sorted(source_root.rglob("*.h")):
|
|
try:
|
|
raw = header.read_bytes()
|
|
text = raw.decode("utf-8")
|
|
except (OSError, UnicodeError) as error:
|
|
raise Refusal(
|
|
"recipient-declaration-ambiguous",
|
|
f"cannot inspect repository header {header}: {error}",
|
|
) from error
|
|
if not re.search(r"\b" + re.escape(identifier) + r"\b", _mask_noncode(text)):
|
|
continue
|
|
declarations = (
|
|
_function_declaration_texts(text, identifier)
|
|
if kind == "function"
|
|
else _data_declaration_texts(text, identifier)
|
|
)
|
|
for declaration in declarations:
|
|
candidates.append(
|
|
{
|
|
"symbol": symbol_name,
|
|
"text": declaration,
|
|
"source": str(header.relative_to(repo_root)).replace("\\", "/"),
|
|
"sha256": _sha256_bytes(raw),
|
|
}
|
|
)
|
|
unique_texts = {item["text"] for item in candidates}
|
|
if not candidates:
|
|
return None
|
|
if len(unique_texts) != 1:
|
|
sources = ", ".join(sorted({item["source"] for item in candidates}))
|
|
raise Refusal(
|
|
"recipient-declaration-ambiguous",
|
|
f"conflicting header declarations for {symbol_name!r}: {sources}",
|
|
)
|
|
return min(candidates, key=lambda item: item["source"])
|
|
|
|
|
|
def _load_verified_declarations(
|
|
repo_root: Path,
|
|
manifest_path: Optional[Path],
|
|
) -> Dict[str, Dict[str, str]]:
|
|
if manifest_path is None:
|
|
return {}
|
|
value = _read_json(manifest_path)
|
|
if not isinstance(value, dict) or not isinstance(value.get("declarations"), list):
|
|
raise Refusal(
|
|
"invalid-declaration-manifest",
|
|
"manifest must contain a declarations list",
|
|
)
|
|
result: Dict[str, Dict[str, str]] = {}
|
|
for item in value["declarations"]:
|
|
if not isinstance(item, dict):
|
|
raise Refusal("invalid-declaration-manifest", "declaration must be an object")
|
|
symbol = item.get("symbol")
|
|
text = item.get("text")
|
|
relative_source = item.get("source")
|
|
expected_hash = item.get("sha256")
|
|
if not all(isinstance(value, str) for value in (
|
|
symbol, text, relative_source, expected_hash
|
|
)):
|
|
raise Refusal(
|
|
"invalid-declaration-manifest",
|
|
"symbol, text, source, and sha256 must be strings",
|
|
)
|
|
if symbol in result:
|
|
raise Refusal(
|
|
"invalid-declaration-manifest",
|
|
f"duplicate declaration for {symbol!r}",
|
|
)
|
|
source = (repo_root / relative_source).resolve()
|
|
if not _inside(source, repo_root) or source.suffix.lower() != ".h":
|
|
raise Refusal(
|
|
"invalid-declaration-manifest",
|
|
f"declaration source is not a repository header: {relative_source!r}",
|
|
)
|
|
try:
|
|
raw = source.read_bytes()
|
|
header_text = raw.decode("utf-8")
|
|
except (OSError, UnicodeError) as error:
|
|
raise Refusal(
|
|
"invalid-declaration-manifest",
|
|
f"cannot read declaration header: {error}",
|
|
) from error
|
|
if _sha256_bytes(raw) != expected_hash:
|
|
raise Refusal(
|
|
"declaration-source-changed",
|
|
f"header hash changed for {relative_source!r}",
|
|
)
|
|
if text not in header_text:
|
|
raise Refusal(
|
|
"declaration-not-verbatim",
|
|
f"declaration is not verbatim in {relative_source!r}",
|
|
)
|
|
identifier = _c_identifier(symbol)
|
|
if (
|
|
not re.search(r"\b" + re.escape(identifier) + r"\b", _mask_noncode(text))
|
|
or not text.strip().endswith(";")
|
|
or "{" in _mask_noncode(text)
|
|
or "}" in _mask_noncode(text)
|
|
or "#" in _mask_noncode(text)
|
|
):
|
|
raise Refusal(
|
|
"invalid-declaration-manifest",
|
|
f"unsafe declaration text for {symbol!r}",
|
|
)
|
|
result[symbol] = {
|
|
"text": text.strip(),
|
|
"source": relative_source.replace("\\", "/"),
|
|
"sha256": expected_hash,
|
|
}
|
|
return result
|
|
|
|
|
|
def _recipient_context(
|
|
repo_root: Path,
|
|
recipient_path: Path,
|
|
recipient_symbols: Mapping[str, str],
|
|
declarations: Mapping[str, Mapping[str, str]],
|
|
) -> Tuple[List[Mapping[str, str]], List[str]]:
|
|
closure = _quoted_include_closure(repo_root, recipient_path)
|
|
context_parts = []
|
|
for path in closure:
|
|
try:
|
|
context_parts.append(path.read_text(encoding="utf-8"))
|
|
except (OSError, UnicodeError) as error:
|
|
raise Refusal("include-context-ambiguous", f"cannot read {path}: {error}") from error
|
|
context = "\n".join(context_parts)
|
|
insertions: List[Mapping[str, str]] = []
|
|
used_manifest_symbols: List[str] = []
|
|
for symbol_name, kind in sorted(recipient_symbols.items()):
|
|
identifier = _c_identifier(symbol_name)
|
|
present = (
|
|
_has_function_declaration(context, identifier)
|
|
if kind == "function"
|
|
else _has_data_declaration(context, identifier)
|
|
)
|
|
if present:
|
|
continue
|
|
declaration = declarations.get(symbol_name)
|
|
if declaration is None:
|
|
declaration = _resolve_unique_header_declaration(
|
|
repo_root, symbol_name, kind
|
|
)
|
|
if declaration is None:
|
|
raise Refusal(
|
|
"recipient-declaration-missing",
|
|
f"no explicit {kind} declaration for {symbol_name!r} in recipient "
|
|
"quoted-include context or a unique repository header",
|
|
)
|
|
insertion = dict(declaration)
|
|
insertion["symbol"] = symbol_name
|
|
insertions.append(insertion)
|
|
used_manifest_symbols.append(symbol_name)
|
|
unused = sorted(set(declarations) - set(used_manifest_symbols))
|
|
if unused:
|
|
raise Refusal(
|
|
"unused-declaration",
|
|
f"manifest contains unnecessary declarations: {', '.join(unused)}",
|
|
)
|
|
return insertions, [str(path.relative_to(repo_root)).replace("\\", "/") for path in closure]
|
|
|
|
|
|
def _compose_fragment(
|
|
transformed_source: str, declarations: Sequence[Mapping[str, str]]
|
|
) -> str:
|
|
parts: List[str] = []
|
|
if declarations:
|
|
parts.append("/* Verified declarations copied from repository headers. */")
|
|
for declaration in declarations:
|
|
parts.append(
|
|
f"/* {declaration['symbol']}: {declaration['source']} "
|
|
f"sha256={declaration['sha256']} */\n{declaration['text']}"
|
|
)
|
|
parts.append(transformed_source.rstrip())
|
|
return "\n\n".join(parts) + "\n"
|
|
|
|
|
|
def _parameter_identifiers(function_source: str) -> List[str]:
|
|
masked = _mask_noncode(function_source)
|
|
opening = masked.find("(")
|
|
if opening < 0:
|
|
return []
|
|
closing = _matching_delimiter(masked, opening, "(", ")")
|
|
parameters = masked[opening + 1:closing]
|
|
parts: List[str] = []
|
|
start = 0
|
|
depth = 0
|
|
for index, character in enumerate(parameters):
|
|
if character in "([":
|
|
depth += 1
|
|
elif character in ")]":
|
|
depth -= 1
|
|
elif character == "," and depth == 0:
|
|
parts.append(parameters[start:index])
|
|
start = index + 1
|
|
parts.append(parameters[start:])
|
|
|
|
type_words = {
|
|
"void", "char", "short", "int", "long", "float", "double",
|
|
"signed", "unsigned", "const", "volatile", "struct", "union",
|
|
"enum", "boolean", "real",
|
|
}
|
|
identifiers: List[str] = []
|
|
for part in parts:
|
|
tokens = re.findall(r"\b[A-Za-z_][A-Za-z0-9_]*\b", part)
|
|
if not tokens or (len(tokens) == 1 and tokens[0] == "void"):
|
|
continue
|
|
candidate = tokens[-1]
|
|
if candidate not in type_words:
|
|
identifiers.append(candidate)
|
|
return identifiers
|
|
|
|
|
|
def _review_lints(
|
|
donor_source: str,
|
|
donor_function: str,
|
|
recipient_function: str,
|
|
declarations: Sequence[Mapping[str, str]],
|
|
) -> List[Dict[str, str]]:
|
|
lints: List[Dict[str, str]] = []
|
|
donor_domain = _c_identifier(donor_function).split("_", 1)[0]
|
|
recipient_domain = _c_identifier(recipient_function).split("_", 1)[0]
|
|
if donor_domain != recipient_domain:
|
|
for parameter in _parameter_identifiers(donor_source):
|
|
if parameter == donor_domain or parameter.startswith(donor_domain + "_"):
|
|
lints.append(
|
|
{
|
|
"code": "donor-domain-parameter-name",
|
|
"severity": "REVIEW_REQUIRED",
|
|
"detail": (
|
|
f"parameter {parameter!r} retains donor domain "
|
|
f"{donor_domain!r}; a human must choose any semantic "
|
|
"recipient name because relocations cannot prove it"
|
|
),
|
|
}
|
|
)
|
|
for declaration in declarations:
|
|
lints.append(
|
|
{
|
|
"code": "generated-declaration-redundancy-review",
|
|
"severity": "REVIEW_REQUIRED",
|
|
"detail": (
|
|
f"{declaration['symbol']!r} was copied from "
|
|
f"{declaration['source']}; verify the full compiler/forced-include "
|
|
"context does not already provide it and prefer the real header "
|
|
"include over a redundant local prototype"
|
|
),
|
|
}
|
|
)
|
|
return lints
|
|
|
|
|
|
def _review_markdown(packet: Mapping[str, Any]) -> str:
|
|
pair = packet["pair"]
|
|
donor = pair["donor"]
|
|
recipient = pair["recipient"]
|
|
events = packet["derived_substitutions"]["events"]
|
|
substitution_lines = (
|
|
"\n".join(f"- `{old}` -> `{new}`" for old, new in events)
|
|
if events
|
|
else "- None (target relocation destinations are identical)."
|
|
)
|
|
lint_lines = (
|
|
"\n".join(
|
|
f"- **{item['severity']} / `{item['code']}`**: {item['detail']}"
|
|
for item in packet["review_lints"]
|
|
)
|
|
if packet["review_lints"]
|
|
else "- None."
|
|
)
|
|
return f"""# Exact-shape transfer review packet
|
|
|
|
Verdict: **CANDIDATE GENERATED — NOT ACCEPTED**
|
|
|
|
- Donor: `{donor['unit']}:{donor['function']}`
|
|
- Recipient: `{recipient['unit']}:{recipient['function']}`
|
|
- Candidate bytes claimed by atlas: `{pair['size']}`
|
|
- Candidate patch: `candidate.patch` (unapplied)
|
|
- Frozen pre-edit TU/object baseline: `frozen_tu_baseline.json`
|
|
- Production source modified: **no**
|
|
- Matching/config/ledger changes: **none**
|
|
|
|
## Target-derived substitutions
|
|
|
|
{substitution_lines}
|
|
|
|
These substitutions were independently derived from the ordered recipient
|
|
January target relocations. The atlas event list matched exactly. The generated
|
|
body is the donor source with token substitutions only.
|
|
|
|
## Review lints
|
|
|
|
{lint_lines}
|
|
|
|
Lints are never auto-fixed because parameter semantics and the complete compiler
|
|
include context are not derivable from recipient target relocations.
|
|
|
|
## Required acceptance work (not run by this executor)
|
|
|
|
1. Apply the patch only in a disposable human review worktree.
|
|
2. Compile the recipient translation unit with the campaign's pinned XDK flags.
|
|
3. Require hardened cross-name `section_infos_equal` for the candidate and
|
|
recipient target symbols.
|
|
4. Require a zero-regression whole-TU manifest comparison.
|
|
5. Require unchanged whole-object data/BSS/ownership fingerprints.
|
|
6. Review the source diff and only then decide whether to commit in a human lane.
|
|
|
|
This packet grants no matching credit and cannot be integrated automatically.
|
|
"""
|
|
|
|
|
|
def _freeze_tu_baseline(
|
|
unit: str,
|
|
recipient_function: str,
|
|
target_path: Path,
|
|
base_path: Path,
|
|
) -> Dict[str, Any]:
|
|
if not base_path.is_file():
|
|
raise Refusal("base-object-missing", f"base object does not exist: {base_path}")
|
|
try:
|
|
target_fingerprint = _object_fingerprint(target_path, {}, {})
|
|
base_fingerprint = _object_fingerprint(base_path, {}, {})
|
|
target_obj = load(target_path)
|
|
base_obj = load(base_path)
|
|
except (OSError, CoffError, GateError) as error:
|
|
raise Refusal(
|
|
"frozen-tu-baseline-failed",
|
|
f"cannot fingerprint {unit}: {error}",
|
|
) from error
|
|
if recipient_function in base_fingerprint["functions"]:
|
|
raise Refusal(
|
|
"recipient-base-already-defines",
|
|
f"pre-edit base object already defines {recipient_function!r}",
|
|
)
|
|
|
|
comparisons: Dict[str, str] = {}
|
|
strict_exact: List[str] = []
|
|
for name in sorted(target_fingerprint["functions"]):
|
|
if name not in base_fingerprint["functions"]:
|
|
comparisons[name] = "ABSENT_IN_BASE"
|
|
continue
|
|
try:
|
|
exact = section_infos_equal(
|
|
section_info(target_obj, name),
|
|
section_info(base_obj, name),
|
|
)
|
|
except CoffError as error:
|
|
raise Refusal(
|
|
"frozen-tu-baseline-failed",
|
|
f"cannot compare pre-edit sibling {unit}:{name}: {error}",
|
|
) from error
|
|
state = "STRICT_EXACT" if exact else "NONEXACT"
|
|
comparisons[name] = state
|
|
if exact:
|
|
strict_exact.append(name)
|
|
base_only = sorted(
|
|
set(base_fingerprint["functions"]) - set(target_fingerprint["functions"])
|
|
)
|
|
return {
|
|
"schema_version": 1,
|
|
"purpose": (
|
|
"pre-edit frozen baseline only; does not accept the generated candidate"
|
|
),
|
|
"unit": unit,
|
|
"recipient_function": recipient_function,
|
|
"recipient_absent_in_base": True,
|
|
"target_path": str(target_path.resolve()),
|
|
"base_path": str(base_path.resolve()),
|
|
"target": target_fingerprint,
|
|
"base": base_fingerprint,
|
|
"pre_edit_function_states": comparisons,
|
|
"pre_edit_strict_exact_functions": strict_exact,
|
|
"base_only_functions": base_only,
|
|
"post_edit_requirements": {
|
|
"recipient_hardened_comparator": "REQUIRED",
|
|
"pre_edit_strict_exact_siblings_unchanged": "REQUIRED_ZERO_REGRESSIONS",
|
|
"base_non_code_sections_unchanged": "REQUIRED",
|
|
"base_symbol_ownership_unchanged_except_reviewed_candidate": "REQUIRED",
|
|
},
|
|
"matching_credit_granted": False,
|
|
}
|
|
|
|
|
|
def plan_candidate(
|
|
*,
|
|
repo_root: Path,
|
|
atlas_path: Path,
|
|
target_root: Path,
|
|
base_root: Optional[Path] = None,
|
|
recipient_unit: str,
|
|
recipient_function: str,
|
|
donor_unit: Optional[str] = None,
|
|
donor_function: Optional[str] = None,
|
|
declaration_manifest: Optional[Path] = None,
|
|
) -> Dict[str, Any]:
|
|
"""Build an in-memory candidate packet without writing any files."""
|
|
repo_root = repo_root.resolve()
|
|
if not (repo_root / ".git").exists():
|
|
raise Refusal("invalid-repository", f"not a Git worktree: {repo_root}")
|
|
atlas = _read_json(atlas_path)
|
|
pair = _select_pair(
|
|
atlas,
|
|
recipient_unit,
|
|
recipient_function,
|
|
donor_unit,
|
|
donor_function,
|
|
)
|
|
_validate_pair_policy(pair)
|
|
|
|
donor = pair["donor"]
|
|
recipient = pair["recipient"]
|
|
donor_source_path = _checked_repo_path(repo_root, donor["unit"], ".c")
|
|
recipient_source_path = _checked_repo_path(repo_root, recipient["unit"], ".c")
|
|
if not recipient_source_path.is_file():
|
|
raise Refusal(
|
|
"recipient-source-missing",
|
|
f"recipient TU does not exist: {recipient_source_path}",
|
|
)
|
|
try:
|
|
recipient_raw = recipient_source_path.read_bytes()
|
|
recipient_text = recipient_raw.decode("utf-8")
|
|
except (OSError, UnicodeError) as error:
|
|
raise Refusal(
|
|
"recipient-source-missing",
|
|
f"cannot read recipient TU: {error}",
|
|
) from error
|
|
recipient_identifier = _c_identifier(recipient["function"])
|
|
if _function_definitions(recipient_text, recipient_identifier):
|
|
raise Refusal(
|
|
"recipient-source-already-defines",
|
|
f"recipient TU already defines {recipient_identifier!r}",
|
|
)
|
|
|
|
donor_target_path = _checked_repo_path(
|
|
target_root.resolve(), donor["unit"], ".obj"
|
|
)
|
|
recipient_target_path = _checked_repo_path(
|
|
target_root.resolve(), recipient["unit"], ".obj"
|
|
)
|
|
donor_target = _load_target_function(donor_target_path, donor["function"])
|
|
recipient_target = _load_target_function(
|
|
recipient_target_path, recipient["function"]
|
|
)
|
|
frozen_baseline = None
|
|
if base_root is not None:
|
|
recipient_base_path = _checked_repo_path(
|
|
base_root.resolve(), recipient["unit"], ".obj"
|
|
)
|
|
frozen_baseline = _freeze_tu_baseline(
|
|
recipient["unit"],
|
|
recipient["function"],
|
|
recipient_target_path,
|
|
recipient_base_path,
|
|
)
|
|
derived = _derive_substitutions(pair, donor_target, recipient_target)
|
|
donor_source = _extract_donor_source(donor_source_path, donor["function"])
|
|
prototype_proof = _prove_prototype_compatibility(
|
|
repo_root,
|
|
recipient_source_path,
|
|
donor_source,
|
|
donor["function"],
|
|
recipient["function"],
|
|
)
|
|
transformed = _transform_source(
|
|
donor_source,
|
|
donor["function"],
|
|
recipient["function"],
|
|
derived,
|
|
)
|
|
|
|
declarations = _load_verified_declarations(repo_root, declaration_manifest)
|
|
insertions, include_context = _recipient_context(
|
|
repo_root,
|
|
recipient_source_path,
|
|
derived["recipient_symbols"],
|
|
declarations,
|
|
)
|
|
fragment = _compose_fragment(transformed, insertions)
|
|
review_lints = _review_lints(
|
|
donor_source["source"],
|
|
donor["function"],
|
|
recipient["function"],
|
|
insertions,
|
|
)
|
|
old_lines = recipient_text.splitlines(keepends=True)
|
|
separator = "" if not recipient_text or recipient_text.endswith("\n") else "\n"
|
|
candidate_text = recipient_text + separator + "\n" + fragment
|
|
patch = "".join(
|
|
difflib.unified_diff(
|
|
old_lines,
|
|
candidate_text.splitlines(keepends=True),
|
|
fromfile=f"a/{recipient['unit']}.c",
|
|
tofile=f"b/{recipient['unit']}.c",
|
|
)
|
|
)
|
|
if not patch:
|
|
raise Refusal("empty-candidate", "candidate patch is unexpectedly empty")
|
|
|
|
pair_snapshot = json.loads(json.dumps(pair))
|
|
packet: Dict[str, Any] = {
|
|
"schema_version": 1,
|
|
"verdict": "CANDIDATE_GENERATED_NOT_ACCEPTED",
|
|
"pair": pair_snapshot,
|
|
"repository": {
|
|
"root": str(repo_root),
|
|
"recipient_source": {
|
|
"path": str(recipient_source_path),
|
|
"sha256": _sha256_bytes(recipient_raw),
|
|
},
|
|
},
|
|
"donor_source": {
|
|
key: value for key, value in donor_source.items() if key != "source"
|
|
},
|
|
"target_objects": {
|
|
"donor": {
|
|
"path": donor_target["path"],
|
|
"sha256": donor_target["sha256"],
|
|
},
|
|
"recipient": {
|
|
"path": recipient_target["path"],
|
|
"sha256": recipient_target["sha256"],
|
|
},
|
|
},
|
|
"frozen_tu_baseline": (
|
|
{
|
|
"sha256": _sha256_bytes(_json_bytes(frozen_baseline)),
|
|
"file": "frozen_tu_baseline.json",
|
|
"pre_edit_strict_exact_function_count": len(
|
|
frozen_baseline["pre_edit_strict_exact_functions"]
|
|
),
|
|
"pre_edit_non_code_section_count": len(
|
|
frozen_baseline["base"]["non_code_sections"]
|
|
),
|
|
}
|
|
if frozen_baseline is not None
|
|
else None
|
|
),
|
|
"derived_substitutions": {
|
|
"events": derived["events"],
|
|
"identifier_map": derived["identifier_map"],
|
|
"source": "ordered donor/recipient January target COFF relocations",
|
|
},
|
|
"declarations_inserted": insertions,
|
|
"prototype_compatibility": prototype_proof,
|
|
"review_lints": review_lints,
|
|
"recipient_include_context": include_context,
|
|
"generation_checks": {
|
|
"atlas_proofs_all_true": True,
|
|
"meaningful_size_over_16": True,
|
|
"padded_and_meaningful_sizes_equal": True,
|
|
"normalized_target_bytes_equal": True,
|
|
"relocation_address_type_order_equal": True,
|
|
"relocation_addends_equal": True,
|
|
"storage_classes_equal": True,
|
|
"prologues_equal": True,
|
|
"substitutions_injective": True,
|
|
"atlas_events_match_derived_events": True,
|
|
"donor_source_definition_unique": True,
|
|
"recipient_prototype_return_abi_compatible": True,
|
|
"statement_order_changed": False,
|
|
"production_source_modified": False,
|
|
"review_lint_count": len(review_lints),
|
|
},
|
|
"acceptance_gates": {
|
|
"hardened_cross_name_section_infos_equal": "NOT_RUN_REQUIRED",
|
|
"whole_tu_zero_regression": (
|
|
"NOT_RUN_REQUIRED_BASELINE_FROZEN"
|
|
if frozen_baseline is not None
|
|
else "NOT_RUN_REQUIRED_BASELINE_MISSING"
|
|
),
|
|
"whole_object_data_bss_ownership_unchanged": (
|
|
"NOT_RUN_REQUIRED_BASELINE_FROZEN"
|
|
if frozen_baseline is not None
|
|
else "NOT_RUN_REQUIRED_BASELINE_MISSING"
|
|
),
|
|
},
|
|
"matching_credit_granted": False,
|
|
"automatic_integration_performed": False,
|
|
"candidate": {
|
|
"fragment_sha256": _sha256_bytes(fragment.encode("utf-8")),
|
|
"patch_sha256": _sha256_bytes(patch.encode("utf-8")),
|
|
},
|
|
}
|
|
return {
|
|
"packet": packet,
|
|
"fragment": fragment,
|
|
"patch": patch,
|
|
"frozen_baseline": frozen_baseline,
|
|
"review": _review_markdown(packet),
|
|
}
|
|
|
|
|
|
def emit_candidate(result: Mapping[str, Any], output_dir: Path, repo_root: Path) -> None:
|
|
"""Write one new out-of-repository review directory."""
|
|
output_dir = output_dir.resolve()
|
|
repo_root = repo_root.resolve()
|
|
if _inside(output_dir, repo_root):
|
|
raise Refusal(
|
|
"output-inside-repository",
|
|
"review output must be outside the repository",
|
|
)
|
|
if output_dir.exists():
|
|
raise Refusal(
|
|
"output-already-exists",
|
|
"review output directory must not already exist",
|
|
)
|
|
if result.get("frozen_baseline") is None:
|
|
raise Refusal(
|
|
"frozen-tu-baseline-missing",
|
|
"candidate emission requires a pre-edit recipient base object",
|
|
)
|
|
hashed_inputs = [
|
|
result["packet"]["repository"]["recipient_source"],
|
|
result["packet"]["donor_source"],
|
|
result["packet"]["target_objects"]["donor"],
|
|
result["packet"]["target_objects"]["recipient"],
|
|
{
|
|
"path": result["frozen_baseline"]["base_path"],
|
|
"sha256": result["frozen_baseline"]["base"]["object_sha256"],
|
|
},
|
|
]
|
|
for item in hashed_inputs:
|
|
path = Path(item["path"])
|
|
try:
|
|
current_hash = _sha256_bytes(path.read_bytes())
|
|
except OSError as error:
|
|
raise Refusal(
|
|
"input-changed-before-emission",
|
|
f"cannot re-read {path}: {error}",
|
|
) from error
|
|
if current_hash != item["sha256"]:
|
|
raise Refusal(
|
|
"input-changed-before-emission",
|
|
f"input changed after planning: {path}",
|
|
)
|
|
staging: Optional[Path] = None
|
|
try:
|
|
output_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
staging = Path(
|
|
tempfile.mkdtemp(
|
|
prefix=f".{output_dir.name}.tmp-",
|
|
dir=str(output_dir.parent),
|
|
)
|
|
)
|
|
(staging / "candidate.c").write_text(
|
|
result["fragment"], encoding="utf-8", newline="\n"
|
|
)
|
|
(staging / "candidate.patch").write_text(
|
|
result["patch"], encoding="utf-8", newline="\n"
|
|
)
|
|
(staging / "review_packet.json").write_bytes(
|
|
_json_bytes(result["packet"])
|
|
)
|
|
(staging / "frozen_tu_baseline.json").write_bytes(
|
|
_json_bytes(result["frozen_baseline"])
|
|
)
|
|
(staging / "REVIEW.md").write_text(
|
|
result["review"], encoding="utf-8", newline="\n"
|
|
)
|
|
staging.replace(output_dir)
|
|
staging = None
|
|
except OSError as error:
|
|
raise Refusal("output-write-failed", str(error)) from error
|
|
finally:
|
|
if staging is not None:
|
|
shutil.rmtree(staging, ignore_errors=True)
|
|
|
|
|
|
def _parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Generate an unapplied exact-shape transfer candidate and review "
|
|
"packet; never modifies the repository or grants matching credit."
|
|
)
|
|
)
|
|
parser.add_argument("--repo-root", type=Path, required=True)
|
|
parser.add_argument("--atlas", type=Path, required=True)
|
|
parser.add_argument(
|
|
"--target-root",
|
|
type=Path,
|
|
required=True,
|
|
help="root containing <unit>.obj January target objects",
|
|
)
|
|
parser.add_argument(
|
|
"--base-root",
|
|
type=Path,
|
|
required=True,
|
|
help="root containing untouched pre-edit <unit>.obj base objects",
|
|
)
|
|
parser.add_argument("--recipient-unit", required=True)
|
|
parser.add_argument("--recipient-function", required=True)
|
|
parser.add_argument("--donor-unit")
|
|
parser.add_argument("--donor-function")
|
|
parser.add_argument("--declaration-manifest", type=Path)
|
|
parser.add_argument(
|
|
"--output-dir",
|
|
type=Path,
|
|
required=True,
|
|
help="new directory outside repo; candidate is never applied",
|
|
)
|
|
return parser
|
|
|
|
|
|
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
arguments = _parser().parse_args(argv)
|
|
try:
|
|
result = plan_candidate(
|
|
repo_root=arguments.repo_root,
|
|
atlas_path=arguments.atlas,
|
|
target_root=arguments.target_root,
|
|
base_root=arguments.base_root,
|
|
recipient_unit=arguments.recipient_unit,
|
|
recipient_function=arguments.recipient_function,
|
|
donor_unit=arguments.donor_unit,
|
|
donor_function=arguments.donor_function,
|
|
declaration_manifest=arguments.declaration_manifest,
|
|
)
|
|
emit_candidate(result, arguments.output_dir, arguments.repo_root)
|
|
except Refusal as refusal:
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"verdict": "REFUSED",
|
|
"refusal": refusal.as_dict(),
|
|
"candidate_emitted": False,
|
|
"production_source_modified": False,
|
|
"matching_credit_granted": False,
|
|
},
|
|
indent=2,
|
|
sort_keys=True,
|
|
),
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"verdict": result["packet"]["verdict"],
|
|
"output_dir": str(arguments.output_dir.resolve()),
|
|
"production_source_modified": False,
|
|
"matching_credit_granted": False,
|
|
},
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|