use super::io::{self, Read, Seek, Write};
use core::fmt::Debug;
use crate::types::{Endian, LittleEndian, U16, U32};
#[cfg(feature = "alloc")]
use alloc::vec::Vec;
#[cfg(feature = "write")]
use super::{
boot::options::BootOptions,
volume::BootRecordVolumeDescriptor,
write::{File, InputFiles},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BootError {
Io,
InvalidValidationChecksum,
InvalidValidationHeader,
InvalidValidationKey,
InvalidDefaultEntry,
ExpectedSectionHeader(u8),
UnexpectedEndOfCatalog,
}
impl core::fmt::Display for BootError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Io => write!(f, "I/O error"),
Self::InvalidValidationChecksum => {
write!(f, "invalid boot catalog validation checksum")
}
Self::InvalidValidationHeader => {
write!(f, "invalid boot catalog validation header (expected 0x01)")
}
Self::InvalidValidationKey => write!(
f,
"invalid boot catalog validation key (expected 0x55, 0xAA)"
),
Self::InvalidDefaultEntry => write!(f, "default boot entry is not marked as bootable"),
Self::ExpectedSectionHeader(id) => write!(f, "expected section header, got: {:#x}", id),
Self::UnexpectedEndOfCatalog => write!(f, "boot catalog ended unexpectedly"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for BootError {}
#[derive(Debug, Clone)]
pub struct BaseBootCatalog {
pub validation: BootValidationEntry,
pub default_entry: BootSectionEntry,
}
impl Default for BaseBootCatalog {
fn default() -> Self {
Self::new(EmulationType::NoEmulation, 0, 0, 0)
}
}
impl BaseBootCatalog {
pub fn new(
media_type: EmulationType,
load_segment: u16,
sector_count: u16,
load_rba: u32,
) -> Self {
Self {
validation: BootValidationEntry::new(),
default_entry: BootSectionEntry::new(media_type, load_segment, sector_count, load_rba),
}
}
}
#[cfg(feature = "alloc")]
#[derive(Debug, Clone)]
pub struct BootCatalog {
base: BaseBootCatalog,
sections: Vec<(BootSectionHeaderEntry, Vec<BootSectionEntry>)>,
}
#[cfg(feature = "alloc")]
impl Default for BootCatalog {
fn default() -> Self {
Self::new(EmulationType::NoEmulation, 0, 0, 0)
}
}
#[cfg(feature = "alloc")]
impl BootCatalog {
pub fn new(
media_type: EmulationType,
load_segment: u16,
sector_count: u16,
load_rba: u32,
) -> Self {
Self {
base: BaseBootCatalog::new(media_type, load_segment, sector_count, load_rba),
sections: Vec::new(),
}
}
pub fn set_default_entry(&mut self, entry: BootSectionEntry) {
self.base.default_entry = entry;
}
pub fn add_section(&mut self, platform_id: PlatformId, entries: Vec<BootSectionEntry>) {
if let Some((header, _entry)) = self.sections.last_mut() {
header.header_type = 0x90;
}
let header = BootSectionHeaderEntry {
header_type: 0x91,
platform_id: platform_id.to_u8(),
section_count: U16::new(1),
section_ident: [0; 28],
};
self.sections.push((header, entries));
}
pub fn size(&self) -> usize {
let sections_size: usize = self
.sections
.iter()
.map(|(_, entries)| (entries.len() + 1) * 32)
.sum();
64 + sections_size + 32 }
}
#[derive(Debug, Clone, Copy)]
pub enum BootCatalogEntry {
Validation(BootValidationEntry),
SectionHeader(BootSectionHeaderEntry),
SectionEntry(BootSectionEntry),
SectionEntryExtension(BootSectionEntryExtension),
}
impl BootCatalogEntry {
pub fn as_bytes(&self) -> &[u8] {
match self {
BootCatalogEntry::Validation(entry) => bytemuck::bytes_of(entry),
BootCatalogEntry::SectionHeader(entry) => bytemuck::bytes_of(entry),
BootCatalogEntry::SectionEntry(entry) => bytemuck::bytes_of(entry),
BootCatalogEntry::SectionEntryExtension(entry) => bytemuck::bytes_of(entry),
}
}
pub const fn size(&self) -> usize {
32
}
}
#[derive(Debug, Clone, Copy)]
pub enum PlatformId {
X80X86,
PowerPC,
Macintosh,
UEFI,
Unknown(u8),
}
impl PlatformId {
pub fn from_u8(value: u8) -> Self {
match value {
0x00 => Self::X80X86,
0x01 => Self::PowerPC,
0x02 => Self::Macintosh,
0xEF => Self::UEFI,
value => Self::Unknown(value),
}
}
pub fn to_u8(self) -> u8 {
match self {
Self::X80X86 => 0x00,
Self::PowerPC => 0x01,
Self::Macintosh => 0x02,
Self::UEFI => 0xEF,
Self::Unknown(value) => value,
}
}
}
#[repr(C)]
#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub struct BootValidationEntry {
pub header_id: u8,
pub platform_id: u8,
pub reserved: [u8; 2],
pub manufacturer: [u8; 24],
pub checksum: U16<LittleEndian>,
pub key: [u8; 2],
}
impl Default for BootValidationEntry {
fn default() -> Self {
Self::new()
}
}
impl BootValidationEntry {
pub fn new() -> Self {
let mut entry = Self {
header_id: 1,
platform_id: 0,
reserved: [0; 2],
manufacturer: [0; 24],
checksum: U16::new(0),
key: [0x55, 0xAA],
};
entry.checksum.set(entry.calculate_checksum());
entry
}
}
impl Debug for BootValidationEntry {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BootValidationEntry")
.field("header_id", &format_args!("{:#x}", self.header_id))
.field("platform_id", &PlatformId::from_u8(self.platform_id))
.field(
"manufacturer",
&core::str::from_utf8(&self.manufacturer).unwrap(),
)
.field("checksum", &self.checksum.get())
.field("key", &self.key)
.finish_non_exhaustive()
}
}
impl BootValidationEntry {
pub fn is_valid(&self) -> bool {
self.header_id == 0x01
&& self.key == [0x55, 0xAA]
&& self.checksum.get() == self.calculate_checksum()
}
pub fn validate(&self) -> Result<(), BootError> {
if self.header_id != 0x01 {
return Err(BootError::InvalidValidationHeader);
}
if self.key != [0x55, 0xAA] {
return Err(BootError::InvalidValidationKey);
}
if self.checksum.get() != self.calculate_checksum() {
return Err(BootError::InvalidValidationChecksum);
}
Ok(())
}
pub fn calculate_checksum(&self) -> u16 {
let mut bytes = [0u8; 32];
bytes.copy_from_slice(bytemuck::bytes_of(self));
bytes[28] = 0;
bytes[29] = 0;
let mut checksum = 0u16;
for i in (0..32).step_by(2) {
let value = u16::from_le_bytes([bytes[i], bytes[i + 1]]);
checksum = checksum.wrapping_add(value);
}
(!checksum) + 1
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct BootSectionHeaderEntry {
pub header_type: u8,
pub platform_id: u8,
pub section_count: U16<LittleEndian>,
pub section_ident: [u8; 28],
}
impl Debug for BootSectionHeaderEntry {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BootSectionHeaderEntry")
.field("header_type", &format_args!("{:#x}", self.header_type))
.field("platform_id", &PlatformId::from_u8(self.platform_id))
.field("section_count", &self.section_count.get())
.field(
"section_ident",
&core::str::from_utf8(&self.section_ident).unwrap(),
)
.finish_non_exhaustive()
}
}
unsafe impl bytemuck::Zeroable for BootSectionHeaderEntry {}
unsafe impl bytemuck::Pod for BootSectionHeaderEntry {}
#[derive(Debug, Clone, Copy)]
pub enum EmulationType {
NoEmulation,
Unknown(u8),
}
impl EmulationType {
pub fn from_u8(value: u8) -> Self {
match value {
0x00 => Self::NoEmulation,
value => Self::Unknown(value),
}
}
pub fn to_u8(self) -> u8 {
match self {
Self::NoEmulation => 0x00,
Self::Unknown(value) => value,
}
}
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct BootSectionEntry {
pub boot_indicator: u8,
pub boot_media_type: u8,
pub load_segment: U16<LittleEndian>,
pub system_type: u8,
pub reserved0: u8,
pub sector_count: U16<LittleEndian>,
pub load_rba: U32<LittleEndian>,
pub selection_criteria: u8,
pub vendor_unique: [u8; 19],
}
impl BootSectionEntry {
pub fn new(
media_type: EmulationType,
load_segment: u16,
sector_count: u16,
load_rba: u32,
) -> Self {
Self {
boot_indicator: 0x88,
boot_media_type: media_type.to_u8(),
load_segment: U16::new(load_segment),
system_type: 0,
reserved0: 0,
sector_count: U16::new(sector_count),
load_rba: U32::new(load_rba),
selection_criteria: 0,
vendor_unique: [0; 19],
}
}
}
impl Debug for BootSectionEntry {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("BootSectionEntry")
.field(
"boot_indicator",
&format_args!("{:#x}", self.boot_indicator),
)
.field(
"boot_media_type",
&EmulationType::from_u8(self.boot_media_type),
)
.field("load_segment", &self.load_segment.get())
.field("system_type", &self.system_type)
.field("sector_count", &self.sector_count.get())
.field("load_rba", &self.load_rba.get())
.field("selection_criteria", &self.selection_criteria)
.finish_non_exhaustive()
}
}
impl BootSectionEntry {
pub fn is_bootable(&self) -> bool {
self.boot_indicator == 0x88
}
pub fn is_valid(&self) -> bool {
self.is_bootable()
}
}
unsafe impl bytemuck::Zeroable for BootSectionEntry {}
unsafe impl bytemuck::Pod for BootSectionEntry {}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct BootSectionEntryExtension {
pub extension_indicator: u8,
pub flags: u8,
pub vendor_unique: [u8; 30],
}
unsafe impl bytemuck::Zeroable for BootSectionEntryExtension {}
unsafe impl bytemuck::Pod for BootSectionEntryExtension {}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub struct BootInfoTable {
pub iso_start: U32<LittleEndian>,
pub file_lba: U32<LittleEndian>,
pub file_len: U32<LittleEndian>,
pub checksum: U32<LittleEndian>,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub struct Grub2BootInfoTable {
pub pvd_lba: U32<LittleEndian>,
pub file_lba: U32<LittleEndian>,
pub file_len: U32<LittleEndian>,
pub checksum: U32<LittleEndian>,
pub reserved: [u8; 40],
}
io_transform! {
impl BaseBootCatalog {
pub async fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self, BootError> {
let validation = BootValidationEntry::parse(reader).await.map_err(|_| BootError::Io)?;
validation.validate()?;
let default_entry = BootSectionEntry::parse(reader).await.map_err(|_| BootError::Io)?;
if !default_entry.is_bootable() {
return Err(BootError::InvalidDefaultEntry);
}
Ok(Self {
validation,
default_entry,
})
}
pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(bytemuck::bytes_of(&self.validation)).await?;
writer.write_all(bytemuck::bytes_of(&self.default_entry)).await?;
Ok(())
}
}
impl BootValidationEntry {
pub async fn parse<T: Read>(reader: &mut T) -> Result<Self, io::Error> {
let mut buf = [0u8; 32];
reader.read_exact(&mut buf).await?;
Ok(bytemuck::cast(buf))
}
}
impl BootSectionEntry {
pub async fn parse<T: Read>(reader: &mut T) -> Result<Self, io::Error> {
let mut buf: [u8; 32] = [0; 32];
reader.read_exact(&mut buf).await?;
Ok(bytemuck::cast(buf))
}
}
#[cfg(feature = "alloc")]
impl BootCatalog {
pub async fn parse<T: Read + Seek>(reader: &mut T) -> Result<Self, BootError> {
let base = BaseBootCatalog::parse(reader).await?;
let mut sections = Vec::new();
let mut buffer = [0u8; 32];
let mut has_more = false;
let mut header = None;
let mut entries = Vec::new();
loop {
reader.read_exact(&mut buffer).await.map_err(|_| BootError::Io)?;
match buffer[0] {
0x00 if !has_more => break,
0x90 => {
has_more = true;
if let Some(header) = header.take() {
sections.push((header, entries));
entries = Vec::new();
}
header = Some(bytemuck::cast(buffer));
}
0x91 => {
has_more = false;
if let Some(header) = header.take() {
sections.push((header, entries));
entries = Vec::new();
}
header = Some(bytemuck::cast(buffer));
}
id => {
if header.is_none() {
return Err(BootError::ExpectedSectionHeader(id));
}
entries.push(bytemuck::cast(buffer));
}
}
}
if has_more {
return Err(BootError::UnexpectedEndOfCatalog);
}
if let Some(header) = header {
sections.push((header, entries));
}
Ok(Self { base, sections })
}
pub async fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
self.base.write(writer).await?;
for (header, entries) in self.sections.iter() {
writer.write_all(bytemuck::bytes_of(header)).await?;
for entry in entries {
writer.write_all(bytemuck::bytes_of(entry)).await?;
}
}
writer.write_all(&[0; 32]).await?;
Ok(())
}
}
}
#[cfg(feature = "write")]
pub struct ElToritoWriter;
#[cfg(feature = "write")]
impl ElToritoWriter {
pub fn create_descriptor(
opts: &BootOptions,
files: &mut InputFiles,
) -> BootRecordVolumeDescriptor {
if opts.write_boot_catalog {
use alloc::string::ToString;
use std::sync::Arc;
let size = 96 + opts.entries.len() * 64;
let size = (size + 2047) & !2047;
let dir_pos = files
.files
.iter()
.position(|f| matches!(f, File::Directory { .. }))
.unwrap_or(0);
files.files.insert(
dir_pos.saturating_sub(1),
File::File {
name: Arc::new("boot.catalog".to_string()),
contents: alloc::vec![0; size],
},
);
}
BootRecordVolumeDescriptor::new(0)
}
}
#[cfg(feature = "write")]
pub mod options {
use alloc::string::String;
use alloc::vec::Vec;
use core::num::NonZeroU16;
use super::{EmulationType, PlatformId};
#[derive(Debug, Clone, Default)]
pub struct BootOptions {
pub write_boot_catalog: bool,
pub default: BootEntryOptions,
pub entries: Vec<(BootSectionOptions, BootEntryOptions)>,
}
impl BootOptions {
pub fn sections(&self) -> Vec<(Option<BootSectionOptions>, BootEntryOptions)> {
let mut sections = Vec::with_capacity(self.entries.len() + 1);
sections.push((None, self.default.clone()));
for (section_ops, ops) in &self.entries {
sections.push((Some(section_ops.clone()), ops.clone()));
}
sections
}
}
#[derive(Debug, Clone)]
pub struct BootSectionOptions {
pub platform: PlatformId,
}
#[derive(Debug, Clone)]
pub struct BootEntryOptions {
pub load_size: Option<NonZeroU16>,
pub boot_image_path: String,
pub boot_info_table: bool,
pub grub2_boot_info: bool,
pub emulation: EmulationType,
}
impl Default for BootEntryOptions {
fn default() -> Self {
Self {
load_size: None,
boot_image_path: String::new(),
boot_info_table: false,
grub2_boot_info: false,
emulation: EmulationType::NoEmulation,
}
}
}
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
use alloc::vec;
use alloc::vec::Vec;
static_assertions::assert_eq_size!(BootValidationEntry, [u8; 32]);
static_assertions::assert_eq_size!(BootSectionHeaderEntry, [u8; 32]);
static_assertions::assert_eq_size!(BootSectionEntry, [u8; 32]);
static_assertions::assert_eq_align!(BootValidationEntry, u8);
static_assertions::assert_eq_align!(BootSectionHeaderEntry, u8);
static_assertions::assert_eq_align!(BootSectionEntry, u8);
#[test]
fn test_validation_entry_new() {
let entry = BootValidationEntry::new();
assert_eq!(entry.header_id, 0x01);
assert_eq!(entry.key, [0x55, 0xAA]);
assert!(entry.is_valid());
assert!(entry.validate().is_ok());
}
#[test]
fn test_validation_entry_checksum() {
let entry = BootValidationEntry::new();
let bytes = bytemuck::bytes_of(&entry);
let mut sum = 0u16;
for i in (0..32).step_by(2) {
let value = u16::from_le_bytes([bytes[i], bytes[i + 1]]);
sum = sum.wrapping_add(value);
}
assert_eq!(sum, 0, "checksum should make total sum equal to 0");
}
#[test]
fn test_validation_entry_invalid_header() {
let mut entry = BootValidationEntry::new();
entry.header_id = 0x00;
assert!(!entry.is_valid());
assert_eq!(entry.validate(), Err(BootError::InvalidValidationHeader));
}
#[test]
fn test_validation_entry_invalid_key() {
let mut entry = BootValidationEntry::new();
entry.key = [0x00, 0x00];
assert!(!entry.is_valid());
assert_eq!(entry.validate(), Err(BootError::InvalidValidationKey));
}
#[test]
fn test_validation_entry_invalid_checksum() {
let mut entry = BootValidationEntry::new();
entry.checksum.set(0x1234);
assert!(!entry.is_valid());
assert_eq!(entry.validate(), Err(BootError::InvalidValidationChecksum));
}
#[test]
fn test_section_entry_new() {
let entry = BootSectionEntry::new(EmulationType::NoEmulation, 0x07C0, 4, 20);
assert!(entry.is_bootable());
assert!(entry.is_valid());
assert_eq!(entry.boot_indicator, 0x88);
assert_eq!(entry.boot_media_type, 0x00);
assert_eq!(entry.load_segment.get(), 0x07C0);
assert_eq!(entry.sector_count.get(), 4);
assert_eq!(entry.load_rba.get(), 20);
}
#[test]
fn test_section_entry_not_bootable() {
let mut entry = BootSectionEntry::new(EmulationType::NoEmulation, 0, 1, 20);
entry.boot_indicator = 0x00;
assert!(!entry.is_bootable());
}
#[test]
fn test_base_boot_catalog_size() {
let catalog = BaseBootCatalog::default();
let mut buf = Vec::new();
catalog.write(&mut buf).unwrap();
assert_eq!(
buf.len(),
64,
"base catalog should be 64 bytes (validation + default entry)"
);
}
#[test]
fn test_boot_catalog_size() {
let catalog = BootCatalog::default();
assert_eq!(catalog.size(), 96);
}
#[test]
fn test_boot_catalog_with_section() {
let mut catalog = BootCatalog::default();
catalog.add_section(
PlatformId::X80X86,
vec![BootSectionEntry::new(EmulationType::NoEmulation, 0, 1, 30)],
);
assert_eq!(catalog.size(), 160);
}
#[test]
fn test_boot_catalog_roundtrip() {
use std::io::Cursor;
let mut catalog = BootCatalog::default();
catalog.add_section(
PlatformId::X80X86,
vec![BootSectionEntry::new(EmulationType::NoEmulation, 0, 1, 30)],
);
let mut buf = Vec::new();
catalog.write(&mut buf).unwrap();
let mut cursor = Cursor::new(buf);
let parsed = BootCatalog::parse(&mut cursor).unwrap();
assert_eq!(parsed.sections.len(), 1);
assert_eq!(parsed.sections[0].1.len(), 1);
}
#[test]
fn test_platform_id_roundtrip() {
for id in [
PlatformId::X80X86,
PlatformId::PowerPC,
PlatformId::Macintosh,
PlatformId::UEFI,
] {
let byte = id.to_u8();
let recovered = PlatformId::from_u8(byte);
assert_eq!(recovered.to_u8(), byte);
}
}
#[test]
fn test_emulation_type_roundtrip() {
let no_emul = EmulationType::NoEmulation;
assert_eq!(no_emul.to_u8(), 0x00);
assert!(matches!(
EmulationType::from_u8(0x00),
EmulationType::NoEmulation
));
let unknown = EmulationType::Unknown(0x42);
assert_eq!(unknown.to_u8(), 0x42);
}
}