lamexfat 0.1.0

no_std read-only exFAT reader for UEFI bootloaders (removable media)
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The public `ExFat<R>` handle and its result types.

use alloc::{string::String, vec, vec::Vec};

use crate::{
    block_read::{read_exact, BlockRead},
    dir::{self, EntrySet},
    error::{Error, Result},
    file, name,
    path::Path,
    resolve,
    upcase::Upcase,
    vbr::{self, Geometry},
};

/// Largest file [`ExFat::read`] will allocate up front. A hostile StreamExtension
/// can declare a multi-GiB `DataLength` while occupying no clusters; this cap
/// refuses the allocation rather than letting it abort the boot (mirrors lamboot's
/// `MAX_BOOT_FILE_BYTES`). [`ExFat::read_at`] streams into a caller-sized buffer
/// and is unaffected.
pub const MAX_FILE_BYTES: u64 = 256 * 1024 * 1024;

/// The kind of a directory entry.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryKind {
    Regular,
    Directory,
    Other,
}

/// Inode-equivalent metadata from a directory entry set.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Metadata {
    size: u64,
    kind: EntryKind,
}

impl Metadata {
    /// File size in bytes (`DataLength`); 0 for a directory.
    pub fn size(&self) -> u64 {
        self.size
    }
    pub fn is_file(&self) -> bool {
        self.kind == EntryKind::Regular
    }
    pub fn is_dir(&self) -> bool {
        self.kind == EntryKind::Directory
    }
    pub fn kind(&self) -> EntryKind {
        self.kind
    }
}

/// A directory entry. `name` is the decoded (lossy-UTF-8) file name.
#[derive(Debug, Clone)]
pub struct DirEntry {
    pub name: String,
    pub kind: EntryKind,
    pub first_cluster: u32,
}

/// A mounted, read-only exFAT volume.
pub struct ExFat<R: BlockRead> {
    reader: R,
    geo: Geometry,
    upcase: Upcase,
    label: Option<String>,
}

impl<R: BlockRead> ExFat<R> {
    /// Mount: read + validate the boot sector (magic, geometry, boot-region
    /// checksum), cache geometry, load the up-case table, and read the volume
    /// label. `device_size_bytes` is accepted for parity / future bounds checks.
    pub fn open(mut reader: R, _device_size_bytes: u64) -> Result<Self> {
        let mut sec0 = [0u8; 512];
        read_exact(&mut reader, 0, &mut sec0, "io_vbr")?;
        let geo = vbr::parse(&sec0)?;

        // Verify the 11-sector Main Boot Region checksum (stored in sector 11).
        let bps = geo.bytes_per_sector as usize;
        let mut region = vec![0u8; bps * 12];
        read_exact(&mut reader, 0, &mut region, "io_vbr")?;
        vbr::verify_boot_checksum(&region, bps)?;

        let meta = dir::scan_root_meta(&mut reader, &geo)?;
        let mut upcase = Upcase::ascii();
        if let Some(cluster) = meta.upcase_cluster {
            upcase.load(&mut reader, &geo, cluster)?;
        }
        let label = meta
            .label
            .as_deref()
            .map(name::decode_lossy)
            .filter(|s| !s.is_empty());

        Ok(Self {
            reader,
            geo,
            upcase,
            label,
        })
    }

    /// The 32-bit Volume Serial Number (what `exfatlabel`/`blkid` report).
    pub fn volume_serial(&self) -> u32 {
        self.geo.volume_serial
    }

    /// The volume label, or `None` if unset.
    pub fn label(&self) -> Option<&str> {
        self.label.as_deref()
    }

    /// Metadata for `path`. The root (`/`) is reported as a zero-size directory.
    pub fn metadata(&mut self, path: Path<'_>) -> Result<Metadata> {
        match resolve::resolve(&mut self.reader, &self.geo, &self.upcase, path)? {
            None => Ok(Metadata {
                size: 0,
                kind: EntryKind::Directory,
            }),
            Some(es) => Ok(Metadata {
                size: es.data_length,
                kind: kind_of(&es),
            }),
        }
    }

    /// Whether `path` resolves to an entry.
    pub fn exists(&mut self, path: Path<'_>) -> Result<bool> {
        match resolve::resolve(&mut self.reader, &self.geo, &self.upcase, path) {
            Ok(_) => Ok(true),
            Err(Error::NotFound { .. }) => Ok(false),
            Err(e) => Err(e),
        }
    }

    /// Read a regular file's full contents (capped at [`MAX_FILE_BYTES`]).
    pub fn read(&mut self, path: Path<'_>) -> Result<Vec<u8>> {
        let es = self.regular_entry(path)?;
        if es.data_length > MAX_FILE_BYTES {
            return Err(Error::FileTooLarge {
                size: es.data_length,
                max: MAX_FILE_BYTES,
            });
        }
        let cap = usize::try_from(es.data_length).map_err(|_| Error::FileTooLarge {
            size: es.data_length,
            max: MAX_FILE_BYTES,
        })?;
        let mut out = vec![0u8; cap];
        let n = file::read_at(&mut self.reader, &self.geo, &es, 0, &mut out)?;
        out.truncate(n);
        Ok(out)
    }

    /// Read up to `buf.len()` bytes of a regular file from `offset` (0 = EOF).
    pub fn read_at(&mut self, path: Path<'_>, offset: u64, buf: &mut [u8]) -> Result<usize> {
        let es = self.regular_entry(path)?;
        file::read_at(&mut self.reader, &self.geo, &es, offset, buf)
    }

    /// Enumerate the entries of a directory (the root for `/`).
    pub fn read_dir(&mut self, path: Path<'_>) -> Result<Vec<DirEntry>> {
        let (cluster, contiguous) =
            match resolve::resolve(&mut self.reader, &self.geo, &self.upcase, path)? {
                None => (self.geo.root_cluster, false),
                Some(es) if es.is_dir => (es.first_cluster, es.contiguous),
                Some(_) => {
                    return Err(Error::NotFound {
                        component: "not a directory",
                    })
                }
            };
        let entries = dir::read_entries(&mut self.reader, &self.geo, cluster, contiguous)?;
        let mut out = Vec::with_capacity(entries.len());
        for es in entries {
            out.push(DirEntry {
                name: name::decode_lossy(&es.name),
                kind: kind_of(&es),
                first_cluster: es.first_cluster,
            });
        }
        Ok(out)
    }

    /// Resolve `path` to a regular-file entry set, erroring on the root or a
    /// directory.
    fn regular_entry(&mut self, path: Path<'_>) -> Result<EntrySet> {
        let es = resolve::resolve(&mut self.reader, &self.geo, &self.upcase, path)?
            .ok_or(Error::NotARegularFile)?;
        if es.is_dir {
            return Err(Error::NotARegularFile);
        }
        Ok(es)
    }
}

fn kind_of(es: &EntrySet) -> EntryKind {
    if es.is_dir {
        EntryKind::Directory
    } else {
        EntryKind::Regular
    }
}