Skip to main content

hadris_iso/
boot.rs

1//! The El Torito boot specification
2//!
3//! This is used for booting from CDs and DVDs
4
5use super::io::{self, Read, Seek, Write};
6use core::fmt::Debug;
7
8use crate::types::{Endian, LittleEndian, U16, U32};
9#[cfg(feature = "alloc")]
10use alloc::vec::Vec;
11
12sync_only! {
13#[cfg(feature = "write")]
14use super::{
15    boot::options::BootOptions,
16    volume::BootRecordVolumeDescriptor,
17    write::{InputEntry, InputEntryKind, InputTree},
18};
19}
20
21/// Errors that can occur during boot catalog operations
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum BootError {
24    /// I/O error occurred
25    Io,
26    /// Validation entry checksum is invalid
27    InvalidValidationChecksum,
28    /// Validation entry header ID is invalid (expected 0x01)
29    InvalidValidationHeader,
30    /// Validation entry key bytes are invalid (expected 0x55, 0xAA)
31    InvalidValidationKey,
32    /// Default boot entry is not marked as bootable
33    InvalidDefaultEntry,
34    /// Expected a section header but got something else
35    ExpectedSectionHeader(u8),
36    /// Boot catalog ended unexpectedly (more sections expected)
37    UnexpectedEndOfCatalog,
38}
39
40impl core::fmt::Display for BootError {
41    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
42        match self {
43            Self::Io => write!(f, "I/O error"),
44            Self::InvalidValidationChecksum => {
45                write!(f, "invalid boot catalog validation checksum")
46            }
47            Self::InvalidValidationHeader => {
48                write!(f, "invalid boot catalog validation header (expected 0x01)")
49            }
50            Self::InvalidValidationKey => write!(
51                f,
52                "invalid boot catalog validation key (expected 0x55, 0xAA)"
53            ),
54            Self::InvalidDefaultEntry => write!(f, "default boot entry is not marked as bootable"),
55            Self::ExpectedSectionHeader(id) => write!(f, "expected section header, got: {id:#x}"),
56            Self::UnexpectedEndOfCatalog => write!(f, "boot catalog ended unexpectedly"),
57        }
58    }
59}
60
61#[cfg(feature = "std")]
62impl std::error::Error for BootError {}
63
64// Types for El Torito boot catalogue
65// The boot catalogue consists of a series of boot catalogue entries:
66// First, the validation entry
67// Next, the initial/default entry
68// Section headers,
69// Section entries,
70// Section entry extensions
71
72/// The base of the boot catalog
73/// This is the minimum required specified by the El-Torito specification
74#[derive(Debug, Clone)]
75pub struct BaseBootCatalog {
76    /// The `validation` field.
77    pub validation: BootValidationEntry,
78    /// The `default_entry` field.
79    pub default_entry: BootSectionEntry,
80}
81
82impl Default for BaseBootCatalog {
83    fn default() -> Self {
84        Self::new(EmulationType::NoEmulation, 0, 0, 0)
85    }
86}
87
88impl BaseBootCatalog {
89    /// Performs the `new` operation.
90    pub fn new(
91        media_type: EmulationType,
92        load_segment: u16,
93        sector_count: u16,
94        load_rba: u32,
95    ) -> Self {
96        Self {
97            validation: BootValidationEntry::new(),
98            default_entry: BootSectionEntry::new(media_type, load_segment, sector_count, load_rba),
99        }
100    }
101}
102
103/// Boot catalogue (requires alloc feature for dynamic sections)
104#[cfg(feature = "alloc")]
105#[derive(Debug, Clone)]
106pub struct BootCatalog {
107    base: BaseBootCatalog,
108    sections: Vec<(BootSectionHeaderEntry, Vec<BootSectionEntry>)>,
109}
110
111#[cfg(feature = "alloc")]
112impl Default for BootCatalog {
113    fn default() -> Self {
114        Self::new(EmulationType::NoEmulation, 0, 0, 0)
115    }
116}
117
118#[cfg(feature = "alloc")]
119impl BootCatalog {
120    /// Performs the `new` operation.
121    pub fn new(
122        media_type: EmulationType,
123        load_segment: u16,
124        sector_count: u16,
125        load_rba: u32,
126    ) -> Self {
127        Self {
128            base: BaseBootCatalog::new(media_type, load_segment, sector_count, load_rba),
129            sections: Vec::new(),
130        }
131    }
132
133    /// Performs the `set_default_entry` operation.
134    pub fn set_default_entry(&mut self, entry: BootSectionEntry) {
135        self.base.default_entry = entry;
136    }
137
138    /// Performs the `add_section` operation.
139    pub fn add_section(&mut self, platform_id: PlatformId, entries: Vec<BootSectionEntry>) {
140        if let Some((header, _entry)) = self.sections.last_mut() {
141            // No longer the last section
142            header.header_type = 0x90;
143        }
144
145        let header = BootSectionHeaderEntry {
146            header_type: 0x91,
147            platform_id: platform_id.to_u8(),
148            section_count: U16::new(1),
149            section_ident: [0; 28],
150        };
151
152        self.sections.push((header, entries));
153    }
154
155    /// Returns the total size of the boot catalog in bytes
156    ///
157    /// This includes:
158    /// - 32 bytes for the validation entry
159    /// - 32 bytes for the default/initial entry
160    /// - 32 bytes for each section header
161    /// - 32 bytes for each section entry
162    /// - 32 bytes for the terminator entry
163    pub fn size(&self) -> usize {
164        // Base: validation (32) + default entry (32) = 64 bytes
165        // Each section: header (32) + entries (32 each)
166        // Terminator: 32 bytes
167        let sections_size: usize = self
168            .sections
169            .iter()
170            .map(|(_, entries)| (entries.len() + 1) * 32)
171            .sum();
172        64 + sections_size + 32 // +32 for terminator
173    }
174}
175
176#[derive(Debug, Clone, Copy)]
177/// Identifies a BootCatalogEntry value.
178pub enum BootCatalogEntry {
179    /// The `Validation` variant.
180    Validation(BootValidationEntry),
181    /// The `SectionHeader` variant.
182    SectionHeader(BootSectionHeaderEntry),
183    /// The `SectionEntry` variant.
184    SectionEntry(BootSectionEntry),
185    /// The `SectionEntryExtension` variant.
186    SectionEntryExtension(BootSectionEntryExtension),
187}
188
189impl BootCatalogEntry {
190    /// Performs the `as_bytes` operation.
191    pub fn as_bytes(&self) -> &[u8] {
192        match self {
193            BootCatalogEntry::Validation(entry) => bytemuck::bytes_of(entry),
194            BootCatalogEntry::SectionHeader(entry) => bytemuck::bytes_of(entry),
195            BootCatalogEntry::SectionEntry(entry) => bytemuck::bytes_of(entry),
196            BootCatalogEntry::SectionEntryExtension(entry) => bytemuck::bytes_of(entry),
197        }
198    }
199
200    /// Performs the `size` operation.
201    pub const fn size(&self) -> usize {
202        32
203    }
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207/// Identifies a PlatformId value.
208pub enum PlatformId {
209    /// This is for X8086, X86, and X86_64 architectures.
210    X80X86,
211    /// The `PowerPC` variant.
212    PowerPC,
213    /// The `Macintosh` variant.
214    Macintosh,
215    /// The `UEFI` variant.
216    UEFI,
217    /// The `Unknown` variant.
218    Unknown(u8),
219}
220
221impl PlatformId {
222    /// Performs the `from_u8` operation.
223    pub fn from_u8(value: u8) -> Self {
224        match value {
225            0x00 => Self::X80X86,
226            0x01 => Self::PowerPC,
227            0x02 => Self::Macintosh,
228            0xEF => Self::UEFI,
229            value => Self::Unknown(value),
230        }
231    }
232
233    /// Performs the `to_u8` operation.
234    pub fn to_u8(self) -> u8 {
235        match self {
236            Self::X80X86 => 0x00,
237            Self::PowerPC => 0x01,
238            Self::Macintosh => 0x02,
239            Self::UEFI => 0xEF,
240            Self::Unknown(value) => value,
241        }
242    }
243}
244
245/// El Torito boot catalog validation entry.
246///
247/// @hadris-spec El-Torito:validation
248/// @hadris-compliance partial
249/// @hadris-note The catalog entry is modeled and interoperability-tested, but the audit has not established clause-complete validation.
250/// @hadris-tests iso::boot::test_eltorito_boot_catalog_comparison
251/// @hadris-fuzz iso_read
252#[repr(C)]
253#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
254pub struct BootValidationEntry {
255    /// The `header_id` field.
256    pub header_id: u8,
257    /// The `platform_id` field.
258    pub platform_id: u8,
259    /// The `reserved` field.
260    pub reserved: [u8; 2],
261    /// The `manufacturer` field.
262    pub manufacturer: [u8; 24],
263    /// The `checksum` field.
264    pub checksum: U16<LittleEndian>,
265    /// 0x55AA
266    pub key: [u8; 2],
267}
268
269impl Default for BootValidationEntry {
270    fn default() -> Self {
271        Self::new()
272    }
273}
274
275impl BootValidationEntry {
276    /// Performs the `new` operation.
277    pub fn new() -> Self {
278        let mut entry = Self {
279            header_id: 1,
280            platform_id: 0,
281            reserved: [0; 2],
282            manufacturer: [0; 24],
283            checksum: U16::new(0),
284            key: [0x55, 0xAA],
285        };
286        entry.checksum.set(entry.calculate_checksum());
287        entry
288    }
289}
290
291impl Debug for BootValidationEntry {
292    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
293        f.debug_struct("BootValidationEntry")
294            .field("header_id", &format_args!("{:#x}", self.header_id))
295            .field("platform_id", &PlatformId::from_u8(self.platform_id))
296            .field(
297                "manufacturer",
298                &core::str::from_utf8(&self.manufacturer).unwrap(),
299            )
300            .field("checksum", &self.checksum.get())
301            .field("key", &self.key)
302            .finish_non_exhaustive()
303    }
304}
305
306impl BootValidationEntry {
307    /// Check if the validation entry is valid (legacy method)
308    pub fn is_valid(&self) -> bool {
309        self.header_id == 0x01
310            && self.key == [0x55, 0xAA]
311            && self.checksum.get() == self.calculate_checksum()
312    }
313
314    /// Validate the entry and return a detailed error if invalid
315    pub fn validate(&self) -> Result<(), BootError> {
316        if self.header_id != 0x01 {
317            return Err(BootError::InvalidValidationHeader);
318        }
319        if self.key != [0x55, 0xAA] {
320            return Err(BootError::InvalidValidationKey);
321        }
322        if self.checksum.get() != self.calculate_checksum() {
323            return Err(BootError::InvalidValidationChecksum);
324        }
325        Ok(())
326    }
327
328    /// Calculates the checksum of the boot catalogue
329    ///
330    /// The checksum works such that the checksum of the data (including checksum bytes) is 0.
331    /// We can do this by finding the sum of the data without the checksum bytes, and negating it
332    /// (using two's complement).
333    pub fn calculate_checksum(&self) -> u16 {
334        // We know the size of the struct, we we can just stack allocate a buffer and copy the data
335        let mut bytes = [0u8; 32];
336        bytes.copy_from_slice(bytemuck::bytes_of(self));
337        // Zero out the checksum bytes (we are basically just ignoring them), since we need to find
338        // what the data equal without them
339        bytes[28] = 0;
340        bytes[29] = 0;
341        let mut checksum = 0u16;
342        for i in (0..32).step_by(2) {
343            let value = u16::from_le_bytes([bytes[i], bytes[i + 1]]);
344            checksum = checksum.wrapping_add(value);
345        }
346        // We use two's complement to negate the checksum, so that the checksum + data = 0 (in 16-bit)
347        (!checksum) + 1
348    }
349}
350
351/// El Torito section header entry (0x90/0x91) introducing a platform's boot
352/// entries.
353///
354/// @hadris-spec El-Torito:section-header
355/// @hadris-compliance partial
356/// @hadris-note The catalog entry is modeled and interoperability-tested, but the audit has not established clause-complete validation.
357/// @hadris-tests iso::boot::test_hadris_multisection_boot_catalog
358/// @hadris-fuzz iso_read
359#[repr(C)]
360#[derive(Clone, Copy)]
361pub struct BootSectionHeaderEntry {
362    /// 0x90 = Header, more headers follow
363    /// 0x91 = Final header
364    pub header_type: u8,
365    /// The `platform_id` field.
366    pub platform_id: u8,
367    /// The `section_count` field.
368    pub section_count: U16<LittleEndian>,
369    /// The `section_ident` field.
370    pub section_ident: [u8; 28],
371}
372
373impl Debug for BootSectionHeaderEntry {
374    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
375        f.debug_struct("BootSectionHeaderEntry")
376            .field("header_type", &format_args!("{:#x}", self.header_type))
377            .field("platform_id", &PlatformId::from_u8(self.platform_id))
378            .field("section_count", &self.section_count.get())
379            .field(
380                "section_ident",
381                &core::str::from_utf8(&self.section_ident).unwrap(),
382            )
383            .finish_non_exhaustive()
384    }
385}
386
387unsafe impl bytemuck::Zeroable for BootSectionHeaderEntry {}
388unsafe impl bytemuck::Pod for BootSectionHeaderEntry {}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391/// El Torito boot-media emulation type (the low nibble of the boot media type
392/// byte, ECMA "El Torito" §2.x).
393pub enum EmulationType {
394    /// 0x00 = No emulation. The firmware loads `sector_count` 512-byte sectors.
395    NoEmulation,
396    /// 0x01 = 1.2 MB diskette emulation.
397    Floppy1_2,
398    /// 0x02 = 1.44 MB diskette emulation.
399    Floppy1_44,
400    /// 0x03 = 2.88 MB diskette emulation.
401    Floppy2_88,
402    /// 0x04 = Hard-disk emulation (drive 0x80).
403    HardDisk,
404    /// Any other (or flag-decorated) media type byte, preserved verbatim.
405    Unknown(u8),
406}
407
408impl EmulationType {
409    /// Performs the `from_u8` operation.
410    pub fn from_u8(value: u8) -> Self {
411        match value {
412            0x00 => Self::NoEmulation,
413            0x01 => Self::Floppy1_2,
414            0x02 => Self::Floppy1_44,
415            0x03 => Self::Floppy2_88,
416            0x04 => Self::HardDisk,
417            value => Self::Unknown(value),
418        }
419    }
420
421    /// Performs the `to_u8` operation.
422    pub fn to_u8(self) -> u8 {
423        match self {
424            Self::NoEmulation => 0x00,
425            Self::Floppy1_2 => 0x01,
426            Self::Floppy1_44 => 0x02,
427            Self::Floppy2_88 => 0x03,
428            Self::HardDisk => 0x04,
429            Self::Unknown(value) => value,
430        }
431    }
432
433    /// Returns `true` for any emulated media (floppy or hard disk), i.e. not
434    /// [`NoEmulation`](Self::NoEmulation). Emulated entries load a single
435    /// virtual boot sector; the firmware then services the emulated medium.
436    ///
437    /// Only the media-type nibble (low 4 bits) is examined, matching the El
438    /// Torito layout where the upper bits are flags (continuation, contains
439    /// ATAPI driver). A flag-decorated byte whose low nibble is `0x0` — a
440    /// no-emulation entry — is therefore correctly reported as not emulated.
441    pub fn is_emulated(self) -> bool {
442        (self.to_u8() & 0x0F) != 0
443    }
444}
445
446/// El Torito section/initial entry: the bootable image reference and its
447/// emulation media type.
448///
449/// @hadris-spec El-Torito:section-entry
450/// @hadris-compliance partial
451/// @hadris-note The catalog entry is modeled and interoperability-tested, but the audit has not established clause-complete validation.
452/// @hadris-tests iso::boot::test_floppy_emulation_media_type_and_default_load_size
453/// @hadris-fuzz iso_read
454#[repr(C)]
455#[derive(Clone, Copy)]
456pub struct BootSectionEntry {
457    /// 0x88 = Bootable, 0x00 = Not bootable
458    pub boot_indicator: u8,
459    /// The `boot_media_type` field.
460    pub boot_media_type: u8,
461    /// The `load_segment` field.
462    pub load_segment: U16<LittleEndian>,
463    /// The `system_type` field.
464    pub system_type: u8,
465    /// The `reserved0` field.
466    pub reserved0: u8,
467    /// The `sector_count` field.
468    pub sector_count: U16<LittleEndian>,
469    /// The `load_rba` field.
470    pub load_rba: U32<LittleEndian>,
471    /// The `selection_criteria` field.
472    pub selection_criteria: u8,
473    /// The `vendor_unique` field.
474    pub vendor_unique: [u8; 19],
475}
476
477impl BootSectionEntry {
478    /// Performs the `new` operation.
479    pub fn new(
480        media_type: EmulationType,
481        load_segment: u16,
482        sector_count: u16,
483        load_rba: u32,
484    ) -> Self {
485        Self {
486            boot_indicator: 0x88,
487            boot_media_type: media_type.to_u8(),
488            load_segment: U16::new(load_segment),
489            system_type: 0,
490            reserved0: 0,
491            sector_count: U16::new(sector_count),
492            load_rba: U32::new(load_rba),
493            selection_criteria: 0,
494            vendor_unique: [0; 19],
495        }
496    }
497}
498
499impl Debug for BootSectionEntry {
500    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
501        f.debug_struct("BootSectionEntry")
502            .field(
503                "boot_indicator",
504                &format_args!("{:#x}", self.boot_indicator),
505            )
506            .field(
507                "boot_media_type",
508                &EmulationType::from_u8(self.boot_media_type),
509            )
510            .field("load_segment", &self.load_segment.get())
511            .field("system_type", &self.system_type)
512            .field("sector_count", &self.sector_count.get())
513            .field("load_rba", &self.load_rba.get())
514            .field("selection_criteria", &self.selection_criteria)
515            .finish_non_exhaustive()
516    }
517}
518
519impl BootSectionEntry {
520    /// Check if this entry is marked as bootable (0x88)
521    pub fn is_bootable(&self) -> bool {
522        self.boot_indicator == 0x88
523    }
524
525    /// Legacy alias for is_bootable
526    pub fn is_valid(&self) -> bool {
527        self.is_bootable()
528    }
529}
530
531unsafe impl bytemuck::Zeroable for BootSectionEntry {}
532unsafe impl bytemuck::Pod for BootSectionEntry {}
533
534#[repr(C)]
535#[derive(Debug, Clone, Copy)]
536/// Represents BootSectionEntryExtension.
537pub struct BootSectionEntryExtension {
538    // Must be 0x44
539    /// The `extension_indicator` field.
540    pub extension_indicator: u8,
541    // Bit 5: 1 = more extensions follow, 0 = final extension
542    /// The `flags` field.
543    pub flags: u8,
544    /// The `vendor_unique` field.
545    pub vendor_unique: [u8; 30],
546}
547
548unsafe impl bytemuck::Zeroable for BootSectionEntryExtension {}
549unsafe impl bytemuck::Pod for BootSectionEntryExtension {}
550
551/// Boot information table (16 bytes)
552///
553/// This table is located in the boot binary and contains information about the
554/// ISO image and the boot binary. It is written at offset 8 in the boot image.
555#[repr(C)]
556#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
557pub struct BootInfoTable {
558    /// The start LBA of the ISO image (This would 16 in most cases)
559    pub iso_start: U32<LittleEndian>,
560    /// The start LBA of the boot binary
561    pub file_lba: U32<LittleEndian>,
562    /// The length of the boot binary (in bytes)
563    pub file_len: U32<LittleEndian>,
564    /// The checksum of the boot binary
565    pub checksum: U32<LittleEndian>,
566}
567
568impl BootInfoTable {
569    /// Creates a new boot information table.
570    #[allow(dead_code)]
571    pub(crate) fn new(file_lba: u32, file_len: u32, checksum: u32) -> Self {
572        Self {
573            iso_start: U32::new(16),
574            file_lba: U32::new(file_lba),
575            file_len: U32::new(file_len),
576            checksum: U32::new(checksum),
577        }
578    }
579}
580
581/// GRUB2/ISOLINUX boot information table (56 bytes)
582///
583/// This is the extended boot info table format used by GRUB2 and ISOLINUX.
584/// It is the same as BootInfoTable but includes 40 bytes of reserved space.
585/// Written at offset 8 in the boot image.
586#[repr(C)]
587#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
588pub struct Grub2BootInfoTable {
589    /// The start LBA of the primary volume descriptor (usually 16)
590    pub pvd_lba: U32<LittleEndian>,
591    /// The start LBA of the boot binary
592    pub file_lba: U32<LittleEndian>,
593    /// The length of the boot binary (in bytes)
594    pub file_len: U32<LittleEndian>,
595    /// The checksum of the boot binary (sum of 32-bit words from offset 64 to EOF)
596    pub checksum: U32<LittleEndian>,
597    /// Reserved bytes (must be zero)
598    pub reserved: [u8; 40],
599}
600
601impl Grub2BootInfoTable {
602    /// Creates a new GRUB2/ISOLINUX boot information table.
603    #[allow(dead_code)]
604    pub(crate) fn new(file_lba: u32, file_len: u32, checksum: u32) -> Self {
605        Self {
606            pvd_lba: U32::new(16),
607            file_lba: U32::new(file_lba),
608            file_len: U32::new(file_len),
609            checksum: U32::new(checksum),
610            reserved: [0u8; 40],
611        }
612    }
613}
614
615io_transform! {
616impl BaseBootCatalog {
617    /// Parse the base boot catalog from the reader
618    ///
619    /// # Errors
620    /// Returns an error if:
621    /// - I/O error occurs
622    /// - Validation entry checksum is invalid
623    /// - Default entry is not marked as bootable
624    pub async fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self, BootError> {
625        let validation = BootValidationEntry::parse(reader).await.map_err(|_| BootError::Io)?;
626        validation.validate()?;
627
628        let default_entry = BootSectionEntry::parse(reader).await.map_err(|_| BootError::Io)?;
629        if !default_entry.is_bootable() {
630            return Err(BootError::InvalidDefaultEntry);
631        }
632
633        Ok(Self {
634            validation,
635            default_entry,
636        })
637    }
638
639    /// Performs the `write` operation.
640    pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
641        writer.write_all(bytemuck::bytes_of(&self.validation)).await?;
642        writer.write_all(bytemuck::bytes_of(&self.default_entry)).await?;
643        Ok(())
644    }
645}
646
647impl BootValidationEntry {
648    /// Performs the `parse` operation.
649    pub async fn parse<T: Read>(reader: &mut T) -> Result<Self, io::Error> {
650        let mut buf = [0u8; 32];
651        reader.read_exact(&mut buf).await?;
652        Ok(bytemuck::cast(buf))
653    }
654}
655
656impl BootSectionEntry {
657    /// Performs the `parse` operation.
658    pub async fn parse<T: Read>(reader: &mut T) -> Result<Self, io::Error> {
659        let mut buf: [u8; 32] = [0; 32];
660        reader.read_exact(&mut buf).await?;
661        Ok(bytemuck::cast(buf))
662    }
663}
664
665#[cfg(feature = "alloc")]
666impl BootCatalog {
667    /// Parse the boot catalogue from the given reader,
668    /// expects the reader to seek to the start of the catalogue
669    ///
670    /// # Errors
671    /// Returns an error if the boot catalog is malformed or an I/O error occurs.
672    pub async fn parse<T: Read + Seek>(reader: &mut T) -> Result<Self, BootError> {
673        let base = BaseBootCatalog::parse(reader).await?;
674        let mut sections = Vec::new();
675        let mut buffer = [0u8; 32];
676        let mut has_more = false;
677        let mut header = None;
678        let mut entries = Vec::new();
679        loop {
680            reader.read_exact(&mut buffer).await.map_err(|_| BootError::Io)?;
681            match buffer[0] {
682                0x00 if !has_more => break,
683                0x90 => {
684                    has_more = true;
685                    if let Some(header) = header.take() {
686                        sections.push((header, entries));
687                        entries = Vec::new();
688                    }
689                    header = Some(bytemuck::cast(buffer));
690                }
691                0x91 => {
692                    has_more = false;
693                    if let Some(header) = header.take() {
694                        sections.push((header, entries));
695                        entries = Vec::new();
696                    }
697                    header = Some(bytemuck::cast(buffer));
698                }
699                id => {
700                    if header.is_none() {
701                        return Err(BootError::ExpectedSectionHeader(id));
702                    }
703                    entries.push(bytemuck::cast(buffer));
704                }
705            }
706        }
707
708        if has_more {
709            return Err(BootError::UnexpectedEndOfCatalog);
710        }
711        if let Some(header) = header {
712            sections.push((header, entries));
713        }
714
715        Ok(Self { base, sections })
716    }
717
718    /// Performs the `write` operation.
719    pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
720        self.base.write(writer).await?;
721        for (header, entries) in self.sections.iter() {
722            writer.write_all(bytemuck::bytes_of(header)).await?;
723            for entry in entries {
724                writer.write_all(bytemuck::bytes_of(entry)).await?;
725            }
726        }
727        // End of entries
728        writer.write_all(&[0; 32]).await?;
729        Ok(())
730    }
731}
732} // io_transform!
733
734sync_only! {
735#[cfg(feature = "write")]
736/// Represents ElToritoWriter.
737pub struct ElToritoWriter;
738
739#[cfg(feature = "write")]
740impl ElToritoWriter {
741    /// Creates the El-Torito ovlume descriptor based on the given options and files
742    /// This will append a boot catalogue to the given files if the options require it
743    /// When extra checks are enabled, this will also check that the boot entry paths are valid
744    /// (included in the files)
745    pub fn create_descriptor(
746        opts: &BootOptions,
747        files: &mut InputTree,
748    ) -> BootRecordVolumeDescriptor {
749        if opts.write_boot_catalog {
750            let size = 96 + opts.entries.len() * 64;
751            let size = (size + 2047) & !2047;
752            let dir_pos = files
753                .entries
754                .iter()
755                .position(|entry| matches!(entry.kind, InputEntryKind::Directory(_)))
756                .unwrap_or(0);
757            files.entries.insert(
758                dir_pos.saturating_sub(1),
759                InputEntry::file("boot.catalog", alloc::vec![0; size]),
760            );
761        }
762        BootRecordVolumeDescriptor::new(0)
763    }
764}
765}
766
767#[cfg(feature = "write")]
768/// APIs for options.
769pub mod options {
770    use alloc::string::String;
771    use alloc::vec::Vec;
772    use core::num::NonZeroU16;
773
774    use super::{EmulationType, PlatformId};
775
776    #[derive(Debug, Clone, Default)]
777    /// Represents BootOptions.
778    pub struct BootOptions {
779        /// The `write_boot_catalog` field.
780        pub write_boot_catalog: bool,
781        /// The `default` field.
782        pub default: BootEntryOptions,
783        /// The `entries` field.
784        pub entries: Vec<(BootSectionOptions, BootEntryOptions)>,
785    }
786
787    impl BootOptions {
788        /// Performs the `sections` operation.
789        pub fn sections(&self) -> Vec<(Option<BootSectionOptions>, BootEntryOptions)> {
790            let mut sections = Vec::with_capacity(self.entries.len() + 1);
791            sections.push((None, self.default.clone()));
792            for (section_ops, ops) in &self.entries {
793                sections.push((Some(section_ops.clone()), ops.clone()));
794            }
795            sections
796        }
797    }
798
799    #[derive(Debug, Clone)]
800    /// Represents BootSectionOptions.
801    pub struct BootSectionOptions {
802        /// The `platform` field.
803        pub platform: PlatformId,
804    }
805
806    #[derive(Debug, Clone)]
807    /// Represents BootEntryOptions.
808    pub struct BootEntryOptions {
809        /// Number of 512-byte virtual sectors loaded by firmware.
810        pub load_size: Option<NonZeroU16>,
811        /// Path of the prepared boot image inside the ISO.
812        ///
813        /// The image is treated as opaque. For floppy or hard-disk emulation,
814        /// callers must provide an image containing the required filesystem
815        /// and partition structures.
816        pub boot_image_path: String,
817        /// The `boot_info_table` field.
818        pub boot_info_table: bool,
819        /// The `grub2_boot_info` field.
820        pub grub2_boot_info: bool,
821        /// The `emulation` field.
822        pub emulation: EmulationType,
823    }
824
825    impl Default for BootEntryOptions {
826        fn default() -> Self {
827            Self {
828                load_size: None,
829                boot_image_path: String::new(),
830                boot_info_table: false,
831                grub2_boot_info: false,
832                emulation: EmulationType::NoEmulation,
833            }
834        }
835    }
836}
837
838sync_only! {
839#[cfg(all(test, feature = "alloc"))]
840mod tests {
841    use super::*;
842    use alloc::vec;
843    use alloc::vec::Vec;
844
845    static_assertions::assert_eq_size!(BootValidationEntry, [u8; 32]);
846    static_assertions::assert_eq_size!(BootSectionHeaderEntry, [u8; 32]);
847    static_assertions::assert_eq_size!(BootSectionEntry, [u8; 32]);
848
849    static_assertions::assert_eq_align!(BootValidationEntry, u8);
850    static_assertions::assert_eq_align!(BootSectionHeaderEntry, u8);
851    static_assertions::assert_eq_align!(BootSectionEntry, u8);
852
853    #[test]
854    fn test_validation_entry_new() {
855        let entry = BootValidationEntry::new();
856        assert_eq!(entry.header_id, 0x01);
857        assert_eq!(entry.key, [0x55, 0xAA]);
858        assert!(entry.is_valid());
859        assert!(entry.validate().is_ok());
860    }
861
862    #[test]
863    fn test_validation_entry_checksum() {
864        let entry = BootValidationEntry::new();
865        // Checksum should be calculated so that sum of all 16-bit words = 0
866        let bytes = bytemuck::bytes_of(&entry);
867        let mut sum = 0u16;
868        for i in (0..32).step_by(2) {
869            let value = u16::from_le_bytes([bytes[i], bytes[i + 1]]);
870            sum = sum.wrapping_add(value);
871        }
872        assert_eq!(sum, 0, "checksum should make total sum equal to 0");
873    }
874
875    #[test]
876    fn test_validation_entry_invalid_header() {
877        let mut entry = BootValidationEntry::new();
878        entry.header_id = 0x00;
879        assert!(!entry.is_valid());
880        assert_eq!(entry.validate(), Err(BootError::InvalidValidationHeader));
881    }
882
883    #[test]
884    fn test_validation_entry_invalid_key() {
885        let mut entry = BootValidationEntry::new();
886        entry.key = [0x00, 0x00];
887        assert!(!entry.is_valid());
888        assert_eq!(entry.validate(), Err(BootError::InvalidValidationKey));
889    }
890
891    #[test]
892    fn test_validation_entry_invalid_checksum() {
893        let mut entry = BootValidationEntry::new();
894        entry.checksum.set(0x1234);
895        assert!(!entry.is_valid());
896        assert_eq!(entry.validate(), Err(BootError::InvalidValidationChecksum));
897    }
898
899    #[test]
900    fn test_section_entry_new() {
901        let entry = BootSectionEntry::new(EmulationType::NoEmulation, 0x07C0, 4, 20);
902        assert!(entry.is_bootable());
903        assert!(entry.is_valid());
904        assert_eq!(entry.boot_indicator, 0x88);
905        assert_eq!(entry.boot_media_type, 0x00);
906        assert_eq!(entry.load_segment.get(), 0x07C0);
907        assert_eq!(entry.sector_count.get(), 4);
908        assert_eq!(entry.load_rba.get(), 20);
909    }
910
911    #[test]
912    fn test_section_entry_not_bootable() {
913        let mut entry = BootSectionEntry::new(EmulationType::NoEmulation, 0, 1, 20);
914        entry.boot_indicator = 0x00;
915        assert!(!entry.is_bootable());
916    }
917
918    #[test]
919    fn test_base_boot_catalog_size() {
920        let catalog = BaseBootCatalog::default();
921        let mut buf = Vec::new();
922        catalog.write(&mut buf).unwrap();
923        assert_eq!(
924            buf.len(),
925            64,
926            "base catalog should be 64 bytes (validation + default entry)"
927        );
928    }
929
930    #[test]
931    fn test_boot_catalog_size() {
932        let catalog = BootCatalog::default();
933        // Default catalog: validation (32) + default entry (32) + terminator (32) = 96
934        assert_eq!(catalog.size(), 96);
935    }
936
937    #[test]
938    fn test_boot_catalog_with_section() {
939        let mut catalog = BootCatalog::default();
940        catalog.add_section(
941            PlatformId::X80X86,
942            vec![BootSectionEntry::new(EmulationType::NoEmulation, 0, 1, 30)],
943        );
944        // validation (32) + default (32) + section header (32) + 1 entry (32) + terminator (32) = 160
945        assert_eq!(catalog.size(), 160);
946    }
947
948    #[test]
949    fn test_boot_catalog_roundtrip() {
950        use std::io::Cursor;
951
952        let mut catalog = BootCatalog::default();
953        catalog.add_section(
954            PlatformId::X80X86,
955            vec![BootSectionEntry::new(EmulationType::NoEmulation, 0, 1, 30)],
956        );
957
958        let mut buf = Vec::new();
959        catalog.write(&mut buf).unwrap();
960
961        let mut cursor = Cursor::new(buf);
962        let parsed = BootCatalog::parse(&mut cursor).unwrap();
963
964        assert_eq!(parsed.sections.len(), 1);
965        assert_eq!(parsed.sections[0].1.len(), 1);
966    }
967
968    #[test]
969    fn test_platform_id_roundtrip() {
970        for id in [
971            PlatformId::X80X86,
972            PlatformId::PowerPC,
973            PlatformId::Macintosh,
974            PlatformId::UEFI,
975        ] {
976            let byte = id.to_u8();
977            let recovered = PlatformId::from_u8(byte);
978            assert_eq!(recovered.to_u8(), byte);
979        }
980    }
981
982    #[test]
983    fn test_emulation_type_roundtrip() {
984        for (byte, ty, emulated) in [
985            (0x00u8, EmulationType::NoEmulation, false),
986            (0x01, EmulationType::Floppy1_2, true),
987            (0x02, EmulationType::Floppy1_44, true),
988            (0x03, EmulationType::Floppy2_88, true),
989            (0x04, EmulationType::HardDisk, true),
990            // Flag-decorated byte with a non-zero media nibble (0x2 = 1.44 MB).
991            (0x42, EmulationType::Unknown(0x42), true),
992            // Continuation flag set on a no-emulation entry: the low nibble is
993            // 0x0, so this must NOT be treated as emulated.
994            (0x20, EmulationType::Unknown(0x20), false),
995        ] {
996            assert_eq!(ty.to_u8(), byte, "{ty:?} to_u8");
997            assert_eq!(EmulationType::from_u8(byte), ty, "from_u8({byte:#x})");
998            assert_eq!(ty.is_emulated(), emulated, "{ty:?} is_emulated");
999        }
1000    }
1001}
1002}