ahrightos-fs 0.1.0

Portable filesystem, block-device, and IPC primitives for the AhRightOS teaching kernel.
Documentation
//! Lightweight disk-format detection for safe BlkFS mounting.
//!
//! This module intentionally detects only the small set of signatures needed to
//! avoid destructive classroom mistakes. It is **not** a partition driver and it
//! does not parse ext4/FAT; it only reports that such formats are unsupported.
//! Blank-device classification is also bounded: large all-zero prefixes are
//! treated as `Unknown` instead of scanning an entire host disk image.

use super::blkfs::{BlkFS, BLKFS_MIN_BLOCKS};
use super::block_dev::{BlockDeviceHandle, BLOCK_SIZE};
use super::file::FileError;

/// Coarse block-device format classification used before BlkFS mount.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiskFormat {
    /// Every block is zero, so compatibility mode may format BlkFS.
    Blank,
    /// Valid AhRightOS BlkFS superblock and root metadata.
    BlkFS,
    /// The BlkFS magic is present but the full superblock fails validation.
    InvalidBlkFS,
    /// Device is too small for the AhRightOS BlkFS layout.
    TooSmall,
    /// MBR signature was detected. AhRightOS does not mount partitions yet.
    MasterBootRecord,
    /// GPT header signature was detected. AhRightOS does not mount partitions yet.
    GuidPartitionTable,
    /// FAT boot-sector markers were detected. FAT is unsupported.
    FatFamily,
    /// ext-family superblock magic was detected. ext is unsupported.
    ExtFamily,
    /// Non-empty device with no supported/recognized AhRightOS BlkFS signature.
    Unknown,
}

impl DiskFormat {
    pub const fn description(self) -> &'static str {
        match self {
            DiskFormat::Blank => "blank device",
            DiskFormat::BlkFS => "AhRightOS BlkFS",
            DiskFormat::InvalidBlkFS => "invalid AhRightOS BlkFS",
            DiskFormat::TooSmall => "device too small for BlkFS",
            DiskFormat::MasterBootRecord => "MBR partition table",
            DiskFormat::GuidPartitionTable => "GPT partition table",
            DiskFormat::FatFamily => "FAT filesystem",
            DiskFormat::ExtFamily => "ext-family filesystem",
            DiskFormat::Unknown => "unknown disk format",
        }
    }
}

const BLKFS_MAGIC_LE: [u8; 4] = 0x424C_4B46u32.to_le_bytes();
const BLANK_SCAN_MAX_BLOCKS: usize = BLKFS_MIN_BLOCKS * 64;

/// Detect the broad format of a block device without mutating it.
pub fn detect_disk_format(dev: &BlockDeviceHandle) -> Result<DiskFormat, FileError> {
    if dev.block_count() < BLKFS_MIN_BLOCKS {
        return Ok(DiskFormat::TooSmall);
    }

    let mut block0 = [0u8; BLOCK_SIZE];
    dev.read_block(0, &mut block0)?;

    if block0[..4] == BLKFS_MAGIC_LE {
        return if BlkFS::open(dev.clone()).is_ok() {
            Ok(DiskFormat::BlkFS)
        } else {
            Ok(DiskFormat::InvalidBlkFS)
        };
    }

    if looks_like_gpt(dev)? {
        return Ok(DiskFormat::GuidPartitionTable);
    }
    if looks_like_ext(dev)? {
        return Ok(DiskFormat::ExtFamily);
    }
    if looks_like_fat(&block0) {
        return Ok(DiskFormat::FatFamily);
    }
    if has_mbr_signature(&block0) {
        return Ok(DiskFormat::MasterBootRecord);
    }
    if let Some(true) = device_is_blank(dev, &block0)? {
        return Ok(DiskFormat::Blank);
    }

    Ok(DiskFormat::Unknown)
}

fn device_is_blank(
    dev: &BlockDeviceHandle,
    block0: &[u8; BLOCK_SIZE],
) -> Result<Option<bool>, FileError> {
    if !block0.iter().all(|&byte| byte == 0) {
        return Ok(Some(false));
    }
    if dev.block_count() > BLANK_SCAN_MAX_BLOCKS {
        return Ok(None);
    }

    let mut block = [0u8; BLOCK_SIZE];
    for block_id in 1..dev.block_count() {
        dev.read_block(block_id, &mut block)?;
        if !block.iter().all(|&byte| byte == 0) {
            return Ok(Some(false));
        }
    }
    Ok(Some(true))
}

fn looks_like_gpt(dev: &BlockDeviceHandle) -> Result<bool, FileError> {
    if dev.block_count() <= 1 {
        return Ok(false);
    }
    let mut block1 = [0u8; BLOCK_SIZE];
    dev.read_block(1, &mut block1)?;
    Ok(block1[..8] == *b"EFI PART")
}

fn looks_like_ext(dev: &BlockDeviceHandle) -> Result<bool, FileError> {
    if dev.block_count() <= 2 {
        return Ok(false);
    }
    let mut block2 = [0u8; BLOCK_SIZE];
    dev.read_block(2, &mut block2)?;
    // ext superblock begins at byte 1024; magic lives at offset 56 in that
    // superblock, i.e. block 2 offset 56 for 512-byte AhRightOS block devices.
    Ok(block2[56] == 0x53 && block2[57] == 0xEF)
}

fn looks_like_fat(block0: &[u8; BLOCK_SIZE]) -> bool {
    let has_boot_signature = has_mbr_signature(block0);
    let fat12_or_16 = block0[54..59] == *b"FAT12" || block0[54..59] == *b"FAT16";
    let fat32 = block0[82..87] == *b"FAT32";
    has_boot_signature && (fat12_or_16 || fat32)
}

fn has_mbr_signature(block0: &[u8; BLOCK_SIZE]) -> bool {
    block0[510] == 0x55 && block0[511] == 0xAA
}