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)]
87pub struct LinkTargetIdList {
88 pub raw: Vec<u8>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct LinkInfo {
95 pub volume_id: Option<VolumeId>,
97 pub local_base_path: Option<String>,
99 pub common_network_relative_link: Option<CommonNetworkRelativeLink>,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct VolumeId {
106 pub drive_type: u32,
108 pub drive_serial_number: u32,
111 pub volume_label: Option<String>,
113}
114
115pub mod drive_type {
117 pub const UNKNOWN: u32 = 0;
119 pub const NO_ROOT_DIR: u32 = 1;
121 pub const REMOVABLE: u32 = 2;
123 pub const FIXED: u32 = 3;
125 pub const REMOTE: u32 = 4;
127 pub const CDROM: u32 = 5;
129 pub const RAMDISK: u32 = 6;
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct CommonNetworkRelativeLink {
136 pub net_name: Option<String>,
138 pub device_name: Option<String>,
140}
141
142#[derive(Debug, Clone, Default, PartialEq, Eq)]
147pub struct StringData {
148 pub name: Option<String>,
150 pub relative_path: Option<String>,
152 pub working_dir: Option<String>,
154 pub arguments: Option<String>,
156 pub icon_location: Option<String>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
162pub struct TrackerDataBlock {
163 pub machine_id: String,
165 pub droid: DroidGuids,
167 pub birth_droid: DroidGuids,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
173pub struct DroidGuids {
174 pub volume: String,
176 pub object: String,
178}
179
180fn le_u16(data: &[u8], off: usize) -> u16 {
183 let mut b = [0u8; 2];
184 if let Some(s) = data.get(off..off + 2) {
185 b.copy_from_slice(s);
186 }
187 u16::from_le_bytes(b)
188}
189
190fn le_u32(data: &[u8], off: usize) -> u32 {
191 let mut b = [0u8; 4];
192 if let Some(s) = data.get(off..off + 4) {
193 b.copy_from_slice(s);
194 }
195 u32::from_le_bytes(b)
196}
197
198fn le_i32(data: &[u8], off: usize) -> i32 {
199 le_u32(data, off) as i32
200}
201
202fn le_u64(data: &[u8], off: usize) -> u64 {
203 let mut b = [0u8; 8];
204 if let Some(s) = data.get(off..off + 8) {
205 b.copy_from_slice(s);
206 }
207 u64::from_le_bytes(b)
208}
209
210fn filetime_to_unix(ft: u64) -> i64 {
213 if ft == 0 {
214 return 0;
215 }
216 ((ft as i64) - FILETIME_UNIX_DELTA_100NS) / 10_000_000
217}
218
219fn guid_string(b: &[u8]) -> Option<String> {
224 let g = b.get(0..16)?;
225 Some(format!(
226 "{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
227 u32::from_le_bytes([g[0], g[1], g[2], g[3]]),
228 u16::from_le_bytes([g[4], g[5]]),
229 u16::from_le_bytes([g[6], g[7]]),
230 g[8],
231 g[9],
232 g[10],
233 g[11],
234 g[12],
235 g[13],
236 g[14],
237 g[15],
238 ))
239}
240
241fn ansi_z(data: &[u8], off: usize) -> Option<String> {
243 let slice = data.get(off..)?;
244 let end = slice.iter().position(|&c| c == 0).unwrap_or(slice.len());
245 Some(String::from_utf8_lossy(&slice[..end]).into_owned())
246}
247
248fn unicode_z(data: &[u8], off: usize) -> Option<String> {
250 let slice = data.get(off..)?;
251 let mut units = Vec::new();
252 let mut i = 0;
253 while i + 1 < slice.len() {
254 let u = u16::from_le_bytes([slice[i], slice[i + 1]]);
255 if u == 0 {
256 break;
257 }
258 units.push(u);
259 i += 2;
260 }
261 Some(String::from_utf16_lossy(&units))
262}
263
264#[must_use]
271pub fn parse_shell_link(data: &[u8]) -> Option<ShellLink> {
272 if le_u32(data, 0) != shlink::HEADER_SIZE {
274 return None;
275 }
276 let clsid = guid_string(data.get(4..20)?)?;
277 if clsid != shlink::LINK_CLSID {
278 return None;
279 }
280
281 let link_flags = le_u32(data, 20);
282 let file_attributes = le_u32(data, 24);
283 let creation_time = filetime_to_unix(le_u64(data, 28));
284 let access_time = filetime_to_unix(le_u64(data, 36));
285 let write_time = filetime_to_unix(le_u64(data, 44));
286 let file_size = le_u32(data, 52);
287 let icon_index = le_i32(data, 56);
288 let show_command = le_u32(data, 60);
289 let hotkey = le_u16(data, 64);
290
291 let header = ShellLinkHeader {
292 link_flags,
293 file_attributes,
294 creation_time,
295 access_time,
296 write_time,
297 file_size,
298 icon_index,
299 show_command,
300 hotkey,
301 };
302
303 let mut off = shlink::HEADER_SIZE as usize;
305
306 let link_target_idlist = if header.has_flag(shlink::LINK_FLAG_HAS_LINK_TARGET_ID_LIST) {
308 let id_list_size = le_u16(data, off) as usize;
309 let blob_start = off + 2;
310 let raw = data
311 .get(blob_start..blob_start + id_list_size)
312 .map(<[u8]>::to_vec)
313 .unwrap_or_default();
314 off = blob_start + id_list_size;
315 Some(LinkTargetIdList { raw })
316 } else {
317 None
318 };
319
320 let link_info = if header.has_flag(shlink::LINK_FLAG_HAS_LINK_INFO) {
322 let info = parse_link_info(data, off);
323 let size = le_u32(data, off) as usize;
325 off += size.max(4);
326 info
327 } else {
328 None
329 };
330
331 let is_unicode = header.has_flag(shlink::LINK_FLAG_IS_UNICODE);
333 let mut string_data = StringData::default();
334 for (flag, slot) in [
335 (
336 shlink::LINK_FLAG_HAS_NAME,
337 &mut string_data.name as &mut Option<String>,
338 ),
339 (
340 shlink::LINK_FLAG_HAS_RELATIVE_PATH,
341 &mut string_data.relative_path,
342 ),
343 (
344 shlink::LINK_FLAG_HAS_WORKING_DIR,
345 &mut string_data.working_dir,
346 ),
347 (shlink::LINK_FLAG_HAS_ARGUMENTS, &mut string_data.arguments),
348 (
349 shlink::LINK_FLAG_HAS_ICON_LOCATION,
350 &mut string_data.icon_location,
351 ),
352 ] {
353 if header.has_flag(flag) {
354 let (value, next) = read_sized_string(data, off, is_unicode);
355 *slot = value;
356 off = next;
357 }
358 }
359
360 let tracker = parse_extra_data_tracker(data, off);
363
364 Some(ShellLink {
365 header,
366 link_target_idlist,
367 link_info,
368 string_data,
369 tracker,
370 })
371}
372
373fn parse_link_info(data: &[u8], base: usize) -> Option<LinkInfo> {
375 let size = le_u32(data, base) as usize;
376 if size < 0x1C {
377 return None;
378 }
379 let header_size = le_u32(data, base + 4) as usize;
380 let flags = le_u32(data, base + 8);
381 let volume_id_offset = le_u32(data, base + 12) as usize;
382 let local_base_path_offset = le_u32(data, base + 16) as usize;
383 let cnrl_offset = le_u32(data, base + 20) as usize;
384 let local_base_path_offset_unicode = if header_size >= 0x24 {
386 le_u32(data, base + 28) as usize
387 } else {
388 0
389 };
390
391 const VOLUME_ID_AND_LOCAL_BASE_PATH: u32 = 0x1;
392 const CNRL_AND_PATH_SUFFIX: u32 = 0x2;
393
394 let volume_id = if flags & VOLUME_ID_AND_LOCAL_BASE_PATH != 0 && volume_id_offset != 0 {
395 parse_volume_id(data, base + volume_id_offset)
396 } else {
397 None
398 };
399
400 let local_base_path = if flags & VOLUME_ID_AND_LOCAL_BASE_PATH != 0 {
401 if local_base_path_offset_unicode != 0 {
402 unicode_z(data, base + local_base_path_offset_unicode)
403 } else if local_base_path_offset != 0 {
404 ansi_z(data, base + local_base_path_offset)
405 } else {
406 None
407 }
408 } else {
409 None
410 };
411
412 let common_network_relative_link = if flags & CNRL_AND_PATH_SUFFIX != 0 && cnrl_offset != 0 {
413 parse_cnrl(data, base + cnrl_offset)
414 } else {
415 None
416 };
417
418 Some(LinkInfo {
419 volume_id,
420 local_base_path,
421 common_network_relative_link,
422 })
423}
424
425fn parse_volume_id(data: &[u8], base: usize) -> Option<VolumeId> {
427 let size = le_u32(data, base) as usize;
428 if size < 0x10 {
429 return None;
430 }
431 let drive_type = le_u32(data, base + 4);
432 let drive_serial_number = le_u32(data, base + 8);
433 let label_offset = le_u32(data, base + 12) as usize;
434
435 let volume_label = if label_offset == 0x14 {
437 let uni_off = le_u32(data, base + 16) as usize;
438 unicode_z(data, base + uni_off)
439 } else if label_offset != 0 {
440 ansi_z(data, base + label_offset)
441 } else {
442 None
443 }
444 .filter(|s| !s.is_empty());
445
446 Some(VolumeId {
447 drive_type,
448 drive_serial_number,
449 volume_label,
450 })
451}
452
453fn parse_cnrl(data: &[u8], base: usize) -> Option<CommonNetworkRelativeLink> {
455 let size = le_u32(data, base) as usize;
456 if size < 0x14 {
457 return None;
458 }
459 let flags = le_u32(data, base + 4);
460 let net_name_offset = le_u32(data, base + 8) as usize;
461 let device_name_offset = le_u32(data, base + 12) as usize;
462
463 const VALID_DEVICE: u32 = 0x1;
464
465 let net_name = if net_name_offset != 0 {
466 ansi_z(data, base + net_name_offset)
467 } else {
468 None
469 };
470 let device_name = if flags & VALID_DEVICE != 0 && device_name_offset != 0 {
471 ansi_z(data, base + device_name_offset)
472 } else {
473 None
474 };
475
476 Some(CommonNetworkRelativeLink {
477 net_name,
478 device_name,
479 })
480}
481
482fn read_sized_string(data: &[u8], off: usize, is_unicode: bool) -> (Option<String>, usize) {
485 let count = le_u16(data, off) as usize;
486 let body = off + 2;
487 if is_unicode {
488 let byte_len = count * 2;
489 let value = data
490 .get(body..body + byte_len)
491 .map(decode_utf16le)
492 .filter(|s| !s.is_empty());
493 (value, body + byte_len)
494 } else {
495 let value = data
496 .get(body..body + count)
497 .map(|s| String::from_utf8_lossy(s).into_owned())
498 .filter(|s| !s.is_empty());
499 (value, body + count)
500 }
501}
502
503fn decode_utf16le(bytes: &[u8]) -> String {
504 let units: Vec<u16> = bytes
505 .chunks_exact(2)
506 .map(|c| u16::from_le_bytes([c[0], c[1]]))
507 .collect();
508 String::from_utf16_lossy(&units)
509}
510
511fn parse_extra_data_tracker(data: &[u8], start: usize) -> Option<TrackerDataBlock> {
513 let mut off = start;
514 while off + 8 <= data.len() {
516 let block_size = le_u32(data, off) as usize;
517 if (block_size as u32) < shlink::EXTRA_DATA_TERMINAL_BLOCK_SIZE {
518 break;
519 }
520 let signature = le_u32(data, off + 4);
521 if signature == shlink::EXTRA_TRACKER_DATA_BLOCK {
522 return parse_tracker_block(data, off);
523 }
524 if block_size < 4 {
526 break; }
528 off += block_size;
529 }
530 None
531}
532
533fn parse_tracker_block(data: &[u8], base: usize) -> Option<TrackerDataBlock> {
535 let machine_id = ansi_z(data, base + 16)?;
539 let droid = DroidGuids {
540 volume: guid_string(data.get(base + 32..base + 48)?)?,
541 object: guid_string(data.get(base + 48..base + 64)?)?,
542 };
543 let birth_droid = DroidGuids {
544 volume: guid_string(data.get(base + 64..base + 80)?)?,
545 object: guid_string(data.get(base + 80..base + 96)?)?,
546 };
547 Some(TrackerDataBlock {
548 machine_id,
549 droid,
550 birth_droid,
551 })
552}
553
554#[cfg(test)]
555mod tests {
556 include!("tests.rs");
557}