diff --git a/build.rs b/build.rs index b5652bc6..275fbdea 100644 --- a/build.rs +++ b/build.rs @@ -266,6 +266,7 @@ async fn build_bios_stage_4(out_dir: &Path) -> PathBuf { convert_elf_to_bin(elf_path).await } +#[cfg(not(docsrs_dummy_build))] #[cfg(feature = "bios")] async fn convert_elf_to_bin(elf_path: PathBuf) -> PathBuf { let flat_binary_path = elf_path.with_extension("bin"); diff --git a/src/bios/mod.rs b/src/bios/mod.rs index ec0db402..6000adcc 100644 --- a/src/bios/mod.rs +++ b/src/bios/mod.rs @@ -1,98 +1,36 @@ -use crate::fat; -use anyhow::Context; -use bootloader_boot_config::BootConfig; -use std::io::Write; -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, -}; -use tempfile::NamedTempFile; +use std::path::Path; -mod mbr; +use bootloader_boot_config::BootConfig; -const BIOS_STAGE_3: &str = "boot-stage-3"; -const BIOS_STAGE_4: &str = "boot-stage-4"; +use crate::DiskImageBuilder; /// Create disk images for booting on legacy BIOS systems. pub struct BiosBoot { - kernel: PathBuf, - ramdisk: Option, - config: Option, + image_builder: DiskImageBuilder, } impl BiosBoot { /// Start creating a disk image for the given bootloader ELF executable. pub fn new(kernel_path: &Path) -> Self { Self { - kernel: kernel_path.to_owned(), - ramdisk: None, - config: None, + image_builder: DiskImageBuilder::new(kernel_path.to_owned()), } } /// Add a ramdisk file to the image. pub fn set_ramdisk(&mut self, ramdisk_path: &Path) -> &mut Self { - self.ramdisk = Some(ramdisk_path.to_owned()); + self.image_builder.set_ramdisk(ramdisk_path.to_owned()); self } - /// Configures the runtime behavior of the bootloader. + /// Creates a configuration file (boot.json) that configures the runtime behavior of the bootloader. pub fn set_boot_config(&mut self, config: &BootConfig) -> &mut Self { - self.config = Some(serde_json::to_string(&config).expect("failed to serialize BootConfig")); + self.image_builder.set_boot_config(config); self } /// Create a bootable BIOS disk image at the given path. pub fn create_disk_image(&self, out_path: &Path) -> anyhow::Result<()> { - let bootsector_path = Path::new(env!("BIOS_BOOT_SECTOR_PATH")); - let stage_2_path = Path::new(env!("BIOS_STAGE_2_PATH")); - - let fat_partition = self - .create_fat_partition() - .context("failed to create FAT partition")?; - - mbr::create_mbr_disk( - bootsector_path, - stage_2_path, - fat_partition.path(), - out_path, - ) - .context("failed to create BIOS MBR disk image")?; - - fat_partition - .close() - .context("failed to delete FAT partition after disk image creation")?; - - Ok(()) - } - - /// Creates an BIOS-bootable FAT partition with the kernel. - fn create_fat_partition(&self) -> anyhow::Result { - let stage_3_path = Path::new(env!("BIOS_STAGE_3_PATH")); - let stage_4_path = Path::new(env!("BIOS_STAGE_4_PATH")); - - let mut files = BTreeMap::new(); - files.insert(crate::KERNEL_FILE_NAME, self.kernel.as_path()); - files.insert(BIOS_STAGE_3, stage_3_path); - files.insert(BIOS_STAGE_4, stage_4_path); - if let Some(ramdisk_path) = &self.ramdisk { - files.insert(crate::RAMDISK_FILE_NAME, ramdisk_path); - } - - let mut config_file: NamedTempFile; - - if let Some(config_ser) = &self.config { - config_file = NamedTempFile::new() - .context("failed to create temp file") - .unwrap(); - writeln!(config_file, "{config_ser}")?; - files.insert(crate::CONFIG_FILE_NAME, config_file.path()); - } - - let out_file = NamedTempFile::new().context("failed to create temp file")?; - fat::create_fat_filesystem(files, out_file.path()) - .context("failed to create BIOS FAT filesystem")?; - - Ok(out_file) + self.image_builder.create_bios_image(out_path) } } diff --git a/src/fat.rs b/src/fat.rs index d94a57b6..aa2a450b 100644 --- a/src/fat.rs +++ b/src/fat.rs @@ -1,21 +1,21 @@ +use crate::file_data_source::FileDataSource; use anyhow::Context; -use std::{collections::BTreeMap, fs, io, path::Path}; +use fatfs::Dir; +use std::fs::File; +use std::{collections::BTreeMap, fs, path::Path}; use crate::KERNEL_FILE_NAME; pub fn create_fat_filesystem( - files: BTreeMap<&str, &Path>, + files: BTreeMap<&str, &FileDataSource>, out_fat_path: &Path, ) -> anyhow::Result<()> { const MB: u64 = 1024 * 1024; // calculate needed size let mut needed_size = 0; - for path in files.values() { - let file_size = fs::metadata(path) - .with_context(|| format!("failed to read metadata of file `{}`", path.display()))? - .len(); - needed_size += file_size; + for source in files.values() { + needed_size += source.len()?; } // create new filesystem image file at the given path and set its length @@ -31,7 +31,9 @@ pub fn create_fat_filesystem( // choose a file system label let mut label = *b"MY_RUST_OS!"; - if let Some(path) = files.get(KERNEL_FILE_NAME) { + + // This __should__ always be a file, but maybe not. Should we allow the caller to set the volume label instead? + if let Some(FileDataSource::File(path)) = files.get(KERNEL_FILE_NAME) { if let Some(name) = path.file_stem() { let converted = name.to_string_lossy(); let name = converted.as_bytes(); @@ -48,10 +50,17 @@ pub fn create_fat_filesystem( fatfs::format_volume(&fat_file, format_options).context("Failed to format FAT file")?; let filesystem = fatfs::FileSystem::new(&fat_file, fatfs::FsOptions::new()) .context("Failed to open FAT file system of UEFI FAT file")?; + let root_dir = filesystem.root_dir(); // copy files to file system - let root_dir = filesystem.root_dir(); - for (target_path_raw, file_path) in files { + add_files_to_image(&root_dir, files) +} + +pub fn add_files_to_image( + root_dir: &Dir<&File>, + files: BTreeMap<&str, &FileDataSource>, +) -> anyhow::Result<()> { + for (target_path_raw, source) in files { let target_path = Path::new(target_path_raw); // create parent directories let ancestors: Vec<_> = target_path.ancestors().skip(1).collect(); @@ -70,12 +79,14 @@ pub fn create_fat_filesystem( .create_file(target_path_raw) .with_context(|| format!("failed to create file at `{}`", target_path.display()))?; new_file.truncate().unwrap(); - io::copy( - &mut fs::File::open(file_path) - .with_context(|| format!("failed to open `{}` for copying", file_path.display()))?, - &mut new_file, - ) - .with_context(|| format!("failed to copy `{}` to FAT filesystem", file_path.display()))?; + + source.copy_to(&mut new_file).with_context(|| { + format!( + "failed to copy source data `{:?}` to file at `{}`", + source, + target_path.display() + ) + })?; } Ok(()) diff --git a/src/file_data_source.rs b/src/file_data_source.rs new file mode 100644 index 00000000..3e1128b3 --- /dev/null +++ b/src/file_data_source.rs @@ -0,0 +1,58 @@ +use alloc::vec::Vec; +use anyhow::Context; +use core::fmt::{Debug, Formatter}; + +use std::io::Cursor; +use std::path::PathBuf; +use std::{fs, io}; + +#[derive(Clone)] +/// Defines a data source, either a source `std::path::PathBuf`, or a vector of bytes. +pub enum FileDataSource { + File(PathBuf), + Data(Vec), +} + +impl Debug for FileDataSource { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + FileDataSource::File(file) => { + f.write_fmt(format_args!("data source: File {}", file.display())) + } + FileDataSource::Data(d) => { + f.write_fmt(format_args!("data source: {} raw bytes ", d.len())) + } + } + } +} + +impl FileDataSource { + /// Get the length of the inner data source + pub fn len(&self) -> anyhow::Result { + Ok(match self { + FileDataSource::File(path) => fs::metadata(path) + .with_context(|| format!("failed to read metadata of file `{}`", path.display()))? + .len(), + FileDataSource::Data(v) => v.len() as u64, + }) + } + /// Copy this data source to the specified target that implements io::Write + pub fn copy_to(&self, target: &mut dyn io::Write) -> anyhow::Result<()> { + match self { + FileDataSource::File(file_path) => { + io::copy( + &mut fs::File::open(file_path).with_context(|| { + format!("failed to open `{}` for copying", file_path.display()) + })?, + target, + )?; + } + FileDataSource::Data(contents) => { + let mut cursor = Cursor::new(contents); + io::copy(&mut cursor, target)?; + } + }; + + Ok(()) + } +} diff --git a/src/uefi/gpt.rs b/src/gpt.rs similarity index 100% rename from src/uefi/gpt.rs rename to src/gpt.rs diff --git a/src/lib.rs b/src/lib.rs index ff1894de..179f1d10 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,20 +4,223 @@ An experimental x86_64 bootloader that works on both BIOS and UEFI systems. #![warn(missing_docs)] +extern crate alloc; + #[cfg(feature = "bios")] mod bios; -mod fat; +#[cfg(feature = "uefi")] +mod gpt; +#[cfg(feature = "bios")] +mod mbr; #[cfg(feature = "uefi")] mod uefi; +#[cfg(feature = "uefi")] +pub use uefi::UefiBoot; + #[cfg(feature = "bios")] pub use bios::BiosBoot; -#[cfg(feature = "uefi")] -pub use uefi::UefiBoot; +mod fat; +mod file_data_source; +use std::{ + borrow::Cow, + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +use anyhow::Context; + +use tempfile::NamedTempFile; + +use crate::file_data_source::FileDataSource; pub use bootloader_boot_config::BootConfig; const KERNEL_FILE_NAME: &str = "kernel-x86_64"; const RAMDISK_FILE_NAME: &str = "ramdisk"; const CONFIG_FILE_NAME: &str = "boot.json"; + +/// Allows creating disk images for a specified set of files. +/// +/// It can currently create `MBR` (BIOS), `GPT` (UEFI), and `TFTP` (UEFI) images. +pub struct DiskImageBuilder { + files: BTreeMap, FileDataSource>, +} + +impl DiskImageBuilder { + /// Create a new instance of DiskImageBuilder, with the specified kernel. + pub fn new(kernel: PathBuf) -> Self { + let mut obj = Self::empty(); + obj.set_kernel(kernel); + obj + } + + /// Create a new, empty instance of DiskImageBuilder + pub fn empty() -> Self { + Self { + files: BTreeMap::new(), + } + } + + /// Add or replace a kernel to be included in the final image. + pub fn set_kernel(&mut self, path: PathBuf) -> &mut Self { + self.set_file_source(KERNEL_FILE_NAME.into(), FileDataSource::File(path)) + } + + /// Add or replace a ramdisk to be included in the final image. + pub fn set_ramdisk(&mut self, path: PathBuf) -> &mut Self { + self.set_file_source(RAMDISK_FILE_NAME.into(), FileDataSource::File(path)) + } + + /// Configures the runtime behavior of the bootloader. + pub fn set_boot_config(&mut self, boot_config: &BootConfig) -> &mut Self { + let json = serde_json::to_vec_pretty(boot_config).expect("failed to serialize BootConfig"); + self.set_file_source(CONFIG_FILE_NAME.into(), FileDataSource::Data(json)) + } + + /// Add a file with the specified bytes to the disk image + /// + /// Note that the bootloader only loads the kernel and ramdisk files into memory on boot. + /// Other files need to be loaded manually by the kernel. + pub fn set_file_contents(&mut self, destination: String, data: Vec) -> &mut Self { + self.set_file_source(destination.into(), FileDataSource::Data(data)) + } + + /// Add a file with the specified source file to the disk image + /// + /// Note that the bootloader only loads the kernel and ramdisk files into memory on boot. + /// Other files need to be loaded manually by the kernel. + pub fn set_file(&mut self, destination: String, file_path: PathBuf) -> &mut Self { + self.set_file_source(destination.into(), FileDataSource::File(file_path)) + } + + #[cfg(feature = "bios")] + /// Create an MBR disk image for booting on BIOS systems. + pub fn create_bios_image(&self, image_path: &Path) -> anyhow::Result<()> { + const BIOS_STAGE_3: &str = "boot-stage-3"; + const BIOS_STAGE_4: &str = "boot-stage-4"; + let bootsector_path = Path::new(env!("BIOS_BOOT_SECTOR_PATH")); + let stage_2_path = Path::new(env!("BIOS_STAGE_2_PATH")); + let stage_3_path = Path::new(env!("BIOS_STAGE_3_PATH")); + let stage_4_path = Path::new(env!("BIOS_STAGE_4_PATH")); + let mut internal_files = BTreeMap::new(); + internal_files.insert( + BIOS_STAGE_3, + FileDataSource::File(stage_3_path.to_path_buf()), + ); + internal_files.insert( + BIOS_STAGE_4, + FileDataSource::File(stage_4_path.to_path_buf()), + ); + + let fat_partition = self + .create_fat_filesystem_image(internal_files) + .context("failed to create FAT partition")?; + mbr::create_mbr_disk( + bootsector_path, + stage_2_path, + fat_partition.path(), + image_path, + ) + .context("failed to create BIOS MBR disk image")?; + + fat_partition + .close() + .context("failed to delete FAT partition after disk image creation")?; + Ok(()) + } + + #[cfg(feature = "uefi")] + /// Create a GPT disk image for booting on UEFI systems. + pub fn create_uefi_image(&self, image_path: &Path) -> anyhow::Result<()> { + const UEFI_BOOT_FILENAME: &str = "efi/boot/bootx64.efi"; + let bootloader_path = Path::new(env!("UEFI_BOOTLOADER_PATH")); + let mut internal_files = BTreeMap::new(); + internal_files.insert( + UEFI_BOOT_FILENAME, + FileDataSource::File(bootloader_path.to_path_buf()), + ); + let fat_partition = self + .create_fat_filesystem_image(internal_files) + .context("failed to create FAT partition")?; + gpt::create_gpt_disk(fat_partition.path(), image_path) + .context("failed to create UEFI GPT disk image")?; + fat_partition + .close() + .context("failed to delete FAT partition after disk image creation")?; + + Ok(()) + } + + #[cfg(feature = "uefi")] + /// Create a folder containing the needed files for UEFI TFTP/PXE booting. + pub fn create_uefi_tftp_folder(&self, tftp_path: &Path) -> anyhow::Result<()> { + use std::{fs, ops::Deref}; + + const UEFI_TFTP_BOOT_FILENAME: &str = "bootloader"; + let bootloader_path = Path::new(env!("UEFI_BOOTLOADER_PATH")); + fs::create_dir_all(tftp_path) + .with_context(|| format!("failed to create out dir at {}", tftp_path.display()))?; + + let to = tftp_path.join(UEFI_TFTP_BOOT_FILENAME); + fs::copy(bootloader_path, &to).with_context(|| { + format!( + "failed to copy bootloader from {} to {}", + bootloader_path.display(), + to.display() + ) + })?; + + for f in &self.files { + let to = tftp_path.join(f.0.deref()); + + let mut new_file = fs::OpenOptions::new() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(to)?; + + f.1.copy_to(&mut new_file)?; + } + + Ok(()) + } + + /// Add a file source to the disk image + fn set_file_source( + &mut self, + destination: Cow<'static, str>, + source: FileDataSource, + ) -> &mut Self { + self.files.insert(destination, source); + self + } + + fn create_fat_filesystem_image( + &self, + internal_files: BTreeMap<&str, FileDataSource>, + ) -> anyhow::Result { + let mut local_map: BTreeMap<&str, _> = BTreeMap::new(); + + for (name, source) in &self.files { + local_map.insert(name, source); + } + + for k in &internal_files { + if local_map.insert(k.0, k.1).is_some() { + return Err(anyhow::Error::msg(format!( + "Attempted to overwrite internal file: {}", + k.0 + ))); + } + } + + let out_file = NamedTempFile::new().context("failed to create temp file")?; + fat::create_fat_filesystem(local_map, out_file.path()) + .context("failed to create BIOS FAT filesystem")?; + + Ok(out_file) + } +} diff --git a/src/bios/mbr.rs b/src/mbr.rs similarity index 100% rename from src/bios/mbr.rs rename to src/mbr.rs diff --git a/src/uefi/mod.rs b/src/uefi/mod.rs index 030ea9bb..7a27e728 100644 --- a/src/uefi/mod.rs +++ b/src/uefi/mod.rs @@ -1,59 +1,37 @@ -use crate::fat; -use anyhow::Context; +use std::path::Path; + use bootloader_boot_config::BootConfig; -use std::io::Write; -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, -}; -use tempfile::NamedTempFile; -mod gpt; -mod pxe; +use crate::DiskImageBuilder; -/// Create disk images for booting on UEFI systems. +/// Create disk images for booting on legacy BIOS systems. pub struct UefiBoot { - kernel: PathBuf, - ramdisk: Option, - config: Option, + image_builder: DiskImageBuilder, } impl UefiBoot { /// Start creating a disk image for the given bootloader ELF executable. pub fn new(kernel_path: &Path) -> Self { Self { - kernel: kernel_path.to_owned(), - ramdisk: None, - config: None, + image_builder: DiskImageBuilder::new(kernel_path.to_owned()), } } - /// Add a ramdisk file to the disk image. + /// Add a ramdisk file to the image pub fn set_ramdisk(&mut self, ramdisk_path: &Path) -> &mut Self { - self.ramdisk = Some(ramdisk_path.to_owned()); + self.image_builder.set_ramdisk(ramdisk_path.to_owned()); self } - /// Configures the runtime behavior of the bootloader. + /// Creates a configuration file (boot.json) that configures the runtime behavior of the bootloader. pub fn set_boot_config(&mut self, config: &BootConfig) -> &mut Self { - self.config = Some(serde_json::to_string(&config).expect("failed to serialize BootConfig")); + self.image_builder.set_boot_config(config); self } /// Create a bootable UEFI disk image at the given path. pub fn create_disk_image(&self, out_path: &Path) -> anyhow::Result<()> { - let fat_partition = self - .create_fat_partition() - .context("failed to create FAT partition")?; - - gpt::create_gpt_disk(fat_partition.path(), out_path) - .context("failed to create UEFI GPT disk image")?; - - fat_partition - .close() - .context("failed to delete FAT partition after disk image creation")?; - - Ok(()) + self.image_builder.create_uefi_image(out_path) } /// Prepare a folder for use with booting over UEFI_PXE. @@ -62,45 +40,6 @@ impl UefiBoot { /// DHCP server should set the filename option to that path, otherwise the /// bootloader won't be found. pub fn create_pxe_tftp_folder(&self, out_path: &Path) -> anyhow::Result<()> { - let bootloader_path = Path::new(env!("UEFI_BOOTLOADER_PATH")); - - pxe::create_uefi_tftp_folder( - bootloader_path, - self.kernel.as_path(), - self.ramdisk.as_deref(), - self.config.as_deref(), - out_path, - ) - .context("failed to create UEFI PXE tftp folder")?; - - Ok(()) - } - - /// Creates an UEFI-bootable FAT partition with the kernel. - fn create_fat_partition(&self) -> anyhow::Result { - let bootloader_path = Path::new(env!("UEFI_BOOTLOADER_PATH")); - - let mut files = BTreeMap::new(); - files.insert("efi/boot/bootx64.efi", bootloader_path); - files.insert(crate::KERNEL_FILE_NAME, self.kernel.as_path()); - if let Some(ramdisk_path) = &self.ramdisk { - files.insert(crate::RAMDISK_FILE_NAME, ramdisk_path); - } - - let mut config_file: NamedTempFile; - - if let Some(config_ser) = &self.config { - config_file = NamedTempFile::new() - .context("failed to create temp file") - .unwrap(); - writeln!(config_file, "{config_ser}")?; - files.insert(crate::CONFIG_FILE_NAME, config_file.path()); - } - - let out_file = NamedTempFile::new().context("failed to create temp file")?; - fat::create_fat_filesystem(files, out_file.path()) - .context("failed to create UEFI FAT filesystem")?; - - Ok(out_file) + self.image_builder.create_uefi_tftp_folder(out_path) } } diff --git a/src/uefi/pxe.rs b/src/uefi/pxe.rs deleted file mode 100644 index 4eac7f8c..00000000 --- a/src/uefi/pxe.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::path::Path; - -use anyhow::Context; -use bootloader_boot_config::BootConfig; - -pub fn create_uefi_tftp_folder( - bootloader_path: &Path, - kernel_binary: &Path, - ramdisk_path: Option<&Path>, - boot_config: Option<&str>, - out_path: &Path, -) -> anyhow::Result<()> { - std::fs::create_dir_all(out_path) - .with_context(|| format!("failed to create out dir at {}", out_path.display()))?; - - let to = out_path.join("bootloader"); - std::fs::copy(bootloader_path, &to).with_context(|| { - format!( - "failed to copy bootloader from {} to {}", - bootloader_path.display(), - to.display() - ) - })?; - - let to = out_path.join("kernel-x86_64"); - std::fs::copy(kernel_binary, &to).with_context(|| { - format!( - "failed to copy kernel from {} to {}", - kernel_binary.display(), - to.display() - ) - })?; - let to = out_path.join("ramdisk"); - if let Some(rp) = ramdisk_path { - std::fs::copy(rp, &to).with_context(|| { - format!( - "failed to copy ramdisk from {} to {}", - rp.display(), - to.display() - ) - })?; - } - - if let Some(config) = boot_config { - let to = out_path.join("boot.json"); - std::fs::write(to, config).context("failed to write boot.json")?; - } - - Ok(()) -} diff --git a/tests/runner/src/lib.rs b/tests/runner/src/lib.rs index a25932ed..7d293c1e 100644 --- a/tests/runner/src/lib.rs +++ b/tests/runner/src/lib.rs @@ -1,4 +1,5 @@ use bootloader::BootConfig; +use bootloader::DiskImageBuilder; use std::{io::Read, path::Path, process::Command}; const QEMU_ARGS: &[&str] = &[ @@ -31,26 +32,20 @@ pub fn run_test_kernel_internal( config_file_path: Option<&BootConfig>, ) { let kernel_path = Path::new(kernel_binary_path); + let mut image_builder = DiskImageBuilder::new(kernel_path.to_owned()); + if let Some(rdp) = ramdisk_path { + image_builder.set_ramdisk(rdp.to_owned()); + } + if let Some(cfp) = config_file_path { + image_builder.set_boot_config(cfp); + } #[cfg(feature = "uefi")] { - // create a GPT disk image for UEFI booting let gpt_path = kernel_path.with_extension("gpt"); - let mut uefi_builder = bootloader::UefiBoot::new(kernel_path); - // Set ramdisk for test, if supplied. - if let Some(rdp) = ramdisk_path { - uefi_builder.set_ramdisk(rdp); - } - if let Some(cfp) = config_file_path { - uefi_builder.set_boot_config(cfp); - } - uefi_builder.create_disk_image(&gpt_path).unwrap(); - - // create a TFTP folder with the kernel executable and UEFI bootloader for - // UEFI PXE booting let tftp_path = kernel_path.with_extension("tftp"); - uefi_builder.create_pxe_tftp_folder(&tftp_path).unwrap(); - + image_builder.create_uefi_image(&gpt_path).unwrap(); + image_builder.create_uefi_tftp_folder(&tftp_path).unwrap(); run_test_kernel_on_uefi(&gpt_path); run_test_kernel_on_uefi_pxe(&tftp_path); } @@ -59,15 +54,7 @@ pub fn run_test_kernel_internal( { // create an MBR disk image for legacy BIOS booting let mbr_path = kernel_path.with_extension("mbr"); - let mut bios_builder = bootloader::BiosBoot::new(kernel_path); - // Set ramdisk for test, if supplied. - if let Some(rdp) = ramdisk_path { - bios_builder.set_ramdisk(rdp); - } - if let Some(cfp) = config_file_path { - bios_builder.set_boot_config(cfp); - } - bios_builder.create_disk_image(&mbr_path).unwrap(); + image_builder.create_bios_image(mbr_path.as_path()).unwrap(); run_test_kernel_on_bios(&mbr_path); }