lamexfat 0.1.0

no_std read-only exFAT reader for UEFI bootloaders (removable media)
Documentation
// SPDX-License-Identifier: Apache-2.0
//! Cluster-chain traversal: the File Allocation Table follow and the NoFatChain
//! contiguous case, plus the one primitive that reads a logical byte range out
//! of a data stream.
//!
//! Adapted from exfat-slim @2ffd2c2 `fat.rs` (Apache-2.0) — the FAT-entry layout
//! and the `2..0xFFFFFFF6` valid-next-cluster range are the on-disk format; the
//! byte-offset stream reader, the cycle guard, and the contiguous-span bounds
//! are `lamexfat`.

use crate::{
    block_read::{read_exact, BlockRead},
    error::{Error, Result},
    vbr::Geometry,
};

/// The next cluster in the FAT chain after `cluster_id`, or `None` at the
/// end-of-chain / bad-cluster markers (`>= 0xFFFFFFF6`).
pub(crate) fn next_cluster<R: BlockRead>(
    reader: &mut R,
    geo: &Geometry,
    cluster_id: u32,
) -> Result<Option<u32>> {
    let off = geo.fat_entry_byte(cluster_id);
    let mut b = [0u8; 4];
    read_exact(reader, off, &mut b, "io_fat")?;
    let next = u32::from_le_bytes(b);
    if (2..0xFFFF_FFF6).contains(&next) {
        Ok(Some(next))
    } else {
        Ok(None)
    }
}

/// Step to the cluster after `cluster` in a stream. Contiguous streams advance
/// linearly (bounded against the heap); chained streams follow the FAT. A chain
/// that ends where the caller still expects data is a corruption, not EOF.
fn advance<R: BlockRead>(
    reader: &mut R,
    geo: &Geometry,
    cluster: u32,
    contiguous: bool,
) -> Result<u32> {
    if contiguous {
        let next = cluster.checked_add(1).ok_or(Error::Inconsistent {
            token: "cluster_oob",
        })?;
        if next
            .checked_sub(2)
            .is_none_or(|rel| rel >= geo.cluster_count)
        {
            return Err(Error::Inconsistent {
                token: "cluster_oob",
            });
        }
        Ok(next)
    } else {
        next_cluster(reader, geo, cluster)?.ok_or(Error::Inconsistent {
            token: "cluster_oob",
        })
    }
}

/// Fill `buf` from a data stream (file or directory) whose first cluster is
/// `first_cluster`, starting at logical byte offset `off`. `contiguous` selects
/// the NoFatChain path. Every cluster step is bounded by `cluster_count`, so a
/// cyclic or runaway chain terminates with `Error::Inconsistent` rather than
/// looping. The caller guarantees `off + buf.len()` is within the stream.
pub(crate) fn read_stream<R: BlockRead>(
    reader: &mut R,
    geo: &Geometry,
    first_cluster: u32,
    contiguous: bool,
    off: u64,
    buf: &mut [u8],
) -> Result<()> {
    if buf.is_empty() {
        return Ok(());
    }
    let csize = geo.cluster_size();
    let max_steps = u64::from(geo.cluster_count);
    let mut steps = 0u64;
    let mut cluster = first_cluster;

    // Walk to the cluster containing `off`.
    let mut intra = off;
    while intra >= csize {
        cluster = advance(reader, geo, cluster, contiguous)?;
        steps += 1;
        if steps > max_steps {
            return Err(Error::Inconsistent { token: "fat_cycle" });
        }
        intra -= csize;
    }

    let mut filled = 0usize;
    while filled < buf.len() {
        let base = geo.cluster_byte(cluster).ok_or(Error::Inconsistent {
            token: "cluster_oob",
        })?;
        let in_cluster = (csize - intra) as usize;
        let want = core::cmp::min(in_cluster, buf.len() - filled);
        read_exact(
            reader,
            base + intra,
            &mut buf[filled..filled + want],
            "io_data",
        )?;
        filled += want;
        intra = 0;
        if filled < buf.len() {
            cluster = advance(reader, geo, cluster, contiguous)?;
            steps += 1;
            if steps > max_steps {
                return Err(Error::Inconsistent { token: "fat_cycle" });
            }
        }
    }
    Ok(())
}