1use std::io::{self, Read, Seek, SeekFrom};
20
21pub mod findings;
22
23pub use forensicnomicon::report::Severity;
26
27const TAG_AVDP: u16 = 2;
30const TAG_PD: u16 = 5;
31const TAG_LVD: u16 = 6;
32const TAG_TERM: u16 = 8;
33const TAG_FSD: u16 = 256;
34const TAG_FID: u16 = 257;
35const TAG_FE: u16 = 260;
36const TAG_FE_ALT: u16 = 261;
38const TAG_EFE: u16 = 266;
39
40const FC_DIRECTORY: u8 = 0x02;
42const FC_PARENT: u8 = 0x08;
43
44const ALLOC_SHORT: u16 = 0;
46const ALLOC_LONG: u16 = 1;
47const ALLOC_INLINE: u16 = 3;
48
49const EXTENT_RECORDED: u32 = 0x0000_0000; const MAX_BLOCK_SIZE: usize = 4096;
56
57const BLOCK_SIZE_CANDIDATES: [u32; 4] = [2048, 512, 1024, 4096];
60
61#[derive(Debug, Clone)]
65pub struct UdfFileEntry {
66 pub name: String,
68 pub is_dir: bool,
70 pub size: u64,
72 pub fe_lba: u32,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87pub enum UdfPartitionKind {
88 Physical,
90 Virtual,
92 Sparable,
94 Metadata,
96 Unknown,
98}
99
100#[derive(Debug)]
103pub struct UdfState {
104 pub partition_start: u32,
105 pub root_fe_lba: u32,
106 pub partition_kind: UdfPartitionKind,
107 pub partition_map_count: u32,
108 pub block_size: u32,
112 pub fsd_lba: u32,
116 pub vds_loc: u32,
120 pub vds_len_sectors: u32,
122}
123
124pub fn detect_udf<R: Read + Seek>(reader: &mut R) -> bool {
130 let mut buf = [0u8; 6];
131 for lba in 16u64..32 {
132 let pos = lba * 2048 + 1;
133 if reader.seek(SeekFrom::Start(pos)).is_err() {
134 break;
135 }
136 if reader.read_exact(&mut buf).is_err() {
137 break;
138 }
139 let id = &buf[..5];
140 if id == b"NSR02" || id == b"NSR03" {
141 return true;
142 }
143 if id == b"TEA01" {
144 break;
145 }
146 }
147 false
148}
149
150pub fn parse_udf_state<R: Read + Seek>(reader: &mut R) -> Option<UdfState> {
160 parse_udf_state_checked(reader).ok().flatten()
161}
162
163pub fn parse_udf_state_checked<R: Read + Seek>(
176 reader: &mut R,
177) -> Result<Option<UdfState>, io::Error> {
178 let Some(block_size) = detect_block_size(reader)? else {
179 return Ok(None);
180 };
181 let Some((vds_loc, vds_len)) = read_avdp_checked(reader, block_size)? else {
182 return Ok(None);
183 };
184 let Some(vds) = read_vds_checked(reader, block_size, vds_loc, vds_len)? else {
185 return Ok(None);
186 };
187 let Some(root_fe_lba) = read_fsd_checked(reader, block_size, vds.fsd_lba, vds.partition_start)?
188 else {
189 return Ok(None);
190 };
191 Ok(Some(UdfState {
192 partition_start: vds.partition_start,
193 root_fe_lba,
194 partition_kind: vds.partition_kind,
195 partition_map_count: vds.map_count,
196 block_size,
197 fsd_lba: vds.fsd_lba,
198 vds_loc,
199 vds_len_sectors: (vds_len as usize).div_ceil(block_size as usize) as u32,
200 }))
201}
202
203struct VdsInfo {
205 partition_start: u32,
206 fsd_lba: u32,
207 partition_kind: UdfPartitionKind,
208 map_count: u32,
209}
210
211struct PartitionMap {
213 kind: UdfPartitionKind,
214 partition_number: Option<u16>,
216}
217
218fn classify_type2(map: &[u8]) -> UdfPartitionKind {
221 let scan = |needle: &[u8]| map.windows(needle.len()).any(|w| w == needle);
222 if scan(b"*UDF Metadata Partition") {
223 UdfPartitionKind::Metadata
224 } else if scan(b"*UDF Virtual Partition") {
225 UdfPartitionKind::Virtual
226 } else if scan(b"*UDF Sparable Partition") {
227 UdfPartitionKind::Sparable
228 } else {
229 UdfPartitionKind::Unknown
230 }
231}
232
233fn parse_partition_maps(lvd: &[u8]) -> Vec<PartitionMap> {
239 let n_pm = u32::from_le_bytes(lvd[268..272].try_into().unwrap()) as usize;
240 let mt_l = u32::from_le_bytes(lvd[264..268].try_into().unwrap()) as usize;
241 let maps_end = (440 + mt_l).min(lvd.len());
242 let mut out = Vec::new();
243 let mut off = 440;
244 while out.len() < n_pm && off + 2 <= maps_end {
245 let map_type = lvd[off];
246 let map_len = lvd[off + 1] as usize;
247 if map_len < 2 || off + map_len > maps_end {
248 break;
249 }
250 let map = &lvd[off..off + map_len];
251 let pm = match map_type {
252 1 if map_len >= 6 => PartitionMap {
253 kind: UdfPartitionKind::Physical,
254 partition_number: Some(u16::from_le_bytes([map[4], map[5]])),
255 },
256 2 => PartitionMap {
257 kind: classify_type2(map),
258 partition_number: None,
259 },
260 _ => PartitionMap {
261 kind: UdfPartitionKind::Unknown,
262 partition_number: None,
263 },
264 };
265 out.push(pm);
266 off += map_len;
267 }
268 out
269}
270
271pub fn read_dir_at_lba<R: Read + Seek>(
274 reader: &mut R,
275 block_size: u32,
276 partition_start: u32,
277 dir_fe_lba: u32,
278) -> Option<Vec<UdfFileEntry>> {
279 let dir_data = read_fe_data(reader, block_size, partition_start, dir_fe_lba)?;
280 Some(parse_fids(reader, block_size, partition_start, &dir_data))
281}
282
283pub fn read_fe_data<R: Read + Seek>(
285 reader: &mut R,
286 block_size: u32,
287 partition_start: u32,
288 fe_lba: u32,
289) -> Option<Vec<u8>> {
290 let mut sector = [0u8; MAX_BLOCK_SIZE];
291 let sector = &mut sector[..block_size as usize];
292 seek_read(reader, fe_lba as u64 * block_size as u64, sector)?;
293
294 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
295 let is_efe = tag_ident == TAG_EFE;
296 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
297 return None;
298 }
299
300 let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
301 let alloc_type = icb_flags & 0x0007;
302 let info_len = u64::from_le_bytes(sector[56..64].try_into().unwrap());
303
304 let (ea_off, ad_off, header) = if is_efe {
306 (176usize, 180usize, 184usize)
307 } else {
308 (168usize, 172usize, 176usize)
309 };
310
311 if ad_off + 4 > sector.len() {
312 return None;
313 }
314 let ea_len = u32::from_le_bytes(sector[ea_off..ea_off + 4].try_into().unwrap()) as usize;
315 let ad_len = u32::from_le_bytes(sector[ad_off..ad_off + 4].try_into().unwrap()) as usize;
316
317 let ad_start = header + ea_len;
318 let ad_end = ad_start + ad_len;
319 if ad_end > sector.len() {
320 return None;
321 }
322 let ad_area = sector[ad_start..ad_end].to_vec();
323
324 match alloc_type {
325 ALLOC_INLINE => Some(ad_area[..info_len.min(ad_area.len() as u64) as usize].to_vec()),
326 ALLOC_SHORT => read_extents_short(reader, block_size, partition_start, &ad_area, info_len),
327 ALLOC_LONG => read_extents_long(reader, block_size, partition_start, &ad_area, info_len),
328 _ => None,
329 }
330}
331
332fn detect_block_size<R: Read + Seek>(reader: &mut R) -> Result<Option<u32>, io::Error> {
346 let mut tag = [0u8; 16];
347 let mut last_eof: Option<io::Error> = None;
348 let mut any_read_ok = false;
349 for bs in BLOCK_SIZE_CANDIDATES {
350 match seek_read_checked(reader, 256 * bs as u64, &mut tag) {
351 Ok(()) => {
352 any_read_ok = true;
353 let tag_ident = u16::from_le_bytes([tag[0], tag[1]]);
354 let tag_location = u32::from_le_bytes(tag[12..16].try_into().unwrap());
355 if tag_ident == TAG_AVDP && tag_location == 256 {
356 return Ok(Some(bs));
357 }
358 }
359 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => last_eof = Some(e),
360 Err(e) => return Err(e),
361 }
362 }
363 if !any_read_ok {
364 if let Some(e) = last_eof {
365 return Err(e);
366 }
367 }
368 Ok(None)
369}
370
371fn read_avdp_checked<R: Read + Seek>(
375 reader: &mut R,
376 block_size: u32,
377) -> Result<Option<(u32, u32)>, io::Error> {
378 let mut sector = [0u8; MAX_BLOCK_SIZE];
379 let sector = &mut sector[..block_size as usize];
380 seek_read_checked(reader, 256 * block_size as u64, sector)?;
381 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_AVDP {
382 return Ok(None);
383 }
384 let vds_len = u32::from_le_bytes(sector[16..20].try_into().unwrap());
385 let vds_loc = u32::from_le_bytes(sector[20..24].try_into().unwrap());
386 Ok(Some((vds_loc, vds_len)))
387}
388
389fn read_vds_checked<R: Read + Seek>(
394 reader: &mut R,
395 block_size: u32,
396 vds_loc: u32,
397 vds_len: u32,
398) -> Result<Option<VdsInfo>, io::Error> {
399 use std::collections::HashMap;
400 let sectors = (vds_len as usize).div_ceil(block_size as usize);
401
402 let mut pd_start: HashMap<u16, u32> = HashMap::new();
404 let mut fsd_lbn: Option<u32> = None;
405 let mut fsd_part_ref: u16 = 0;
406 let mut maps: Vec<PartitionMap> = Vec::new();
407
408 for i in 0..sectors {
409 let mut sector = [0u8; MAX_BLOCK_SIZE];
410 let sector = &mut sector[..block_size as usize];
411 seek_read_checked(
412 reader,
413 (vds_loc as u64 + i as u64) * block_size as u64,
414 sector,
415 )?;
416 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
417 match tag_ident {
418 TAG_PD => {
419 let part_num = u16::from_le_bytes([sector[22], sector[23]]);
420 let psl = u32::from_le_bytes(sector[188..192].try_into().unwrap());
421 pd_start.insert(part_num, psl);
422 }
423 TAG_LVD => {
424 fsd_lbn = Some(u32::from_le_bytes(sector[252..256].try_into().unwrap()));
427 fsd_part_ref = u16::from_le_bytes([sector[256], sector[257]]);
428 maps = parse_partition_maps(sector);
429 }
430 TAG_TERM | 0 => break,
431 _ => {}
432 }
433 }
434
435 let Some(fsd) = fsd_lbn else {
437 return Ok(None);
438 };
439 let map_count = maps.len() as u32;
440
441 let referenced = maps.get(fsd_part_ref as usize);
443 let kind = referenced.map_or(UdfPartitionKind::Unknown, |m| m.kind);
444
445 let partition_start = referenced
450 .and_then(|m| m.partition_number)
451 .and_then(|pn| pd_start.get(&pn).copied())
452 .or_else(|| pd_start.values().min().copied());
453 let Some(partition_start) = partition_start else {
454 return Ok(None);
455 };
456
457 Ok(Some(VdsInfo {
458 partition_start,
459 fsd_lba: partition_start + fsd,
460 partition_kind: kind,
461 map_count,
462 }))
463}
464
465fn read_fsd_checked<R: Read + Seek>(
469 reader: &mut R,
470 block_size: u32,
471 fsd_lba: u32,
472 partition_start: u32,
473) -> Result<Option<u32>, io::Error> {
474 let mut sector = [0u8; MAX_BLOCK_SIZE];
475 let sector = &mut sector[..block_size as usize];
476 seek_read_checked(reader, fsd_lba as u64 * block_size as u64, sector)?;
477 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
478 return Ok(None);
479 }
480 let lbn = u32::from_le_bytes(sector[404..408].try_into().unwrap());
487 Ok(Some(partition_start + lbn))
488}
489
490fn detect_fid_tag_size(data: &[u8]) -> usize {
498 let mut off = 0;
499 while off + 28 <= data.len() {
500 let ti = u16::from_le_bytes([data[off], data[off + 1]]);
501 if ti == TAG_FID {
502 let lbn16 = if off + 26 <= data.len() {
503 u32::from_le_bytes(data[off + 22..off + 26].try_into().unwrap())
504 } else {
505 u32::MAX
506 };
507 let lbn18 = if off + 28 <= data.len() {
508 u32::from_le_bytes(data[off + 24..off + 28].try_into().unwrap())
509 } else {
510 u32::MAX
511 };
512 if lbn16 < 0x10000 {
513 return 16;
514 }
515 if lbn18 < 0x10000 {
516 return 18;
517 }
518 return 16; }
520 off += 4;
521 }
522 16
523}
524
525fn parse_fids<R: Read + Seek>(
527 reader: &mut R,
528 block_size: u32,
529 partition_start: u32,
530 data: &[u8],
531) -> Vec<UdfFileEntry> {
532 let tag_size = detect_fid_tag_size(data);
535 let min_fid = tag_size + 20; let mut entries = Vec::new();
538 let mut off = 0;
539
540 while off + min_fid <= data.len() {
541 let tag_ident = u16::from_le_bytes([data[off], data[off + 1]]);
542 if tag_ident != TAG_FID {
543 off += 4;
545 continue;
546 }
547
548 let crc_len = u16::from_le_bytes([data[off + 10], data[off + 11]]) as usize;
550 let fid_advance = ((16 + crc_len + 3) & !3).max(min_fid);
551 if off + fid_advance > data.len() {
552 break;
553 }
554
555 let file_chars = data[off + tag_size];
556 let file_id_len = data[off + tag_size + 1] as usize;
557 let icb_lbn = if off + tag_size + 10 <= data.len() {
559 u32::from_le_bytes(
560 data[off + tag_size + 6..off + tag_size + 10]
561 .try_into()
562 .unwrap(),
563 )
564 } else {
565 off += fid_advance.max(4);
566 continue;
567 };
568 let impl_use_len = if off + tag_size + 20 <= data.len() {
569 u16::from_le_bytes([data[off + tag_size + 18], data[off + tag_size + 19]]) as usize
570 } else {
571 off += fid_advance.max(4);
572 continue;
573 };
574
575 if file_chars & FC_PARENT == 0 {
576 let is_dir = file_chars & FC_DIRECTORY != 0;
577 let fe_lba = partition_start + icb_lbn;
578
579 let id_start = off + tag_size + 20 + impl_use_len;
580 let id_end = (id_start + file_id_len).min(data.len());
581 let name = if id_end > id_start {
582 decode_osta_cs0(&data[id_start..id_end])
583 } else {
584 String::new()
585 };
586
587 let size = read_fe_info_len(reader, block_size, fe_lba).unwrap_or(0);
589
590 entries.push(UdfFileEntry {
591 name,
592 is_dir,
593 size,
594 fe_lba,
595 });
596 }
597
598 off += fid_advance.max(4);
599 }
600 entries
601}
602
603fn read_fe_info_len<R: Read + Seek>(reader: &mut R, block_size: u32, fe_lba: u32) -> Option<u64> {
605 let mut sector = [0u8; MAX_BLOCK_SIZE];
606 let sector = &mut sector[..block_size as usize];
607 seek_read(reader, fe_lba as u64 * block_size as u64, sector)?;
608 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
609 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
610 return None;
611 }
612 Some(u64::from_le_bytes(sector[56..64].try_into().unwrap()))
613}
614
615fn read_extents_short<R: Read + Seek>(
617 reader: &mut R,
618 block_size: u32,
619 partition_start: u32,
620 ad_area: &[u8],
621 total_len: u64,
622) -> Option<Vec<u8>> {
623 let mut data = Vec::new();
624 let mut pos = 0;
625 while pos + 8 <= ad_area.len() && (data.len() as u64) < total_len {
626 let len_raw = u32::from_le_bytes(ad_area[pos..pos + 4].try_into().unwrap());
627 let ext_pos = u32::from_le_bytes(ad_area[pos + 4..pos + 8].try_into().unwrap());
628 let ext_type = len_raw >> 30;
629 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
630 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
631 let phys = (partition_start as u64 + ext_pos as u64) * block_size as u64;
632 read_extent(reader, block_size, phys, ext_len, total_len, &mut data)?;
633 }
634 pos += 8;
635 }
636 data.truncate(total_len as usize);
637 Some(data)
638}
639
640fn read_extents_long<R: Read + Seek>(
642 reader: &mut R,
643 block_size: u32,
644 partition_start: u32,
645 ad_area: &[u8],
646 total_len: u64,
647) -> Option<Vec<u8>> {
648 let mut data = Vec::new();
649 let mut pos = 0;
650 while pos + 16 <= ad_area.len() && (data.len() as u64) < total_len {
651 let len_raw = u32::from_le_bytes(ad_area[pos..pos + 4].try_into().unwrap());
652 let lbn = u32::from_le_bytes(ad_area[pos + 4..pos + 8].try_into().unwrap());
653 let ext_type = len_raw >> 30;
654 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
655 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
656 let phys = (partition_start as u64 + lbn as u64) * block_size as u64;
657 read_extent(reader, block_size, phys, ext_len, total_len, &mut data)?;
658 }
659 pos += 16;
660 }
661 data.truncate(total_len as usize);
662 Some(data)
663}
664
665fn read_extent<R: Read + Seek>(
667 reader: &mut R,
668 block_size: u32,
669 byte_pos: u64,
670 ext_len: usize,
671 total_len: u64,
672 data: &mut Vec<u8>,
673) -> Option<()> {
674 let bs = block_size as usize;
675 let sectors = ext_len.div_ceil(bs);
676 for i in 0..sectors {
677 let mut sector = [0u8; MAX_BLOCK_SIZE];
678 let sector = &mut sector[..bs];
679 seek_read(reader, byte_pos + i as u64 * block_size as u64, sector)?;
680 let already = data.len() as u64;
681 let remaining = total_len.saturating_sub(already) as usize;
682 let sector_bytes = (ext_len - i * bs).min(bs);
683 let take = sector_bytes.min(remaining);
684 data.extend_from_slice(§or[..take]);
685 }
686 Some(())
687}
688
689fn decode_osta_cs0(bytes: &[u8]) -> String {
692 if bytes.is_empty() {
693 return String::new();
694 }
695 let comp_id = bytes[0];
696 let payload = &bytes[1..];
697 match comp_id {
698 8 => String::from_utf8_lossy(payload).into_owned(),
699 16 => {
700 let pairs: Vec<u16> = payload
701 .chunks_exact(2)
702 .map(|c| u16::from_be_bytes([c[0], c[1]]))
703 .collect();
704 String::from_utf16_lossy(&pairs)
705 }
706 _ => String::from_utf8_lossy(payload).into_owned(),
707 }
708}
709
710fn seek_read<R: Read + Seek>(reader: &mut R, byte_pos: u64, buf: &mut [u8]) -> Option<()> {
712 seek_read_checked(reader, byte_pos, buf).ok()
713}
714
715fn seek_read_checked<R: Read + Seek>(
718 reader: &mut R,
719 byte_pos: u64,
720 buf: &mut [u8],
721) -> Result<(), io::Error> {
722 reader.seek(SeekFrom::Start(byte_pos))?;
723 reader.read_exact(buf)?;
724 Ok(())
725}
726
727pub(crate) fn tag_checksum(tag: &[u8]) -> u8 {
732 let mut sum: u32 = 0;
733 for (i, &b) in tag.iter().take(16).enumerate() {
734 if i == 4 {
735 continue;
736 }
737 sum = sum.wrapping_add(u32::from(b));
738 }
739 (sum & 0xFF) as u8
740}
741
742pub(crate) fn ecma167_crc(body: &[u8]) -> u16 {
746 let mut crc: u16 = 0;
747 for &b in body {
748 crc ^= u16::from(b) << 8;
749 for _ in 0..8 {
750 if crc & 0x8000 != 0 {
751 crc = (crc << 1) ^ 0x1021;
752 } else {
753 crc <<= 1;
754 }
755 }
756 }
757 crc
758}
759
760pub(crate) fn descriptor_label(tag_ident: u16) -> Option<&'static str> {
764 Some(match tag_ident {
765 TAG_AVDP => "AVDP",
766 TAG_PD => "PartitionDescriptor",
767 TAG_LVD => "LogicalVolumeDescriptor",
768 TAG_TERM => "TerminatingDescriptor",
769 TAG_FSD => "FileSetDescriptor",
770 TAG_FID => "FileIdentifierDescriptor",
771 TAG_FE | TAG_FE_ALT => "FileEntry",
772 TAG_EFE => "ExtendedFileEntry",
773 1 => "PrimaryVolumeDescriptor",
774 3 => "VolumeDescriptorPointer",
775 4 => "ImplementationUseVolumeDescriptor",
776 7 => "UnallocatedSpaceDescriptor",
777 9 => "LogicalVolumeIntegrityDescriptor",
778 258 => "AllocationExtentDescriptor",
779 259 => "IndirectEntry",
780 262 => "SpaceBitmapDescriptor",
781 263 => "PartitionIntegrityEntry",
782 264 => "ExtendedAttributeHeaderDescriptor",
783 265 => "UnallocatedSpaceEntry",
784 _ => return None,
785 })
786}
787
788pub(crate) fn decode_timestamp(b: &[u8]) -> Option<String> {
792 if b.len() < 12 {
793 return None;
794 }
795 let year = i16::from_le_bytes([b[2], b[3]]);
796 if !(1970..=2200).contains(&year) {
797 return None;
798 }
799 let (month, day, hour, minute, second) = (b[4], b[5], b[6], b[7], b[8]);
800 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
801 return None;
802 }
803 Some(format!(
804 "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"
805 ))
806}
807
808pub(crate) fn fsd_recording_time<R: Read + Seek>(
812 reader: &mut R,
813 block_size: u32,
814 fsd_lba: u32,
815) -> Result<Option<String>, io::Error> {
816 let mut buf = [0u8; MAX_BLOCK_SIZE];
817 let sector = &mut buf[..block_size as usize];
818 seek_read_checked(reader, u64::from(fsd_lba) * u64::from(block_size), sector)?;
819 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
820 return Ok(None);
821 }
822 Ok(decode_timestamp(§or[16..28]))
823}
824
825pub(crate) fn fe_modification_time(sector: &[u8], is_efe: bool) -> Option<String> {
829 let off = if is_efe { 92 } else { 84 };
830 decode_timestamp(sector.get(off..off + 12)?)
831}
832
833pub(crate) fn fe_slack_nonzero<R: Read + Seek>(
842 reader: &mut R,
843 block_size: u32,
844 partition_start: u32,
845 fe_lba: u32,
846) -> Option<(u32, u32)> {
847 let info_len = read_fe_info_len(reader, block_size, fe_lba)?;
848 let bs = u64::from(block_size);
849 let slack = (bs - info_len % bs) % bs;
850 if info_len == 0 || slack == 0 {
851 return None;
852 }
853 let last_block_pos = fe_last_block_pos(reader, block_size, partition_start, fe_lba)?;
858 let mut buf = [0u8; MAX_BLOCK_SIZE];
859 let block = &mut buf[..block_size as usize];
860 seek_read_checked(reader, last_block_pos, block).ok()?;
861
862 let slack_start = (info_len % bs) as usize;
863 let nonzero = block[slack_start..].iter().filter(|&&b| b != 0).count() as u32;
864 Some((nonzero, slack as u32))
865}
866
867fn fe_last_block_pos<R: Read + Seek>(
871 reader: &mut R,
872 block_size: u32,
873 partition_start: u32,
874 fe_lba: u32,
875) -> Option<u64> {
876 let mut buf = [0u8; MAX_BLOCK_SIZE];
877 let sector = &mut buf[..block_size as usize];
878 seek_read_checked(reader, u64::from(fe_lba) * u64::from(block_size), sector).ok()?;
879
880 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
881 let is_efe = tag_ident == TAG_EFE;
882 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
883 return None;
884 }
885
886 let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
887 let alloc_type = icb_flags & 0x0007;
888 let (ea_off, ad_off, header) = if is_efe {
889 (176usize, 180usize, 184usize)
890 } else {
891 (168usize, 172usize, 176usize)
892 };
893 if ad_off + 4 > sector.len() {
894 return None; }
896 let ea_len = u32::from_le_bytes(sector[ea_off..ea_off + 4].try_into().ok()?) as usize;
897 let ad_len = u32::from_le_bytes(sector[ad_off..ad_off + 4].try_into().ok()?) as usize;
898 let ad_start = header + ea_len;
899 let ad_end = ad_start.checked_add(ad_len)?;
900 if ad_end > sector.len() {
901 return None;
902 }
903 let ad_area = §or[ad_start..ad_end];
904
905 let stride = match alloc_type {
909 ALLOC_SHORT => 8,
910 ALLOC_LONG => 16,
911 _ => return None,
912 };
913 let mut last: Option<u64> = None;
914 let mut pos = 0;
915 while pos + stride <= ad_area.len() {
916 let len_raw = read_le_u32(ad_area, pos);
917 let ext_type = len_raw >> 30;
918 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
919 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
920 let lbn = read_le_u32(ad_area, pos + 4);
921 let blocks_in_ext = ext_len.div_ceil(block_size as usize) as u64;
922 let last_lbn = u64::from(partition_start) + u64::from(lbn) + (blocks_in_ext - 1);
923 last = Some(last_lbn * u64::from(block_size));
924 }
925 pos += stride;
926 }
927 last
928}
929
930fn read_le_u32(data: &[u8], off: usize) -> u32 {
933 let mut b = [0u8; 4];
934 if let Some(s) = data.get(off..off + 4) {
935 b.copy_from_slice(s);
936 }
937 u32::from_le_bytes(b)
938}
939
940#[cfg(test)]
941mod real_media_tests {
942 use super::{parse_udf_state, UdfPartitionKind};
949 use std::fs::File;
950
951 fn state(name: &str) -> Option<super::UdfState> {
952 let path = format!("{}/tests/data/{}", env!("CARGO_MANIFEST_DIR"), name);
953 let mut f = File::open(&path).ok()?;
954 parse_udf_state(&mut f)
955 }
956
957 #[test]
958 fn vat_image_classified_virtual() {
959 let Some(st) = state("udf_vat.img") else {
960 eprintln!("skip: udf_vat.img");
961 return;
962 };
963 assert_eq!(
964 st.partition_kind,
965 UdfPartitionKind::Virtual,
966 "mkudffs cdr/1.50 image must classify as Virtual (VAT)"
967 );
968 }
969
970 #[test]
971 fn sparable_image_classified_sparable() {
972 let Some(st) = state("udf_spar.img") else {
973 eprintln!("skip: udf_spar.img");
974 return;
975 };
976 assert_eq!(
977 st.partition_kind,
978 UdfPartitionKind::Sparable,
979 "mkudffs dvdrw/2.01 image must classify as Sparable"
980 );
981 }
982
983 #[test]
994 fn vat_image_matches_udfinfo_oracle() {
995 let Some(st) = state("udf_vat.img") else {
996 eprintln!("skip: udf_vat.img");
997 return;
998 };
999 assert_eq!(st.partition_kind, UdfPartitionKind::Virtual);
1000 assert_eq!(
1002 st.partition_start, 257,
1003 "partition start must match udfinfo PSPACE start=257"
1004 );
1005 assert_eq!(
1007 st.partition_map_count, 2,
1008 "cdr/1.50 carries a physical map plus the VAT Type-2 map"
1009 );
1010 }
1011
1012 #[test]
1018 fn sparable_image_matches_udfinfo_oracle() {
1019 let Some(st) = state("udf_spar.img") else {
1020 eprintln!("skip: udf_spar.img");
1021 return;
1022 };
1023 assert_eq!(st.partition_kind, UdfPartitionKind::Sparable);
1024 assert_eq!(
1026 st.partition_start, 1296,
1027 "partition start must match udfinfo PSPACE start=1296"
1028 );
1029 assert_eq!(
1030 st.partition_map_count, 1,
1031 "dvdrw/2.01 carries a single Sparable Type-2 map"
1032 );
1033 }
1034
1035 #[test]
1041 fn plain_512_block_image_parses_via_detected_block_size() {
1042 let path = format!("{}/tests/data/udf_plain.img", env!("CARGO_MANIFEST_DIR"));
1043 let mut f = File::open(&path).expect("udf_plain.img fixture must be present");
1044 let st = super::parse_udf_state(&mut f)
1045 .expect("512-byte-block UDF must parse once the block size is detected from the AVDP");
1046 assert_eq!(st.block_size, 512, "udfinfo reports blocksize=512");
1047 assert_eq!(
1048 st.partition_kind,
1049 UdfPartitionKind::Physical,
1050 "mkudffs hd image is a Type-1 physical partition"
1051 );
1052 assert_eq!(
1053 st.partition_start, 257,
1054 "partition start must match udfinfo PSPACE start=257"
1055 );
1056 assert_eq!(
1057 st.partition_map_count, 1,
1058 "hd/2.01 carries a single physical map"
1059 );
1060 }
1061}
1062
1063#[cfg(test)]
1064mod checked_bootstrap_tests {
1065 use super::parse_udf_state_checked;
1069 use std::io::{self, Cursor, Read, Seek, SeekFrom};
1070
1071 struct FaultyReader;
1074
1075 impl Read for FaultyReader {
1076 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1077 Err(io::Error::other("device read fault"))
1078 }
1079 }
1080 impl Seek for FaultyReader {
1081 fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
1082 Ok(0)
1083 }
1084 }
1085
1086 #[test]
1087 fn io_error_at_anchor_surfaces_as_err() {
1088 let mut r = FaultyReader;
1089 let res = parse_udf_state_checked(&mut r);
1090 assert!(
1091 res.is_err(),
1092 "a device read fault reading the anchor must surface as Err, not Ok(None)"
1093 );
1094 }
1095
1096 #[test]
1097 fn truncated_before_anchor_surfaces_as_err() {
1098 let buf = vec![0u8; 4096];
1102 let mut r = Cursor::new(buf);
1103 let res = parse_udf_state_checked(&mut r);
1104 assert!(
1105 res.is_err(),
1106 "truncation before the AVDP anchor must surface as Err (UnexpectedEof)"
1107 );
1108 let err = res.err().unwrap();
1109 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1110 }
1111
1112 #[test]
1113 fn full_size_but_wrong_anchor_is_ok_none() {
1114 let buf = vec![0u8; 257 * 2048];
1118 let mut r = Cursor::new(buf);
1119 let res = parse_udf_state_checked(&mut r);
1120 assert!(
1121 matches!(res, Ok(None)),
1122 "a readable image with a non-AVDP anchor must be Ok(None), got {res:?}"
1123 );
1124 }
1125}
1126
1127#[cfg(test)]
1128mod findings_support_tests {
1129 use super::*;
1135 use std::io::Cursor;
1136
1137 #[test]
1138 fn crc_ccitt_matches_known_vectors() {
1139 assert_eq!(ecma167_crc(b"123456789"), 0x31C3);
1142 assert_eq!(ecma167_crc(&[]), 0x0000);
1143 }
1144
1145 #[test]
1146 fn tag_checksum_skips_byte_4() {
1147 let mut tag = [0u8; 16];
1148 tag[0] = 2; tag[4] = 0xFF; tag[6] = 3; assert_eq!(tag_checksum(&tag), 5);
1152 }
1153
1154 #[test]
1155 fn descriptor_label_known_and_unknown() {
1156 for (id, name) in [
1159 (TAG_FSD, "FileSetDescriptor"),
1160 (TAG_EFE, "ExtendedFileEntry"),
1161 (TAG_FID, "FileIdentifierDescriptor"),
1162 (TAG_FE, "FileEntry"),
1163 (TAG_FE_ALT, "FileEntry"),
1164 (1, "PrimaryVolumeDescriptor"),
1165 (3, "VolumeDescriptorPointer"),
1166 (4, "ImplementationUseVolumeDescriptor"),
1167 (7, "UnallocatedSpaceDescriptor"),
1168 (9, "LogicalVolumeIntegrityDescriptor"),
1169 (258, "AllocationExtentDescriptor"),
1170 (259, "IndirectEntry"),
1171 (262, "SpaceBitmapDescriptor"),
1172 (263, "PartitionIntegrityEntry"),
1173 (264, "ExtendedAttributeHeaderDescriptor"),
1174 (265, "UnallocatedSpaceEntry"),
1175 ] {
1176 assert_eq!(descriptor_label(id), Some(name), "tag {id}");
1177 }
1178 assert_eq!(descriptor_label(0xFFFF), None);
1179 }
1180
1181 #[test]
1182 fn fsd_recording_time_none_for_non_fsd() {
1183 let img = vec![0u8; 512];
1184 let mut r = Cursor::new(img);
1185 assert_eq!(fsd_recording_time(&mut r, 512, 0).unwrap(), None);
1186 }
1187
1188 #[test]
1189 fn last_block_pos_none_for_non_file_entry() {
1190 let img = vec![0u8; 512];
1191 let mut r = Cursor::new(img);
1192 assert_eq!(fe_last_block_pos(&mut r, 512, 0, 0), None);
1193 }
1194
1195 #[test]
1196 fn slack_via_long_allocation_descriptor() {
1197 let bs = 512usize;
1199 let mut img = vec![0u8; bs * 8];
1200 let fe = 4 * bs;
1201 img[fe..fe + 2].copy_from_slice(&TAG_FE.to_le_bytes());
1202 img[fe + 34..fe + 36].copy_from_slice(&1u16.to_le_bytes()); img[fe + 56..fe + 64].copy_from_slice(&100u64.to_le_bytes());
1204 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;
1209 img[data] = 0x7F;
1210 let mut r = Cursor::new(img);
1211 let (nonzero, slack) = fe_slack_nonzero(&mut r, 512, 0, 4).expect("slack present");
1212 assert_eq!(slack, 412);
1213 assert_eq!(nonzero, 1);
1214 }
1215
1216 #[test]
1217 fn timestamp_decodes_and_rejects_implausible() {
1218 let mut t = [0u8; 12];
1219 t[2..4].copy_from_slice(&2026i16.to_le_bytes());
1220 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"));
1226
1227 let mut bad = t;
1229 bad[2..4].copy_from_slice(&0i16.to_le_bytes());
1230 assert_eq!(decode_timestamp(&bad), None);
1231
1232 let mut badmon = t;
1234 badmon[4] = 0;
1235 assert_eq!(decode_timestamp(&badmon), None);
1236
1237 assert_eq!(decode_timestamp(&[0u8; 4]), None);
1239 }
1240
1241 #[test]
1242 fn fe_modification_time_offset_differs_for_efe() {
1243 let mut fe = vec![0u8; 512];
1245 let stamp = |buf: &mut [u8], off: usize, year: i16| {
1246 buf[off + 2..off + 4].copy_from_slice(&year.to_le_bytes());
1247 buf[off + 4] = 1; buf[off + 5] = 1; };
1250 stamp(&mut fe, 84, 2030);
1251 assert_eq!(
1252 fe_modification_time(&fe, false).as_deref(),
1253 Some("2030-01-01 00:00:00")
1254 );
1255 let mut efe = vec![0u8; 512];
1256 stamp(&mut efe, 92, 2031);
1257 assert_eq!(
1258 fe_modification_time(&efe, true).as_deref(),
1259 Some("2031-01-01 00:00:00")
1260 );
1261 }
1262
1263 fn image_with_slack(info_len: u64, slack_fill: &[u8]) -> (Vec<u8>, u32, u32) {
1268 let bs = 512usize;
1269 let part_start = 0u32;
1270 let fe_lba = 4u32;
1271 let data_lbn = 5u32; let mut img = vec![0u8; bs * 8];
1273
1274 let fe = fe_lba as usize * bs;
1275 img[fe..fe + 2].copy_from_slice(&TAG_FE.to_le_bytes());
1277 img[fe + 34..fe + 36].copy_from_slice(&0u16.to_le_bytes());
1279 img[fe + 56..fe + 64].copy_from_slice(&info_len.to_le_bytes());
1281 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;
1286 img[ad..ad + 4].copy_from_slice(&(info_len as u32).to_le_bytes());
1287 img[ad + 4..ad + 8].copy_from_slice(&data_lbn.to_le_bytes());
1288
1289 let data = (part_start + data_lbn) as usize * bs;
1291 let slack_start = data + (info_len as usize % bs);
1292 for (i, &b) in slack_fill.iter().enumerate() {
1293 img[slack_start + i] = b;
1294 }
1295 (img, part_start, fe_lba)
1296 }
1297
1298 #[test]
1299 fn slack_counts_nonzero_tail_bytes() {
1300 let (img, ps, fe) = image_with_slack(100, &[0xAA, 0x00, 0xBB, 0xCC]);
1302 let mut r = Cursor::new(img);
1303 let (nonzero, slack) = fe_slack_nonzero(&mut r, 512, ps, fe).expect("slack present");
1304 assert_eq!(slack, 412);
1305 assert_eq!(nonzero, 3);
1306 }
1307
1308 #[test]
1309 fn slack_none_when_block_aligned() {
1310 let (img, ps, fe) = image_with_slack(512, &[0xFF]);
1312 let mut r = Cursor::new(img);
1313 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1314 }
1315
1316 #[test]
1317 fn slack_none_when_zero_length() {
1318 let (img, ps, fe) = image_with_slack(0, &[]);
1319 let mut r = Cursor::new(img);
1320 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1321 }
1322
1323 #[test]
1324 fn slack_none_for_inline_data() {
1325 let (mut img, ps, fe) = image_with_slack(100, &[0xAA]);
1327 let fe_off = fe as usize * 512;
1328 img[fe_off + 34..fe_off + 36].copy_from_slice(&3u16.to_le_bytes());
1329 let mut r = Cursor::new(img);
1330 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1331 }
1332}