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