#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
use common::hive_builder::TestHiveBuilder;
use winreg_artifacts::shimcache::{parse, ShimcacheEntry};
use winreg_core::hive::Hive;
const APPCOMPAT_KEY: &str = "CurrentControlSet\\Control\\Session Manager\\AppCompatCache";
const APPCOMPAT_VALUE: &str = "AppCompatCache";
const REG_BINARY: u32 = 3;
fn empty_appcompat_blob() -> Vec<u8> {
let mut blob = Vec::new();
blob.extend_from_slice(&0x73743031u32.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob
}
fn unknown_signature_blob() -> Vec<u8> {
vec![0xAA, 0xBB, 0xCC, 0xDD, 0x01, 0x00, 0x00, 0x00]
}
#[test]
fn parse_empty_hive_returns_empty() {
let data = TestHiveBuilder::new().build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(
entries.is_empty(),
"empty hive (no AppCompatCache key) should return empty Vec"
);
}
#[test]
fn parse_missing_key_returns_empty() {
let data = TestHiveBuilder::new().add_key("SomeOtherKey\\Foo").build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(
entries.is_empty(),
"hive without AppCompatCache key should return empty Vec"
);
}
#[test]
fn parse_present_blob_returns_entry() {
let blob = unknown_signature_blob();
let data = TestHiveBuilder::new()
.add_key(APPCOMPAT_KEY)
.add_value(APPCOMPAT_KEY, APPCOMPAT_VALUE, REG_BINARY, &blob)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(
!entries.is_empty(),
"hive with AppCompatCache blob should return at least one entry"
);
}
#[test]
fn parse_entry_raw_size_matches_blob() {
let blob = unknown_signature_blob();
let expected_size = blob.len();
let data = TestHiveBuilder::new()
.add_key(APPCOMPAT_KEY)
.add_value(APPCOMPAT_KEY, APPCOMPAT_VALUE, REG_BINARY, &blob)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(!entries.is_empty());
assert_eq!(
entries[0].raw_size, expected_size,
"raw_size should equal the byte length of the AppCompatCache blob"
);
}
#[test]
fn parse_entry_index_is_zero_for_first() {
let blob = unknown_signature_blob();
let data = TestHiveBuilder::new()
.add_key(APPCOMPAT_KEY)
.add_value(APPCOMPAT_KEY, APPCOMPAT_VALUE, REG_BINARY, &blob)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(!entries.is_empty());
assert_eq!(
entries[0].entry_index, 0,
"first entry should have entry_index == 0"
);
}
#[test]
fn parse_multiple_format_graceful() {
let blob: Vec<u8> = vec![0x01, 0x02, 0x03];
let data = TestHiveBuilder::new()
.add_key(APPCOMPAT_KEY)
.add_value(APPCOMPAT_KEY, APPCOMPAT_VALUE, REG_BINARY, &blob)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(
entries.is_empty(),
"blob shorter than 4 bytes should return empty vec, not panic"
);
}
#[test]
fn parse_result_is_serializable() {
let blob = unknown_signature_blob();
let data = TestHiveBuilder::new()
.add_key(APPCOMPAT_KEY)
.add_value(APPCOMPAT_KEY, APPCOMPAT_VALUE, REG_BINARY, &blob)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
let json = serde_json::to_string(&entries);
assert!(
json.is_ok(),
"parse result should be JSON-serializable: {:?}",
json.err()
);
}
#[test]
fn parse_key_path_is_correct() {
let blob = unknown_signature_blob();
let correct_data = TestHiveBuilder::new()
.add_key(APPCOMPAT_KEY)
.add_value(APPCOMPAT_KEY, APPCOMPAT_VALUE, REG_BINARY, &blob)
.build();
let wrong_path = "CurrentControlSet\\Control\\Session Manager\\NotShimCache";
let wrong_data = TestHiveBuilder::new()
.add_key(wrong_path)
.add_value(wrong_path, APPCOMPAT_VALUE, REG_BINARY, &blob)
.build();
let correct_hive = Hive::from_bytes(correct_data).unwrap();
let wrong_hive = Hive::from_bytes(wrong_data).unwrap();
let correct_entries = parse(&correct_hive);
let wrong_entries = parse(&wrong_hive);
assert!(
!correct_entries.is_empty(),
"correct key path should yield entries"
);
assert!(
wrong_entries.is_empty(),
"wrong key path should yield no entries"
);
}
const REG_DWORD: u32 = 4;
#[test]
fn parse_resolves_controlset_from_select_on_offline_hive() {
let blob = unknown_signature_blob();
let key = "ControlSet001\\Control\\Session Manager\\AppCompatCache";
let data = TestHiveBuilder::new()
.add_key(key)
.add_value(key, APPCOMPAT_VALUE, REG_BINARY, &blob)
.add_key("Select")
.add_value("Select", "Current", REG_DWORD, &1u32.to_le_bytes())
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert!(
!entries.is_empty(),
"must resolve AppCompatCache via Select\\Current → ControlSet001 on an \
offline hive that has no CurrentControlSet"
);
}
fn win10_appcompat_blob(entries: &[(&str, u64)]) -> Vec<u8> {
let mut blob = Vec::new();
blob.extend_from_slice(&0x34u32.to_le_bytes()); blob.resize(0x34, 0); for (path, filetime) in entries {
let path_utf16: Vec<u8> = path.encode_utf16().flat_map(u16::to_le_bytes).collect();
let mut body = Vec::new();
body.extend_from_slice(&(path_utf16.len() as u16).to_le_bytes()); body.extend_from_slice(&path_utf16); body.extend_from_slice(&filetime.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(b"10ts"); blob.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(&(body.len() as u32).to_le_bytes()); blob.extend_from_slice(&body);
}
blob
}
fn win81_appcompat_blob(entries: &[(&str, u64)]) -> Vec<u8> {
let mut blob = vec![0u8; 128]; for (path, filetime) in entries {
let path_utf16: Vec<u8> = path.encode_utf16().flat_map(u16::to_le_bytes).collect();
let mut body = Vec::new();
body.extend_from_slice(&(path_utf16.len() as u16).to_le_bytes()); body.extend_from_slice(&path_utf16); body.extend_from_slice(&0u16.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); body.extend_from_slice(&filetime.to_le_bytes()); body.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(b"10ts"); blob.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(&(body.len() as u32).to_le_bytes()); blob.extend_from_slice(&body);
}
blob
}
fn hive_with_appcompat(blob: &[u8]) -> Hive<std::io::Cursor<Vec<u8>>> {
let key = "ControlSet001\\Control\\Session Manager\\AppCompatCache";
let data = TestHiveBuilder::new()
.add_key(key)
.add_value(key, APPCOMPAT_VALUE, REG_BINARY, blob)
.build();
Hive::from_bytes(data).unwrap()
}
#[test]
fn parse_decodes_win81_128byte_header_with_timestamp() {
let ft = 132_449_604_494_103_203u64; let blob = win81_appcompat_blob(&[
("C:\\Windows\\System32\\cmd.exe", ft),
("C:\\Windows\\System32\\coreupdater.exe", ft),
]);
let got = parse(&hive_with_appcompat(&blob));
assert_eq!(got.len(), 2, "must decode both Win8.1 entries, not a sentinel");
assert!(got[1].path.to_uppercase().contains("COREUPDATER.EXE"));
let reference = parse(&hive_with_appcompat(&win10_appcompat_blob(&[("C:\\X", ft)])));
assert!(
reference[0].last_modified.is_some(),
"Win10 reference timestamp must decode"
);
assert_eq!(
got[0].last_modified, reference[0].last_modified,
"Win8.1 body FILETIME must decode to the same value as Win10 (path_end+2+package_len+8)"
);
}
#[test]
fn parse_decodes_real_win10_appcompat_entries() {
let blob = win10_appcompat_blob(&[
("C:\\Windows\\System32\\cmd.exe", 132_449_604_494_103_203),
("C:\\Windows\\System32\\coreupdater.exe", 132_449_604_494_103_203),
]);
let key = "ControlSet001\\Control\\Session Manager\\AppCompatCache";
let data = TestHiveBuilder::new()
.add_key(key)
.add_value(key, APPCOMPAT_VALUE, REG_BINARY, &blob)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 2, "should decode both Win10 entries, not a sentinel");
assert!(
entries[0].path.to_uppercase().contains("CMD.EXE"),
"entry 0 path: {:?}",
entries[0].path
);
assert!(
entries[1].path.to_uppercase().contains("COREUPDATER.EXE"),
"entry 1 path: {:?}",
entries[1].path
);
assert!(entries[0].last_modified.is_some(), "FILETIME must decode");
assert_eq!(entries[0].entry_index, 0);
assert_eq!(entries[1].entry_index, 1);
}