use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
fn mk(name: &str, value: Value) -> Tag {
let pv = value.to_display_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: name.to_string(),
group: TagGroup {
family0: "LNK".into(),
family1: "LNK".into(),
family2: "Other".into(),
},
raw_value: value,
print_value: pv,
priority: 0,
}
}
fn mk_str(name: &str, val: &str) -> Tag {
mk(name, Value::String(val.to_string()))
}
fn mk_time(name: &str, val: &str) -> Tag {
let mut t = mk_str(name, val);
t.group.family2 = "Time".into();
t
}
fn get_url_tz_offset() -> i64 {
if let Ok(tz) = std::env::var("TZ") {
let tz = tz.trim();
if let Some(sign_pos) = tz.rfind(['+', '-']) {
let sign: i64 = if &tz[sign_pos..sign_pos + 1] == "+" {
1
} else {
-1
};
if let Ok(h) = tz[sign_pos + 1..].parse::<i64>() {
return -sign * h * 3600;
}
}
}
0
}
fn filetime_to_datetime(lo: u32, hi: u32, tz_offset_hours: i64) -> Option<String> {
let filetime = (hi as u64) * 4294967296u64 + (lo as u64);
if filetime == 0 {
return None;
}
let unix_secs = (filetime as i64) / 10_000_000 - 11_644_473_600;
let secs = unix_secs + tz_offset_hours * 3600;
let (year, month, day, h, m, s) = unix_to_components(secs);
let sign = if tz_offset_hours >= 0 { '+' } else { '-' };
let abs_h = tz_offset_hours.unsigned_abs();
Some(format!(
"{:04}:{:02}:{:02} {:02}:{:02}:{:02}{}{:02}:00",
year, month, day, h, m, s, sign, abs_h
))
}
fn filetime64_to_datetime(val: u64) -> Option<String> {
if val == 0 {
return None;
}
let unix_secs = (val as i64) / 10_000_000 - 11_644_473_600;
let (year, month, day, h, m, s) = unix_to_components(unix_secs);
Some(format!(
"{:04}:{:02}:{:02} {:02}:{:02}:{:02}",
year, month, day, h, m, s
))
}
fn unix_to_components(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
let s = (secs % 60 + 60) as u32 % 60;
let secs2 = if secs % 60 < 0 {
secs - secs % 60 - 60
} else {
secs - secs % 60
};
let mins = secs2 / 60;
let m = (mins % 60 + 60) as u32 % 60;
let mins2 = if mins % 60 < 0 {
mins - mins % 60 - 60
} else {
mins - mins % 60
};
let hours = mins2 / 60;
let h = (hours % 24 + 24) as u32 % 24;
let hours2 = if hours % 24 < 0 {
hours - hours % 24 - 24
} else {
hours - hours % 24
};
let days = hours2 / 24;
let (year, month, day) = days_to_date(days);
(year, month, day, h, m, s)
}
fn days_to_date(days: i64) -> (i32, u32, u32) {
let z = days + 719468;
let era = if z >= 0 { z } else { z - 146096 } / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as i32, m as u32, d as u32)
}
fn dos_time(val: u32) -> Option<String> {
if val == 0 {
return None;
}
let year = ((val >> 9) & 0x7f) as i32 + 1980;
let month = (val >> 5) & 0x0f;
let day = val & 0x1f;
let hour = (val >> 27) & 0x1f;
let min = (val >> 21) & 0x3f;
let sec = (val >> 15) & 0x3e; if month == 0 || day == 0 {
return None;
}
Some(format!(
"{:04}:{:02}:{:02} {:02}:{:02}:{:02}",
year, month, day, hour, min, sec
))
}
fn read_u16_le(data: &[u8], off: usize) -> Option<u16> {
if off + 2 <= data.len() {
Some(u16::from_le_bytes([data[off], data[off + 1]]))
} else {
None
}
}
fn read_u32_le(data: &[u8], off: usize) -> Option<u32> {
if off + 4 <= data.len() {
Some(u32::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
]))
} else {
None
}
}
fn read_u64_le(data: &[u8], off: usize) -> Option<u64> {
if off + 8 <= data.len() {
Some(u64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
]))
} else {
None
}
}
fn read_cstring(data: &[u8], off: usize) -> Option<String> {
if off >= data.len() {
return None;
}
let end = data[off..]
.iter()
.position(|&b| b == 0)
.unwrap_or(data.len() - off);
Some(crate::encoding::decode_utf8_or_latin1(&data[off..off + end]).to_string())
}
fn read_utf16le_string(data: &[u8], off: usize) -> Option<String> {
if off + 2 > data.len() {
return None;
}
let mut chars = Vec::new();
let mut i = off;
while i + 1 < data.len() {
let ch = u16::from_le_bytes([data[i], data[i + 1]]);
if ch == 0 {
break;
}
chars.push(ch);
i += 2;
}
Some(String::from_utf16_lossy(&chars))
}
fn read_utf16le_fixed(data: &[u8], off: usize, byte_len: usize) -> Option<String> {
if off + byte_len > data.len() {
return None;
}
let chars: Vec<u16> = data[off..off + byte_len]
.chunks_exact(2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.collect();
let s = String::from_utf16_lossy(&chars);
Some(s.trim_end_matches('\0').to_string())
}
fn flags_to_string(val: u32, bits: &[(u32, &str)]) -> String {
let parts: Vec<&str> = bits
.iter()
.filter(|(mask, _)| val & (1 << mask) != 0)
.map(|(_, name)| *name)
.collect();
if parts.is_empty() {
format!("0x{:08x}", val)
} else {
parts.join(", ")
}
}
fn format_guid(data: &[u8]) -> Option<String> {
if data.len() < 16 {
return None;
}
let d1 = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
let d2 = u16::from_le_bytes([data[4], data[5]]);
let d3 = u16::from_le_bytes([data[6], data[7]]);
let d4 = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
let d5 = u32::from_be_bytes([data[12], data[13], data[14], data[15]]);
Some(
format!("{:08X}-{:04X}-{:04X}-{:08X}{:08X}", d1, d2, d3, d4, d5).replacen(
&format!("{:08X}{:08X}", d4, d5),
&{
let s = format!("{:08X}{:08X}", d4, d5);
format!("{}-{}", &s[0..4], &s[4..])
},
1,
),
)
}
fn guid_lookup(guid: &str) -> Option<&'static str> {
let upper = guid.to_uppercase();
GUID_TABLE
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(&upper))
.map(|(_, v)| *v)
}
fn format_guid_with_name(data: &[u8]) -> Option<String> {
let guid = format_guid(data)?;
if let Some(name) = guid_lookup(&guid) {
Some(format!("{} ({})", guid, name))
} else {
Some(guid)
}
}
static GUID_TABLE: &[(&str, &str)] = &[
(
"008CA0B1-55B4-4C56-B8A8-4DE4B299D3BE",
"Account Pictures (per-user)",
),
("DE61D971-5EBC-4F02-A3A9-6C82895E5C04", "Add New Programs"),
(
"724EF170-A42D-4FEF-9F26-B60E846FBA4F",
"Administrative Tools (per-user)",
),
(
"D0384E7D-BAC3-4797-8F14-CBA229B392B5",
"Administrative Tools",
),
("1E87508D-89C2-42F0-8A7E-645A0F50CA58", "Applications"),
(
"A3918781-E5F2-4890-B3D9-A7E54332328C",
"Application Shortcuts (per-user)",
),
("A305CE99-F527-492B-8B1A-7E76FA98D6E4", "Installed Updates"),
(
"9E52AB10-F80D-49DF-ACB8-4330F5687855",
"Assemblies (per-user)",
),
("1FE35CDE-B250-4B6A-A1C4-8164E91E8EC7", "Assemblies"),
(
"56784854-C6CB-462B-8169-88E350ACB882",
"Contacts (per-user)",
),
("B4BFCC3A-DB2C-424C-B029-7FE99A87C641", "Desktop (per-user)"),
("C4AA340D-F20F-4863-AFEF-F87EF2E6BA25", "Desktop"),
(
"5CE4A5E9-E4EB-479D-B89F-130C02886155",
"Device Metadata Store",
),
("7B0DB17D-9CD2-4A93-9733-46CC89022E7C", "Documents Library"),
(
"FDD39AD0-238F-46AF-ADB4-6C85480369C7",
"Documents (per-user)",
),
("ED4824AF-DCE4-45A8-81E2-FC7965083634", "Documents"),
(
"3B193882-D3AD-4EAB-965A-69829D1FB59F",
"Downloads (per-user)",
),
(
"374DE290-123F-4565-9164-39C4925E467B",
"Downloads (per-user) [2]",
),
(
"7D1D3A04-DEBB-4115-95CF-2F29DA2920DA",
"Saved Searches (per-user)",
),
("1AC14E77-02E7-4E5D-B744-2EB1AE5198B7", "System"),
("D65231B0-B2F1-4857-A4CE-A8E7C6EA7D27", "System32\\x86"),
("0762D272-C50A-4BB0-A382-697DCD729B80", "User Profiles"),
(
"5CD7AEE2-2219-4A67-B85D-6C9CE15660CB",
"Programs (per-user)",
),
("BCBD3057-CA5C-4622-B42D-BC56DB0AE516", "Programs"),
(
"4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4",
"Saved Games (per-user)",
),
(
"7C5A40EF-A0FB-4BFC-874A-C0F2E0B9FA8E",
"Programs (per-user Start Menu)",
),
(
"A4115719-D62E-491D-AA7C-E74B8BE3B067",
"Programs (Start Menu)",
),
(
"625B53C3-AB48-4EC1-BA1F-A1EF4146FC19",
"Start Menu (per-user)",
),
("A77F5D77-2E2B-44C3-A6A2-ABA601054A51", "Start Menu"),
("B97D20BB-F46A-4C97-BA10-5E3608430854", "Startup (per-user)"),
("82A5EA35-D9CD-47C5-9629-E15D2F714E6E", "Startup"),
("43668BF8-C14E-49B2-97C9-747784D784B7", "Sync Manager"),
("289A9A43-BE44-4057-A41B-587A76D7E7F9", "Sync Results"),
("0F214138-B1D3-4A90-BBA9-27CBC0C5389A", "Sync Setup"),
("1F3427C8-5C10-4210-AA03-2EE45287D668", "User Pinned"),
("F3CE0F7C-4901-4ACC-8648-D5D44B04EF8F", "Users Files"),
(
"A52BBA46-E9E1-435F-B3D9-28DAA648C0F6",
"OneDrive (per-user)",
),
("DFDF76A2-C82A-4D63-906A-5644AC457385", "Public (per-user)"),
("C4900540-2379-4C75-844B-64E6FAF8716B", "Public"),
("ED4824AF-DCE4-45A8-81E2-FC7965083634", "Public Documents"),
("3D644C9B-1FB8-4F30-9B45-F670235F79C0", "Public Downloads"),
(
"DEBF2536-E1A8-4C59-B6A2-414586476AEA",
"Public Game Explorer",
),
("48DAF80B-E6CF-4F4E-B800-0E69D84EE384", "Public Libraries"),
("3214FAB5-9757-4298-BB61-92A9DEAA44FF", "Public Music"),
("B6EBFB86-6907-413C-9AF7-4FC2ABF07CC5", "Public Pictures"),
("2400183A-6185-49FB-A2D8-4A392A602BA3", "Public Videos"),
("52A4F021-7B75-48A9-9F6B-4B87A210BC8F", "Quick Launch"),
(
"AE50C081-EBD2-438A-8655-8A092E34987A",
"Recent Items (per-user)",
),
("B7534046-3ECB-4C18-BE4E-64CD4CB7D6AC", "Recycle Bin"),
(
"8AD10C31-2ADB-4296-A8F7-E4701232C972",
"Resources (per-user)",
),
(
"C870044B-F49E-4126-A9C3-B52A1FF411E8",
"Ringtones (per-user)",
),
("E555AB60-153B-4D17-9F04-A5FE99FC15EC", "Ringtones"),
("3EB685DB-65F9-4CF6-A03A-E3EF65729F3D", "Roaming (per-user)"),
(
"AAA8D5A5-F1D6-4259-BAA8-78E7EF60835E",
"Roaming Tiles (per-user)",
),
(
"B250C668-F532-474D-A4B0-6A08CB4F0F74",
"Sample Music (obsolete)",
),
(
"C4900540-2379-4C75-844B-64E6FAF8716B",
"Sample Pictures (obsolete)",
),
(
"15CA69B3-30EE-49C1-ACE1-6B5EC372AFB5",
"Sample Playlists (obsolete)",
),
(
"859EAD94-2E85-48AD-A71A-0969CB56A6CD",
"Sample Videos (obsolete)",
),
(
"4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4",
"Saved Games (per-user)",
),
("EE32E446-31CA-4ABA-814F-A5EBD2FD6D5E", "Offline Files"),
(
"98EC0E18-2098-4D44-8644-66979315A281",
"Microsoft Office Outlook (per-user)",
),
(
"190337D1-B8CA-4121-A639-6D472D16972A",
"Searches (per-user)",
),
("8983036C-27C0-404B-8F08-102D10DCFD74", "SendTo (per-user)"),
(
"A3918781-E5F2-4890-B3D9-A7E54332328C",
"Application Shortcuts (per-user)",
),
(
"AB5FB87B-7CE2-4F83-915D-550846C9537B",
"Camera Roll (per-user)",
),
(
"B7BEDE81-DF94-4682-A7D8-57A52620B86F",
"Screenshots (per-user)",
),
(
"2B20D9D9-7BBE-4C7B-A2DA-B76FAADCC677",
"3D Objects (per-user)",
),
(
"767E6811-49CB-4273-87C2-20F355E1085B",
"Camera Roll (per-user)",
),
("2112AB0A-C86A-4FFE-A368-0DE96E47012E", "Music (per-user)"),
(
"339719B5-8C47-4894-94C2-D8F77ADD44A6",
"Pictures (per-user)",
),
(
"33E28130-4E1E-4676-835A-98395C3BC3BB",
"Pictures (per-user)",
),
(
"A302545D-DEFF-464B-ABE8-61C8648D939B",
"Libraries (virtual)",
),
(
"18989B1D-99B5-455B-841C-AB7C74E4DDFC",
"MyVideos (per-user)",
),
("491E922F-5643-4AF4-A7EB-4E7A138D8174", "Videos (per-user)"),
(
"00021401-0000-0000-C000-000000000046",
"Shell Link Class Identifier",
),
("20D04FE0-3AEA-1069-A2D8-08002B30309D", "My Computer"),
("450D8FBA-AD25-11D0-A2A8-0800361B3003", "My Documents"),
("D8B0C1EE-DA91-44CB-A0F8-6851F14ECBC7", "OneDrive"),
(
"B4FB3F98-C1EA-428D-A78A-D1F5659CBA93",
"My Documents (Home)",
),
("F02C1A0D-BE21-4350-88B0-7367FC96EF3C", "Network"),
("871C5380-42A0-1069-A2EA-08002B30301D", "Internet Explorer"),
("645FF040-5081-101B-9F08-00AA002F954E", "Recycle Bin"),
("B4FB3F98-C1EA-428D-A78A-D1F5659CBA93", "HomeGroup"),
("9E395ED8-512D-4315-9960-9110B74616C8", "Recent Items"),
(
"21EC2020-3AEA-1069-A2DD-08002B30309D",
"Control Panel Items",
),
(
"7007ACC7-3202-11D1-AAD2-00805FC1270E",
"Network Connections",
),
("26EE0668-A00A-44D7-9371-BEB064C98683", "Control Panel"),
(
"2559A1F1-21D7-11D4-BDAF-00C04F60B9F0",
"Windows Help and Support",
),
("031E4825-7B94-4DC3-B131-E946B44C8DD5", "Libraries"),
("22877A6D-37A1-461A-91B0-DBDA5AAEBC99", "Recent Items"),
("2559A1F3-21D7-11D4-BDAF-00C04F60B9F0", "Run Dialog Box"),
("3080F90D-D7AD-11D9-BD98-0000947B0257", "Desktop"),
("3080F90E-D7AD-11D9-BD98-0000947B0257", "Task View"),
(
"4336A54D-038B-4685-AB02-99BB52D3FB8B",
"Public User Root Folder",
),
("5399E694-6CE5-4D6C-8FCE-1D8870FDCBA0", "Control Panel"),
("59031A47-3F72-44A7-89C5-5595FE6B30EE", "User Profile"),
("871C5380-42A0-1069-A2EA-08002B30309D", "Internet"),
("ED228FDF-9EA8-4870-83B1-96B02CFE0D52", "Game Explorer"),
("A8CDFF1C-4878-43BE-B5FD-F8091C1C60D0", "Documents"),
("3ADD1653-EB32-4CB0-BBD7-DFA0ABB5ACCA", "My Pictures"),
("0C39A5CF-1A7A-40C8-BA74-8900E6DF5FCD", "Recent Items"),
("5E591A74-DF96-48D3-8D67-1733BCEE28BA", "Delegate GUID"),
("04731B67-D933-450A-90E6-4ACD2E9408FE", "Search Folder"),
("DFFACDC5-679F-4156-8947-C5C76BC0B67F", "Users Files"),
("289AF617-1CC3-42A6-926C-E6A863F0E3BA", "My Computer"),
("3134EF9C-6B18-4996-AD04-ED5912E00EB5", "Recent Files"),
("35786D3C-B075-49B9-88DD-029876E11C01", "Portable Devices"),
("3936E9E4-D92C-4EEE-A85A-BC16D5EA0819", "Frequent Places"),
("59031A47-3F72-44A7-89C5-5595FE6B30EE", "Shared Documents"),
(
"640167B4-59B0-47A6-B335-A6B3C0695AEA",
"Portable Media Devices",
),
("896664F7-12E1-490F-8782-C0835AFD98FC", "Libraries"),
(
"9113A02D-00A3-46B9-BC5F-9C04DADDD5D7",
"Enhanced Storage Data Source",
),
("9DB7A13C-F208-4981-8353-73CC61AE2783", "Previous Versions"),
("B155BDF8-02F0-451E-9A26-AE317CFD7779", "NetHood"),
("C2B136E2-D50E-405C-8784-363C582BF43E", "Wireless Devices"),
("D34A6CA6-62C2-4C34-8A7C-14709C1AD938", "Common Places"),
("ED50FC29-B964-48A9-AFB3-15EBB9B97F36", "PrintHood"),
("F5FB2C77-0E2F-4A16-A381-3E560C68BC83", "Removable Drives"),
("80E170D2-1055-4A3E-B952-82CC4F8A8689", "Content Type All"),
(
"0FED060E-8793-4B1E-90C9-48AC389AC631",
"Content Type Appointment",
),
("4AD2C85E-5E2D-45E5-8864-4F229E3C6CF0", "Content Type Audio"),
(
"AA18737E-5009-48FA-AE21-85F24383B4E6",
"Content Type Audio Album",
),
(
"A1FD5967-6023-49A0-9DF1-F8060BE751B0",
"Content Type Calendar",
),
(
"DC3876E8-A948-4060-9050-CBD77E8A3D87",
"Content Type Certificate",
),
(
"EABA8313-4525-4707-9F0E-87C6808E9435",
"Content Type Contact",
),
(
"346B8932-4C36-40D8-9415-1828291F9DE9",
"Content Type Contact Group",
),
(
"680ADF52-950A-4041-9B41-65E393648155",
"Content Type Document",
),
("8038044A-7E51-4F8F-883D-1D0623D14533", "Content Type Email"),
(
"27E2E392-A111-48E0-AB0C-E17705A05F85",
"Content Type Folder",
),
(
"99ED0160-17FF-4C44-9D98-1D7A6F941921",
"Content Type Functional Object",
),
(
"0085E0A6-8D34-45D7-BC5C-447E59C73D48",
"Content Type Generic File",
),
(
"E80EAAF8-B2DB-4133-B67E-1BEF4B4A6E5F",
"Content Type Generic Message",
),
("EF2107D5-A52A-4243-A26B-62D4176D7603", "Content Type Image"),
(
"75793148-15F5-4A30-A813-54ED8A37E226",
"Content Type Image Album",
),
(
"5E88B3CC-3E65-4E62-BFFF-229495253AB0",
"Content Type Media Cast",
),
("9CD20ECF-3B50-414F-A641-E473FFE45751", "Content Type Memo"),
(
"00F0C3AC-A593-49AC-9219-24ABCA5A2563",
"Content Type Mixed Content Album",
),
(
"031DA7EE-18C8-4205-847E-89A11261D0F3",
"Content Type Network Association",
),
(
"1A33F7E4-AF13-48F5-994E-77369DFE04A3",
"Content Type Playlist",
),
(
"D269F96A-247C-4BFF-98FB-97F3C49220E6",
"Content Type Program",
),
(
"821089F5-1D91-4DC9-BE3C-BBB1B35B18CE",
"Content Type Section",
),
("63252F2C-887F-4CB6-B1AC-D29855DCEF6C", "Content Type Task"),
(
"60A169CF-F2AE-4E21-9375-9677F11C1C6E",
"Content Type Television",
),
(
"28D8D31E-249C-454E-AABC-34883168E634",
"Content Type Unspecified",
),
("9261B03C-3D78-4519-85E3-02C5E1F50BB9", "Content Type Video"),
(
"012B0DB7-D4C1-45D6-B081-94B87779614F",
"Content Type Video Album",
),
(
"0BAC070A-9F5F-4DA4-A8F6-3DE44D68FD6C",
"Content Type Wireless Profile",
),
];
fn process_lnk_header(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 0x48 {
return;
}
if let Some(flags) = read_u32_le(data, 0x14) {
let flag_bits = [
(0u32, "IDList"),
(1, "LinkInfo"),
(2, "Description"),
(3, "RelativePath"),
(4, "WorkingDir"),
(5, "CommandArgs"),
(6, "IconFile"),
(7, "Unicode"),
(8, "NoLinkInfo"),
(9, "ExpString"),
(10, "SeparateProc"),
(12, "DarwinID"),
(13, "RunAsUser"),
(14, "ExpIcon"),
(15, "NoPidAlias"),
(17, "RunWithShim"),
(18, "NoLinkTrack"),
(19, "TargetMetadata"),
(20, "NoLinkPathTracking"),
(21, "NoKnownFolderTracking"),
(22, "NoKnownFolderAlias"),
(23, "LinkToLink"),
(24, "UnaliasOnSave"),
(25, "PreferEnvPath"),
(26, "KeepLocalIDList"),
];
let s = flags_to_string(flags, &flag_bits);
tags.push(mk_str("Flags", &s));
}
if let Some(attrs) = read_u32_le(data, 0x18) {
let attr_bits = [
(0u32, "Read-only"),
(1, "Hidden"),
(2, "System"),
(4, "Directory"),
(5, "Archive"),
(7, "Normal"),
(8, "Temporary"),
(9, "Sparse"),
(10, "Reparse point"),
(11, "Compressed"),
(12, "Offline"),
(13, "Not indexed"),
(14, "Encrypted"),
];
let s = if attrs == 0 {
"(none)".to_string()
} else if attrs & 0x80 != 0 {
"Normal".to_string()
} else {
let parts: Vec<&str> = attr_bits
.iter()
.filter(|(bit, _)| attrs & (1 << bit) != 0)
.map(|(_, name)| *name)
.collect();
if parts.is_empty() {
format!("0x{:08x}", attrs)
} else {
parts.join(", ")
}
};
tags.push(mk_str("FileAttributes", &s));
}
if let (Some(lo), Some(hi)) = (read_u32_le(data, 0x1c), read_u32_le(data, 0x20)) {
if let Some(dt) = filetime_to_datetime(lo, hi, 0) {
tags.push(mk_time("CreateDate", &dt));
}
}
if let (Some(lo), Some(hi)) = (read_u32_le(data, 0x24), read_u32_le(data, 0x28)) {
if let Some(dt) = filetime_to_datetime(lo, hi, 0) {
tags.push(mk_time("AccessDate", &dt));
}
}
if let (Some(lo), Some(hi)) = (read_u32_le(data, 0x2c), read_u32_le(data, 0x30)) {
if let Some(dt) = filetime_to_datetime(lo, hi, 0) {
tags.push(mk_time("ModifyDate", &dt));
}
}
if let Some(sz) = read_u32_le(data, 0x34) {
tags.push(mk("TargetFileSize", Value::U32(sz)));
}
if let Some(idx) = read_u32_le(data, 0x38) {
let s = if idx == 0 {
"(none)".to_string()
} else {
format!("{}", idx)
};
tags.push(mk_str("IconIndex", &s));
}
if let Some(rw) = read_u32_le(data, 0x3c) {
let s = match rw {
0 => "Hide",
1 => "Normal",
2 => "Show Minimized",
3 => "Show Maximized",
4 => "Show No Activate",
5 => "Show",
6 => "Minimized",
7 => "Show Minimized No Activate",
8 => "Show NA",
9 => "Restore",
10 => "Show Default",
_ => "Unknown",
};
tags.push(mk_str("RunWindow", s));
}
if let Some(hk) = read_u32_le(data, 0x40) {
let s = if hk == 0 {
"(none)".to_string()
} else {
let ch = hk & 0xff;
let mut key = if (0x30..=0x39).contains(&ch) || (0x41..=0x5a).contains(&ch) {
format!("{}", (ch as u8) as char)
} else if (0x70..=0x87).contains(&ch) {
format!("F{}", ch - 0x6f)
} else if ch == 0x90 {
"Num Lock".to_string()
} else if ch == 0x91 {
"Scroll Lock".to_string()
} else {
format!("Unknown (0x{:x})", ch)
};
if hk & 0x400 != 0 {
key = format!("Alt-{}", key);
}
if hk & 0x200 != 0 {
key = format!("Control-{}", key);
}
if hk & 0x100 != 0 {
key = format!("Shift-{}", key);
}
key
};
tags.push(mk_str("HotKey", &s));
}
}
fn process_item_id(data: &[u8], tags: &mut Vec<Tag>) {
let mut pos = 0;
while pos + 2 <= data.len() {
let size = match read_u16_le(data, pos) {
Some(s) => s as usize,
None => break,
};
if size == 0 {
break;
}
if size < 4 {
break;
}
let actual_size = if pos + size > data.len() {
data.len() - pos
} else {
size
};
let item_data = &data[pos..pos + actual_size];
let item_type = data[pos + 2];
let beef_start = find_beef_offset(item_data);
let item_len = if let Some(bs) = beef_start {
bs
} else {
actual_size
};
let effective_type = resolve_item_type(item_type);
match effective_type {
0x00 => process_item_00(&item_data[..item_len], tags),
0x01 => process_control_panel_info(&item_data[..item_len], tags),
0x1e | 0x1f => process_root_folder(&item_data[..item_len], tags),
0x2e => process_volume_guid(&item_data[..item_len], tags),
0x2f => {
if item_len > 5 {
if let Some(name) = read_cstring(item_data, 5) {
if !name.is_empty() {
tags.push(mk_str("VolumeName", &name));
}
}
}
}
0x31 => process_target_info(&item_data[..item_len], tags),
0x40 => process_network_location(&item_data[..item_len], tags),
0x61 => process_uri_item(&item_data[..item_len], tags),
0x70 | 0x71 => {
if item_len >= 30 {
if let Some(s) = format_guid_with_name(&item_data[16..32]) {
tags.push(mk_str("ControlPanelShellItem", &s));
}
}
}
0x74 => process_users_files_folder(&item_data[..item_len], tags),
0xff => process_vendor_data(&item_data[..item_len], tags),
_ => {}
}
if let Some(bs) = beef_start {
process_beef_extensions(item_data, bs, tags);
}
pos += actual_size;
}
}
fn find_beef_offset(data: &[u8]) -> Option<usize> {
if data.len() < 10 {
return None;
}
for i in 4..data.len().saturating_sub(3) {
if data[i + 1] == 0x00 && data[i + 2] == 0xef && data[i + 3] == 0xbe {
let block_start = i - 4; if block_start < 5 {
continue;
} let off2 = read_u16_le(data, data.len() - 2).unwrap_or(0) as usize;
if off2 == block_start {
return Some(block_start);
}
}
}
None
}
fn resolve_item_type(t: u8) -> u8 {
match t {
0x20..=0x2d => 0x2e, 0x2e => 0x2e,
0x2f => 0x2f,
0x30..=0x3f => 0x31,
0x40..=0x4f => 0x40,
other => other,
}
}
fn process_item_00(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 8 {
return;
}
if data.len() > 7 && data[5] == 0xff && data[6] == 0xff && data[7] == 0xff {
if let Some(special_type) = read_u32_le(data, 4) {
tags.push(mk_str(
"Item00SpecialType",
&format!("0x{:08x} (ControlPanelCPL)", special_type),
));
}
if data.len() > 14 {
extract_item00_strings(data, 14, tags, "CPLFilePath");
}
return;
}
if data.len() > 7 && &data[4..8] == b"GFSI" {
tags.push(mk_str("Item00SpecialType", "0x49534647 (GameFolder)"));
return;
}
if data.len() > 9 && data[6] == 0xee && data[7] == 0xbb && data[8] == 0xfe && data[9] == 0x23 {
if data.len() >= 30 {
if let Some(s) = format_guid_with_name(&data[14..30]) {
tags.push(mk_str("PropertyStoreGUID", &s));
}
}
return;
}
if data.len() > 9 && data[6] == 0x05 && data[7] == 0x20 && data[8] == 0x31 && data[9] == 0x10 {
process_mtp_type2(data, tags);
return;
}
if data.len() >= 10 {
if let Some(item_type) = read_u32_le(data, 6) {
tags.push(mk_str("Item00Type", &format!("0x{:08x}", item_type)));
}
}
if data.len() >= 26 {
if let (Some(prop1_len), Some(prop2_len)) = (read_u16_le(data, 20), read_u16_le(data, 22)) {
let expected_size = 24 + 2 * (prop1_len as usize + prop2_len as usize);
if expected_size == data.len() && prop1_len > 0 {
let off1 = 24;
let byte_len1 = prop1_len as usize * 2;
if let Some(s) = read_utf16le_fixed(data, off1, byte_len1) {
if !s.is_empty() {
tags.push(mk_str("PropertyString1", &s));
}
}
if prop2_len > 0 {
let off2 = off1 + byte_len1;
let byte_len2 = prop2_len as usize * 2;
if let Some(s) = read_utf16le_fixed(data, off2, byte_len2) {
if !s.is_empty() {
tags.push(mk_str("PropertyString2", &s));
}
}
}
}
}
}
}
fn extract_item00_strings(data: &[u8], off: usize, tags: &mut Vec<Tag>, tag_name: &str) {
if off >= data.len() {
return;
}
let sub = &data[off..];
if sub.len() >= 3
&& sub[0] >= 0x20
&& sub[0] <= 0x7f
&& sub[1] == 0x00
&& sub[2] >= 0x20
&& sub[2] <= 0x7f
{
let mut strings = Vec::new();
let mut pos = 0;
while pos + 1 < sub.len() {
if let Some(s) = read_utf16le_string(sub, pos) {
if !s.is_empty() {
let byte_len = s.encode_utf16().count() * 2 + 2;
strings.push(s);
pos += byte_len;
} else {
pos += 2;
}
} else {
break;
}
}
if !strings.is_empty() {
tags.push(mk_str(tag_name, &strings.join(", ")));
}
} else {
let mut strings = Vec::new();
for segment in sub.split(|&b| b == 0) {
let s: String = segment
.iter()
.filter(|&&b| (0x20..=0x7f).contains(&b))
.map(|&b| b as char)
.collect();
if !s.is_empty() {
strings.push(s);
}
}
if !strings.is_empty() {
tags.push(mk_str(tag_name, &strings.join(", ")));
}
}
}
fn process_mtp_type2(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 54 {
return;
}
let storage_name_len = read_u32_le(data, 38).unwrap_or(0) as usize;
let storage_id_len = read_u32_le(data, 42).unwrap_or(0) as usize;
let fs_name_len = read_u32_le(data, 46).unwrap_or(0) as usize;
let num_guids = read_u32_le(data, 50).unwrap_or(0) as usize;
let mut off = 54;
let byte_len = storage_name_len * 2;
if off + byte_len <= data.len() {
if let Some(s) = read_utf16le_fixed(data, off, byte_len) {
if !s.is_empty() {
tags.push(mk_str("MTPStorageName", &s));
}
}
}
off += byte_len;
let byte_len = storage_id_len * 2;
if off + byte_len <= data.len() {
if let Some(s) = read_utf16le_fixed(data, off, byte_len) {
if !s.is_empty() {
tags.push(mk_str("MTPStorageID", &s));
}
}
}
off += byte_len;
let byte_len = fs_name_len * 2;
if off + byte_len <= data.len() {
if let Some(s) = read_utf16le_fixed(data, off, byte_len) {
if !s.is_empty() {
tags.push(mk_str("MTPFileSystem", &s));
}
}
}
off += byte_len;
let max_guids = num_guids.min(8);
for i in 0..max_guids {
if off + 72 > data.len() {
break;
}
if let Some(s) = read_utf16le_fixed(data, off, 72) {
let trimmed = s.trim();
if !trimmed.is_empty() {
let name = format!("MTP_GUID{}", i + 1);
if let Some(lookup) = guid_lookup(trimmed) {
tags.push(mk_str(&name, &format!("{} ({})", trimmed, lookup)));
} else {
tags.push(mk_str(&name, trimmed));
}
}
}
off += 78; }
}
fn process_control_panel_info(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 12 {
return;
}
if let Some(cat) = read_u32_le(data, 8) {
let s = match cat {
0 => "All Control Panel Items",
1 => "Appearance and Personalization",
2 => "Hardware and Sound",
3 => "Network and Internet",
4 => "Sounds, Speech, and Audio Devices",
5 => "System and Security",
6 => "Clock, Language, and Region",
7 => "Ease of Access",
8 => "Programs",
9 => "User Accounts",
10 => "Security Center",
11 => "Mobile PC",
_ => "",
};
if s.is_empty() {
tags.push(mk_str("ControlPanelCategory", &format!("{}", cat)));
} else {
tags.push(mk_str("ControlPanelCategory", s));
}
}
}
fn process_root_folder(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 5 {
return;
}
let sort_idx = data[3];
let sort_name = match sort_idx {
0x00 => "Internet Explorer",
0x42 => "Libraries",
0x44 => "Users",
0x48 => "My Documents",
0x4c => "Public Folder",
0x50 => "My Computer",
0x54 => "Users Libraries",
0x58 => "My Network Places/Network",
0x60 => "Recycle Bin",
0x68 => "Internet Explorer",
0x70 => "Control Panel",
0x78 => "Recycle Bin",
0x80 => "My Games",
_ => "",
};
if !sort_name.is_empty() {
tags.push(mk_str(
"SortIndex",
&format!("0x{:02x} ({})", sort_idx, sort_name),
));
} else {
tags.push(mk_str("SortIndex", &format!("0x{:02x}", sort_idx)));
}
if data.len() >= 20 {
if let Some(s) = format_guid_with_name(&data[4..20]) {
tags.push(mk_str("RootFolderGUID", &s));
}
}
}
fn process_volume_guid(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 20 {
return;
}
let guid_off = data.len() - 16;
if let Some(s) = format_guid_with_name(&data[guid_off..guid_off + 16]) {
tags.push(mk_str("VolumeGUID", &s));
}
}
fn process_target_info(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 14 {
return;
}
if let Some(val) = read_u32_le(data, 8) {
if val != 0 {
if let Some(dt) = dos_time(val) {
tags.push(mk_time("TargetFileModifyDate", &dt));
}
}
}
if let Some(attrs) = read_u16_le(data, 12) {
let attr_bits = [
(0u32, "Read-only"),
(1, "Hidden"),
(2, "System"),
(4, "Directory"),
(5, "Archive"),
(7, "Normal"),
];
let s = if attrs & 0x80 != 0 {
"Normal".to_string()
} else {
let parts: Vec<&str> = attr_bits
.iter()
.filter(|(bit, _)| (attrs as u32) & (1 << bit) != 0)
.map(|(_, name)| *name)
.collect();
if parts.is_empty() {
"(none)".to_string()
} else {
parts.join(", ")
}
};
tags.push(mk_str("TargetFileAttributes", &s));
}
if data.len() > 16 {
if data[14] >= 0x20
&& data[14] <= 0x7f
&& data[15] == 0x00
&& data.len() > 16
&& data[16] >= 0x20
&& data[16] <= 0x7f
{
if let Some(name) = read_utf16le_string(data, 14) {
if !name.is_empty() {
tags.push(mk_str("TargetFileDOSName", &name));
}
}
} else {
if let Some(name) = read_cstring(data, 14) {
if !name.is_empty() {
tags.push(mk_str("TargetFileDOSName", &name));
}
}
}
}
}
fn process_network_location(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 8 {
return;
}
let sub = &data[6..];
let mut strings = Vec::new();
for segment in sub.split(|&b| b == 0) {
let s: String = segment
.iter()
.filter(|&&b| (0x20..=0x7f).contains(&b))
.map(|&b| b as char)
.collect();
if !s.is_empty() {
strings.push(s);
}
}
if !strings.is_empty() {
tags.push(mk_str("NetworkLocation", &strings.join(", ")));
}
}
fn process_uri_item(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 8 {
return;
}
let uri_flags = data[3];
let uri_data_size = read_u16_le(data, 4).unwrap_or(0);
let is_unicode = uri_flags & 0x80 != 0;
if uri_data_size == 0 {
if data.len() > 8 {
let s = if is_unicode {
read_utf16le_string(data, 8).unwrap_or_default()
} else {
read_cstring(data, 8).unwrap_or_default()
};
if !s.is_empty() {
tags.push(mk_str("URI", &s));
}
}
} else {
if data.len() >= 22 {
if let Some(ft) = read_u64_le(data, 14) {
if let Some(dt) = filetime64_to_datetime(ft) {
tags.push(mk_time("TimeStamp", &dt));
}
}
}
let mut off = 42;
let ftp_fields = ["FTPHost", "FTPUserName", "FTPPassword"];
for field_name in &ftp_fields {
if off + 4 > data.len() {
break;
}
let field_len = read_u32_le(data, off).unwrap_or(0) as usize;
off += 4;
if field_len > 0 && off + field_len <= data.len() {
let s = if is_unicode {
read_utf16le_fixed(data, off, field_len).unwrap_or_default()
} else {
crate::encoding::decode_utf8_or_latin1(&data[off..off + field_len])
.trim_end_matches('\0')
.to_string()
};
if !s.is_empty() {
tags.push(mk_str(field_name, &s));
}
}
off += field_len;
}
if off < data.len() {
let s = if is_unicode {
read_utf16le_string(data, off).unwrap_or_default()
} else {
read_cstring(data, off).unwrap_or_default()
};
if !s.is_empty() {
tags.push(mk_str("URI", &s));
}
}
}
}
fn process_users_files_folder(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 6 {
return;
}
let inner_size = read_u16_le(data, 4).unwrap_or(0) as usize;
let off = 6 + inner_size;
if off + 16 <= data.len() {
if let Some(s) = format_guid_with_name(&data[off..off + 16]) {
tags.push(mk_str("DelegateClassGUID", &s));
}
}
if off + 32 <= data.len() {
if let Some(s) = format_guid_with_name(&data[off + 16..off + 32]) {
tags.push(mk_str("DelegateFolderGUID", &s));
}
}
}
fn process_vendor_data(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 4 {
return;
}
let mut strings = Vec::new();
let mut i = 2;
while i + 5 < data.len() {
if data[i] >= 0x20 && data[i] <= 0x7f && data[i + 1] == 0x00 {
if let Some(s) = read_utf16le_string(data, i) {
if s.len() >= 3 {
strings.push(s.clone());
i += (s.encode_utf16().count() * 2) + 2;
continue;
}
}
}
i += 1;
}
if strings.is_empty() {
for segment in data[2..].split(|&b| b == 0) {
let s: String = segment
.iter()
.filter(|&&b| (0x20..=0x7e).contains(&b))
.map(|&b| b as char)
.collect();
if s.len() >= 3 {
strings.push(s);
}
}
}
if !strings.is_empty() {
tags.push(mk_str("VendorData", &strings.join(", ")));
}
}
fn process_beef_extensions(data: &[u8], start: usize, tags: &mut Vec<Tag>) {
let end = data.len();
let mut off = start;
while off + 8 <= end {
let len = read_u16_le(data, off).unwrap_or(0) as usize;
if len < 4 {
break;
}
if off + len > end {
break;
}
let beef_id = read_u32_le(data, off + 4).unwrap_or(0);
if beef_id & 0xffff0000 != 0xbeef0000 {
break;
}
let block = &data[off..off + len];
match beef_id {
0xbeef0003 => process_beef0003(block, tags),
0xbeef0004 => process_beef0004(block, tags),
0xbeef0014 => process_beef0014(block, tags),
0xbeef0025 => process_beef0025(block, tags),
0xbeef0026 => process_beef0026(block, tags),
_ => {}
}
let padded_len = if len & 1 != 0 { len + 1 } else { len };
off += padded_len;
}
}
fn process_beef0003(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() >= 24 {
if let Some(s) = format_guid_with_name(&data[8..24]) {
tags.push(mk_str("UnknownGUID", &s));
}
}
}
fn process_beef0004(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 12 {
return;
}
let version = read_u16_le(data, 2).unwrap_or(0);
if let Some(val) = read_u32_le(data, 8) {
if val != 0 {
if let Some(dt) = dos_time(val) {
tags.push(mk_time("TargetFileCreateDate", &dt));
}
}
}
if data.len() >= 16 {
if let Some(val) = read_u32_le(data, 12) {
if val != 0 {
if let Some(dt) = dos_time(val) {
tags.push(mk_time("TargetFileAccessDate", &dt));
}
}
}
}
if data.len() >= 18 {
if let Some(os_val) = read_u16_le(data, 16) {
let os_name = match os_val {
0x14 => "Windows XP, 2003",
0x26 => "Windows Vista",
0x2a => "Windows 2008, 7, 8",
0x2e => "Windows 8.1, 10",
_ => "",
};
if !os_name.is_empty() {
tags.push(mk_str("OperatingSystem", os_name));
} else {
tags.push(mk_str("OperatingSystem", &format!("0x{:04x}", os_val)));
}
}
}
let mut var_size: usize = 0;
if version >= 7 {
var_size += 18;
}
if version >= 3 {
var_size += 2;
}
if version >= 9 {
var_size += 4;
}
if version >= 8 {
var_size += 4;
}
let name_off = 18 + var_size;
if name_off + 4 < data.len() {
let name_len = data.len().saturating_sub(name_off + 2); if name_len >= 2 {
let name_data = &data[name_off..name_off + name_len];
let mut strings = Vec::new();
let mut pos = 0;
while pos + 1 < name_data.len() {
if let Some(s) = read_utf16le_string(name_data, pos) {
if !s.is_empty() {
let byte_len = s.encode_utf16().count() * 2 + 2;
strings.push(s);
pos += byte_len;
} else {
pos += 2;
}
} else {
break;
}
}
if strings.len() == 1 {
tags.push(mk_str("TargetFileName", &strings[0]));
} else if strings.len() > 1 {
tags.push(mk_str("TargetFileName", &strings.join(", ")));
}
}
}
}
fn process_beef0014(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 56 {
return;
}
let num = read_u32_le(data, 52).unwrap_or(0) as usize;
let mut off = 56;
let uri_tag_names = [
"AbsoluteURI",
"URIAuthority",
"DisplayURI",
"URIDomain",
"URIExtension",
"URIFragment",
"URIHost",
"URIPassword",
"URIPath",
"URIPathAndQuery",
"URIQuery",
"RawURI",
"URISchemeName",
"URIUserInfo",
"URIUserName",
"URIHostType",
"URIPort",
"URIScheme",
"URIZone",
];
for _i in 0..num {
if off + 8 > data.len() {
break;
}
let tag_id = read_u32_le(data, off).unwrap_or(0) as usize;
let size = read_u32_le(data, off + 4).unwrap_or(0) as usize;
off += 8;
if size == 0 {
continue;
}
if off + size > data.len() {
break;
}
let val = read_utf16le_fixed(data, off, size).unwrap_or_default();
if !val.is_empty() {
let name = if tag_id < uri_tag_names.len() {
uri_tag_names[tag_id]
} else {
"UnknownURI"
};
tags.push(mk_str(name, &val));
}
off += size;
}
}
fn process_beef0025(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() >= 0x14 {
if let Some(ft) = read_u64_le(data, 0x0c) {
if let Some(dt) = filetime64_to_datetime(ft) {
tags.push(mk_time("FileTime1", &dt));
}
}
}
if data.len() >= 0x1c {
if let Some(ft) = read_u64_le(data, 0x14) {
if let Some(dt) = filetime64_to_datetime(ft) {
tags.push(mk_time("FileTime2", &dt));
}
}
}
}
fn process_beef0026(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 9 {
return;
}
let marker = data[8];
if marker != 0x11 && marker != 0x10 && marker != 0x12 && marker != 0x34 && marker != 0x31 {
return;
}
if data.len() >= 0x14 {
if let Some(ft) = read_u64_le(data, 0x0c) {
if let Some(dt) = filetime64_to_datetime(ft) {
tags.push(mk_time("CreateDate", &dt));
}
}
}
if data.len() >= 0x1c {
if let Some(ft) = read_u64_le(data, 0x14) {
if let Some(dt) = filetime64_to_datetime(ft) {
tags.push(mk_time("ModifyDate", &dt));
}
}
}
if data.len() >= 0x24 {
if let Some(ft) = read_u64_le(data, 0x1c) {
if let Some(dt) = filetime64_to_datetime(ft) {
tags.push(mk_time("LastAccessDate", &dt));
}
}
}
}
fn process_link_info(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 0x1c {
return;
}
let hdr_len = read_u32_le(data, 4).unwrap_or(0x1c) as usize;
let lif = read_u32_le(data, 8).unwrap_or(0);
if lif & 0x01 != 0 {
let vol_off = read_u32_le(data, 0x0c).unwrap_or(0) as usize;
if vol_off != 0 && vol_off + 0x14 <= data.len() {
if let Some(dt) = read_u32_le(data, vol_off + 4) {
let s = match dt {
0 => "Unknown",
1 => "Invalid Root Path",
2 => "Removable Media",
3 => "Fixed Disk",
4 => "Remote Drive",
5 => "CD-ROM",
6 => "Ram Disk",
_ => "Unknown",
};
tags.push(mk_str("DriveType", s));
}
if let Some(sn) = read_u32_le(data, vol_off + 8) {
let s = format!("{:04X}-{:04X}", (sn >> 16) & 0xffff, sn & 0xffff);
tags.push(mk_str("DriveSerialNumber", &s));
}
if vol_off + 0x10 <= data.len() {
let lbl_off_rel = read_u32_le(data, vol_off + 0x0c).unwrap_or(0) as usize;
let (lbl_str, unicode) = if lbl_off_rel == 0x14 && vol_off + 0x14 <= data.len() {
let uni_off = read_u32_le(data, vol_off + 0x10).unwrap_or(0) as usize;
(read_utf16le_string(data, vol_off + uni_off), true)
} else {
(read_cstring(data, vol_off + lbl_off_rel), false)
};
let _ = unicode;
if let Some(lbl) = lbl_str {
tags.push(mk_str("VolumeLabel", &lbl));
}
}
}
let lbp_off = if hdr_len >= 0x24 && data.len() >= 0x24 {
read_u32_le(data, 0x1c).unwrap_or(0) as usize
} else {
read_u32_le(data, 0x10).unwrap_or(0) as usize
};
let unicode_path = hdr_len >= 0x24;
let lbp = if unicode_path {
read_utf16le_string(data, lbp_off)
} else {
read_cstring(data, lbp_off)
};
if let Some(path) = lbp {
if !path.is_empty() {
tags.push(mk_str("LocalBasePath", &path));
}
}
}
let cps_off = read_u32_le(data, 0x18).unwrap_or(0) as usize;
if cps_off != 0 && cps_off < data.len() {
let cps = read_cstring(data, cps_off).unwrap_or_default();
tags.push(mk_str("CommonPathSuffix", &cps));
}
}
fn process_console_data(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 0x90 {
return;
}
if let Some(v) = read_u16_le(data, 0x08) {
tags.push(mk_str("FillAttributes", &format!("0x{:02x}", v)));
}
if let Some(v) = read_u16_le(data, 0x0a) {
tags.push(mk_str("PopupFillAttributes", &format!("0x{:02x}", v)));
}
if let (Some(x), Some(y)) = (read_u16_le(data, 0x0c), read_u16_le(data, 0x0e)) {
tags.push(mk_str("ScreenBufferSize", &format!("{} x {}", x, y)));
}
if let (Some(x), Some(y)) = (read_u16_le(data, 0x10), read_u16_le(data, 0x12)) {
tags.push(mk_str("WindowSize", &format!("{} x {}", x, y)));
}
if let (Some(x), Some(y)) = (read_u16_le(data, 0x14), read_u16_le(data, 0x16)) {
tags.push(mk_str("WindowOrigin", &format!("{} x {}", x, y)));
}
if let (Some(x), Some(y)) = (read_u16_le(data, 0x20), read_u16_le(data, 0x22)) {
tags.push(mk_str("FontSize", &format!("{} x {}", x, y)));
}
if let Some(ff) = read_u32_le(data, 0x24) {
let s = match (ff & 0xf0) >> 4 {
0x0 => "Don't Care",
0x1 => "Roman",
0x2 => "Swiss",
0x3 => "Modern",
0x4 => "Script",
0x5 => "Decorative",
_ => "Unknown",
};
tags.push(mk_str("FontFamily", s));
}
if let Some(fw) = read_u32_le(data, 0x28) {
tags.push(mk("FontWeight", Value::U32(fw)));
}
if data.len() >= 0x6c {
let fn_data = &data[0x2c..0x2c + 64];
let chars: Vec<u16> = fn_data
.chunks_exact(2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.take_while(|&c| c != 0)
.collect();
let name = String::from_utf16_lossy(&chars);
if !name.is_empty() {
tags.push(mk_str("FontName", &name));
}
}
if let Some(cs) = read_u32_le(data, 0x6c) {
tags.push(mk("CursorSize", Value::U32(cs)));
}
if let Some(v) = read_u32_le(data, 0x70) {
tags.push(mk_str("FullScreen", if v != 0 { "Yes" } else { "No" }));
}
if let Some(v) = read_u32_le(data, 0x74) {
tags.push(mk_str("QuickEdit", if v != 0 { "Yes" } else { "No" }));
}
if let Some(v) = read_u32_le(data, 0x78) {
tags.push(mk_str("InsertMode", if v != 0 { "Yes" } else { "No" }));
}
if let Some(v) = read_u32_le(data, 0x7c) {
tags.push(mk_str(
"WindowOriginAuto",
if v != 0 { "Yes" } else { "No" },
));
}
if let Some(v) = read_u32_le(data, 0x80) {
tags.push(mk("HistoryBufferSize", Value::U32(v)));
}
if let Some(v) = read_u32_le(data, 0x84) {
tags.push(mk("NumHistoryBuffers", Value::U32(v)));
}
if let Some(v) = read_u32_le(data, 0x88) {
tags.push(mk_str(
"RemoveHistoryDuplicates",
if v != 0 { "Yes" } else { "No" },
));
}
}
fn process_tracker_data(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 0x20 {
return;
}
if let Some(id) = read_cstring(data, 0x10) {
if !id.is_empty() {
tags.push(mk_str("MachineID", &id));
}
}
}
fn process_console_fe_data(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 0x0c {
return;
}
if let Some(cp) = read_u32_le(data, 0x08) {
tags.push(mk("CodePage", Value::U32(cp)));
}
}
pub fn read_lnk(data: &[u8]) -> crate::error::Result<Vec<Tag>> {
if data.len() < 0x4c {
return Ok(Vec::new());
}
let hdr_size = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
if hdr_size < 0x4c {
return Ok(Vec::new());
}
let clsid_ok = data[4..20]
== [
0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x46,
];
if !clsid_ok {
return Ok(Vec::new());
}
let mut tags = Vec::new();
let flags = read_u32_le(data, 0x14).unwrap_or(0);
process_lnk_header(data, &mut tags);
let mut pos = hdr_size;
if flags & 0x01 != 0 {
if pos + 2 > data.len() {
return Ok(tags);
}
let list_len = read_u16_le(data, pos).unwrap_or(0) as usize;
pos += 2;
if pos + list_len > data.len() {
return Ok(tags);
}
let id_data = &data[pos..pos + list_len];
process_item_id(id_data, &mut tags);
pos += list_len;
}
if flags & 0x02 != 0 {
if pos + 4 > data.len() {
return Ok(tags);
}
let li_len = read_u32_le(data, pos).unwrap_or(0) as usize;
if pos + li_len > data.len() {
return Ok(tags);
}
let li_data = &data[pos..pos + li_len];
process_link_info(li_data, &mut tags);
pos += li_len;
}
let string_names = [
"Description",
"RelativePath",
"WorkingDirectory",
"CommandLineArguments",
"IconFileName",
];
let string_flag_masks = [0x04u32, 0x08, 0x10, 0x20, 0x40];
let is_unicode = (flags & 0x80) != 0;
for (i, (&mask, &name)) in string_flag_masks
.iter()
.zip(string_names.iter())
.enumerate()
{
if flags & mask == 0 {
continue;
}
if pos + 2 > data.len() {
break;
}
let char_count = read_u16_le(data, pos).unwrap_or(0) as usize;
pos += 2;
if char_count == 0 {
continue;
}
let limit = if i != 3 { 260 } else { usize::MAX };
let actual_count = char_count.min(limit);
let byte_len = if is_unicode {
actual_count * 2
} else {
actual_count
};
if pos + byte_len > data.len() {
break;
}
let s = if is_unicode {
let chars: Vec<u16> = data[pos..pos + byte_len]
.chunks_exact(2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.collect();
String::from_utf16_lossy(&chars).to_string()
} else {
crate::encoding::decode_utf8_or_latin1(&data[pos..pos + byte_len]).to_string()
};
let full_byte_len = if is_unicode {
char_count * 2
} else {
char_count
};
pos += full_byte_len.min(data.len() - pos);
if !s.is_empty() {
tags.push(mk_str(name, &s));
}
}
while pos + 4 <= data.len() {
let block_len = read_u32_le(data, pos).unwrap_or(0) as usize;
if block_len < 4 {
break;
}
if pos + block_len > data.len() {
break;
}
let block_data = &data[pos..pos + block_len];
if block_data.len() < 8 {
pos += block_len;
continue;
}
let block_sig = read_u32_le(block_data, 4).unwrap_or(0);
match block_sig {
0xa0000002 => process_console_data(block_data, &mut tags),
0xa0000003 => process_tracker_data(block_data, &mut tags),
0xa0000004 => process_console_fe_data(block_data, &mut tags),
_ => {}
}
pos += block_len;
}
Ok(tags)
}
pub fn read_url(data: &[u8]) -> crate::error::Result<Vec<Tag>> {
if data.len() >= 20 {
let clsid_ok = data.len() >= 20
&& data[4..20]
== [
0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x46,
];
if clsid_ok {
return read_lnk(data);
}
}
let text = crate::encoding::decode_utf8_or_latin1(data);
let mut tags = Vec::new();
for line in text.lines() {
let line = line.trim_end_matches('\r');
if line.starts_with('[') {
continue;
}
if let Some(eq) = line.find('=') {
let key = &line[..eq];
let val = &line[eq + 1..];
match key {
"URL" | "IconFile" | "IconIndex" | "WorkingDirectory" | "HotKey" | "Author"
| "WhatsNew" | "Comment" | "Desc" | "Roamed" | "IDList" => {
tags.push(mk_str(key, val));
}
"Modified" => {
let hex = val.trim();
if hex.len() >= 16 {
let bytes: Vec<u8> = (0..16)
.step_by(2)
.filter_map(|i| u8::from_str_radix(&hex[i..i + 2], 16).ok())
.collect();
if bytes.len() >= 8 {
let lo = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let hi = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
let tz_offset = get_url_tz_offset();
if let Some(dt) = filetime_to_datetime(lo, hi, tz_offset / 3600) {
let mut t = mk_str("Modified", &dt);
t.group.family1 = "LNK".into();
tags.push(t);
}
}
}
}
"ShowCommand" => {
let pval = match val.trim() {
"1" => "Normal",
"2" => "Minimized",
"3" => "Maximized",
v => v,
};
let mut t = mk_str("ShowCommand", val);
t.print_value = pval.to_string();
tags.push(t);
}
_ => {}
}
}
}
Ok(tags)
}