"""Conservative progress corrections for proven objdiff false negatives. Entries in config/semantic_matches.json are never trusted on their own. Each entry is re-verified against the current target and rebuilt COFF objects before its function is credited. This keeps the ordinary objdiff report authoritative except where a stricter semantic relocation comparison proves exact equality. """ import json from pathlib import Path from typing import Any, Dict, List from .coff_compare import ( CoffError, IMAGE_SCN_CNT_CODE, image_symbol_addresses, load, section_info, section_info_by_number, section_info_resolved, section_info_source_relative, section_infos_equal, ) class SemanticProgressError(RuntimeError): pass def _verify_local_label_continuation(target, base, label_name, owner_name): """Prove that an objdiff ``$L`` pseudo-function is an exact continuation. csplit can expose a compiler-local jump-table label as an external, function-typed symbol while the rebuilt COFF emits a differently named static label at the same offset. The label is creditable only when the complete owning COMDAT is semantically exact, both labels are unique at that offset, and an internal relocation in the owner proves the offset is an actual encoded destination. Any ambiguity fails closed. """ if not label_name.startswith("$"): raise SemanticProgressError( f"local-label match does not name a local label: {label_name}") target_labels = [ item for item in target["symbols"] if item["name"] == label_name and item["section"] > 0 ] if len(target_labels) != 1: raise SemanticProgressError( f"expected one target local label {label_name}, found {len(target_labels)}") target_label = target_labels[0] if target_label["value"] <= 0: raise SemanticProgressError( f"target local label is not a continuation: {label_name}") target_owners = [ item for item in target["symbols"] if item["name"] == owner_name and item["section"] > 0 ] base_owners = [ item for item in base["symbols"] if item["name"] == owner_name and item["section"] > 0 ] if len(target_owners) != 1 or len(base_owners) != 1: raise SemanticProgressError( f"expected unique local-label owner {owner_name}, found " f"{len(target_owners)}/{len(base_owners)}") target_owner = target_owners[0] base_owner = base_owners[0] if target_owner["value"] != 0 or base_owner["value"] != 0: raise SemanticProgressError( f"local-label owner is not a COMDAT entry: {owner_name}") if target_label["section"] != target_owner["section"]: raise SemanticProgressError( f"target local label is outside owner {owner_name}: {label_name}") target_info = section_info(target, owner_name) base_info = section_info(base, owner_name) if not section_infos_equal(target_info, base_info): raise SemanticProgressError( f"local-label owner is no longer exact: {owner_name}") offset = target_label["value"] base_labels = [ item for item in base["symbols"] if item["section"] == base_owner["section"] and item["value"] == offset and item["name"].startswith("$") ] if len(base_labels) != 1: raise SemanticProgressError( f"expected one base local destination at {offset:#x}, found {len(base_labels)}") if not any( relocation["target"] == ["internal", offset] for relocation in target_info["relocations"] ): raise SemanticProgressError( f"target local label has no proven internal relocation: {label_name}") return target_info def _percent(numerator: int, denominator: int) -> float: return 100.0 * numerator / denominator if denominator else 0.0 def _credit(measures: Dict[str, Any], code_bytes: int) -> None: measures["matched_code"] = int(measures.get("matched_code", 0)) + code_bytes measures["matched_functions"] = int(measures.get("matched_functions", 0)) + 1 measures["matched_code_percent"] = _percent( measures["matched_code"], int(measures.get("total_code", 0)) ) measures["matched_functions_percent"] = _percent( measures["matched_functions"], int(measures.get("total_functions", 0)) ) def _debit(measures: Dict[str, Any], code_bytes: int) -> None: measures["matched_code"] = int(measures.get("matched_code", 0)) - code_bytes measures["matched_functions"] = int(measures.get("matched_functions", 0)) - 1 if measures["matched_code"] < 0 or measures["matched_functions"] < 0: raise SemanticProgressError("semantic rejection would make progress negative") measures["matched_code_percent"] = _percent( measures["matched_code"], int(measures.get("total_code", 0)) ) measures["matched_functions_percent"] = _percent( measures["matched_functions"], int(measures.get("total_functions", 0)) ) def _revoke_completion( measures: Dict[str, Any], code_bytes: int, data_bytes: int ) -> None: """Remove one previously complete unit from aggregate progress measures.""" measures["complete_code"] = int(measures.get("complete_code", 0)) - code_bytes measures["complete_data"] = int(measures.get("complete_data", 0)) - data_bytes measures["complete_units"] = int(measures.get("complete_units", 0)) - 1 if ( measures["complete_code"] < 0 or measures["complete_data"] < 0 or measures["complete_units"] < 0 ): raise SemanticProgressError("semantic rejection would make completion negative") measures["complete_code_percent"] = _percent( measures["complete_code"], int(measures.get("total_code", 0)) ) measures["complete_data_percent"] = _percent( measures["complete_data"], int(measures.get("total_data", 0)) ) def _credit_data(measures: Dict[str, Any], data_bytes: int) -> None: measures["matched_data"] = int(measures.get("matched_data", 0)) + data_bytes total_data = int(measures.get("total_data", 0)) if measures["matched_data"] > total_data: raise SemanticProgressError("semantic data credit exceeds total data") measures["matched_data_percent"] = _percent( measures["matched_data"], total_data) def revoke_incomplete_units(report: Dict[str, Any]) -> List[str]: """Revoke config-level completion when measured content is incomplete. ``metadata.complete`` originates from the manually maintained Matching label. It must not grant linked-object credit when the current report, after strict semantic corrections, still contains unmatched functions or data. This is deliberately a one-way safety gate: it can revoke a stale label, but it never promotes an object to complete. """ categories = {item["id"]: item for item in report.get("categories", [])} revoked = [] for report_unit in report.get("units", []): if not report_unit.get("metadata", {}).get("complete", False): continue unit_measures = report_unit.get("measures", {}) matched_functions = int(unit_measures.get("matched_functions", 0)) total_functions = int(unit_measures.get("total_functions", 0)) matched_data = int(unit_measures.get("matched_data", 0)) total_data = int(unit_measures.get("total_data", 0)) missing_functions = total_functions - matched_functions missing_data = total_data - matched_data if missing_functions < 0 or missing_data < 0: raise SemanticProgressError( f"unit progress exceeds totals: {report_unit.get('name', '')}" ) if missing_functions == 0 and missing_data == 0: continue if int(unit_measures.get("complete_units", 0)) != 1: raise SemanticProgressError( f"complete unit has inconsistent completion measures: " f"{report_unit.get('name', '')}" ) complete_code = int(unit_measures.get("complete_code", 0)) complete_data = int(unit_measures.get("complete_data", 0)) _revoke_completion(report["measures"], complete_code, complete_data) progress_categories = report_unit.get("metadata", {}).get( "progress_categories", [] ) if isinstance(progress_categories, str): progress_categories = [progress_categories] for category_id in progress_categories: if category_id not in categories: raise SemanticProgressError( f"progress category not found: {category_id}" ) _revoke_completion( categories[category_id]["measures"], complete_code, complete_data ) unit_measures["complete_code"] = 0 unit_measures["complete_data"] = 0 unit_measures["complete_units"] = 0 unit_measures["complete_code_percent"] = 0.0 unit_measures["complete_data_percent"] = 0.0 report_unit["metadata"]["complete"] = False revoked.append( f"{report_unit['name']} ({missing_functions} unmatched functions, " f"{missing_data} unmatched data bytes)" ) return revoked def _section_ownership_snapshot(obj: Dict[str, Any], section_name: str) -> Dict[str, Any]: sections = [section for section in obj["sections"] if section["name"] == section_name] if len(sections) != 1: raise SemanticProgressError( f"expected one {section_name} section, found {len(sections)}" ) section = sections[0] info = section_info_by_number(obj, int(section["index"])) symbols = sorted( ( { "name": symbol["name"], "value": int(symbol["value"]), "type": int(symbol["type"]), "storage": int(symbol["storage"]), } for symbol in obj["symbols"] if int(symbol["section"]) == int(section["index"]) and symbol["name"] != section_name ), key=lambda symbol: (symbol["value"], symbol["name"]), ) return { "size": int(section["size"]), "flags": int(section["flags"]), "relocation_count": int(info["relocation_count"]), "normalized_sha256": info["normalized_sha256"], "symbols": symbols, } def require_symbol_ownership_snapshots( project_root: Path, manifest_path: Path, objdiff_config_path: Path, ) -> List[str]: """Require exact COFF section ownership for admission-sensitive data. Zero-filled BSS can compare byte-exact even when symbols move or change linkage. This manifest records the complete named-symbol set for a section and validates both the csplit target and rebuilt object. It is a pure safety gate: it grants no progress credit and any drift fails closed. """ if not manifest_path.is_file(): return [] entries = json.loads(manifest_path.read_text(encoding="utf-8")) objdiff = json.loads(objdiff_config_path.read_text(encoding="utf-8")) config_units = {unit["name"]: unit for unit in objdiff.get("units", [])} validated = [] for entry in entries: unit_name = entry["unit"] section_name = entry["section"] if unit_name not in config_units: raise SemanticProgressError( f"ownership snapshot unit not found: {unit_name}" ) config_unit = config_units[unit_name] try: target = load(project_root / config_unit["target_path"]) base = load(project_root / config_unit["base_path"]) target_snapshot = _section_ownership_snapshot(target, section_name) base_snapshot = _section_ownership_snapshot(base, section_name) except (CoffError, KeyError, OSError) as error: raise SemanticProgressError( f"cannot verify ownership snapshot {unit_name}:{section_name}: {error}" ) from error expected = entry.get("snapshot", {}) if target_snapshot != expected: raise SemanticProgressError( f"target ownership snapshot changed: {unit_name}:{section_name}" ) if base_snapshot != expected: raise SemanticProgressError( f"rebuilt ownership snapshot changed: {unit_name}:{section_name}" ) validated.append( f"{unit_name}:{section_name} ({len(expected.get('symbols', []))} symbols)" ) return validated def apply_semantic_rejections( report: Dict[str, Any], semantic_report_path: Path, ) -> List[str]: """Remove objdiff credits rejected by the stricter COFF-shape audit.""" if not semantic_report_path.is_file(): return [] semantic_report = json.loads(semantic_report_path.read_text(encoding="utf-8")) report_units = {unit["name"]: unit for unit in report.get("units", [])} categories = {item["id"]: item for item in report.get("categories", [])} rejected = [] revoked_units = set() for entry in semantic_report.get("ordinary_rejected", []): unit_name = entry["unit"] function_name = entry["function"] report_unit = report_units.get(unit_name) if report_unit is None: raise SemanticProgressError(f"semantic rejection unit not found: {unit_name}") functions = [ function for function in report_unit.get("functions", []) if function.get("name") == function_name ] if len(functions) != 1 or functions[0].get("fuzzy_match_percent") != 100.0: raise SemanticProgressError( f"semantic rejection is not an objdiff exact function: " f"{unit_name}:{function_name}" ) code_bytes = int(functions[0]["size"]) _debit(report["measures"], code_bytes) _debit(report_unit["measures"], code_bytes) progress_categories = report_unit.get("metadata", {}).get( "progress_categories", [] ) if isinstance(progress_categories, str): progress_categories = [progress_categories] for category_id in progress_categories: if category_id not in categories: raise SemanticProgressError(f"progress category not found: {category_id}") _debit(categories[category_id]["measures"], code_bytes) if unit_name not in revoked_units and report_unit.get("metadata", {}).get( "complete", False ): unit_measures = report_unit["measures"] complete_code = int(unit_measures.get("complete_code", 0)) complete_data = int(unit_measures.get("complete_data", 0)) _revoke_completion(report["measures"], complete_code, complete_data) for category_id in progress_categories: _revoke_completion( categories[category_id]["measures"], complete_code, complete_data ) unit_measures["complete_code"] = 0 unit_measures["complete_data"] = 0 unit_measures["complete_units"] = 0 unit_measures["complete_code_percent"] = 0.0 unit_measures["complete_data_percent"] = 0.0 report_unit["metadata"]["complete"] = False revoked_units.add(unit_name) rejected.append( f"{unit_name}:{function_name} (-{code_bytes} code bytes, -1 function)" ) return rejected def apply_semantic_matches( report: Dict[str, Any], project_root: Path, manifest_path: Path, objdiff_config_path: Path, ) -> List[str]: """Verify and credit manifest entries that objdiff did not count exactly. Returns human-readable notes for credited entries. Missing, ambiguous, or non-equal evidence raises instead of silently inflating progress. """ if not manifest_path.is_file(): return [] entries = json.loads(manifest_path.read_text(encoding="utf-8")) objdiff = json.loads(objdiff_config_path.read_text(encoding="utf-8")) report_units = {unit["name"]: unit for unit in report.get("units", [])} config_units = {unit["name"]: unit for unit in objdiff.get("units", [])} categories = {item["id"]: item for item in report.get("categories", [])} credited = [] for entry in entries: unit_name = entry["unit"] function_name = entry["function"] if unit_name not in report_units or unit_name not in config_units: raise SemanticProgressError(f"semantic match unit not found: {unit_name}") report_unit = report_units[unit_name] functions = [ function for function in report_unit.get("functions", []) if function.get("name") == function_name ] if len(functions) != 1: raise SemanticProgressError( f"expected one report function {unit_name}:{function_name}, found {len(functions)}" ) function = functions[0] if function.get("fuzzy_match_percent") == 100.0: continue config_unit = config_units[unit_name] try: target = load(project_root / config_unit["target_path"]) base = load(project_root / config_unit["base_path"]) owner_function = entry.get("owner_function") if owner_function: target_info = _verify_local_label_continuation( target, base, function_name, owner_function) base_info = target_info else: target_info = section_info(target, function_name) # csplit may expose a source/SDK function under an anonymous # image name while VC7 emits its authentic public name. An # explicit per-unit alias is safe only because the unchanged # strict comparator below still proves the complete function # shape, including relocation destinations and addends. base_info = section_info( base, entry.get("base_function", function_name)) except (CoffError, KeyError, OSError) as error: raise SemanticProgressError( f"cannot verify semantic match {unit_name}:{function_name}: {error}" ) from error if not section_infos_equal(target_info, base_info): raise SemanticProgressError( f"semantic match is no longer exact: {unit_name}:{function_name}" ) code_bytes = int(function["size"]) _credit(report["measures"], code_bytes) _credit(report_unit["measures"], code_bytes) progress_categories = report_unit.get("metadata", {}).get("progress_categories", []) if isinstance(progress_categories, str): progress_categories = [progress_categories] for category_id in progress_categories: if category_id not in categories: raise SemanticProgressError(f"progress category not found: {category_id}") _credit(categories[category_id]["measures"], code_bytes) credited.append( f"{unit_name}:{function_name} (+{code_bytes} code bytes, +1 function)" ) return credited def _unique_defined_symbol(obj, name, description): matches = [ item for item in obj["symbols"] if item["name"] == name and item["section"] > 0 ] if len(matches) != 1: raise CoffError( f"expected one {description} {name!r}, found {len(matches)}") return matches[0] def _semantic_data_member_snapshot(owner, section, info): if int(section["flags"]) & IMAGE_SCN_CNT_CODE: raise CoffError( f"semantic data owner {owner['name']!r} names code") alignment_code = (int(section["flags"]) >> 20) & 0xF if alignment_code == 0: alignment = 1 elif 1 <= alignment_code <= 14: alignment = 1 << (alignment_code - 1) else: raise CoffError( f"invalid COFF section alignment code {alignment_code}") padded_size = (int(section["size"]) + alignment - 1) & ~(alignment - 1) return { "section": section["name"], "size": info["size"], "padded_size": padded_size, "flags": int(section["flags"]), "relocation_count": info["relocation_count"], "normalized_sha256": info["normalized_sha256"], "owner": { "value": int(owner["value"]), "type": int(owner["type"]), "storage": int(owner["storage"]), }, } def apply_semantic_data_matches( report: Dict[str, Any], project_root: Path, manifest_path: Path, objdiff_config_path: Path, symbol_manifest_path: Path, ) -> List[str]: """Verify and credit executable-split data relocation aliases. A manifest entry is accepted only when the target and rebuilt data sections have identical normalized bytes, relocation locations/types, and independently resolved destinations. A grouped entry additionally snapshots every member's flags, alignment-derived padded extent, and producer-specific owner. Its members must account for the unit's entire remaining unmatched data. An incomplete unit requires an explicit manifest opt-in so partial spans are never credited accidentally. """ if not manifest_path.is_file(): return [] entries = json.loads(manifest_path.read_text(encoding="utf-8")) objdiff = json.loads(objdiff_config_path.read_text(encoding="utf-8")) symbol_entries = json.loads(symbol_manifest_path.read_text(encoding="utf-8")) symbol_addresses = image_symbol_addresses(symbol_entries) report_units = {unit["name"]: unit for unit in report.get("units", [])} config_units = {unit["name"]: unit for unit in objdiff.get("units", [])} categories = {item["id"]: item for item in report.get("categories", [])} credited = [] for entry in entries: unit_name = entry["unit"] if unit_name not in report_units or unit_name not in config_units: raise SemanticProgressError( f"semantic data match unit not found: {unit_name}") report_unit = report_units[unit_name] config_unit = config_units[unit_name] if ( not config_unit.get("metadata", {}).get("complete") and not entry.get("allow_incomplete_unit", False) ): raise SemanticProgressError( f"semantic data unit is not marked complete: {unit_name}") members = entry.get("members") if members: section_label = entry.get("group", "data-section-group") target = load(project_root / config_unit["target_path"]) base = load(project_root / config_unit["base_path"]) credited_size = 0 target_section_numbers = set() base_section_numbers = set() grouped_sections = {} for member in members: target_symbol = member["symbol"] base_symbol = member.get("base_symbol", target_symbol) source_function = member.get("source_function") base_source_function = member.get( "base_source_function", source_function) try: if source_function: target_info = section_info_source_relative( target, target_symbol, source_function) base_info = section_info_source_relative( base, base_symbol, base_source_function) else: target_info = section_info(target, target_symbol) base_info = section_info(base, base_symbol) target_owner = _unique_defined_symbol( target, target_symbol, "semantic data target owner") base_owner = _unique_defined_symbol( base, base_symbol, "semantic data base owner") target_section = target["sections"][ target_owner["section"] - 1] base_section = base["sections"][base_owner["section"] - 1] except (CoffError, KeyError, OSError) as error: raise SemanticProgressError( f"cannot verify semantic data group member " f"{unit_name}:{target_symbol}: {error}") from error if target_owner["section"] in target_section_numbers \ or base_owner["section"] in base_section_numbers: raise SemanticProgressError( f"semantic data group repeats a section: " f"{unit_name}:{target_symbol}") target_section_numbers.add(target_owner["section"]) base_section_numbers.add(base_owner["section"]) if not section_infos_equal(target_info, base_info): raise SemanticProgressError( f"semantic data group member is no longer exact: " f"{unit_name}:{target_symbol}") target_snapshot = _semantic_data_member_snapshot( target_owner, target_section, target_info) base_snapshot = _semantic_data_member_snapshot( base_owner, base_section, base_info) for key in ("section", "size", "padded_size", "flags"): if target_snapshot[key] != base_snapshot[key]: raise SemanticProgressError( f"semantic data group member layout differs: " f"{unit_name}:{target_symbol} ({key})") expected = member.get("measurements", {}) snapshot = { "target": target_snapshot, "base": base_snapshot, } if expected != snapshot: raise SemanticProgressError( f"semantic data group member snapshot changed: " f"{unit_name}:{target_symbol}") credited_size += target_snapshot["padded_size"] grouped_sections[target_snapshot["section"]] = ( grouped_sections.get(target_snapshot["section"], 0) + target_snapshot["padded_size"]) unmatched_sections = {} for report_section in report_unit.get("sections", []): if report_section.get("name") == ".text" \ or float(report_section.get( "fuzzy_match_percent", 0.0)) == 100.0: continue name = report_section["name"] unmatched_sections[name] = ( unmatched_sections.get(name, 0) + int(report_section.get("size", 0))) if grouped_sections != unmatched_sections: raise SemanticProgressError( f"semantic data group does not cover the reported " f"unmatched sections: {unit_name}:{section_label}") else: section_symbol = entry["symbol"] section_label = section_symbol try: target_info = section_info_resolved( load(project_root / config_unit["target_path"]), section_symbol, symbol_addresses, ) base_info = section_info_resolved( load(project_root / config_unit["base_path"]), section_symbol, symbol_addresses, ) except (CoffError, KeyError, OSError) as error: raise SemanticProgressError( f"cannot verify semantic data match " f"{unit_name}:{section_symbol}: {error}") from error if target_info != base_info: raise SemanticProgressError( f"semantic data match is no longer exact: " f"{unit_name}:{section_symbol}") expected = entry.get("measurements", {}) snapshot = { key: target_info[key] for key in ("size", "relocation_count", "normalized_sha256") } if expected != snapshot: raise SemanticProgressError( f"semantic data target snapshot changed: " f"{unit_name}:{section_symbol}") credited_size = target_info["size"] unit_measures = report_unit["measures"] unmatched_data = ( int(unit_measures.get("total_data", 0)) - int(unit_measures.get("matched_data", 0)) ) if unmatched_data == 0: continue if unmatched_data != credited_size: raise SemanticProgressError( f"semantic data section does not cover all unmatched data: " f"{unit_name}:{section_label} covers {credited_size}, " f"remaining {unmatched_data}") _credit_data(report["measures"], unmatched_data) _credit_data(unit_measures, unmatched_data) progress_categories = report_unit.get("metadata", {}).get( "progress_categories", []) if isinstance(progress_categories, str): progress_categories = [progress_categories] for category_id in progress_categories: if category_id not in categories: raise SemanticProgressError( f"semantic data category not found: {category_id}") _credit_data(categories[category_id]["measures"], unmatched_data) credited.append( f"{unit_name}:{section_label} (+{unmatched_data} data bytes)") return credited