Skip to main content

openentropy_core/sources/
helpers.rs

1//! Shared helpers used by multiple entropy source implementations.
2//!
3//! This module prevents code duplication across sources that need common
4//! low-level primitives like high-resolution timestamps and LSB extraction.
5
6// ---------------------------------------------------------------------------
7// High-resolution timing
8// ---------------------------------------------------------------------------
9
10/// High-resolution timestamp in nanoseconds.
11///
12/// On macOS, this reads the ARM system counter directly via `mach_absolute_time()`.
13/// On other platforms, it falls back to `std::time::Instant` relative to a
14/// process-local epoch.
15#[cfg(target_os = "macos")]
16pub fn mach_time() -> u64 {
17    unsafe extern "C" {
18        fn mach_absolute_time() -> u64;
19    }
20    // SAFETY: mach_absolute_time() is a stable macOS API that returns the
21    // current value of the system absolute time counter. Always safe to call.
22    unsafe { mach_absolute_time() }
23}
24
25#[cfg(not(target_os = "macos"))]
26pub fn mach_time() -> u64 {
27    use std::sync::OnceLock;
28    use std::time::Instant;
29    static EPOCH: OnceLock<Instant> = OnceLock::new();
30    let epoch = EPOCH.get_or_init(Instant::now);
31    epoch.elapsed().as_nanos() as u64
32}
33
34// ---------------------------------------------------------------------------
35// LSB extraction
36// ---------------------------------------------------------------------------
37
38/// Pack a stream of individual bits (0 or 1) into bytes (MSB-first packing).
39///
40/// For every 8 input bits, one output byte is produced.
41fn pack_bits_into_bytes(bits: &[u8]) -> Vec<u8> {
42    let mut bytes = Vec::with_capacity(bits.len() / 8);
43    for chunk in bits.chunks_exact(8) {
44        let mut byte = 0u8;
45        for (i, &bit) in chunk.iter().enumerate() {
46            byte |= bit << (7 - i);
47        }
48        bytes.push(byte);
49    }
50    bytes
51}
52
53/// Extract the least-significant bit of each `u64` delta and pack into bytes.
54///
55/// For every 8 input values, one output byte is produced (MSB-first packing).
56pub fn extract_lsbs_u64(deltas: &[u64]) -> Vec<u8> {
57    let bits: Vec<u8> = deltas.iter().map(|d| (d & 1) as u8).collect();
58    pack_bits_into_bytes(&bits)
59}
60
61/// Extract the least-significant bit of each `i64` delta and pack into bytes.
62///
63/// Identical to [`extract_lsbs_u64`] but for signed deltas.
64pub fn extract_lsbs_i64(deltas: &[i64]) -> Vec<u8> {
65    let bits: Vec<u8> = deltas.iter().map(|d| (d & 1) as u8).collect();
66    pack_bits_into_bytes(&bits)
67}
68
69// ---------------------------------------------------------------------------
70// Shared command utilities
71// ---------------------------------------------------------------------------
72
73/// Check if a command exists by running `which`.
74pub fn command_exists(name: &str) -> bool {
75    std::process::Command::new("which")
76        .arg(name)
77        .stdout(std::process::Stdio::null())
78        .stderr(std::process::Stdio::null())
79        .status()
80        .map(|s| s.success())
81        .unwrap_or(false)
82}
83
84/// Execute a command and return its `Output` if it succeeds.
85///
86/// Uses a 30-second timeout to prevent indefinite hangs from system utilities
87/// that might block (e.g. `ioreg -l -w0` on complex IOKit trees).
88fn run_command_output(program: &str, args: &[&str]) -> Option<std::process::Output> {
89    run_command_output_timeout(program, args, 30_000)
90}
91
92/// Run a subprocess command with a timeout and return full `Output`.
93///
94/// If the process does not finish within `timeout_ms`, it is killed and `None`
95/// is returned. Stderr is suppressed to keep entropy collection paths quiet.
96pub fn run_command_output_timeout(
97    program: &str,
98    args: &[&str],
99    timeout_ms: u64,
100) -> Option<std::process::Output> {
101    use std::io::Read;
102    use std::process::{Command, Stdio};
103    use std::thread;
104    use std::time::{Duration, Instant};
105
106    let mut child = Command::new(program)
107        .args(args)
108        .stdin(Stdio::null())
109        .stdout(Stdio::piped())
110        .stderr(Stdio::null())
111        .spawn()
112        .ok()?;
113
114    // Drain stdout concurrently so producers like ffmpeg rawvideo do not block
115    // on a full pipe before process exit.
116    let mut stdout_reader = child.stdout.take().map(|mut stdout| {
117        thread::spawn(move || {
118            let mut buf = Vec::new();
119            let _ = stdout.read_to_end(&mut buf);
120            buf
121        })
122    });
123
124    let start = Instant::now();
125    let timeout = Duration::from_millis(timeout_ms.max(1));
126
127    loop {
128        match child.try_wait() {
129            Ok(Some(status)) => {
130                let stdout = stdout_reader
131                    .take()
132                    .and_then(|h| h.join().ok())
133                    .unwrap_or_default();
134                let output = std::process::Output {
135                    status,
136                    stdout,
137                    stderr: Vec::new(),
138                };
139                return output.status.success().then_some(output);
140            }
141            Ok(None) => {
142                if start.elapsed() >= timeout {
143                    let _ = child.kill();
144                    let _ = child.wait();
145                    if let Some(reader) = stdout_reader.take() {
146                        let _ = reader.join();
147                    }
148                    return None;
149                }
150                thread::sleep(Duration::from_millis(10));
151            }
152            Err(_e) => {
153                let _ = child.kill();
154                let _ = child.wait();
155                if let Some(reader) = stdout_reader.take() {
156                    let _ = reader.join();
157                }
158                return None;
159            }
160        }
161    }
162}
163
164/// Run a subprocess command and return its stdout as a `String`.
165///
166/// Returns `None` if the command fails to execute or exits with a non-zero
167/// status. This is the shared helper for sources that shell out to system
168/// utilities (sysctl, vm_stat, ps, ioreg, mdls, etc.).
169pub fn run_command(program: &str, args: &[&str]) -> Option<String> {
170    let output = run_command_output(program, args)?;
171    Some(String::from_utf8_lossy(&output.stdout).into_owned())
172}
173
174/// Run a subprocess command and return its raw stdout bytes.
175///
176/// Returns `None` if the command fails to execute or exits with a non-zero
177/// status.
178pub fn run_command_raw(program: &str, args: &[&str]) -> Option<Vec<u8>> {
179    run_command_output(program, args).map(|o| o.stdout)
180}
181
182/// Run a subprocess command with timeout and return raw stdout bytes.
183pub fn run_command_raw_timeout(program: &str, args: &[&str], timeout_ms: u64) -> Option<Vec<u8>> {
184    run_command_output_timeout(program, args, timeout_ms).map(|o| o.stdout)
185}
186
187/// Capture one grayscale frame from the camera via ffmpeg/avfoundation.
188///
189/// If `device_index` is `Some(n)`, only the selector `"{n}:none"` is tried.
190/// If `None`, tries common avfoundation input selectors in fallback order
191/// (`default:none`, `0:none`, `1:none`, `0:0`). Returns raw 8-bit grayscale
192/// bytes when any selector succeeds.
193pub fn capture_camera_gray_frame(timeout_ms: u64, device_index: Option<u32>) -> Option<Vec<u8>> {
194    match device_index {
195        Some(n) => {
196            let input = format!("{n}:none");
197            capture_camera_with_inputs(timeout_ms, &[&input])
198        }
199        None => {
200            capture_camera_with_inputs(timeout_ms, &["default:none", "0:none", "1:none", "0:0"])
201        }
202    }
203}
204
205fn capture_camera_with_inputs(timeout_ms: u64, inputs: &[&str]) -> Option<Vec<u8>> {
206    for &input in inputs {
207        let args = [
208            "-hide_banner",
209            "-loglevel",
210            "error",
211            "-nostdin",
212            "-f",
213            "avfoundation",
214            "-framerate",
215            "30",
216            "-i",
217            input,
218            "-frames:v",
219            "1",
220            "-f",
221            "rawvideo",
222            "-pix_fmt",
223            "gray",
224            "pipe:1",
225        ];
226        if let Some(frame) = run_command_raw_timeout("ffmpeg", &args, timeout_ms)
227            && !frame.is_empty()
228        {
229            return Some(frame);
230        }
231    }
232    None
233}
234
235// ---------------------------------------------------------------------------
236// ARM counter (CNTVCT_EL0)
237// ---------------------------------------------------------------------------
238
239/// Read the ARM generic timer counter (CNTVCT_EL0) directly.
240///
241/// Returns the raw hardware counter driven by the CPU's 24 MHz crystal oscillator,
242/// independent of any OS abstraction layer. Used by frontier sources that measure
243/// clock domain crossings against independent PLLs (audio, display, PCIe).
244#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
245#[inline(always)]
246pub fn read_cntvct() -> u64 {
247    let val: u64;
248    // SAFETY: CNTVCT_EL0 is always readable from EL0 on Apple Silicon.
249    // Read-only system register, no side effects.
250    unsafe {
251        std::arch::asm!("mrs {}, cntvct_el0", out(reg) val, options(nostack, nomem));
252    }
253    val
254}
255
256#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
257#[inline(always)]
258pub fn read_cntvct() -> u64 {
259    0
260}
261
262// ---------------------------------------------------------------------------
263// Safe JIT instruction probe
264// ---------------------------------------------------------------------------
265
266/// Test whether a JIT-generated ARM64 instruction can execute without SIGILL.
267///
268/// Forks a child process that builds a MAP_JIT page with the given instruction
269/// followed by RET, executes it, and exits. If the child exits normally (status 0),
270/// the instruction is safe. If it crashes (SIGILL), only the child dies.
271///
272/// Returns `true` if the instruction executed successfully.
273#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
274pub fn probe_jit_instruction_safe(mrs_instr: u32) -> bool {
275    // Fork: child tests the instruction, parent waits for result.
276    let pid = unsafe { libc::fork() };
277    if pid < 0 {
278        return false; // fork failed
279    }
280    if pid == 0 {
281        // Child process: build JIT page, execute, exit
282        let page = unsafe {
283            libc::mmap(
284                std::ptr::null_mut(),
285                4096,
286                libc::PROT_READ | libc::PROT_WRITE | libc::PROT_EXEC,
287                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | 0x0800, // MAP_JIT
288                -1,
289                0,
290            )
291        };
292        if page == libc::MAP_FAILED {
293            unsafe { libc::_exit(1) };
294        }
295        unsafe {
296            libc::pthread_jit_write_protect_np(0);
297            let code = page as *mut u32;
298            code.write(mrs_instr);
299            code.add(1).write(0xD65F03C0u32); // RET
300            libc::pthread_jit_write_protect_np(1);
301            core::arch::asm!("dc cvau, {p}", "ic ivau, {p}", p = in(reg) page, options(nostack));
302            core::arch::asm!("dsb ish", "isb", options(nostack));
303        }
304        type FnPtr = unsafe extern "C" fn() -> u64;
305        let fn_ptr: FnPtr = unsafe { std::mem::transmute(page) };
306        let _val = unsafe { fn_ptr() };
307        unsafe {
308            libc::munmap(page, 4096);
309            libc::_exit(0); // Success — instruction didn't trap
310        }
311    }
312    // Parent: wait for child
313    let mut status: libc::c_int = 0;
314    let ret = unsafe { libc::waitpid(pid, &mut status, 0) };
315    if ret < 0 {
316        return false;
317    }
318    // Check child exited normally with status 0
319    libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0
320}
321
322// ---------------------------------------------------------------------------
323// XOR-fold
324// ---------------------------------------------------------------------------
325
326/// XOR-fold all 8 bytes of a `u64` into a single byte.
327///
328/// Preserves entropy from every byte position instead of discarding the
329/// upper 7 bytes. Used by timing-based sources where entropy is spread
330/// across multiple byte positions.
331#[inline]
332pub fn xor_fold_u64(v: u64) -> u8 {
333    let b = v.to_le_bytes();
334    b[0] ^ b[1] ^ b[2] ^ b[3] ^ b[4] ^ b[5] ^ b[6] ^ b[7]
335}
336
337// ---------------------------------------------------------------------------
338// Timing entropy extraction
339// ---------------------------------------------------------------------------
340
341/// Extract entropy bytes from a slice of raw timestamps.
342///
343/// Computes consecutive deltas, XORs adjacent deltas for mixing, then
344/// XOR-folds each 8-byte value into one output byte. Returns at most
345/// `n_samples` bytes.
346///
347/// Requires at least 4 input timings to produce any output (2 deltas
348/// needed for the XOR mixing step).
349pub fn extract_timing_entropy(timings: &[u64], n_samples: usize) -> Vec<u8> {
350    if timings.len() < 2 {
351        return Vec::new();
352    }
353
354    let deltas: Vec<u64> = timings
355        .windows(2)
356        .map(|w| w[1].wrapping_sub(w[0]))
357        .collect();
358
359    // XOR consecutive deltas for mixing (not conditioning — just combines adjacent values)
360    let xored: Vec<u64> = deltas.windows(2).map(|w| w[0] ^ w[1]).collect();
361
362    // XOR-fold all 8 bytes of each value into one byte
363    let mut raw: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
364    raw.truncate(n_samples);
365    raw
366}
367
368// ---------------------------------------------------------------------------
369// Nibble packing
370// ---------------------------------------------------------------------------
371
372/// Pack pairs of 4-bit nibbles into bytes.
373///
374/// Used by audio and camera sources to pack noise LSBs efficiently.
375/// Returns at most `max_bytes` output bytes.
376pub fn pack_nibbles(nibbles: impl Iterator<Item = u8>, max_bytes: usize) -> Vec<u8> {
377    let mut output = Vec::with_capacity(max_bytes);
378    let mut buf: u8 = 0;
379    let mut count: u8 = 0;
380
381    for nibble in nibbles {
382        if count == 0 {
383            buf = nibble << 4;
384            count = 1;
385        } else {
386            buf |= nibble;
387            output.push(buf);
388            count = 0;
389            if output.len() >= max_bytes {
390                break;
391            }
392        }
393    }
394
395    // If we have an odd nibble left and still need more, include it.
396    if count == 1 && output.len() < max_bytes {
397        output.push(buf);
398    }
399
400    output.truncate(max_bytes);
401    output
402}
403
404// ---------------------------------------------------------------------------
405// i64 delta byte extraction (sysctl/vmstat/ioregistry pattern)
406// ---------------------------------------------------------------------------
407
408/// Extract entropy bytes from a list of i64 deltas.
409///
410/// First emits raw LE bytes from all deltas, then XOR'd consecutive delta bytes
411/// if more output is needed. Returns at most `n_samples` bytes.
412pub fn extract_delta_bytes_i64(deltas: &[i64], n_samples: usize) -> Vec<u8> {
413    // XOR consecutive deltas for extra mixing
414    let xor_deltas: Vec<i64> = if deltas.len() >= 2 {
415        deltas.windows(2).map(|w| w[0] ^ w[1]).collect()
416    } else {
417        Vec::new()
418    };
419
420    let mut entropy = Vec::with_capacity(n_samples);
421
422    // First: raw LE bytes from all non-zero deltas
423    for d in deltas {
424        for &b in &d.to_le_bytes() {
425            entropy.push(b);
426        }
427        if entropy.len() >= n_samples {
428            entropy.truncate(n_samples);
429            return entropy;
430        }
431    }
432
433    // Then: XOR'd delta bytes for more mixing
434    for d in &xor_deltas {
435        for &b in &d.to_le_bytes() {
436            entropy.push(b);
437        }
438        if entropy.len() >= n_samples {
439            break;
440        }
441    }
442
443    entropy.truncate(n_samples);
444    entropy
445}
446
447// ---------------------------------------------------------------------------
448// Von Neumann debiased timing extraction
449// ---------------------------------------------------------------------------
450
451/// Von Neumann debiased timing extraction.
452///
453/// Takes pairs of consecutive timing deltas. If they differ, emit one bit
454/// based on their relative order (first < second → 1, else → 0). This
455/// removes bias from the raw timing stream at the cost of ~50% data loss.
456#[cfg(target_os = "macos")]
457pub fn extract_timing_entropy_debiased(timings: &[u64], n_samples: usize) -> Vec<u8> {
458    if timings.len() < 4 {
459        return Vec::new();
460    }
461
462    let deltas: Vec<u64> = timings
463        .windows(2)
464        .map(|w| w[1].wrapping_sub(w[0]))
465        .collect();
466
467    // Von Neumann debias: take pairs, discard equal, emit comparison bit.
468    let mut debiased_bits: Vec<u8> = Vec::with_capacity(deltas.len() / 2);
469    for pair in deltas.chunks_exact(2) {
470        if pair[0] != pair[1] {
471            debiased_bits.push(if pair[0] < pair[1] { 1 } else { 0 });
472        }
473    }
474
475    // Pack bits into bytes (only full bytes).
476    let mut bytes = Vec::with_capacity(n_samples);
477    for chunk in debiased_bits.chunks(8) {
478        if chunk.len() < 8 {
479            break;
480        }
481        let mut byte = 0u8;
482        for (i, &bit) in chunk.iter().enumerate() {
483            byte |= bit << (7 - i);
484        }
485        bytes.push(byte);
486        if bytes.len() >= n_samples {
487            break;
488        }
489    }
490    bytes.truncate(n_samples);
491    bytes
492}
493
494// ---------------------------------------------------------------------------
495// Timing variance extraction
496// ---------------------------------------------------------------------------
497
498/// Extract entropy from timing variance (delta-of-deltas).
499///
500/// Computes first-order deltas, then second-order deltas (capturing the
501/// *change* in timing). This removes systematic bias and amplifies the
502/// nondeterministic component.
503pub fn extract_timing_entropy_variance(timings: &[u64], n_samples: usize) -> Vec<u8> {
504    if timings.len() < 4 {
505        return Vec::new();
506    }
507
508    let deltas: Vec<u64> = timings
509        .windows(2)
510        .map(|w| w[1].wrapping_sub(w[0]))
511        .collect();
512
513    let variance: Vec<u64> = deltas.windows(2).map(|w| w[1].wrapping_sub(w[0])).collect();
514
515    let xored: Vec<u64> = variance.windows(2).map(|w| w[0] ^ w[1]).collect();
516
517    let mut raw: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
518    raw.truncate(n_samples);
519    raw
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    // -----------------------------------------------------------------------
527    // LSB extraction tests
528    // -----------------------------------------------------------------------
529
530    #[test]
531    fn extract_lsbs_u64_basic() {
532        // 8 values with alternating LSBs: 0,1,0,1,0,1,0,1 → 0b01010101 = 0x55
533        let deltas = vec![2, 3, 4, 5, 6, 7, 8, 9];
534        let bytes = extract_lsbs_u64(&deltas);
535        assert_eq!(bytes.len(), 1);
536        assert_eq!(bytes[0], 0b01010101);
537    }
538
539    #[test]
540    fn extract_lsbs_i64_basic() {
541        let deltas = vec![2i64, 3, 4, 5, 6, 7, 8, 9];
542        let bytes = extract_lsbs_i64(&deltas);
543        assert_eq!(bytes.len(), 1);
544        assert_eq!(bytes[0], 0b01010101);
545    }
546
547    #[test]
548    fn extract_lsbs_u64_empty() {
549        let bytes = extract_lsbs_u64(&[]);
550        assert!(bytes.is_empty());
551    }
552
553    #[test]
554    fn extract_lsbs_i64_empty() {
555        let bytes = extract_lsbs_i64(&[]);
556        assert!(bytes.is_empty());
557    }
558
559    #[test]
560    fn extract_lsbs_u64_all_odd() {
561        // All odd -> all LSBs are 1 -> 0xFF
562        let deltas = vec![1u64, 3, 5, 7, 9, 11, 13, 15];
563        let bytes = extract_lsbs_u64(&deltas);
564        assert_eq!(bytes[0], 0xFF);
565    }
566
567    #[test]
568    fn extract_lsbs_u64_all_even() {
569        // All even -> all LSBs are 0 -> 0x00
570        let deltas = vec![0u64, 2, 4, 6, 8, 10, 12, 14];
571        let bytes = extract_lsbs_u64(&deltas);
572        assert_eq!(bytes[0], 0x00);
573    }
574
575    #[test]
576    fn extract_lsbs_partial_byte() {
577        // 5 values -> only 5 bits, not enough for a full byte -> empty
578        let deltas = vec![1u64, 0, 1, 0, 1];
579        let bytes = extract_lsbs_u64(&deltas);
580        assert!(bytes.is_empty());
581    }
582
583    #[test]
584    fn extract_lsbs_u64_i64_agree() {
585        // Same absolute values should produce same LSBs
586        let u_deltas = vec![1u64, 2, 3, 4, 5, 6, 7, 8];
587        let i_deltas = vec![1i64, 2, 3, 4, 5, 6, 7, 8];
588        assert_eq!(extract_lsbs_u64(&u_deltas), extract_lsbs_i64(&i_deltas));
589    }
590
591    // -----------------------------------------------------------------------
592    // pack_bits_into_bytes tests
593    // -----------------------------------------------------------------------
594
595    #[test]
596    fn pack_bits_empty() {
597        let bits: Vec<u8> = vec![];
598        let bytes = pack_bits_into_bytes(&bits);
599        assert!(bytes.is_empty());
600    }
601
602    #[test]
603    fn pack_bits_full_byte() {
604        let bits = vec![1, 0, 1, 0, 1, 0, 1, 0];
605        let bytes = pack_bits_into_bytes(&bits);
606        assert_eq!(bytes, vec![0b10101010]);
607    }
608
609    // -----------------------------------------------------------------------
610    // mach_time tests
611    // -----------------------------------------------------------------------
612
613    #[test]
614    fn mach_time_is_monotonic() {
615        let t1 = mach_time();
616        let t2 = mach_time();
617        assert!(t2 >= t1);
618    }
619
620    // -----------------------------------------------------------------------
621    // pack_nibbles tests
622    // -----------------------------------------------------------------------
623
624    #[test]
625    fn pack_nibbles_basic() {
626        let nibbles = vec![0x0A_u8, 0x0B];
627        let bytes = pack_nibbles(nibbles.into_iter(), 10);
628        assert_eq!(bytes.len(), 1);
629        assert_eq!(bytes[0], 0xAB);
630    }
631
632    #[test]
633    fn pack_nibbles_empty() {
634        let bytes = pack_nibbles(std::iter::empty(), 10);
635        assert!(bytes.is_empty());
636    }
637
638    #[test]
639    fn pack_nibbles_odd_count() {
640        let nibbles = vec![0x0C_u8, 0x0D, 0x0E];
641        let bytes = pack_nibbles(nibbles.into_iter(), 10);
642        assert_eq!(bytes.len(), 2);
643        assert_eq!(bytes[0], 0xCD);
644        assert_eq!(bytes[1], 0xE0); // odd nibble shifted left
645    }
646
647    #[test]
648    fn pack_nibbles_respects_max() {
649        let nibbles = vec![0x01_u8, 0x02, 0x03, 0x04, 0x05, 0x06];
650        let bytes = pack_nibbles(nibbles.into_iter(), 2);
651        assert_eq!(bytes.len(), 2);
652    }
653
654    // -----------------------------------------------------------------------
655    // extract_delta_bytes_i64 tests
656    // -----------------------------------------------------------------------
657
658    #[test]
659    fn extract_delta_bytes_empty() {
660        let bytes = extract_delta_bytes_i64(&[], 10);
661        assert!(bytes.is_empty());
662    }
663
664    #[test]
665    fn extract_delta_bytes_single_delta() {
666        let deltas = vec![0x0102030405060708i64];
667        let bytes = extract_delta_bytes_i64(&deltas, 8);
668        // LE bytes of the delta
669        assert_eq!(bytes, 0x0102030405060708i64.to_le_bytes().to_vec());
670    }
671
672    #[test]
673    fn extract_delta_bytes_truncated() {
674        let deltas = vec![0x0102030405060708i64];
675        let bytes = extract_delta_bytes_i64(&deltas, 4);
676        assert_eq!(bytes.len(), 4);
677        assert_eq!(bytes, &0x0102030405060708i64.to_le_bytes()[..4]);
678    }
679
680    #[test]
681    fn extract_delta_bytes_with_xor_mixing() {
682        // Two deltas -> also produces XOR'd delta bytes for extra output
683        let deltas = vec![100i64, 200];
684        let bytes = extract_delta_bytes_i64(&deltas, 24);
685        // 2 deltas * 8 bytes = 16 raw LE bytes + 1 XOR'd delta * 8 bytes = 24 total
686        assert_eq!(bytes.len(), 24);
687    }
688
689    #[test]
690    fn extract_delta_bytes_respects_n_samples() {
691        let deltas: Vec<i64> = (1..=100).collect();
692        let bytes = extract_delta_bytes_i64(&deltas, 50);
693        assert_eq!(bytes.len(), 50);
694    }
695
696    // -----------------------------------------------------------------------
697    // xor_fold_u64 tests
698    // -----------------------------------------------------------------------
699
700    #[test]
701    fn xor_fold_u64_zero() {
702        assert_eq!(xor_fold_u64(0), 0);
703    }
704
705    #[test]
706    fn xor_fold_u64_identical_bytes() {
707        // All bytes the same: XOR-fold of 8 identical bytes = 0 (even count)
708        assert_eq!(xor_fold_u64(0x0101010101010101), 0);
709    }
710
711    #[test]
712    fn xor_fold_u64_single_byte() {
713        assert_eq!(xor_fold_u64(0xFF), 0xFF);
714    }
715
716    #[test]
717    fn xor_fold_u64_two_bytes() {
718        // 0xAA ^ 0xBB = 0x11
719        assert_eq!(xor_fold_u64(0xBB_00_00_00_00_00_00_AA), 0xAA ^ 0xBB);
720    }
721
722    #[test]
723    fn xor_fold_u64_max() {
724        // All 0xFF bytes: XOR of 8 identical = 0 (even count)
725        assert_eq!(xor_fold_u64(u64::MAX), 0);
726    }
727
728    // -----------------------------------------------------------------------
729    // extract_timing_entropy tests
730    // -----------------------------------------------------------------------
731
732    #[test]
733    fn extract_timing_entropy_basic() {
734        let timings = vec![100, 110, 105, 120, 108, 130, 112, 125];
735        let result = extract_timing_entropy(&timings, 4);
736        assert!(!result.is_empty());
737        assert!(result.len() <= 4);
738    }
739
740    #[test]
741    fn extract_timing_entropy_too_few_samples() {
742        assert!(extract_timing_entropy(&[], 10).is_empty());
743        assert!(extract_timing_entropy(&[42], 10).is_empty());
744    }
745
746    #[test]
747    fn extract_timing_entropy_exactly_two_timings() {
748        // 2 timings → 1 delta → 0 XOR'd pairs → empty
749        let result = extract_timing_entropy(&[100, 200], 10);
750        assert!(result.is_empty());
751    }
752
753    #[test]
754    fn extract_timing_entropy_exactly_three_timings() {
755        // 3 timings → 2 deltas → 1 XOR'd pair → 1 byte
756        let result = extract_timing_entropy(&[100, 200, 150], 10);
757        assert_eq!(result.len(), 1);
758    }
759
760    #[test]
761    fn extract_timing_entropy_truncates_to_n_samples() {
762        let timings: Vec<u64> = (0..100).collect();
763        let result = extract_timing_entropy(&timings, 5);
764        assert_eq!(result.len(), 5);
765    }
766
767    #[test]
768    fn extract_timing_entropy_constant_timings() {
769        // Constant timings → all deltas are 0 → XOR of 0s = 0 → all zeros
770        let timings = vec![42u64; 20];
771        let result = extract_timing_entropy(&timings, 10);
772        assert!(result.iter().all(|&b| b == 0));
773    }
774
775    // -----------------------------------------------------------------------
776    // run_command / run_command_raw tests
777    // -----------------------------------------------------------------------
778
779    #[test]
780    fn run_command_echo() {
781        let out = run_command("echo", &["hello"]);
782        assert!(out.is_some());
783        assert_eq!(out.unwrap().trim(), "hello");
784    }
785
786    #[test]
787    fn run_command_nonexistent() {
788        let out = run_command("/nonexistent/binary", &[]);
789        assert!(out.is_none());
790    }
791
792    #[test]
793    fn run_command_raw_echo() {
794        let out = run_command_raw("echo", &["bytes"]);
795        assert!(out.is_some());
796        assert!(out.unwrap().starts_with(b"bytes"));
797    }
798
799    #[test]
800    fn run_command_failing_status() {
801        // `false` always exits with status 1
802        let out = run_command("false", &[]);
803        assert!(out.is_none());
804    }
805
806    #[test]
807    fn run_command_raw_failing_status() {
808        let out = run_command_raw("false", &[]);
809        assert!(out.is_none());
810    }
811
812    #[test]
813    fn run_command_empty_output() {
814        // `true` exits 0 with no output
815        let out = run_command("true", &[]);
816        assert!(out.is_some());
817        assert!(out.unwrap().is_empty());
818    }
819
820    #[test]
821    fn command_exists_true() {
822        assert!(command_exists("echo"));
823    }
824
825    #[test]
826    fn command_exists_false() {
827        assert!(!command_exists("nonexistent_binary_xyz_12345"));
828    }
829
830    // -----------------------------------------------------------------------
831    // extract_timing_entropy_debiased tests
832    // -----------------------------------------------------------------------
833
834    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
835    #[test]
836    fn debiased_extraction_basic() {
837        let timings: Vec<u64> = (0..200).map(|i| 100 + (i * 7 + i * i) % 50).collect();
838        let result = extract_timing_entropy_debiased(&timings, 10);
839        assert!(result.len() <= 10);
840    }
841
842    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
843    #[test]
844    fn debiased_extraction_too_few() {
845        assert!(extract_timing_entropy_debiased(&[1, 2, 3], 10).is_empty());
846        assert!(extract_timing_entropy_debiased(&[], 10).is_empty());
847    }
848
849    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
850    #[test]
851    fn debiased_extraction_constant_input() {
852        let timings = vec![42u64; 100];
853        let result = extract_timing_entropy_debiased(&timings, 10);
854        assert!(result.is_empty());
855    }
856
857    // -----------------------------------------------------------------------
858    // extract_timing_entropy_variance tests
859    // -----------------------------------------------------------------------
860
861    #[test]
862    fn variance_extraction_basic() {
863        let timings: Vec<u64> = (0..100).map(|i| 100 + (i * 7 + i * i) % 50).collect();
864        let result = extract_timing_entropy_variance(&timings, 10);
865        assert!(!result.is_empty());
866        assert!(result.len() <= 10);
867    }
868
869    #[test]
870    fn variance_extraction_too_few() {
871        assert!(extract_timing_entropy_variance(&[1, 2, 3], 10).is_empty());
872    }
873}