1#![forbid(unsafe_code)]
12
13#[derive(Debug, PartialEq, Eq)]
15pub enum PrefetchError {
16 TooShort,
18 BadSignature,
20 Decompress(xpress_huffman::Error),
22 UnsupportedVersion(u32),
26 TruncatedRecord,
28}
29
30const MAM_SIGNATURE: &[u8; 3] = b"MAM";
31const MAM_XPRESS_HUFFMAN: u8 = 0x04;
33pub const SCCA_SIGNATURE: &[u8; 4] = b"SCCA";
37pub const SCCA_SIGNATURE_OFFSET: usize = 4;
39
40pub fn decompress(data: &[u8]) -> Result<Vec<u8>, PrefetchError> {
46 if data.len() < 8 {
47 return Err(PrefetchError::TooShort);
48 }
49 if &data[SCCA_SIGNATURE_OFFSET..SCCA_SIGNATURE_OFFSET + 4] == SCCA_SIGNATURE {
52 return Ok(data.to_vec());
53 }
54 if &data[0..3] != MAM_SIGNATURE || data[3] != MAM_XPRESS_HUFFMAN {
55 return Err(PrefetchError::BadSignature);
56 }
57 let decompressed_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
58 xpress_huffman::decompress(&data[8..], decompressed_size).map_err(PrefetchError::Decompress)
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct VolumeInfo {
66 pub device_path: String,
68 pub serial: u32,
70 pub creation_time: i64,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct PrefetchInfo {
77 pub version: u32,
79 pub executable: String,
81 pub run_count: u32,
83 pub last_run_times: Vec<i64>,
85 pub volumes: Vec<VolumeInfo>,
87 pub filenames: Vec<String>,
89}
90
91fn rd_u32(d: &[u8], off: usize) -> Option<u32> {
93 d.get(off..off + 4)
94 .map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
95}
96
97fn rd_i64(d: &[u8], off: usize) -> Option<i64> {
99 d.get(off..off + 8).map(|s| {
100 let mut a = [0u8; 8];
101 a.copy_from_slice(s);
102 i64::from_le_bytes(a)
103 })
104}
105
106fn rd_utf16_z(d: &[u8], off: usize, byte_len: usize) -> Option<String> {
109 let s = d.get(off..off + byte_len)?;
110 let units: Vec<u16> = s
111 .chunks_exact(2)
112 .map(|c| u16::from_le_bytes([c[0], c[1]]))
113 .take_while(|&u| u != 0)
114 .collect();
115 Some(String::from_utf16_lossy(&units))
116}
117
118pub fn parse(file_bytes: &[u8]) -> Result<PrefetchInfo, PrefetchError> {
123 let scca = decompress(file_bytes)?;
124 parse_decompressed(&scca)
125}
126
127const FILE_INFO_OFFSET: usize = 84;
129const MAX_VOLUMES: u32 = 64;
131
132pub fn parse_decompressed(scca: &[u8]) -> Result<PrefetchInfo, PrefetchError> {
134 if scca.len() < FILE_INFO_OFFSET {
135 return Err(PrefetchError::TooShort);
136 }
137 if scca.get(4..8) != Some(SCCA_SIGNATURE.as_slice()) {
138 return Err(PrefetchError::BadSignature);
139 }
140 let version = rd_u32(scca, 0).ok_or(PrefetchError::TooShort)?;
141 if version != 30 && version != 31 {
142 return Err(PrefetchError::UnsupportedVersion(version));
143 }
144
145 let executable = rd_utf16_z(scca, 16, 60).ok_or(PrefetchError::TruncatedRecord)?;
147
148 let fi = FILE_INFO_OFFSET;
150 let filename_off = rd_u32(scca, fi + 16).ok_or(PrefetchError::TruncatedRecord)? as usize;
151 let filename_sz = rd_u32(scca, fi + 20).ok_or(PrefetchError::TruncatedRecord)? as usize;
152 let volumes_off = rd_u32(scca, fi + 24).ok_or(PrefetchError::TruncatedRecord)? as usize;
153 let volume_count = rd_u32(scca, fi + 28).ok_or(PrefetchError::TruncatedRecord)?;
154
155 let mut last_run_times = Vec::with_capacity(8);
157 for i in 0..8 {
158 match rd_i64(scca, fi + 44 + i * 8) {
159 Some(t) if t > 0 => last_run_times.push(t),
160 _ => break,
161 }
162 }
163
164 let run_count = if rd_u32(scca, fi + 120).unwrap_or(0) == 0 {
168 rd_u32(scca, fi + 124).unwrap_or(0)
169 } else {
170 rd_u32(scca, fi + 116).unwrap_or(0)
171 };
172
173 let filenames = parse_filenames(scca, filename_off, filename_sz);
174 let volumes = parse_volumes(scca, volumes_off, volume_count.min(MAX_VOLUMES));
175
176 Ok(PrefetchInfo {
177 version,
178 executable,
179 run_count,
180 last_run_times,
181 volumes,
182 filenames,
183 })
184}
185
186fn parse_filenames(scca: &[u8], off: usize, size: usize) -> Vec<String> {
188 let Some(block) = scca.get(off..off.saturating_add(size)) else {
189 return Vec::new();
190 };
191 let units: Vec<u16> = block
192 .chunks_exact(2)
193 .map(|c| u16::from_le_bytes([c[0], c[1]]))
194 .collect();
195 String::from_utf16_lossy(&units)
196 .split('\0')
197 .filter(|s| !s.is_empty())
198 .map(str::to_string)
199 .collect()
200}
201
202fn parse_volumes(scca: &[u8], vol_off: usize, count: u32) -> Vec<VolumeInfo> {
204 let mut out = Vec::with_capacity(count as usize);
205 for j in 0..count as usize {
206 let rec = vol_off + j * 96;
207 let (Some(dev_off), Some(dev_nchar), Some(ct), Some(serial)) = (
208 rd_u32(scca, rec).map(|v| v as usize),
209 rd_u32(scca, rec + 4).map(|v| v as usize),
210 rd_i64(scca, rec + 8),
211 rd_u32(scca, rec + 16),
212 ) else {
213 break;
214 };
215 let device_path = rd_utf16_z(scca, vol_off + dev_off, dev_nchar * 2).unwrap_or_default();
216 out.push(VolumeInfo {
217 device_path,
218 serial,
219 creation_time: ct,
220 });
221 }
222 out
223}
224
225#[cfg(test)]
226#[allow(clippy::unwrap_used)]
227mod tests {
228 use super::*;
229
230 const COREUPDATER: &[u8] = include_bytes!("../../tests/data/COREUPDATER.EXE-157C54BB.pf");
233 const AUDIODG: &[u8] = include_bytes!("../../tests/data/AUDIODG.EXE-AB22E9A6.pf");
234
235 #[test]
236 fn mam_header_rejects_non_prefetch() {
237 assert_eq!(
239 decompress(b"NOPE\x00\x00\x00\x00").err(),
240 Some(PrefetchError::BadSignature)
241 );
242 assert_eq!(
244 decompress(b"MAM\x02\x00\x00\x00\x00").err(),
245 Some(PrefetchError::BadSignature)
246 );
247 assert_eq!(decompress(b"MA").err(), Some(PrefetchError::TooShort));
249 }
250
251 #[test]
252 fn raw_scca_passes_through() {
253 let mut raw = 23u32.to_le_bytes().to_vec();
255 raw.extend_from_slice(b"SCCA");
256 raw.extend_from_slice(&[0u8; 20]);
257 assert_eq!(decompress(&raw).unwrap(), raw);
258 }
259
260 #[test]
263 fn decompresses_real_win10_prefetch_to_scca() {
264 assert_eq!(&COREUPDATER[0..3], b"MAM");
266 assert_eq!(COREUPDATER[3], 0x04);
267 let declared = u32::from_le_bytes([
268 COREUPDATER[4],
269 COREUPDATER[5],
270 COREUPDATER[6],
271 COREUPDATER[7],
272 ]) as usize;
273
274 let out = decompress(COREUPDATER).unwrap();
276 assert_eq!(out.len(), declared);
277 assert_eq!(
280 &out[SCCA_SIGNATURE_OFFSET..SCCA_SIGNATURE_OFFSET + 4],
281 SCCA_SIGNATURE
282 );
283 assert_eq!(u32::from_le_bytes([out[0], out[1], out[2], out[3]]), 30);
284 }
285
286 #[test]
287 fn decompresses_second_real_prefetch() {
288 let out = decompress(AUDIODG).unwrap();
289 assert_eq!(
290 &out[SCCA_SIGNATURE_OFFSET..SCCA_SIGNATURE_OFFSET + 4],
291 SCCA_SIGNATURE
292 );
293 }
294
295 #[test]
299 fn parses_real_coreupdater_scca() {
300 let info = parse(COREUPDATER).unwrap();
301 assert_eq!(info.version, 30);
302 assert_eq!(info.executable, "COREUPDATER.EXE");
303 assert_eq!(info.run_count, 1);
304 assert_eq!(info.last_run_times, vec![132_449_604_494_103_203]);
305 assert_eq!(info.volumes.len(), 1);
306 assert_eq!(info.volumes[0].serial, 0xB0E0_E8FF);
307 assert_eq!(
308 info.volumes[0].device_path,
309 r"\VOLUME{01d68d85e0da1e22-b0e0e8ff}"
310 );
311 assert_eq!(info.filenames.len(), 51);
312 assert!(info.filenames.iter().any(|f| f.ends_with("NTDLL.DLL")));
313 assert!(info
314 .filenames
315 .iter()
316 .any(|f| f.ends_with("COREUPDATER.EXE")));
317 }
318
319 #[test]
322 fn parses_audiodg_run_count_and_times() {
323 let info = parse(AUDIODG).unwrap();
324 assert_eq!(info.run_count, 8);
325 assert_eq!(info.last_run_times.len(), 8);
326 assert_eq!(info.last_run_times[0], 132_449_663_254_875_727);
327 assert_eq!(info.filenames.len(), 79);
328 }
329
330 #[test]
331 fn parse_rejects_unsupported_version() {
332 let mut p = vec![0u8; 256];
334 p[0..4].copy_from_slice(&23u32.to_le_bytes());
335 p[4..8].copy_from_slice(b"SCCA");
336 assert_eq!(parse(&p).err(), Some(PrefetchError::UnsupportedVersion(23)));
337 }
338
339 fn put16(buf: &mut [u8], off: usize, s: &str) {
340 for (i, u) in s.encode_utf16().enumerate() {
341 buf[off + i * 2..off + i * 2 + 2].copy_from_slice(&u.to_le_bytes());
342 }
343 }
344
345 fn build_scca(old_run_count: bool) -> Vec<u8> {
349 let mut p = vec![0u8; 84 + 224];
350 p[0..4].copy_from_slice(&30u32.to_le_bytes());
351 p[4..8].copy_from_slice(b"SCCA");
352 put16(&mut p, 16, "X.EXE");
353
354 let fname = r"\VOL\X.EXE";
355 let mut fbytes = vec![0u8; (fname.encode_utf16().count() + 1) * 2];
356 put16(&mut fbytes, 0, fname); let fname_off = p.len();
358 p.extend_from_slice(&fbytes);
359
360 let vol_off = p.len();
361 let dev = r"\VOLUME{abcd}";
362 let dev_nchar = dev.encode_utf16().count();
363 let mut vol = vec![0u8; 96];
364 vol[0..4].copy_from_slice(&96u32.to_le_bytes()); vol[4..8].copy_from_slice(&(dev_nchar as u32).to_le_bytes());
366 vol[8..16].copy_from_slice(&123i64.to_le_bytes()); vol[16..20].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes()); p.extend_from_slice(&vol);
369 let mut dbytes = vec![0u8; dev_nchar * 2];
370 put16(&mut dbytes, 0, dev);
371 p.extend_from_slice(&dbytes);
372 let vol_size = (p.len() - vol_off) as u32;
373
374 let fi = FILE_INFO_OFFSET;
375 p[fi + 16..fi + 20].copy_from_slice(&(fname_off as u32).to_le_bytes());
376 p[fi + 20..fi + 24].copy_from_slice(&(fbytes.len() as u32).to_le_bytes());
377 p[fi + 24..fi + 28].copy_from_slice(&(vol_off as u32).to_le_bytes());
378 p[fi + 28..fi + 32].copy_from_slice(&1u32.to_le_bytes());
379 p[fi + 32..fi + 36].copy_from_slice(&vol_size.to_le_bytes());
380 p[fi + 44..fi + 52].copy_from_slice(&1000i64.to_le_bytes()); if old_run_count {
382 p[fi + 124..fi + 128].copy_from_slice(&5u32.to_le_bytes());
383 } else {
384 p[fi + 120..fi + 124].copy_from_slice(&3u32.to_le_bytes());
385 p[fi + 116..fi + 120].copy_from_slice(&7u32.to_le_bytes());
386 }
387 p
388 }
389
390 #[test]
391 fn parses_synthetic_scca_old_and_new_run_count() {
392 let info = parse_decompressed(&build_scca(true)).unwrap();
393 assert_eq!(info.executable, "X.EXE");
394 assert_eq!(info.run_count, 5); assert_eq!(info.last_run_times, vec![1000]);
396 assert_eq!(info.volumes.len(), 1);
397 assert_eq!(info.volumes[0].serial, 0xDEAD_BEEF);
398 assert_eq!(info.volumes[0].device_path, r"\VOLUME{abcd}");
399 assert_eq!(info.filenames, vec![r"\VOL\X.EXE".to_string()]);
400
401 let shifted = parse_decompressed(&build_scca(false)).unwrap();
402 assert_eq!(shifted.run_count, 7); }
404
405 #[test]
406 fn parse_decompressed_rejects_short_and_unsigned() {
407 assert_eq!(
408 parse_decompressed(&[0u8; 50]).err(),
409 Some(PrefetchError::TooShort)
410 );
411 assert_eq!(
413 parse_decompressed(&[0u8; 100]).err(),
414 Some(PrefetchError::BadSignature)
415 );
416 }
417
418 #[test]
419 fn truncated_filename_and_volume_offsets_degrade_gracefully() {
420 let fi = FILE_INFO_OFFSET;
421 let mut p = build_scca(true);
422 let past = (p.len() as u32) + 1000;
423 p[fi + 16..fi + 20].copy_from_slice(&past.to_le_bytes()); assert!(parse_decompressed(&p).unwrap().filenames.is_empty());
425
426 let mut q = build_scca(true);
427 q[fi + 24..fi + 28].copy_from_slice(&past.to_le_bytes()); assert!(parse_decompressed(&q).unwrap().volumes.is_empty());
429 }
430}