pub const DESTLIST_HEADER_FORMAT_VERSION_OFFSET: usize = 0;
pub const DESTLIST_HEADER_ENTRY_COUNT_OFFSET: usize = 4;
pub const DESTLIST_HEADER_PINNED_COUNT_OFFSET: usize = 8;
pub const DESTLIST_HEADER_LAST_ENTRY_NUMBER_OFFSET: usize = 16;
pub const DESTLIST_HEADER_LAST_REVISION_OFFSET: usize = 24;
pub const DESTLIST_HEADER_SIZE: usize = 32;
pub const DESTLIST_FORMAT_VERSION_WIN7: u32 = 1;
pub const DESTLIST_FORMAT_VERSION_WIN10: u32 = 3;
pub const DESTLIST_ENTRY_DROID_VOLUME_GUID_OFFSET: usize = 8;
pub const DESTLIST_ENTRY_DROID_FILE_GUID_OFFSET: usize = 24;
pub const DESTLIST_ENTRY_BIRTH_DROID_VOLUME_GUID_OFFSET: usize = 40;
pub const DESTLIST_ENTRY_BIRTH_DROID_FILE_GUID_OFFSET: usize = 56;
pub const DESTLIST_ENTRY_HOSTNAME_OFFSET: usize = 72;
pub const DESTLIST_ENTRY_HOSTNAME_SIZE: usize = 16;
pub const DESTLIST_ENTRY_ENTRY_NUMBER_OFFSET: usize = 88;
pub const DESTLIST_ENTRY_LAST_ACCESS_FILETIME_OFFSET: usize = 100;
pub const DESTLIST_ENTRY_PIN_STATUS_OFFSET: usize = 108;
pub const DESTLIST_PIN_STATUS_UNPINNED: i32 = -1;
pub const DESTLIST_ENTRY_V1_PATH_SIZE_OFFSET: usize = 112;
pub const DESTLIST_ENTRY_V1_PATH_OFFSET: usize = 114;
pub const DESTLIST_ENTRY_V2_STATUS_OFFSET: usize = 112;
pub const DESTLIST_ENTRY_V2_ACCESS_COUNT_OFFSET: usize = 116;
pub const DESTLIST_ENTRY_V2_UNKNOWN_OFFSET: usize = 120;
pub const DESTLIST_ENTRY_V2_PATH_SIZE_OFFSET: usize = 128;
pub const DESTLIST_ENTRY_V2_PATH_OFFSET: usize = 130;
pub const DESTLIST_ENTRY_V2_TRAILING_ALIGNMENT: usize = 4;
pub const CUSTOM_DESTINATIONS_FORMAT_VERSION: u32 = 2;
pub const CUSTOM_DESTINATIONS_FOOTER_SIGNATURE: u32 = 0xBABF_FBAB;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CategoryType {
Custom,
Known,
UserTasks,
}
impl CategoryType {
#[must_use]
pub fn from_u32(value: u32) -> Option<CategoryType> {
match value {
0 => Some(CategoryType::Custom),
1 => Some(CategoryType::Known),
2 => Some(CategoryType::UserTasks),
_ => None,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum KnownCategory {
Frequent,
Recent,
}
impl KnownCategory {
#[must_use]
pub fn from_u32(value: u32) -> Option<KnownCategory> {
match value {
1 => Some(KnownCategory::Frequent),
2 => Some(KnownCategory::Recent),
_ => None,
}
}
}
pub const LNK_CLSID: &str = crate::shlink::LINK_CLSID;
pub const APPID_CRC64_POLY: u64 = 0x92C6_4265_D321_39A4;
#[must_use]
pub fn appid_crc64(normalized_utf16le: &[u8]) -> u64 {
let mut crc: u64 = 0;
for &byte in normalized_utf16le {
crc ^= u64::from(byte);
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ APPID_CRC64_POLY;
} else {
crc >>= 1;
}
}
}
crc
}
#[must_use]
pub fn appid_name(appid: &str) -> Option<&'static str> {
let key = appid.to_ascii_lowercase();
WELL_KNOWN_APPIDS
.iter()
.find(|(id, _)| *id == key)
.map(|(_, name)| *name)
}
pub static WELL_KNOWN_APPIDS: &[(&str, &str)] = &[
("1b4dd67f29cb1962", "Windows Explorer"),
("5f7b5f1e01b83767", "Quick Access"),
("5d696d521de238c3", "Chrome"),
("918e0ecb43d17e23", "Notepad"),
("39ce6ede51235ede", "Notepad++"),
("469e4a7982cea4d4", "WordPad"),
("1bc392b8e104a00e", "Remote Desktop (mstsc)"),
("3c3871276e149215", "PowerShell 7"),
("9b9cdc69c1c24e2b", "Notepad (Win11)"),
("4cb9c5750d51c07f", "Windows PowerShell"),
("c8866f9516ac1604", "Internet Explorer (64-bit)"),
("aa63b1a90e9050ce", "Word 2016"),
("a4a5324453d3e0c2", "Visual Studio 2017"),
("e0f5df85162b2e74", "Opera"),
("b4866339a794afcf", "Paint.Net"),
("1bc9bbbe61f14501", "OneNote"),
("1c7a9be1b15a03ba", "Snip & Sketch"),
("9c7cc110ff56d1bd", "Microsoft Edge"),
("47bf5b94595fb517", "Firefox"),
("d64d36b238c843a3", "Excel 2016"),
("65009083bfa6a094", "PowerPoint 2016"),
("12dc1ea8e34b5a6", "Outlook 2016"),
("28c8b86deab549a1", "Internet Explorer (32-bit)"),
("271e609288bcd6d", "VLC media player"),
("17d3eb086439f9f0", "7-Zip"),
("9839aec31243a928", "Microsoft Word 2010"),
("4cce4b69dc977dd1", "WinRAR"),
("a8c43ef36da523f1", "Adobe Reader 10"),
("ff103e2cc310d0d", "Adobe Reader XI"),
("fc98c00f85d4ce77", "EditPad Pro 8"),
("6274ff22c2061c60", "Google PhotoViewer (Picasa)"),
("c9533998e1308d73", "MS Teams x64"),
("d67eec451f4b0a17", "MS Teams x64"),
("db53b23fd1edbd46", "WinZip64"),
("5da8f997fc2e25e", "Wordpad (Win10)"),
("d6481c79c4c2afda", "Calculator"),
("23646679aaccfae0", "Adobe Photoshop"),
("2f8b1eee0fab7d60", "Sublime Text"),
("e36b6e3a18d1c9aa", "Visual Studio Code"),
("0b3f13480c2785ae", "Paint"),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn destlist_header_offsets_and_size() {
assert_eq!(DESTLIST_HEADER_FORMAT_VERSION_OFFSET, 0);
assert_eq!(DESTLIST_HEADER_ENTRY_COUNT_OFFSET, 4);
assert_eq!(DESTLIST_HEADER_PINNED_COUNT_OFFSET, 8);
assert_eq!(DESTLIST_HEADER_LAST_ENTRY_NUMBER_OFFSET, 16);
assert_eq!(DESTLIST_HEADER_LAST_REVISION_OFFSET, 24);
assert_eq!(DESTLIST_HEADER_SIZE, 32);
}
#[test]
fn destlist_entry_common_prefix_offsets() {
assert_eq!(DESTLIST_ENTRY_DROID_VOLUME_GUID_OFFSET, 8);
assert_eq!(DESTLIST_ENTRY_DROID_FILE_GUID_OFFSET, 24);
assert_eq!(DESTLIST_ENTRY_BIRTH_DROID_VOLUME_GUID_OFFSET, 40);
assert_eq!(DESTLIST_ENTRY_BIRTH_DROID_FILE_GUID_OFFSET, 56);
assert_eq!(DESTLIST_ENTRY_HOSTNAME_OFFSET, 72);
assert_eq!(DESTLIST_ENTRY_HOSTNAME_SIZE, 16);
assert_eq!(DESTLIST_ENTRY_ENTRY_NUMBER_OFFSET, 88);
assert_eq!(DESTLIST_ENTRY_LAST_ACCESS_FILETIME_OFFSET, 100);
assert_eq!(DESTLIST_ENTRY_PIN_STATUS_OFFSET, 108);
}
#[test]
fn destlist_entry_v1_layout() {
assert_eq!(DESTLIST_ENTRY_V1_PATH_SIZE_OFFSET, 112);
assert_eq!(DESTLIST_ENTRY_V1_PATH_OFFSET, 114);
assert_eq!(
DESTLIST_ENTRY_V1_PATH_OFFSET,
DESTLIST_ENTRY_V1_PATH_SIZE_OFFSET + 2
);
}
#[test]
fn destlist_entry_v2_layout_inserts_16_byte_block() {
assert_eq!(DESTLIST_ENTRY_V2_STATUS_OFFSET, 112);
assert_eq!(DESTLIST_ENTRY_V2_ACCESS_COUNT_OFFSET, 116);
assert_eq!(DESTLIST_ENTRY_V2_UNKNOWN_OFFSET, 120);
assert_eq!(DESTLIST_ENTRY_V2_PATH_SIZE_OFFSET, 128);
assert_eq!(DESTLIST_ENTRY_V2_PATH_OFFSET, 130);
assert_eq!(DESTLIST_ENTRY_V2_TRAILING_ALIGNMENT, 4);
assert_eq!(
DESTLIST_ENTRY_V2_PATH_SIZE_OFFSET - DESTLIST_ENTRY_V1_PATH_SIZE_OFFSET,
16
);
assert_eq!(
DESTLIST_ENTRY_V2_UNKNOWN_OFFSET + 8,
DESTLIST_ENTRY_V2_PATH_SIZE_OFFSET
);
}
#[test]
fn pin_status_sentinel() {
assert_eq!(DESTLIST_PIN_STATUS_UNPINNED, -1);
}
#[test]
fn destlist_format_versions() {
assert_eq!(DESTLIST_FORMAT_VERSION_WIN7, 1);
assert_eq!(DESTLIST_FORMAT_VERSION_WIN10, 3);
}
#[test]
fn custom_destinations_constants() {
assert_eq!(CUSTOM_DESTINATIONS_FORMAT_VERSION, 2);
assert_eq!(CUSTOM_DESTINATIONS_FOOTER_SIGNATURE, 0xBABF_FBAB);
}
#[test]
fn category_type_mapping() {
assert_eq!(CategoryType::from_u32(0), Some(CategoryType::Custom));
assert_eq!(CategoryType::from_u32(1), Some(CategoryType::Known));
assert_eq!(CategoryType::from_u32(2), Some(CategoryType::UserTasks));
assert_eq!(CategoryType::from_u32(3), None);
}
#[test]
fn known_category_mapping() {
assert_eq!(KnownCategory::from_u32(1), Some(KnownCategory::Frequent));
assert_eq!(KnownCategory::from_u32(2), Some(KnownCategory::Recent));
assert_eq!(KnownCategory::from_u32(0), None);
assert_eq!(KnownCategory::from_u32(99), None);
}
#[test]
fn lnk_clsid_matches_shlink() {
assert_eq!(LNK_CLSID, crate::shlink::LINK_CLSID);
assert_eq!(LNK_CLSID, "00021401-0000-0000-C000-000000000046");
}
#[test]
fn appid_poly_value() {
assert_eq!(APPID_CRC64_POLY, 0x92C6_4265_D321_39A4);
}
#[test]
fn appid_name_resolves_known_and_normalizes_case() {
assert_eq!(appid_name("1b4dd67f29cb1962"), Some("Windows Explorer"));
assert_eq!(appid_name("5f7b5f1e01b83767"), Some("Quick Access"));
assert_eq!(appid_name("5d696d521de238c3"), Some("Chrome"));
assert_eq!(appid_name("39ce6ede51235ede"), Some("Notepad++"));
assert_eq!(appid_name("1B4DD67F29CB1962"), Some("Windows Explorer"));
assert_eq!(appid_name("ffffffffffffffff"), None);
}
#[test]
fn well_known_appids_are_lowercase_and_nonempty() {
for (id, name) in WELL_KNOWN_APPIDS {
assert_eq!(*id, id.to_ascii_lowercase(), "AppID key must be lowercase");
assert!(!name.is_empty(), "AppID name must be non-empty");
assert!(
id.chars().all(|c| c.is_ascii_hexdigit()),
"AppID must be hex"
);
}
}
#[test]
fn appid_crc64_is_deterministic_and_zero_for_empty() {
assert_eq!(appid_crc64(&[]), 0);
let bytes = b"\x43\x00\x3a\x00"; assert_eq!(appid_crc64(bytes), appid_crc64(bytes));
assert_ne!(appid_crc64(b"\x01"), 0);
}
}