| 1 | //! Differential harness: compare afs-ld output against Apple `ld` output. |
| 2 | //! |
| 3 | //! Sprint 0 lands the diffing surface. The `link_both` function that actually |
| 4 | //! shells out to both linkers arrives once afs-ld can produce a real binary |
| 5 | //! (Sprint 18). Until then, tests exercise `diff_macho` directly against |
| 6 | //! synthesized byte slices. |
| 7 | |
| 8 | #![allow(dead_code)] |
| 9 | |
| 10 | use std::path::PathBuf; |
| 11 | |
| 12 | pub struct LinkCase { |
| 13 | pub name: &'static str, |
| 14 | pub inputs: Vec<PathBuf>, |
| 15 | pub args: Vec<String>, |
| 16 | } |
| 17 | |
| 18 | pub struct LinkOutputs { |
| 19 | pub ours: Vec<u8>, |
| 20 | pub theirs: Vec<u8>, |
| 21 | } |
| 22 | |
| 23 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 24 | pub enum DiffCategory { |
| 25 | /// A diff we expect: UUID bytes, timestamps, hash-backed temp paths, etc. |
| 26 | Tolerated(&'static str), |
| 27 | /// Anything else. Fails the parity test. |
| 28 | Critical, |
| 29 | } |
| 30 | |
| 31 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 32 | pub struct DiffChunk { |
| 33 | pub offset: usize, |
| 34 | pub len: usize, |
| 35 | pub reason: String, |
| 36 | pub category: DiffCategory, |
| 37 | } |
| 38 | |
| 39 | #[derive(Debug, Default)] |
| 40 | pub struct DiffReport { |
| 41 | pub tolerated: Vec<DiffChunk>, |
| 42 | pub critical: Vec<DiffChunk>, |
| 43 | } |
| 44 | |
| 45 | impl DiffReport { |
| 46 | pub fn is_clean(&self) -> bool { |
| 47 | self.critical.is_empty() |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | /// Byte-level diff between two Mach-O images. Sprint 0 treats every byte diff |
| 52 | /// as Critical; later sprints layer in the tolerated-diff predicates (UUID, |
| 53 | /// timestamp, code-signature hashes, string-table suffix-dedup variance). |
| 54 | pub fn diff_macho(ours: &[u8], theirs: &[u8]) -> DiffReport { |
| 55 | let mut report = DiffReport::default(); |
| 56 | |
| 57 | if ours.len() != theirs.len() { |
| 58 | report.critical.push(DiffChunk { |
| 59 | offset: 0, |
| 60 | len: ours.len().max(theirs.len()), |
| 61 | reason: format!( |
| 62 | "total size differs: ours = {}, theirs = {}", |
| 63 | ours.len(), |
| 64 | theirs.len() |
| 65 | ), |
| 66 | category: DiffCategory::Critical, |
| 67 | }); |
| 68 | return report; |
| 69 | } |
| 70 | |
| 71 | let mut i = 0; |
| 72 | while i < ours.len() { |
| 73 | if ours[i] != theirs[i] { |
| 74 | let start = i; |
| 75 | while i < ours.len() && ours[i] != theirs[i] { |
| 76 | i += 1; |
| 77 | } |
| 78 | report.critical.push(DiffChunk { |
| 79 | offset: start, |
| 80 | len: i - start, |
| 81 | reason: format!("{} byte(s) differ starting at 0x{start:x}", i - start), |
| 82 | category: DiffCategory::Critical, |
| 83 | }); |
| 84 | } else { |
| 85 | i += 1; |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | report |
| 90 | } |
| 91 | |
| 92 | /// Placeholder for the full linker-spawning contract. Sprint 18 wires this to |
| 93 | /// real invocations of afs-ld and the system `ld` via `xcrun -f ld`. |
| 94 | pub fn link_both(_case: &LinkCase) -> LinkOutputs { |
| 95 | panic!("link_both is not implemented until Sprint 18 (hello-world milestone)"); |
| 96 | } |
| 97 |