1use std::io::{Read, Seek, SeekFrom};
20
21const TAG_AVDP: u16 = 2;
24const TAG_PD: u16 = 5;
25const TAG_LVD: u16 = 6;
26const TAG_TERM: u16 = 8;
27const TAG_FSD: u16 = 256;
28const TAG_FID: u16 = 257;
29const TAG_FE: u16 = 260;
30const TAG_FE_ALT: u16 = 261;
32const TAG_EFE: u16 = 266;
33
34const FC_DIRECTORY: u8 = 0x02;
36const FC_PARENT: u8 = 0x08;
37
38const ALLOC_SHORT: u16 = 0;
40const ALLOC_LONG: u16 = 1;
41const ALLOC_INLINE: u16 = 3;
42
43const EXTENT_RECORDED: u32 = 0x0000_0000; #[derive(Debug, Clone)]
50pub struct UdfFileEntry {
51 pub name: String,
53 pub is_dir: bool,
55 pub size: u64,
57 pub fe_lba: u32,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
72pub enum UdfPartitionKind {
73 Physical,
75 Virtual,
77 Sparable,
79 Metadata,
81 Unknown,
83}
84
85pub struct UdfState {
88 pub partition_start: u32,
89 pub root_fe_lba: u32,
90 pub partition_kind: UdfPartitionKind,
91 pub partition_map_count: u32,
92}
93
94pub fn detect_udf<R: Read + Seek>(reader: &mut R) -> bool {
100 let mut buf = [0u8; 6];
101 for lba in 16u64..32 {
102 let pos = lba * 2048 + 1;
103 if reader.seek(SeekFrom::Start(pos)).is_err() {
104 break;
105 }
106 if reader.read_exact(&mut buf).is_err() {
107 break;
108 }
109 let id = &buf[..5];
110 if id == b"NSR02" || id == b"NSR03" {
111 return true;
112 }
113 if id == b"TEA01" {
114 break;
115 }
116 }
117 false
118}
119
120pub fn parse_udf_state<R: Read + Seek>(reader: &mut R) -> Option<UdfState> {
125 let (vds_loc, vds_len) = read_avdp(reader)?;
126 let vds = read_vds(reader, vds_loc, vds_len)?;
127 let root_fe_lba = read_fsd(reader, vds.fsd_lba, vds.partition_start)?;
128 Some(UdfState {
129 partition_start: vds.partition_start,
130 root_fe_lba,
131 partition_kind: vds.partition_kind,
132 partition_map_count: vds.map_count,
133 })
134}
135
136struct VdsInfo {
138 partition_start: u32,
139 fsd_lba: u32,
140 partition_kind: UdfPartitionKind,
141 map_count: u32,
142}
143
144struct PartitionMap {
146 kind: UdfPartitionKind,
147 partition_number: Option<u16>,
149}
150
151fn classify_type2(map: &[u8]) -> UdfPartitionKind {
154 let scan = |needle: &[u8]| map.windows(needle.len()).any(|w| w == needle);
155 if scan(b"*UDF Metadata Partition") {
156 UdfPartitionKind::Metadata
157 } else if scan(b"*UDF Virtual Partition") {
158 UdfPartitionKind::Virtual
159 } else if scan(b"*UDF Sparable Partition") {
160 UdfPartitionKind::Sparable
161 } else {
162 UdfPartitionKind::Unknown
163 }
164}
165
166fn parse_partition_maps(lvd: &[u8]) -> Vec<PartitionMap> {
172 let n_pm = u32::from_le_bytes(lvd[268..272].try_into().unwrap()) as usize;
173 let mt_l = u32::from_le_bytes(lvd[264..268].try_into().unwrap()) as usize;
174 let maps_end = (440 + mt_l).min(lvd.len());
175 let mut out = Vec::new();
176 let mut off = 440;
177 while out.len() < n_pm && off + 2 <= maps_end {
178 let map_type = lvd[off];
179 let map_len = lvd[off + 1] as usize;
180 if map_len < 2 || off + map_len > maps_end {
181 break;
182 }
183 let map = &lvd[off..off + map_len];
184 let pm = match map_type {
185 1 if map_len >= 6 => PartitionMap {
186 kind: UdfPartitionKind::Physical,
187 partition_number: Some(u16::from_le_bytes([map[4], map[5]])),
188 },
189 2 => PartitionMap {
190 kind: classify_type2(map),
191 partition_number: None,
192 },
193 _ => PartitionMap {
194 kind: UdfPartitionKind::Unknown,
195 partition_number: None,
196 },
197 };
198 out.push(pm);
199 off += map_len;
200 }
201 out
202}
203
204pub fn read_dir_at_lba<R: Read + Seek>(
207 reader: &mut R,
208 partition_start: u32,
209 dir_fe_lba: u32,
210) -> Option<Vec<UdfFileEntry>> {
211 let dir_data = read_fe_data(reader, partition_start, dir_fe_lba)?;
212 Some(parse_fids(reader, partition_start, &dir_data))
213}
214
215pub fn read_fe_data<R: Read + Seek>(
217 reader: &mut R,
218 partition_start: u32,
219 fe_lba: u32,
220) -> Option<Vec<u8>> {
221 let mut sector = [0u8; 2048];
222 seek_read(reader, fe_lba as u64 * 2048, &mut sector)?;
223
224 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
225 let is_efe = tag_ident == TAG_EFE;
226 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && !is_efe {
227 return None;
228 }
229
230 let icb_flags = u16::from_le_bytes([sector[34], sector[35]]);
231 let alloc_type = icb_flags & 0x0007;
232 let info_len = u64::from_le_bytes(sector[56..64].try_into().unwrap());
233
234 let (ea_off, ad_off, header) = if is_efe {
236 (176usize, 180usize, 184usize)
237 } else {
238 (168usize, 172usize, 176usize)
239 };
240
241 if ad_off + 4 > sector.len() {
242 return None;
243 }
244 let ea_len = u32::from_le_bytes(sector[ea_off..ea_off + 4].try_into().unwrap()) as usize;
245 let ad_len = u32::from_le_bytes(sector[ad_off..ad_off + 4].try_into().unwrap()) as usize;
246
247 let ad_start = header + ea_len;
248 let ad_end = ad_start + ad_len;
249 if ad_end > sector.len() {
250 return None;
251 }
252 let ad_area = sector[ad_start..ad_end].to_vec();
253
254 match alloc_type {
255 ALLOC_INLINE => Some(ad_area[..info_len.min(ad_area.len() as u64) as usize].to_vec()),
256 ALLOC_SHORT => read_extents_short(reader, partition_start, &ad_area, info_len),
257 ALLOC_LONG => read_extents_long(reader, partition_start, &ad_area, info_len),
258 _ => None,
259 }
260}
261
262fn read_avdp<R: Read + Seek>(reader: &mut R) -> Option<(u32, u32)> {
266 let mut sector = [0u8; 2048];
267 seek_read(reader, 256 * 2048, &mut sector)?;
268 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_AVDP {
269 return None;
270 }
271 let vds_len = u32::from_le_bytes(sector[16..20].try_into().unwrap());
272 let vds_loc = u32::from_le_bytes(sector[20..24].try_into().unwrap());
273 Some((vds_loc, vds_len))
274}
275
276fn read_vds<R: Read + Seek>(reader: &mut R, vds_loc: u32, vds_len: u32) -> Option<VdsInfo> {
281 use std::collections::HashMap;
282 let sectors = (vds_len as usize).div_ceil(2048);
283
284 let mut pd_start: HashMap<u16, u32> = HashMap::new();
286 let mut fsd_lbn: Option<u32> = None;
287 let mut fsd_part_ref: u16 = 0;
288 let mut maps: Vec<PartitionMap> = Vec::new();
289
290 for i in 0..sectors {
291 let mut sector = [0u8; 2048];
292 seek_read(reader, (vds_loc as u64 + i as u64) * 2048, &mut sector)?;
293 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
294 match tag_ident {
295 TAG_PD => {
296 let part_num = u16::from_le_bytes([sector[22], sector[23]]);
297 let psl = u32::from_le_bytes(sector[188..192].try_into().unwrap());
298 pd_start.insert(part_num, psl);
299 }
300 TAG_LVD => {
301 fsd_lbn = Some(u32::from_le_bytes(sector[252..256].try_into().unwrap()));
304 fsd_part_ref = u16::from_le_bytes([sector[256], sector[257]]);
305 maps = parse_partition_maps(§or);
306 }
307 TAG_TERM | 0 => break,
308 _ => {}
309 }
310 }
311
312 let fsd = fsd_lbn?;
313 let map_count = maps.len() as u32;
314
315 let referenced = maps.get(fsd_part_ref as usize);
317 let kind = referenced.map_or(UdfPartitionKind::Unknown, |m| m.kind);
318
319 let partition_start = referenced
324 .and_then(|m| m.partition_number)
325 .and_then(|pn| pd_start.get(&pn).copied())
326 .or_else(|| pd_start.values().min().copied())?;
327
328 Some(VdsInfo {
329 partition_start,
330 fsd_lba: partition_start + fsd,
331 partition_kind: kind,
332 map_count,
333 })
334}
335
336fn read_fsd<R: Read + Seek>(reader: &mut R, fsd_lba: u32, partition_start: u32) -> Option<u32> {
338 let mut sector = [0u8; 2048];
339 seek_read(reader, fsd_lba as u64 * 2048, &mut sector)?;
340 if u16::from_le_bytes([sector[0], sector[1]]) != TAG_FSD {
341 return None;
342 }
343 let lbn = u32::from_le_bytes(sector[404..408].try_into().unwrap());
350 Some(partition_start + lbn)
351}
352
353fn detect_fid_tag_size(data: &[u8]) -> usize {
361 let mut off = 0;
362 while off + 28 <= data.len() {
363 let ti = u16::from_le_bytes([data[off], data[off + 1]]);
364 if ti == TAG_FID {
365 let lbn16 = if off + 26 <= data.len() {
366 u32::from_le_bytes(data[off + 22..off + 26].try_into().unwrap())
367 } else {
368 u32::MAX
369 };
370 let lbn18 = if off + 28 <= data.len() {
371 u32::from_le_bytes(data[off + 24..off + 28].try_into().unwrap())
372 } else {
373 u32::MAX
374 };
375 if lbn16 < 0x10000 {
376 return 16;
377 }
378 if lbn18 < 0x10000 {
379 return 18;
380 }
381 return 16; }
383 off += 4;
384 }
385 16
386}
387
388fn parse_fids<R: Read + Seek>(
390 reader: &mut R,
391 partition_start: u32,
392 data: &[u8],
393) -> Vec<UdfFileEntry> {
394 let tag_size = detect_fid_tag_size(data);
397 let min_fid = tag_size + 20; let mut entries = Vec::new();
400 let mut off = 0;
401
402 while off + min_fid <= data.len() {
403 let tag_ident = u16::from_le_bytes([data[off], data[off + 1]]);
404 if tag_ident != TAG_FID {
405 off += 4;
407 continue;
408 }
409
410 let crc_len = u16::from_le_bytes([data[off + 10], data[off + 11]]) as usize;
412 let fid_advance = ((16 + crc_len + 3) & !3).max(min_fid);
413 if off + fid_advance > data.len() {
414 break;
415 }
416
417 let file_chars = data[off + tag_size];
418 let file_id_len = data[off + tag_size + 1] as usize;
419 let icb_lbn = if off + tag_size + 10 <= data.len() {
421 u32::from_le_bytes(
422 data[off + tag_size + 6..off + tag_size + 10]
423 .try_into()
424 .unwrap(),
425 )
426 } else {
427 off += fid_advance.max(4);
428 continue;
429 };
430 let impl_use_len = if off + tag_size + 20 <= data.len() {
431 u16::from_le_bytes([data[off + tag_size + 18], data[off + tag_size + 19]]) as usize
432 } else {
433 off += fid_advance.max(4);
434 continue;
435 };
436
437 if file_chars & FC_PARENT == 0 {
438 let is_dir = file_chars & FC_DIRECTORY != 0;
439 let fe_lba = partition_start + icb_lbn;
440
441 let id_start = off + tag_size + 20 + impl_use_len;
442 let id_end = (id_start + file_id_len).min(data.len());
443 let name = if id_end > id_start {
444 decode_osta_cs0(&data[id_start..id_end])
445 } else {
446 String::new()
447 };
448
449 let size = read_fe_info_len(reader, fe_lba).unwrap_or(0);
451
452 entries.push(UdfFileEntry {
453 name,
454 is_dir,
455 size,
456 fe_lba,
457 });
458 }
459
460 off += fid_advance.max(4);
461 }
462 entries
463}
464
465fn read_fe_info_len<R: Read + Seek>(reader: &mut R, fe_lba: u32) -> Option<u64> {
467 let mut sector = [0u8; 2048];
468 seek_read(reader, fe_lba as u64 * 2048, &mut sector)?;
469 let tag_ident = u16::from_le_bytes([sector[0], sector[1]]);
470 if tag_ident != TAG_FE && tag_ident != TAG_FE_ALT && tag_ident != TAG_EFE {
471 return None;
472 }
473 Some(u64::from_le_bytes(sector[56..64].try_into().unwrap()))
474}
475
476fn read_extents_short<R: Read + Seek>(
478 reader: &mut R,
479 partition_start: u32,
480 ad_area: &[u8],
481 total_len: u64,
482) -> Option<Vec<u8>> {
483 let mut data = Vec::new();
484 let mut pos = 0;
485 while pos + 8 <= ad_area.len() && (data.len() as u64) < total_len {
486 let len_raw = u32::from_le_bytes(ad_area[pos..pos + 4].try_into().unwrap());
487 let ext_pos = u32::from_le_bytes(ad_area[pos + 4..pos + 8].try_into().unwrap());
488 let ext_type = len_raw >> 30;
489 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
490 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
491 let phys = (partition_start as u64 + ext_pos as u64) * 2048;
492 read_extent(reader, phys, ext_len, total_len, &mut data)?;
493 }
494 pos += 8;
495 }
496 data.truncate(total_len as usize);
497 Some(data)
498}
499
500fn read_extents_long<R: Read + Seek>(
502 reader: &mut R,
503 partition_start: u32,
504 ad_area: &[u8],
505 total_len: u64,
506) -> Option<Vec<u8>> {
507 let mut data = Vec::new();
508 let mut pos = 0;
509 while pos + 16 <= ad_area.len() && (data.len() as u64) < total_len {
510 let len_raw = u32::from_le_bytes(ad_area[pos..pos + 4].try_into().unwrap());
511 let lbn = u32::from_le_bytes(ad_area[pos + 4..pos + 8].try_into().unwrap());
512 let ext_type = len_raw >> 30;
513 let ext_len = (len_raw & 0x3FFF_FFFF) as usize;
514 if ext_type == (EXTENT_RECORDED >> 30) && ext_len > 0 {
515 let phys = (partition_start as u64 + lbn as u64) * 2048;
516 read_extent(reader, phys, ext_len, total_len, &mut data)?;
517 }
518 pos += 16;
519 }
520 data.truncate(total_len as usize);
521 Some(data)
522}
523
524fn read_extent<R: Read + Seek>(
526 reader: &mut R,
527 byte_pos: u64,
528 ext_len: usize,
529 total_len: u64,
530 data: &mut Vec<u8>,
531) -> Option<()> {
532 let sectors = ext_len.div_ceil(2048);
533 for i in 0..sectors {
534 let mut sector = [0u8; 2048];
535 seek_read(reader, byte_pos + i as u64 * 2048, &mut sector)?;
536 let already = data.len() as u64;
537 let remaining = total_len.saturating_sub(already) as usize;
538 let sector_bytes = (ext_len - i * 2048).min(2048);
539 let take = sector_bytes.min(remaining);
540 data.extend_from_slice(§or[..take]);
541 }
542 Some(())
543}
544
545fn decode_osta_cs0(bytes: &[u8]) -> String {
548 if bytes.is_empty() {
549 return String::new();
550 }
551 let comp_id = bytes[0];
552 let payload = &bytes[1..];
553 match comp_id {
554 8 => String::from_utf8_lossy(payload).into_owned(),
555 16 => {
556 let pairs: Vec<u16> = payload
557 .chunks_exact(2)
558 .map(|c| u16::from_be_bytes([c[0], c[1]]))
559 .collect();
560 String::from_utf16_lossy(&pairs)
561 }
562 _ => String::from_utf8_lossy(payload).into_owned(),
563 }
564}
565
566fn seek_read<R: Read + Seek>(reader: &mut R, byte_pos: u64, buf: &mut [u8]) -> Option<()> {
568 reader.seek(SeekFrom::Start(byte_pos)).ok()?;
569 reader.read_exact(buf).ok()?;
570 Some(())
571}
572
573#[cfg(test)]
574mod real_media_tests {
575 use super::{parse_udf_state, UdfPartitionKind};
578 use std::fs::File;
579
580 fn state(name: &str) -> Option<super::UdfState> {
581 let path = format!("{}/tests/data/{}", env!("CARGO_MANIFEST_DIR"), name);
582 let mut f = File::open(&path).ok()?;
583 parse_udf_state(&mut f)
584 }
585
586 #[test]
587 fn vat_image_classified_virtual() {
588 let Some(st) = state("udf_vat.img") else {
589 eprintln!("skip: udf_vat.img");
590 return;
591 };
592 assert_eq!(
593 st.partition_kind,
594 UdfPartitionKind::Virtual,
595 "mkudffs cdr/1.50 image must classify as Virtual (VAT)"
596 );
597 }
598
599 #[test]
600 fn sparable_image_classified_sparable() {
601 let Some(st) = state("udf_spar.img") else {
602 eprintln!("skip: udf_spar.img");
603 return;
604 };
605 assert_eq!(
606 st.partition_kind,
607 UdfPartitionKind::Sparable,
608 "mkudffs dvdrw/2.01 image must classify as Sparable"
609 );
610 }
611}