Skip to main content

supercov_engine/
rust_runtime.rs

1//! Generated std-only runtime and strict reader for owned Rust probes.
2//!
3//! The target runtime writes an intentionally small append-only transport.
4//! It never computes coverage. Rust reads, validates, de-duplicates and maps
5//! these observations into the shared evidence-v3 model after test execution.
6
7use serde::{Deserialize, Serialize};
8use std::{
9    collections::BTreeMap,
10    fs,
11    path::{Component, Path},
12};
13
14const RUST_PROBE_MAGIC: &str = "SUPERCOV-RUST-PROBE-1";
15
16#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
17#[serde(rename_all = "camelCase", tag = "kind")]
18pub enum RustProbeObservation {
19    Hit {
20        id: String,
21    },
22    Decision {
23        id: String,
24        values: Vec<Option<bool>>,
25        outcome: bool,
26    },
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum RustProbeReadError {
31    Io(String),
32    UnsafeEntry(String),
33    InvalidHeader,
34    InvalidRecord(usize),
35}
36
37impl std::fmt::Display for RustProbeReadError {
38    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::Io(error) => write!(formatter, "Rust probe I/O failed: {error}"),
41            Self::UnsafeEntry(path) => write!(formatter, "unsafe Rust probe entry: {path}"),
42            Self::InvalidHeader => write!(formatter, "invalid Rust probe header"),
43            Self::InvalidRecord(line) => {
44                write!(formatter, "invalid Rust probe record at line {line}")
45            }
46        }
47    }
48}
49
50impl std::error::Error for RustProbeReadError {}
51
52pub(crate) fn valid_probe_id(id: &str) -> bool {
53    let mut parts = id.split(':');
54    matches!(parts.next(), Some("rs"))
55        && matches!(
56            parts.next(),
57            Some("statement" | "function" | "decision" | "branch")
58        )
59        && parts.next().is_some_and(|digest| {
60            digest.len() == 24 && digest.bytes().all(|byte| byte.is_ascii_hexdigit())
61        })
62        && parts.all(|suffix| {
63            !suffix.is_empty()
64                && suffix
65                    .bytes()
66                    .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
67        })
68}
69
70pub fn render_rust_runtime(module_name: &str, crate_key: &str) -> Result<String, String> {
71    let valid_identifier = !module_name.is_empty()
72        && module_name.bytes().enumerate().all(|(index, byte)| {
73            byte == b'_' || byte.is_ascii_alphabetic() || (index > 0 && byte.is_ascii_digit())
74        });
75    if !valid_identifier {
76        return Err("invalid Rust runtime module name".into());
77    }
78    if crate_key.len() != 24 || !crate_key.bytes().all(|byte| byte.is_ascii_hexdigit()) {
79        return Err("invalid Rust runtime crate key".into());
80    }
81
82    Ok(format!(
83        r#"
84#[doc(hidden)]
85// Injected code must be immune to the HOST crate's lint configuration: serde
86// builds with `#![deny(warnings)]`, so this module's fully-qualified imports
87// (required for no_std hosts) became hard errors as "unused imports".
88#[allow(warnings)]
89mod {module_name} {{
90    // The host crate may be `#![no_std]` -- `bytes` is, and so is much of the
91    // ecosystem's foundation. Nothing here can rely on the std prelude being in
92    // scope, so std is brought in explicitly and every prelude item below is
93    // written out in full. Without this the module does not compile and the
94    // whole build fails, which is a hard failure rather than a degradation.
95    extern crate std;
96    use std::fs::{{File, OpenOptions}};
97    use std::io::Write as _;
98    use std::option::Option::{{self, None, Some}};
99    use std::string::String;
100    use std::sync::atomic::{{AtomicBool, AtomicU8, AtomicU64, AtomicUsize, Ordering}};
101    use std::sync::{{Mutex, OnceLock}};
102    use std::ops::ControlFlow;
103    use std::task::Poll;
104    use std::vec::Vec;
105
106    const MAGIC: &[u8] = b"{RUST_PROBE_MAGIC}\n";
107    const CRATE_KEY: &str = "{crate_key}";
108
109    // NOTHING ON THE PROBE PATH MAY ALLOCATE.
110    //
111    // The crate under test may install a `#[global_allocator]` whose body -- or
112    // anything it calls, at any depth -- carries probes. An allocating probe
113    // then re-enters the allocator, which probes, which allocates. bytes-1.12.1
114    // does this in tests/test_bytes_odd_alloc.rs and tests/test_bytes_vec_alloc.rs,
115    // and both died with SIGSEGV before libtest could list a single test.
116    //
117    // A reentrancy flag cannot fix this, and the attempt is instructive: on
118    // macOS the FIRST touch of a thread-local calls `_tlv_bootstrap`, which
119    // allocates -- so the guard recursed inside its own initialisation, before
120    // it could be consulted. A guard that must allocate to answer "am I already
121    // allocating?" is unfixable. Not allocating at all is.
122    //
123    // Records are therefore built in a stack buffer and written with one call.
124    // That also removes a malloc and a free from every probe, which is where
125    // most of a probe's cost used to be.
126    const RECORD_CAPACITY: usize = 256;
127
128    /// The widest condition vector a decision can carry.
129    ///
130    /// Beyond this the frame refuses to record rather than emit a vector whose
131    /// width disagrees with the manifest -- a malformed record the runner
132    /// rejects, which is a wrong number rather than a missing one.
133    const MAX_CONDITIONS: usize = 64;
134
135    // A statement or function hit answers "did this ever run", so only the FIRST
136    // sighting in a process carries information -- and each libtest case runs in
137    // its own process, so first-in-process is first-in-test. Without this, a loop
138    // writes one identical record per iteration: bytes'
139    // advance_bytes_mut_remaining_capacity runs ~2.8M iterations and was still
140    // writing syscalls after four minutes.
141    //
142    // The table is a fixed, open-addressed set of `&'static str` POINTERS, so it
143    // never allocates and never grows -- both of which the probe path forbids.
144    // A crowded table simply writes the record again: a duplicate costs time,
145    // never correctness, whereas dropping one would cost a real observation.
146    const SEEN_SLOTS: usize = 1 << 16;
147    static SEEN: [AtomicUsize; SEEN_SLOTS] = [const {{ AtomicUsize::new(0) }}; SEEN_SLOTS];
148
149    #[inline(always)]
150    fn first_sighting(id: &'static str) -> bool {{
151        let key = id.as_ptr() as usize;
152        let mut slot = (key >> 4) & (SEEN_SLOTS - 1);
153        for _ in 0..8 {{
154            // Almost every call is a repeat, and a plain load settles it; the
155            // CAS is for the one insertion.
156            let seen = SEEN[slot].load(Ordering::Relaxed);
157            if seen == key {{
158                return false;
159            }}
160            if seen == 0 {{
161                match SEEN[slot].compare_exchange(0, key, Ordering::Relaxed, Ordering::Relaxed) {{
162                    Ok(_) => return true,
163                    Err(now) if now == key => return false,
164                    Err(_) => {{}}
165                }}
166            }}
167            slot = (slot + 1) & (SEEN_SLOTS - 1);
168        }}
169        true
170    }}
171
172    // Decisions cannot collapse by id the way hits do: MC/DC needs the SET of
173    // distinct condition vectors, so every vector must reach the log once. What
174    // carries nothing is the REPEAT of a vector already seen, and in a loop that
175    // is nearly all of them -- bytes' advance_bytes_mut_remaining_capacity took
176    // 40.6s against a 0.367s baseline writing one syscall per evaluation across
177    // ~2.8M iterations.
178    //
179    // Entries hold the whole record -- id pointer, outcome, width, values -- and
180    // are compared word for word. A hash would be smaller and faster, but a
181    // collision would silently drop a distinct vector and understate MC/DC, and
182    // that is exactly the kind of wrong number this project refuses to risk.
183    // A full probe chain falls back to writing, which costs a duplicate.
184    //
185    // The table is lock-free: a decision in a hot loop is evaluated millions
186    // of times, and a mutex per evaluation was most of what a probe cost. A
187    // slot's state goes 0 (empty) -> 1 (being written) -> 2 (readable); a
188    // reader compares only readable slots, so a slot mid-write reads as
189    // "different" and at worst costs one duplicate record.
190    const DECISION_SLOTS: usize = 1 << 11;
191    const DECISION_ENTRY: usize = 10 + MAX_CONDITIONS;
192    const DECISION_WORDS: usize = DECISION_ENTRY.div_ceil(8);
193
194    struct DecisionSlot {{
195        state: AtomicU8,
196        words: [AtomicU64; DECISION_WORDS],
197    }}
198
199    static DECISIONS: [DecisionSlot; DECISION_SLOTS] = [const {{
200        DecisionSlot {{
201            state: AtomicU8::new(0),
202            words: [const {{ AtomicU64::new(0) }}; DECISION_WORDS],
203        }}
204    }}; DECISION_SLOTS];
205
206    // This module is compiled in the crate's own profile -- unoptimized under
207    // `cargo test` -- so the paths below are plain indexed loops: iterator
208    // chains and closures cost real function calls there, and a decision in a
209    // hot loop pays them millions of times.
210    fn first_decision(frame: &DecisionFrame, outcome: bool) -> bool {{
211        let key = frame.id.as_ptr() as usize;
212        let conditions = if frame.conditions < MAX_CONDITIONS {{ frame.conditions }} else {{ MAX_CONDITIONS }};
213        // Word 0 is the id pointer; word 1 starts with outcome and width, then
214        // the condition values fill the remaining bytes. Entries of the same
215        // width are zero beyond `used` words, and the width byte sits in word
216        // 1, so comparing `used` words is exact.
217        let mut words = [0u64; DECISION_WORDS];
218        words[0] = key as u64;
219        words[1] = (if outcome {{ 2u64 }} else {{ 1u64 }}) | ((conditions as u64) << 8);
220        let mut index = 0;
221        while index < conditions {{
222            let byte = 10 + index;
223            words[byte / 8] |= (frame.value(index) as u64) << ((byte % 8) * 8);
224            index += 1;
225        }}
226        let used = (10 + conditions).div_ceil(8);
227        let mut slot = (key >> 4) & (DECISION_SLOTS - 1);
228        let mut attempts = 0;
229        while attempts < 16 {{
230            let cell = &DECISIONS[slot];
231            let state = cell.state.load(Ordering::Acquire);
232            if state == 2 {{
233                let mut same = true;
234                let mut word = 0;
235                while word < used {{
236                    if cell.words[word].load(Ordering::Relaxed) != words[word] {{
237                        same = false;
238                        break;
239                    }}
240                    word += 1;
241                }}
242                if same {{
243                    return false;
244                }}
245            }} else if state == 0 {{
246                if cell
247                    .state
248                    .compare_exchange(0, 1, Ordering::Acquire, Ordering::Relaxed)
249                    .is_ok()
250                {{
251                    let mut word = 0;
252                    while word < DECISION_WORDS {{
253                        cell.words[word].store(words[word], Ordering::Relaxed);
254                        word += 1;
255                    }}
256                    cell.state.store(2, Ordering::Release);
257                    return true;
258                }}
259                // Another thread took this slot first: look at it again.
260                continue;
261            }}
262            slot = (slot + 1) & (DECISION_SLOTS - 1);
263            attempts += 1;
264        }}
265        true
266    }}
267
268    fn writer() -> Option<&'static Mutex<File>> {{
269        static WRITER: OnceLock<Option<Mutex<File>>> = OnceLock::new();
270        static OPENING: AtomicBool = AtomicBool::new(false);
271        if let Some(writer) = WRITER.get() {{
272            return writer.as_ref();
273        }}
274        // Opening the file is the one step that must allocate: an environment
275        // lookup, a path, a formatted file name. That allocation re-enters an
276        // instrumented allocator, whose probe arrives back here while the
277        // OnceLock is still unset. Declining for the duration of the open costs
278        // a few observations at startup and makes the recursion impossible.
279        if OPENING.swap(true, Ordering::SeqCst) {{
280            return None;
281        }}
282        let opened = WRITER.get_or_init(|| {{
283            let directory = std::env::var_os("SUPERCOV_RUST_EVIDENCE_DIR")?;
284            let directory = std::path::PathBuf::from(directory);
285            std::fs::create_dir_all(&directory).ok()?;
286            let path =
287                directory.join(std::format!("{{CRATE_KEY}}-{{}}.events", std::process::id()));
288            let empty = std::fs::metadata(&path).map_or(true, |metadata| metadata.len() == 0);
289            let mut file = OpenOptions::new().create(true).append(true).open(path).ok()?;
290            if empty {{
291                file.write_all(MAGIC).ok()?;
292            }}
293            let guarded = Mutex::new(file);
294            // `std::sync::Mutex` boxes a platform mutex on its FIRST lock, and
295            // that allocation would otherwise land on the probe path and
296            // re-enter the host allocator. Force it here, where `OPENING`
297            // already makes re-entry harmless.
298            drop(guarded.lock());
299            Some(guarded)
300        }});
301        OPENING.store(false, Ordering::SeqCst);
302        opened.as_ref()
303    }}
304
305    fn write_record(record: &[u8]) {{
306        let Some(writer) = writer() else {{ return }};
307        let Ok(mut writer) = writer.lock() else {{ return }};
308        let _ = writer.write_all(record);
309    }}
310
311    /// Append to a stack record, reporting whether it all fit.
312    fn push(record: &mut [u8; RECORD_CAPACITY], length: &mut usize, bytes: &[u8]) -> bool {{
313        let Some(slice) = record.get_mut(*length..*length + bytes.len()) else {{
314            return false;
315        }};
316        slice.copy_from_slice(bytes);
317        *length += bytes.len();
318        true
319    }}
320
321    // A frame lives on the stack of the function whose decision it records,
322    // once per decision, and a recursive descent parser carries every one of
323    // them down every level: serde_json's recursion-limit test overflowed the
324    // test thread's stack when each frame held two 64-byte arrays. Values are
325    // two bits each in a u128 (0 unevaluated, 1 false, 2 true) and reached
326    // marks one bit each, so a frame is 48 bytes.
327    pub struct DecisionFrame {{
328        id: &'static str,
329        values: u128,
330        /// Let-chain conditions the evaluation got to: a `let` cannot be
331        /// wrapped, so it is marked reached instead and resolved later.
332        reached: u64,
333        conditions: usize,
334        recordable: bool,
335    }}
336
337    impl DecisionFrame {{
338        pub fn new(id: &'static str, conditions: usize) -> Self {{
339            Self {{
340                id,
341                values: 0,
342                reached: 0,
343                conditions,
344                recordable: conditions <= MAX_CONDITIONS,
345            }}
346        }}
347
348        #[inline(always)]
349        fn value(&self, index: usize) -> u8 {{
350            ((self.values >> (2 * index)) & 3) as u8
351        }}
352
353        #[inline(always)]
354        fn set_value(&mut self, index: usize, value: u8) {{
355            let shift = 2 * index;
356            self.values = (self.values & !(3u128 << shift)) | ((value as u128) << shift);
357        }}
358
359        #[inline(always)]
360        fn is_reached(&self, index: usize) -> bool {{
361            index < MAX_CONDITIONS && (self.reached >> index) & 1 == 1
362        }}
363    }}
364
365    /// A let chain got to condition `index` (0 marks the chain evaluated at
366    /// all). Always true, so it sits in the chain as an operand.
367    #[inline(always)]
368    pub fn reached(frame: &mut DecisionFrame, index: usize) -> bool {{
369        if index < MAX_CONDITIONS {{
370            frame.reached |= 1u64 << index;
371        }}
372        true
373    }}
374
375    /// A let chain decided. A chain tries its conditions in order and stops
376    /// at the first that fails, so every reached `let` before the last
377    /// reached condition held, the last one held when the chain was taken and
378    /// failed when it was not, and conditions never reached stay unevaluated.
379    /// `operators` lists the `&&` whose left side holds a `let`, by the index
380    /// of their right side's first condition: reached means the operator
381    /// evaluated its right side, otherwise it short-circuited. The frame then
382    /// resets for the next evaluation, which a `while let` makes every turn.
383    pub fn decision_chain(
384        frame: &mut DecisionFrame,
385        outcome: bool,
386        operators: &[(usize, &'static str, &'static str)],
387    ) {{
388        if !frame.is_reached(0) {{
389            return;
390        }}
391        let conditions = frame.conditions.min(MAX_CONDITIONS);
392        let mut last = 0;
393        let mut index = 0;
394        while index < conditions {{
395            if frame.is_reached(index) || frame.value(index) != 0 {{
396                last = index;
397            }}
398            index += 1;
399        }}
400        let mut index = 0;
401        while index < conditions {{
402            if frame.value(index) == 0 && frame.is_reached(index) {{
403                frame.set_value(index, if index < last || outcome {{ 2 }} else {{ 1 }});
404            }}
405            index += 1;
406        }}
407        for (first, short_circuit, evaluated) in operators {{
408            let got_there = frame.is_reached(*first)
409                || (*first < MAX_CONDITIONS && frame.value(*first) != 0);
410            hit(if got_there {{ evaluated }} else {{ short_circuit }});
411        }}
412        decision(outcome, frame);
413        frame.values = 0;
414        frame.reached = 0;
415    }}
416
417    // The hot paths are inlined into every probe site, so nothing with a
418    // stack buffer may be: an inlined 256-byte record per site turned each
419    // instrumented function's frame into kilobytes, and serde_json's
420    // recursion-limit test overflowed. Writing a record is the rare path and
421    // stays a call of its own.
422    #[inline(always)]
423    pub fn hit(id: &'static str) {{
424        if first_sighting(id) {{
425            record_hit(id);
426        }}
427    }}
428
429    #[inline(never)]
430    fn record_hit(id: &'static str) {{
431        let mut record = [0u8; RECORD_CAPACITY];
432        let mut length = 0;
433        if push(&mut record, &mut length, b"H\t")
434            && push(&mut record, &mut length, id.as_bytes())
435            && push(&mut record, &mut length, b"\n")
436        {{
437            write_record(&record[..length]);
438        }}
439    }}
440
441    /// One arm of a match was selected. `ids` holds each arm's `not selected`
442    /// and `selected` IDs in source order, so every arm before `selected` was
443    /// considered and passed over. Each ID is a distinct static string, which
444    /// is what `hit` dedupes on.
445    #[inline(always)]
446    pub fn arms(ids: &[&'static str], selected: usize) {{
447        for arm in 0..selected {{
448            if let Some(id) = ids.get(arm * 2) {{
449                hit(id);
450            }}
451        }}
452        if let Some(id) = ids.get(selected * 2 + 1) {{
453            hit(id);
454        }}
455    }}
456
457    /// The left operand of `&&` or `||`: it short-circuits when it equals
458    /// `short_circuits_when`, otherwise the right operand is about to run.
459    #[inline(always)]
460    pub fn logical(
461        left: bool,
462        short_circuits_when: bool,
463        short_circuit: &'static str,
464        evaluated: &'static str,
465    ) -> bool {{
466        hit(if left == short_circuits_when {{ short_circuit }} else {{ evaluated }});
467        left
468    }}
469
470    /// A `for` loop's iterator, recording on the first `next` whether the
471    /// body ran at all. `size_hint` passes through so collection sizing is
472    /// unchanged; nothing else about the iterator is observable to the loop.
473    pub struct ForLoop<I> {{
474        inner: I,
475        first: bool,
476        zero: &'static str,
477        entered: &'static str,
478    }}
479
480    impl<I: Iterator> Iterator for ForLoop<I> {{
481        type Item = I::Item;
482
483        #[inline(always)]
484        fn next(&mut self) -> Option<I::Item> {{
485            let item = self.inner.next();
486            if self.first {{
487                self.first = false;
488                hit(if item.is_some() {{ self.entered }} else {{ self.zero }});
489            }}
490            item
491        }}
492
493        #[inline(always)]
494        fn size_hint(&self) -> (usize, Option<usize>) {{
495            self.inner.size_hint()
496        }}
497    }}
498
499    #[inline(always)]
500    pub fn for_loop<I: IntoIterator>(
501        iterable: I,
502        zero: &'static str,
503        entered: &'static str,
504    ) -> ForLoop<I::IntoIter> {{
505        ForLoop {{ inner: iterable.into_iter(), first: true, zero, entered }}
506    }}
507
508    /// A `while` body ran: clear the loop's flag on the first entry.
509    #[inline(always)]
510    pub fn entered(first: &mut bool, id: &'static str) {{
511        if *first {{
512            *first = false;
513            hit(id);
514        }}
515    }}
516
517    /// A `while` loop is over: a flag still set means the body never ran.
518    #[inline(always)]
519    pub fn zero_iterations(first: bool, id: &'static str) {{
520        if first {{
521            hit(id);
522        }}
523    }}
524
525    /// The operand of `?`, recording which way the operator goes. Every type
526    /// `?` accepts on stable Rust implements this.
527    pub trait TryProbe: Sized {{
528        fn probe(self, continued: &'static str, returned: &'static str) -> Self;
529    }}
530
531    impl<T> TryProbe for Option<T> {{
532        #[inline(always)]
533        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
534            hit(if self.is_some() {{ continued }} else {{ returned }});
535            self
536        }}
537    }}
538
539    impl<T, E> TryProbe for Result<T, E> {{
540        #[inline(always)]
541        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
542            hit(if self.is_ok() {{ continued }} else {{ returned }});
543            self
544        }}
545    }}
546
547    impl<B, C> TryProbe for ControlFlow<B, C> {{
548        #[inline(always)]
549        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
550            hit(if matches!(self, ControlFlow::Continue(_)) {{ continued }} else {{ returned }});
551            self
552        }}
553    }}
554
555    impl<T, E> TryProbe for Poll<Result<T, E>> {{
556        #[inline(always)]
557        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
558            hit(if matches!(self, Poll::Ready(Err(_))) {{ returned }} else {{ continued }});
559            self
560        }}
561    }}
562
563    impl<T, E> TryProbe for Poll<Option<Result<T, E>>> {{
564        #[inline(always)]
565        fn probe(self, continued: &'static str, returned: &'static str) -> Self {{
566            hit(if matches!(self, Poll::Ready(Some(Err(_)))) {{ returned }} else {{ continued }});
567            self
568        }}
569    }}
570
571    // Anything `!` turns into a bool: `assert!(was_seen)` compiles with a
572    // `&bool` (tokio iterates `for was_seen in &seen`), since the macro only
573    // negates its operand.
574    #[inline(always)]
575    pub fn condition<V: std::ops::Not<Output = bool>>(
576        value: V,
577        frame: &mut DecisionFrame,
578        index: usize,
579    ) -> bool {{
580        let value = !!value;
581        if index < frame.conditions && index < MAX_CONDITIONS {{
582            frame.set_value(index, if value {{ 2 }} else {{ 1 }});
583        }}
584        value
585    }}
586
587    #[inline(always)]
588    pub fn decision(value: bool, frame: &mut DecisionFrame) -> bool {{
589        if frame.recordable {{
590            record_decision(value, frame);
591        }}
592        value
593    }}
594
595    #[inline(never)]
596    fn record_decision(value: bool, frame: &DecisionFrame) {{
597        // `writer()` comes first so the file's mutex boxes its platform mutex
598        // while `OPENING` still makes re-entry harmless; the decision table
599        // itself is lock-free and allocates nothing.
600        if writer().is_none() || !first_decision(frame, value) {{
601            return;
602        }}
603        let mut record = [0u8; RECORD_CAPACITY];
604        let mut length = 0;
605        let mut fits = push(&mut record, &mut length, b"D\t")
606            && push(&mut record, &mut length, frame.id.as_bytes())
607            && push(&mut record, &mut length, b"\t");
608        let mut index = 0;
609        while index < frame.conditions {{
610            fits = fits && push(&mut record, &mut length, &[b'0' + frame.value(index)]);
611            index += 1;
612        }}
613        fits = fits
614            && push(&mut record, &mut length, b"\t")
615            && push(&mut record, &mut length, if value {{ b"1" }} else {{ b"0" }})
616            && push(&mut record, &mut length, b"\n");
617        if fits {{
618            write_record(&record[..length]);
619        }}
620    }}
621}}
622"#
623    ))
624}
625
626pub fn parse_rust_probe_events(
627    input: &[u8],
628) -> Result<Vec<RustProbeObservation>, RustProbeReadError> {
629    let text = std::str::from_utf8(input).map_err(|_| RustProbeReadError::InvalidHeader)?;
630    let mut lines = text.lines();
631    if lines.next() != Some(RUST_PROBE_MAGIC) {
632        return Err(RustProbeReadError::InvalidHeader);
633    }
634    let mut observations = Vec::new();
635    for (index, line) in lines.enumerate() {
636        let line_number = index + 2;
637        let fields = line.split('\t').collect::<Vec<_>>();
638        match fields.as_slice() {
639            ["H", id] if valid_probe_id(id) => {
640                observations.push(RustProbeObservation::Hit { id: (*id).into() })
641            }
642            ["D", id, digits, outcome]
643                if valid_probe_id(id)
644                    && id.starts_with("rs:decision:")
645                    && !digits.is_empty()
646                    && digits
647                        .bytes()
648                        .all(|digit| matches!(digit, b'0' | b'1' | b'2'))
649                    && matches!(*outcome, "0" | "1") =>
650            {
651                observations.push(RustProbeObservation::Decision {
652                    id: (*id).into(),
653                    values: digits
654                        .bytes()
655                        .map(|digit| match digit {
656                            b'0' => None,
657                            b'1' => Some(false),
658                            b'2' => Some(true),
659                            _ => unreachable!(),
660                        })
661                        .collect(),
662                    outcome: *outcome == "1",
663                });
664            }
665            _ => return Err(RustProbeReadError::InvalidRecord(line_number)),
666        }
667    }
668    Ok(observations)
669}
670
671pub fn read_rust_probe_directory(
672    directory: &Path,
673) -> Result<BTreeMap<String, Vec<RustProbeObservation>>, RustProbeReadError> {
674    let mut files = fs::read_dir(directory)
675        .map_err(|error| RustProbeReadError::Io(error.to_string()))?
676        .collect::<Result<Vec<_>, _>>()
677        .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
678    files.sort_by_key(|entry| entry.file_name());
679    let mut observations = BTreeMap::new();
680    for entry in files {
681        let name = entry
682            .file_name()
683            .into_string()
684            .map_err(|_| RustProbeReadError::UnsafeEntry("<non-utf8>".into()))?;
685        if Path::new(&name)
686            .components()
687            .any(|component| !matches!(component, Component::Normal(_)))
688            || !name.ends_with(".events")
689        {
690            return Err(RustProbeReadError::UnsafeEntry(name));
691        }
692        let metadata = fs::symlink_metadata(entry.path())
693            .map_err(|error| RustProbeReadError::Io(error.to_string()))?;
694        if !metadata.file_type().is_file() {
695            return Err(RustProbeReadError::UnsafeEntry(name));
696        }
697        let contents =
698            fs::read(entry.path()).map_err(|error| RustProbeReadError::Io(error.to_string()))?;
699        observations.insert(name, parse_rust_probe_events(&contents)?);
700    }
701    Ok(observations)
702}
703
704#[cfg(test)]
705mod tests {
706    use std::{
707        fs,
708        process::Command,
709        time::{SystemTime, UNIX_EPOCH},
710    };
711
712    use super::*;
713    use crate::rust_instrumenter::instrument_rust_source;
714
715    fn temporary_directory(name: &str) -> std::path::PathBuf {
716        let nonce = SystemTime::now()
717            .duration_since(UNIX_EPOCH)
718            .unwrap()
719            .as_nanos();
720        let path = std::env::temp_dir().join(format!(
721            "supercov-rust-runtime-{}-{nonce}-{name}",
722            std::process::id()
723        ));
724        fs::create_dir(&path).unwrap();
725        path
726    }
727
728    #[test]
729    fn generated_runtime_records_owned_points_and_exact_short_circuit_vectors() {
730        let source = r#"fn choose(first: bool, second: bool) -> i32 {
731    if first && second { 7 } else { 3 }
732}
733
734fn main() {
735    println!("{} {}", choose(false, true), choose(true, true));
736}
737"#;
738        let transformed =
739            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
740        let runtime =
741            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
742        let directory = temporary_directory("record");
743        let input = directory.join("main.rs");
744        let binary = directory.join("program");
745        let evidence = directory.join("evidence");
746        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
747        let compile = Command::new("rustc")
748            .arg("--edition=2024")
749            .arg(&input)
750            .arg("-o")
751            .arg(&binary)
752            .output()
753            .unwrap();
754        assert!(
755            compile.status.success(),
756            "{}",
757            String::from_utf8_lossy(&compile.stderr)
758        );
759        let output = Command::new(&binary)
760            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
761            .output()
762            .unwrap();
763        assert!(output.status.success());
764        assert_eq!(output.stdout, b"3 7\n");
765        let files = read_rust_probe_directory(&evidence).unwrap();
766        assert_eq!(files.len(), 1);
767        let observations = files.values().next().unwrap();
768        let decisions = observations
769            .iter()
770            .filter_map(|observation| match observation {
771                RustProbeObservation::Decision {
772                    values, outcome, ..
773                } => Some((values.clone(), *outcome)),
774                RustProbeObservation::Hit { .. } => None,
775            })
776            .collect::<Vec<_>>();
777        assert_eq!(
778            decisions,
779            [
780                (vec![Some(false), None], false),
781                (vec![Some(true), Some(true)], true)
782            ]
783        );
784        fs::remove_dir_all(directory).unwrap();
785    }
786
787    #[test]
788    fn generated_runtime_compiles_into_a_no_std_host_crate() {
789        // `#![no_std]` swaps the std prelude for core's, so `format!`, `vec!`
790        // and `Vec` are simply not in scope. The injected module named them
791        // unqualified and every no_std crate failed to build -- found on
792        // bytes-1.12.1, which is `#![no_std]` (src/lib.rs:6). `extern crate std`
793        // here mirrors what the injected module does: it links std without
794        // restoring the prelude, which is precisely the condition under test.
795        let source = r#"#![no_std]
796
797extern crate std;
798
799fn choose(first: bool, second: bool) -> i32 {
800    if first && second { 7 } else { 3 }
801}
802
803fn main() {
804    std::println!("{} {}", choose(false, true), choose(true, true));
805}
806"#;
807        let transformed =
808            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
809        let runtime =
810            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
811        let directory = temporary_directory("no-std");
812        let input = directory.join("main.rs");
813        let binary = directory.join("program");
814        let evidence = directory.join("evidence");
815        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
816        let compile = Command::new("rustc")
817            .arg("--edition=2024")
818            .arg(&input)
819            .arg("-o")
820            .arg(&binary)
821            .output()
822            .unwrap();
823        assert!(
824            compile.status.success(),
825            "{}",
826            String::from_utf8_lossy(&compile.stderr)
827        );
828        let output = Command::new(&binary)
829            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
830            .output()
831            .unwrap();
832        assert!(output.status.success());
833        assert_eq!(output.stdout, b"3 7\n");
834        // Probes must still record, not merely compile.
835        let files = read_rust_probe_directory(&evidence).unwrap();
836        assert_eq!(files.len(), 1);
837        assert!(!files.values().next().unwrap().is_empty());
838        fs::remove_dir_all(directory).unwrap();
839    }
840
841    #[test]
842    fn probes_reached_through_a_global_allocator_do_not_recurse() {
843        // The shape from bytes-1.12.1 tests/test_bytes_vec_alloc.rs: the
844        // allocator's `alloc` calls an inherent method, which calls a FREE
845        // FUNCTION. Skipping `impl GlobalAlloc` blocks syntactically does not
846        // cover `note`, and nothing syntactic can -- the chain may leave the
847        // file or the crate. Only the runtime knows a probe is already running,
848        // so the guard has to live there. Without it this binary dies with
849        // SIGSEGV instead of printing anything.
850        let source = r#"use std::alloc::{GlobalAlloc, Layout, System};
851use std::sync::atomic::{AtomicUsize, Ordering};
852
853static SEEN: AtomicUsize = AtomicUsize::new(0);
854
855fn note(size: usize) {
856    if size > 0 {
857        SEEN.fetch_add(1, Ordering::SeqCst);
858    }
859}
860
861struct Ledger;
862
863impl Ledger {
864    fn record(&self, size: usize) {
865        note(size);
866    }
867}
868
869unsafe impl GlobalAlloc for Ledger {
870    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
871        self.record(layout.size());
872        System.alloc(layout)
873    }
874
875    unsafe fn dealloc(&self, pointer: *mut u8, layout: Layout) {
876        // dealloc must be instrumented too, or the test never exercises the
877        // ladder that actually crashed: freeing the probe's OWN buffer re-enters
878        // here, and a guard released before that free recurses without bound.
879        self.record(layout.size());
880        System.dealloc(pointer, layout);
881    }
882}
883
884#[global_allocator]
885static LEDGER: Ledger = Ledger;
886
887fn classify(flag: bool) -> usize {
888    if flag { 1 } else { 2 }
889}
890
891fn main() {
892    let held = std::vec![7u8; 32];
893    println!("{} {}", classify(!held.is_empty()), held.len());
894}
895"#;
896        let transformed =
897            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
898        // `note` is a free function, so it IS instrumented -- proving the guard,
899        // not a syntactic skip, is what prevents the recursion.
900        assert!(
901            transformed
902                .code
903                .contains("fn note(size: usize) {\ncrate::__supercov_runtime_v1::hit("),
904            "the free function reached from the allocator should still be probed"
905        );
906        let runtime =
907            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
908        let directory = temporary_directory("allocator-reentry");
909        let input = directory.join("main.rs");
910        let binary = directory.join("program");
911        let evidence = directory.join("evidence");
912        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
913        let compile = Command::new("rustc")
914            .arg("--edition=2024")
915            .arg(&input)
916            .arg("-o")
917            .arg(&binary)
918            .output()
919            .unwrap();
920        assert!(
921            compile.status.success(),
922            "{}",
923            String::from_utf8_lossy(&compile.stderr)
924        );
925        let output = Command::new(&binary)
926            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
927            .output()
928            .unwrap();
929        assert!(
930            output.status.success(),
931            "instrumented allocator did not survive: {:?}",
932            output.status
933        );
934        assert_eq!(output.stdout, b"1 32\n");
935        let files = read_rust_probe_directory(&evidence).unwrap();
936        let observations = files.values().next().unwrap();
937        let decisions = observations
938            .iter()
939            .filter_map(|observation| match observation {
940                RustProbeObservation::Decision {
941                    id,
942                    values,
943                    outcome,
944                } => Some((id.clone(), values.clone(), *outcome)),
945                RustProbeObservation::Hit { .. } => None,
946            })
947            .collect::<Vec<_>>();
948        // Suppression costs only duplicates: `classify` runs outside any probe,
949        // so its decision is still recorded exactly.
950        let classify = transformed
951            .manifest
952            .decisions
953            .iter()
954            .find(|decision| decision.source == "flag")
955            .expect("classify's decision reached the manifest");
956        assert!(
957            decisions
958                .iter()
959                .any(|(id, values, outcome)| id == &classify.id
960                    && values == &[Some(true)]
961                    && *outcome),
962            "classify's decision was lost: {decisions:?}"
963        );
964        // `note` records too -- every ordinary allocation reaches it outside a
965        // probe -- which is why suppressing the nested ones loses nothing.
966        let note = transformed
967            .manifest
968            .decisions
969            .iter()
970            .find(|decision| decision.source == "size > 0")
971            .expect("note's decision reached the manifest");
972        assert!(decisions.iter().any(|(id, ..)| id == &note.id));
973        // No frame built while nested may reach the log: a zero-width vector
974        // for a one-condition decision is a malformed record, not a lost one.
975        assert!(
976            decisions.iter().all(|(_, values, _)| values.len() == 1),
977            "a suppressed frame emitted a malformed vector: {decisions:?}"
978        );
979        fs::remove_dir_all(directory).unwrap();
980    }
981
982    #[test]
983    fn a_hit_in_a_loop_is_written_once_but_decisions_keep_every_vector() {
984        // bytes' advance_bytes_mut_remaining_capacity is a triple-nested loop of
985        // ~2.8M iterations. One write syscall per probe per iteration left it
986        // still running after four minutes. A hit only answers "did this ever
987        // run", so the repeats carry nothing -- but a decision's condition
988        // vector differs per iteration and every distinct one must survive.
989        let source = r#"fn step(value: usize) -> bool {
990    let doubled = value * 2;
991    doubled > 4
992}
993
994fn main() {
995    let mut seen = 0;
996    for value in 0..64 {
997        if step(value) { seen += 1; }
998    }
999    println!("{seen}");
1000}
1001"#;
1002        let transformed =
1003            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1004        let runtime =
1005            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1006        let directory = temporary_directory("dedup");
1007        let input = directory.join("main.rs");
1008        let binary = directory.join("program");
1009        let evidence = directory.join("evidence");
1010        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1011        let compile = Command::new("rustc")
1012            .arg("--edition=2024")
1013            .arg(&input)
1014            .arg("-o")
1015            .arg(&binary)
1016            .output()
1017            .unwrap();
1018        assert!(
1019            compile.status.success(),
1020            "{}",
1021            String::from_utf8_lossy(&compile.stderr)
1022        );
1023        let output = Command::new(&binary)
1024            .env("SUPERCOV_RUST_EVIDENCE_DIR", &evidence)
1025            .output()
1026            .unwrap();
1027        assert_eq!(output.stdout, b"61\n");
1028        let files = read_rust_probe_directory(&evidence).unwrap();
1029        let observations = files.values().next().unwrap();
1030
1031        // `doubled > 4` runs 64 times; its hit is recorded once.
1032        let mut hits = BTreeMap::<&str, usize>::new();
1033        for observation in observations {
1034            if let RustProbeObservation::Hit { id } = observation {
1035                *hits.entry(id.as_str()).or_default() += 1;
1036            }
1037        }
1038        assert!(!hits.is_empty(), "no hits recorded at all");
1039        assert!(
1040            hits.values().all(|count| *count == 1),
1041            "a repeated hit was written more than once: {hits:?}"
1042        );
1043
1044        // MC/DC needs the SET of condition vectors, not how often each recurred,
1045        // so a repeat of an already-seen vector carries nothing -- but every
1046        // DISTINCT vector must still arrive. `doubled > 4` is evaluated 64 times
1047        // and takes exactly two distinct shapes, so exactly two records survive.
1048        let decisions = observations
1049            .iter()
1050            .filter_map(|observation| match observation {
1051                RustProbeObservation::Decision {
1052                    values, outcome, ..
1053                } => Some((values.clone(), *outcome)),
1054                RustProbeObservation::Hit { .. } => None,
1055            })
1056            .collect::<Vec<_>>();
1057        assert_eq!(
1058            decisions,
1059            // In first-occurrence order: `step(0)` is false before any value
1060            // exceeds the threshold.
1061            [(vec![Some(false)], false), (vec![Some(true)], true)],
1062            "both distinct vectors must survive, and neither may repeat"
1063        );
1064        fs::remove_dir_all(directory).unwrap();
1065    }
1066
1067    #[test]
1068    fn generated_runtime_survives_a_deny_warnings_host() {
1069        // serde builds with `#![deny(warnings)]`; the injected module's
1070        // fully-qualified imports (required for no_std hosts) read as unused
1071        // imports and became hard errors. Injected code must be immune to the
1072        // host's lint policy.
1073        let source = r#"#![deny(warnings)]
1074
1075fn choose(first: bool, second: bool) -> i32 {
1076    if first && second { 7 } else { 3 }
1077}
1078
1079fn main() {
1080    println!("{} {}", choose(false, true), choose(true, true));
1081}
1082"#;
1083        let transformed =
1084            instrument_rust_source("src/main.rs", source, "crate::__supercov_runtime_v1").unwrap();
1085        let runtime =
1086            render_rust_runtime("__supercov_runtime_v1", "0123456789abcdef01234567").unwrap();
1087        let directory = temporary_directory("deny-warnings");
1088        let input = directory.join("main.rs");
1089        let binary = directory.join("program");
1090        fs::write(&input, format!("{}\n{runtime}", transformed.code)).unwrap();
1091        // --cap-lints=warn mirrors what the runner passes for the instrumented
1092        // workspace: the host policy must not reject generated code, including
1093        // the `if ({{ frame ... }})` decision wrapping that trips unused_parens.
1094        let compile = Command::new("rustc")
1095            .arg("--edition=2024")
1096            .arg("--cap-lints=warn")
1097            .arg(&input)
1098            .arg("-o")
1099            .arg(&binary)
1100            .output()
1101            .unwrap();
1102        assert!(
1103            compile.status.success(),
1104            "{}",
1105            String::from_utf8_lossy(&compile.stderr)
1106        );
1107        let output = Command::new(&binary).output().unwrap();
1108        assert_eq!(output.stdout, b"3 7\n");
1109        fs::remove_dir_all(directory).unwrap();
1110    }
1111
1112    #[test]
1113    fn reader_rejects_truncation_invalid_digits_and_non_files() {
1114        assert_eq!(
1115            parse_rust_probe_events(
1116                b"SUPERCOV-RUST-PROBE-1\nD\trs:decision:0123456789abcdef01234567\t03\t1\n"
1117            ),
1118            Err(RustProbeReadError::InvalidRecord(2))
1119        );
1120        assert_eq!(
1121            parse_rust_probe_events(b"SUPERCOV-RUST-PROBE-"),
1122            Err(RustProbeReadError::InvalidHeader)
1123        );
1124
1125        let directory = temporary_directory("unsafe");
1126        fs::create_dir(directory.join("nested.events")).unwrap();
1127        assert!(matches!(
1128            read_rust_probe_directory(&directory),
1129            Err(RustProbeReadError::UnsafeEntry(_))
1130        ));
1131        fs::remove_dir_all(directory).unwrap();
1132    }
1133}