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 Ok(Some(VdsInfo {
476 partition_start,
477 fsd_lba: partition_start + fsd,
478 partition_kind: kind,
479 map_count,
480 }))
481}
482
483fn read_fsd_checked<R: Read + Seek>(
487 reader: &mut R,
488 block_size: u32,
489 fsd_lba: u32,
490 partition_start: u32,
491) -> Result<Option<u32>, io::Error> {
492 let mut sector = [0u8; MAX_BLOCK_SIZE];
493 let sector = &mut sector[..block_size as usize];
494 seek_read_checked(reader, u64::from(fsd_lba) * u64::from(block_size), sector)?;
495 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
496 return Ok(None);
497 }
498 let lbn = le_u32(sector, 404);
505 Ok(Some(partition_start + lbn))
506}
507
508fn detect_fid_tag_size(data: &[u8]) -> usize {
516 let mut off = 0;
517 while off + 28 <= data.len() {
518 let ti = u16::from_le_bytes([data[off], data[off + 1]]);
519 if ti == TAG_FID {
520 let lbn16 = if off + 26 <= data.len() {
524 le_u32(data, off + 22)
525 } else {
526 u32::MAX
527 };
528 let lbn18 = if off + 28 <= data.len() {
529 le_u32(data, off + 24)
530 } else {
531 u32::MAX
532 };
533 if lbn16 < 0x10000 {
534 return 16;
535 }
536 if lbn18 < 0x10000 {
537 return 18;
538 }
539 return 16; }
541 off += 4;
542 }
543 16
544}
545
546fn parse_fids<R: Read + Seek>(
548 reader: &mut R,
549 block_size: u32,
550 partition_start: u32,
551 data: &[u8],
552) -> Vec<UdfFileEntry> {
553 let tag_size = detect_fid_tag_size(data);
556 let min_fid = tag_size + 20; let mut entries = Vec::new();
559 let mut off = 0;
560
561 while off + min_fid <= data.len() {
562 let tag_ident = u16::from_le_bytes([data[off], data[off + 1]]);
563 if tag_ident != TAG_FID {
564 off += 4;
566 continue;
567 }
568
569 let crc_len = u16::from_le_bytes([data[off + 10], data[off + 11]]) as usize;
571 let fid_advance = ((16 + crc_len + 3) & !3).max(min_fid);
572 if off + fid_advance > data.len() {
573 break;
574 }
575
576 let file_chars = data[off + tag_size];
577 let file_id_len = data[off + tag_size + 1] as usize;
578 let icb_lbn = if off + tag_size + 10 <= data.len() {
580 le_u32(data, off + tag_size + 6)
581 } else {
582 off += fid_advance.max(4);
584 continue;
585 };
586 let impl_use_len = if off + tag_size + 20 <= data.len() {
587 u16::from_le_bytes([data[off + tag_size + 18], data[off + tag_size + 19]]) as usize
588 } else {
589 off += fid_advance.max(4);
591 continue;
592 };
593
594 if file_chars & FC_PARENT == 0 {
595 let is_dir = file_chars & FC_DIRECTORY != 0;
596 let fe_lba = partition_start + icb_lbn;
597
598 let id_start = off + tag_size + 20 + impl_use_len;
599 let id_end = (id_start + file_id_len).min(data.len());
600 let name = if id_end > id_start {
601 decode_osta_cs0(&data[id_start..id_end])
602 } else {
603 String::new()
604 };
605
606 let size = read_fe_info_len(reader, block_size, fe_lba).unwrap_or(0);
608
609 entries.push(UdfFileEntry {
610 name,
611 is_dir,
612 size,
613 fe_lba,
614 });
615 }
616
617 off += fid_advance.max(4);
618 }
619 entries
620}
621
622fn read_fe_info_len<R: Read + Seek>(reader: &mut R, block_size: u32, fe_lba: u32) -> Option<u64> {
624 let mut sector = [0u8; MAX_BLOCK_SIZE];
625 let sector = &mut sector[..block_size as usize];
626 seek_read(reader, u64::from(fe_lba) * u64::from(block_size), sector)?;
627 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
628 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
629 return None;
630 }
631 Some(le_u64(sector, 56))
632}
633
634#[cfg(feature = "vfs")]
642pub(crate) fn read_fe_file_type<R: Read + Seek>(
643 reader: &mut R,
644 block_size: u32,
645 fe_lba: u32,
646) -> Option<u8> {
647 let mut sector = [0u8; MAX_BLOCK_SIZE];
648 let sector = &mut sector[..block_size as usize];
649 seek_read(reader, u64::from(fe_lba) * u64::from(block_size), sector)?;
650 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
651 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
652 return None;
653 }
654 sector.get(27).copied()
655}
656
657#[cfg(feature = "vfs")]
659pub(crate) const FILE_TYPE_DIRECTORY: u8 = 4;
660
661fn read_extents_short<R: Read + Seek>(
663 reader: &mut R,
664 block_size: u32,
665 partition_start: u32,
666 ad_area: &[u8],
667 total_len: u64,
668) -> Option<Vec<u8>> {
669 let mut data = Vec::new();
670 let mut pos = 0;
671 while pos + 8 <= ad_area.len() && (data.len() as u64) < total_len {
672 let len_raw = le_u32(ad_area, pos);
673 let ext_pos = le_u32(ad_area, pos + 4);
674 let ext_type = len_raw >> 30;
675 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
676 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
677 let phys = (u64::from(partition_start) + u64::from(ext_pos)) * u64::from(block_size);
678 read_extent(reader, block_size, phys, ext_len, total_len, &mut data)?;
679 }
680 pos += 8;
681 }
682 data.truncate(total_len as usize);
683 Some(data)
684}
685
686fn read_extents_long<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 + 16 <= ad_area.len() && (data.len() as u64) < total_len {
697 let len_raw = le_u32(ad_area, pos);
698 let lbn = 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(lbn)) * u64::from(block_size);
703 read_extent(reader, block_size, phys, ext_len, total_len, &mut data)?;
704 }
705 pos += 16;
706 }
707 data.truncate(total_len as usize);
708 Some(data)
709}
710
711fn read_extent<R: Read + Seek>(
713 reader: &mut R,
714 block_size: u32,
715 byte_pos: u64,
716 ext_len: usize,
717 total_len: u64,
718 data: &mut Vec<u8>,
719) -> Option<()> {
720 let bs = block_size as usize;
721 let sectors = ext_len.div_ceil(bs);
722 for i in 0..sectors {
723 let mut sector = [0u8; MAX_BLOCK_SIZE];
724 let sector = &mut sector[..bs];
725 seek_read(reader, byte_pos + i as u64 * u64::from(block_size), sector)?;
726 let already = data.len() as u64;
727 let remaining = total_len.saturating_sub(already) as usize;
728 let sector_bytes = (ext_len - i * bs).min(bs);
729 let take = sector_bytes.min(remaining);
730 data.extend_from_slice(§or[..take]);
731 }
732 Some(())
733}
734
735fn decode_osta_cs0(bytes: &[u8]) -> String {
738 if bytes.is_empty() {
739 return String::new();
740 }
741 let comp_id = bytes[0];
742 let payload = &bytes[1..];
743 if comp_id == 16 {
746 let pairs: Vec<u16> = payload
747 .chunks_exact(2)
748 .map(|c| u16::from_be_bytes([c[0], c[1]]))
749 .collect();
750 String::from_utf16_lossy(&pairs)
751 } else {
752 String::from_utf8_lossy(payload).into_owned()
753 }
754}
755
756fn seek_read<R: Read + Seek>(reader: &mut R, byte_pos: u64, buf: &mut [u8]) -> Option<()> {
758 seek_read_checked(reader, byte_pos, buf).ok()
759}
760
761fn seek_read_checked<R: Read + Seek>(
764 reader: &mut R,
765 byte_pos: u64,
766 buf: &mut [u8],
767) -> Result<(), io::Error> {
768 reader.seek(SeekFrom::Start(byte_pos))?;
769 reader.read_exact(buf)?;
770 Ok(())
771}
772
773pub(crate) fn tag_checksum(tag: &[u8]) -> u8 {
778 let mut sum: u32 = 0;
779 for (i, &b) in tag.iter().take(16).enumerate() {
780 if i == 4 {
781 continue;
782 }
783 sum = sum.wrapping_add(u32::from(b));
784 }
785 (sum & 0xFF) as u8
786}
787
788pub(crate) fn ecma167_crc(body: &[u8]) -> u16 {
792 let mut crc: u16 = 0;
793 for &b in body {
794 crc ^= u16::from(b) << 8;
795 for _ in 0..8 {
796 if crc & 0x8000 != 0 {
797 crc = (crc << 1) ^ 0x1021;
798 } else {
799 crc <<= 1;
800 }
801 }
802 }
803 crc
804}
805
806pub(crate) fn descriptor_label(tag_ident: u16) -> Option<&'static str> {
810 Some(match tag_ident {
811 TAG_AVDP => "AVDP",
812 TAG_PD => "PartitionDescriptor",
813 TAG_LVD => "LogicalVolumeDescriptor",
814 TAG_TERM => "TerminatingDescriptor",
815 TAG_FSD => "FileSetDescriptor",
816 TAG_FID => "FileIdentifierDescriptor",
817 TAG_FE | TAG_FE_ALT => "FileEntry",
818 TAG_EFE => "ExtendedFileEntry",
819 1 => "PrimaryVolumeDescriptor",
820 3 => "VolumeDescriptorPointer",
821 4 => "ImplementationUseVolumeDescriptor",
822 7 => "UnallocatedSpaceDescriptor",
823 9 => "LogicalVolumeIntegrityDescriptor",
824 258 => "AllocationExtentDescriptor",
825 259 => "IndirectEntry",
826 262 => "SpaceBitmapDescriptor",
827 263 => "PartitionIntegrityEntry",
828 264 => "ExtendedAttributeHeaderDescriptor",
829 265 => "UnallocatedSpaceEntry",
830 _ => return None,
831 })
832}
833
834pub(crate) fn decode_timestamp(b: &[u8]) -> Option<String> {
838 if b.len() < 12 {
839 return None;
840 }
841 let year = i16::from_le_bytes([b[2], b[3]]);
842 if !(1970..=2200).contains(&year) {
843 return None;
844 }
845 let (month, day, hour, minute, second) = (b[4], b[5], b[6], b[7], b[8]);
846 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
847 return None;
848 }
849 Some(format!(
850 "{year:04}-{month:02}-{day:02} {hour:02}:{minute:02}:{second:02}"
851 ))
852}
853
854pub(crate) fn fsd_recording_time<R: Read + Seek>(
858 reader: &mut R,
859 block_size: u32,
860 fsd_lba: u32,
861) -> Result<Option<String>, io::Error> {
862 let mut buf = [0u8; MAX_BLOCK_SIZE];
863 let sector = &mut buf[..block_size as usize];
864 seek_read_checked(reader, u64::from(fsd_lba) * u64::from(block_size), sector)?;
865 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
866 return Ok(None);
867 }
868 Ok(decode_timestamp(§or[16..28]))
869}
870
871pub(crate) fn fe_modification_time(sector: &[u8], is_efe: bool) -> Option<String> {
875 let off = if is_efe { 92 } else { 84 };
876 decode_timestamp(sector.get(off..off + 12)?)
877}
878
879pub(crate) fn fe_slack_nonzero<R: Read + Seek>(
888 reader: &mut R,
889 block_size: u32,
890 partition_start: u32,
891 fe_lba: u32,
892) -> Option<(u32, u32)> {
893 let info_len = read_fe_info_len(reader, block_size, fe_lba)?;
894 let bs = u64::from(block_size);
895 let slack = (bs - info_len % bs) % bs;
896 if info_len == 0 || slack == 0 {
897 return None;
898 }
899 let last_block_pos = fe_last_block_pos(reader, block_size, partition_start, fe_lba)?;
904 let mut buf = [0u8; MAX_BLOCK_SIZE];
905 let block = &mut buf[..block_size as usize];
906 seek_read_checked(reader, last_block_pos, block).ok()?;
907
908 let slack_start = (info_len % bs) as usize;
909 let nonzero = block[slack_start..].iter().filter(|&&b| b != 0).count() as u32;
910 Some((nonzero, slack as u32))
911}
912
913fn fe_last_block_pos<R: Read + Seek>(
917 reader: &mut R,
918 block_size: u32,
919 partition_start: u32,
920 fe_lba: u32,
921) -> Option<u64> {
922 let mut buf = [0u8; MAX_BLOCK_SIZE];
923 let sector = &mut buf[..block_size as usize];
924 seek_read_checked(reader, u64::from(fe_lba) * u64::from(block_size), sector).ok()?;
925
926 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
927 let is_efe = tag_ident == TAG_EFE;
928 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
929 return None;
930 }
931
932 let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
933 let alloc_type = icb_flags & 0x0007;
934 let (ea_off, ad_off, header) = if is_efe {
937 (208usize, 212usize, 216usize)
938 } else {
939 (168usize, 172usize, 176usize)
940 };
941 if ad_off + 4 > sector.len() {
942 return None; }
944 let ea_len = u32::from_le_bytes(sector[ea_off..ea_off + 4].try_into().ok()?) as usize;
945 let ad_len = u32::from_le_bytes(sector[ad_off..ad_off + 4].try_into().ok()?) as usize;
946 let ad_start = header + ea_len;
947 let ad_end = ad_start.checked_add(ad_len)?;
948 if ad_end > sector.len() {
949 return None;
950 }
951 let ad_area = §or[ad_start..ad_end];
952
953 let stride = match alloc_type {
957 ALLOC_SHORT => 8,
958 ALLOC_LONG => 16,
959 _ => return None,
960 };
961 let mut last: Option<u64> = None;
962 let mut pos = 0;
963 while pos + stride <= ad_area.len() {
964 let len_raw = le_u32(ad_area, pos);
965 let ext_type = len_raw >> 30;
966 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
967 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
968 let lbn = le_u32(ad_area, pos + 4);
969 let blocks_in_ext = ext_len.div_ceil(block_size as usize) as u64;
970 let last_lbn = u64::from(partition_start) + u64::from(lbn) + (blocks_in_ext - 1);
971 last = Some(last_lbn * u64::from(block_size));
972 }
973 pos += stride;
974 }
975 last
976}
977
978#[cfg(test)]
979mod real_media_tests {
980 use super::{parse_udf_state, UdfPartitionKind};
987 use std::fs::File;
988
989 fn state(name: &str) -> Option<super::UdfState> {
990 let path = format!("{}/tests/data/{}", env!("CARGO_MANIFEST_DIR"), name);
991 let mut f = File::open(&path).ok()?;
992 parse_udf_state(&mut f)
993 }
994
995 #[test]
996 fn vat_image_classified_virtual() {
997 let Some(st) = state("udf_vat.img") else {
998 eprintln!("skip: udf_vat.img");
999 return;
1000 };
1001 assert_eq!(
1002 st.partition_kind,
1003 UdfPartitionKind::Virtual,
1004 "mkudffs cdr/1.50 image must classify as Virtual (VAT)"
1005 );
1006 }
1007
1008 #[test]
1009 fn sparable_image_classified_sparable() {
1010 let Some(st) = state("udf_spar.img") else {
1011 eprintln!("skip: udf_spar.img");
1012 return;
1013 };
1014 assert_eq!(
1015 st.partition_kind,
1016 UdfPartitionKind::Sparable,
1017 "mkudffs dvdrw/2.01 image must classify as Sparable"
1018 );
1019 }
1020
1021 #[test]
1032 fn vat_image_matches_udfinfo_oracle() {
1033 let Some(st) = state("udf_vat.img") else {
1034 eprintln!("skip: udf_vat.img");
1035 return;
1036 };
1037 assert_eq!(st.partition_kind, UdfPartitionKind::Virtual);
1038 assert_eq!(
1040 st.partition_start, 257,
1041 "partition start must match udfinfo PSPACE start=257"
1042 );
1043 assert_eq!(
1045 st.partition_map_count, 2,
1046 "cdr/1.50 carries a physical map plus the VAT Type-2 map"
1047 );
1048 }
1049
1050 #[test]
1056 fn sparable_image_matches_udfinfo_oracle() {
1057 let Some(st) = state("udf_spar.img") else {
1058 eprintln!("skip: udf_spar.img");
1059 return;
1060 };
1061 assert_eq!(st.partition_kind, UdfPartitionKind::Sparable);
1062 assert_eq!(
1064 st.partition_start, 1296,
1065 "partition start must match udfinfo PSPACE start=1296"
1066 );
1067 assert_eq!(
1068 st.partition_map_count, 1,
1069 "dvdrw/2.01 carries a single Sparable Type-2 map"
1070 );
1071 }
1072
1073 #[test]
1079 fn plain_512_block_image_parses_via_detected_block_size() {
1080 let path = format!("{}/tests/data/udf_plain.img", env!("CARGO_MANIFEST_DIR"));
1081 let mut f = File::open(&path).expect("udf_plain.img fixture must be present");
1082 let st = super::parse_udf_state(&mut f)
1083 .expect("512-byte-block UDF must parse once the block size is detected from the AVDP");
1084 assert_eq!(st.block_size, 512, "udfinfo reports blocksize=512");
1085 assert_eq!(
1086 st.partition_kind,
1087 UdfPartitionKind::Physical,
1088 "mkudffs hd image is a Type-1 physical partition"
1089 );
1090 assert_eq!(
1091 st.partition_start, 257,
1092 "partition start must match udfinfo PSPACE start=257"
1093 );
1094 assert_eq!(
1095 st.partition_map_count, 1,
1096 "hd/2.01 carries a single physical map"
1097 );
1098 }
1099}
1100
1101#[cfg(test)]
1102mod checked_bootstrap_tests {
1103 use super::parse_udf_state_checked;
1107 use std::io::{self, Cursor, Read, Seek, SeekFrom};
1108
1109 struct FaultyReader;
1112
1113 impl Read for FaultyReader {
1114 fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1115 Err(io::Error::other("device read fault"))
1116 }
1117 }
1118 impl Seek for FaultyReader {
1119 fn seek(&mut self, _pos: SeekFrom) -> io::Result<u64> {
1120 Ok(0)
1121 }
1122 }
1123
1124 #[test]
1125 fn io_error_at_anchor_surfaces_as_err() {
1126 let mut r = FaultyReader;
1127 let res = parse_udf_state_checked(&mut r);
1128 assert!(
1129 res.is_err(),
1130 "a device read fault reading the anchor must surface as Err, not Ok(None)"
1131 );
1132 }
1133
1134 #[test]
1135 fn truncated_before_anchor_surfaces_as_err() {
1136 let buf = vec![0u8; 4096];
1140 let mut r = Cursor::new(buf);
1141 let res = parse_udf_state_checked(&mut r);
1142 assert!(
1143 res.is_err(),
1144 "truncation before the AVDP anchor must surface as Err (UnexpectedEof)"
1145 );
1146 let err = res.err().unwrap();
1147 assert_eq!(err.kind(), io::ErrorKind::UnexpectedEof);
1148 }
1149
1150 #[test]
1151 fn full_size_but_wrong_anchor_is_ok_none() {
1152 let buf = vec![0u8; 257 * 2048];
1156 let mut r = Cursor::new(buf);
1157 let res = parse_udf_state_checked(&mut r);
1158 assert!(
1159 matches!(res, Ok(None)),
1160 "a readable image with a non-AVDP anchor must be Ok(None), got {res:?}"
1161 );
1162 }
1163}
1164
1165#[cfg(test)]
1166mod findings_support_tests {
1167 use super::*;
1173 use std::io::Cursor;
1174
1175 #[test]
1176 fn crc_ccitt_matches_known_vectors() {
1177 assert_eq!(ecma167_crc(b"123456789"), 0x31C3);
1180 assert_eq!(ecma167_crc(&[]), 0x0000);
1181 }
1182
1183 #[test]
1184 fn tag_checksum_skips_byte_4() {
1185 let mut tag = [0u8; 16];
1186 tag[0] = 2; tag[4] = 0xFF; tag[6] = 3; assert_eq!(tag_checksum(&tag), 5);
1190 }
1191
1192 #[test]
1193 fn descriptor_label_known_and_unknown() {
1194 for (id, name) in [
1197 (TAG_FSD, "FileSetDescriptor"),
1198 (TAG_EFE, "ExtendedFileEntry"),
1199 (TAG_FID, "FileIdentifierDescriptor"),
1200 (TAG_FE, "FileEntry"),
1201 (TAG_FE_ALT, "FileEntry"),
1202 (1, "PrimaryVolumeDescriptor"),
1203 (3, "VolumeDescriptorPointer"),
1204 (4, "ImplementationUseVolumeDescriptor"),
1205 (7, "UnallocatedSpaceDescriptor"),
1206 (9, "LogicalVolumeIntegrityDescriptor"),
1207 (258, "AllocationExtentDescriptor"),
1208 (259, "IndirectEntry"),
1209 (262, "SpaceBitmapDescriptor"),
1210 (263, "PartitionIntegrityEntry"),
1211 (264, "ExtendedAttributeHeaderDescriptor"),
1212 (265, "UnallocatedSpaceEntry"),
1213 ] {
1214 assert_eq!(descriptor_label(id), Some(name), "tag {id}");
1215 }
1216 assert_eq!(descriptor_label(0xFFFF), None);
1217 }
1218
1219 #[test]
1220 fn fsd_recording_time_none_for_non_fsd() {
1221 let img = vec![0u8; 512];
1222 let mut r = Cursor::new(img);
1223 assert_eq!(fsd_recording_time(&mut r, 512, 0).unwrap(), None);
1224 }
1225
1226 #[test]
1227 fn last_block_pos_none_for_non_file_entry() {
1228 let img = vec![0u8; 512];
1229 let mut r = Cursor::new(img);
1230 assert_eq!(fe_last_block_pos(&mut r, 512, 0, 0), None);
1231 }
1232
1233 #[test]
1234 fn slack_via_long_allocation_descriptor() {
1235 let bs = 512usize;
1237 let mut img = vec![0u8; bs * 8];
1238 let fe = 4 * bs;
1239 img[fe..fe + 2].copy_from_slice(&TAG_FE.to_le_bytes());
1240 img[fe + 34..fe + 36].copy_from_slice(&1u16.to_le_bytes()); img[fe + 56..fe + 64].copy_from_slice(&100u64.to_le_bytes());
1242 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;
1247 img[data] = 0x7F;
1248 let mut r = Cursor::new(img);
1249 let (nonzero, slack) = fe_slack_nonzero(&mut r, 512, 0, 4).expect("slack present");
1250 assert_eq!(slack, 412);
1251 assert_eq!(nonzero, 1);
1252 }
1253
1254 #[test]
1255 fn timestamp_decodes_and_rejects_implausible() {
1256 let mut t = [0u8; 12];
1257 t[2..4].copy_from_slice(&2026i16.to_le_bytes());
1258 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"));
1264
1265 let mut bad = t;
1267 bad[2..4].copy_from_slice(&0i16.to_le_bytes());
1268 assert_eq!(decode_timestamp(&bad), None);
1269
1270 let mut badmon = t;
1272 badmon[4] = 0;
1273 assert_eq!(decode_timestamp(&badmon), None);
1274
1275 assert_eq!(decode_timestamp(&[0u8; 4]), None);
1277 }
1278
1279 #[test]
1280 fn fe_modification_time_offset_differs_for_efe() {
1281 let mut fe = vec![0u8; 512];
1283 let stamp = |buf: &mut [u8], off: usize, year: i16| {
1284 buf[off + 2..off + 4].copy_from_slice(&year.to_le_bytes());
1285 buf[off + 4] = 1; buf[off + 5] = 1; };
1288 stamp(&mut fe, 84, 2030);
1289 assert_eq!(
1290 fe_modification_time(&fe, false).as_deref(),
1291 Some("2030-01-01 00:00:00")
1292 );
1293 let mut efe = vec![0u8; 512];
1294 stamp(&mut efe, 92, 2031);
1295 assert_eq!(
1296 fe_modification_time(&efe, true).as_deref(),
1297 Some("2031-01-01 00:00:00")
1298 );
1299 }
1300
1301 fn image_with_slack(info_len: u64, slack_fill: &[u8]) -> (Vec<u8>, u32, u32) {
1306 let bs = 512usize;
1307 let part_start = 0u32;
1308 let fe_lba = 4u32;
1309 let data_lbn = 5u32; let mut img = vec![0u8; bs * 8];
1311
1312 let fe = fe_lba as usize * bs;
1313 img[fe..fe + 2].copy_from_slice(&TAG_FE.to_le_bytes());
1315 img[fe + 34..fe + 36].copy_from_slice(&0u16.to_le_bytes());
1317 img[fe + 56..fe + 64].copy_from_slice(&info_len.to_le_bytes());
1319 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;
1324 img[ad..ad + 4].copy_from_slice(&(info_len as u32).to_le_bytes());
1325 img[ad + 4..ad + 8].copy_from_slice(&data_lbn.to_le_bytes());
1326
1327 let data = (part_start + data_lbn) as usize * bs;
1329 let slack_start = data + (info_len as usize % bs);
1330 for (i, &b) in slack_fill.iter().enumerate() {
1331 img[slack_start + i] = b;
1332 }
1333 (img, part_start, fe_lba)
1334 }
1335
1336 #[test]
1337 fn slack_counts_nonzero_tail_bytes() {
1338 let (img, ps, fe) = image_with_slack(100, &[0xAA, 0x00, 0xBB, 0xCC]);
1340 let mut r = Cursor::new(img);
1341 let (nonzero, slack) = fe_slack_nonzero(&mut r, 512, ps, fe).expect("slack present");
1342 assert_eq!(slack, 412);
1343 assert_eq!(nonzero, 3);
1344 }
1345
1346 #[test]
1347 fn slack_none_when_block_aligned() {
1348 let (img, ps, fe) = image_with_slack(512, &[0xFF]);
1350 let mut r = Cursor::new(img);
1351 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1352 }
1353
1354 #[test]
1355 fn slack_none_when_zero_length() {
1356 let (img, ps, fe) = image_with_slack(0, &[]);
1357 let mut r = Cursor::new(img);
1358 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1359 }
1360
1361 #[test]
1362 fn slack_none_for_inline_data() {
1363 let (mut img, ps, fe) = image_with_slack(100, &[0xAA]);
1365 let fe_off = fe as usize * 512;
1366 img[fe_off + 34..fe_off + 36].copy_from_slice(&3u16.to_le_bytes());
1367 let mut r = Cursor::new(img);
1368 assert_eq!(fe_slack_nonzero(&mut r, 512, ps, fe), None);
1369 }
1370}
1371
1372#[cfg(test)]
1373mod synth_traversal_tests {
1374 use super::*;
1380 use crate::test_support as ts;
1381 use std::io::Cursor;
1382
1383 #[test]
1384 fn read_dir_lists_children_with_decoded_names_and_sizes() {
1385 let mut r = Cursor::new(ts::image());
1386 let st = parse_udf_state(&mut r).expect("synthetic UDF parses");
1387 assert_eq!(st.block_size, 512);
1388 assert_eq!(st.partition_kind, UdfPartitionKind::Physical);
1389
1390 let entries = read_dir_at_lba(&mut r, st.block_size, st.partition_start, st.root_fe_lba)
1391 .expect("root directory reads");
1392 assert_eq!(entries.len(), 6, "entries: {entries:?}");
1394 let by_name = |n: &str| entries.iter().find(|e| e.name == n);
1395
1396 assert!(by_name("sub").expect("sub").is_dir);
1397 assert!(by_name("bd").expect("bd").is_dir);
1398 let inline = by_name("inline.txt").expect("inline.txt");
1399 assert!(!inline.is_dir);
1400 assert_eq!(inline.size, 4);
1401 assert_eq!(
1402 by_name("short.bin").expect("short.bin").size,
1403 ts::SHORT_FILE_LEN
1404 );
1405 assert_eq!(
1406 by_name("long.bin").expect("long.bin").size,
1407 ts::LONG_FILE_LEN
1408 );
1409 assert!(by_name("U").is_some(), "utf-16 name decoded: {entries:?}");
1411 }
1412
1413 #[test]
1414 fn read_fe_data_reads_short_extents() {
1415 let mut r = Cursor::new(ts::image());
1416 let st = parse_udf_state(&mut r).unwrap();
1417 let data = read_fe_data(&mut r, st.block_size, st.partition_start, ts::SHORT_FILE_FE)
1418 .expect("short-extent file data");
1419 assert_eq!(data.len() as u64, ts::SHORT_FILE_LEN);
1420 assert!(data[..512].iter().all(|&b| b == 0x41));
1421 assert!(data[512..].iter().all(|&b| b == 0x42));
1422 }
1423
1424 #[test]
1425 fn read_fe_data_reads_long_extents() {
1426 let mut r = Cursor::new(ts::image());
1427 let st = parse_udf_state(&mut r).unwrap();
1428 let data = read_fe_data(&mut r, st.block_size, st.partition_start, ts::LONG_FILE_FE)
1429 .expect("long-extent file data");
1430 assert_eq!(data.len() as u64, ts::LONG_FILE_LEN);
1431 assert!(data.iter().all(|&b| b == 0x43));
1432 }
1433
1434 #[test]
1435 fn read_fe_data_reads_inline() {
1436 let mut r = Cursor::new(ts::image());
1437 let st = parse_udf_state(&mut r).unwrap();
1438 let data = read_fe_data(
1439 &mut r,
1440 st.block_size,
1441 st.partition_start,
1442 ts::INLINE_FILE_FE,
1443 )
1444 .expect("inline file data");
1445 assert_eq!(data, b"abcd");
1446 }
1447
1448 #[test]
1449 fn read_fe_data_none_for_non_file_entry() {
1450 let mut r = Cursor::new(vec![0u8; 4096]);
1451 assert!(read_fe_data(&mut r, 512, 0, 0).is_none());
1452 }
1453
1454 #[test]
1455 fn read_fe_data_none_for_broken_directory() {
1456 let mut r = Cursor::new(ts::image());
1458 let st = parse_udf_state(&mut r).unwrap();
1459 assert!(
1460 read_fe_data(&mut r, st.block_size, st.partition_start, ts::BROKEN_DIR_FE).is_none()
1461 );
1462 }
1463
1464 #[test]
1465 fn read_fe_data_none_for_unsupported_alloc_type() {
1466 let mut img = vec![0u8; 1024];
1468 img[0..2].copy_from_slice(&TAG_FE.to_le_bytes());
1469 img[34..36].copy_from_slice(&2u16.to_le_bytes()); let mut r = Cursor::new(img);
1471 assert!(read_fe_data(&mut r, 512, 0, 0).is_none());
1472 }
1473
1474 #[test]
1475 fn decode_osta_cs0_utf8_utf16_and_empty() {
1476 assert_eq!(decode_osta_cs0(&[]), "");
1477 assert_eq!(decode_osta_cs0(b"\x08hello"), "hello");
1478 assert_eq!(decode_osta_cs0(&[16, 0x00, 0x41, 0x00, 0x42]), "AB");
1480 }
1481
1482 #[cfg(feature = "vfs")]
1483 #[test]
1484 fn read_fe_file_type_classifies_and_rejects() {
1485 let mut r = Cursor::new(ts::image());
1486 let st = parse_udf_state(&mut r).unwrap();
1487 assert_eq!(
1488 read_fe_file_type(&mut r, st.block_size, ts::SUBDIR_FE),
1489 Some(FILE_TYPE_DIRECTORY)
1490 );
1491 assert_eq!(
1492 read_fe_file_type(&mut r, st.block_size, ts::INLINE_FILE_FE),
1493 Some(5)
1494 );
1495 let mut z = Cursor::new(vec![0u8; 4096]);
1496 assert_eq!(read_fe_file_type(&mut z, 512, 0), None);
1497 }
1498
1499 #[test]
1500 fn parse_fids_skips_padding_and_stops_on_overflow() {
1501 let mut r = Cursor::new(vec![0u8; 64]);
1503 assert!(parse_fids(&mut r, 512, 0, &[0u8; 64]).is_empty());
1504 let mut data = vec![0u8; 40];
1507 data[0..2].copy_from_slice(&TAG_FID.to_le_bytes());
1508 data[10..12].copy_from_slice(&0xFFFFu16.to_le_bytes());
1509 assert!(parse_fids(&mut r, 512, 0, &data).is_empty());
1510 }
1511
1512 #[test]
1513 fn parse_state_none_when_vds_has_no_lvd() {
1514 let mut img = vec![0u8; 512 * 264];
1517 let avdp = 256 * 512;
1518 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);
1524 assert!(parse_udf_state_checked(&mut r).unwrap().is_none());
1525 }
1526
1527 #[test]
1528 fn parse_state_none_when_fsd_tag_wrong() {
1529 let mut img = ts::image();
1531 img[3 * 512..3 * 512 + 2].copy_from_slice(&0u16.to_le_bytes());
1532 let mut r = Cursor::new(img);
1533 assert!(parse_udf_state_checked(&mut r).unwrap().is_none());
1534 }
1535
1536 #[test]
1537 fn descriptor_label_terminating() {
1538 assert_eq!(descriptor_label(TAG_TERM), Some("TerminatingDescriptor"));
1539 }
1540
1541 #[test]
1542 fn detect_udf_recognises_nsr_and_stops_on_terminator() {
1543 let mut img = vec![0u8; 20 * 2048];
1545 img[16 * 2048 + 1..16 * 2048 + 6].copy_from_slice(b"NSR03");
1546 assert!(detect_udf(&mut Cursor::new(img)));
1547 let mut tea = vec![0u8; 20 * 2048];
1549 tea[16 * 2048 + 1..16 * 2048 + 6].copy_from_slice(b"TEA01");
1550 assert!(!detect_udf(&mut Cursor::new(tea)));
1551 assert!(!detect_udf(&mut Cursor::new(vec![0u8; 100])));
1553 }
1554
1555 #[test]
1556 fn classify_type2_maps_entity_strings() {
1557 assert_eq!(
1558 classify_type2(b"....*UDF Metadata Partition...."),
1559 UdfPartitionKind::Metadata
1560 );
1561 assert_eq!(
1562 classify_type2(b"....*UDF Virtual Partition...."),
1563 UdfPartitionKind::Virtual
1564 );
1565 assert_eq!(
1566 classify_type2(b"....*UDF Sparable Partition...."),
1567 UdfPartitionKind::Sparable
1568 );
1569 assert_eq!(
1570 classify_type2(b"no entity string"),
1571 UdfPartitionKind::Unknown
1572 );
1573 }
1574
1575 #[test]
1576 fn parse_partition_maps_type1_type2_and_unknown() {
1577 let mut lvd = vec![0u8; 512];
1578 lvd[268..272].copy_from_slice(&3u32.to_le_bytes()); let mut off = 440usize;
1580 lvd[off] = 1;
1582 lvd[off + 1] = 6;
1583 lvd[off + 4..off + 6].copy_from_slice(&2u16.to_le_bytes());
1584 off += 6;
1585 lvd[off] = 2;
1587 lvd[off + 1] = 40;
1588 lvd[off + 4..off + 4 + 23].copy_from_slice(b"*UDF Metadata Partition");
1589 off += 40;
1590 lvd[off] = 9;
1592 lvd[off + 1] = 4;
1593 off += 4;
1594 lvd[264..268].copy_from_slice(&((off - 440) as u32).to_le_bytes()); let maps = parse_partition_maps(&lvd);
1597 assert_eq!(maps.len(), 3);
1598 assert_eq!(maps[0].kind, UdfPartitionKind::Physical);
1599 assert_eq!(maps[0].partition_number, Some(2));
1600 assert_eq!(maps[1].kind, UdfPartitionKind::Metadata);
1601 assert_eq!(maps[2].kind, UdfPartitionKind::Unknown);
1602 }
1603}