ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_third_party_patches.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Offline gate for reproducible third-party patch series.
5
6Vendored dependencies stay ready to compile: reviewed patches are already
7present in their checked-in bytes. This gate proves, without fetching the
8network, that each declared patch series reverses those bytes to the upstream
9blob recorded in docs/sbom/upstream and reapplies to the checked-in blob.
10
11Fetched dependencies use the other supported delivery model: the build checks
12out a pin and applies the numbered series. For those, this gate verifies the
13pin, series, and application entry point are connected, AND that the recorded
14upstream record still describes the CURRENT pin: `upstream_pin` must equal the
15live pin value, and every file the series touches must be recorded with a blob
16whose id the patch's own pre-image abbreviation prefixes.
17
18Honest limits, offline. The gate has no git objects for a fetched dependency,
19so it cannot recompute a blob: it compares the registry against the patch's
20`index` line, and those are two first-party artifacts. What it DOES catch is a
21pin bump that orphans the series, a patch edited away from its recorded
22pre-image, a target with no record, and a record no patch touches. Proving the
23blob ids against the real upstream remains the job of the networked
24`soup-upstream-refresh` gate.
25"""
26
27from __future__ import annotations
28
29import argparse
30import hashlib
31import re
32import shutil
33import subprocess
34import sys
35import tempfile
36import tomllib
37from dataclasses import dataclass
38from pathlib import Path, PurePosixPath
39from types import SimpleNamespace
40
41sys.path.insert(0, str(Path(__file__).resolve().parent))
42sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "gen"))
43sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "dev"))
44
45from git_environment import sanitized_git_environment, trusted_git_executable
46from sbom_registry import PROV_NOT_VENDORED, REGISTRY
47from soup_manifest import KIND_PATCH, ManifestError, manifest_path, parse_manifest
48
49REPO_ROOT = Path(__file__).resolve().parents[2]
50POLICY_PATH = Path("docs/sbom/patches/registry.toml")
51NUMBERED_PATCH_RE = re.compile(r"\A[0-9]{4}-[a-z0-9][a-z0-9-]*\.patch\Z")
52HEX40_RE = re.compile(r"\A[0-9a-f]{40}\Z")
53DELIVERIES = frozenset(("vendored", "fetched"))
54CLASSIFICATIONS = frozenset(("functional", "metadata"))
55METADATA_NAMES = frozenset((".gitattributes", ".gitignore", ".gitmodules"))
56NUMSTAT_FIELD_COUNT = 3
57
58
59@dataclass(frozen=True)
60class PatchItem:
61 """One numbered transformation and its review classification."""
62
63 file: str
64 classification: str
65
66
67@dataclass(frozen=True)
68class PatchComponent:
69 """One vendored or fetched component's ordered patch series."""
70
71 key: str
72 delivery: str
73 series: Path
74 patches: tuple[PatchItem, ...]
75 apply_script: Path | None = None
76 series_token: str | None = None
77 pin_file: Path | None = None
78 pin_key: str | None = None
79 # (upstream path, 40-hex blob) at the pin, for every file the series touches,
80 # plus the pin those blobs were read from.
81 upstream_blobs: tuple[tuple[str, str], ...] = ()
82 upstream_pin: str | None = None
83
84
85def _safe_rel_path(value: object, where: str) -> Path:
86 """Return a validated repository-relative POSIX path."""
87 if not isinstance(value, str) or not value:
88 msg = f"{where}: expected a non-empty path string"
89 raise ValueError(msg)
90 pure = PurePosixPath(value)
91 if pure.is_absolute() or ".." in pure.parts or str(pure) != value:
92 msg = f"{where}: path must be normalized and repository-relative: {value!r}"
93 raise ValueError(msg)
94 return Path(value)
95
96
97def _parse_patch(raw: object, where: str) -> PatchItem:
98 """Parse and validate one patch table."""
99 if not isinstance(raw, dict):
100 msg = f"{where}: patch entry must be a table"
101 raise TypeError(msg)
102 unknown = set(raw) - {"file", "classification"}
103 if unknown:
104 msg = f"{where}: unknown patch fields: {', '.join(sorted(unknown))}"
105 raise ValueError(msg)
106 filename = raw.get("file")
107 classification = raw.get("classification")
108 if not isinstance(filename, str) or not NUMBERED_PATCH_RE.fullmatch(filename):
109 msg = f"{where}: patch file must match NNNN-lower-kebab.patch"
110 raise ValueError(msg)
111 if classification not in CLASSIFICATIONS:
112 msg = f"{where}: classification must be functional or metadata"
113 raise ValueError(msg)
114 return PatchItem(filename, classification)
115
116
117def _parse_upstream_blobs(raw: object, where: str) -> tuple[tuple[str, str], ...]:
118 """Parse the recorded pin blob table for one fetched component."""
119 if not isinstance(raw, dict) or not raw:
120 msg = f"{where}.upstream_blobs: expected a non-empty table"
121 raise ValueError(msg)
122 for name, value in raw.items():
123 if not isinstance(value, str) or not HEX40_RE.fullmatch(value):
124 msg = f"{where}.upstream_blobs.{name}: expected one full 40-hex blob id"
125 raise ValueError(msg)
126 return tuple(sorted(raw.items()))
127
128
129def _parse_upstream_record(raw: dict, delivery: object, where: str) -> dict[str, object]:
130 """Parse the optional recorded-upstream fields of a fetched component."""
131 if "upstream_blobs" not in raw and "upstream_pin" not in raw:
132 return {}
133 if delivery != "fetched":
134 msg = f"{where}: upstream_blobs/upstream_pin apply only to a fetched component"
135 raise ValueError(msg)
136 upstream_pin = raw.get("upstream_pin")
137 if not isinstance(upstream_pin, str) or not HEX40_RE.fullmatch(upstream_pin):
138 msg = f"{where}.upstream_pin: expected one full 40-hex commit id"
139 raise ValueError(msg)
140 return {
141 "upstream_blobs": _parse_upstream_blobs(raw.get("upstream_blobs"), where),
142 "upstream_pin": upstream_pin,
143 }
144
145
146def _parse_component(raw: object, index: int) -> PatchComponent:
147 """Parse and validate one component table."""
148 where = f"component[{index}]"
149 if not isinstance(raw, dict):
150 msg = f"{where}: entry must be a table"
151 raise TypeError(msg)
152 allowed = {
153 "key",
154 "delivery",
155 "series",
156 "patches",
157 "apply_script",
158 "series_token",
159 "pin_file",
160 "pin_key",
161 "upstream_blobs",
162 "upstream_pin",
163 }
164 unknown = set(raw) - allowed
165 if unknown:
166 msg = f"{where}: unknown fields: {', '.join(sorted(unknown))}"
167 raise ValueError(msg)
168 key = raw.get("key")
169 delivery = raw.get("delivery")
170 if not isinstance(key, str) or not key:
171 msg = f"{where}: key must be a non-empty string"
172 raise ValueError(msg)
173 if delivery not in DELIVERIES:
174 msg = f"{where}: delivery must be vendored or fetched"
175 raise ValueError(msg)
176 patches_raw = raw.get("patches")
177 if not isinstance(patches_raw, list) or not patches_raw:
178 msg = f"{where}: at least one [[component.patches]] entry is required"
179 raise ValueError(msg)
180 patches = tuple(
181 _parse_patch(item, f"{where}.patches[{i}]") for i, item in enumerate(patches_raw)
182 )
183 filenames = tuple(item.file for item in patches)
184 if len(set(filenames)) != len(filenames):
185 msg = f"{where}: duplicate patch filename"
186 raise ValueError(msg)
187 kwargs: dict[str, object] = {}
188 for field in ("apply_script", "pin_file"):
189 if field in raw:
190 kwargs[field] = _safe_rel_path(raw[field], f"{where}.{field}")
191 for field in ("series_token", "pin_key"):
192 if field in raw:
193 value = raw[field]
194 if not isinstance(value, str) or not value:
195 msg = f"{where}.{field}: expected a non-empty string"
196 raise ValueError(msg)
197 kwargs[field] = value
198 kwargs.update(_parse_upstream_record(raw, delivery, where))
199 return PatchComponent(
200 key,
201 delivery,
202 _safe_rel_path(raw.get("series"), f"{where}.series"),
203 patches,
204 **kwargs,
205 )
206
207
208def load_policy(root: Path) -> tuple[PatchComponent, ...]:
209 """Load the strict machine-readable patch registry."""
210 path = root / POLICY_PATH
211 try:
212 raw = tomllib.loads(path.read_text(encoding="utf-8"))
213 except (OSError, tomllib.TOMLDecodeError) as exc:
214 msg = f"{POLICY_PATH}: {exc}"
215 raise ValueError(msg) from exc
216 if set(raw) != {"schema_version", "component"} or raw.get("schema_version") != 1:
217 msg = f"{POLICY_PATH}: expected only schema_version=1 and component tables"
218 raise ValueError(msg)
219 rows = raw.get("component")
220 if not isinstance(rows, list) or not rows:
221 msg = f"{POLICY_PATH}: no component tables"
222 raise ValueError(msg)
223 components = tuple(_parse_component(row, index) for index, row in enumerate(rows))
224 keys = tuple(component.key for component in components)
225 if len(set(keys)) != len(keys):
226 msg = f"{POLICY_PATH}: duplicate component key"
227 raise ValueError(msg)
228 return components
229
230
231def _series_files(component: PatchComponent, root: Path) -> tuple[Path, ...]:
232 """Validate a series file and return its patch paths in order."""
233 series_path = root / component.series
234 try:
235 lines = series_path.read_text(encoding="utf-8").splitlines()
236 except OSError as exc:
237 msg = f"{component.series}: {exc}"
238 raise ValueError(msg) from exc
239 names = tuple(
240 line.strip() for line in lines if line.strip() and not line.lstrip().startswith("#")
241 )
242 expected = tuple(item.file for item in component.patches)
243 if names != expected:
244 msg = f"{component.series}: series order {names!r} != registry {expected!r}"
245 raise ValueError(msg)
246 paths = tuple(series_path.parent / name for name in names)
247 missing = tuple(path.relative_to(root).as_posix() for path in paths if not path.is_file())
248 if missing:
249 msg = f"{component.key}: missing patch files: {', '.join(missing)}"
250 raise ValueError(msg)
251 return paths
252
253
254def _patch_targets(patch: Path, root: Path) -> tuple[str, ...]:
255 """Return normalized target paths reported by Git's patch parser."""
256 proc = subprocess.run( # noqa: S603 -- fixed Git executable and validated patch path
257 (trusted_git_executable(), "apply", "--numstat", str(patch)),
258 cwd=root,
259 env=sanitized_git_environment(),
260 text=True,
261 capture_output=True,
262 check=False,
263 )
264 if proc.returncode != 0:
265 detail = proc.stderr.strip()
266 msg = f"{patch.relative_to(root)}: git apply --numstat failed: {detail}"
267 raise ValueError(msg)
268 targets: list[str] = []
269 for line in proc.stdout.splitlines():
270 fields = line.split("\t", 2)
271 if len(fields) != NUMSTAT_FIELD_COUNT or not fields[2]:
272 msg = f"{patch.relative_to(root)}: malformed numstat row: {line!r}"
273 raise ValueError(msg)
274 target = fields[2]
275 if " => " in target or target.startswith("{"):
276 msg = f"{patch.relative_to(root)}: rename patches are unsupported: {target}"
277 raise ValueError(msg)
278 _safe_rel_path(target, str(patch.relative_to(root)))
279 targets.append(target)
280 if not targets:
281 msg = f"{patch.relative_to(root)}: patch changes no files"
282 raise ValueError(msg)
283 return tuple(dict.fromkeys(targets))
284
285
286def _blob_id(path: Path) -> str:
287 """Return the raw Git blob SHA-1 for one file, without attributes."""
288 data = path.read_bytes()
289 return hashlib.sha1(b"blob %d\0" % len(data) + data).hexdigest() # noqa: S324 -- Git object IDs require SHA-1
290
291
292def _apply(patch: Path, work: Path, reverse: bool) -> str | None:
293 """Apply one patch in a disposable component tree."""
294 argv = [trusted_git_executable(), "apply", "--whitespace=nowarn"]
295 if reverse:
296 argv.append("--reverse")
297 argv.append(str(patch))
298 proc = subprocess.run( # noqa: S603 -- fixed Git executable and validated patch path
299 argv,
300 cwd=work,
301 env=sanitized_git_environment(),
302 text=True,
303 capture_output=True,
304 check=False,
305 )
306 if proc.returncode == 0:
307 return None
308 direction = "reverse" if reverse else "forward"
309 return f"{patch}: {direction} apply failed: {proc.stderr.strip()}"
310
311
312def _copy_targets(source: Path, work: Path, targets: set[str]) -> list[str]:
313 """Copy only files touched by patches into a disposable tree."""
314 errors: list[str] = []
315 for rel in sorted(targets):
316 src = source / rel
317 if not src.is_file():
318 errors.append(f"{source}/{rel}: declared patched file is missing")
319 continue
320 dst = work / rel
321 dst.parent.mkdir(parents=True, exist_ok=True)
322 shutil.copy2(src, dst)
323 return errors
324
325
326def _validate_metadata(
327 component: PatchComponent, targets_by_patch: tuple[tuple[str, ...], ...]
328) -> list[str]:
329 """Reject a metadata classification on any code or payload file."""
330 errors: list[str] = []
331 for item, targets in zip(component.patches, targets_by_patch, strict=True):
332 if item.classification != "metadata":
333 continue
334 invalid = tuple(path for path in targets if PurePosixPath(path).name not in METADATA_NAMES)
335 if invalid:
336 errors.append(
337 f"{component.key}: {item.file} is classified metadata but changes "
338 + ", ".join(invalid)
339 )
340 return errors
341
342
343def _validate_vendored(
344 component: PatchComponent,
345 registry_component: object,
346 manifest: object,
347 patch_paths: tuple[Path, ...],
348 root: Path,
349) -> list[str]:
350 """Prove a vendored series reverses to upstream and reapplies exactly."""
351 errors: list[str] = []
352 targets_by_patch = tuple(_patch_targets(path, root) for path in patch_paths)
353 errors.extend(_validate_metadata(component, targets_by_patch))
354 targets = {target for group in targets_by_patch for target in group}
355 declared = set(dict(registry_component.patched_files))
356 recorded = {entry.rel_path for entry in manifest.entries if entry.kind == KIND_PATCH}
357 if targets != declared:
358 errors.append(
359 f"{component.key}: patch targets {sorted(targets)!r} != "
360 f"patched_files {sorted(declared)!r}"
361 )
362 if targets != recorded:
363 errors.append(
364 f"{component.key}: patch targets {sorted(targets)!r} != "
365 f"manifest patch rows {sorted(recorded)!r}"
366 )
367 if errors:
368 return errors
369 entries = manifest.by_path()
370 source = root / registry_component.path
371 with tempfile.TemporaryDirectory(prefix="ra8-patch-check-") as raw_tmp:
372 work = Path(raw_tmp) / "component"
373 work.mkdir()
374 errors.extend(_copy_targets(source, work, targets))
375 if errors:
376 return errors
377 for patch in reversed(patch_paths):
378 failure = _apply(patch.resolve(), work, reverse=True)
379 if failure:
380 errors.append(f"{component.key}: {failure}")
381 return errors
382 errors.extend(
383 f"{component.key}: reverse series does not reproduce upstream blob for {rel}"
384 for rel in sorted(targets)
385 if _blob_id(work / rel) != entries[rel].upstream_blob
386 )
387 for patch in patch_paths:
388 failure = _apply(patch.resolve(), work, reverse=False)
389 if failure:
390 errors.append(f"{component.key}: {failure}")
391 return errors
392 for rel in sorted(targets):
393 local_blob = entries[rel].local_blob
394 same_bytes = (work / rel).read_bytes() == (source / rel).read_bytes()
395 if _blob_id(work / rel) != local_blob or not same_bytes:
396 errors.append(
397 f"{component.key}: forward series does not reproduce vendored blob for {rel}"
398 )
399 return errors
400
401
402def _pin_value(path: Path, key: str) -> str | None:
403 """Read one strict KEY=value pin from a shell-compatible pin file."""
404 prefix = key + "="
405 rows = [
406 line[len(prefix) :].strip().strip("\"'")
407 for line in path.read_text(encoding="utf-8").splitlines()
408 if line.startswith(prefix)
409 ]
410 return rows[0] if len(rows) == 1 else None
411
412
413MIN_ABBREV = 7
414
415
416def _patch_preimages(patch: Path) -> tuple[tuple[str, str], ...]:
417 """Return (path, abbreviated pre-image blob) for every file a patch touches."""
418 pairs: list[tuple[str, str]] = []
419 current: str | None = None
420 for line in patch.read_text(encoding="utf-8").splitlines():
421 if line.startswith("diff --git a/"):
422 current = line.removeprefix("diff --git a/").split(" b/", 1)[0]
423 elif line.startswith("index ") and current is not None:
424 pre = line.removeprefix("index ").split("..", 1)[0].strip()
425 pairs.append((current, pre))
426 current = None
427 return tuple(pairs)
428
429
430def _validate_upstream_binding(
431 component: PatchComponent,
432 patch_paths: tuple[Path, ...],
433 targets_by_patch: tuple[tuple[str, ...], ...],
434 pin: str | None,
435) -> list[str]:
436 """Require every touched file to name the exact blob recorded for the pin.
437
438 Without this the fetched model has no offline proof at all: the series is
439 applied with --unidiff-zero, so `git apply --check` still succeeds against
440 an upstream whose target files have drifted anywhere outside the hunks.
441 """
442 recorded = dict(component.upstream_blobs)
443 if not recorded:
444 return [f"{component.key}: fetched entry records no upstream_blobs for its pin"]
445 errors: list[str] = []
446 if component.upstream_pin is None:
447 errors.append(f"{component.key}: fetched entry records no upstream_pin")
448 elif pin is not None and component.upstream_pin != pin:
449 errors.append(
450 f"{component.key}: upstream_blobs were recorded against "
451 f"{component.upstream_pin[:12]}, but the live pin is {pin[:12]}"
452 )
453 seen: set[str] = set()
454 for patch, targets in zip(patch_paths, targets_by_patch, strict=True):
455 pairs = _patch_preimages(patch)
456 if not pairs:
457 errors.append(f"{component.key}: {patch.name} declares no pre-image blob")
458 indexed = {rel for rel, _pre in pairs}
459 # git's own target list is authoritative: a file with no `index` line
460 # would otherwise never enter the comparison at all.
461 errors.extend(
462 f"{component.key}: {patch.name} touches {rel} with no pre-image blob line"
463 for rel in sorted(set(targets) - indexed)
464 )
465 seen.update(targets)
466 for rel, pre in pairs:
467 seen.add(rel)
468 expected = recorded.get(rel)
469 if len(pre) < MIN_ABBREV or not all(c in "0123456789abcdef" for c in pre):
470 errors.append(
471 f"{component.key}: {patch.name} pre-image for {rel} is not a usable "
472 f"blob abbreviation: {pre!r}"
473 )
474 elif expected is None:
475 errors.append(
476 f"{component.key}: {patch.name} touches {rel}, which has no recorded "
477 "upstream blob for the pin"
478 )
479 elif not expected.startswith(pre):
480 errors.append(
481 f"{component.key}: {patch.name} expects {rel} at {pre}, but the pin "
482 f"records {expected[: len(pre)]}"
483 )
484 errors.extend(
485 f"{component.key}: upstream_blobs records {rel}, which no patch in the series touches"
486 for rel in sorted(set(recorded) - seen)
487 )
488 return errors
489
490
491def _validate_fetched(
492 component: PatchComponent, patch_paths: tuple[Path, ...], root: Path
493) -> list[str]:
494 """Verify a fetched dependency connects its pin, series, and build script."""
495 apply_script = component.apply_script
496 series_token = component.series_token
497 pin_file = component.pin_file
498 pin_key = component.pin_key
499 if apply_script is None or series_token is None or pin_file is None or pin_key is None:
500 return [
501 f"{component.key}: fetched entry requires apply_script, series_token, "
502 "pin_file, and pin_key"
503 ]
504 targets_by_patch = tuple(_patch_targets(path, root) for path in patch_paths)
505 errors = _validate_metadata(component, targets_by_patch)
506 script_path = root / apply_script
507 pin_path = root / pin_file
508 if not script_path.is_file() or not pin_path.is_file():
509 return [*errors, f"{component.key}: apply script or pin file is missing"]
510 script = script_path.read_text(encoding="utf-8")
511 pin = _pin_value(pin_path, pin_key)
512 if pin is None or not HEX40_RE.fullmatch(pin):
513 errors.append(f"{component.key}: {component.pin_file}:{pin_key} is not one full 40-hex pin")
514 pin = None
515 errors.extend(_validate_upstream_binding(component, patch_paths, targets_by_patch, pin))
516 positions = (
517 script.find(pin_key),
518 script.find(series_token),
519 script.find("git -C"),
520 script.find(" apply "),
521 )
522 if min(positions) < 0 or positions[0] >= positions[1]:
523 errors.append(
524 f"{component.key}: build script does not connect pin-before-series and git apply"
525 )
526 direct = re.findall(r"patches/[0-9]{4}-[a-z0-9-]+\.patch", script)
527 if direct:
528 detail = (
529 f"{component.key}: build script hard-codes patches "
530 f"instead of consuming series: {direct}"
531 )
532 errors.append(detail)
533 return errors
534
535
536def check(root: Path = REPO_ROOT) -> list[str]:
537 """Return all patch-policy violations in root."""
538 try:
539 policy = load_policy(root)
540 except (TypeError, ValueError) as exc:
541 return [str(exc)]
542 registry = {
543 component.key: component
544 for component in REGISTRY
545 if component.provenance != PROV_NOT_VENDORED
546 }
547 declared_patched = {key for key, component in registry.items() if component.patched_files}
548 policy_vendored = {component.key for component in policy if component.delivery == "vendored"}
549 errors: list[str] = []
550 if declared_patched != policy_vendored:
551 errors.append(
552 f"vendored patch policy keys {sorted(policy_vendored)!r} != "
553 f"registry patched keys {sorted(declared_patched)!r}"
554 )
555 registered_patch_files: set[Path] = set()
556 for component in policy:
557 try:
558 patch_paths = _series_files(component, root)
559 registered_patch_files.update(path.relative_to(root) for path in patch_paths)
560 if component.delivery == "fetched":
561 errors.extend(_validate_fetched(component, patch_paths, root))
562 continue
563 registry_component = registry.get(component.key)
564 if registry_component is None:
565 errors.append(
566 f"{component.key}: vendored patch policy has no SBOM registry component"
567 )
568 continue
569 manifest_file = root / manifest_path(component.key)
570 manifest = parse_manifest(
571 component.key,
572 manifest_file.read_text(encoding="utf-8"),
573 manifest_path(component.key),
574 )
575 errors.extend(
576 _validate_vendored(component, registry_component, manifest, patch_paths, root)
577 )
578 except (ManifestError, OSError, ValueError) as exc:
579 errors.append(str(exc))
580 patch_roots = (root / "docs/sbom/patches", root / "coprocessor")
581 discovered = {
582 path.relative_to(root)
583 for scan_root in patch_roots
584 if scan_root.exists()
585 for path in scan_root.rglob("*.patch")
586 if "build" not in path.parts and "upstream" not in path.parts
587 }
588 unregistered = discovered - registered_patch_files
589 if unregistered:
590 rendered = ", ".join(sorted(path.as_posix() for path in unregistered))
591 errors.append(f"unregistered first-party patch files: {rendered}")
592 return errors
593
594
595def _selftest_patch() -> str:
596 """Return a minimal patch used by the both-directions fixture."""
597 return """diff --git a/src/value.c b/src/value.c
598index 788b307..e58e70c 100644
599--- a/src/value.c
600+++ b/src/value.c
601@@ -1 +1 @@
602-int value = 1;
603+int value = 2;
604"""
605
606
607def _selftest_vendored_replay(
608 root: Path,
609) -> tuple[list[str], list[str], list[str], Path, str, str]:
610 """Return the vendored-replay directions plus the shared fetched fixture inputs."""
611 patch_dir = root / "patches"
612 source = root / "vendor"
613 patch_dir = root / "patches"
614 (source / "src").mkdir(parents=True)
615 patch_dir.mkdir()
616 current = source / "src/value.c"
617 current.write_text("int value = 2;\n", encoding="utf-8")
618 patch = patch_dir / "0001-change-value.patch"
619 patch.write_text(_selftest_patch(), encoding="utf-8")
620 upstream_blob = hashlib.sha1( # noqa: S324 -- Git object IDs require SHA-1
621 b"blob 15\0int value = 1;\n"
622 ).hexdigest()
623 local_blob = _blob_id(current)
624 entry = SimpleNamespace(
625 kind=KIND_PATCH,
626 rel_path="src/value.c",
627 upstream_blob=upstream_blob,
628 local_blob=local_blob,
629 )
630 manifest = SimpleNamespace(entries=(entry,), by_path=lambda: {entry.rel_path: entry})
631 registry_component = SimpleNamespace(
632 path="vendor", patched_files=((entry.rel_path, "fixture"),)
633 )
634 component = PatchComponent(
635 "fixture",
636 "vendored",
637 Path("patches/series"),
638 (PatchItem(patch.name, "functional"),),
639 )
640 clean = _validate_vendored(component, registry_component, manifest, (patch,), root)
641 current.write_text("int value = 3;\n", encoding="utf-8")
642 drift = _validate_vendored(component, registry_component, manifest, (patch,), root)
643 metadata = PatchComponent(
644 "fixture",
645 "vendored",
646 component.series,
647 (PatchItem(patch.name, "metadata"),),
648 )
649 current.write_text("int value = 2;\n", encoding="utf-8")
650 mislabeled = _validate_vendored(metadata, registry_component, manifest, (patch,), root)
651 return clean, drift, mislabeled, patch_dir, upstream_blob, local_blob
652
653
654def _fetched_fixture_root(
655 base: Path, blobs: dict[str, str], pin: str, recorded_pin: str, patch_text: str
656) -> Path:
657 """Materialize a complete fetched-component tree for one selftest direction."""
658 root = base
659 (root / "docs/sbom/patches").mkdir(parents=True, exist_ok=True)
660 (root / "coprocessor/fix/patches").mkdir(parents=True, exist_ok=True)
661 (root / "coprocessor/fix/patches/0001-fixture.patch").write_text(patch_text, encoding="utf-8")
662 (root / "coprocessor/fix/patches/series").write_text("0001-fixture.patch\n", encoding="utf-8")
663 (root / "coprocessor/fix/pins.env").write_text(f"FIXTURE_COMMIT={pin}\n", encoding="utf-8")
664 (root / "coprocessor/fix/build.sh").write_text(
665 '#!/bin/sh\n. ./pins.env\n: "$FIXTURE_COMMIT"\n'
666 'while read -r p; do git -C "$c" apply "patches/$p"; done < patches/series\n',
667 encoding="utf-8",
668 )
669 rows = "\n".join(f'"{rel}" = "{blob}"' for rel, blob in sorted(blobs.items()))
670 (root / "docs/sbom/patches/registry.toml").write_text(
671 "schema_version = 1\n\n[[component]]\n"
672 'key = "fixture"\ndelivery = "fetched"\n'
673 'series = "coprocessor/fix/patches/series"\n'
674 'apply_script = "coprocessor/fix/build.sh"\n'
675 'series_token = "patches/series"\n'
676 'pin_file = "coprocessor/fix/pins.env"\n'
677 'pin_key = "FIXTURE_COMMIT"\n'
678 + (f'upstream_pin = "{recorded_pin}"\n' if recorded_pin else "")
679 + (f"\n[component.upstream_blobs]\n{rows}\n" if rows else "")
680 + '\n[[component.patches]]\nfile = "0001-fixture.patch"\n'
681 'classification = "functional"\n',
682 encoding="utf-8",
683 )
684 return root
685
686
687@dataclass(frozen=True)
688class _FetchedFixture:
689 """One fetched-component selftest direction, as data."""
690
691 name: str
692 blobs: dict[str, str]
693 pin: str
694 recorded_pin: str
695 patch_text: str
696
697
698def _fetched_binding_errors(base: Path, case: _FetchedFixture) -> list[str]:
699 """Run the REAL entry point CI runs and return only fetched-binding errors.
700
701 Driving check() rather than the private helper is the point: deleting the
702 production call site must make these cases fail, which calling the helper
703 directly could never detect.
704 """
705 root = _fetched_fixture_root(
706 base / case.name, case.blobs, case.pin, case.recorded_pin, case.patch_text
707 )
708 return [error for error in check(root) if not error.startswith("vendored patch policy keys")]
709
710
711def _selftest_fetched_binding(
712 base: Path, upstream_blob: str, local_blob: str
713) -> dict[str, list[str]]:
714 """Return every direction of the fetched pin binding, keyed by case name."""
715 pin = "9" * 40
716 other = ("f" if upstream_blob[0] != "f" else "0") + upstream_blob[1:]
717 good = (
718 "diff --git a/src/value.c b/src/value.c\n"
719 f"index {upstream_blob[:7]}..{local_blob[:7]} 100644\n"
720 "--- a/src/value.c\n+++ b/src/value.c\n"
721 "@@ -1 +1 @@\n-int value = 1;\n+int value = 2;\n"
722 )
723 blank = good.replace(f"index {upstream_blob[:7]}..", "index ..", 1)
724 extra = good + (
725 "diff --git a/src/other.c b/src/other.c\n"
726 "--- a/src/other.c\n+++ b/src/other.c\n"
727 "@@ -1 +1 @@\n-int other = 1;\n+int other = 2;\n"
728 )
729 one = {"src/value.c": upstream_blob}
730 return {
731 "clean": _fetched_binding_errors(base, _FetchedFixture("clean", one, pin, pin, good)),
732 "drift": _fetched_binding_errors(
733 base, _FetchedFixture("drift", {"src/value.c": other}, pin, pin, good)
734 ),
735 "unrecorded": _fetched_binding_errors(
736 base, _FetchedFixture("unrec", {"src/other.c": other}, pin, pin, good)
737 ),
738 "empty": _fetched_binding_errors(base, _FetchedFixture("empty", {}, pin, pin, good)),
739 "pin-moved": _fetched_binding_errors(
740 base, _FetchedFixture("pinmv", one, "1" * 40, pin, good)
741 ),
742 "blank-preimage": _fetched_binding_errors(
743 base, _FetchedFixture("blank", one, pin, pin, blank)
744 ),
745 "no-index-line": _fetched_binding_errors(
746 base, _FetchedFixture("noidx", one, pin, pin, extra)
747 ),
748 }
749
750
751def selftest() -> int:
752 """Prove exact replay and representative failure directions."""
753 with tempfile.TemporaryDirectory(prefix="ra8-patch-selftest-") as raw_tmp:
754 root = Path(raw_tmp)
755 clean, drift, mislabeled, _patch_dir, upstream_blob, local_blob = _selftest_vendored_replay(
756 root
757 )
758
759 fetched = _selftest_fetched_binding(root / "fetched", upstream_blob, local_blob)
760 failures = []
761 if fetched["clean"]:
762 failures.append(f"recorded pin blob was rejected: {fetched['clean']}")
763 failures.extend(
764 f"fetched pin binding did not fail: {case}"
765 for case in ("drift", "unrecorded", "empty", "pin-moved", "blank-preimage", "no-index-line")
766 if not fetched[case]
767 )
768 if clean:
769 failures.append(f"clean fixture failed: {clean}")
770 if not drift:
771 failures.append("vendored-byte drift did not fail")
772 if not any("classified metadata" in error for error in mislabeled):
773 failures.append("metadata classification on source did not fail")
774 if failures:
775 print("check_third_party_patches: selftest FAILED", file=sys.stderr)
776 for failure in failures:
777 print(f" {failure}", file=sys.stderr)
778 return 1
779 print(
780 "check_third_party_patches: selftest passed "
781 "(vendored replay + fetched pin binding, both directions)."
782 )
783 return 0
784
785
786def main() -> int:
787 """CLI entry point."""
788 parser = argparse.ArgumentParser(description=__doc__)
789 parser.add_argument(
790 "--selftest", action="store_true", help="run isolated both-directions tests"
791 )
792 args = parser.parse_args()
793 if args.selftest:
794 return selftest()
795 errors = check()
796 if errors:
797 print(
798 f"check_third_party_patches: FAIL ({len(errors)} finding(s))",
799 file=sys.stderr,
800 )
801 for error in errors:
802 print(f" {error}", file=sys.stderr)
803 return 1
804 print("check_third_party_patches: PASS -- every reviewed series reproduces its declared bytes.")
805 return 0
806
807
808if __name__ == "__main__":
809 raise SystemExit(main())
-copyright
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298
#define min(x, y)
Untyped minimum shim used by the SOUP's buffer clamping.
Definition xz_config.h:157