Skip to main content

hadris_iso/
volume.rs

1#[cfg(feature = "std")]
2use super::io::Parsable;
3#[cfg(any(feature = "std", feature = "alloc"))]
4use super::io::{self, Read, Write};
5#[cfg(feature = "alloc")]
6use alloc::vec::Vec;
7use core::{ffi::CStr, fmt::Debug};
8
9use super::directory::RootDirectoryEntry;
10use crate::types::{
11    BigEndian, Charset, DecDateTime, Endian, IsoStr, IsoStrA, IsoStrD, LittleEndian, U16LsbMsb,
12    U32, U32LsbMsb,
13};
14
15/// Errors that can occur when parsing volume descriptors
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum VolumeError {
18    /// I/O error occurred
19    Io,
20    /// Invalid volume descriptor header (missing CD001 signature)
21    InvalidHeader,
22    /// Primary volume descriptor not found
23    PrimaryNotFound,
24}
25
26impl core::fmt::Display for VolumeError {
27    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
28        match self {
29            Self::Io => write!(f, "I/O error"),
30            Self::InvalidHeader => write!(
31                f,
32                "invalid volume descriptor header (missing CD001 signature)"
33            ),
34            Self::PrimaryNotFound => write!(f, "primary volume descriptor not found"),
35        }
36    }
37}
38
39#[cfg(feature = "std")]
40impl std::error::Error for VolumeError {}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43/// Identifies a VolumeDescriptorType value.
44pub enum VolumeDescriptorType {
45    /// The `BootRecord` variant.
46    BootRecord,
47    /// The `PrimaryVolumeDescriptor` variant.
48    PrimaryVolumeDescriptor,
49    /// The `SupplementaryVolumeDescriptor` variant.
50    SupplementaryVolumeDescriptor,
51    /// The `VolumePartitionDescriptor` variant.
52    VolumePartitionDescriptor,
53    /// The `VolumeSetTerminator` variant.
54    VolumeSetTerminator,
55    /// The `Unknown` variant.
56    Unknown(u8),
57}
58
59impl VolumeDescriptorType {
60    /// Performs the `to_u8` operation.
61    pub fn to_u8(self) -> u8 {
62        match self {
63            Self::BootRecord => 0x00,
64            Self::PrimaryVolumeDescriptor => 0x01,
65            Self::SupplementaryVolumeDescriptor => 0x02,
66            Self::VolumePartitionDescriptor => 0x03,
67            Self::VolumeSetTerminator => 0xFF,
68            Self::Unknown(value) => value,
69        }
70    }
71    /// Performs the `from_u8` operation.
72    pub fn from_u8(value: u8) -> Self {
73        match value {
74            0x00 => Self::BootRecord,
75            0x01 => Self::PrimaryVolumeDescriptor,
76            0x02 => Self::SupplementaryVolumeDescriptor,
77            0x03 => Self::VolumePartitionDescriptor,
78            0xFF => Self::VolumeSetTerminator,
79            value => Self::Unknown(value),
80        }
81    }
82}
83
84#[derive(Debug, Clone, Copy)]
85/// Identifies a VolumeDescriptor value.
86pub enum VolumeDescriptor {
87    /// The `BootRecord` variant.
88    BootRecord(BootRecordVolumeDescriptor),
89    /// The `Primary` variant.
90    Primary(PrimaryVolumeDescriptor),
91    /// The `Supplementary` variant.
92    Supplementary(SupplementaryVolumeDescriptor),
93    /// The `End` variant.
94    End(VolumeDescriptorSetTerminator),
95    /// The `Unknown` variant.
96    Unknown(UnknownVolumeDescriptor),
97}
98
99io_transform! {
100#[cfg(feature = "std")]
101impl Parsable for VolumeDescriptor {
102    async fn parse<R: Read>(reader: &mut R) -> io::Result<Self> {
103        let mut buf = [0u8; 2048];
104        reader.read_exact(&mut buf).await?;
105        let header = VolumeDescriptorHeader::from_bytes(&buf[0..7]);
106        if !header.is_valid() {
107            // Invalid, which means either we are at the wrong place, or the writer didn't
108            // write an end record
109            return Err(io::Error::new(
110                io::ErrorKind::InvalidData,
111                "Invalid volume descriptor header (missing CD001 signature)",
112            ));
113        }
114
115        Ok(VolumeDescriptor::new(buf))
116    }
117}
118} // io_transform!
119
120impl VolumeDescriptor {
121    /// Performs the `as_bytes` operation.
122    pub fn as_bytes(&self) -> &[u8] {
123        match self {
124            VolumeDescriptor::BootRecord(entry) => bytemuck::bytes_of(entry),
125            VolumeDescriptor::Primary(entry) => bytemuck::bytes_of(entry),
126            VolumeDescriptor::Supplementary(entry) => bytemuck::bytes_of(entry),
127            VolumeDescriptor::End(entry) => bytemuck::bytes_of(entry),
128            VolumeDescriptor::Unknown(entry) => bytemuck::bytes_of(entry),
129        }
130    }
131
132    /// Performs the `header` operation.
133    pub fn header(&self) -> VolumeDescriptorHeader {
134        match self {
135            VolumeDescriptor::BootRecord(entry) => entry.header,
136            VolumeDescriptor::Primary(entry) => entry.header,
137            VolumeDescriptor::Supplementary(entry) => entry.header,
138            VolumeDescriptor::End(entry) => entry.header,
139            VolumeDescriptor::Unknown(entry) => entry.header,
140        }
141    }
142
143    /// Performs the `new` operation.
144    pub fn new(data: [u8; 2048]) -> Self {
145        let ty = VolumeDescriptorType::from_u8(data[0]);
146        match ty {
147            VolumeDescriptorType::BootRecord => VolumeDescriptor::BootRecord(bytemuck::cast(data)),
148            VolumeDescriptorType::PrimaryVolumeDescriptor => {
149                VolumeDescriptor::Primary(bytemuck::cast(data))
150            }
151            VolumeDescriptorType::SupplementaryVolumeDescriptor => {
152                VolumeDescriptor::Supplementary(bytemuck::cast(data))
153            }
154            VolumeDescriptorType::VolumeSetTerminator => {
155                VolumeDescriptor::End(bytemuck::cast(data))
156            }
157            _ => VolumeDescriptor::Unknown(bytemuck::cast(data)),
158        }
159    }
160}
161
162#[cfg(feature = "alloc")]
163#[derive(Debug, Clone)]
164/// Represents VolumeDescriptorList.
165pub struct VolumeDescriptorList {
166    /// The `descriptors` field.
167    pub descriptors: Vec<VolumeDescriptor>,
168}
169
170#[cfg(feature = "alloc")]
171impl VolumeDescriptorList {
172    /// Performs the `empty` operation.
173    pub fn empty() -> Self {
174        Self {
175            descriptors: Vec::new(),
176        }
177    }
178
179    /// Performs the `primary` operation.
180    pub fn primary(&self) -> &PrimaryVolumeDescriptor {
181        self.try_primary()
182            .expect("Primary volume descriptor not found")
183    }
184
185    /// Returns the primary volume descriptor, if the sequence contains one.
186    pub fn try_primary(&self) -> Option<&PrimaryVolumeDescriptor> {
187        self.descriptors.iter().find_map(|d| match d {
188            VolumeDescriptor::Primary(d) => Some(d),
189            _ => None,
190        })
191    }
192
193    /// Performs the `primary_mut` operation.
194    pub fn primary_mut(&mut self) -> &mut PrimaryVolumeDescriptor {
195        self.descriptors
196            .iter_mut()
197            .find_map(|d| match d {
198                VolumeDescriptor::Primary(d) => Some(d),
199                _ => None,
200            })
201            .expect("Primary volume descriptor not found")
202    }
203
204    /// Performs the `supplementary` operation.
205    pub fn supplementary(&self) -> impl Iterator<Item = &SupplementaryVolumeDescriptor> {
206        self.descriptors.iter().filter_map(|d| match d {
207            VolumeDescriptor::Supplementary(d) => Some(d),
208            _ => None,
209        })
210    }
211
212    /// Performs the `supplementary_mut` operation.
213    pub fn supplementary_mut(
214        &mut self,
215    ) -> impl Iterator<Item = &mut SupplementaryVolumeDescriptor> {
216        self.descriptors.iter_mut().filter_map(|d| match d {
217            VolumeDescriptor::Supplementary(d) => Some(d),
218            _ => None,
219        })
220    }
221
222    /// Performs the `boot_record` operation.
223    pub fn boot_record(&self) -> Option<&BootRecordVolumeDescriptor> {
224        self.descriptors.iter().find_map(|d| match d {
225            VolumeDescriptor::BootRecord(d) => Some(d),
226            _ => None,
227        })
228    }
229
230    /// Performs the `boot_record_mut` operation.
231    pub fn boot_record_mut(&mut self) -> Option<&mut BootRecordVolumeDescriptor> {
232        self.descriptors.iter_mut().find_map(|d| match d {
233            VolumeDescriptor::BootRecord(d) => Some(d),
234            _ => None,
235        })
236    }
237
238    /// Performs the `push` operation.
239    pub fn push(&mut self, descriptor: VolumeDescriptor) {
240        self.descriptors.push(descriptor);
241    }
242
243    /// Performs the `insert` operation.
244    pub fn insert(&mut self, index: usize, descriptor: VolumeDescriptor) {
245        self.descriptors.insert(index, descriptor);
246    }
247
248    /// Performs the `size_required` operation.
249    pub fn size_required(&self) -> usize {
250        (self.descriptors.len() + 1) * 2048
251    }
252}
253
254io_transform! {
255#[cfg(feature = "alloc")]
256impl VolumeDescriptorList {
257    /// Parse the volume descriptor list from the given reader
258    ///
259    /// The caller should seek to the start of the volume descriptor list, which is usually at LBA 16
260    ///
261    /// # Errors
262    /// Returns an error if:
263    /// - I/O error occurs
264    /// - Volume descriptor header is invalid (missing CD001 signature)
265    pub async fn parse<T: Read>(reader: &mut T) -> Result<Self, io::Error> {
266        let mut descriptors = Vec::new();
267        let mut buffer = [0u8; 2048];
268        loop {
269            reader.read_exact(&mut buffer).await?;
270            let header = VolumeDescriptorHeader::from_bytes(&buffer[0..7]);
271            if !header.is_valid() {
272                return Err(io::Error::new(
273                    io::ErrorKind::InvalidData,
274                    "invalid volume descriptor identifier or version",
275                ));
276            }
277            let ty = VolumeDescriptorType::from_u8(header.descriptor_type);
278            if let VolumeDescriptorType::VolumeSetTerminator = ty {
279                if buffer[7..].iter().any(|byte| *byte != 0) {
280                    return Err(io::Error::new(
281                        io::ErrorKind::InvalidData,
282                        "volume descriptor set terminator body is not zero-filled",
283                    ));
284                }
285                break;
286            }
287
288            descriptors.push(VolumeDescriptor::new(buffer));
289        }
290
291        Ok(Self { descriptors })
292    }
293
294    /// Performs the `write` operation.
295    pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<usize> {
296        let mut written = 0;
297        for descriptor in &self.descriptors {
298            writer.write_all(descriptor.as_bytes()).await?;
299            written += 2048;
300        }
301        writer.write_all(VolumeDescriptorSetTerminator::new().to_bytes()).await?;
302        written += 2048;
303        Ok(written)
304    }
305}
306} // io_transform!
307
308#[repr(C)]
309#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
310/// Represents VolumeDescriptorHeader.
311pub struct VolumeDescriptorHeader {
312    /// The `descriptor_type` field.
313    pub descriptor_type: u8,
314    /// The `standard_identifier` field.
315    pub standard_identifier: IsoStrA<5>,
316    /// The `version` field.
317    pub version: u8,
318}
319
320impl Debug for VolumeDescriptorHeader {
321    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
322        f.debug_struct("VolumeDescriptorHeader")
323            .field(
324                "descriptor_type",
325                &VolumeDescriptorType::from_u8(self.descriptor_type),
326            )
327            .field("standard_identifier", &self.standard_identifier)
328            .field("version", &self.version)
329            .finish()
330    }
331}
332
333impl VolumeDescriptorHeader {
334    const IDENTIFIER: IsoStrA<5> = IsoStrA::from_bytes_exact(*b"CD001");
335    /// Performs the `new` operation.
336    pub fn new(ty: VolumeDescriptorType) -> Self {
337        Self {
338            descriptor_type: ty.to_u8(),
339            standard_identifier: Self::IDENTIFIER,
340            version: 1,
341        }
342    }
343
344    /// Performs the `is_valid` operation.
345    pub fn is_valid(&self) -> bool {
346        self.standard_identifier == Self::IDENTIFIER
347            && (self.version == 1
348                || (self.descriptor_type
349                    == VolumeDescriptorType::SupplementaryVolumeDescriptor.to_u8()
350                    && self.version == 2))
351    }
352
353    /// Performs the `from_bytes` operation.
354    pub fn from_bytes(bytes: &[u8]) -> &Self {
355        bytemuck::from_bytes(bytes)
356    }
357}
358
359#[repr(C)]
360#[derive(Clone, Copy)]
361/// Represents UnknownVolumeDescriptor.
362pub struct UnknownVolumeDescriptor {
363    /// The `header` field.
364    pub header: VolumeDescriptorHeader,
365    /// The `data` field.
366    pub data: [u8; 2041],
367}
368
369impl Debug for UnknownVolumeDescriptor {
370    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
371        f.debug_struct("UnknownVolumeDescriptor")
372            .field("header", &self.header)
373            .finish_non_exhaustive()
374    }
375}
376
377unsafe impl bytemuck::Zeroable for UnknownVolumeDescriptor {}
378unsafe impl bytemuck::Pod for UnknownVolumeDescriptor {}
379
380/// Builds a lossy identifier: truncates to fit and substitutes invalid
381/// characters instead of failing.
382fn lossy_identifier<C: Charset, const N: usize>(s: &str) -> IsoStr<C, N> {
383    let mut chars = [b' '; N];
384    let len = s.len().min(N);
385    chars[..len].copy_from_slice(&s.as_bytes()[..len]);
386    C::substitute_invalid(chars[..len].iter_mut());
387    IsoStr::from_bytes_exact(chars)
388}
389
390/// Primary Volume Descriptor (ECMA-119 8.4)
391///
392/// @hadris-spec ECMA-119:8.4
393/// @hadris-compliance partial
394/// @hadris-note Core fields are modeled, but reserved fields, character sets, redundant endian values, and semantic constraints are not all validated.
395/// @hadris-tests comprehensive_iso::test_pvd_standard_identifier
396/// @hadris-fuzz iso_read
397#[repr(C)]
398#[derive(Clone, Copy)]
399pub struct PrimaryVolumeDescriptor {
400    /// The `header` field.
401    pub header: VolumeDescriptorHeader,
402    /// The `unused0` field.
403    pub unused0: u8,
404    /// The `system_identifier` field.
405    pub system_identifier: IsoStrA<32>,
406    /// The `volume_identifier` field.
407    pub volume_identifier: IsoStrD<32>,
408    /// The `unused1` field.
409    pub unused1: [u8; 8],
410    /// The `volume_space_size` field.
411    pub volume_space_size: U32LsbMsb,
412    /// The `unused2` field.
413    pub unused2: [u8; 32],
414    /// The `volume_set_size` field.
415    pub volume_set_size: U16LsbMsb,
416    /// The `volume_sequence_number` field.
417    pub volume_sequence_number: U16LsbMsb,
418    /// The `logical_block_size` field.
419    pub logical_block_size: U16LsbMsb,
420    /// The `path_table_size` field.
421    pub path_table_size: U32LsbMsb,
422    /// The `type_l_path_table` field.
423    pub type_l_path_table: U32<LittleEndian>,
424    /// The `opt_type_l_path_table` field.
425    pub opt_type_l_path_table: U32<LittleEndian>,
426    /// The `type_m_path_table` field.
427    pub type_m_path_table: U32<BigEndian>,
428    /// The `opt_type_m_path_table` field.
429    pub opt_type_m_path_table: U32<BigEndian>,
430    /// The `dir_record` field.
431    pub dir_record: RootDirectoryEntry,
432    /// The `volume_set_identifier` field.
433    pub volume_set_identifier: IsoStrD<128>,
434    /// The `publisher_identifier` field.
435    pub publisher_identifier: IsoStrA<128>,
436    /// The `preparer_identifier` field.
437    pub preparer_identifier: IsoStrA<128>,
438    /// The `application_identifier` field.
439    pub application_identifier: IsoStrA<128>,
440    /// The `copyright_file_identifier` field.
441    pub copyright_file_identifier: IsoStrD<37>,
442    /// The `abstract_file_identifier` field.
443    pub abstract_file_identifier: IsoStrD<37>,
444    /// The `bibliographic_file_identifier` field.
445    pub bibliographic_file_identifier: IsoStrD<37>,
446    /// The `creation_date` field.
447    pub creation_date: DecDateTime,
448    /// The `modification_date` field.
449    pub modification_date: DecDateTime,
450    /// The `expiration_date` field.
451    pub expiration_date: DecDateTime,
452    /// The `effective_date` field.
453    pub effective_date: DecDateTime,
454    /// The `file_structure_version` field.
455    pub file_structure_version: u8,
456    /// The `unused3` field.
457    pub unused3: u8,
458    /// The `app_data` field.
459    pub app_data: [u8; 512],
460    /// The `reserved` field.
461    pub reserved: [u8; 653],
462}
463
464impl Debug for PrimaryVolumeDescriptor {
465    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
466        f.debug_struct("PrimaryVolumeDescriptor")
467            .field("header", &self.header)
468            .field("system_identifier", &self.system_identifier)
469            .field("volume_identifier", &self.volume_identifier)
470            .field("volume_space_size", &self.volume_space_size)
471            .field("volume_set_size", &self.volume_set_size)
472            .field("volume_sequence_number", &self.volume_sequence_number)
473            .field("logical_block_size", &self.logical_block_size)
474            .field("path_table_size", &self.path_table_size)
475            .field("type_l_path_table", &self.type_l_path_table)
476            .field("opt_type_l_path_table", &self.opt_type_l_path_table)
477            .field("type_m_path_table", &self.type_m_path_table)
478            .field("opt_type_m_path_table", &self.opt_type_m_path_table)
479            .field("dir_record", &self.dir_record)
480            .field("volume_set_identifier", &self.volume_set_identifier)
481            .field("publisher_identifier", &self.publisher_identifier)
482            .field("preparer_identifier", &self.preparer_identifier)
483            .field("application_identifier", &self.application_identifier)
484            .field("copyright_file_identifier", &self.copyright_file_identifier)
485            .field("abstract_file_identifier", &self.abstract_file_identifier)
486            .field(
487                "bibliographic_file_identifier",
488                &self.bibliographic_file_identifier,
489            )
490            .field("creation_date", &self.creation_date)
491            .field("modification_date", &self.modification_date)
492            .field("expiration_date", &self.expiration_date)
493            .field("effective_date", &self.effective_date)
494            .field("file_structure_version", &self.file_structure_version)
495            .finish_non_exhaustive()
496    }
497}
498
499impl PrimaryVolumeDescriptor {
500    /// Performs the `new` operation.
501    pub fn new(name: &str, sectors: u32) -> Self {
502        Self {
503            header: VolumeDescriptorHeader {
504                descriptor_type: VolumeDescriptorType::PrimaryVolumeDescriptor.to_u8(),
505                standard_identifier: IsoStrA::from_str("CD001").unwrap(),
506                version: 1,
507            },
508            unused0: 0,
509            system_identifier: IsoStrA::empty(),
510            volume_identifier: lossy_identifier(name),
511            unused1: [0; 8],
512            volume_space_size: U32LsbMsb::new(sectors),
513            unused2: [0; 32],
514            volume_set_size: U16LsbMsb::new(1),
515            volume_sequence_number: U16LsbMsb::new(1),
516            logical_block_size: U16LsbMsb::new(2048),
517            path_table_size: U32LsbMsb::new(0),
518            type_l_path_table: U32::<LittleEndian>::new(0),
519            opt_type_l_path_table: U32::<LittleEndian>::new(0),
520            type_m_path_table: U32::<BigEndian>::new(0),
521            opt_type_m_path_table: U32::<BigEndian>::new(0),
522            dir_record: RootDirectoryEntry::default(),
523            volume_set_identifier: IsoStrD::empty(),
524            publisher_identifier: IsoStrA::empty(),
525            preparer_identifier: IsoStrA::empty(),
526            application_identifier: IsoStrA::from_str("HADRIS-ISO").unwrap(),
527            copyright_file_identifier: IsoStrD::empty(),
528            abstract_file_identifier: IsoStrD::empty(),
529            bibliographic_file_identifier: IsoStrD::empty(),
530            creation_date: DecDateTime::now(),
531            modification_date: DecDateTime::now(),
532            expiration_date: DecDateTime::default(),
533            effective_date: DecDateTime::default(),
534            file_structure_version: 1,
535            unused3: 0,
536            app_data: [0; 512],
537            reserved: [0; 653],
538        }
539    }
540}
541
542unsafe impl bytemuck::Zeroable for PrimaryVolumeDescriptor {}
543unsafe impl bytemuck::Pod for PrimaryVolumeDescriptor {}
544
545/// Boot Record Volume Descriptor (ECMA-119 8.2), locating the El Torito boot
546/// catalog.
547///
548/// @hadris-spec ECMA-119:8.2
549/// @hadris-compliance partial
550/// @hadris-note The descriptor locates El Torito data, but all ECMA-119 boot-record semantics are not implemented.
551/// @hadris-tests xorriso_boot::test_hadris_multisection_boot_catalog
552/// @hadris-fuzz iso_read
553#[repr(C)]
554#[derive(Clone, Copy)]
555pub struct BootRecordVolumeDescriptor {
556    /// The `header` field.
557    pub header: VolumeDescriptorHeader,
558    /// The `boot_system_identifier` field.
559    pub boot_system_identifier: [u8; 32],
560    /// The `unused0` field.
561    pub unused0: [u8; 32],
562    /// The `catalog_ptr` field.
563    pub catalog_ptr: U32<LittleEndian>,
564    /// The `unused1` field.
565    pub unused1: [u8; 1973],
566}
567
568impl BootRecordVolumeDescriptor {
569    /// Performs the `new` operation.
570    pub fn new(catalog_sector: u32) -> Self {
571        const BOOT_SYSTEM_IDENTIFIER: [u8; 32] = *b"EL TORITO SPECIFICATION\0\0\0\0\0\0\0\0\0";
572        Self {
573            header: VolumeDescriptorHeader::new(VolumeDescriptorType::BootRecord),
574            boot_system_identifier: BOOT_SYSTEM_IDENTIFIER,
575            unused0: [0; 32],
576            catalog_ptr: U32::<LittleEndian>::new(catalog_sector),
577            unused1: [0; 1973],
578        }
579    }
580}
581
582impl Debug for BootRecordVolumeDescriptor {
583    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
584        let system_identifier = CStr::from_bytes_until_nul(&self.boot_system_identifier);
585        f.debug_struct("BootRecordVolumeDescriptor")
586            .field("header", &self.header)
587            .field("boot_system_identifier", &system_identifier)
588            .field("catalog_ptr", &self.catalog_ptr)
589            .finish_non_exhaustive()
590    }
591}
592
593unsafe impl bytemuck::Zeroable for BootRecordVolumeDescriptor {}
594unsafe impl bytemuck::Pod for BootRecordVolumeDescriptor {}
595
596/// Supplementary / Enhanced Volume Descriptor (ECMA-119 8.5), used here for the
597/// Joliet namespace.
598///
599/// @hadris-spec ECMA-119:8.5
600/// @hadris-compliance partial
601/// @hadris-note Joliet SVD is read/written (UCS-2, BMP only); the version-2 "enhanced" form is repurposed as a UDF-bridge signal rather than a conformant ISO 9660:1999 secondary descriptor.
602/// @hadris-fuzz iso_read
603#[repr(C)]
604#[derive(Clone, Copy)]
605pub struct SupplementaryVolumeDescriptor {
606    /// The `header` field.
607    pub header: VolumeDescriptorHeader,
608    /// The `flags` field.
609    pub flags: u8,
610    /// The `system_identifier` field.
611    pub system_identifier: IsoStrA<32>,
612    /// The `volume_identifier` field.
613    pub volume_identifier: IsoStrD<32>,
614    /// The `unused1` field.
615    pub unused1: [u8; 8],
616    /// The `volume_space_size` field.
617    pub volume_space_size: U32LsbMsb,
618    /// The `escape_sequences` field.
619    pub escape_sequences: [u8; 32],
620    /// The `volume_set_size` field.
621    pub volume_set_size: U16LsbMsb,
622    /// The `volume_sequence_number` field.
623    pub volume_sequence_number: U16LsbMsb,
624    /// The `logical_block_size` field.
625    pub logical_block_size: U16LsbMsb,
626    /// The `path_table_size` field.
627    pub path_table_size: U32LsbMsb,
628    /// The `type_l_path_table` field.
629    pub type_l_path_table: U32<LittleEndian>,
630    /// The `opt_type_l_path_table` field.
631    pub opt_type_l_path_table: U32<LittleEndian>,
632    /// The `type_m_path_table` field.
633    pub type_m_path_table: U32<BigEndian>,
634    /// The `opt_type_m_path_table` field.
635    pub opt_type_m_path_table: U32<BigEndian>,
636    /// The `dir_record` field.
637    pub dir_record: RootDirectoryEntry,
638    /// The `volume_set_identifier` field.
639    pub volume_set_identifier: IsoStrD<128>,
640    /// The `publisher_identifier` field.
641    pub publisher_identifier: IsoStrA<128>,
642    /// The `preparer_identifier` field.
643    pub preparer_identifier: IsoStrA<128>,
644    /// The `application_identifier` field.
645    pub application_identifier: IsoStrA<128>,
646    /// The `copyright_file_identifier` field.
647    pub copyright_file_identifier: IsoStrD<37>,
648    /// The `abstract_file_identifier` field.
649    pub abstract_file_identifier: IsoStrD<37>,
650    /// The `bibliographic_file_identifier` field.
651    pub bibliographic_file_identifier: IsoStrD<37>,
652    /// The `creation_date` field.
653    pub creation_date: DecDateTime,
654    /// The `modification_date` field.
655    pub modification_date: DecDateTime,
656    /// The `expiration_date` field.
657    pub expiration_date: DecDateTime,
658    /// The `effective_date` field.
659    pub effective_date: DecDateTime,
660    /// If set to 1, it is a SVD
661    /// If set to 2, it is a EVD
662    pub file_structure_version: u8,
663    /// The `unused3` field.
664    pub unused3: u8,
665    /// The `app_data` field.
666    pub app_data: [u8; 512],
667    /// The `reserved` field.
668    pub reserved: [u8; 653],
669}
670
671impl SupplementaryVolumeDescriptor {
672    /// Encode a string as UTF-16BE into a fixed-size byte array, padded with UTF-16BE spaces.
673    pub fn utf16be_str<C: Charset, const N: usize>(s: &str) -> IsoStr<C, N> {
674        let mut bytes = [0u8; N];
675        let paired = N & !1; // round down to even
676        // Fill with UTF-16BE spaces (0x00, 0x20)
677        for i in (0..paired).step_by(2) {
678            bytes[i] = 0x00;
679            bytes[i + 1] = 0x20;
680        }
681        // Encode string as UTF-16BE
682        let mut pos = 0;
683        for c in s.encode_utf16() {
684            if pos + 2 > paired {
685                break;
686            }
687            let be = c.to_be_bytes();
688            bytes[pos] = be[0];
689            bytes[pos + 1] = be[1];
690            pos += 2;
691        }
692        IsoStr::from_bytes_exact(bytes)
693    }
694
695    /// Create an empty UTF-16BE padded field (all UTF-16BE spaces).
696    pub fn utf16be_empty<C: Charset, const N: usize>() -> IsoStr<C, N> {
697        let mut bytes = [0u8; N];
698        let paired = N & !1;
699        for i in (0..paired).step_by(2) {
700            bytes[i] = 0x00;
701            bytes[i + 1] = 0x20;
702        }
703        IsoStr::from_bytes_exact(bytes)
704    }
705
706    /// Performs the `new_svd` operation.
707    pub fn new_svd(name: &str, sectors: u32, escape_sequences: [u8; 32]) -> Self {
708        Self {
709            header: VolumeDescriptorHeader {
710                descriptor_type: VolumeDescriptorType::SupplementaryVolumeDescriptor.to_u8(),
711                standard_identifier: IsoStrA::from_str("CD001").unwrap(),
712                version: 1,
713            },
714            flags: 0,
715            system_identifier: Self::utf16be_empty(),
716            volume_identifier: Self::utf16be_str(name),
717            unused1: [0; 8],
718            volume_space_size: U32LsbMsb::new(sectors),
719            escape_sequences,
720            volume_set_size: U16LsbMsb::new(1),
721            volume_sequence_number: U16LsbMsb::new(1),
722            logical_block_size: U16LsbMsb::new(2048),
723            path_table_size: U32LsbMsb::new(0),
724            type_l_path_table: U32::<LittleEndian>::new(0),
725            opt_type_l_path_table: U32::<LittleEndian>::new(0),
726            type_m_path_table: U32::<BigEndian>::new(0),
727            opt_type_m_path_table: U32::<BigEndian>::new(0),
728            dir_record: RootDirectoryEntry::default(),
729            volume_set_identifier: Self::utf16be_empty(),
730            publisher_identifier: Self::utf16be_empty(),
731            preparer_identifier: Self::utf16be_empty(),
732            application_identifier: Self::utf16be_str("HADRIS-ISO"),
733            copyright_file_identifier: Self::utf16be_empty(),
734            abstract_file_identifier: Self::utf16be_empty(),
735            bibliographic_file_identifier: Self::utf16be_empty(),
736            creation_date: DecDateTime::now(),
737            modification_date: DecDateTime::now(),
738            expiration_date: DecDateTime::default(),
739            effective_date: DecDateTime::default(),
740            file_structure_version: 1,
741            unused3: 0,
742            app_data: [0; 512],
743            reserved: [0; 653],
744        }
745    }
746
747    /// Performs the `new_evd` operation.
748    pub fn new_evd(name: &str, sectors: u32) -> Self {
749        Self {
750            header: VolumeDescriptorHeader {
751                descriptor_type: VolumeDescriptorType::SupplementaryVolumeDescriptor.to_u8(),
752                standard_identifier: IsoStrA::from_str("CD001").unwrap(),
753                version: 2,
754            },
755            flags: 0,
756            system_identifier: IsoStrA::empty(),
757            volume_identifier: lossy_identifier(name),
758            unused1: [0; 8],
759            volume_space_size: U32LsbMsb::new(sectors),
760            escape_sequences: [b' '; 32],
761            volume_set_size: U16LsbMsb::new(1),
762            volume_sequence_number: U16LsbMsb::new(1),
763            logical_block_size: U16LsbMsb::new(2048),
764            path_table_size: U32LsbMsb::new(0),
765            type_l_path_table: U32::<LittleEndian>::new(0),
766            opt_type_l_path_table: U32::<LittleEndian>::new(0),
767            type_m_path_table: U32::<BigEndian>::new(0),
768            opt_type_m_path_table: U32::<BigEndian>::new(0),
769            dir_record: RootDirectoryEntry::default(),
770            volume_set_identifier: IsoStrD::empty(),
771            publisher_identifier: IsoStrA::empty(),
772            preparer_identifier: IsoStrA::empty(),
773            application_identifier: IsoStrA::from_str("HADRIS-ISO").unwrap(),
774            copyright_file_identifier: IsoStrD::empty(),
775            abstract_file_identifier: IsoStrD::empty(),
776            bibliographic_file_identifier: IsoStrD::empty(),
777            creation_date: DecDateTime::now(),
778            modification_date: DecDateTime::now(),
779            expiration_date: DecDateTime::default(),
780            effective_date: DecDateTime::default(),
781            file_structure_version: 2,
782            unused3: 0,
783            app_data: [0; 512],
784            reserved: [0; 653],
785        }
786    }
787}
788
789unsafe impl bytemuck::Zeroable for SupplementaryVolumeDescriptor {}
790unsafe impl bytemuck::Pod for SupplementaryVolumeDescriptor {}
791
792impl Debug for SupplementaryVolumeDescriptor {
793    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
794        f.debug_struct("SupplementaryVolumeDescriptor")
795            .field("header", &self.header)
796            .field("flags", &self.flags)
797            .field("system_identifier", &self.system_identifier)
798            .field("volume_identifier", &self.volume_identifier)
799            .field("volume_space_size", &self.volume_space_size)
800            .field("escape_sequences", &self.escape_sequences)
801            .field("volume_set_size", &self.volume_set_size)
802            .field("volume_sequence_number", &self.volume_sequence_number)
803            .field("logical_block_size", &self.logical_block_size)
804            .field("path_table_size", &self.path_table_size)
805            .field("type_l_path_table", &self.type_l_path_table)
806            .field("opt_type_l_path_table", &self.opt_type_l_path_table)
807            .field("type_m_path_table", &self.type_m_path_table)
808            .field("opt_type_m_path_table", &self.opt_type_m_path_table)
809            .field("dir_record", &self.dir_record)
810            .field("volume_set_identifier", &self.volume_set_identifier)
811            .field("publisher_identifier", &self.publisher_identifier)
812            .field("preparer_identifier", &self.preparer_identifier)
813            .field("application_identifier", &self.application_identifier)
814            .field("copyright_file_identifier", &self.copyright_file_identifier)
815            .field("abstract_file_identifier", &self.abstract_file_identifier)
816            .field(
817                "bibliographic_file_identifier",
818                &self.bibliographic_file_identifier,
819            )
820            .field("creation_date", &self.creation_date)
821            .field("modification_date", &self.modification_date)
822            .field("expiration_date", &self.expiration_date)
823            .field("effective_date", &self.effective_date)
824            .field("file_structure_version", &self.file_structure_version)
825            .finish_non_exhaustive()
826    }
827}
828
829/// Volume Descriptor Set Terminator (ECMA-119 8.3).
830///
831/// @hadris-spec ECMA-119:8.3
832/// @hadris-compliance partial
833/// @hadris-note The descriptor is emitted and recognized, but the audit has not established validation of every reserved byte.
834/// @hadris-tests comprehensive_iso::test_volume_descriptor_set_terminator
835/// @hadris-fuzz iso_read
836#[repr(C)]
837#[derive(Clone, Copy)]
838pub struct VolumeDescriptorSetTerminator {
839    header: VolumeDescriptorHeader,
840    padding: [u8; 2041],
841}
842
843impl Default for VolumeDescriptorSetTerminator {
844    fn default() -> Self {
845        Self::new()
846    }
847}
848
849impl VolumeDescriptorSetTerminator {
850    /// Performs the `new` operation.
851    pub fn new() -> Self {
852        Self {
853            header: VolumeDescriptorHeader {
854                descriptor_type: VolumeDescriptorType::VolumeSetTerminator.to_u8(),
855                standard_identifier: VolumeDescriptorHeader::IDENTIFIER,
856                version: 1,
857            },
858            padding: [0; 2041],
859        }
860    }
861
862    /// Performs the `to_bytes` operation.
863    pub fn to_bytes(&self) -> &[u8] {
864        bytemuck::bytes_of(self)
865    }
866}
867
868impl Debug for VolumeDescriptorSetTerminator {
869    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
870        f.debug_struct("VolumeDescriptorSetTerminator")
871            .field("header", &self.header)
872            .finish_non_exhaustive()
873    }
874}
875
876unsafe impl bytemuck::Zeroable for VolumeDescriptorSetTerminator {}
877unsafe impl bytemuck::Pod for VolumeDescriptorSetTerminator {}
878
879#[cfg(test)]
880mod tests {
881    use super::*;
882
883    static_assertions::assert_eq_size!(PrimaryVolumeDescriptor, [u8; 2048]);
884    static_assertions::assert_eq_size!(VolumeDescriptorSetTerminator, [u8; 2048]);
885    static_assertions::assert_eq_size!(BootRecordVolumeDescriptor, [u8; 2048]);
886    static_assertions::assert_eq_size!(UnknownVolumeDescriptor, [u8; 2048]);
887
888    static_assertions::assert_eq_align!(PrimaryVolumeDescriptor, u8);
889    static_assertions::assert_eq_align!(VolumeDescriptorSetTerminator, u8);
890    static_assertions::assert_eq_align!(BootRecordVolumeDescriptor, u8);
891
892    #[test]
893    fn pvd_new_truncates_overlong_volume_name_without_panicking() {
894        let name = "A".repeat(40);
895        let pvd = PrimaryVolumeDescriptor::new(&name, 0);
896        assert_eq!(pvd.volume_identifier.to_str(), "A".repeat(32));
897    }
898
899    #[test]
900    fn evd_new_truncates_overlong_volume_name_without_panicking() {
901        let name = "B".repeat(40);
902        let evd = SupplementaryVolumeDescriptor::new_evd(&name, 0);
903        assert_eq!(evd.volume_identifier.to_str(), "B".repeat(32));
904    }
905
906    sync_only! {
907        use std::io::Cursor;
908
909        #[test]
910        fn descriptor_parser_rejects_invalid_terminator_header() {
911            let mut bytes = [0_u8; 2048];
912            bytes[0] = VolumeDescriptorType::VolumeSetTerminator.to_u8();
913            bytes[1..6].copy_from_slice(b"WRONG");
914            bytes[6] = 1;
915            let error = VolumeDescriptorList::parse(&mut Cursor::new(bytes)).unwrap_err();
916            assert_eq!(error.kind(), io::ErrorKind::InvalidData);
917        }
918
919        #[test]
920        fn descriptor_parser_rejects_nonzero_terminator_body() {
921            let mut bytes = [0_u8; 2048];
922            bytes.copy_from_slice(VolumeDescriptorSetTerminator::new().to_bytes());
923            bytes[7] = 1;
924            let error = VolumeDescriptorList::parse(&mut Cursor::new(bytes)).unwrap_err();
925            assert_eq!(error.kind(), io::ErrorKind::InvalidData);
926        }
927    }
928}