1#![forbid(unsafe_code)]
23
24use forensicnomicon::shlink;
25
26mod jumplist;
27pub use jumplist::{
28 parse_automatic_destinations, parse_custom_destinations, DestListEntry, JumpList,
29 JumpListEntry, JumpListKind,
30};
31
32const FILETIME_UNIX_DELTA_100NS: i64 = 116_444_736_000_000_000;
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ShellLink {
39 pub header: ShellLinkHeader,
41 pub link_target_idlist: Option<LinkTargetIdList>,
46 pub link_info: Option<LinkInfo>,
48 pub string_data: StringData,
50 pub tracker: Option<TrackerDataBlock>,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct ShellLinkHeader {
57 pub link_flags: u32,
59 pub file_attributes: u32,
61 pub creation_time: i64,
63 pub access_time: i64,
65 pub write_time: i64,
67 pub file_size: u32,
69 pub icon_index: i32,
71 pub show_command: u32,
73 pub hotkey: u16,
75}
76
77impl ShellLinkHeader {
78 #[must_use]
80 pub fn has_flag(&self, flag: u32) -> bool {
81 self.link_flags & flag != 0
82 }
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct LinkTargetIdList {
90 pub raw: Vec<u8>,
92 pub items: Vec<shellitem::ShellItem>,
95 pub path: Option<String>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq)]
103pub struct LinkInfo {
104 pub volume_id: Option<VolumeId>,
106 pub local_base_path: Option<String>,
108 pub common_network_relative_link: Option<CommonNetworkRelativeLink>,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct VolumeId {
115 pub drive_type: u32,
117 pub drive_serial_number: u32,
120 pub volume_label: Option<String>,
122}
123
124pub mod drive_type {
126 pub const UNKNOWN: u32 = 0;
128 pub const NO_ROOT_DIR: u32 = 1;
130 pub const REMOVABLE: u32 = 2;
132 pub const FIXED: u32 = 3;
134 pub const REMOTE: u32 = 4;
136 pub const CDROM: u32 = 5;
138 pub const RAMDISK: u32 = 6;
140}
141
142#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct CommonNetworkRelativeLink {
145 pub net_name: Option<String>,
147 pub device_name: Option<String>,
149}
150
151#[derive(Debug, Clone, Default, PartialEq, Eq)]
156pub struct StringData {
157 pub name: Option<String>,
159 pub relative_path: Option<String>,
161 pub working_dir: Option<String>,
163 pub arguments: Option<String>,
165 pub icon_location: Option<String>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
171pub struct TrackerDataBlock {
172 pub machine_id: String,
174 pub droid: DroidGuids,
176 pub birth_droid: DroidGuids,
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct DroidGuids {
183 pub volume: String,
185 pub object: String,
187}
188
189fn le_u16(data: &[u8], off: usize) -> u16 {
192 let mut b = [0u8; 2];
193 if let Some(s) = data.get(off..off + 2) {
194 b.copy_from_slice(s);
195 }
196 u16::from_le_bytes(b)
197}
198
199fn le_u32(data: &[u8], off: usize) -> u32 {
200 let mut b = [0u8; 4];
201 if let Some(s) = data.get(off..off + 4) {
202 b.copy_from_slice(s);
203 }
204 u32::from_le_bytes(b)
205}
206
207fn le_i32(data: &[u8], off: usize) -> i32 {
208 le_u32(data, off) as i32
209}
210
211fn le_u64(data: &[u8], off: usize) -> u64 {
212 let mut b = [0u8; 8];
213 if let Some(s) = data.get(off..off + 8) {
214 b.copy_from_slice(s);
215 }
216 u64::from_le_bytes(b)
217}
218
219fn filetime_to_unix(ft: u64) -> i64 {
222 if ft == 0 {
223 return 0;
224 }
225 ((ft as i64) - FILETIME_UNIX_DELTA_100NS) / 10_000_000
226}
227
228fn guid_string(b: &[u8]) -> Option<String> {
233 let g = b.get(0..16)?;
234 Some(format!(
235 "{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
236 u32::from_le_bytes([g[0], g[1], g[2], g[3]]),
237 u16::from_le_bytes([g[4], g[5]]),
238 u16::from_le_bytes([g[6], g[7]]),
239 g[8],
240 g[9],
241 g[10],
242 g[11],
243 g[12],
244 g[13],
245 g[14],
246 g[15],
247 ))
248}
249
250fn ansi_z(data: &[u8], off: usize) -> Option<String> {
252 let slice = data.get(off..)?;
253 let end = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
254 Some(String::from_utf8_lossy(&slice[..end]).into_owned())
255}
256
257fn unicode_z(data: &[u8], off: usize) -> Option<String> {
259 let slice = data.get(off..)?;
260 let mut units = Vec::new();
261 let mut i = 0;
262 while i + 1 < slice.len() {
263 let u = u16::from_le_bytes([slice[i], slice[i + 1]]);
264 if u == 0 {
265 break;
266 }
267 units.push(u);
268 i += 2;
269 }
270 Some(String::from_utf16_lossy(&units))
271}
272
273#[must_use]
280pub fn parse_shell_link(data: &[u8]) -> Option<ShellLink> {
281 if le_u32(data, 0) != shlink::HEADER_SIZE {
283 return None;
284 }
285 let clsid = guid_string(data.get(4..20)?)?;
286 if clsid != shlink::LINK_CLSID {
287 return None;
288 }
289
290 let link_flags = le_u32(data, 20);
291 let file_attributes = le_u32(data, 24);
292 let creation_time = filetime_to_unix(le_u64(data, 28));
293 let access_time = filetime_to_unix(le_u64(data, 36));
294 let write_time = filetime_to_unix(le_u64(data, 44));
295 let file_size = le_u32(data, 52);
296 let icon_index = le_i32(data, 56);
297 let show_command = le_u32(data, 60);
298 let hotkey = le_u16(data, 64);
299
300 let header = ShellLinkHeader {
301 link_flags,
302 file_attributes,
303 creation_time,
304 access_time,
305 write_time,
306 file_size,
307 icon_index,
308 show_command,
309 hotkey,
310 };
311
312 let mut off = shlink::HEADER_SIZE as usize;
314
315 let link_target_idlist = if header.has_flag(shlink::LINK_FLAG_HAS_LINK_TARGET_ID_LIST) {
318 let id_list_size = le_u16(data, off) as usize;
319 let blob_start = off + 2;
320 let raw = data
321 .get(blob_start..blob_start + id_list_size)
322 .map(<[u8]>::to_vec)
323 .unwrap_or_default();
324 off = blob_start + id_list_size;
325 let items = shellitem::parse_idlist(&raw);
326 let path = if items.is_empty() {
327 None
328 } else {
329 Some(shellitem::reconstruct_path(&items))
330 };
331 Some(LinkTargetIdList { raw, items, path })
332 } else {
333 None
334 };
335
336 let link_info = if header.has_flag(shlink::LINK_FLAG_HAS_LINK_INFO) {
338 let info = parse_link_info(data, off);
339 let size = le_u32(data, off) as usize;
341 off += size.max(4);
342 info
343 } else {
344 None
345 };
346
347 let is_unicode = header.has_flag(shlink::LINK_FLAG_IS_UNICODE);
349 let mut string_data = StringData::default();
350 for (flag, slot) in [
351 (
352 shlink::LINK_FLAG_HAS_NAME,
353 &mut string_data.name as &mut Option<String>,
354 ),
355 (
356 shlink::LINK_FLAG_HAS_RELATIVE_PATH,
357 &mut string_data.relative_path,
358 ),
359 (
360 shlink::LINK_FLAG_HAS_WORKING_DIR,
361 &mut string_data.working_dir,
362 ),
363 (shlink::LINK_FLAG_HAS_ARGUMENTS, &mut string_data.arguments),
364 (
365 shlink::LINK_FLAG_HAS_ICON_LOCATION,
366 &mut string_data.icon_location,
367 ),
368 ] {
369 if header.has_flag(flag) {
370 let (value, next) = read_sized_string(data, off, is_unicode);
371 *slot = value;
372 off = next;
373 }
374 }
375
376 let tracker = parse_extra_data_tracker(data, off);
379
380 Some(ShellLink {
381 header,
382 link_target_idlist,
383 link_info,
384 string_data,
385 tracker,
386 })
387}
388
389fn parse_link_info(data: &[u8], base: usize) -> Option<LinkInfo> {
391 let size = le_u32(data, base) as usize;
392 if size < 0x1C {
393 return None;
394 }
395 let header_size = le_u32(data, base + 4) as usize;
396 let flags = le_u32(data, base + 8);
397 let volume_id_offset = le_u32(data, base + 12) as usize;
398 let local_base_path_offset = le_u32(data, base + 16) as usize;
399 let cnrl_offset = le_u32(data, base + 20) as usize;
400 let local_base_path_offset_unicode = if header_size >= 0x24 {
402 le_u32(data, base + 28) as usize
403 } else {
404 0
405 };
406
407 const VOLUME_ID_AND_LOCAL_BASE_PATH: u32 = 0x1;
408 const CNRL_AND_PATH_SUFFIX: u32 = 0x2;
409
410 let volume_id = if flags & VOLUME_ID_AND_LOCAL_BASE_PATH != 0 && volume_id_offset != 0 {
411 parse_volume_id(data, base + volume_id_offset)
412 } else {
413 None
414 };
415
416 let local_base_path = if flags & VOLUME_ID_AND_LOCAL_BASE_PATH != 0 {
417 if local_base_path_offset_unicode != 0 {
418 unicode_z(data, base + local_base_path_offset_unicode)
419 } else if local_base_path_offset != 0 {
420 ansi_z(data, base + local_base_path_offset)
421 } else {
422 None
423 }
424 } else {
425 None
426 };
427
428 let common_network_relative_link = if flags & CNRL_AND_PATH_SUFFIX != 0 && cnrl_offset != 0 {
429 parse_cnrl(data, base + cnrl_offset)
430 } else {
431 None
432 };
433
434 Some(LinkInfo {
435 volume_id,
436 local_base_path,
437 common_network_relative_link,
438 })
439}
440
441fn parse_volume_id(data: &[u8], base: usize) -> Option<VolumeId> {
443 let size = le_u32(data, base) as usize;
444 if size < 0x10 {
445 return None;
446 }
447 let drive_type = le_u32(data, base + 4);
448 let drive_serial_number = le_u32(data, base + 8);
449 let label_offset = le_u32(data, base + 12) as usize;
450
451 let volume_label = if label_offset == 0x14 {
453 let uni_off = le_u32(data, base + 16) as usize;
454 unicode_z(data, base + uni_off)
455 } else if label_offset != 0 {
456 ansi_z(data, base + label_offset)
457 } else {
458 None
459 }
460 .filter(|s| !s.is_empty());
461
462 Some(VolumeId {
463 drive_type,
464 drive_serial_number,
465 volume_label,
466 })
467}
468
469fn parse_cnrl(data: &[u8], base: usize) -> Option<CommonNetworkRelativeLink> {
471 let size = le_u32(data, base) as usize;
472 if size < 0x14 {
473 return None;
474 }
475 let flags = le_u32(data, base + 4);
476 let net_name_offset = le_u32(data, base + 8) as usize;
477 let device_name_offset = le_u32(data, base + 12) as usize;
478
479 const VALID_DEVICE: u32 = 0x1;
480
481 let net_name = if net_name_offset != 0 {
482 ansi_z(data, base + net_name_offset)
483 } else {
484 None
485 };
486 let device_name = if flags & VALID_DEVICE != 0 && device_name_offset != 0 {
487 ansi_z(data, base + device_name_offset)
488 } else {
489 None
490 };
491
492 Some(CommonNetworkRelativeLink {
493 net_name,
494 device_name,
495 })
496}
497
498fn read_sized_string(data: &[u8], off: usize, is_unicode: bool) -> (Option<String>, usize) {
501 let count = le_u16(data, off) as usize;
502 let body = off + 2;
503 if is_unicode {
504 let byte_len = count * 2;
505 let value = data
506 .get(body..body + byte_len)
507 .map(decode_utf16le)
508 .filter(|s| !s.is_empty());
509 (value, body + byte_len)
510 } else {
511 let value = data
512 .get(body..body + count)
513 .map(|s| String::from_utf8_lossy(s).into_owned())
514 .filter(|s| !s.is_empty());
515 (value, body + count)
516 }
517}
518
519fn decode_utf16le(bytes: &[u8]) -> String {
520 let units: Vec<u16> = bytes
521 .chunks_exact(2)
522 .map(|c| u16::from_le_bytes([c[0], c[1]]))
523 .collect();
524 String::from_utf16_lossy(&units)
525}
526
527fn parse_extra_data_tracker(data: &[u8], start: usize) -> Option<TrackerDataBlock> {
529 let mut off = start;
530 while off + 8 <= data.len() {
532 let block_size = le_u32(data, off) as usize;
533 if (block_size as u32) < shlink::EXTRA_DATA_TERMINAL_BLOCK_SIZE {
534 break;
535 }
536 let signature = le_u32(data, off + 4);
537 if signature == shlink::EXTRA_TRACKER_DATA_BLOCK {
538 return parse_tracker_block(data, off);
539 }
540 if block_size < 4 {
542 break; }
544 off += block_size;
545 }
546 None
547}
548
549fn parse_tracker_block(data: &[u8], base: usize) -> Option<TrackerDataBlock> {
551 let machine_id = ansi_z(data, base + 16)?;
555 let droid = DroidGuids {
556 volume: guid_string(data.get(base + 32..base + 48)?)?,
557 object: guid_string(data.get(base + 48..base + 64)?)?,
558 };
559 let birth_droid = DroidGuids {
560 volume: guid_string(data.get(base + 64..base + 80)?)?,
561 object: guid_string(data.get(base + 80..base + 96)?)?,
562 };
563 Some(TrackerDataBlock {
564 machine_id,
565 droid,
566 birth_droid,
567 })
568}
569
570#[cfg(test)]
571mod tests {
572 include!("tests.rs");
573}