From 93d9cf0ae1de0fff477eea59138daf6e1559315a Mon Sep 17 00:00:00 2001 From: Ebu Date: Sun, 22 Mar 2026 10:58:47 +0100 Subject: [PATCH] Simple FAT16/32 driver implementation --- .cargo/config.toml | 2 + .gitignore | 4 + .zed/settings.json | 11 + Cargo.lock | 164 +++++++++++ Cargo.toml | 12 + README.md | 7 + run.sh | 29 ++ rust-toolchain.toml | 2 + src/fs/fat/cluster_chain.rs | 33 +++ src/fs/fat/directory.rs | 289 ++++++++++++++++++ src/fs/fat/fat_type.rs | 78 +++++ src/fs/fat/mod.rs | 568 ++++++++++++++++++++++++++++++++++++ src/fs/mod.rs | 51 ++++ src/loader/mod.rs | 1 + src/main.rs | 257 ++++++++++++++++ 15 files changed, 1508 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 .zed/settings.json create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100755 run.sh create mode 100644 rust-toolchain.toml create mode 100644 src/fs/fat/cluster_chain.rs create mode 100644 src/fs/fat/directory.rs create mode 100644 src/fs/fat/fat_type.rs create mode 100644 src/fs/fat/mod.rs create mode 100644 src/fs/mod.rs create mode 100644 src/loader/mod.rs create mode 100644 src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..85d49dc --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "x86_64-unknown-uefi" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37e8bf1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/disk.img +/OVMF_CODE.fd +/OVMF_VARS.fd diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..b7fd80b --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,11 @@ +{ + "lsp": { + "rust-analyzer": { + "initialization_options": { + "cargo": { + "allTargets": false, + }, + }, + }, + }, +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f7b3b32 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,164 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "efi-loader" +version = "0.1.0" +dependencies = [ + "bitflags", + "log", + "uefi", + "zerocopy", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "ucs2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79298e11f316400c57ec268f3c2c29ac3c4d4777687955cd3d4f3a35ce7eba" +dependencies = [ + "bit_field", +] + +[[package]] +name = "uefi" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe9058b73ee2b6559524af9e33199c13b2485ddbf3ad1181b68051cdc50c17" +dependencies = [ + "bitflags", + "cfg-if", + "log", + "ptr_meta", + "ucs2", + "uefi-macros", + "uefi-raw", + "uguid", +] + +[[package]] +name = "uefi-macros" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4687412b5ac74d245d5bfb1733ede50c31be19bf8a4b6a967a29b451bab49e67" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "uefi-raw" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f64fe59e11af447d12fd60a403c74106eb104309f34b4c6dbce6e927d97da9d" +dependencies = [ + "bitflags", + "uguid", +] + +[[package]] +name = "uguid" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6374ea5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "efi-loader" +version = "0.1.0" +edition = "2024" + +[dependencies] +bitflags = "2.11.0" +log = "0.4.29" +uefi = { version = "0.36.1", features = ["logger", "panic_handler", "log-debugcon", "alloc", "global_allocator"] } +zerocopy = { version = "0.8.47", features = ["derive"] } + +[workspace] diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e34644 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Setup +- Install `qemu-system-x86_64`, `gdisk` +- Install `cargo` (using [rustup](https://rustup.rs/) is the preferred cross-platform way to get Rust set up on your machine) +- We also require `OVMF_CODE.fd` and `OVMF_VARS.fd` in the root directory to boot QEMU in UEFI mode. These files are omitted due to potential licensing issues (read: I haven't properly looked into it yet). +- For creating a test disk image automatically, we require `mkfs.fat` and `mkfs.ext4` to be available. +# Running +Simply run `./run.sh` in the root directory to take care of setting up the environment and running QEMU with the required options. diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..f7acd9a --- /dev/null +++ b/run.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e + +PROJECT_DIR=$(dirname "$(realpath "$0")") + +cargo build +mkdir -p "$PROJECT_DIR/target/esp/efi/boot" +cp "$PROJECT_DIR/target/x86_64-unknown-uefi/debug/efi-loader.efi" "$PROJECT_DIR/target/esp/efi/boot/bootx64.efi" + +if [[ ! -f "$PROJECT_DIR/disk.img" ]]; then + dd if=/dev/zero of="$PROJECT_DIR/disk.img" bs=1M count=256 + sgdisk --zap-all --new=1::+64M --new=2:: disk.img + echo Creating file systems on test disk, might require password + DEVICE=$(sudo losetup -fP --show "$PROJECT_DIR/disk.img") + sudo mkfs.fat -F32 ${DEVICE}p1 + sudo mkfs.ext4 ${DEVICE}p2 + sudo losetup -d $DEVICE +fi + +qemu-system-x86_64 -enable-kvm \ + -drive format=raw,file=fat:rw:target/esp,id=disk0,if=none \ + -drive format=raw,file=disk.img,id=disk1,if=none \ + -drive if=pflash,format=raw,readonly=on,file=OVMF_CODE.fd \ + -drive if=pflash,format=raw,readonly=on,file=OVMF_VARS.fd \ + -device ahci,id=ahci \ + -device ide-hd,bus=ahci.0,drive=disk0 \ + -device ide-hd,bus=ahci.1,drive=disk1 \ + --no-shutdown diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..60d74b5 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +targets = ["x86_64-unknown-uefi"] diff --git a/src/fs/fat/cluster_chain.rs b/src/fs/fat/cluster_chain.rs new file mode 100644 index 0000000..f13d9b0 --- /dev/null +++ b/src/fs/fat/cluster_chain.rs @@ -0,0 +1,33 @@ +use core::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; + +use alloc::vec::Vec; + +use crate::fs::fat::Cluster; + +#[derive(Clone, Default)] +pub struct ClusterChain(Vec); +impl Deref for ClusterChain { + type Target = [Cluster]; + + fn deref(&self) -> &[Cluster] { + &self.0[..] + } +} +impl DerefMut for ClusterChain { + fn deref_mut(&mut self) -> &mut [Cluster] { + &mut self.0[..] + } +} +impl From> for ClusterChain { + fn from(value: Vec) -> Self { + Self(value) + } +} +impl Debug for ClusterChain { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_fmt(format_args!("ClusterChain({:04X?})", self.0)) + } +} diff --git a/src/fs/fat/directory.rs b/src/fs/fat/directory.rs new file mode 100644 index 0000000..5d07b81 --- /dev/null +++ b/src/fs/fat/directory.rs @@ -0,0 +1,289 @@ +use core::{fmt::Display, ops::Index}; + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use bitflags::bitflags; +use zerocopy::{FromBytes, TryFromBytes}; + +use crate::fs::{ + Error, + fat::{Cluster, FatFs, cluster_chain::ClusterChain}, +}; + +#[derive(Debug, Clone, Copy, FromBytes)] +pub struct DirectoryAttribute(u8); +impl Display for DirectoryAttribute { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let r = if self.contains(Self::READ_ONLY) { + "R" + } else { + "W" + }; + let d = if self.contains(Self::DIRECTORY) { + "DIR" + } else if self.contains(Self::LFN) { + "LFN" + } else { + " " + }; + let h = if self.contains(Self::HIDDEN) { + "H" + } else { + "-" + }; + let s = if self.contains(Self::SYSTEM) { + "S" + } else { + "-" + }; + let v = if self.contains(Self::VOLUME_ID) { + "V" + } else { + "-" + }; + let a = if self.contains(Self::ARCHIVE) { + "A" + } else { + "-" + }; + f.write_fmt(format_args!("{d} {r}{h}{s}{v}{a}")) + } +} + +#[allow(unused)] +impl DirectoryAttribute { + pub fn is_writable(&self) -> bool { + !self.contains(Self::READ_ONLY) + } + pub fn is_hidden(&self) -> bool { + self.contains(Self::HIDDEN) + } + pub fn is_system(&self) -> bool { + self.contains(Self::SYSTEM) + } + pub fn is_directory(&self) -> bool { + self.contains(Self::DIRECTORY) + } + pub fn is_archive(&self) -> bool { + self.contains(Self::ARCHIVE) + } + pub fn is_volume_id(&self) -> bool { + self.contains(Self::VOLUME_ID) + } + pub fn is_lfn(&self) -> bool { + self.intersects(Self::LFN) + } +} + +bitflags! { + impl DirectoryAttribute: u8 { + const NONE = 0; + const READ_ONLY = 0x01; + const HIDDEN = 0x02; + const SYSTEM = 0x04; + const VOLUME_ID = 0x08; + const DIRECTORY = 0x10; + const ARCHIVE = 0x20; + const LFN = 0x0F; + } +} + +#[derive(Debug, Clone)] +struct DirectoryList(Vec); +impl Index for DirectoryList { + type Output = DirectoryEntry; + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +pub struct DirectoryListIter { + list: DirectoryList, + list_index: usize, +} +impl DirectoryListIter { + pub fn from_chain(fs: &mut FatFs, cluster_chain: &ClusterChain) -> Self { + let mut result = Self { + list_index: 0, + list: DirectoryList(Vec::with_capacity(32)), + }; + result.buffer_entries_from_chain(fs, cluster_chain).unwrap(); + result + } + pub fn from_root(fs: &mut FatFs) -> Self { + let mut result = Self { + list_index: 0, + list: DirectoryList(Vec::with_capacity(fs.boot_sector.root_entry_count as usize)), + }; + result.buffer_entries_from_root(fs).unwrap(); + result + } + fn buffer_entries_from_chain( + &mut self, + fs: &mut FatFs, + chain: &ClusterChain, + ) -> Result<(), Error<()>> { + let mut sector_buffer = Vec::with_capacity(fs.sector_size_bytes()); + sector_buffer.resize(fs.sector_size_bytes(), 0); + for cluster in chain.iter() { + self.append_directory_list_from_cluster(fs, *cluster, sector_buffer.as_mut_slice())?; + } + Ok(()) + } + fn buffer_entries_from_root(&mut self, fs: &mut FatFs) -> Result<(), Error<()>> { + let mut sector_buffer = Vec::with_capacity(fs.sector_size_bytes()); + sector_buffer.resize(fs.sector_size_bytes(), 0); + fs.seek(fs.root_dir_sector()); + + for _ in 0..fs.root_dir_sectors { + fs.partition.read(&mut sector_buffer)?; + if !self.load_entries_from_bytes(sector_buffer.as_slice()) { + break; + } + } + Ok(()) + } + fn append_directory_list_from_cluster( + &mut self, + fs: &mut FatFs, + cluster: Cluster, + buffer: &mut [u8], + ) -> Result<(), Error<()>> { + fs.seek(cluster); + fs.partition.read(buffer)?; + + self.load_entries_from_bytes(buffer as &[u8]); + Ok(()) + } + + fn load_entries_from_bytes(&mut self, mut bytes: &[u8]) -> bool { + while bytes.len() > 0 { + let (entry, remainder) = DirectoryEntry::try_read_from_prefix(bytes).unwrap(); + bytes = remainder; + if entry.is_end() { + return false; + } + if entry.is_used() { + self.list.0.push(entry); + } + } + true + } +} +impl Iterator for DirectoryListIter { + type Item = DirectoryEntry; + + fn next(&mut self) -> Option { + if self.list_index >= self.list.0.len() { + None + } else { + let result = self.list.0[self.list_index]; + self.list_index += 1; + Some(result) + } + } +} + +#[derive(Debug, Copy, Clone, FromBytes)] +#[repr(packed, C)] +pub struct DirectoryEntry { + filename: [u8; 11], + attributes: DirectoryAttribute, + reserved_0: u8, + ctime_100th: u8, + ctime: u32, + atime: u16, + cluster_high: u16, + mtime: u32, + cluster_low: u16, + file_size: u32, +} +impl DirectoryEntry { + pub fn start_cluster(&self) -> Cluster { + Cluster((self.cluster_high as u32) << 16 | self.cluster_low as u32) + } + pub fn is_end(&self) -> bool { + self.filename[0] == 0 + } + pub fn is_unused(&self) -> bool { + self.filename[0] == 0xE5 + } + pub fn is_used(&self) -> bool { + !self.is_unused() && !self.is_end() + } + pub fn is_lfn(&self) -> bool { + self.attributes.is_lfn() + } + pub fn decode_lfn(&self) -> (u8, String) { + let mut concat = [0u8; 26]; + let mut result = [0u16; 13]; + unsafe { + let data = core::mem::transmute::<*const DirectoryEntry, *const u8>(self as *const _); + let temp = concat.as_mut_ptr(); + temp.byte_add(0) + .copy_from_nonoverlapping(data.byte_add(1), 10); + temp.byte_add(10) + .copy_from_nonoverlapping(data.byte_add(14), 12); + temp.byte_add(22) + .copy_from_nonoverlapping(data.byte_add(28), 4); + //String::from_utf16(unsafe { core::mem::transmute(concat.as_slice()) }) + } + concat + .chunks(2) + .enumerate() + .for_each(|(i, val)| result[i] = u16::from_le_bytes([val[0], val[1]])); + let end = result.iter().position(|p| *p == 0).unwrap_or(13); + (self.filename[0], String::from_utf16_lossy(&result[..end])) + } + pub fn name(&self) -> String { + String::from_utf8_lossy(&self.filename).to_string() + } + pub fn attributes(&self) -> DirectoryAttribute { + self.attributes + } + pub fn size(&self) -> u32 { + self.file_size + } +} +impl Display for DirectoryEntry { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let end = if self.is_end() { + "END " + } else if self.is_used() { + "USED" + } else { + "FREE" + }; + + if self.attributes.is_lfn() { + let (part, lfn) = self.decode_lfn(); + f.write_fmt(format_args!("{lfn:13} {} [{:02X}]", self.attributes, part)) + } else { + let mut filename = self.filename.clone(); + if self.is_used() { + f.write_fmt(format_args!( + "{:13} {} {end} {}", + self.name(), + self.attributes, + self.start_cluster() + )) + } else { + filename.iter_mut().for_each(|e| { + if *e < 0x20 { + *e = b' ' + } else if *e == 0xE5 { + *e = b'_' + } + }); + f.write_fmt(format_args!( + "{:13} {} {end} {}", + self.name(), + self.attributes, + self.start_cluster() + )) + } + } + } +} diff --git a/src/fs/fat/fat_type.rs b/src/fs/fat/fat_type.rs new file mode 100644 index 0000000..0301a7e --- /dev/null +++ b/src/fs/fat/fat_type.rs @@ -0,0 +1,78 @@ +use alloc::vec::Vec; +use zerocopy::FromBytes; + +use crate::fs::fat::{Cluster, FatFs, Sector}; + +#[derive(Debug, Copy, Clone)] +#[repr(packed, C)] +pub struct Fat12; +//impl Fat12 { +// pub fn read_fat(_fat: &mut FatFs) -> Result, uefi::Error> { +// unimplemented!() +// } +//} + +#[derive(Debug, Copy, Clone, FromBytes)] +#[repr(packed, C)] +pub struct Fat16 { + pub bios_drive_num: u8, + reserved_0: u8, + pub boot_signature: u16, + pub volume_id: u32, + pub volume_label: [u8; 11], + pub fat_type_label: [u8; 8], +} +impl Fat16 { + pub fn read_fat(fat: &mut FatFs) -> Result, uefi::Error> { + let fat_len_bytes = fat.fat_size_bytes() as usize; + let mut buffer = Vec::with_capacity(fat_len_bytes); + buffer.resize(fat_len_bytes, 0u8); + fat.seek(Sector(fat.first_fat_sector)); + fat.partition.read(&mut buffer)?; + Ok(buffer + .chunks_exact(2) + .into_iter() + .map(|val| Cluster::from_16(u16::from_ne_bytes([val[0], val[1]]))) + .collect::>()) + } +} + +#[derive(Debug, Copy, Clone, FromBytes)] +#[repr(packed, C)] +pub struct Fat32 { + pub table_size_32: u32, + pub extended_flags: u16, + pub fat_version: u16, + pub root_cluster: u32, + pub fat_info: u16, + pub backup_bs_sector: u16, + reserved_0: [u8; 12], + pub drive_number: u8, + reserved_1: u8, + pub boot_signature: u8, + pub volume_id: u32, + pub volume_label: [u8; 11], + pub fat_type_label: [u8; 8], +} +impl Fat32 { + pub fn read_fat(fat: &mut FatFs) -> Result, uefi::Error> { + let fat_len_bytes = fat.fat_size_bytes() as usize; + let mut buffer = Vec::with_capacity(fat_len_bytes); + buffer.resize(fat_len_bytes, 0u8); + fat.seek(Sector(fat.first_fat_sector)); + fat.partition.read(&mut buffer)?; + Ok(buffer + .chunks_exact(4) + .into_iter() + .map(|val| Cluster::from_32(u32::from_ne_bytes([val[0], val[1], val[2], val[3]]))) + .collect::>()) + } +} + +#[derive(Debug, Copy, Clone)] +#[allow(unused)] +pub enum FatType { + Fat12(Fat12), + Fat16(Fat16), + Fat32(Fat32), +} diff --git a/src/fs/fat/mod.rs b/src/fs/fat/mod.rs new file mode 100644 index 0000000..8868446 --- /dev/null +++ b/src/fs/fat/mod.rs @@ -0,0 +1,568 @@ +pub mod cluster_chain; +pub mod directory; +pub mod fat_type; + +use core::fmt::{Debug, Display}; + +use alloc::{ + string::{String, ToString}, + vec::Vec, +}; +use log::debug; +use zerocopy::{FromBytes, TryFromBytes}; + +use crate::{ + Partition, Seek, + fs::{ + BpbError, Error, Seekable, + fat::{ + cluster_chain::ClusterChain, + directory::{DirectoryAttribute, DirectoryEntry, DirectoryListIter}, + fat_type::{Fat12, Fat16, Fat32, FatType}, + }, + }, +}; + +pub struct LfnBuilder(Vec<(u8, String)>); +impl LfnBuilder { + pub fn new() -> Self { + Self(Vec::with_capacity(16)) + } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn add(&mut self, part: (u8, String)) { + self.0.push(part); + } + pub fn build(mut self) -> String { + self.0.sort_by_key(|e| e.0); + self.0.into_iter().map(|e| e.1).collect::>().join("") + } +} + +#[derive(Debug, Copy, Clone, Default, FromBytes)] +#[repr(packed)] +pub struct FatBootSector { + bootjmp: [u8; 3], + oem_name: [u8; 8], + bytes_per_sector: u16, + sectors_per_cluster: u8, + reserved_sector_count: u16, + table_count: u8, + root_entry_count: u16, + total_sectors_16: u16, + media_type: u8, + table_size_16: u16, + sectors_per_track: u16, + head_side_count: u16, + hidden_sector_count: u32, + total_sectors_32: u32, +} +impl FatBootSector { + pub fn cluster_size_bytes(&self) -> u32 { + let bps = unsafe { (&raw const self.bytes_per_sector).read_unaligned() }; + bps as u32 * self.sectors_per_cluster as u32 + } + pub fn from_bytes(bytes: &[u8]) -> Result<(FatBootSector, FatType), BpbError> { + let (bpb, extended_fat_info) = FatBootSector::try_read_from_prefix(bytes).unwrap(); + let bps = unsafe { (&raw const bpb.bytes_per_sector).read_unaligned() as u32 }; + if ![512, 1024, 2048, 4096].contains(&bps) { + return Err(BpbError::BytesPerSector); + } + + let total_sectors = if bpb.total_sectors_16 == 0 { + bpb.total_sectors_32 + } else { + bpb.total_sectors_16 as u32 + }; + let fat_size = bpb.table_size_16 as u32; + let root_dir_sectors = (bpb.root_entry_count as u32 * 32 + (bps - 1)) / bps; + let data_sectors = total_sectors - bpb.reserved_sector_count as u32 + + bpb.table_count as u32 * fat_size + + root_dir_sectors; + let total_clusters = data_sectors / bpb.sectors_per_cluster as u32; + + let fat_type = if total_clusters < 4085 { + FatType::Fat12(Fat12) + } else if total_clusters < 65525 { + let (fat, _) = Fat16::try_read_from_prefix(extended_fat_info).unwrap(); + FatType::Fat16(fat) + } else { + let (fat, _) = Fat32::try_read_from_prefix(extended_fat_info).unwrap(); + FatType::Fat32(fat) + }; + + if bpb.sectors_per_cluster == 0 + || (bpb.sectors_per_cluster & (bpb.sectors_per_cluster.wrapping_sub(1))) != 0 + { + return Err(BpbError::SectorsPerCluster); + } + if bpb.reserved_sector_count == 0 { + return Err(BpbError::ReservedSectorCount); + } + if bpb.table_count == 0 { + return Err(BpbError::NumFats); + } + if bps * bpb.sectors_per_cluster as u32 == 0 { + return Err(BpbError::ClusterSize); + } + if total_sectors == 0 { + return Err(BpbError::TotalSectorCount); + } + + Ok((bpb, fat_type)) + } +} + +#[derive(Debug)] +pub struct FatFs { + partition: Partition, + boot_sector: FatBootSector, + fat_type: FatType, + allocation_table: Option, + root_directory: CachedFsEntry, + + //total_sectors: u32, + fat_size: u32, + root_dir_sectors: u32, + first_data_sector: u32, + first_fat_sector: u32, + //data_sectors: u32, + //total_clusters: u32, +} + +impl FatFs { + pub fn mount(mut partition: Partition) -> Result> { + let mut sector = [0u8; 90]; + partition.seek(Seek::SetFromStart(0)); + if partition.read(&mut sector).is_err() { + return Err(Error::Open); + } + + let (boot_sector, fat_type) = FatBootSector::from_bytes(§or)?; + let bps = unsafe { (&raw const boot_sector.bytes_per_sector).read_unaligned() as u32 }; + + //let total_sectors = if boot_sector.total_sectors_16 == 0 { + // boot_sector.total_sectors_32 + //} else { + // boot_sector.total_sectors_16 as u32 + //}; + let fat_size = if boot_sector.table_size_16 == 0 { + match fat_type { + FatType::Fat32(fat) => fat.table_size_32, + _ => boot_sector.table_size_16 as u32, + } + } else { + boot_sector.table_size_16 as u32 + }; + let root_dir_sectors = (boot_sector.root_entry_count as u32 * 32 + (bps - 1)) / bps; + //let data_sectors = total_sectors - boot_sector.reserved_sector_count as u32 + // + boot_sector.table_count as u32 * fat_size + // + root_dir_sectors; + //let total_clusters = data_sectors / boot_sector.sectors_per_cluster as u32; + + Ok(Self { + boot_sector, + fat_type, + partition, + allocation_table: None, + root_directory: CachedFsEntry::root(), + + //total_sectors, + fat_size, + root_dir_sectors, + first_data_sector: boot_sector.reserved_sector_count as u32 + + (boot_sector.table_count as u32 * fat_size) + + root_dir_sectors, + first_fat_sector: boot_sector.reserved_sector_count as u32, + //data_sectors, + //total_clusters, + }) + } + //pub fn is_fat12(&self) -> bool { + // match self.fat_type { + // FatType::Fat12(_) => true, + // _ => false, + // } + //} + pub fn is_fat16(&self) -> bool { + match self.fat_type { + FatType::Fat16(_) => true, + _ => false, + } + } + pub fn is_fat32(&self) -> bool { + match self.fat_type { + FatType::Fat32(_) => true, + _ => false, + } + } + pub fn sector_size_bytes(&self) -> usize { + self.boot_sector.bytes_per_sector as usize + } + pub fn cluster_size_bytes(&self) -> usize { + self.boot_sector.cluster_size_bytes() as usize + } + fn fat_size_bytes(&self) -> u32 { + self.fat_size * self.boot_sector.bytes_per_sector as u32 + } + fn read_allocation_table(&mut self) -> Result<(), uefi::Error> { + self.allocation_table = Some(FileAllocationTable::from_fat(self)?); + Ok(()) + } + fn root_dir_sector(&self) -> Sector { + Sector(self.first_data_sector - self.root_dir_sectors) + } + + pub fn open(&mut self, path: &str) -> Result> { + if !path.starts_with("/") { + return Err(Error::InvalidPath); + } + if self.allocation_table.is_none() { + self.read_allocation_table()?; + if let Some(fat) = &self.allocation_table { + match self.fat_type { + FatType::Fat32(fat32) => { + self.root_directory.cluster_chain = + fat.read_cluster_chain(Cluster::from_32(fat32.root_cluster)); + } + FatType::Fat16(_) => { + let children = CachedFsEntry::load_children_from_root_dir(self); + self.root_directory.children = Some(children); + } + _ => {} + } + } + } + let path_segments = path[1..].split("/").collect::>(); + let mut path_index = 0; + let mut current_entry = None; + while path_index < path_segments.len() { + if current_entry + .as_ref() + .unwrap_or(&self.root_directory) + .children + .is_none() + { + let cluster_chain = current_entry + .as_ref() + .unwrap_or(&self.root_directory) + .cluster_chain + .clone(); + let loaded_children = CachedFsEntry::load_children(&cluster_chain, self); + current_entry + .as_mut() + .unwrap_or(&mut self.root_directory) + .children = Some(loaded_children); + } + + if let Some(child) = current_entry + .as_ref() + .unwrap_or(&self.root_directory) + .find_child_cached(path_segments[path_index]) + { + debug!("{child}"); + current_entry = Some(child); + path_index += 1; + } else { + return Err(Error::NotFound); + } + } + + let entry = current_entry.ok_or(Error::NotFound)?; + if entry.is_directory() { + return Err(Error::Directory); + } + + Ok(entry.into()) + } + + pub fn seek>(&mut self, to: S) { + let offset = to.to_byte_offset(self); + self.partition.seek(Seek::SetFromStart(offset)); + } +} + +#[derive(Debug)] +pub struct FileAllocationTable { + cluster_chain: Vec, +} + +impl FileAllocationTable { + pub fn from_fat(fat_fs: &mut FatFs) -> Result { + let cluster_chain = match fat_fs.fat_type { + FatType::Fat32(_) => Fat32::read_fat(fat_fs)?, + FatType::Fat16(_) => Fat16::read_fat(fat_fs)?, + _ => unimplemented!(), + }; + Ok(Self { cluster_chain }) + } + pub fn read_cluster_chain(&self, mut start_cluster: Cluster) -> ClusterChain { + let mut buffer = Vec::with_capacity(16); + while start_cluster.is_allocated() { + let next_cluster = self.cluster_chain[start_cluster.masked() as usize]; + + if next_cluster.is_empty() { + break; + } + buffer.push(start_cluster); + start_cluster = next_cluster; + } + let chain: ClusterChain = buffer.into(); + chain + } +} + +#[derive(Debug, Clone)] +pub struct CachedFsEntry { + long_name: Option, + short_name: String, + attributes: DirectoryAttribute, + cluster_chain: ClusterChain, + size: u32, + children: Option>, + root: bool, +} +impl CachedFsEntry { + pub fn root() -> Self { + Self { + long_name: None, + short_name: "".to_string(), + attributes: DirectoryAttribute::DIRECTORY, + children: None, + cluster_chain: ClusterChain::default(), + size: 0, + root: true, + } + } + + pub fn find_child_cached(&self, name: &str) -> Option { + if let Some(children) = &self.children { + for child in children { + let name_match = if let Some(long_name) = &child.long_name { + long_name == name || child.short_name == name.to_ascii_uppercase() + } else { + child.short_name == name.to_ascii_uppercase() + }; + if name_match { + return Some(child.clone()); + } + } + } + None + } + + fn load_children(cluster_chain: &ClusterChain, fs: &mut FatFs) -> Vec { + let mut children = Vec::new(); + let iter = DirectoryListIter::from_chain(fs, cluster_chain); + let mut lfn_builder = LfnBuilder::new(); + + for dir in iter { + if dir.is_lfn() { + lfn_builder.add(dir.decode_lfn()); + } else { + let mut lfn = None; + if !lfn_builder.is_empty() { + lfn = Some(lfn_builder.build()); + lfn_builder = LfnBuilder::new(); + } + let mut entry = CachedFsEntry::from(dir, fs); + entry.long_name = lfn; + children.push(entry); + } + } + children + } + fn load_children_from_root_dir(fs: &mut FatFs) -> Vec { + let mut children = Vec::new(); + let iter = DirectoryListIter::from_root(fs); + let mut lfn_builder = LfnBuilder::new(); + + for dir in iter { + if dir.is_lfn() { + lfn_builder.add(dir.decode_lfn()); + } else { + let mut lfn = None; + if !lfn_builder.is_empty() { + lfn = Some(lfn_builder.build()); + lfn_builder = LfnBuilder::new(); + } + let mut entry = CachedFsEntry::from(dir, fs); + entry.long_name = lfn; + children.push(entry); + } + } + children + } + pub fn is_directory(&self) -> bool { + self.root || self.attributes.is_directory() + } +} +impl CachedFsEntry { + pub fn from(value: DirectoryEntry, fs: &FatFs) -> Self { + Self { + attributes: value.attributes(), + children: None, + cluster_chain: fs + .allocation_table + .as_ref() + .unwrap() + .read_cluster_chain(value.start_cluster()), + long_name: None, + root: false, + short_name: value.name(), + size: value.size(), + } + } +} +impl Display for CachedFsEntry { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_fmt(format_args!( + "{} {}", + self.attributes, + self.long_name.as_ref().unwrap_or(&self.short_name) + )) + } +} + +pub struct FileHandle { + chain: ClusterChain, + length: u64, + position: u64, +} +impl FileHandle { + //pub fn seek(&mut self, seek: Seek) { + // match seek { + // Seek::Move(i) => self.position += i.cast_unsigned(), + // Seek::SetFromStart(i) => self.position = i, + // Seek::SetFromEnd(i) => self.position = self.length - i, + // }; + // self.position = self.position.clamp(0, self.length); + //} + pub fn read(&mut self, buffer: &mut [u8], fs: &mut FatFs) -> Result> { + let max_copy = ((self.length - self.position) as usize).min(buffer.len()); + + let cluster_low = self.position as usize / fs.cluster_size_bytes(); + let cluster_high = (self.position as usize + max_copy) / fs.cluster_size_bytes(); + + let needed_clusters = if cluster_low == cluster_high { + &[self.chain[cluster_low]] + } else { + &self.chain[cluster_low..=cluster_high] + }; + let cluster_buffer_len = needed_clusters.len() * fs.cluster_size_bytes(); + let mut cluster_buffer = Vec::with_capacity(cluster_buffer_len); + cluster_buffer.resize(cluster_buffer_len, 0u8); + + for (i, cluster) in needed_clusters.iter().enumerate() { + let slice = + &mut cluster_buffer[i * fs.cluster_size_bytes()..(i + 1) * fs.cluster_size_bytes()]; + fs.seek(cluster); + fs.partition.read(slice)?; + } + + let cluster_slice_start = self.position as usize % fs.cluster_size_bytes(); + buffer[0..max_copy] + .copy_from_slice(&cluster_buffer[cluster_slice_start..cluster_slice_start + max_copy]); + + Ok(max_copy) + } + pub fn len(&self) -> usize { + self.length as usize + } +} + +impl From for FileHandle { + fn from(value: CachedFsEntry) -> Self { + Self { + chain: value.cluster_chain.clone(), + length: value.size as u64, + position: 0, + } + } +} + +#[repr(transparent)] +#[derive(Debug, Copy, Clone)] +pub struct Cluster(u32); +impl Cluster { + pub fn from_32(value: u32) -> Self { + Cluster(value) + } + pub fn from_16(value: u16) -> Self { + if value >= 0xfff8 { + Cluster(0x0ffffff8) + } else { + Cluster(value as u32) + } + } + //pub fn from_12(_value: u16) -> Self { + // unimplemented!() + //} + + pub fn start_sector(&self, fs: &FatFs) -> Sector { + Sector((self.0 - 2) * fs.boot_sector.sectors_per_cluster as u32 + fs.first_data_sector) + } + pub fn is_empty(&self) -> bool { + self.masked() == 0 + } + pub fn is_end(&self) -> bool { + self.masked() >= 0x0ffffff8 + } + pub fn is_allocated(&self) -> bool { + self.masked() >= 2 && !self.is_end() + } + pub fn masked(&self) -> u32 { + self.0 & 0x0fffffff + } + //pub fn unmasked(&self) -> u32 { + // self.0 + //} +} +// TODO: There must be a nicer way to handle both `Cluster` as well as `&Cluster` +impl Seekable for Cluster { + fn to_byte_offset(self, fs: &FatFs) -> u64 { + self.start_sector(fs).byte_offset(fs) + } +} +// TODO: There must be a nicer way to handle both `Cluster` as well as `&Cluster` +impl Seekable for &Cluster { + fn to_byte_offset(self, fs: &FatFs) -> u64 { + self.start_sector(fs).byte_offset(fs) + } +} +impl Display for Cluster { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if self.is_allocated() { + f.write_fmt(format_args!("Cluster 0x{:08X}", self.0)) + } else { + f.write_str("No cluster") + } + } +} + +#[repr(transparent)] +#[derive(Debug, Copy, Clone)] +pub struct Sector(u32); +impl Sector { + pub fn byte_offset(&self, fs: &FatFs) -> u64 { + self.0 as u64 * fs.boot_sector.bytes_per_sector as u64 + } +} +// TODO: There must be a nicer way to handle both `Sector` as well as `&Sector` +impl Seekable for Sector { + fn to_byte_offset(self, fs: &FatFs) -> u64 { + self.byte_offset(fs) + } +} +// TODO: There must be a nicer way to handle both `Sector` as well as `&Sector` +impl Seekable for &Sector { + fn to_byte_offset(self, fs: &FatFs) -> u64 { + self.byte_offset(fs) + } +} +impl From for Sector { + fn from(value: u32) -> Self { + Sector(value) + } +} diff --git a/src/fs/mod.rs b/src/fs/mod.rs new file mode 100644 index 0000000..8c215f8 --- /dev/null +++ b/src/fs/mod.rs @@ -0,0 +1,51 @@ +use core::fmt::Debug; + +pub mod fat; + +pub enum Error { + Open, + NotFound, + Directory, + InvalidPath, + Uefi(uefi::Error), + Other(T), +} +impl Debug for Error +where + T: Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Open => write!(f, "Open"), + Self::NotFound => write!(f, "NotFound"), + Self::Directory => write!(f, "Directory"), + Self::InvalidPath => write!(f, "InvalidPath"), + Self::Uefi(arg0) => f.debug_tuple("Uefi").field(arg0).finish(), + Self::Other(arg0) => f.debug_tuple("Other").field(arg0).finish(), + } + } +} + +#[derive(Debug)] +pub enum BpbError { + BytesPerSector, + SectorsPerCluster, + ReservedSectorCount, + NumFats, + ClusterSize, + TotalSectorCount, +} +impl From for Error { + fn from(value: BpbError) -> Self { + Error::Other(value) + } +} +impl From for Error { + fn from(value: uefi::Error) -> Self { + Error::Uefi(value) + } +} + +pub trait Seekable { + fn to_byte_offset(self, fs: &T) -> u64; +} diff --git a/src/loader/mod.rs b/src/loader/mod.rs new file mode 100644 index 0000000..195d1d8 --- /dev/null +++ b/src/loader/mod.rs @@ -0,0 +1 @@ +pub fn load_elf_at() {} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..494a443 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,257 @@ +#![no_main] +#![no_std] + +extern crate alloc; + +mod fs; +mod loader; + +use alloc::vec::Vec; +use core::{fmt::Display, time::Duration}; +use log::{debug, error, warn}; +use uefi::{ + Identify, + boot::{OpenProtocolParams, ScopedProtocol, SearchType, locate_handle_buffer, open_protocol}, + prelude::*, + proto::{ + ProtocolPointer, + device_path::{ + DevicePath, DevicePathNodeEnum, DevicePathNodeIterator, + media::{HardDrive, PartitionFormat}, + messaging::{Atapi, Iscsi, MasterSlave, NvmeNamespace, PrimarySecondary, Sata, Scsi}, + }, + media::{ + block::{BlockIO, BlockIOMedia}, + disk::DiskIo, + }, + }, +}; + +use crate::fs::fat::FatFs; + +enum Seek { + SetFromStart(u64), + SetFromEnd(u64), + Move(i64), +} + +#[derive(Debug)] +struct Partition { + part_offset: u64, + media_id: u32, + disk: ScopedProtocol, + position: u64, + length: u64, +} +impl Partition { + fn read(&mut self, buffer: &mut [u8]) -> Result { + if let Err(err) = self.disk.read_disk(self.media_id, self.position, buffer) { + Err(err) + } else { + self.position += buffer.len() as u64; + Ok(buffer.len() as u64) + } + } + fn write(&mut self, buffer: &mut [u8]) -> Result { + if let Err(err) = self.disk.write_disk(self.media_id, self.position, buffer) { + Err(err) + } else { + self.position += buffer.len() as u64; + Ok(buffer.len() as u64) + } + } + fn seek(&mut self, seek: Seek) { + match seek { + Seek::Move(i) => self.position += i.cast_unsigned(), + Seek::SetFromStart(i) => self.position = i, + Seek::SetFromEnd(i) => self.position = self.length - i, + }; + //info!( + // "Seek to 0x{:08X} (0x{:08X})", + // self.position, + // self.position + self.part_offset + //); + } +} + +struct BlockDevice<'a> { + handle: &'a Handle, + media: BlockDeviceMedia<'a>, + raw_media: &'a BlockIOMedia, + hd: &'a HardDrive, +} +impl<'a> BlockDevice<'a> { + pub fn new( + handle: &'a Handle, + raw_media: &'a BlockIOMedia, + path_iter: DevicePathNodeIterator<'a>, + ) -> Result { + let mut part: Option<&'a HardDrive> = None; + let mut media: Option> = None; + for node in path_iter { + if let Ok(node_enum) = node.as_enum() { + match node_enum { + DevicePathNodeEnum::MediaHardDrive(hd) => part = Some(hd), + _ => {} + } + if let Ok(node_media) = BlockDeviceMedia::try_from(node_enum) { + media = Some(node_media); + } + } + } + if let Some(part) = part.take() + && let Some(media) = media.take() + { + Ok(BlockDevice { + handle, + media, + raw_media, + hd: part, + }) + } else { + Err(uefi::Error::new(Status::NOT_FOUND, ())) + } + } + fn open_disk(&self) -> uefi::Result { + open_proto::(self.handle).map(|proto| Partition { + part_offset: self.hd.partition_start() * self.raw_media.block_size() as u64, + disk: proto, + position: 0, + length: self.hd.partition_size() * self.raw_media.block_size() as u64, + media_id: self.raw_media.media_id(), + }) + } +} +impl Display for BlockDevice<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let format = if self.hd.partition_format() == PartitionFormat::MBR { + "MBR" + } else { + "GPT" + }; + f.write_fmt(format_args!( + "{}/{format}(0x{:X})", + self.media, + self.hd.partition_number() + )) + } +} + +#[derive(Debug)] +enum BlockDeviceMedia<'a> { + Atapi(&'a Atapi), + Nvme(&'a NvmeNamespace), + Sata(&'a Sata), + Scsi(&'a Scsi), + Iscsi(&'a Iscsi), +} +impl Display for BlockDeviceMedia<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + BlockDeviceMedia::Sata(sata) => f.write_fmt(format_args!( + "Sata(0x{:X},0x{:X},0x{:X})", + sata.hba_port_number(), + sata.port_multiplier_port_number(), + sata.logical_unit_number() + )), + BlockDeviceMedia::Atapi(atapi) => f.write_fmt(format_args!( + "Atapi({},{},0x{:X})", + if atapi.primary_secondary() == PrimarySecondary::PRIMARY { + "Primary" + } else { + "Secondary" + }, + if atapi.master_slave() == MasterSlave::MASTER { + "Master" + } else { + "Slave" + }, + atapi.logical_unit_number() + )), + BlockDeviceMedia::Nvme(nvme) => f.write_fmt(format_args!( + "NVMe(0x{:X},0x{:X})", + nvme.namespace_identifier(), + nvme.ieee_extended_unique_identifier() + )), + BlockDeviceMedia::Scsi(_) => f.write_fmt(format_args!("SCSI(???)")), + BlockDeviceMedia::Iscsi(_) => f.write_fmt(format_args!("iSCSI(???)")), + } + } +} +impl<'a> TryFrom> for BlockDeviceMedia<'a> { + type Error = (); + + fn try_from(value: DevicePathNodeEnum<'a>) -> Result { + match value { + DevicePathNodeEnum::MessagingAtapi(atapi) => Ok(Self::Atapi(atapi)), + DevicePathNodeEnum::MessagingNvmeNamespace(nvme) => Ok(Self::Nvme(nvme)), + DevicePathNodeEnum::MessagingSata(sata) => Ok(Self::Sata(sata)), + DevicePathNodeEnum::MessagingScsi(scsi) => Ok(Self::Scsi(scsi)), + DevicePathNodeEnum::MessagingIscsi(iscsi) => Ok(Self::Iscsi(iscsi)), + _ => Err(()), + } + } +} + +fn open_proto(handle: &Handle) -> uefi::Result> { + unsafe { + open_protocol::

( + OpenProtocolParams { + handle: *handle, + agent: boot::image_handle(), + controller: None, + }, + boot::OpenProtocolAttributes::GetProtocol, + ) + } +} + +#[entry] +fn main() -> Status { + uefi::helpers::init().unwrap(); + + let all_block_io_handles = + locate_handle_buffer(SearchType::ByProtocol(&BlockIO::GUID)).unwrap(); + for handle in all_block_io_handles.iter() { + if let Ok(block_io) = open_proto::(handle) { + let media = block_io.media(); + if !media.is_logical_partition() || !media.is_media_present() { + continue; + } + if let Ok(device_path) = open_proto::(handle) + && let Ok(block_dev) = BlockDevice::new(handle, media, device_path.node_iter()) + && let Ok(disk) = block_dev.open_disk() + { + match FatFs::mount(disk) { + Ok(mut fat) => { + if fat.is_fat32() { + if let Ok(mut handle) = fat.open("/path/to/beemovie.txt") { + let mut buffer = [0u8; 1000]; + let read = handle.read(&mut buffer, &mut fat).unwrap(); + debug!("Read {read} bytes"); + } else { + warn!("FAT32 open failed"); + } + } else if fat.is_fat16() { + if let Ok(mut handle) = fat.open("/efi/boot/bootx64.efi") { + let size = handle.len(); + let mut buffer = Vec::with_capacity(size); + buffer.resize(size, 0u8); + let read = handle.read(&mut buffer, &mut fat).unwrap(); + debug!("Read {read} bytes"); + } else { + warn!("FAT16 open failed"); + } + } + } + Err(_) => {} + } + } + } else { + error!("Failed to open BlockIO {handle:?}"); + } + } + + boot::stall(Duration::from_secs(60)); + Status::SUCCESS +}