1#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
22
23use safe_read::{le_u32, le_u64};
24use std::io::{self, Read, Seek, SeekFrom};
25
26pub mod findings;
27
28#[cfg(feature = "vfs")]
30pub mod vfs;
31
32#[cfg(test)]
33pub(crate) mod test_support;
34
35pub use forensicnomicon::report::Severity;
38
39const TAG_AVDP: u16 = 2;
42const TAG_PD: u16 = 5;
43const TAG_LVD: u16 = 6;
44const TAG_TERM: u16 = 8;
45const TAG_FSD: u16 = 256;
46const TAG_FID: u16 = 257;
47const TAG_FE: u16 = 260;
48const TAG_FE_ALT: u16 = 261;
50const TAG_EFE: u16 = 266;
51
52const FC_DIRECTORY: u8 = 0x02;
54const FC_PARENT: u8 = 0x08;
55
56const ALLOC_SHORT: u16 = 0;
58const ALLOC_LONG: u16 = 1;
59const ALLOC_INLINE: u16 = 3;
60
61const EXTENT_RECORDED: u32 = 0x0000_0000; const MAX_BLOCK_SIZE: usize = 4096;
68
69const BLOCK_SIZE_CANDIDATES: [u32; 4] = [2048, 512, 1024, 4096];
72
73#[derive(Debug, Clone)]
77pub struct UdfFileEntry {
78 pub name: String,
80 pub is_dir: bool,
82 pub size: u64,
84 pub fe_lba: u32,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99pub enum UdfPartitionKind {
100 Physical,
102 Virtual,
104 Sparable,
106 Metadata,
108 Unknown,
110}
111
112#[derive(Debug)]
115pub struct UdfState {
116 pub partition_start: u32,
117 pub root_fe_lba: u32,
118 pub partition_kind: UdfPartitionKind,
119 pub partition_map_count: u32,
120 pub block_size: u32,
124 pub fsd_lba: u32,
128 pub vds_loc: u32,
132 pub vds_len_sectors: u32,
134}
135
136pub fn detect_udf<R: Read + Seek>(reader: &mut R) -> bool {
142 let mut buf = [0u8; 6];
143 for lba in 16u64..32 {
144 let pos = lba * 2048 + 1;
145 if reader.seek(SeekFrom::Start(pos)).is_err() {
146 break;
147 }
148 if reader.read_exact(&mut buf).is_err() {
149 break;
150 }
151 let id = &buf[..5];
152 if id == b"NSR02" || id == b"NSR03" {
153 return true;
154 }
155 if id == b"TEA01" {
156 break;
157 }
158 }
159 false
160}
161
162pub fn parse_udf_state<R: Read + Seek>(reader: &mut R) -> Option<UdfState> {
172 parse_udf_state_checked(reader).ok().flatten()
173}
174
175pub fn parse_udf_state_checked<R: Read + Seek>(
188 reader: &mut R,
189) -> Result<Option<UdfState>, io::Error> {
190 let Some(block_size) = detect_block_size(reader)? else {
191 return Ok(None);
192 };
193 let Some((vds_loc, vds_len)) = read_avdp_checked(reader, block_size)? else {
194 return Ok(None); };
196 let Some(vds) = read_vds_checked(reader, block_size, vds_loc, vds_len)? else {
197 return Ok(None);
198 };
199 let Some(root_fe_lba) = read_fsd_checked(reader, block_size, vds.fsd_lba, vds.partition_start)?
200 else {
201 return Ok(None);
202 };
203 Ok(Some(UdfState {
204 partition_start: vds.partition_start,
205 root_fe_lba,
206 partition_kind: vds.partition_kind,
207 partition_map_count: vds.map_count,
208 block_size,
209 fsd_lba: vds.fsd_lba,
210 vds_loc,
211 vds_len_sectors: (vds_len as usize).div_ceil(block_size as usize) as u32,
212 }))
213}
214
215struct VdsInfo {
217 partition_start: u32,
218 fsd_lba: u32,
219 partition_kind: UdfPartitionKind,
220 map_count: u32,
221}
222
223struct PartitionMap {
225 kind: UdfPartitionKind,
226 partition_number: Option<u16>,
228}
229
230fn classify_type2(map: &[u8]) -> UdfPartitionKind {
233 let scan = |needle: &[u8]| map.windows(needle.len()).any(|w| w == needle);
234 if scan(b"*UDF Metadata Partition") {
235 UdfPartitionKind::Metadata
236 } else if scan(b"*UDF Virtual Partition") {
237 UdfPartitionKind::Virtual
238 } else if scan(b"*UDF Sparable Partition") {
239 UdfPartitionKind::Sparable
240 } else {
241 UdfPartitionKind::Unknown
242 }
243}
244
245fn parse_partition_maps(lvd: &[u8]) -> Vec<PartitionMap> {
251 let n_pm = le_u32(lvd, 268) as usize;
252 let mt_l = le_u32(lvd, 264) as usize;
253 let maps_end = (440 + mt_l).min(lvd.len());
254 let mut out = Vec::new();
255 let mut off = 440;
256 while out.len() < n_pm && off + 2 <= maps_end {
257 let map_type = lvd[off];
258 let map_len = lvd[off + 1] as usize;
259 if map_len < 2 || off + map_len > maps_end {
260 break;
261 }
262 let map = &lvd[off..off + map_len];
263 let pm = match map_type {
264 1 if map_len >= 6 => PartitionMap {
265 kind: UdfPartitionKind::Physical,
266 partition_number: Some(u16::from_le_bytes([map[4], map[5]])),
267 },
268 2 => PartitionMap {
269 kind: classify_type2(map),
270 partition_number: None,
271 },
272 _ => PartitionMap {
273 kind: UdfPartitionKind::Unknown,
274 partition_number: None,
275 },
276 };
277 out.push(pm);
278 off += map_len;
279 }
280 out
281}
282
283pub fn read_dir_at_lba<R: Read + Seek>(
286 reader: &mut R,
287 block_size: u32,
288 partition_start: u32,
289 dir_fe_lba: u32,
290) -> Option<Vec<UdfFileEntry>> {
291 let dir_data = read_fe_data(reader, block_size, partition_start, dir_fe_lba)?;
292 Some(parse_fids(reader, block_size, partition_start, &dir_data))
293}
294
295pub fn read_fe_data<R: Read + Seek>(
297 reader: &mut R,
298 block_size: u32,
299 partition_start: u32,
300 fe_lba: u32,
301) -> Option<Vec<u8>> {
302 let mut sector = [0u8; MAX_BLOCK_SIZE];
303 let sector = &mut sector[..block_size as usize];
304 seek_read(reader, u64::from(fe_lba) * u64::from(block_size), sector)?;
305
306 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
307 let is_efe = tag_ident == TAG_EFE;
308 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
309 return None;
310 }
311
312 let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
313 let alloc_type = icb_flags & 0x0007;
314 let info_len = le_u64(sector, 56);
315
316 let (ea_off, ad_off, header) = if is_efe {
324 (208usize, 212usize, 216usize)
325 } else {
326 (168usize, 172usize, 176usize)
327 };
328
329 if ad_off + 4 > sector.len() {
330 return None; }
332 let ea_len = le_u32(sector, ea_off) as usize;
333 let ad_len = le_u32(sector, ad_off) as usize;
334
335 let ad_start = header + ea_len;
336 let ad_end = ad_start + ad_len;
337 if ad_end > sector.len() {
338 return None;
339 }
340 let ad_area = sector[ad_start..ad_end].to_vec();
341
342 match alloc_type {
343 ALLOC_INLINE => Some(ad_area[..info_len.min(ad_area.len() as u64) as usize].to_vec()),
344 ALLOC_SHORT => read_extents_short(reader, block_size, partition_start, &ad_area, info_len),
345 ALLOC_LONG => read_extents_long(reader, block_size, partition_start, &ad_area, info_len),
346 _ => None,
347 }
348}
349
350fn detect_block_size<R: Read + Seek>(reader: &mut R) -> Result<Option<u32>, io::Error> {
364 let mut tag = [0u8; 16];
365 let mut last_eof: Option<io::Error> = None;
366 let mut any_read_ok = false;
367 for bs in BLOCK_SIZE_CANDIDATES {
368 match seek_read_checked(reader, 256 * u64::from(bs), &mut tag) {
369 Ok(()) => {
370 any_read_ok = true;
371 let tag_ident = u16::from_le_bytes([tag[0], tag[1]]);
372 let tag_location = le_u32(&tag, 12);
373 if tag_ident == TAG_AVDP && tag_location == 256 {
374 return Ok(Some(bs));
375 }
376 }
377 Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => last_eof = Some(e),
378 Err(e) => return Err(e),
379 }
380 }
381 if !any_read_ok {
382 if let Some(e) = last_eof {
383 return Err(e);
384 }
385 }
386 Ok(None)
387}
388
389fn read_avdp_checked<R: Read + Seek>(
393 reader: &mut R,
394 block_size: u32,
395) -> Result<Option<(u32, u32)>, io::Error> {
396 let mut sector = [0u8; MAX_BLOCK_SIZE];
397 let sector = &mut sector[..block_size as usize];
398 seek_read_checked(reader, 256 * u64::from(block_size), sector)?;
399 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_AVDP {
400 return Ok(None);
401 }
402 let vds_len = le_u32(sector, 16);
403 let vds_loc = le_u32(sector, 20);
404 Ok(Some((vds_loc, vds_len)))
405}
406
407fn read_vds_checked<R: Read + Seek>(
412 reader: &mut R,
413 block_size: u32,
414 vds_loc: u32,
415 vds_len: u32,
416) -> Result<Option<VdsInfo>, io::Error> {
417 use std::collections::HashMap;
418 let sectors = (vds_len as usize).div_ceil(block_size as usize);
419
420 let mut pd_start: HashMap<u16, u32> = HashMap::new();
422 let mut fsd_lbn: Option<u32> = None;
423 let mut fsd_part_ref: u16 = 0;
424 let mut maps: Vec<PartitionMap> = Vec::new();
425
426 for i in 0..sectors {
427 let mut sector = [0u8; MAX_BLOCK_SIZE];
428 let sector = &mut sector[..block_size as usize];
429 seek_read_checked(
430 reader,
431 (u64::from(vds_loc) + i as u64) * u64::from(block_size),
432 sector,
433 )?;
434 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
435 match tag_ident {
436 TAG_PD => {
437 let part_num = u16::from_le_bytes([sector[22], sector[23]]);
438 let psl = le_u32(sector, 188);
439 pd_start.insert(part_num, psl);
440 }
441 TAG_LVD => {
442 fsd_lbn = Some(le_u32(sector, 252));
445 fsd_part_ref = u16::from_le_bytes([sector[256], sector[257]]);
446 maps = parse_partition_maps(sector);
447 }
448 TAG_TERM | 0 => break,
449 _ => {}
450 }
451 }
452
453 let Some(fsd) = fsd_lbn else {
455 return Ok(None);
456 };
457 let map_count = maps.len() as u32;
458
459 let referenced = maps.get(fsd_part_ref as usize);
461 let kind = referenced.map_or(UdfPartitionKind::Unknown, |m| m.kind);
462
463 let partition_start = referenced
468 .and_then(|m| m.partition_number)
469 .and_then(|pn| pd_start.get(&pn).copied())
470 .or_else(|| pd_start.values().min().copied());
471 let Some(partition_start) = partition_start else {
472 return Ok(None);
473 };
474
475 let Some(fsd_lba) = partition_start.checked_add(fsd) else {
481 return Ok(None);
482 };
483
484 Ok(Some(VdsInfo {
485 partition_start,
486 fsd_lba,
487 partition_kind: kind,
488 map_count,
489 }))
490}
491
492fn read_fsd_checked<R: Read + Seek>(
496 reader: &mut R,
497 block_size: u32,
498 fsd_lba: u32,
499 partition_start: u32,
500) -> Result<Option<u32>, io::Error> {
501 let mut sector = [0u8; MAX_BLOCK_SIZE];
502 let sector = &mut sector[..block_size as usize];
503 seek_read_checked(reader, u64::from(fsd_lba) * u64::from(block_size), sector)?;
504 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
505 return Ok(None);
506 }
507 let lbn = le_u32(sector, 404);
514 Ok(Some(partition_start + lbn))
515}
516
517fn detect_fid_tag_size(data: &[u8]) -> usize {
525 let mut off = 0;
526 while off + 28 <= data.len() {
527 let ti = u16::from_le_bytes([data[off], data[off + 1]]);
528 if ti == TAG_FID {
529 let lbn16 = if off + 26 <= data.len() {
533 le_u32(data, off + 22)
534 } else {
535 u32::MAX
536 };
537 let lbn18 = if off + 28 <= data.len() {
538 le_u32(data, off + 24)
539 } else {
540 u32::MAX
541 };
542 if lbn16 < 0x10000 {
543 return 16;
544 }
545 if lbn18 < 0x10000 {
546 return 18;
547 }
548 return 16; }
550 off += 4;
551 }
552 16
553}
554
555fn parse_fids<R: Read + Seek>(
557 reader: &mut R,
558 block_size: u32,
559 partition_start: u32,
560 data: &[u8],
561) -> Vec<UdfFileEntry> {
562 let tag_size = detect_fid_tag_size(data);
565 let min_fid = tag_size + 20; let mut entries = Vec::new();
568 let mut off = 0;
569
570 while off + min_fid <= data.len() {
571 let tag_ident = u16::from_le_bytes([data[off], data[off + 1]]);
572 if tag_ident != TAG_FID {
573 off += 4;
575 continue;
576 }
577
578 let crc_len = u16::from_le_bytes([data[off + 10], data[off + 11]]) as usize;
580 let fid_advance = ((16 + crc_len + 3) & !3).max(min_fid);
581 if off + fid_advance > data.len() {
582 break;
583 }
584
585 let file_chars = data[off + tag_size];
586 let file_id_len = data[off + tag_size + 1] as usize;
587 let icb_lbn = if off + tag_size + 10 <= data.len() {
589 le_u32(data, off + tag_size + 6)
590 } else {
591 off += fid_advance.max(4);
593 continue;
594 };
595 let impl_use_len = if off + tag_size + 20 <= data.len() {
596 u16::from_le_bytes([data[off + tag_size + 18], data[off + tag_size + 19]]) as usize
597 } else {
598 off += fid_advance.max(4);
600 continue;
601 };
602
603 if file_chars & FC_PARENT == 0 {
604 let is_dir = file_chars & FC_DIRECTORY != 0;
605 let id_start = off
610 .saturating_add(tag_size)
611 .saturating_add(20)
612 .saturating_add(impl_use_len);
613 let id_end = id_start.saturating_add(file_id_len).min(data.len());
614 let name = if id_end > id_start {
615 decode_osta_cs0(&data[id_start..id_end])
616 } else {
617 String::new()
618 };
619
620 if let Some(fe_lba) = partition_start.checked_add(icb_lbn) {
630 let size = read_fe_info_len(reader, block_size, fe_lba).unwrap_or(0);
632
633 entries.push(UdfFileEntry {
634 name,
635 is_dir,
636 size,
637 fe_lba,
638 });
639 }
640 }
641
642 off += fid_advance.max(4);
643 }
644 entries
645}
646
647fn read_fe_info_len<R: Read + Seek>(reader: &mut R, block_size: u32, fe_lba: u32) -> Option<u64> {
649 let mut sector = [0u8; MAX_BLOCK_SIZE];
650 let sector = &mut sector[..block_size as usize];
651 seek_read(reader, u64::from(fe_lba) * u64::from(block_size), sector)?;
652 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
653 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
654 return None;
655 }
656 Some(le_u64(sector, 56))
657}
658
659#[cfg(feature = "vfs")]
667pub(crate) fn read_fe_file_type<R: Read + Seek>(
668 reader: &mut R,
669 block_size: u32,
670 fe_lba: u32,
671) -> Option<u8> {
672 let mut sector = [0u8; MAX_BLOCK_SIZE];
673 let sector = &mut sector[..block_size as usize];
674 seek_read(reader, u64::from(fe_lba) * u64::from(block_size), sector)?;
675 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
676 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
677 return None;
678 }
679 sector.get(27).copied()
680}
681
682#[cfg(feature = "vfs")]
684pub(crate) const FILE_TYPE_DIRECTORY: u8 = 4;
685
686fn read_extents_short<R: Read + Seek>(
688 reader: &mut R,
689 block_size: u32,
690 partition_start: u32,
691 ad_area: &[u8],
692 total_len: u64,
693) -> Option<Vec<u8>> {
694 let mut data = Vec::new();
695 let mut pos = 0;
696 while pos + 8 <= ad_area.len() && (data.len() as u64) < total_len {
697 let len_raw = le_u32(ad_area, pos);
698 let ext_pos = le_u32(ad_area, pos + 4);
699 let ext_type = len_raw >> 30;
700 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
701 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
702 let phys = (u64::from(partition_start) + u64::from(ext_pos)) * u64::from(block_size);
703 read_extent(reader, block_size, phys, ext_len, total_len, &mut data)?;
704 }
705 pos += 8;
706 }
707 data.truncate(total_len as usize);
708 Some(data)
709}
710
711fn read_extents_long<R: Read + Seek>(
713 reader: &mut R,
714 block_size: u32,
715 partition_start: u32,
716 ad_area: &[u8],
717 total_len: u64,
718) -> Option<Vec<u8>> {
719 let mut data = Vec::new();
720 let mut pos = 0;
721 while pos + 16 <= ad_area.len() && (data.len() as u64) < total_len {
722 let len_raw = le_u32(ad_area, pos);
723 let lbn = le_u32(ad_area, pos + 4);
724 let ext_type = len_raw >> 30;
725 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
726 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
727 let phys = (u64::from(partition_start) + u64::from(lbn)) * u64::from(block_size);
728 read_extent(reader, block_size, phys, ext_len, total_len, &mut data)?;
729 }
730 pos += 16;
731 }
732 data.truncate(total_len as usize);
733 Some(data)
734}
735
736fn read_extent<R: Read + Seek>(
738 reader: &mut R,
739 block_size: u32,
740 byte_pos: u64,
741 ext_len: usize,
742 total_len: u64,
743 data: &mut Vec<u8>,
744) -> Option<()> {
745 let bs = block_size as usize;
746 let sectors = ext_len.div_ceil(bs);
747 for i in 0..sectors {
748 let mut sector = [0u8; MAX_BLOCK_SIZE];
749 let sector = &mut sector[..bs];
750 seek_read(reader, byte_pos + i as u64 * u64::from(block_size), sector)?;
751 let already = data.len() as u64;
752 let remaining = total_len.saturating_sub(already) as usize;
753 let sector_bytes = (ext_len - i * bs).min(bs);
754 let take = sector_bytes.min(remaining);
755 data.extend_from_slice(§or[..take]);
756 }
757 Some(())
758}
759
760fn decode_osta_cs0(bytes: &[u8]) -> String {
763 if bytes.is_empty() {
764 return String::new();
765 }
766 let comp_id = bytes[0];
767 let payload = &bytes[1..];
768 if comp_id == 16 {
771 let pairs: Vec<u16> = payload
772 .chunks_exact(2)
773 .map(|c| u16::from_be_bytes([c[0], c[1]]))
774 .collect();
775 String::from_utf16_lossy(&pairs)
776 } else {
777 String::from_utf8_lossy(payload).into_owned()
778 }
779}
780
781fn seek_read<R: Read + Seek>(reader: &mut R, byte_pos: u64, buf: &mut [u8]) -> Option<()> {
783 seek_read_checked(reader, byte_pos, buf).ok()
784}
785
786fn seek_read_checked<R: Read + Seek>(
789 reader: &mut R,
790 byte_pos: u64,
791 buf: &mut [u8],
792) -> Result<(), io::Error> {
793 reader.seek(SeekFrom::Start(byte_pos))?;
794 reader.read_exact(buf)?;
795 Ok(())
796}
797
798pub(crate) fn tag_checksum(tag: &[u8]) -> u8 {
803 let mut sum: u32 = 0;
804 for (i, &b) in tag.iter().take(16).enumerate() {
805 if i == 4 {
806 continue;
807 }
808 sum = sum.wrapping_add(u32::from(b));
809 }
810 (sum & 0xFF) as u8
811}
812
813pub(crate) fn ecma167_crc(body: &[u8]) -> u16 {
817 let mut crc: u16 = 0;
818 for &b in body {
819 crc ^= u16::from(b) << 8;
820 for _ in 0..8 {
821 if crc & 0x8000 != 0 {
822 crc = (crc << 1) ^ 0x1021;
823 } else {
824 crc <<= 1;
825 }
826 }
827 }
828 crc
829}
830
831pub(crate) fn descriptor_label(tag_ident: u16) -> Option<&'static str> {
835 Some(match tag_ident {
836 TAG_AVDP => "AVDP",
837 TAG_PD => "PartitionDescriptor",
838 TAG_LVD => "LogicalVolumeDescriptor",
839 TAG_TERM => "TerminatingDescriptor",
840 TAG_FSD => "FileSetDescriptor",
841 TAG_FID => "FileIdentifierDescriptor",
842 TAG_FE | TAG_FE_ALT => "FileEntry",
843 TAG_EFE => "ExtendedFileEntry",
844 1 => "PrimaryVolumeDescriptor",
845 3 => "VolumeDescriptorPointer",
846 4 => "ImplementationUseVolumeDescriptor",
847 7 => "UnallocatedSpaceDescriptor",
848 9 => "LogicalVolumeIntegrityDescriptor",
849 258 => "AllocationExtentDescriptor",
850 259 => "IndirectEntry",
851 262 => "SpaceBitmapDescriptor",
852 263 => "PartitionIntegrityEntry",
853 264 => "ExtendedAttributeHeaderDescriptor",
854 265 => "UnallocatedSpaceEntry",
855 _ => return None,
856 })
857}
858
859pub(crate) fn decode_timestamp(b: &[u8]) -> Option<String> {
863 if b.len() < 12 {
864 return None;
865 }
866 let year = i16::from_le_bytes([b[2], b[3]]);
867 if !(1970..=2200).contains(&year) {
868 return None;
869 }
870 let (month, day, hour, minute, second) = (b[4], b[5], b[6], b[7], b[8]);
871 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
872 return None;
873 }
874 Some(format!(
875 "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"
876 ))
877}
878
879pub(crate) fn fsd_recording_time<R: Read + Seek>(
883 reader: &mut R,
884 block_size: u32,
885 fsd_lba: u32,
886) -> Result<Option<String>, io::Error> {
887 let mut buf = [0u8; MAX_BLOCK_SIZE];
888 let sector = &mut buf[..block_size as usize];
889 seek_read_checked(reader, u64::from(fsd_lba) * u64::from(block_size), sector)?;
890 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
891 return Ok(None);
892 }
893 Ok(decode_timestamp(§or[16..28]))
894}
895
896pub(crate) fn fe_modification_time(sector: &[u8], is_efe: bool) -> Option<String> {
900 let off = if is_efe { 92 } else { 84 };
901 decode_timestamp(sector.get(off..off + 12)?)
902}
903
904pub(crate) fn fe_slack_nonzero<R: Read + Seek>(
913 reader: &mut R,
914 block_size: u32,
915 partition_start: u32,
916 fe_lba: u32,
917) -> Option<(u32, u32)> {
918 let info_len = read_fe_info_len(reader, block_size, fe_lba)?;
919 let bs = u64::from(block_size);
920 let slack = (bs - info_len % bs) % bs;
921 if info_len == 0 || slack == 0 {
922 return None;
923 }
924 let last_block_pos = fe_last_block_pos(reader, block_size, partition_start, fe_lba)?;
929 let mut buf = [0u8; MAX_BLOCK_SIZE];
930 let block = &mut buf[..block_size as usize];
931 seek_read_checked(reader, last_block_pos, block).ok()?;
932
933 let slack_start = (info_len % bs) as usize;
934 let nonzero = block[slack_start..].iter().filter(|&&b| b != 0).count() as u32;
935 Some((nonzero, slack as u32))
936}
937
938fn fe_last_block_pos<R: Read + Seek>(
942 reader: &mut R,
943 block_size: u32,
944 partition_start: u32,
945 fe_lba: u32,
946) -> Option<u64> {
947 let mut buf = [0u8; MAX_BLOCK_SIZE];
948 let sector = &mut buf[..block_size as usize];
949 seek_read_checked(reader, u64::from(fe_lba) * u64::from(block_size), sector).ok()?;
950
951 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
952 let is_efe = tag_ident == TAG_EFE;
953 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
954 return None;
955 }
956
957 let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
958 let alloc_type = icb_flags & 0x0007;
959 let (ea_off, ad_off, header) = if is_efe {
962 (208usize, 212usize, 216usize)
963 } else {
964 (168usize, 172usize, 176usize)
965 };
966 if ad_off + 4 > sector.len() {
967 return None; }
969 let ea_len = u32::from_le_bytes(sector[ea_off..ea_off + 4].try_into().ok()?) as usize;
970 let ad_len = u32::from_le_bytes(sector[ad_off..ad_off + 4].try_into().ok()?) as usize;
971 let ad_start = header + ea_len;
972 let ad_end = ad_start.checked_add(ad_len)?;
973 if ad_end > sector.len() {
974 return None;
975 }
976 let ad_area = §or[ad_start..ad_end];
977
978 let stride = match alloc_type {
982 ALLOC_SHORT => 8,
983 ALLOC_LONG => 16,
984 _ => return None,
985 };
986 let mut last: Option<u64> = None;
987 let mut pos = 0;
988 while pos + stride <= ad_area.len() {
989 let len_raw = le_u32(ad_area, pos);
990 let ext_type = len_raw >> 30;
991 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
992 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
993 let lbn = le_u32(ad_area, pos + 4);
994 let blocks_in_ext = ext_len.div_ceil(block_size as usize) as u64;
995 let last_lbn = u64::from(partition_start) + u64::from(lbn) + (blocks_in_ext - 1);
996 last = Some(last_lbn * u64::from(block_size));
997 }
998 pos += stride;
999 }
1000 last
1001}
1002
1003#[cfg(test)]
1004mod real_media_tests {
1005 use super::{parse_udf_state, UdfPartitionKind};
1012 use std::fs::File;
1013
1014 fn state(name: &str) -> Option<super::UdfState> {
1015 let path = format!("{}/tests/data/{}", env!("CARGO_MANIFEST_DIR"), name);
1016 let mut f = File::open(&path).ok()?;
1017 parse_udf_state(&mut f)
1018 }
1019
1020 #[test]
1021 fn vat_image_classified_virtual() {
1022 let Some(st) = state("udf_vat.img") else {
1023 eprintln!("skip: udf_vat.img");
1024 return;
1025 };
1026 assert_eq!(
1027 st.partition_kind,
1028 UdfPartitionKind::Virtual,
1029 "mkudffs cdr/1.50 image must classify as Virtual (VAT)"
1030 );
1031 }
1032
1033 #[test]
1034 fn sparable_image_classified_sparable() {
1035 let Some(st) = state("udf_spar.img") else {
1036 eprintln!("skip: udf_spar.img");
1037 return;
1038 };
1039 assert_eq!(
1040 st.partition_kind,
1041 UdfPartitionKind::Sparable,
1042 "mkudffs dvdrw/2.01 image must classify as Sparable"
1043 );
1044 }
1045
1046 #[test]
1057 fn vat_image_matches_udfinfo_oracle() {
1058 let Some(st) = state("udf_vat.img") else {
1059 eprintln!("skip: udf_vat.img");
1060 return;
1061 };
1062 assert_eq!(st.partition_kind, UdfPartitionKind::Virtual);
1063 assert_eq!(
1065 st.partition_start, 257,
1066 "partition start must match udfinfo PSPACE start=257"
1067 );
1068 assert_eq!(
1070 st.partition_map_count, 2,
1071 "cdr/1.50 carries a physical map plus the VAT Type-2 map"
1072 );
1073 }
1074
1075 #[test]
1081 fn sparable_image_matches_udfinfo_oracle() {
1082 let Some(st) = state("udf_spar.img") else {
1083 eprintln!("skip: udf_spar.img");
1084 return;
1085 };
1086 assert_eq!(st.partition_kind, UdfPartitionKind::Sparable);
1087 assert_eq!(
1089 st.partition_start, 1296,
1090 "partition start must match udfinfo PSPACE start=1296"
1091 );
1092 assert_eq!(
1093 st.partition_map_count, 1,
1094 "dvdrw/2.01 carries a single Sparable Type-2 map"
1095 );
1096 }
1097
1098 #[test]
1104 fn plain_512_block_image_parses_via_detected_block_size() {
1105 let path = format!("{}/tests/data/udf_plain.img", env!("CARGO_MANIFEST_DIR"));
1106 let mut f = File::open(&path).expect("udf_plain.img fixture must be present");
1107 let st = super::parse_udf_state(&mut f)
1108 .expect("512-byte-block UDF must parse once the block size is detected from the AVDP");
1109 assert_eq!(st.block_size, 512, "udfinfo reports blocksize=512");
1110 assert_eq!(
1111 st.partition_kind,
1112 UdfPartitionKind::Physical,
1113 "mkudffs hd image is a Type-1 physical partition"
1114 );
1115 assert_eq!(
1116 st.partition_start, 257,
1117 "partition start must match udfinfo PSPACE start=257"
1118 );
1119 assert_eq!(
1120 st.partition_map_count, 1,
1121 "hd/2.01 carries a single physical map"
1122 );
1123 }
1124}
1125
1126#[cfg(test)]
1127mod checked_bootstrap_tests {
1128 use super::parse_udf_state_checked;
1132 use std::io::{self, Cursor, Read, Seek, SeekFrom};
1133
1134 struct FaultyReader;
1137
1138 impl Read for FaultyReader {
1139 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1140 Err(io::Error::other("device read fault"))
1141 }
1142 }
1143 impl Seek for FaultyReader {
1144 fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
1145 Ok(0)
1146 }
1147 }
1148
1149 #[test]
1150 fn io_error_at_anchor_surfaces_as_err() {
1151 let mut r = FaultyReader;
1152 let res = parse_udf_state_checked(&mut r);
1153 assert!(
1154 res.is_err(),
1155 "a device read fault reading the anchor must surface as Err, not Ok(None)"
1156 );
1157 }
1158
1159 #[test]
1160 fn truncated_before_anchor_surfaces_as_err() {
1161 let buf = vec![0u8; 4096];
1165 let mut r = Cursor::new(buf);
1166 let res = parse_udf_state_checked(&mut r);
1167 assert!(
1168 res.is_err(),
1169 "truncation before the AVDP anchor must surface as Err (UnexpectedEof)"
1170 );
1171 let err = res.err().unwrap();
1172 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1173 }
1174
1175 #[test]
1176 fn full_size_but_wrong_anchor_is_ok_none() {
1177 let buf = vec![0u8; 257 * 2048];
1181 let mut r = Cursor::new(buf);
1182 let res = parse_udf_state_checked(&mut r);
1183 assert!(
1184 matches!(res, Ok(None)),
1185 "a readable image with a non-AVDP anchor must be Ok(None), got {res:?}"
1186 );
1187 }
1188}
1189
1190#[cfg(test)]
1191mod findings_support_tests {
1192 use super::*;
1198 use std::io::Cursor;
1199
1200 #[test]
1201 fn crc_ccitt_matches_known_vectors() {
1202 assert_eq!(ecma167_crc(b"123456789"), 0x31C3);
1205 assert_eq!(ecma167_crc(&[]), 0x0000);
1206 }
1207
1208 #[test]
1209 fn tag_checksum_skips_byte_4() {
1210 let mut tag = [0u8; 16];
1211 tag[0] = 2; tag[4] = 0xFF; tag[6] = 3; assert_eq!(tag_checksum(&tag), 5);
1215 }
1216
1217 #[test]
1218 fn descriptor_label_known_and_unknown() {
1219 for (id, name) in [
1222 (TAG_FSD, "FileSetDescriptor"),
1223 (TAG_EFE, "ExtendedFileEntry"),
1224 (TAG_FID, "FileIdentifierDescriptor"),
1225 (TAG_FE, "FileEntry"),
1226 (TAG_FE_ALT, "FileEntry"),
1227 (1, "PrimaryVolumeDescriptor"),
1228 (3, "VolumeDescriptorPointer"),
1229 (4, "ImplementationUseVolumeDescriptor"),
1230 (7, "UnallocatedSpaceDescriptor"),
1231 (9, "LogicalVolumeIntegrityDescriptor"),
1232 (258, "AllocationExtentDescriptor"),
1233 (259, "IndirectEntry"),
1234 (262, "SpaceBitmapDescriptor"),
1235 (263, "PartitionIntegrityEntry"),
1236 (264, "ExtendedAttributeHeaderDescriptor"),
1237 (265, "UnallocatedSpaceEntry"),
1238 ] {
1239 assert_eq!(descriptor_label(id), Some(name), "tag {id}");
1240 }
1241 assert_eq!(descriptor_label(0xFFFF), None);
1242 }
1243
1244 #[test]
1245 fn fsd_recording_time_none_for_non_fsd() {
1246 let img = vec![0u8; 512];
1247 let mut r = Cursor::new(img);
1248 assert_eq!(fsd_recording_time(&mut r, 512, 0).unwrap(), None);
1249 }
1250
1251 #[test]
1252 fn last_block_pos_none_for_non_file_entry() {
1253 let img = vec![0u8; 512];
1254 let mut r = Cursor::new(img);
1255 assert_eq!(fe_last_block_pos(&mut r, 512, 0, 0), None);
1256 }
1257
1258 #[test]
1259 fn slack_via_long_allocation_descriptor() {
1260 let bs = 512usize;
1262 let mut img = vec![0u8; bs * 8];
1263 let fe = 4 * bs;
1264 img[fe..fe + 2].copy_from_slice(&TAG_FE.to_le_bytes());
1265 img[fe + 34..fe + 36].copy_from_slice(&1u16.to_le_bytes()); img[fe + 56..fe + 64].copy_from_slice(&100u64.to_le_bytes());
1267 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;
1272 img[data] = 0x7F;
1273 let mut r = Cursor::new(img);
1274 let (nonzero, slack) = fe_slack_nonzero(&mut r, 512, 0, 4).expect("slack present");
1275 assert_eq!(slack, 412);
1276 assert_eq!(nonzero, 1);
1277 }
1278
1279 #[test]
1280 fn timestamp_decodes_and_rejects_implausible() {
1281 let mut t = [0u8; 12];
1282 t[2..4].copy_from_slice(&2026i16.to_le_bytes());
1283 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"));
1289
1290 let mut bad = t;
1292 bad[2..4].copy_from_slice(&0i16.to_le_bytes());
1293 assert_eq!(decode_timestamp(&bad), None);
1294
1295 let mut badmon = t;
1297 badmon[4] = 0;
1298 assert_eq!(decode_timestamp(&badmon), None);
1299
1300 assert_eq!(decode_timestamp(&[0u8; 4]), None);
1302 }
1303
1304 #[test]
1305 fn fe_modification_time_offset_differs_for_efe() {
1306 let mut fe = vec![0u8; 512];
1308 let stamp = |buf: &mut [u8], off: usize, year: i16| {
1309 buf[off + 2..off + 4].copy_from_slice(&year.to_le_bytes());
1310 buf[off + 4] = 1; buf[off + 5] = 1; };
1313 stamp(&mut fe, 84, 2030);
1314 assert_eq!(
1315 fe_modification_time(&fe, false).as_deref(),
1316 Some("2030-01-01 00:00:00")
1317 );
1318 let mut efe = vec![0u8; 512];
1319 stamp(&mut efe, 92, 2031);
1320 assert_eq!(
1321 fe_modification_time(&efe, true).as_deref(),
1322 Some("2031-01-01 00:00:00")
1323 );
1324 }
1325
1326 fn image_with_slack(info_len: u64, slack_fill: &[u8]) -> (Vec<u8>, u32, u32) {
1331 let bs = 512usize;
1332 let part_start = 0u32;
1333 let fe_lba = 4u32;
1334 let data_lbn = 5u32; let mut img = vec![0u8; bs * 8];
1336
1337 let fe = fe_lba as usize * bs;
1338 img[fe..fe + 2].copy_from_slice(&TAG_FE.to_le_bytes());
1340 img[fe + 34..fe + 36].copy_from_slice(&0u16.to_le_bytes());
1342 img[fe + 56..fe + 64].copy_from_slice(&info_len.to_le_bytes());
1344 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;
1349 img[ad..ad + 4].copy_from_slice(&(info_len as u32).to_le_bytes());
1350 img[ad + 4..ad + 8].copy_from_slice(&data_lbn.to_le_bytes());
1351
1352 let data = (part_start + data_lbn) as usize * bs;
1354 let slack_start = data + (info_len as usize % bs);
1355 for (i, &b) in slack_fill.iter().enumerate() {
1356 img[slack_start + i] = b;
1357 }
1358 (img, part_start, fe_lba)
1359 }
1360
1361 #[test]
1362 fn slack_counts_nonzero_tail_bytes() {
1363 let (img, ps, fe) = image_with_slack(100, &[0xAA, 0x00, 0xBB, 0xCC]);
1365 let mut r = Cursor::new(img);
1366 let (nonzero, slack) = fe_slack_nonzero(&mut r, 512, ps, fe).expect("slack present");
1367 assert_eq!(slack, 412);
1368 assert_eq!(nonzero, 3);
1369 }
1370
1371 #[test]
1372 fn slack_none_when_block_aligned() {
1373 let (img, ps, fe) = image_with_slack(512, &[0xFF]);
1375 let mut r = Cursor::new(img);
1376 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1377 }
1378
1379 #[test]
1380 fn slack_none_when_zero_length() {
1381 let (img, ps, fe) = image_with_slack(0, &[]);
1382 let mut r = Cursor::new(img);
1383 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1384 }
1385
1386 #[test]
1387 fn slack_none_for_inline_data() {
1388 let (mut img, ps, fe) = image_with_slack(100, &[0xAA]);
1390 let fe_off = fe as usize * 512;
1391 img[fe_off + 34..fe_off + 36].copy_from_slice(&3u16.to_le_bytes());
1392 let mut r = Cursor::new(img);
1393 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1394 }
1395}
1396
1397#[cfg(test)]
1398mod synth_traversal_tests {
1399 use super::*;
1405 use crate::test_support as ts;
1406 use std::io::Cursor;
1407
1408 #[test]
1409 fn read_dir_lists_children_with_decoded_names_and_sizes() {
1410 let mut r = Cursor::new(ts::image());
1411 let st = parse_udf_state(&mut r).expect("synthetic UDF parses");
1412 assert_eq!(st.block_size, 512);
1413 assert_eq!(st.partition_kind, UdfPartitionKind::Physical);
1414
1415 let entries = read_dir_at_lba(&mut r, st.block_size, st.partition_start, st.root_fe_lba)
1416 .expect("root directory reads");
1417 assert_eq!(entries.len(), 6, "entries: {entries:?}");
1419 let by_name = |n: &str| entries.iter().find(|e| e.name == n);
1420
1421 assert!(by_name("sub").expect("sub").is_dir);
1422 assert!(by_name("bd").expect("bd").is_dir);
1423 let inline = by_name("inline.txt").expect("inline.txt");
1424 assert!(!inline.is_dir);
1425 assert_eq!(inline.size, 4);
1426 assert_eq!(
1427 by_name("short.bin").expect("short.bin").size,
1428 ts::SHORT_FILE_LEN
1429 );
1430 assert_eq!(
1431 by_name("long.bin").expect("long.bin").size,
1432 ts::LONG_FILE_LEN
1433 );
1434 assert!(by_name("U").is_some(), "utf-16 name decoded: {entries:?}");
1436 }
1437
1438 #[test]
1439 fn read_fe_data_reads_short_extents() {
1440 let mut r = Cursor::new(ts::image());
1441 let st = parse_udf_state(&mut r).unwrap();
1442 let data = read_fe_data(&mut r, st.block_size, st.partition_start, ts::SHORT_FILE_FE)
1443 .expect("short-extent file data");
1444 assert_eq!(data.len() as u64, ts::SHORT_FILE_LEN);
1445 assert!(data[..512].iter().all(|&b| b == 0x41));
1446 assert!(data[512..].iter().all(|&b| b == 0x42));
1447 }
1448
1449 #[test]
1450 fn read_fe_data_reads_long_extents() {
1451 let mut r = Cursor::new(ts::image());
1452 let st = parse_udf_state(&mut r).unwrap();
1453 let data = read_fe_data(&mut r, st.block_size, st.partition_start, ts::LONG_FILE_FE)
1454 .expect("long-extent file data");
1455 assert_eq!(data.len() as u64, ts::LONG_FILE_LEN);
1456 assert!(data.iter().all(|&b| b == 0x43));
1457 }
1458
1459 #[test]
1460 fn read_fe_data_reads_inline() {
1461 let mut r = Cursor::new(ts::image());
1462 let st = parse_udf_state(&mut r).unwrap();
1463 let data = read_fe_data(
1464 &mut r,
1465 st.block_size,
1466 st.partition_start,
1467 ts::INLINE_FILE_FE,
1468 )
1469 .expect("inline file data");
1470 assert_eq!(data, b"abcd");
1471 }
1472
1473 #[test]
1474 fn read_fe_data_none_for_non_file_entry() {
1475 let mut r = Cursor::new(vec![0u8; 4096]);
1476 assert!(read_fe_data(&mut r, 512, 0, 0).is_none());
1477 }
1478
1479 #[test]
1480 fn read_fe_data_none_for_broken_directory() {
1481 let mut r = Cursor::new(ts::image());
1483 let st = parse_udf_state(&mut r).unwrap();
1484 assert!(
1485 read_fe_data(&mut r, st.block_size, st.partition_start, ts::BROKEN_DIR_FE).is_none()
1486 );
1487 }
1488
1489 #[test]
1490 fn read_fe_data_none_for_unsupported_alloc_type() {
1491 let mut img = vec![0u8; 1024];
1493 img[0..2].copy_from_slice(&TAG_FE.to_le_bytes());
1494 img[34..36].copy_from_slice(&2u16.to_le_bytes()); let mut r = Cursor::new(img);
1496 assert!(read_fe_data(&mut r, 512, 0, 0).is_none());
1497 }
1498
1499 #[test]
1500 fn decode_osta_cs0_utf8_utf16_and_empty() {
1501 assert_eq!(decode_osta_cs0(&[]), "");
1502 assert_eq!(decode_osta_cs0(b"\x08hello"), "hello");
1503 assert_eq!(decode_osta_cs0(&[16, 0x00, 0x41, 0x00, 0x42]), "AB");
1505 }
1506
1507 #[cfg(feature = "vfs")]
1508 #[test]
1509 fn read_fe_file_type_classifies_and_rejects() {
1510 let mut r = Cursor::new(ts::image());
1511 let st = parse_udf_state(&mut r).unwrap();
1512 assert_eq!(
1513 read_fe_file_type(&mut r, st.block_size, ts::SUBDIR_FE),
1514 Some(FILE_TYPE_DIRECTORY)
1515 );
1516 assert_eq!(
1517 read_fe_file_type(&mut r, st.block_size, ts::INLINE_FILE_FE),
1518 Some(5)
1519 );
1520 let mut z = Cursor::new(vec![0u8; 4096]);
1521 assert_eq!(read_fe_file_type(&mut z, 512, 0), None);
1522 }
1523
1524 #[test]
1525 fn parse_fids_skips_padding_and_stops_on_overflow() {
1526 let mut r = Cursor::new(vec![0u8; 64]);
1528 assert!(parse_fids(&mut r, 512, 0, &[0u8; 64]).is_empty());
1529 let mut data = vec![0u8; 40];
1532 data[0..2].copy_from_slice(&TAG_FID.to_le_bytes());
1533 data[10..12].copy_from_slice(&0xFFFFu16.to_le_bytes());
1534 assert!(parse_fids(&mut r, 512, 0, &data).is_empty());
1535 }
1536
1537 #[test]
1538 fn parse_state_none_when_vds_has_no_lvd() {
1539 let mut img = vec![0u8; 512 * 264];
1542 let avdp = 256 * 512;
1543 img[avdp..avdp + 2].copy_from_slice(&2u16.to_le_bytes()); img[avdp + 12..avdp + 16].copy_from_slice(&256u32.to_le_bytes()); img[avdp + 16..avdp + 20].copy_from_slice(&512u32.to_le_bytes()); img[avdp + 20..avdp + 24].copy_from_slice(&260u32.to_le_bytes()); let mut r = Cursor::new(img);
1549 assert!(parse_udf_state_checked(&mut r).unwrap().is_none());
1550 }
1551
1552 #[test]
1553 fn parse_state_none_when_fsd_tag_wrong() {
1554 let mut img = ts::image();
1556 img[3 * 512..3 * 512 + 2].copy_from_slice(&0u16.to_le_bytes());
1557 let mut r = Cursor::new(img);
1558 assert!(parse_udf_state_checked(&mut r).unwrap().is_none());
1559 }
1560
1561 #[test]
1562 fn descriptor_label_terminating() {
1563 assert_eq!(descriptor_label(TAG_TERM), Some("TerminatingDescriptor"));
1564 }
1565
1566 #[test]
1567 fn detect_udf_recognises_nsr_and_stops_on_terminator() {
1568 let mut img = vec![0u8; 20 * 2048];
1570 img[16 * 2048 + 1..16 * 2048 + 6].copy_from_slice(b"NSR03");
1571 assert!(detect_udf(&mut Cursor::new(img)));
1572 let mut tea = vec![0u8; 20 * 2048];
1574 tea[16 * 2048 + 1..16 * 2048 + 6].copy_from_slice(b"TEA01");
1575 assert!(!detect_udf(&mut Cursor::new(tea)));
1576 assert!(!detect_udf(&mut Cursor::new(vec![0u8; 100])));
1578 }
1579
1580 #[test]
1581 fn classify_type2_maps_entity_strings() {
1582 assert_eq!(
1583 classify_type2(b"....*UDF Metadata Partition...."),
1584 UdfPartitionKind::Metadata
1585 );
1586 assert_eq!(
1587 classify_type2(b"....*UDF Virtual Partition...."),
1588 UdfPartitionKind::Virtual
1589 );
1590 assert_eq!(
1591 classify_type2(b"....*UDF Sparable Partition...."),
1592 UdfPartitionKind::Sparable
1593 );
1594 assert_eq!(
1595 classify_type2(b"no entity string"),
1596 UdfPartitionKind::Unknown
1597 );
1598 }
1599
1600 #[test]
1601 fn parse_partition_maps_type1_type2_and_unknown() {
1602 let mut lvd = vec![0u8; 512];
1603 lvd[268..272].copy_from_slice(&3u32.to_le_bytes()); let mut off = 440usize;
1605 lvd[off] = 1;
1607 lvd[off + 1] = 6;
1608 lvd[off + 4..off + 6].copy_from_slice(&2u16.to_le_bytes());
1609 off += 6;
1610 lvd[off] = 2;
1612 lvd[off + 1] = 40;
1613 lvd[off + 4..off + 4 + 23].copy_from_slice(b"*UDF Metadata Partition");
1614 off += 40;
1615 lvd[off] = 9;
1617 lvd[off + 1] = 4;
1618 off += 4;
1619 lvd[264..268].copy_from_slice(&((off - 440) as u32).to_le_bytes()); let maps = parse_partition_maps(&lvd);
1622 assert_eq!(maps.len(), 3);
1623 assert_eq!(maps[0].kind, UdfPartitionKind::Physical);
1624 assert_eq!(maps[0].partition_number, Some(2));
1625 assert_eq!(maps[1].kind, UdfPartitionKind::Metadata);
1626 assert_eq!(maps[2].kind, UdfPartitionKind::Unknown);
1627 }
1628}