Skip to main content

oxideav_dvd/
udf.rs

1//! Minimal read-only UDF 1.02 mounter, scoped to what's needed to
2//! walk the `VIDEO_TS/` and `AUDIO_TS/` directories of a DVD-Video
3//! disc.
4//!
5//! References:
6//! - ECMA-167 3rd edition (June 1997) — the UDF base standard.
7//! - OSTA UDF 1.02 (2007 Wayback snapshot) — the OSTA profile used
8//!   by DVD-Video and DVD-ROM, per ECMA-268 §9.
9//!
10//! UDF 1.02 is a strict subset of UDF 2.50 by ECMA-167 — every
11//! descriptor we touch (PVD, Partition, LVD, FSD, FID, FE) has an
12//! identical wire format. The notable differences for DVD-Video:
13//!
14//! - Extended File Entries (tag 266) do **not** appear in 1.02 —
15//!   only the plain File Entry (tag 261). We refuse EFE as a
16//!   safety net but in practice DVD-Video discs never carry them.
17//! - The Logical Volume Descriptor's domain identifier suffix is
18//!   `\x00\x02\x01\x00` (UDF Revision = 0x0102) for DVD-Video,
19//!   versus `\x00\x02\x50\x00` for UDF 2.50. We do not gate on this.
20//! - The AVDP can live at sector 256, the second-anchor sector
21//!   (typically 512 or last-sector for DVD-Video), and N-256 per
22//!   §3/10.2. We probe in that order; the first valid AVDP wins.
23//!
24//! ## What's implemented
25//!
26//! - 2048-byte logical sector size (mandatory on DVD per ECMA-268).
27//! - AVDP probe at sectors 256 / 512 / (volume_size - 256).
28//! - Volume Descriptor Sequence (PVD / PD / LVD / Terminating).
29//! - File Set Descriptor + Root Directory ICB.
30//! - File Identifier Descriptor walks with the §14.4 padding rule
31//!   (records are rounded up to a 4-byte boundary).
32//! - File Entry parsing with Short_ad / Long_ad / Ext_ad and
33//!   embedded-in-ICB content.
34//! - OSTA Compressed Unicode `d-string` decoding (compression IDs
35//!   8 and 16) per UDF 1.02 §2.1.3.
36//!
37//! ## What's not implemented (Phase 1 — surface `Unsupported`)
38//!
39//! - Multi-extent partition maps (`partition_map_count > 1`).
40//! - ICB strategy types other than 4 (the spec's default linear).
41//! - Sparse / sequential files.
42//! - Allocation Extent Descriptor continuation (extent_type == 3).
43//! - Extended File Entry (tag 266).
44//! - Symbolic Links / Streams.
45
46use std::io::{Read, Seek, SeekFrom};
47
48use crate::error::{Error, Result};
49
50/// Logical sector size on a DVD: mandatory 2048 bytes (ECMA-268 §6.1).
51pub const SECTOR_SIZE: u64 = 2048;
52/// First sector at which we look for the Anchor Volume Descriptor
53/// Pointer per ECMA-167 §3/10.2.
54pub const AVDP_SECTOR_PRIMARY: u64 = 256;
55/// Conventional secondary AVDP location used by DVD-Video authoring
56/// tools. The actual ECMA-167 rule is "last sector or last-sector-
57/// minus-256" but DVDs commonly mirror the AVDP at sector 512 too;
58/// we probe both.
59pub const AVDP_SECTOR_SECONDARY: u64 = 512;
60
61// ─────────────────────── Descriptor tag (§7.2) ───────────────────────
62
63/// Numeric `TagIdentifier` of every descriptor we touch. Values from
64/// ECMA-167 §3/7.2.1 unless otherwise noted.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[repr(u16)]
67pub enum TagId {
68    PrimaryVolume = 1,
69    AnchorVolumeDescriptorPointer = 2,
70    VolumeDescriptorPointer = 3,
71    ImplementationUseVolume = 4,
72    Partition = 5,
73    LogicalVolume = 6,
74    UnallocatedSpace = 7,
75    Terminating = 8,
76    LogicalVolumeIntegrity = 9,
77    FileSet = 256,
78    FileIdentifier = 257,
79    AllocationExtent = 258,
80    Indirect = 259,
81    Terminal = 260,
82    FileEntry = 261,
83    ExtendedAttributeHeader = 262,
84    UnallocatedSpaceEntry = 263,
85    SpaceBitmap = 264,
86    PartitionIntegrityEntry = 265,
87    ExtendedFileEntry = 266,
88}
89
90impl TagId {
91    pub fn from_raw(v: u16) -> Option<Self> {
92        Some(match v {
93            1 => Self::PrimaryVolume,
94            2 => Self::AnchorVolumeDescriptorPointer,
95            3 => Self::VolumeDescriptorPointer,
96            4 => Self::ImplementationUseVolume,
97            5 => Self::Partition,
98            6 => Self::LogicalVolume,
99            7 => Self::UnallocatedSpace,
100            8 => Self::Terminating,
101            9 => Self::LogicalVolumeIntegrity,
102            256 => Self::FileSet,
103            257 => Self::FileIdentifier,
104            258 => Self::AllocationExtent,
105            259 => Self::Indirect,
106            260 => Self::Terminal,
107            261 => Self::FileEntry,
108            262 => Self::ExtendedAttributeHeader,
109            263 => Self::UnallocatedSpaceEntry,
110            264 => Self::SpaceBitmap,
111            265 => Self::PartitionIntegrityEntry,
112            266 => Self::ExtendedFileEntry,
113            _ => return None,
114        })
115    }
116}
117
118/// The 16-byte descriptor tag prefix common to every numbered
119/// descriptor (§7.2). All multi-byte fields are little-endian.
120///
121/// ```text
122///   0  TagIdentifier        u16 LE
123///   2  DescriptorVersion    u16 LE   (2 for UDF 1.0x, 3 for 2.x+)
124///   4  TagChecksum          u8       (sum of bytes 0..16 except [4] mod 256)
125///   5  Reserved             u8
126///   6  TagSerialNumber      u16 LE
127///   8  DescriptorCRC        u16 LE
128///  10  DescriptorCRCLength  u16 LE
129///  12  TagLocation          u32 LE   (sector this tag is recorded at)
130/// ```
131#[derive(Debug, Clone, Copy)]
132pub struct DescriptorTag {
133    pub id: TagId,
134    pub descriptor_version: u16,
135    pub serial_number: u16,
136    pub crc: u16,
137    pub crc_length: u16,
138    pub location: u32,
139}
140
141impl DescriptorTag {
142    pub const SIZE: usize = 16;
143
144    pub fn parse(bytes: &[u8]) -> Result<Self> {
145        if bytes.len() < Self::SIZE {
146            return Err(Error::InvalidUdf("descriptor tag truncated"));
147        }
148        let id_raw = u16::from_le_bytes([bytes[0], bytes[1]]);
149        let id = TagId::from_raw(id_raw).ok_or(Error::InvalidUdf("unknown TagId"))?;
150        let descriptor_version = u16::from_le_bytes([bytes[2], bytes[3]]);
151        let checksum = bytes[4];
152        if bytes[5] != 0 {
153            return Err(Error::InvalidUdf("DescriptorTag reserved byte non-zero"));
154        }
155        let serial_number = u16::from_le_bytes([bytes[6], bytes[7]]);
156        let crc = u16::from_le_bytes([bytes[8], bytes[9]]);
157        let crc_length = u16::from_le_bytes([bytes[10], bytes[11]]);
158        let location = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
159
160        let calc: u32 = bytes[..Self::SIZE]
161            .iter()
162            .enumerate()
163            .filter(|(i, _)| *i != 4)
164            .map(|(_, b)| *b as u32)
165            .sum();
166        if (calc & 0xFF) as u8 != checksum {
167            return Err(Error::InvalidUdf("DescriptorTag checksum mismatch"));
168        }
169
170        Ok(Self {
171            id,
172            descriptor_version,
173            serial_number,
174            crc,
175            crc_length,
176            location,
177        })
178    }
179
180    pub fn encode(&self) -> [u8; Self::SIZE] {
181        let mut out = [0u8; Self::SIZE];
182        let id = self.id as u16;
183        out[0..2].copy_from_slice(&id.to_le_bytes());
184        out[2..4].copy_from_slice(&self.descriptor_version.to_le_bytes());
185        out[6..8].copy_from_slice(&self.serial_number.to_le_bytes());
186        out[8..10].copy_from_slice(&self.crc.to_le_bytes());
187        out[10..12].copy_from_slice(&self.crc_length.to_le_bytes());
188        out[12..16].copy_from_slice(&self.location.to_le_bytes());
189        let sum: u32 = out
190            .iter()
191            .enumerate()
192            .filter(|(i, _)| *i != 4)
193            .map(|(_, b)| *b as u32)
194            .sum();
195        out[4] = (sum & 0xFF) as u8;
196        out
197    }
198}
199
200// ─────────────────────── extent_ad (§7.1) ───────────────────────
201
202#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub struct ExtentAd {
204    pub length: u32,   // bytes
205    pub location: u32, // logical block number
206}
207
208impl ExtentAd {
209    pub const SIZE: usize = 8;
210    pub fn parse(bytes: &[u8]) -> Result<Self> {
211        if bytes.len() < Self::SIZE {
212            return Err(Error::InvalidUdf("extent_ad truncated"));
213        }
214        Ok(Self {
215            length: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
216            location: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
217        })
218    }
219
220    pub fn encode(self) -> [u8; Self::SIZE] {
221        let mut out = [0u8; Self::SIZE];
222        out[0..4].copy_from_slice(&self.length.to_le_bytes());
223        out[4..8].copy_from_slice(&self.location.to_le_bytes());
224        out
225    }
226}
227
228// ─────────────────────── short_ad (§14.14.1.1) ───────────────────────
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub struct ShortAd {
232    pub length: u32,
233    pub extent_type: u8,
234    pub block_location: u32,
235}
236
237impl ShortAd {
238    pub const SIZE: usize = 8;
239    pub fn parse(bytes: &[u8]) -> Result<Self> {
240        if bytes.len() < Self::SIZE {
241            return Err(Error::InvalidUdf("short_ad truncated"));
242        }
243        let raw_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
244        let block_location = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
245        Ok(Self {
246            length: raw_len & 0x3FFF_FFFF,
247            extent_type: ((raw_len >> 30) & 0b11) as u8,
248            block_location,
249        })
250    }
251
252    pub fn encode(self) -> [u8; Self::SIZE] {
253        let mut out = [0u8; Self::SIZE];
254        let raw_len = (self.length & 0x3FFF_FFFF) | ((self.extent_type as u32 & 0b11) << 30);
255        out[0..4].copy_from_slice(&raw_len.to_le_bytes());
256        out[4..8].copy_from_slice(&self.block_location.to_le_bytes());
257        out
258    }
259}
260
261// ─────────────────────── lb_addr / long_ad / ext_ad (§7.1) ───────────────────────
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
264pub struct LbAddr {
265    pub block: u32,
266    pub partition_ref: u16,
267}
268
269impl LbAddr {
270    pub const SIZE: usize = 6;
271    pub fn parse(bytes: &[u8]) -> Result<Self> {
272        if bytes.len() < Self::SIZE {
273            return Err(Error::InvalidUdf("lb_addr truncated"));
274        }
275        Ok(Self {
276            block: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
277            partition_ref: u16::from_le_bytes([bytes[4], bytes[5]]),
278        })
279    }
280
281    pub fn encode(self) -> [u8; Self::SIZE] {
282        let mut out = [0u8; Self::SIZE];
283        out[0..4].copy_from_slice(&self.block.to_le_bytes());
284        out[4..6].copy_from_slice(&self.partition_ref.to_le_bytes());
285        out
286    }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub struct LongAd {
291    pub length: u32,
292    pub extent_type: u8,
293    pub location: LbAddr,
294    pub implementation_use: [u8; 6],
295}
296
297impl LongAd {
298    pub const SIZE: usize = 16;
299    pub fn parse(bytes: &[u8]) -> Result<Self> {
300        if bytes.len() < Self::SIZE {
301            return Err(Error::InvalidUdf("long_ad truncated"));
302        }
303        let raw_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
304        let location = LbAddr::parse(&bytes[4..10])?;
305        let mut impl_use = [0u8; 6];
306        impl_use.copy_from_slice(&bytes[10..16]);
307        Ok(Self {
308            length: raw_len & 0x3FFF_FFFF,
309            extent_type: ((raw_len >> 30) & 0b11) as u8,
310            location,
311            implementation_use: impl_use,
312        })
313    }
314
315    pub fn encode(self) -> [u8; Self::SIZE] {
316        let mut out = [0u8; Self::SIZE];
317        let raw_len = (self.length & 0x3FFF_FFFF) | ((self.extent_type as u32 & 0b11) << 30);
318        out[0..4].copy_from_slice(&raw_len.to_le_bytes());
319        out[4..10].copy_from_slice(&self.location.encode());
320        out[10..16].copy_from_slice(&self.implementation_use);
321        out
322    }
323}
324
325/// `ext_ad`: 20-byte extended allocation descriptor (§7.1).
326/// Layout: 4-byte length+type, 4-byte recorded length, 4-byte
327/// information length, 6-byte logical block address, 2-byte
328/// implementation use field.
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330pub struct ExtAd {
331    pub length: u32,
332    pub extent_type: u8,
333    pub recorded_length: u32,
334    pub information_length: u32,
335    pub location: LbAddr,
336    pub implementation_use: [u8; 2],
337}
338
339impl ExtAd {
340    pub const SIZE: usize = 20;
341    pub fn parse(bytes: &[u8]) -> Result<Self> {
342        if bytes.len() < Self::SIZE {
343            return Err(Error::InvalidUdf("ext_ad truncated"));
344        }
345        let raw_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
346        let recorded_length = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
347        let information_length = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
348        let location = LbAddr::parse(&bytes[12..18])?;
349        let mut impl_use = [0u8; 2];
350        impl_use.copy_from_slice(&bytes[18..20]);
351        Ok(Self {
352            length: raw_len & 0x3FFF_FFFF,
353            extent_type: ((raw_len >> 30) & 0b11) as u8,
354            recorded_length,
355            information_length,
356            location,
357            implementation_use: impl_use,
358        })
359    }
360
361    pub fn encode(self) -> [u8; Self::SIZE] {
362        let mut out = [0u8; Self::SIZE];
363        let raw_len = (self.length & 0x3FFF_FFFF) | ((self.extent_type as u32 & 0b11) << 30);
364        out[0..4].copy_from_slice(&raw_len.to_le_bytes());
365        out[4..8].copy_from_slice(&self.recorded_length.to_le_bytes());
366        out[8..12].copy_from_slice(&self.information_length.to_le_bytes());
367        out[12..18].copy_from_slice(&self.location.encode());
368        out[18..20].copy_from_slice(&self.implementation_use);
369        out
370    }
371}
372
373// ─────────────────────── d-string / OSTA compressed unicode ───────────────────────
374
375/// Decode an OSTA Compressed Unicode `d-string` per UDF 1.02 §2.1.3.
376/// First byte is the compression ID (8 = 8-bit per char, 16 = 16-bit
377/// BE). Returns the decoded `String`; null bytes truncate.
378pub fn decode_dstring(payload: &[u8]) -> Result<String> {
379    if payload.is_empty() {
380        return Ok(String::new());
381    }
382    match payload[0] {
383        0 => Ok(String::new()),
384        8 => {
385            let mut s = String::with_capacity(payload.len() - 1);
386            for &b in &payload[1..] {
387                if b == 0 {
388                    break;
389                }
390                s.push(b as char);
391            }
392            Ok(s)
393        }
394        16 => {
395            let body = &payload[1..];
396            if body.len() % 2 != 0 {
397                return Err(Error::InvalidUdf("16-bit d-string with odd byte count"));
398            }
399            let mut s = String::with_capacity(body.len() / 2);
400            for chunk in body.chunks_exact(2) {
401                let cp = u16::from_be_bytes([chunk[0], chunk[1]]);
402                if cp == 0 {
403                    break;
404                }
405                if let Some(c) = char::from_u32(cp as u32) {
406                    s.push(c);
407                }
408            }
409            Ok(s)
410        }
411        _ => Err(Error::InvalidUdf("unknown d-string compression id")),
412    }
413}
414
415/// Decode a fixed-length d-string field; the last byte of the field
416/// holds the payload length in bytes.
417pub fn decode_dstring_field(field: &[u8]) -> Result<String> {
418    if field.is_empty() {
419        return Ok(String::new());
420    }
421    let len = *field.last().unwrap() as usize;
422    if len > field.len() - 1 {
423        return Err(Error::InvalidUdf("d-string length overflows field"));
424    }
425    decode_dstring(&field[..len])
426}
427
428// ─────────────────────── AnchorVolumeDescriptorPointer (§10.2) ───────────────────────
429
430#[derive(Debug, Clone, Copy)]
431pub struct AnchorVolumeDescriptorPointer {
432    pub tag: DescriptorTag,
433    pub main_volume_descriptor_sequence: ExtentAd,
434    pub reserve_volume_descriptor_sequence: ExtentAd,
435}
436
437impl AnchorVolumeDescriptorPointer {
438    pub fn parse(bytes: &[u8]) -> Result<Self> {
439        let tag = DescriptorTag::parse(bytes)?;
440        if tag.id != TagId::AnchorVolumeDescriptorPointer {
441            return Err(Error::InvalidUdf("expected AVDP tag"));
442        }
443        let main = ExtentAd::parse(&bytes[16..24])?;
444        let reserve = ExtentAd::parse(&bytes[24..32])?;
445        Ok(Self {
446            tag,
447            main_volume_descriptor_sequence: main,
448            reserve_volume_descriptor_sequence: reserve,
449        })
450    }
451}
452
453// ─────────────────────── PrimaryVolumeDescriptor (§10.1) ───────────────────────
454
455#[derive(Debug, Clone)]
456pub struct PrimaryVolumeDescriptor {
457    pub tag: DescriptorTag,
458    pub volume_descriptor_sequence_number: u32,
459    pub primary_volume_descriptor_number: u32,
460    pub volume_identifier: String,
461}
462
463impl PrimaryVolumeDescriptor {
464    pub fn parse(bytes: &[u8]) -> Result<Self> {
465        let tag = DescriptorTag::parse(bytes)?;
466        if tag.id != TagId::PrimaryVolume {
467            return Err(Error::InvalidUdf("expected PVD tag"));
468        }
469        if bytes.len() < 56 {
470            return Err(Error::InvalidUdf("PVD truncated"));
471        }
472        let vds_n = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
473        let pvd_n = u32::from_le_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
474        let volume_identifier = decode_dstring_field(&bytes[24..56])?;
475        Ok(Self {
476            tag,
477            volume_descriptor_sequence_number: vds_n,
478            primary_volume_descriptor_number: pvd_n,
479            volume_identifier,
480        })
481    }
482}
483
484// ─────────────────────── PartitionDescriptor (§10.5) ───────────────────────
485
486#[derive(Debug, Clone)]
487pub struct PartitionDescriptor {
488    pub tag: DescriptorTag,
489    pub volume_descriptor_sequence_number: u32,
490    pub partition_flags: u16,
491    pub partition_number: u16,
492    pub partition_starting_location: u32,
493    pub partition_length: u32,
494}
495
496impl PartitionDescriptor {
497    pub fn parse(bytes: &[u8]) -> Result<Self> {
498        let tag = DescriptorTag::parse(bytes)?;
499        if tag.id != TagId::Partition {
500            return Err(Error::InvalidUdf("expected PD tag"));
501        }
502        if bytes.len() < 196 {
503            return Err(Error::InvalidUdf("PD truncated"));
504        }
505        let vds_n = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
506        let part_flags = u16::from_le_bytes([bytes[20], bytes[21]]);
507        let part_num = u16::from_le_bytes([bytes[22], bytes[23]]);
508        let part_start = u32::from_le_bytes([bytes[188], bytes[189], bytes[190], bytes[191]]);
509        let part_len = u32::from_le_bytes([bytes[192], bytes[193], bytes[194], bytes[195]]);
510        Ok(Self {
511            tag,
512            volume_descriptor_sequence_number: vds_n,
513            partition_flags: part_flags,
514            partition_number: part_num,
515            partition_starting_location: part_start,
516            partition_length: part_len,
517        })
518    }
519}
520
521// ─────────────────────── LogicalVolumeDescriptor (§10.6) ───────────────────────
522
523#[derive(Debug, Clone)]
524pub struct LogicalVolumeDescriptor {
525    pub tag: DescriptorTag,
526    pub volume_descriptor_sequence_number: u32,
527    pub logical_volume_identifier: String,
528    pub logical_block_size: u32,
529    pub file_set_descriptor_location: LongAd,
530}
531
532impl LogicalVolumeDescriptor {
533    pub fn parse(bytes: &[u8]) -> Result<Self> {
534        let tag = DescriptorTag::parse(bytes)?;
535        if tag.id != TagId::LogicalVolume {
536            return Err(Error::InvalidUdf("expected LVD tag"));
537        }
538        if bytes.len() < 440 {
539            return Err(Error::InvalidUdf("LVD truncated"));
540        }
541        let vds_n = u32::from_le_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
542        let lvi = decode_dstring_field(&bytes[84..212])?;
543        let lbs = u32::from_le_bytes([bytes[212], bytes[213], bytes[214], bytes[215]]);
544        let fsd = LongAd::parse(&bytes[248..264])?;
545        Ok(Self {
546            tag,
547            volume_descriptor_sequence_number: vds_n,
548            logical_volume_identifier: lvi,
549            logical_block_size: lbs,
550            file_set_descriptor_location: fsd,
551        })
552    }
553}
554
555// ─────────────────────── LogicalVolumeIntegrityDescriptor (§10.10) ───────────────────────
556
557#[derive(Debug, Clone)]
558pub struct LogicalVolumeIntegrityDescriptor {
559    pub tag: DescriptorTag,
560    pub number_of_partitions: u32,
561    pub length_of_implementation_use: u32,
562}
563
564impl LogicalVolumeIntegrityDescriptor {
565    pub fn parse(bytes: &[u8]) -> Result<Self> {
566        let tag = DescriptorTag::parse(bytes)?;
567        if tag.id != TagId::LogicalVolumeIntegrity {
568            return Err(Error::InvalidUdf("expected LVID tag"));
569        }
570        if bytes.len() < 80 {
571            return Err(Error::InvalidUdf("LVID truncated"));
572        }
573        // bytes 16..28 — recording date (12 bytes)
574        // bytes 28..32 — integrity type
575        // bytes 32..40 — next integrity extent
576        // bytes 40..72 — logical volume contents use
577        let nop = u32::from_le_bytes([bytes[72], bytes[73], bytes[74], bytes[75]]);
578        let liu = u32::from_le_bytes([bytes[76], bytes[77], bytes[78], bytes[79]]);
579        Ok(Self {
580            tag,
581            number_of_partitions: nop,
582            length_of_implementation_use: liu,
583        })
584    }
585}
586
587// ─────────────────────── FileSetDescriptor (§14.1) ───────────────────────
588
589#[derive(Debug, Clone)]
590pub struct FileSetDescriptor {
591    pub tag: DescriptorTag,
592    pub root_directory_icb: LongAd,
593}
594
595impl FileSetDescriptor {
596    pub fn parse(bytes: &[u8]) -> Result<Self> {
597        let tag = DescriptorTag::parse(bytes)?;
598        if tag.id != TagId::FileSet {
599            return Err(Error::InvalidUdf("expected FSD tag"));
600        }
601        if bytes.len() < 416 {
602            return Err(Error::InvalidUdf("FSD truncated"));
603        }
604        let root = LongAd::parse(&bytes[400..416])?;
605        Ok(Self {
606            tag,
607            root_directory_icb: root,
608        })
609    }
610}
611
612// ─────────────────────── FileIdentifierDescriptor (§14.4) ───────────────────────
613
614#[derive(Debug, Clone)]
615pub struct FileIdentifierDescriptor {
616    pub tag: DescriptorTag,
617    pub file_version_number: u16,
618    pub file_characteristics: u8,
619    pub identifier: String,
620    pub icb: LongAd,
621    /// Total padded size of the FID record on disc (§14.4.9: round up
622    /// to the next 4-byte boundary).
623    pub total_size: usize,
624}
625
626impl FileIdentifierDescriptor {
627    pub fn parse(bytes: &[u8]) -> Result<Self> {
628        if bytes.len() < 38 {
629            return Err(Error::InvalidUdf("FID truncated"));
630        }
631        let tag = DescriptorTag::parse(bytes)?;
632        if tag.id != TagId::FileIdentifier {
633            return Err(Error::InvalidUdf("expected FID tag"));
634        }
635        let file_version_number = u16::from_le_bytes([bytes[16], bytes[17]]);
636        let file_characteristics = bytes[18];
637        let len_fi = bytes[19] as usize;
638        let icb = LongAd::parse(&bytes[20..36])?;
639        let len_impl_use = u16::from_le_bytes([bytes[36], bytes[37]]) as usize;
640        let id_off = 38 + len_impl_use;
641        let id_end = id_off + len_fi;
642        if bytes.len() < id_end {
643            return Err(Error::InvalidUdf("FID identifier overruns buffer"));
644        }
645        let identifier = decode_dstring(&bytes[id_off..id_end])?;
646        let total = id_end.div_ceil(4) * 4;
647        Ok(Self {
648            tag,
649            file_version_number,
650            file_characteristics,
651            identifier,
652            icb,
653            total_size: total,
654        })
655    }
656
657    pub fn is_deleted(&self) -> bool {
658        self.file_characteristics & 0x04 != 0
659    }
660    pub fn is_directory(&self) -> bool {
661        self.file_characteristics & 0x02 != 0
662    }
663    pub fn is_parent(&self) -> bool {
664        self.file_characteristics & 0x08 != 0
665    }
666}
667
668// ─────────────────────── FileEntry / ICB (§14.6, §14.9) ───────────────────────
669
670#[derive(Debug, Clone, Copy)]
671pub struct IcbTag {
672    pub prior_recorded_entries: u32,
673    pub strategy_type: u16,
674    pub strategy_parameter: u16,
675    pub max_entries: u16,
676    pub file_type: u8,
677    pub parent_icb: LbAddr,
678    pub flags: u16,
679}
680
681impl IcbTag {
682    pub const SIZE: usize = 20;
683    pub fn parse(bytes: &[u8]) -> Result<Self> {
684        if bytes.len() < Self::SIZE {
685            return Err(Error::InvalidUdf("ICB tag truncated"));
686        }
687        Ok(Self {
688            prior_recorded_entries: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
689            strategy_type: u16::from_le_bytes([bytes[4], bytes[5]]),
690            strategy_parameter: u16::from_le_bytes([bytes[6], bytes[7]]),
691            max_entries: u16::from_le_bytes([bytes[8], bytes[9]]),
692            file_type: bytes[11],
693            parent_icb: LbAddr::parse(&bytes[12..18])?,
694            flags: u16::from_le_bytes([bytes[18], bytes[19]]),
695        })
696    }
697}
698
699/// Allocation Descriptor type encoded in `IcbTag::flags & 0b111`
700/// (§14.6.8).
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
702pub enum AdType {
703    Short,
704    Long,
705    Extended,
706    EmbeddedInIcb,
707}
708
709impl AdType {
710    pub fn from_flags(flags: u16) -> Result<Self> {
711        match flags & 0b111 {
712            0 => Ok(Self::Short),
713            1 => Ok(Self::Long),
714            2 => Ok(Self::Extended),
715            3 => Ok(Self::EmbeddedInIcb),
716            _ => Err(Error::InvalidUdf("unknown ad_type")),
717        }
718    }
719}
720
721/// A normalised allocation extent — the unifying type carrying enough
722/// information for the high-level disc reader regardless of which AD
723/// variant (`short_ad` / `long_ad` / `ext_ad`) was on disc.
724#[derive(Debug, Clone, Copy)]
725pub struct Extent {
726    /// Length in bytes.
727    pub length: u32,
728    /// In-partition block number.
729    pub block_location: u32,
730    /// 0 = recorded+allocated, 1 = allocated-not-recorded,
731    /// 2 = not-allocated, 3 = AllocationExtentDescriptor continuation.
732    pub extent_type: u8,
733}
734
735#[derive(Debug, Clone)]
736pub struct FileEntry {
737    pub tag: DescriptorTag,
738    pub icb_tag: IcbTag,
739    pub uid: u32,
740    pub gid: u32,
741    pub permissions: u32,
742    pub file_link_count: u16,
743    pub record_format: u8,
744    pub record_display_attributes: u8,
745    pub record_length: u32,
746    pub information_length: u64,
747    pub logical_blocks_recorded: u64,
748    pub length_of_extended_attributes: u32,
749    pub length_of_allocation_descriptors: u32,
750    pub extents: Vec<Extent>,
751    pub embedded_data: Vec<u8>,
752    pub ad_type: AdType,
753}
754
755impl FileEntry {
756    pub const PREFIX_SIZE: usize = 176;
757
758    pub fn parse(bytes: &[u8]) -> Result<Self> {
759        if bytes.len() < Self::PREFIX_SIZE {
760            return Err(Error::InvalidUdf("FileEntry truncated"));
761        }
762        let tag = DescriptorTag::parse(bytes)?;
763        if tag.id != TagId::FileEntry {
764            // UDF 1.02 only emits plain FE; reject EFE explicitly.
765            return Err(Error::InvalidUdf("expected FileEntry tag (no EFE in 1.02)"));
766        }
767        let icb_tag = IcbTag::parse(&bytes[16..36])?;
768        let uid = u32::from_le_bytes([bytes[36], bytes[37], bytes[38], bytes[39]]);
769        let gid = u32::from_le_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]);
770        let permissions = u32::from_le_bytes([bytes[44], bytes[45], bytes[46], bytes[47]]);
771        let flc = u16::from_le_bytes([bytes[48], bytes[49]]);
772        let rec_format = bytes[50];
773        let rec_disp_attr = bytes[51];
774        let rec_len = u32::from_le_bytes([bytes[52], bytes[53], bytes[54], bytes[55]]);
775        let info_len = u64::from_le_bytes([
776            bytes[56], bytes[57], bytes[58], bytes[59], bytes[60], bytes[61], bytes[62], bytes[63],
777        ]);
778        let lbr = u64::from_le_bytes([
779            bytes[64], bytes[65], bytes[66], bytes[67], bytes[68], bytes[69], bytes[70], bytes[71],
780        ]);
781        let l_ea = u32::from_le_bytes([bytes[168], bytes[169], bytes[170], bytes[171]]);
782        let l_ad = u32::from_le_bytes([bytes[172], bytes[173], bytes[174], bytes[175]]);
783
784        let ad_type = AdType::from_flags(icb_tag.flags)?;
785
786        let ea_off = Self::PREFIX_SIZE;
787        let ea_end = ea_off + l_ea as usize;
788        let ad_off = ea_end;
789        let ad_end = ad_off + l_ad as usize;
790        if bytes.len() < ad_end {
791            return Err(Error::InvalidUdf("FE allocation area overruns FE"));
792        }
793
794        let mut extents = Vec::new();
795        let mut embedded_data = Vec::new();
796        match ad_type {
797            AdType::Short => {
798                let mut o = 0;
799                while o + ShortAd::SIZE <= l_ad as usize {
800                    let ad = ShortAd::parse(&bytes[ad_off + o..ad_off + o + ShortAd::SIZE])?;
801                    if ad.length == 0 {
802                        break;
803                    }
804                    if ad.extent_type == 3 {
805                        return Err(Error::InvalidUdf(
806                            "Allocation Extent Descriptor continuation unsupported",
807                        ));
808                    }
809                    extents.push(Extent {
810                        length: ad.length,
811                        block_location: ad.block_location,
812                        extent_type: ad.extent_type,
813                    });
814                    o += ShortAd::SIZE;
815                }
816            }
817            AdType::Long => {
818                let mut o = 0;
819                while o + LongAd::SIZE <= l_ad as usize {
820                    let ad = LongAd::parse(&bytes[ad_off + o..ad_off + o + LongAd::SIZE])?;
821                    if ad.length == 0 {
822                        break;
823                    }
824                    if ad.extent_type == 3 {
825                        return Err(Error::InvalidUdf(
826                            "Allocation Extent Descriptor continuation unsupported",
827                        ));
828                    }
829                    extents.push(Extent {
830                        length: ad.length,
831                        block_location: ad.location.block,
832                        extent_type: ad.extent_type,
833                    });
834                    o += LongAd::SIZE;
835                }
836            }
837            AdType::Extended => {
838                let mut o = 0;
839                while o + ExtAd::SIZE <= l_ad as usize {
840                    let ad = ExtAd::parse(&bytes[ad_off + o..ad_off + o + ExtAd::SIZE])?;
841                    if ad.length == 0 {
842                        break;
843                    }
844                    if ad.extent_type == 3 {
845                        return Err(Error::InvalidUdf(
846                            "Allocation Extent Descriptor continuation unsupported",
847                        ));
848                    }
849                    extents.push(Extent {
850                        length: ad.length,
851                        block_location: ad.location.block,
852                        extent_type: ad.extent_type,
853                    });
854                    o += ExtAd::SIZE;
855                }
856            }
857            AdType::EmbeddedInIcb => {
858                embedded_data.extend_from_slice(&bytes[ad_off..ad_end]);
859            }
860        }
861
862        Ok(Self {
863            tag,
864            icb_tag,
865            uid,
866            gid,
867            permissions,
868            file_link_count: flc,
869            record_format: rec_format,
870            record_display_attributes: rec_disp_attr,
871            record_length: rec_len,
872            information_length: info_len,
873            logical_blocks_recorded: lbr,
874            length_of_extended_attributes: l_ea,
875            length_of_allocation_descriptors: l_ad,
876            extents,
877            embedded_data,
878            ad_type,
879        })
880    }
881
882    pub fn is_directory(&self) -> bool {
883        self.icb_tag.file_type == 4
884    }
885}
886
887// ─────────────────────── UdfVolume + UdfFile (high-level surface) ───────────────────────
888
889/// Where a file's bytes live on disc, as in-partition extents.
890#[derive(Debug, Clone)]
891pub struct UdfFile {
892    pub name: String,
893    pub is_dir: bool,
894    pub extents: Vec<Extent>,
895    pub length: u64,
896    /// Resolved ICB (kept around so a future Phase-2 caller can re-
897    /// parse the File Entry for stat-like metadata).
898    pub icb: LongAd,
899}
900
901/// A mounted UDF 1.02 volume with its file enumeration pre-computed.
902pub struct UdfVolume<R: Read + Seek> {
903    pub reader: R,
904    pub partition_start_sector: u64,
905    pub logical_block_size: u32,
906    pub volume_identifier: String,
907    pub logical_volume_identifier: String,
908    pub root_directory_icb: LongAd,
909}
910
911impl<R: Read + Seek> std::fmt::Debug for UdfVolume<R> {
912    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
913        f.debug_struct("UdfVolume")
914            .field("partition_start_sector", &self.partition_start_sector)
915            .field("logical_block_size", &self.logical_block_size)
916            .field("volume_identifier", &self.volume_identifier)
917            .field("logical_volume_identifier", &self.logical_volume_identifier)
918            .field("root_directory_icb", &self.root_directory_icb)
919            .finish()
920    }
921}
922
923impl<R: Read + Seek> UdfVolume<R> {
924    /// Mount the volume. Probes AVDP at the conventional candidate
925    /// sectors and walks the Volume Descriptor Sequence.
926    pub fn open(mut reader: R) -> Result<Self> {
927        let avdp = find_avdp(&mut reader)?;
928        let main = avdp.main_volume_descriptor_sequence;
929        let mut pvd: Option<PrimaryVolumeDescriptor> = None;
930        let mut pd: Option<PartitionDescriptor> = None;
931        let mut lvd: Option<LogicalVolumeDescriptor> = None;
932        let max_sectors = (main.length as u64).div_ceil(SECTOR_SIZE);
933        for i in 0..max_sectors {
934            let sec = main.location as u64 + i;
935            let buf = read_sector(&mut reader, sec)?;
936            let id_raw = u16::from_le_bytes([buf[0], buf[1]]);
937            let tag_id = match TagId::from_raw(id_raw) {
938                Some(t) => t,
939                None => continue,
940            };
941            match tag_id {
942                TagId::PrimaryVolume => pvd = Some(PrimaryVolumeDescriptor::parse(&buf)?),
943                TagId::Partition => pd = Some(PartitionDescriptor::parse(&buf)?),
944                TagId::LogicalVolume => lvd = Some(LogicalVolumeDescriptor::parse(&buf)?),
945                TagId::Terminating => break,
946                _ => {}
947            }
948        }
949        let pvd = pvd.ok_or(Error::InvalidUdf("no Primary Volume Descriptor"))?;
950        let pd = pd.ok_or(Error::InvalidUdf("no Partition Descriptor"))?;
951        let lvd = lvd.ok_or(Error::InvalidUdf("no Logical Volume Descriptor"))?;
952
953        if lvd.logical_block_size as u64 != SECTOR_SIZE {
954            return Err(Error::InvalidUdf(
955                "logical_block_size != 2048 (DVD mandates 2048)",
956            ));
957        }
958        if lvd.file_set_descriptor_location.location.partition_ref != pd.partition_number {
959            return Err(Error::InvalidUdf("FSD references non-default partition"));
960        }
961
962        let fsd_sec = pd.partition_starting_location as u64
963            + lvd.file_set_descriptor_location.location.block as u64;
964        let fsd_buf = read_sector_into_vec(&mut reader, fsd_sec)?;
965        let fsd = FileSetDescriptor::parse(&fsd_buf)?;
966
967        Ok(Self {
968            reader,
969            partition_start_sector: pd.partition_starting_location as u64,
970            logical_block_size: lvd.logical_block_size,
971            volume_identifier: pvd.volume_identifier,
972            logical_volume_identifier: lvd.logical_volume_identifier,
973            root_directory_icb: fsd.root_directory_icb,
974        })
975    }
976
977    /// Read a logical block from the partition (absolute sector =
978    /// `partition_start_sector + block`).
979    pub fn read_partition_block(&mut self, partition_block: u64) -> Result<Vec<u8>> {
980        let sec = self.partition_start_sector + partition_block;
981        read_sector_into_vec(&mut self.reader, sec)
982    }
983
984    /// Read the File Entry at the given ICB location.
985    pub fn read_file_entry(&mut self, icb: LongAd) -> Result<FileEntry> {
986        if icb.length == 0 {
987            return Err(Error::InvalidUdf("FE ICB length 0"));
988        }
989        let buf = self.read_partition_block(icb.location.block as u64)?;
990        FileEntry::parse(&buf)
991    }
992
993    /// Materialise the bytes of a file via its ICB.
994    pub fn read_file(&mut self, icb: LongAd) -> Result<Vec<u8>> {
995        let fe = self.read_file_entry(icb)?;
996        let want = fe.information_length as usize;
997        if fe.ad_type == AdType::EmbeddedInIcb {
998            return Ok(fe.embedded_data[..want.min(fe.embedded_data.len())].to_vec());
999        }
1000        let mut out = Vec::with_capacity(want);
1001        for ad in &fe.extents {
1002            if ad.extent_type != 0 {
1003                return Err(Error::InvalidUdf("non-recorded extent in file"));
1004            }
1005            let blocks = (ad.length as u64).div_ceil(SECTOR_SIZE);
1006            for i in 0..blocks {
1007                let buf = self.read_partition_block(ad.block_location as u64 + i)?;
1008                let to_copy =
1009                    (ad.length as usize).saturating_sub(i as usize * SECTOR_SIZE as usize);
1010                let take = to_copy.min(SECTOR_SIZE as usize);
1011                out.extend_from_slice(&buf[..take]);
1012                if out.len() >= want {
1013                    break;
1014                }
1015            }
1016            if out.len() >= want {
1017                break;
1018            }
1019        }
1020        out.truncate(want);
1021        Ok(out)
1022    }
1023
1024    /// List the entries of a directory at `dir_icb`.
1025    pub fn read_directory(&mut self, dir_icb: LongAd) -> Result<Vec<UdfFile>> {
1026        let raw = self.read_file(dir_icb)?;
1027        let mut out = Vec::new();
1028        let mut o = 0;
1029        while o + 38 <= raw.len() {
1030            let fid = FileIdentifierDescriptor::parse(&raw[o..])?;
1031            let step = fid.total_size;
1032            if step == 0 {
1033                break;
1034            }
1035            o += step;
1036            if fid.is_deleted() || fid.is_parent() {
1037                continue;
1038            }
1039            // For each entry, follow the ICB to learn the extents +
1040            // length. Embedded directories surface zero extents +
1041            // their length is the embedded payload size — callers
1042            // who want the FID's child names re-enter via
1043            // `read_directory(fid.icb)`.
1044            let fe = self.read_file_entry(fid.icb)?;
1045            let extents = fe.extents.clone();
1046            out.push(UdfFile {
1047                name: fid.identifier.clone(),
1048                is_dir: fid.is_directory(),
1049                extents,
1050                length: fe.information_length,
1051                icb: fid.icb,
1052            });
1053        }
1054        Ok(out)
1055    }
1056
1057    /// Recursively enumerate every regular file under the volume.
1058    /// Returns `(path, file)`. Directories appear as path prefixes.
1059    pub fn enumerate(&mut self) -> Result<Vec<(String, UdfFile)>> {
1060        let mut out = Vec::new();
1061        let root_icb = self.root_directory_icb;
1062        self.enumerate_into("", root_icb, &mut out)?;
1063        Ok(out)
1064    }
1065
1066    fn enumerate_into(
1067        &mut self,
1068        prefix: &str,
1069        dir_icb: LongAd,
1070        out: &mut Vec<(String, UdfFile)>,
1071    ) -> Result<()> {
1072        let entries = self.read_directory(dir_icb)?;
1073        for entry in entries {
1074            let p = if prefix.is_empty() {
1075                entry.name.clone()
1076            } else {
1077                format!("{prefix}/{}", entry.name)
1078            };
1079            if entry.is_dir {
1080                let icb = entry.icb;
1081                out.push((p.clone(), entry));
1082                self.enumerate_into(&p, icb, out)?;
1083            } else {
1084                out.push((p, entry));
1085            }
1086        }
1087        Ok(())
1088    }
1089}
1090
1091/// Probe the conventional AVDP sector locations (256, 512) for a
1092/// well-formed AVDP. Returns the first valid one.
1093pub fn find_avdp<R: Read + Seek>(reader: &mut R) -> Result<AnchorVolumeDescriptorPointer> {
1094    for sec in [AVDP_SECTOR_PRIMARY, AVDP_SECTOR_SECONDARY] {
1095        let Ok(buf) = read_sector(reader, sec) else {
1096            continue;
1097        };
1098        if let Ok(avdp) = AnchorVolumeDescriptorPointer::parse(&buf) {
1099            return Ok(avdp);
1100        }
1101    }
1102    // Last-resort: try N-256 by extending the file to discover its size.
1103    if let Ok(end) = reader.seek(SeekFrom::End(0)) {
1104        let total_sectors = end / SECTOR_SIZE;
1105        if total_sectors > 256 {
1106            let sec = total_sectors - 256;
1107            if let Ok(buf) = read_sector(reader, sec) {
1108                if let Ok(avdp) = AnchorVolumeDescriptorPointer::parse(&buf) {
1109                    return Ok(avdp);
1110                }
1111            }
1112        }
1113    }
1114    Err(Error::InvalidUdf(
1115        "no Anchor Volume Descriptor Pointer found",
1116    ))
1117}
1118
1119// ─────────────────────── sector helpers ───────────────────────
1120
1121fn read_sector<R: Read + Seek>(r: &mut R, sector: u64) -> Result<[u8; SECTOR_SIZE as usize]> {
1122    r.seek(SeekFrom::Start(sector * SECTOR_SIZE))?;
1123    let mut buf = [0u8; SECTOR_SIZE as usize];
1124    r.read_exact(&mut buf)?;
1125    Ok(buf)
1126}
1127
1128fn read_sector_into_vec<R: Read + Seek>(r: &mut R, sector: u64) -> Result<Vec<u8>> {
1129    Ok(read_sector(r, sector)?.to_vec())
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134    use super::*;
1135
1136    #[test]
1137    fn descriptor_tag_round_trip() {
1138        let tag = DescriptorTag {
1139            id: TagId::FileSet,
1140            descriptor_version: 2,
1141            serial_number: 0x1234,
1142            crc: 0xABCD,
1143            crc_length: 200,
1144            location: 0xDEAD_BEEF,
1145        };
1146        let bytes = tag.encode();
1147        let parsed = DescriptorTag::parse(&bytes).unwrap();
1148        assert_eq!(parsed.id, TagId::FileSet);
1149        assert_eq!(parsed.descriptor_version, 2);
1150        assert_eq!(parsed.serial_number, 0x1234);
1151        assert_eq!(parsed.location, 0xDEAD_BEEF);
1152    }
1153
1154    #[test]
1155    fn tag_checksum_detects_corruption() {
1156        let tag = DescriptorTag {
1157            id: TagId::FileEntry,
1158            descriptor_version: 2,
1159            serial_number: 1,
1160            crc: 0,
1161            crc_length: 0,
1162            location: 100,
1163        };
1164        let mut bytes = tag.encode();
1165        bytes[12] ^= 0xFF;
1166        assert!(matches!(
1167            DescriptorTag::parse(&bytes),
1168            Err(Error::InvalidUdf(_))
1169        ));
1170    }
1171
1172    #[test]
1173    fn lb_addr_round_trip() {
1174        let a = LbAddr {
1175            block: 0x12345678,
1176            partition_ref: 7,
1177        };
1178        assert_eq!(LbAddr::parse(&a.encode()).unwrap(), a);
1179    }
1180
1181    #[test]
1182    fn short_ad_packs_extent_type() {
1183        let ad = ShortAd {
1184            length: 0x12345678 & 0x3FFF_FFFF,
1185            extent_type: 2,
1186            block_location: 99,
1187        };
1188        let parsed = ShortAd::parse(&ad.encode()).unwrap();
1189        assert_eq!(parsed.length, ad.length);
1190        assert_eq!(parsed.extent_type, ad.extent_type);
1191        assert_eq!(parsed.block_location, ad.block_location);
1192    }
1193
1194    #[test]
1195    fn long_ad_round_trip() {
1196        let a = LongAd {
1197            length: 4096,
1198            extent_type: 0,
1199            location: LbAddr {
1200                block: 12,
1201                partition_ref: 0,
1202            },
1203            implementation_use: [0xAA; 6],
1204        };
1205        let parsed = LongAd::parse(&a.encode()).unwrap();
1206        assert_eq!(parsed, a);
1207    }
1208
1209    #[test]
1210    fn ext_ad_round_trip() {
1211        let a = ExtAd {
1212            length: 2048,
1213            extent_type: 0,
1214            recorded_length: 2048,
1215            information_length: 2048,
1216            location: LbAddr {
1217                block: 77,
1218                partition_ref: 0,
1219            },
1220            implementation_use: [0xCC, 0xDD],
1221        };
1222        let parsed = ExtAd::parse(&a.encode()).unwrap();
1223        assert_eq!(parsed, a);
1224    }
1225
1226    #[test]
1227    fn dstring_8bit() {
1228        let payload = b"\x08VIDEO_TS";
1229        assert_eq!(decode_dstring(payload).unwrap(), "VIDEO_TS");
1230    }
1231
1232    #[test]
1233    fn dstring_16bit_be() {
1234        let mut payload = vec![16u8];
1235        for c in "DVD".chars() {
1236            let v = c as u16;
1237            payload.push((v >> 8) as u8);
1238            payload.push(v as u8);
1239        }
1240        assert_eq!(decode_dstring(&payload).unwrap(), "DVD");
1241    }
1242
1243    #[test]
1244    fn ad_type_from_flags_table() {
1245        assert_eq!(AdType::from_flags(0).unwrap(), AdType::Short);
1246        assert_eq!(AdType::from_flags(1).unwrap(), AdType::Long);
1247        assert_eq!(AdType::from_flags(2).unwrap(), AdType::Extended);
1248        assert_eq!(AdType::from_flags(3).unwrap(), AdType::EmbeddedInIcb);
1249        assert_eq!(AdType::from_flags(0xFFF8).unwrap(), AdType::Short);
1250    }
1251
1252    #[test]
1253    fn lvid_parses_partition_count() {
1254        let mut buf = vec![0u8; SECTOR_SIZE as usize];
1255        // Construct a tag with id=9 (LVID), version=2, location=200.
1256        let tag = DescriptorTag {
1257            id: TagId::LogicalVolumeIntegrity,
1258            descriptor_version: 2,
1259            serial_number: 0,
1260            crc: 0,
1261            crc_length: 0,
1262            location: 200,
1263        };
1264        buf[..16].copy_from_slice(&tag.encode());
1265        // number_of_partitions at offset 72; lvid_use stub at 76.
1266        buf[72..76].copy_from_slice(&1u32.to_le_bytes());
1267        buf[76..80].copy_from_slice(&0u32.to_le_bytes());
1268        let lvid = LogicalVolumeIntegrityDescriptor::parse(&buf).unwrap();
1269        assert_eq!(lvid.number_of_partitions, 1);
1270    }
1271}