#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
use common::hive_builder::TestHiveBuilder;
use winreg_artifacts::amcache::{parse, AmcacheEntry};
use winreg_core::hive::Hive;
const IAF_PATH: &str = "Root\\InventoryApplicationFile";
fn utf16le(s: &str) -> Vec<u8> {
let mut out: Vec<u8> = s.encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
out.push(0);
out.push(0);
out
}
fn dword(v: u32) -> Vec<u8> {
v.to_le_bytes().to_vec()
}
const REG_SZ: u32 = 1;
const REG_DWORD: u32 = 4;
#[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 InventoryApplicationFile key) should return empty Vec"
);
}
#[test]
fn parse_single_entry_returns_entry() {
let subkey = format!("{IAF_PATH}\\abc123");
let data = TestHiveBuilder::new()
.add_key(&subkey)
.add_value(
&subkey,
"LowerCaseLongPath",
REG_SZ,
&utf16le("C:\\windows\\system32\\foo.exe"),
)
.add_value(&subkey, "FileId", REG_SZ, &utf16le("00001234567890abcdef"))
.add_value(&subkey, "Size", REG_DWORD, &dword(12345))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1, "should return one entry");
}
#[test]
fn parse_file_path_extracted() {
let subkey = format!("{IAF_PATH}\\entry1");
let expected_path = "C:\\windows\\system32\\notepad.exe";
let data = TestHiveBuilder::new()
.add_key(&subkey)
.add_value(
&subkey,
"LowerCaseLongPath",
REG_SZ,
&utf16le(expected_path),
)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].file_path, expected_path,
"file_path should match LowerCaseLongPath value"
);
}
#[test]
fn parse_sha1_strips_0000_prefix() {
let subkey = format!("{IAF_PATH}\\sha1test");
let file_id = "0000aabbccddeeff00112233445566778899aabb";
let data = TestHiveBuilder::new()
.add_key(&subkey)
.add_value(&subkey, "FileId", REG_SZ, &utf16le(file_id))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].sha1, "aabbccddeeff00112233445566778899aabb",
"sha1 should have '0000' prefix stripped"
);
}
#[test]
fn parse_sha1_absent_gives_empty() {
let subkey = format!("{IAF_PATH}\\nosha1");
let data = TestHiveBuilder::new()
.add_key(&subkey)
.add_value(
&subkey,
"LowerCaseLongPath",
REG_SZ,
&utf16le("C:\\foo.exe"),
)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1);
assert!(
entries[0].sha1.is_empty(),
"sha1 should be empty string when FileId is absent"
);
}
#[test]
fn parse_size_extracted() {
let subkey = format!("{IAF_PATH}\\sizetest");
let data = TestHiveBuilder::new()
.add_key(&subkey)
.add_value(&subkey, "Size", REG_DWORD, &dword(98765))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].size, 98765, "size should match REG_DWORD value");
}
#[test]
fn parse_publisher_extracted() {
let subkey = format!("{IAF_PATH}\\pubtest");
let data = TestHiveBuilder::new()
.add_key(&subkey)
.add_value(
&subkey,
"Publisher",
REG_SZ,
&utf16le("Microsoft Corporation"),
)
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].publisher, "Microsoft Corporation",
"publisher should match Publisher value"
);
}
#[test]
fn parse_missing_values_default_to_empty() {
let subkey = format!("{IAF_PATH}\\emptyentry");
let data = TestHiveBuilder::new().add_key(&subkey).build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1);
let e = &entries[0];
assert!(e.file_path.is_empty(), "file_path should be empty");
assert!(e.sha1.is_empty(), "sha1 should be empty");
assert_eq!(e.size, 0, "size should be 0");
assert!(e.publisher.is_empty(), "publisher should be empty");
assert!(e.product_name.is_empty(), "product_name should be empty");
assert!(
e.product_version.is_empty(),
"product_version should be empty"
);
assert!(
e.bin_file_version.is_empty(),
"bin_file_version should be empty"
);
}
#[test]
fn parse_last_written_populated() {
let subkey = format!("{IAF_PATH}\\lwtest");
let data = TestHiveBuilder::new().add_key(&subkey).build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1);
let _ = entries[0].last_written.as_deref();
}
#[test]
fn parse_key_name_populated() {
let subkey_name = "deadbeef1234";
let subkey = format!("{IAF_PATH}\\{subkey_name}");
let data = TestHiveBuilder::new().add_key(&subkey).build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 1);
assert_eq!(
entries[0].key_name, subkey_name,
"key_name should be the subkey name (hash identifier)"
);
}
#[test]
fn parse_multiple_entries() {
let sub1 = format!("{IAF_PATH}\\entry_a");
let sub2 = format!("{IAF_PATH}\\entry_b");
let data = TestHiveBuilder::new()
.add_key(&sub1)
.add_value(&sub1, "LowerCaseLongPath", REG_SZ, &utf16le("C:\\a.exe"))
.add_key(&sub2)
.add_value(&sub2, "LowerCaseLongPath", REG_SZ, &utf16le("C:\\b.exe"))
.build();
let hive = Hive::from_bytes(data).unwrap();
let entries = parse(&hive);
assert_eq!(entries.len(), 2, "should return an entry per subkey");
}
#[test]
fn amcache_entry_struct_fields_accessible() {
let entry = AmcacheEntry {
file_path: "C:\\foo.exe".to_string(),
sha1: "abc123".to_string(),
size: 1024,
link_date: Some("01/15/2023 10:30:00".to_string()),
publisher: "Acme".to_string(),
product_name: "FooApp".to_string(),
product_version: "1.0.0".to_string(),
bin_file_version: "1.0.0.0".to_string(),
key_name: "deadbeef".to_string(),
last_written: None,
};
assert_eq!(entry.file_path, "C:\\foo.exe");
assert_eq!(entry.sha1, "abc123");
assert_eq!(entry.size, 1024);
assert_eq!(entry.publisher, "Acme");
assert_eq!(entry.key_name, "deadbeef");
}