1use std::io::{self, Read, Seek, SeekFrom};
20
21pub mod findings;
22
23#[cfg(feature = "vfs")]
25pub mod vfs;
26
27pub use forensicnomicon::report::Severity;
30
31const TAG_AVDP: u16 = 2;
34const TAG_PD: u16 = 5;
35const TAG_LVD: u16 = 6;
36const TAG_TERM: u16 = 8;
37const TAG_FSD: u16 = 256;
38const TAG_FID: u16 = 257;
39const TAG_FE: u16 = 260;
40const TAG_FE_ALT: u16 = 261;
42const TAG_EFE: u16 = 266;
43
44const FC_DIRECTORY: u8 = 0x02;
46const FC_PARENT: u8 = 0x08;
47
48const ALLOC_SHORT: u16 = 0;
50const ALLOC_LONG: u16 = 1;
51const ALLOC_INLINE: u16 = 3;
52
53const EXTENT_RECORDED: u32 = 0x0000_0000; const MAX_BLOCK_SIZE: usize = 4096;
60
61const BLOCK_SIZE_CANDIDATES: [u32; 4] = [2048, 512, 1024, 4096];
64
65#[derive(Debug, Clone)]
69pub struct UdfFileEntry {
70 pub name: String,
72 pub is_dir: bool,
74 pub size: u64,
76 pub fe_lba: u32,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
91pub enum UdfPartitionKind {
92 Physical,
94 Virtual,
96 Sparable,
98 Metadata,
100 Unknown,
102}
103
104#[derive(Debug)]
107pub struct UdfState {
108 pub partition_start: u32,
109 pub root_fe_lba: u32,
110 pub partition_kind: UdfPartitionKind,
111 pub partition_map_count: u32,
112 pub block_size: u32,
116 pub fsd_lba: u32,
120 pub vds_loc: u32,
124 pub vds_len_sectors: u32,
126}
127
128pub fn detect_udf<R: Read + Seek>(reader: &mut R) -> bool {
134 let mut buf = [0u8; 6];
135 for lba in 16u64..32 {
136 let pos = lba * 2048 + 1;
137 if reader.seek(SeekFrom::Start(pos)).is_err() {
138 break;
139 }
140 if reader.read_exact(&mut buf).is_err() {
141 break;
142 }
143 let id = &buf[..5];
144 if id == b"NSR02" || id == b"NSR03" {
145 return true;
146 }
147 if id == b"TEA01" {
148 break;
149 }
150 }
151 false
152}
153
154pub fn parse_udf_state<R: Read + Seek>(reader: &mut R) -> Option<UdfState> {
164 parse_udf_state_checked(reader).ok().flatten()
165}
166
167pub fn parse_udf_state_checked<R: Read + Seek>(
180 reader: &mut R,
181) -> Result<Option<UdfState>, io::Error> {
182 let Some(block_size) = detect_block_size(reader)? else {
183 return Ok(None);
184 };
185 let Some((vds_loc, vds_len)) = read_avdp_checked(reader, block_size)? else {
186 return Ok(None);
187 };
188 let Some(vds) = read_vds_checked(reader, block_size, vds_loc, vds_len)? else {
189 return Ok(None);
190 };
191 let Some(root_fe_lba) = read_fsd_checked(reader, block_size, vds.fsd_lba, vds.partition_start)?
192 else {
193 return Ok(None);
194 };
195 Ok(Some(UdfState {
196 partition_start: vds.partition_start,
197 root_fe_lba,
198 partition_kind: vds.partition_kind,
199 partition_map_count: vds.map_count,
200 block_size,
201 fsd_lba: vds.fsd_lba,
202 vds_loc,
203 vds_len_sectors: (vds_len as usize).div_ceil(block_size as usize) as u32,
204 }))
205}
206
207struct VdsInfo {
209 partition_start: u32,
210 fsd_lba: u32,
211 partition_kind: UdfPartitionKind,
212 map_count: u32,
213}
214
215struct PartitionMap {
217 kind: UdfPartitionKind,
218 partition_number: Option<u16>,
220}
221
222fn classify_type2(map: &[u8]) -> UdfPartitionKind {
225 let scan = |needle: &[u8]| map.windows(needle.len()).any(|w| w == needle);
226 if scan(b"*UDF Metadata Partition") {
227 UdfPartitionKind::Metadata
228 } else if scan(b"*UDF Virtual Partition") {
229 UdfPartitionKind::Virtual
230 } else if scan(b"*UDF Sparable Partition") {
231 UdfPartitionKind::Sparable
232 } else {
233 UdfPartitionKind::Unknown
234 }
235}
236
237fn parse_partition_maps(lvd: &[u8]) -> Vec<PartitionMap> {
243 let n_pm = u32::from_le_bytes(lvd[268..272].try_into().unwrap()) as usize;
244 let mt_l = u32::from_le_bytes(lvd[264..268].try_into().unwrap()) as usize;
245 let maps_end = (440 + mt_l).min(lvd.len());
246 let mut out = Vec::new();
247 let mut off = 440;
248 while out.len() < n_pm && off + 2 <= maps_end {
249 let map_type = lvd[off];
250 let map_len = lvd[off + 1] as usize;
251 if map_len < 2 || off + map_len > maps_end {
252 break;
253 }
254 let map = &lvd[off..off + map_len];
255 let pm = match map_type {
256 1 if map_len >= 6 => PartitionMap {
257 kind: UdfPartitionKind::Physical,
258 partition_number: Some(u16::from_le_bytes([map[4], map[5]])),
259 },
260 2 => PartitionMap {
261 kind: classify_type2(map),
262 partition_number: None,
263 },
264 _ => PartitionMap {
265 kind: UdfPartitionKind::Unknown,
266 partition_number: None,
267 },
268 };
269 out.push(pm);
270 off += map_len;
271 }
272 out
273}
274
275pub fn read_dir_at_lba<R: Read + Seek>(
278 reader: &mut R,
279 block_size: u32,
280 partition_start: u32,
281 dir_fe_lba: u32,
282) -> Option<Vec<UdfFileEntry>> {
283 let dir_data = read_fe_data(reader, block_size, partition_start, dir_fe_lba)?;
284 Some(parse_fids(reader, block_size, partition_start, &dir_data))
285}
286
287pub fn read_fe_data<R: Read + Seek>(
289 reader: &mut R,
290 block_size: u32,
291 partition_start: u32,
292 fe_lba: u32,
293) -> Option<Vec<u8>> {
294 let mut sector = [0u8; MAX_BLOCK_SIZE];
295 let sector = &mut sector[..block_size as usize];
296 seek_read(reader, fe_lba as u64 * block_size as u64, sector)?;
297
298 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
299 let is_efe = tag_ident == TAG_EFE;
300 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
301 return None;
302 }
303
304 let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
305 let alloc_type = icb_flags & 0x0007;
306 let info_len = u64::from_le_bytes(sector[56..64].try_into().unwrap());
307
308 let (ea_off, ad_off, header) = if is_efe {
316 (208usize, 212usize, 216usize)
317 } else {
318 (168usize, 172usize, 176usize)
319 };
320
321 if ad_off + 4 > sector.len() {
322 return None;
323 }
324 let ea_len = u32::from_le_bytes(sector[ea_off..ea_off + 4].try_into().unwrap()) as usize;
325 let ad_len = u32::from_le_bytes(sector[ad_off..ad_off + 4].try_into().unwrap()) as usize;
326
327 let ad_start = header + ea_len;
328 let ad_end = ad_start + ad_len;
329 if ad_end > sector.len() {
330 return None;
331 }
332 let ad_area = sector[ad_start..ad_end].to_vec();
333
334 match alloc_type {
335 ALLOC_INLINE => Some(ad_area[..info_len.min(ad_area.len() as u64) as usize].to_vec()),
336 ALLOC_SHORT => read_extents_short(reader, block_size, partition_start, &ad_area, info_len),
337 ALLOC_LONG => read_extents_long(reader, block_size, partition_start, &ad_area, info_len),
338 _ => None,
339 }
340}
341
342fn detect_block_size<R: Read + Seek>(reader: &mut R) -> Result<Option<u32>, io::Error> {
356 let mut tag = [0u8; 16];
357 let mut last_eof: Option<io::Error> = None;
358 let mut any_read_ok = false;
359 for bs in BLOCK_SIZE_CANDIDATES {
360 match seek_read_checked(reader, 256 * bs as u64, &mut tag) {
361 Ok(()) => {
362 any_read_ok = true;
363 let tag_ident = u16::from_le_bytes([tag[0], tag[1]]);
364 let tag_location = u32::from_le_bytes(tag[12..16].try_into().unwrap());
365 if tag_ident == TAG_AVDP && tag_location == 256 {
366 return Ok(Some(bs));
367 }
368 }
369 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => last_eof = Some(e),
370 Err(e) => return Err(e),
371 }
372 }
373 if !any_read_ok {
374 if let Some(e) = last_eof {
375 return Err(e);
376 }
377 }
378 Ok(None)
379}
380
381fn read_avdp_checked<R: Read + Seek>(
385 reader: &mut R,
386 block_size: u32,
387) -> Result<Option<(u32, u32)>, io::Error> {
388 let mut sector = [0u8; MAX_BLOCK_SIZE];
389 let sector = &mut sector[..block_size as usize];
390 seek_read_checked(reader, 256 * block_size as u64, sector)?;
391 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_AVDP {
392 return Ok(None);
393 }
394 let vds_len = u32::from_le_bytes(sector[16..20].try_into().unwrap());
395 let vds_loc = u32::from_le_bytes(sector[20..24].try_into().unwrap());
396 Ok(Some((vds_loc, vds_len)))
397}
398
399fn read_vds_checked<R: Read + Seek>(
404 reader: &mut R,
405 block_size: u32,
406 vds_loc: u32,
407 vds_len: u32,
408) -> Result<Option<VdsInfo>, io::Error> {
409 use std::collections::HashMap;
410 let sectors = (vds_len as usize).div_ceil(block_size as usize);
411
412 let mut pd_start: HashMap<u16, u32> = HashMap::new();
414 let mut fsd_lbn: Option<u32> = None;
415 let mut fsd_part_ref: u16 = 0;
416 let mut maps: Vec<PartitionMap> = Vec::new();
417
418 for i in 0..sectors {
419 let mut sector = [0u8; MAX_BLOCK_SIZE];
420 let sector = &mut sector[..block_size as usize];
421 seek_read_checked(
422 reader,
423 (vds_loc as u64 + i as u64) * block_size as u64,
424 sector,
425 )?;
426 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
427 match tag_ident {
428 TAG_PD => {
429 let part_num = u16::from_le_bytes([sector[22], sector[23]]);
430 let psl = u32::from_le_bytes(sector[188..192].try_into().unwrap());
431 pd_start.insert(part_num, psl);
432 }
433 TAG_LVD => {
434 fsd_lbn = Some(u32::from_le_bytes(sector[252..256].try_into().unwrap()));
437 fsd_part_ref = u16::from_le_bytes([sector[256], sector[257]]);
438 maps = parse_partition_maps(sector);
439 }
440 TAG_TERM | 0 => break,
441 _ => {}
442 }
443 }
444
445 let Some(fsd) = fsd_lbn else {
447 return Ok(None);
448 };
449 let map_count = maps.len() as u32;
450
451 let referenced = maps.get(fsd_part_ref as usize);
453 let kind = referenced.map_or(UdfPartitionKind::Unknown, |m| m.kind);
454
455 let partition_start = referenced
460 .and_then(|m| m.partition_number)
461 .and_then(|pn| pd_start.get(&pn).copied())
462 .or_else(|| pd_start.values().min().copied());
463 let Some(partition_start) = partition_start else {
464 return Ok(None);
465 };
466
467 Ok(Some(VdsInfo {
468 partition_start,
469 fsd_lba: partition_start + fsd,
470 partition_kind: kind,
471 map_count,
472 }))
473}
474
475fn read_fsd_checked<R: Read + Seek>(
479 reader: &mut R,
480 block_size: u32,
481 fsd_lba: u32,
482 partition_start: u32,
483) -> Result<Option<u32>, io::Error> {
484 let mut sector = [0u8; MAX_BLOCK_SIZE];
485 let sector = &mut sector[..block_size as usize];
486 seek_read_checked(reader, fsd_lba as u64 * block_size as u64, sector)?;
487 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
488 return Ok(None);
489 }
490 let lbn = u32::from_le_bytes(sector[404..408].try_into().unwrap());
497 Ok(Some(partition_start + lbn))
498}
499
500fn detect_fid_tag_size(data: &[u8]) -> usize {
508 let mut off = 0;
509 while off + 28 <= data.len() {
510 let ti = u16::from_le_bytes([data[off], data[off + 1]]);
511 if ti == TAG_FID {
512 let lbn16 = if off + 26 <= data.len() {
513 u32::from_le_bytes(data[off + 22..off + 26].try_into().unwrap())
514 } else {
515 u32::MAX
516 };
517 let lbn18 = if off + 28 <= data.len() {
518 u32::from_le_bytes(data[off + 24..off + 28].try_into().unwrap())
519 } else {
520 u32::MAX
521 };
522 if lbn16 < 0x10000 {
523 return 16;
524 }
525 if lbn18 < 0x10000 {
526 return 18;
527 }
528 return 16; }
530 off += 4;
531 }
532 16
533}
534
535fn parse_fids<R: Read + Seek>(
537 reader: &mut R,
538 block_size: u32,
539 partition_start: u32,
540 data: &[u8],
541) -> Vec<UdfFileEntry> {
542 let tag_size = detect_fid_tag_size(data);
545 let min_fid = tag_size + 20; let mut entries = Vec::new();
548 let mut off = 0;
549
550 while off + min_fid <= data.len() {
551 let tag_ident = u16::from_le_bytes([data[off], data[off + 1]]);
552 if tag_ident != TAG_FID {
553 off += 4;
555 continue;
556 }
557
558 let crc_len = u16::from_le_bytes([data[off + 10], data[off + 11]]) as usize;
560 let fid_advance = ((16 + crc_len + 3) & !3).max(min_fid);
561 if off + fid_advance > data.len() {
562 break;
563 }
564
565 let file_chars = data[off + tag_size];
566 let file_id_len = data[off + tag_size + 1] as usize;
567 let icb_lbn = if off + tag_size + 10 <= data.len() {
569 u32::from_le_bytes(
570 data[off + tag_size + 6..off + tag_size + 10]
571 .try_into()
572 .unwrap(),
573 )
574 } else {
575 off += fid_advance.max(4);
576 continue;
577 };
578 let impl_use_len = if off + tag_size + 20 <= data.len() {
579 u16::from_le_bytes([data[off + tag_size + 18], data[off + tag_size + 19]]) as usize
580 } else {
581 off += fid_advance.max(4);
582 continue;
583 };
584
585 if file_chars & FC_PARENT == 0 {
586 let is_dir = file_chars & FC_DIRECTORY != 0;
587 let fe_lba = partition_start + icb_lbn;
588
589 let id_start = off + tag_size + 20 + impl_use_len;
590 let id_end = (id_start + file_id_len).min(data.len());
591 let name = if id_end > id_start {
592 decode_osta_cs0(&data[id_start..id_end])
593 } else {
594 String::new()
595 };
596
597 let size = read_fe_info_len(reader, block_size, fe_lba).unwrap_or(0);
599
600 entries.push(UdfFileEntry {
601 name,
602 is_dir,
603 size,
604 fe_lba,
605 });
606 }
607
608 off += fid_advance.max(4);
609 }
610 entries
611}
612
613fn read_fe_info_len<R: Read + Seek>(reader: &mut R, block_size: u32, fe_lba: u32) -> Option<u64> {
615 let mut sector = [0u8; MAX_BLOCK_SIZE];
616 let sector = &mut sector[..block_size as usize];
617 seek_read(reader, fe_lba as u64 * block_size as u64, sector)?;
618 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
619 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
620 return None;
621 }
622 Some(u64::from_le_bytes(sector[56..64].try_into().unwrap()))
623}
624
625#[cfg(feature = "vfs")]
633pub(crate) fn read_fe_file_type<R: Read + Seek>(
634 reader: &mut R,
635 block_size: u32,
636 fe_lba: u32,
637) -> Option<u8> {
638 let mut sector = [0u8; MAX_BLOCK_SIZE];
639 let sector = &mut sector[..block_size as usize];
640 seek_read(reader, fe_lba as u64 * block_size as u64, sector)?;
641 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
642 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
643 return None;
644 }
645 sector.get(27).copied()
646}
647
648#[cfg(feature = "vfs")]
650pub(crate) const FILE_TYPE_DIRECTORY: u8 = 4;
651
652fn read_extents_short<R: Read + Seek>(
654 reader: &mut R,
655 block_size: u32,
656 partition_start: u32,
657 ad_area: &[u8],
658 total_len: u64,
659) -> Option<Vec<u8>> {
660 let mut data = Vec::new();
661 let mut pos = 0;
662 while pos + 8 <= ad_area.len() && (data.len() as u64) < total_len {
663 let len_raw = u32::from_le_bytes(ad_area[pos..pos + 4].try_into().unwrap());
664 let ext_pos = u32::from_le_bytes(ad_area[pos + 4..pos + 8].try_into().unwrap());
665 let ext_type = len_raw >> 30;
666 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
667 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
668 let phys = (partition_start as u64 + ext_pos as u64) * block_size as u64;
669 read_extent(reader, block_size, phys, ext_len, total_len, &mut data)?;
670 }
671 pos += 8;
672 }
673 data.truncate(total_len as usize);
674 Some(data)
675}
676
677fn read_extents_long<R: Read + Seek>(
679 reader: &mut R,
680 block_size: u32,
681 partition_start: u32,
682 ad_area: &[u8],
683 total_len: u64,
684) -> Option<Vec<u8>> {
685 let mut data = Vec::new();
686 let mut pos = 0;
687 while pos + 16 <= ad_area.len() && (data.len() as u64) < total_len {
688 let len_raw = u32::from_le_bytes(ad_area[pos..pos + 4].try_into().unwrap());
689 let lbn = u32::from_le_bytes(ad_area[pos + 4..pos + 8].try_into().unwrap());
690 let ext_type = len_raw >> 30;
691 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
692 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
693 let phys = (partition_start as u64 + lbn as u64) * block_size as u64;
694 read_extent(reader, block_size, phys, ext_len, total_len, &mut data)?;
695 }
696 pos += 16;
697 }
698 data.truncate(total_len as usize);
699 Some(data)
700}
701
702fn read_extent<R: Read + Seek>(
704 reader: &mut R,
705 block_size: u32,
706 byte_pos: u64,
707 ext_len: usize,
708 total_len: u64,
709 data: &mut Vec<u8>,
710) -> Option<()> {
711 let bs = block_size as usize;
712 let sectors = ext_len.div_ceil(bs);
713 for i in 0..sectors {
714 let mut sector = [0u8; MAX_BLOCK_SIZE];
715 let sector = &mut sector[..bs];
716 seek_read(reader, byte_pos + i as u64 * block_size as u64, sector)?;
717 let already = data.len() as u64;
718 let remaining = total_len.saturating_sub(already) as usize;
719 let sector_bytes = (ext_len - i * bs).min(bs);
720 let take = sector_bytes.min(remaining);
721 data.extend_from_slice(§or[..take]);
722 }
723 Some(())
724}
725
726fn decode_osta_cs0(bytes: &[u8]) -> String {
729 if bytes.is_empty() {
730 return String::new();
731 }
732 let comp_id = bytes[0];
733 let payload = &bytes[1..];
734 match comp_id {
735 8 => String::from_utf8_lossy(payload).into_owned(),
736 16 => {
737 let pairs: Vec<u16> = payload
738 .chunks_exact(2)
739 .map(|c| u16::from_be_bytes([c[0], c[1]]))
740 .collect();
741 String::from_utf16_lossy(&pairs)
742 }
743 _ => String::from_utf8_lossy(payload).into_owned(),
744 }
745}
746
747fn seek_read<R: Read + Seek>(reader: &mut R, byte_pos: u64, buf: &mut [u8]) -> Option<()> {
749 seek_read_checked(reader, byte_pos, buf).ok()
750}
751
752fn seek_read_checked<R: Read + Seek>(
755 reader: &mut R,
756 byte_pos: u64,
757 buf: &mut [u8],
758) -> Result<(), io::Error> {
759 reader.seek(SeekFrom::Start(byte_pos))?;
760 reader.read_exact(buf)?;
761 Ok(())
762}
763
764pub(crate) fn tag_checksum(tag: &[u8]) -> u8 {
769 let mut sum: u32 = 0;
770 for (i, &b) in tag.iter().take(16).enumerate() {
771 if i == 4 {
772 continue;
773 }
774 sum = sum.wrapping_add(u32::from(b));
775 }
776 (sum & 0xFF) as u8
777}
778
779pub(crate) fn ecma167_crc(body: &[u8]) -> u16 {
783 let mut crc: u16 = 0;
784 for &b in body {
785 crc ^= u16::from(b) << 8;
786 for _ in 0..8 {
787 if crc & 0x8000 != 0 {
788 crc = (crc << 1) ^ 0x1021;
789 } else {
790 crc <<= 1;
791 }
792 }
793 }
794 crc
795}
796
797pub(crate) fn descriptor_label(tag_ident: u16) -> Option<&'static str> {
801 Some(match tag_ident {
802 TAG_AVDP => "AVDP",
803 TAG_PD => "PartitionDescriptor",
804 TAG_LVD => "LogicalVolumeDescriptor",
805 TAG_TERM => "TerminatingDescriptor",
806 TAG_FSD => "FileSetDescriptor",
807 TAG_FID => "FileIdentifierDescriptor",
808 TAG_FE | TAG_FE_ALT => "FileEntry",
809 TAG_EFE => "ExtendedFileEntry",
810 1 => "PrimaryVolumeDescriptor",
811 3 => "VolumeDescriptorPointer",
812 4 => "ImplementationUseVolumeDescriptor",
813 7 => "UnallocatedSpaceDescriptor",
814 9 => "LogicalVolumeIntegrityDescriptor",
815 258 => "AllocationExtentDescriptor",
816 259 => "IndirectEntry",
817 262 => "SpaceBitmapDescriptor",
818 263 => "PartitionIntegrityEntry",
819 264 => "ExtendedAttributeHeaderDescriptor",
820 265 => "UnallocatedSpaceEntry",
821 _ => return None,
822 })
823}
824
825pub(crate) fn decode_timestamp(b: &[u8]) -> Option<String> {
829 if b.len() < 12 {
830 return None;
831 }
832 let year = i16::from_le_bytes([b[2], b[3]]);
833 if !(1970..=2200).contains(&year) {
834 return None;
835 }
836 let (month, day, hour, minute, second) = (b[4], b[5], b[6], b[7], b[8]);
837 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
838 return None;
839 }
840 Some(format!(
841 "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"
842 ))
843}
844
845pub(crate) fn fsd_recording_time<R: Read + Seek>(
849 reader: &mut R,
850 block_size: u32,
851 fsd_lba: u32,
852) -> Result<Option<String>, io::Error> {
853 let mut buf = [0u8; MAX_BLOCK_SIZE];
854 let sector = &mut buf[..block_size as usize];
855 seek_read_checked(reader, u64::from(fsd_lba) * u64::from(block_size), sector)?;
856 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
857 return Ok(None);
858 }
859 Ok(decode_timestamp(§or[16..28]))
860}
861
862pub(crate) fn fe_modification_time(sector: &[u8], is_efe: bool) -> Option<String> {
866 let off = if is_efe { 92 } else { 84 };
867 decode_timestamp(sector.get(off..off + 12)?)
868}
869
870pub(crate) fn fe_slack_nonzero<R: Read + Seek>(
879 reader: &mut R,
880 block_size: u32,
881 partition_start: u32,
882 fe_lba: u32,
883) -> Option<(u32, u32)> {
884 let info_len = read_fe_info_len(reader, block_size, fe_lba)?;
885 let bs = u64::from(block_size);
886 let slack = (bs - info_len % bs) % bs;
887 if info_len == 0 || slack == 0 {
888 return None;
889 }
890 let last_block_pos = fe_last_block_pos(reader, block_size, partition_start, fe_lba)?;
895 let mut buf = [0u8; MAX_BLOCK_SIZE];
896 let block = &mut buf[..block_size as usize];
897 seek_read_checked(reader, last_block_pos, block).ok()?;
898
899 let slack_start = (info_len % bs) as usize;
900 let nonzero = block[slack_start..].iter().filter(|&&b| b != 0).count() as u32;
901 Some((nonzero, slack as u32))
902}
903
904fn fe_last_block_pos<R: Read + Seek>(
908 reader: &mut R,
909 block_size: u32,
910 partition_start: u32,
911 fe_lba: u32,
912) -> Option<u64> {
913 let mut buf = [0u8; MAX_BLOCK_SIZE];
914 let sector = &mut buf[..block_size as usize];
915 seek_read_checked(reader, u64::from(fe_lba) * u64::from(block_size), sector).ok()?;
916
917 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
918 let is_efe = tag_ident == TAG_EFE;
919 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
920 return None;
921 }
922
923 let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
924 let alloc_type = icb_flags & 0x0007;
925 let (ea_off, ad_off, header) = if is_efe {
928 (208usize, 212usize, 216usize)
929 } else {
930 (168usize, 172usize, 176usize)
931 };
932 if ad_off + 4 > sector.len() {
933 return None; }
935 let ea_len = u32::from_le_bytes(sector[ea_off..ea_off + 4].try_into().ok()?) as usize;
936 let ad_len = u32::from_le_bytes(sector[ad_off..ad_off + 4].try_into().ok()?) as usize;
937 let ad_start = header + ea_len;
938 let ad_end = ad_start.checked_add(ad_len)?;
939 if ad_end > sector.len() {
940 return None;
941 }
942 let ad_area = §or[ad_start..ad_end];
943
944 let stride = match alloc_type {
948 ALLOC_SHORT => 8,
949 ALLOC_LONG => 16,
950 _ => return None,
951 };
952 let mut last: Option<u64> = None;
953 let mut pos = 0;
954 while pos + stride <= ad_area.len() {
955 let len_raw = read_le_u32(ad_area, pos);
956 let ext_type = len_raw >> 30;
957 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
958 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
959 let lbn = read_le_u32(ad_area, pos + 4);
960 let blocks_in_ext = ext_len.div_ceil(block_size as usize) as u64;
961 let last_lbn = u64::from(partition_start) + u64::from(lbn) + (blocks_in_ext - 1);
962 last = Some(last_lbn * u64::from(block_size));
963 }
964 pos += stride;
965 }
966 last
967}
968
969fn read_le_u32(data: &[u8], off: usize) -> u32 {
972 let mut b = [0u8; 4];
973 if let Some(s) = data.get(off..off + 4) {
974 b.copy_from_slice(s);
975 }
976 u32::from_le_bytes(b)
977}
978
979#[cfg(test)]
980mod real_media_tests {
981 use super::{parse_udf_state, UdfPartitionKind};
988 use std::fs::File;
989
990 fn state(name: &str) -> Option<super::UdfState> {
991 let path = format!("{}/tests/data/{}", env!("CARGO_MANIFEST_DIR"), name);
992 let mut f = File::open(&path).ok()?;
993 parse_udf_state(&mut f)
994 }
995
996 #[test]
997 fn vat_image_classified_virtual() {
998 let Some(st) = state("udf_vat.img") else {
999 eprintln!("skip: udf_vat.img");
1000 return;
1001 };
1002 assert_eq!(
1003 st.partition_kind,
1004 UdfPartitionKind::Virtual,
1005 "mkudffs cdr/1.50 image must classify as Virtual (VAT)"
1006 );
1007 }
1008
1009 #[test]
1010 fn sparable_image_classified_sparable() {
1011 let Some(st) = state("udf_spar.img") else {
1012 eprintln!("skip: udf_spar.img");
1013 return;
1014 };
1015 assert_eq!(
1016 st.partition_kind,
1017 UdfPartitionKind::Sparable,
1018 "mkudffs dvdrw/2.01 image must classify as Sparable"
1019 );
1020 }
1021
1022 #[test]
1033 fn vat_image_matches_udfinfo_oracle() {
1034 let Some(st) = state("udf_vat.img") else {
1035 eprintln!("skip: udf_vat.img");
1036 return;
1037 };
1038 assert_eq!(st.partition_kind, UdfPartitionKind::Virtual);
1039 assert_eq!(
1041 st.partition_start, 257,
1042 "partition start must match udfinfo PSPACE start=257"
1043 );
1044 assert_eq!(
1046 st.partition_map_count, 2,
1047 "cdr/1.50 carries a physical map plus the VAT Type-2 map"
1048 );
1049 }
1050
1051 #[test]
1057 fn sparable_image_matches_udfinfo_oracle() {
1058 let Some(st) = state("udf_spar.img") else {
1059 eprintln!("skip: udf_spar.img");
1060 return;
1061 };
1062 assert_eq!(st.partition_kind, UdfPartitionKind::Sparable);
1063 assert_eq!(
1065 st.partition_start, 1296,
1066 "partition start must match udfinfo PSPACE start=1296"
1067 );
1068 assert_eq!(
1069 st.partition_map_count, 1,
1070 "dvdrw/2.01 carries a single Sparable Type-2 map"
1071 );
1072 }
1073
1074 #[test]
1080 fn plain_512_block_image_parses_via_detected_block_size() {
1081 let path = format!("{}/tests/data/udf_plain.img", env!("CARGO_MANIFEST_DIR"));
1082 let mut f = File::open(&path).expect("udf_plain.img fixture must be present");
1083 let st = super::parse_udf_state(&mut f)
1084 .expect("512-byte-block UDF must parse once the block size is detected from the AVDP");
1085 assert_eq!(st.block_size, 512, "udfinfo reports blocksize=512");
1086 assert_eq!(
1087 st.partition_kind,
1088 UdfPartitionKind::Physical,
1089 "mkudffs hd image is a Type-1 physical partition"
1090 );
1091 assert_eq!(
1092 st.partition_start, 257,
1093 "partition start must match udfinfo PSPACE start=257"
1094 );
1095 assert_eq!(
1096 st.partition_map_count, 1,
1097 "hd/2.01 carries a single physical map"
1098 );
1099 }
1100}
1101
1102#[cfg(test)]
1103mod checked_bootstrap_tests {
1104 use super::parse_udf_state_checked;
1108 use std::io::{self, Cursor, Read, Seek, SeekFrom};
1109
1110 struct FaultyReader;
1113
1114 impl Read for FaultyReader {
1115 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1116 Err(io::Error::other("device read fault"))
1117 }
1118 }
1119 impl Seek for FaultyReader {
1120 fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
1121 Ok(0)
1122 }
1123 }
1124
1125 #[test]
1126 fn io_error_at_anchor_surfaces_as_err() {
1127 let mut r = FaultyReader;
1128 let res = parse_udf_state_checked(&mut r);
1129 assert!(
1130 res.is_err(),
1131 "a device read fault reading the anchor must surface as Err, not Ok(None)"
1132 );
1133 }
1134
1135 #[test]
1136 fn truncated_before_anchor_surfaces_as_err() {
1137 let buf = vec![0u8; 4096];
1141 let mut r = Cursor::new(buf);
1142 let res = parse_udf_state_checked(&mut r);
1143 assert!(
1144 res.is_err(),
1145 "truncation before the AVDP anchor must surface as Err (UnexpectedEof)"
1146 );
1147 let err = res.err().unwrap();
1148 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1149 }
1150
1151 #[test]
1152 fn full_size_but_wrong_anchor_is_ok_none() {
1153 let buf = vec![0u8; 257 * 2048];
1157 let mut r = Cursor::new(buf);
1158 let res = parse_udf_state_checked(&mut r);
1159 assert!(
1160 matches!(res, Ok(None)),
1161 "a readable image with a non-AVDP anchor must be Ok(None), got {res:?}"
1162 );
1163 }
1164}
1165
1166#[cfg(test)]
1167mod findings_support_tests {
1168 use super::*;
1174 use std::io::Cursor;
1175
1176 #[test]
1177 fn crc_ccitt_matches_known_vectors() {
1178 assert_eq!(ecma167_crc(b"123456789"), 0x31C3);
1181 assert_eq!(ecma167_crc(&[]), 0x0000);
1182 }
1183
1184 #[test]
1185 fn tag_checksum_skips_byte_4() {
1186 let mut tag = [0u8; 16];
1187 tag[0] = 2; tag[4] = 0xFF; tag[6] = 3; assert_eq!(tag_checksum(&tag), 5);
1191 }
1192
1193 #[test]
1194 fn descriptor_label_known_and_unknown() {
1195 for (id, name) in [
1198 (TAG_FSD, "FileSetDescriptor"),
1199 (TAG_EFE, "ExtendedFileEntry"),
1200 (TAG_FID, "FileIdentifierDescriptor"),
1201 (TAG_FE, "FileEntry"),
1202 (TAG_FE_ALT, "FileEntry"),
1203 (1, "PrimaryVolumeDescriptor"),
1204 (3, "VolumeDescriptorPointer"),
1205 (4, "ImplementationUseVolumeDescriptor"),
1206 (7, "UnallocatedSpaceDescriptor"),
1207 (9, "LogicalVolumeIntegrityDescriptor"),
1208 (258, "AllocationExtentDescriptor"),
1209 (259, "IndirectEntry"),
1210 (262, "SpaceBitmapDescriptor"),
1211 (263, "PartitionIntegrityEntry"),
1212 (264, "ExtendedAttributeHeaderDescriptor"),
1213 (265, "UnallocatedSpaceEntry"),
1214 ] {
1215 assert_eq!(descriptor_label(id), Some(name), "tag {id}");
1216 }
1217 assert_eq!(descriptor_label(0xFFFF), None);
1218 }
1219
1220 #[test]
1221 fn fsd_recording_time_none_for_non_fsd() {
1222 let img = vec![0u8; 512];
1223 let mut r = Cursor::new(img);
1224 assert_eq!(fsd_recording_time(&mut r, 512, 0).unwrap(), None);
1225 }
1226
1227 #[test]
1228 fn last_block_pos_none_for_non_file_entry() {
1229 let img = vec![0u8; 512];
1230 let mut r = Cursor::new(img);
1231 assert_eq!(fe_last_block_pos(&mut r, 512, 0, 0), None);
1232 }
1233
1234 #[test]
1235 fn slack_via_long_allocation_descriptor() {
1236 let bs = 512usize;
1238 let mut img = vec![0u8; bs * 8];
1239 let fe = 4 * bs;
1240 img[fe..fe + 2].copy_from_slice(&TAG_FE.to_le_bytes());
1241 img[fe + 34..fe + 36].copy_from_slice(&1u16.to_le_bytes()); img[fe + 56..fe + 64].copy_from_slice(&100u64.to_le_bytes());
1243 img[fe + 168..fe + 172].copy_from_slice(&0u32.to_le_bytes()); img[fe + 172..fe + 176].copy_from_slice(&16u32.to_le_bytes()); img[fe + 176..fe + 180].copy_from_slice(&100u32.to_le_bytes()); img[fe + 180..fe + 184].copy_from_slice(&5u32.to_le_bytes()); let data = 5 * bs + 100;
1248 img[data] = 0x7F;
1249 let mut r = Cursor::new(img);
1250 let (nonzero, slack) = fe_slack_nonzero(&mut r, 512, 0, 4).expect("slack present");
1251 assert_eq!(slack, 412);
1252 assert_eq!(nonzero, 1);
1253 }
1254
1255 #[test]
1256 fn timestamp_decodes_and_rejects_implausible() {
1257 let mut t = [0u8; 12];
1258 t[2..4].copy_from_slice(&2026i16.to_le_bytes());
1259 t[4] = 6; t[5] = 21; t[6] = 8; t[7] = 46; t[8] = 57; assert_eq!(decode_timestamp(&t).as_deref(), Some("2026-06-21 08:46:57"));
1265
1266 let mut bad = t;
1268 bad[2..4].copy_from_slice(&0i16.to_le_bytes());
1269 assert_eq!(decode_timestamp(&bad), None);
1270
1271 let mut badmon = t;
1273 badmon[4] = 0;
1274 assert_eq!(decode_timestamp(&badmon), None);
1275
1276 assert_eq!(decode_timestamp(&[0u8; 4]), None);
1278 }
1279
1280 #[test]
1281 fn fe_modification_time_offset_differs_for_efe() {
1282 let mut fe = vec![0u8; 512];
1284 let stamp = |buf: &mut [u8], off: usize, year: i16| {
1285 buf[off + 2..off + 4].copy_from_slice(&year.to_le_bytes());
1286 buf[off + 4] = 1; buf[off + 5] = 1; };
1289 stamp(&mut fe, 84, 2030);
1290 assert_eq!(
1291 fe_modification_time(&fe, false).as_deref(),
1292 Some("2030-01-01 00:00:00")
1293 );
1294 let mut efe = vec![0u8; 512];
1295 stamp(&mut efe, 92, 2031);
1296 assert_eq!(
1297 fe_modification_time(&efe, true).as_deref(),
1298 Some("2031-01-01 00:00:00")
1299 );
1300 }
1301
1302 fn image_with_slack(info_len: u64, slack_fill: &[u8]) -> (Vec<u8>, u32, u32) {
1307 let bs = 512usize;
1308 let part_start = 0u32;
1309 let fe_lba = 4u32;
1310 let data_lbn = 5u32; let mut img = vec![0u8; bs * 8];
1312
1313 let fe = fe_lba as usize * bs;
1314 img[fe..fe + 2].copy_from_slice(&TAG_FE.to_le_bytes());
1316 img[fe + 34..fe + 36].copy_from_slice(&0u16.to_le_bytes());
1318 img[fe + 56..fe + 64].copy_from_slice(&info_len.to_le_bytes());
1320 img[fe + 168..fe + 172].copy_from_slice(&0u32.to_le_bytes()); img[fe + 172..fe + 176].copy_from_slice(&8u32.to_le_bytes()); let ad = fe + 176;
1325 img[ad..ad + 4].copy_from_slice(&(info_len as u32).to_le_bytes());
1326 img[ad + 4..ad + 8].copy_from_slice(&data_lbn.to_le_bytes());
1327
1328 let data = (part_start + data_lbn) as usize * bs;
1330 let slack_start = data + (info_len as usize % bs);
1331 for (i, &b) in slack_fill.iter().enumerate() {
1332 img[slack_start + i] = b;
1333 }
1334 (img, part_start, fe_lba)
1335 }
1336
1337 #[test]
1338 fn slack_counts_nonzero_tail_bytes() {
1339 let (img, ps, fe) = image_with_slack(100, &[0xAA, 0x00, 0xBB, 0xCC]);
1341 let mut r = Cursor::new(img);
1342 let (nonzero, slack) = fe_slack_nonzero(&mut r, 512, ps, fe).expect("slack present");
1343 assert_eq!(slack, 412);
1344 assert_eq!(nonzero, 3);
1345 }
1346
1347 #[test]
1348 fn slack_none_when_block_aligned() {
1349 let (img, ps, fe) = image_with_slack(512, &[0xFF]);
1351 let mut r = Cursor::new(img);
1352 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1353 }
1354
1355 #[test]
1356 fn slack_none_when_zero_length() {
1357 let (img, ps, fe) = image_with_slack(0, &[]);
1358 let mut r = Cursor::new(img);
1359 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1360 }
1361
1362 #[test]
1363 fn slack_none_for_inline_data() {
1364 let (mut img, ps, fe) = image_with_slack(100, &[0xAA]);
1366 let fe_off = fe as usize * 512;
1367 img[fe_off + 34..fe_off + 36].copy_from_slice(&3u16.to_le_bytes());
1368 let mut r = Cursor::new(img);
1369 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1370 }
1371}