Skip to main content

recovery_property/
recovery_property.rs

1//! Property probe: recovery must never keep a record the strict parser
2//! would not have seen at that offset, and every diagnostic must be in-bounds.
3//!
4//! Not a unit test: it hammers pseudo-random damaged inputs to look for
5//! fabricated records of the kind the binary-literal defect produced.
6
7#![allow(clippy::unreadable_literal, clippy::cast_possible_truncation)]
8
9use openbim_step::{parse_with, OnMalformed, ParseOptions};
10
11fn lcg(state: &mut u64) -> u64 {
12    *state = state
13        .wrapping_mul(6364136223846793005)
14        .wrapping_add(1442695040888963407);
15    *state >> 33
16}
17
18fn main() {
19    let base = "ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\
20                FILE_NAME('n','t',(''),(''),'p','o','a');\nFILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\n\
21                #1= IFCPERSON($,$,'a;b',$,$,$,$,$);\n\
22                #2= IFCPIXELTEXTURE(1,1,8,(\"00AA;BB\"));\n\
23                #3= IFCORGANIZATION($,'o''x',$,$,$); /* c;c */\n\
24                #4= IFCDOOR('real');\nENDSEC;\nEND-ISO-10303-21;\n";
25
26    let mut state = 0x5eed_1234_u64;
27    let mut recovered = 0usize;
28    let mut fabricated = 0usize;
29    let mut span_faults = 0usize;
30
31    for _ in 0..20_000 {
32        let mut bytes = base.as_bytes().to_vec();
33        // Corrupt 1-3 bytes anywhere in the DATA region.
34        let edits = 1 + (lcg(&mut state) % 3) as usize;
35        for _ in 0..edits {
36            let at = 150 + (lcg(&mut state) as usize % (bytes.len() - 150));
37            match lcg(&mut state) % 3 {
38                0 => bytes[at] = b'@',
39                1 => {
40                    bytes.remove(at);
41                }
42                _ => bytes[at] = (lcg(&mut state) % 128) as u8,
43            }
44        }
45
46        let Ok(outcome) = parse_with(
47            &bytes,
48            ParseOptions::default().on_malformed_record(OnMalformed::Skip),
49        ) else {
50            continue;
51        };
52        recovered += 1;
53
54        for diagnostic in &outcome.diagnostics {
55            let span = diagnostic.span();
56            if span.start > span.end || span.end > bytes.len() {
57                span_faults += 1;
58            }
59        }
60
61        // Every kept record must appear verbatim in the surviving source: its
62        // `#id=` must exist outside any diagnostic-covered range.
63        for record in &outcome.exchange.data.records {
64            let needle = format!("#{}=", record.id.as_str());
65            let found = String::from_utf8_lossy(&bytes).contains(&needle)
66                || String::from_utf8_lossy(&bytes).contains(&format!("#{} =", record.id.as_str()));
67            if !found {
68                fabricated += 1;
69                eprintln!(
70                    "FABRICATED {} in:\n{}",
71                    needle,
72                    String::from_utf8_lossy(&bytes)
73                );
74            }
75        }
76    }
77
78    println!("recovered={recovered} fabricated={fabricated} span_faults={span_faults}");
79    assert_eq!(fabricated, 0, "recovery invented records");
80    assert_eq!(span_faults, 0, "diagnostic span out of bounds");
81    println!("PROPERTY PROBE OK");
82}