#![forbid(unsafe_code)]
#[derive(Debug, PartialEq, Eq)]
pub enum PrefetchError {
TooShort,
BadSignature,
Decompress(xpress_huffman::Error),
UnsupportedVersion(u32),
TruncatedRecord,
}
const MAM_SIGNATURE: &[u8; 3] = b"MAM";
const MAM_XPRESS_HUFFMAN: u8 = 0x04;
pub const SCCA_SIGNATURE: &[u8; 4] = b"SCCA";
pub const SCCA_SIGNATURE_OFFSET: usize = 4;
pub fn decompress(data: &[u8]) -> Result<Vec<u8>, PrefetchError> {
if data.len() < 8 {
return Err(PrefetchError::TooShort);
}
if &data[SCCA_SIGNATURE_OFFSET..SCCA_SIGNATURE_OFFSET + 4] == SCCA_SIGNATURE {
return Ok(data.to_vec());
}
if &data[0..3] != MAM_SIGNATURE || data[3] != MAM_XPRESS_HUFFMAN {
return Err(PrefetchError::BadSignature);
}
let decompressed_size = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
xpress_huffman::decompress(&data[8..], decompressed_size).map_err(PrefetchError::Decompress)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VolumeInfo {
pub device_path: String,
pub serial: u32,
pub creation_time: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrefetchInfo {
pub version: u32,
pub executable: String,
pub run_count: u32,
pub last_run_times: Vec<i64>,
pub volumes: Vec<VolumeInfo>,
pub filenames: Vec<String>,
}
fn rd_u32(d: &[u8], off: usize) -> Option<u32> {
d.get(off..off + 4)
.map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
fn rd_i64(d: &[u8], off: usize) -> Option<i64> {
d.get(off..off + 8).map(|s| {
let mut a = [0u8; 8];
a.copy_from_slice(s);
i64::from_le_bytes(a)
})
}
fn rd_utf16_z(d: &[u8], off: usize, byte_len: usize) -> Option<String> {
let s = d.get(off..off + byte_len)?;
let units: Vec<u16> = s
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.take_while(|&u| u != 0)
.collect();
Some(String::from_utf16_lossy(&units))
}
pub fn parse(file_bytes: &[u8]) -> Result<PrefetchInfo, PrefetchError> {
let scca = decompress(file_bytes)?;
parse_decompressed(&scca)
}
const FILE_INFO_OFFSET: usize = 84;
const MAX_VOLUMES: u32 = 64;
pub fn parse_decompressed(scca: &[u8]) -> Result<PrefetchInfo, PrefetchError> {
if scca.len() < FILE_INFO_OFFSET {
return Err(PrefetchError::TooShort);
}
if scca.get(4..8) != Some(SCCA_SIGNATURE.as_slice()) {
return Err(PrefetchError::BadSignature);
}
let version = rd_u32(scca, 0).ok_or(PrefetchError::TooShort)?;
if version != 30 && version != 31 {
return Err(PrefetchError::UnsupportedVersion(version));
}
let executable = rd_utf16_z(scca, 16, 60).ok_or(PrefetchError::TruncatedRecord)?;
let fi = FILE_INFO_OFFSET;
let filename_off = rd_u32(scca, fi + 16).ok_or(PrefetchError::TruncatedRecord)? as usize;
let filename_sz = rd_u32(scca, fi + 20).ok_or(PrefetchError::TruncatedRecord)? as usize;
let volumes_off = rd_u32(scca, fi + 24).ok_or(PrefetchError::TruncatedRecord)? as usize;
let volume_count = rd_u32(scca, fi + 28).ok_or(PrefetchError::TruncatedRecord)?;
let mut last_run_times = Vec::with_capacity(8);
for i in 0..8 {
match rd_i64(scca, fi + 44 + i * 8) {
Some(t) if t > 0 => last_run_times.push(t),
_ => break,
}
}
let run_count = if rd_u32(scca, fi + 120).unwrap_or(0) == 0 {
rd_u32(scca, fi + 124).unwrap_or(0)
} else {
rd_u32(scca, fi + 116).unwrap_or(0)
};
let filenames = parse_filenames(scca, filename_off, filename_sz);
let volumes = parse_volumes(scca, volumes_off, volume_count.min(MAX_VOLUMES));
Ok(PrefetchInfo {
version,
executable,
run_count,
last_run_times,
volumes,
filenames,
})
}
fn parse_filenames(scca: &[u8], off: usize, size: usize) -> Vec<String> {
let Some(block) = scca.get(off..off.saturating_add(size)) else {
return Vec::new();
};
let units: Vec<u16> = block
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&units)
.split('\0')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
fn parse_volumes(scca: &[u8], vol_off: usize, count: u32) -> Vec<VolumeInfo> {
let mut out = Vec::with_capacity(count as usize);
for j in 0..count as usize {
let rec = vol_off + j * 96;
let (Some(dev_off), Some(dev_nchar), Some(ct), Some(serial)) = (
rd_u32(scca, rec).map(|v| v as usize),
rd_u32(scca, rec + 4).map(|v| v as usize),
rd_i64(scca, rec + 8),
rd_u32(scca, rec + 16),
) else {
break;
};
let device_path = rd_utf16_z(scca, vol_off + dev_off, dev_nchar * 2).unwrap_or_default();
out.push(VolumeInfo {
device_path,
serial,
creation_time: ct,
});
}
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
const COREUPDATER: &[u8] = include_bytes!("../../tests/data/COREUPDATER.EXE-157C54BB.pf");
const AUDIODG: &[u8] = include_bytes!("../../tests/data/AUDIODG.EXE-AB22E9A6.pf");
#[test]
fn mam_header_rejects_non_prefetch() {
assert_eq!(
decompress(b"NOPE\x00\x00\x00\x00").err(),
Some(PrefetchError::BadSignature)
);
assert_eq!(
decompress(b"MAM\x02\x00\x00\x00\x00").err(),
Some(PrefetchError::BadSignature)
);
assert_eq!(decompress(b"MA").err(), Some(PrefetchError::TooShort));
}
#[test]
fn raw_scca_passes_through() {
let mut raw = 23u32.to_le_bytes().to_vec();
raw.extend_from_slice(b"SCCA");
raw.extend_from_slice(&[0u8; 20]);
assert_eq!(decompress(&raw).unwrap(), raw);
}
#[test]
fn decompresses_real_win10_prefetch_to_scca() {
assert_eq!(&COREUPDATER[0..3], b"MAM");
assert_eq!(COREUPDATER[3], 0x04);
let declared = u32::from_le_bytes([
COREUPDATER[4],
COREUPDATER[5],
COREUPDATER[6],
COREUPDATER[7],
]) as usize;
let out = decompress(COREUPDATER).unwrap();
assert_eq!(out.len(), declared);
assert_eq!(
&out[SCCA_SIGNATURE_OFFSET..SCCA_SIGNATURE_OFFSET + 4],
SCCA_SIGNATURE
);
assert_eq!(u32::from_le_bytes([out[0], out[1], out[2], out[3]]), 30);
}
#[test]
fn decompresses_second_real_prefetch() {
let out = decompress(AUDIODG).unwrap();
assert_eq!(
&out[SCCA_SIGNATURE_OFFSET..SCCA_SIGNATURE_OFFSET + 4],
SCCA_SIGNATURE
);
}
#[test]
fn parses_real_coreupdater_scca() {
let info = parse(COREUPDATER).unwrap();
assert_eq!(info.version, 30);
assert_eq!(info.executable, "COREUPDATER.EXE");
assert_eq!(info.run_count, 1);
assert_eq!(info.last_run_times, vec![132_449_604_494_103_203]);
assert_eq!(info.volumes.len(), 1);
assert_eq!(info.volumes[0].serial, 0xB0E0_E8FF);
assert_eq!(
info.volumes[0].device_path,
r"\VOLUME{01d68d85e0da1e22-b0e0e8ff}"
);
assert_eq!(info.filenames.len(), 51);
assert!(info.filenames.iter().any(|f| f.ends_with("NTDLL.DLL")));
assert!(info
.filenames
.iter()
.any(|f| f.ends_with("COREUPDATER.EXE")));
}
#[test]
fn parses_audiodg_run_count_and_times() {
let info = parse(AUDIODG).unwrap();
assert_eq!(info.run_count, 8);
assert_eq!(info.last_run_times.len(), 8);
assert_eq!(info.last_run_times[0], 132_449_663_254_875_727);
assert_eq!(info.filenames.len(), 79);
}
#[test]
fn parse_rejects_unsupported_version() {
let mut p = vec![0u8; 256];
p[0..4].copy_from_slice(&23u32.to_le_bytes());
p[4..8].copy_from_slice(b"SCCA");
assert_eq!(parse(&p).err(), Some(PrefetchError::UnsupportedVersion(23)));
}
fn put16(buf: &mut [u8], off: usize, s: &str) {
for (i, u) in s.encode_utf16().enumerate() {
buf[off + i * 2..off + i * 2 + 2].copy_from_slice(&u.to_le_bytes());
}
}
fn build_scca(old_run_count: bool) -> Vec<u8> {
let mut p = vec![0u8; 84 + 224];
p[0..4].copy_from_slice(&30u32.to_le_bytes());
p[4..8].copy_from_slice(b"SCCA");
put16(&mut p, 16, "X.EXE");
let fname = r"\VOL\X.EXE";
let mut fbytes = vec![0u8; (fname.encode_utf16().count() + 1) * 2];
put16(&mut fbytes, 0, fname); let fname_off = p.len();
p.extend_from_slice(&fbytes);
let vol_off = p.len();
let dev = r"\VOLUME{abcd}";
let dev_nchar = dev.encode_utf16().count();
let mut vol = vec![0u8; 96];
vol[0..4].copy_from_slice(&96u32.to_le_bytes()); vol[4..8].copy_from_slice(&(dev_nchar as u32).to_le_bytes());
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);
let mut dbytes = vec![0u8; dev_nchar * 2];
put16(&mut dbytes, 0, dev);
p.extend_from_slice(&dbytes);
let vol_size = (p.len() - vol_off) as u32;
let fi = FILE_INFO_OFFSET;
p[fi + 16..fi + 20].copy_from_slice(&(fname_off as u32).to_le_bytes());
p[fi + 20..fi + 24].copy_from_slice(&(fbytes.len() as u32).to_le_bytes());
p[fi + 24..fi + 28].copy_from_slice(&(vol_off as u32).to_le_bytes());
p[fi + 28..fi + 32].copy_from_slice(&1u32.to_le_bytes());
p[fi + 32..fi + 36].copy_from_slice(&vol_size.to_le_bytes());
p[fi + 44..fi + 52].copy_from_slice(&1000i64.to_le_bytes()); if old_run_count {
p[fi + 124..fi + 128].copy_from_slice(&5u32.to_le_bytes());
} else {
p[fi + 120..fi + 124].copy_from_slice(&3u32.to_le_bytes());
p[fi + 116..fi + 120].copy_from_slice(&7u32.to_le_bytes());
}
p
}
#[test]
fn parses_synthetic_scca_old_and_new_run_count() {
let info = parse_decompressed(&build_scca(true)).unwrap();
assert_eq!(info.executable, "X.EXE");
assert_eq!(info.run_count, 5); assert_eq!(info.last_run_times, vec![1000]);
assert_eq!(info.volumes.len(), 1);
assert_eq!(info.volumes[0].serial, 0xDEAD_BEEF);
assert_eq!(info.volumes[0].device_path, r"\VOLUME{abcd}");
assert_eq!(info.filenames, vec![r"\VOL\X.EXE".to_string()]);
let shifted = parse_decompressed(&build_scca(false)).unwrap();
assert_eq!(shifted.run_count, 7); }
#[test]
fn parse_decompressed_rejects_short_and_unsigned() {
assert_eq!(
parse_decompressed(&[0u8; 50]).err(),
Some(PrefetchError::TooShort)
);
assert_eq!(
parse_decompressed(&[0u8; 100]).err(),
Some(PrefetchError::BadSignature)
);
}
#[test]
fn truncated_filename_and_volume_offsets_degrade_gracefully() {
let fi = FILE_INFO_OFFSET;
let mut p = build_scca(true);
let past = (p.len() as u32) + 1000;
p[fi + 16..fi + 20].copy_from_slice(&past.to_le_bytes()); assert!(parse_decompressed(&p).unwrap().filenames.is_empty());
let mut q = build_scca(true);
q[fi + 24..fi + 28].copy_from_slice(&past.to_le_bytes()); assert!(parse_decompressed(&q).unwrap().volumes.is_empty());
}
}