openentropy_core/sources/signal/
spotlight_timing.rs1use std::process::Command;
4use std::time::{Duration, Instant};
5
6use crate::source::{EntropySource, Platform, SourceCategory, SourceInfo};
7
8use crate::sources::helpers::extract_timing_entropy;
9
10const SPOTLIGHT_FILES: &[&str] = &[
16 "/usr/bin/true",
17 "/usr/bin/false",
18 "/usr/bin/env",
19 "/usr/bin/which",
20];
21
22const MDLS_PATH: &str = "/usr/bin/mdls";
24
25const MDLS_TIMEOUT: Duration = Duration::from_millis(150);
27
28static SPOTLIGHT_TIMING_INFO: SourceInfo = SourceInfo {
29 name: "spotlight_timing",
30 description: "Spotlight metadata index query timing jitter via mdls",
31 physics: "Queries Spotlight\u{2019}s metadata index (mdls) and measures response time. \
32 The index is a complex B-tree/inverted index structure. Query timing depends \
33 on: index size, disk cache residency, concurrent indexing activity, and \
34 filesystem metadata state. When Spotlight is actively indexing new files, \
35 query latency becomes highly variable.",
36 category: SourceCategory::Signal,
37 platform: Platform::MacOS,
38 requirements: &[],
39 entropy_rate_estimate: 2.0,
40 composite: false,
41 is_fast: false,
42};
43
44pub struct SpotlightTimingSource;
46
47impl EntropySource for SpotlightTimingSource {
48 fn info(&self) -> &SourceInfo {
49 &SPOTLIGHT_TIMING_INFO
50 }
51
52 fn is_available(&self) -> bool {
53 std::path::Path::new(MDLS_PATH).exists()
54 }
55
56 fn collect(&self, n_samples: usize) -> Vec<u8> {
57 let raw_count = (n_samples + 16).min(48);
61 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
62 let file_count = SPOTLIGHT_FILES.len();
63 let deadline = Instant::now() + Duration::from_secs(2);
64
65 for i in 0..raw_count {
66 if Instant::now() >= deadline {
67 break;
68 }
69 let file = SPOTLIGHT_FILES[i % file_count];
70
71 let t0 = Instant::now();
74
75 let child = Command::new(MDLS_PATH)
76 .args(["-name", "kMDItemFSName", file])
77 .stdout(std::process::Stdio::null())
78 .stderr(std::process::Stdio::null())
79 .spawn();
80
81 if let Ok(mut child) = child {
82 let per_cmd_deadline = Instant::now() + MDLS_TIMEOUT;
83 loop {
84 match child.try_wait() {
85 Ok(Some(_)) => break,
86 Ok(None) => {
87 if Instant::now() >= per_cmd_deadline {
88 let _ = child.kill();
89 let _ = child.wait();
90 break;
91 }
92 std::thread::sleep(Duration::from_millis(5));
93 }
94 Err(_) => break,
95 }
96 }
97 }
98
99 let elapsed_ns = t0.elapsed().as_nanos() as u64;
101 timings.push(elapsed_ns);
102 }
103
104 extract_timing_entropy(&timings, n_samples)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::sources::helpers::extract_lsbs_u64;
112
113 #[test]
114 fn spotlight_timing_info() {
115 let src = SpotlightTimingSource;
116 assert_eq!(src.name(), "spotlight_timing");
117 assert_eq!(src.info().category, SourceCategory::Signal);
118 assert!((src.info().entropy_rate_estimate - 2.0).abs() < f64::EPSILON);
119 }
120
121 #[test]
122 #[cfg(target_os = "macos")]
123 #[ignore] fn spotlight_timing_collects_bytes() {
125 let src = SpotlightTimingSource;
126 if src.is_available() {
127 let data = src.collect(32);
128 assert!(!data.is_empty());
129 assert!(data.len() <= 32);
130 }
131 }
132
133 #[test]
134 fn extract_lsbs_packing() {
135 let deltas = vec![1u64, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0];
136 let bytes = extract_lsbs_u64(&deltas);
137 assert_eq!(bytes.len(), 2);
138 assert_eq!(bytes[0], 0xAA);
140 assert_eq!(bytes[1], 0xF0);
142 }
143}