use std::io::Cursor;
use winreg_core::hive::Hive;
use winreg_core::key::filetime_to_datetime;
#[derive(Debug, Clone, serde::Serialize)]
pub struct AmcacheEntry {
pub file_path: String,
pub sha1: String,
pub size: u64,
pub link_date: Option<String>,
pub publisher: String,
pub product_name: String,
pub product_version: String,
pub bin_file_version: String,
pub key_name: String,
pub last_written: Option<String>,
}
const INVENTORY_APP_FILE: &str = "Root\\InventoryApplicationFile";
const ROOT_FILE: &str = "Root\\File";
#[must_use]
pub fn parse(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<AmcacheEntry> {
let mut entries = parse_inventory_app_file(hive);
entries.extend(parse_root_file(hive));
entries
}
fn parse_inventory_app_file(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<AmcacheEntry> {
let Ok(Some(container)) = hive.open_key(INVENTORY_APP_FILE) else {
return Vec::new();
};
let Ok(subkeys) = container.subkeys() else {
return Vec::new();
};
let mut entries = Vec::with_capacity(subkeys.len());
for subkey in subkeys {
let key_name = subkey.name();
let last_written = filetime_to_datetime(subkey.last_written_raw())
.map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string());
let read_sz = |name: &str| -> String {
subkey
.value(name)
.ok()
.flatten()
.and_then(|v| v.as_string().ok())
.unwrap_or_default()
};
let file_path = read_sz("LowerCaseLongPath");
let file_id_raw = read_sz("FileId");
let sha1 = file_id_raw
.strip_prefix("0000")
.map_or_else(|| file_id_raw.clone(), ToString::to_string);
let size = u64::from(
subkey
.value("Size")
.ok()
.flatten()
.and_then(|v| v.as_u32().ok())
.unwrap_or(0),
);
let link_date_raw = read_sz("LinkDate");
let link_date = if link_date_raw.is_empty() {
None
} else {
Some(link_date_raw)
};
let publisher = read_sz("Publisher");
let product_name = read_sz("ProductName");
let product_version = read_sz("ProductVersion");
let bin_file_version = read_sz("BinFileVersion");
entries.push(AmcacheEntry {
file_path,
sha1,
size,
link_date,
publisher,
product_name,
product_version,
bin_file_version,
key_name,
last_written,
});
}
entries
}
fn parse_root_file(hive: &Hive<Cursor<Vec<u8>>>) -> Vec<AmcacheEntry> {
let Ok(Some(container)) = hive.open_key(ROOT_FILE) else {
return Vec::new();
};
let Ok(volumes) = container.subkeys() else {
return Vec::new();
};
let mut entries = Vec::new();
for volume in volumes {
let Ok(files) = volume.subkeys() else {
continue; };
for file in files {
let read_sz = |name: &str| -> String {
file.value(name)
.ok()
.flatten()
.and_then(|v| v.as_string().ok())
.unwrap_or_default()
};
let file_path = read_sz("15");
let sha1_raw = read_sz("101");
let sha1 = sha1_raw
.strip_prefix("0000")
.map_or_else(|| sha1_raw.clone(), ToString::to_string);
let last_written = filetime_to_datetime(file.last_written_raw())
.map(|dt| dt.format("%Y-%m-%dT%H:%M:%SZ").to_string());
entries.push(AmcacheEntry {
file_path,
sha1,
size: 0,
link_date: None,
publisher: String::new(),
product_name: String::new(),
product_version: String::new(),
bin_file_version: String::new(),
key_name: file.name(),
last_written,
});
}
}
entries
}