feat(storage): ext4-only disk via mkfs.ext4 -d; drop FAT16; feature-gated mount (ro on unknown ro_compat); ext4 read spans blocks, delete clears inode, mkdir counts dir; virtio-blk writes; test-qemu e2fsck gate
This commit is contained in:
@@ -11,7 +11,10 @@ fn main() {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let bins = manifest.join("../../target/riscv64gc-unknown-none-elf/release");
|
||||
let out = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||
for (var, bin) in [("KOS_INIT_ELF", "init"), ("KOS_STORAGE_ELF", "storage-qemu")] {
|
||||
for (var, bin) in [
|
||||
("KOS_INIT_ELF", "init"),
|
||||
("KOS_STORAGE_ELF", "storage-qemu"),
|
||||
] {
|
||||
let path = bins.join(bin);
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
let path = if path.is_file() {
|
||||
|
||||
@@ -12,9 +12,15 @@ use libkos::syscall;
|
||||
use types::bounded::CapSlot;
|
||||
use types::budget::Limits;
|
||||
|
||||
/// Where init put them (see init's manifest: spawn endpoint, then control).
|
||||
const INIT: CapSlot = CapSlot::const_new(0);
|
||||
const CONTROL: CapSlot = CapSlot::const_new(1);
|
||||
/// Where init put them (see init's manifest: `uses`, the spawn endpoint,
|
||||
/// then control).
|
||||
const STORAGE: CapSlot = CapSlot::const_new(0);
|
||||
const INIT: CapSlot = CapSlot::const_new(1);
|
||||
const CONTROL: CapSlot = CapSlot::const_new(2);
|
||||
/// Where storage's buffer is mapped.
|
||||
const STORAGE_BUF_VA: usize = 0x0180_0000;
|
||||
/// Read back from the disk image by scripts/test-qemu.sh.
|
||||
const NOTE: &[u8] = b"written by k-os on ext4\n";
|
||||
|
||||
/// Over half the allowance (128 pages), so a second one fits only after
|
||||
/// the first has died and its limits came back.
|
||||
@@ -31,7 +37,28 @@ fn run_hello(died: CapSlot) -> Result<(), &'static str> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A file written through storage, which the host then checks with e2fsck.
|
||||
fn write_a_file() -> Result<(), &'static str> {
|
||||
let mut fs = libkos::fs::Storage::attach(STORAGE, STORAGE_BUF_VA).map_err(|_| "cannot attach to storage")?;
|
||||
let h = fs.create(b"selftest.txt").map_err(|_| "cannot create a file")?;
|
||||
if fs.write(h, NOTE) != Ok(NOTE.len()) {
|
||||
return Err("short write");
|
||||
}
|
||||
fs.close(h).map_err(|_| "close failed")?;
|
||||
let h = fs.open(b"selftest.txt").map_err(|_| "cannot reopen the file")?;
|
||||
let mut back = [0u8; 64];
|
||||
let n = fs.read(h, &mut back).map_err(|_| "read failed")?;
|
||||
fs.close(h).map_err(|_| "close failed")?;
|
||||
if &back[..n] != NOTE {
|
||||
return Err("read back something else");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check() -> Result<(), &'static str> {
|
||||
write_a_file()?;
|
||||
libkos::println!("spawntest: a file written to ext4 reads back: ok");
|
||||
|
||||
let died = syscall::create_notification().map_err(|_| "no notification")?;
|
||||
run_hello(died)?;
|
||||
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
|
||||
|
||||
@@ -26,6 +26,7 @@ pub struct VirtioBlockDevice {
|
||||
mmio: UserMmio,
|
||||
queue: VirtioQueue,
|
||||
dma: DmaAddrs,
|
||||
sectors: usize,
|
||||
}
|
||||
|
||||
impl VirtioBlockDevice {
|
||||
@@ -52,20 +53,23 @@ impl VirtioBlockDevice {
|
||||
data_pa: PhysicalAddress::from_raw(hdr_pa + 32),
|
||||
data_va: VirtualAddress::from_raw(REQ_HDR_VA + 32),
|
||||
};
|
||||
let capacity = regs.read(block::CONFIG_CAPACITY) as u64
|
||||
| (regs.read(block::CONFIG_CAPACITY + 4) as u64) << 32;
|
||||
Some(Self {
|
||||
mmio: UserMmio,
|
||||
queue: VirtioQueue::new(QueueAddrs::from_base(VQUEUE_VA)),
|
||||
dma,
|
||||
sectors: usize::try_from(capacity).ok()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockDevice for VirtioBlockDevice {
|
||||
fn sector_size(&self) -> usize { SECTOR_SIZE }
|
||||
fn sector_count(&self) -> usize { 0xFFFF }
|
||||
fn sector_count(&self) -> usize { self.sectors }
|
||||
|
||||
fn read_sector(&mut self, sector: SectorId, buf: &mut [u8]) -> bool {
|
||||
if buf.len() < SECTOR_SIZE { return false; }
|
||||
if buf.len() < SECTOR_SIZE || sector.raw() as usize >= self.sectors { return false; }
|
||||
let mut sector_buf = [0u8; SECTOR_SIZE];
|
||||
if !block::read_sector(&self.mmio, &self.queue, &self.dma, sector.raw(), &mut sector_buf) {
|
||||
return false;
|
||||
@@ -74,8 +78,8 @@ impl BlockDevice for VirtioBlockDevice {
|
||||
true
|
||||
}
|
||||
|
||||
fn write_sector(&mut self, _sector: SectorId, _buf: &[u8]) -> bool { false }
|
||||
|
||||
/// Writing is not implemented yet.
|
||||
fn writable(&self) -> bool { false }
|
||||
fn write_sector(&mut self, sector: SectorId, buf: &[u8]) -> bool {
|
||||
if sector.raw() as usize >= self.sectors { return false; }
|
||||
block::write_sector(&self.mmio, &self.queue, &self.dma, sector.raw(), buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,3 +60,37 @@ pub fn read_sector(
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Request types (virtio spec 5.2.6).
|
||||
const T_OUT: u32 = 1;
|
||||
/// Device configuration: the disk's size in 512-byte sectors (le64).
|
||||
pub const CONFIG_CAPACITY: usize = 0x100;
|
||||
|
||||
/// Writes one sector from `buf`; true once the device reports success.
|
||||
pub fn write_sector(
|
||||
mmio: &impl MmioAccess,
|
||||
queue: &VirtioQueue,
|
||||
dma: &DmaAddrs,
|
||||
sector: u32,
|
||||
buf: &[u8],
|
||||
) -> bool {
|
||||
if buf.len() < SECTOR_SIZE { return false; }
|
||||
|
||||
unsafe {
|
||||
core::ptr::copy_nonoverlapping(buf.as_ptr(), dma.data_va.raw() as *mut u8, SECTOR_SIZE);
|
||||
let hdr = dma.header_va.raw() as *mut BlockReqHeader;
|
||||
core::ptr::write_volatile(&raw mut (*hdr).req_type, T_OUT);
|
||||
core::ptr::write_volatile(&raw mut (*hdr).reserved, 0);
|
||||
core::ptr::write_volatile(&raw mut (*hdr).sector, sector as u64);
|
||||
core::ptr::write_volatile(dma.status_va.raw() as *mut u8, 0xFF);
|
||||
}
|
||||
|
||||
// The device reads the data this time; only the status is written back.
|
||||
let descs = [
|
||||
Descriptor { addr: dma.header_pa.raw() as u64, len: 16, writable: false },
|
||||
Descriptor { addr: dma.data_pa.raw() as u64, len: SECTOR_SIZE as u32, writable: false },
|
||||
Descriptor { addr: dma.status_pa.raw() as u64, len: 1, writable: true },
|
||||
];
|
||||
if !queue.submit(mmio, &descs) { return false; }
|
||||
unsafe { core::ptr::read_volatile(dma.status_va.raw() as *const u8) == 0 }
|
||||
}
|
||||
|
||||
@@ -44,7 +44,10 @@ impl<'a> Launcher<'a> {
|
||||
) -> Result<Self, &'static str> {
|
||||
let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?;
|
||||
let deaths = syscall::create_notification().map_err(|_| "no death notification")?;
|
||||
Ok(Self { storage, scratch, devices, served: [None; MAX_SERVED], next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() })
|
||||
// Storage was started by boot, before init; programs may use it too.
|
||||
let mut served = [None; MAX_SERVED];
|
||||
served[0] = Some(("storage", crate::STORAGE_EP));
|
||||
Ok(Self { storage, scratch, devices, served, next_badge: 1, spawn_ep, deaths, spawner: Spawner::new() })
|
||||
}
|
||||
|
||||
pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) {
|
||||
|
||||
@@ -18,7 +18,7 @@ use types::permission::{Permission, PermissionSet};
|
||||
/// Where the buffer storage shares with init is mapped.
|
||||
const STORAGE_BUF_VA: usize = 0x0180_0000;
|
||||
/// Boot loads storage before init and leaves its endpoint here.
|
||||
const STORAGE_EP: CapSlot = CapSlot::const_new(2);
|
||||
pub(crate) const STORAGE_EP: CapSlot = CapSlot::const_new(2);
|
||||
|
||||
pub fn run() -> ! {
|
||||
libkos::start::set_console(devices::console());
|
||||
|
||||
@@ -70,6 +70,7 @@ pub const SERVERS: &[Program] = &[
|
||||
/// spawns through init, then reports and halts.
|
||||
pub const TESTS: &[Program] = &[Program {
|
||||
serves: false,
|
||||
uses: &["storage"],
|
||||
spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
|
||||
system_control: true,
|
||||
..server("spawntest", b"spawntst.elf", SMALL)
|
||||
|
||||
@@ -7,6 +7,7 @@ pub struct BlockGroupDescriptor {
|
||||
pub inode_table: u32,
|
||||
pub free_blocks_count: u16,
|
||||
pub free_inodes_count: u16,
|
||||
pub used_dirs_count: u16,
|
||||
}
|
||||
|
||||
impl BlockGroupDescriptor {
|
||||
@@ -21,6 +22,7 @@ impl BlockGroupDescriptor {
|
||||
inode_table: read_u32(&buf, off + 8),
|
||||
free_blocks_count: read_u16(&buf, off + 12),
|
||||
free_inodes_count: read_u16(&buf, off + 14),
|
||||
used_dirs_count: read_u16(&buf, off + 16),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -34,6 +36,7 @@ impl BlockGroupDescriptor {
|
||||
write_u32(&mut buf, off + 8, self.inode_table);
|
||||
write_u16(&mut buf, off + 12, self.free_blocks_count);
|
||||
write_u16(&mut buf, off + 14, self.free_inodes_count);
|
||||
write_u16(&mut buf, off + 16, self.used_dirs_count);
|
||||
write_block(device, gdt_block as u64, spb, &buf)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,29 @@ impl<B: BlockDevice> Ext4<B> {
|
||||
io::write_block(&mut self.device, block_num, self.sb.sectors_per_block(), &block_buf)
|
||||
}
|
||||
|
||||
/// Zeroes `inode_num`'s on-disk record: how ext4 marks an inode unused,
|
||||
/// so nothing still points at the blocks it had.
|
||||
pub(super) fn clear_inode(&mut self, inode_num: u32) -> bool {
|
||||
let Some(bgd) = BlockGroupDescriptor::read(
|
||||
&mut self.device,
|
||||
self.gdt_block,
|
||||
self.sb.inode_block_group(inode_num),
|
||||
self.sb.sectors_per_block(),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let size = self.sb.inode_size as usize;
|
||||
let inode_offset = self.sb.inode_index_in_group(inode_num) as usize * size;
|
||||
let block_num = (bgd.inode_table + (inode_offset / self.sb.block_size) as u32) as u64;
|
||||
let at = inode_offset % self.sb.block_size;
|
||||
let mut block_buf = [0u8; 4096];
|
||||
if !io::read_block(&mut self.device, block_num, self.sb.sectors_per_block(), &mut block_buf) {
|
||||
return false;
|
||||
}
|
||||
block_buf[at..at + size].fill(0);
|
||||
io::write_block(&mut self.device, block_num, self.sb.sectors_per_block(), &block_buf)
|
||||
}
|
||||
|
||||
pub fn init_empty_inode(mode: u16) -> Inode {
|
||||
let mut block_data = [0u8; 60];
|
||||
io::write_u16(&mut block_data, 0, EXTENT_MAGIC);
|
||||
|
||||
@@ -50,9 +50,27 @@ impl<B: BlockDevice> Ext4<B> {
|
||||
open_files: [const { None }; MAX_OPEN_FILES],
|
||||
})
|
||||
}
|
||||
|
||||
/// One more directory in `inode`'s block group (e2fsck checks the count).
|
||||
fn count_dir(&mut self, inode: u32) {
|
||||
let (bg, spb) = (self.sb.inode_block_group(inode), self.sb.sectors_per_block());
|
||||
if let Some(mut d) = bgd::BlockGroupDescriptor::read(&mut self.device, self.gdt_block, bg, spb) {
|
||||
d.used_dirs_count += 1;
|
||||
d.write_back(&mut self.device, self.gdt_block, bg, spb);
|
||||
}
|
||||
}
|
||||
|
||||
/// The disk, for a test to look at what was written.
|
||||
pub fn into_device(self) -> B {
|
||||
self.device
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockDevice> FileSystem for Ext4<B> {
|
||||
fn writable(&self) -> bool {
|
||||
self.sb.writable && self.device.writable()
|
||||
}
|
||||
|
||||
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
|
||||
let inode_num = self.resolve_path(path)?;
|
||||
let inode = self.read_inode(inode_num)?;
|
||||
@@ -73,13 +91,9 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
|
||||
let file = *self.open_files[handle.raw() as usize].as_ref()?;
|
||||
if file.offset >= file.size { return Some(0); }
|
||||
|
||||
let remaining = file.size - file.offset;
|
||||
let to_read = buf.len().min(remaining);
|
||||
// As much as `buf` holds, across blocks: a short read means the end.
|
||||
let to_read = buf.len().min(file.size - file.offset);
|
||||
let bs = self.sb.block_size;
|
||||
|
||||
let logical_block = (file.offset / bs) as u32;
|
||||
let byte_in_block = file.offset % bs;
|
||||
|
||||
let inode = Inode {
|
||||
mode: 0,
|
||||
size: file.size as u64,
|
||||
@@ -88,17 +102,22 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
|
||||
blocks_count: 0,
|
||||
block_data: file.block_data,
|
||||
};
|
||||
let phys_block = self.map_logical_block(&inode, logical_block)?;
|
||||
|
||||
let mut done = 0;
|
||||
let mut block_buf = [0u8; 4096];
|
||||
read_block(&mut self.device, phys_block, self.sb.sectors_per_block(), &mut block_buf);
|
||||
while done < to_read {
|
||||
let offset = file.offset + done;
|
||||
let phys_block = self.map_logical_block(&inode, (offset / bs) as u32)?;
|
||||
if !read_block(&mut self.device, phys_block, self.sb.sectors_per_block(), &mut block_buf) {
|
||||
return None;
|
||||
}
|
||||
let byte_in_block = offset % bs;
|
||||
let chunk = (to_read - done).min(bs - byte_in_block);
|
||||
buf[done..done + chunk].copy_from_slice(&block_buf[byte_in_block..byte_in_block + chunk]);
|
||||
done += chunk;
|
||||
}
|
||||
|
||||
let available = bs - byte_in_block;
|
||||
let chunk = to_read.min(available);
|
||||
buf[..chunk].copy_from_slice(&block_buf[byte_in_block..byte_in_block + chunk]);
|
||||
|
||||
self.open_files[handle.raw() as usize].as_mut().map(|f| f.offset += chunk);
|
||||
Some(chunk)
|
||||
self.open_files[handle.raw() as usize].as_mut().map(|f| f.offset += done);
|
||||
Some(done)
|
||||
}
|
||||
|
||||
fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option<usize> {
|
||||
@@ -262,6 +281,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
|
||||
if let Some(child_inode) = self.read_inode(child_inode_num) {
|
||||
self.free_all_blocks(&child_inode);
|
||||
}
|
||||
self.clear_inode(child_inode_num);
|
||||
self.free_inode(child_inode_num);
|
||||
for slot in self.open_files.iter_mut() {
|
||||
if let Some(f) = slot {
|
||||
@@ -331,6 +351,7 @@ impl<B: BlockDevice> FileSystem for Ext4<B> {
|
||||
pi.links_count += 1;
|
||||
self.write_inode(parent_inode_num, &pi);
|
||||
}
|
||||
self.count_dir(new_inode_num);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -3,6 +3,22 @@ use super::io::{read_u32, write_u32, read_u16, write_u16, SECTOR_SIZE};
|
||||
|
||||
const EXT4_MAGIC: u16 = 0xEF53;
|
||||
|
||||
// What this driver understands. A disk with any other incompatible feature
|
||||
// is not mounted; with any other read-only-compatible one, or a journal or
|
||||
// hashed directories it would not keep up to date, it is mounted read-only.
|
||||
const INCOMPAT_FILETYPE: u32 = 0x2;
|
||||
const INCOMPAT_EXTENTS: u32 = 0x40;
|
||||
const INCOMPAT_FLEX_BG: u32 = 0x200;
|
||||
const KNOWN_INCOMPAT: u32 = INCOMPAT_FILETYPE | INCOMPAT_EXTENTS | INCOMPAT_FLEX_BG;
|
||||
const RO_COMPAT_SPARSE_SUPER: u32 = 0x1;
|
||||
const RO_COMPAT_LARGE_FILE: u32 = 0x2;
|
||||
const RO_COMPAT_DIR_NLINK: u32 = 0x20;
|
||||
const RO_COMPAT_EXTRA_ISIZE: u32 = 0x40;
|
||||
const KNOWN_RO_COMPAT: u32 =
|
||||
RO_COMPAT_SPARSE_SUPER | RO_COMPAT_LARGE_FILE | RO_COMPAT_DIR_NLINK | RO_COMPAT_EXTRA_ISIZE;
|
||||
const COMPAT_HAS_JOURNAL: u32 = 0x4;
|
||||
const COMPAT_DIR_INDEX: u32 = 0x20;
|
||||
|
||||
pub struct Superblock {
|
||||
pub inodes_count: u32,
|
||||
pub blocks_count: u32,
|
||||
@@ -13,6 +29,9 @@ pub struct Superblock {
|
||||
pub first_data_block: u32,
|
||||
pub free_blocks_count: u32,
|
||||
pub free_inodes_count: u32,
|
||||
/// False when a feature it does not maintain means writing would
|
||||
/// leave the disk inconsistent.
|
||||
pub writable: bool,
|
||||
}
|
||||
|
||||
impl Superblock {
|
||||
@@ -22,7 +41,13 @@ impl Superblock {
|
||||
if magic != EXT4_MAGIC { return None; }
|
||||
|
||||
let log_block_size = read_u32(data, 0x18);
|
||||
let (compat, incompat, ro_compat) = (read_u32(data, 0x5C), read_u32(data, 0x60), read_u32(data, 0x64));
|
||||
if incompat & !KNOWN_INCOMPAT != 0 || log_block_size > 2 {
|
||||
return None;
|
||||
}
|
||||
let writable = ro_compat & !KNOWN_RO_COMPAT == 0 && compat & (COMPAT_HAS_JOURNAL | COMPAT_DIR_INDEX) == 0;
|
||||
Some(Self {
|
||||
writable,
|
||||
inodes_count: read_u32(data, 0x00),
|
||||
blocks_count: read_u32(data, 0x04),
|
||||
block_size: 1024 << log_block_size,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
//! BIOS parameter block: disk geometry.
|
||||
|
||||
use super::SECTOR_SIZE;
|
||||
|
||||
pub(super) struct Bpb {
|
||||
pub(super) bytes_per_sector: u16,
|
||||
pub(super) sectors_per_cluster: u8,
|
||||
pub(super) reserved_sectors: u16,
|
||||
pub(super) num_fats: u8,
|
||||
pub(super) root_entry_count: u16,
|
||||
pub(super) total_sectors: u16,
|
||||
pub(super) sectors_per_fat: u16,
|
||||
}
|
||||
|
||||
impl Bpb {
|
||||
/// Reads the BPB, refusing geometry the rest of this module cannot
|
||||
/// work with: the disk is untrusted input like any client request.
|
||||
pub(super) fn parse(sector: &[u8]) -> Option<Self> {
|
||||
if sector.len() < SECTOR_SIZE {
|
||||
return None;
|
||||
}
|
||||
if sector[510] != 0x55 || sector[511] != 0xAA {
|
||||
return None;
|
||||
}
|
||||
let bpb = Self {
|
||||
bytes_per_sector: u16::from_le_bytes([sector[11], sector[12]]),
|
||||
sectors_per_cluster: sector[13],
|
||||
reserved_sectors: u16::from_le_bytes([sector[14], sector[15]]),
|
||||
num_fats: sector[16],
|
||||
root_entry_count: u16::from_le_bytes([sector[17], sector[18]]),
|
||||
total_sectors: u16::from_le_bytes([sector[19], sector[20]]),
|
||||
sectors_per_fat: u16::from_le_bytes([sector[22], sector[23]]),
|
||||
};
|
||||
let usable = bpb.bytes_per_sector as usize == SECTOR_SIZE
|
||||
&& bpb.sectors_per_cluster.is_power_of_two()
|
||||
&& bpb.num_fats > 0
|
||||
&& bpb.sectors_per_fat > 0;
|
||||
usable.then_some(bpb)
|
||||
}
|
||||
|
||||
pub(super) fn fat_start(&self) -> u32 {
|
||||
self.reserved_sectors as u32
|
||||
}
|
||||
|
||||
pub(super) fn root_dir_start(&self) -> u32 {
|
||||
self.fat_start() + (self.num_fats as u32) * (self.sectors_per_fat as u32)
|
||||
}
|
||||
|
||||
pub(super) fn root_dir_sectors(&self) -> u32 {
|
||||
((self.root_entry_count as u32) * 32 + (self.bytes_per_sector as u32) - 1)
|
||||
/ (self.bytes_per_sector as u32)
|
||||
}
|
||||
|
||||
pub(super) fn data_start(&self) -> u32 {
|
||||
self.root_dir_start() + self.root_dir_sectors()
|
||||
}
|
||||
|
||||
/// The first sector of data cluster `cluster`. Clusters 0 and 1 are
|
||||
/// reserved and hold no data.
|
||||
pub(super) fn cluster_to_sector(&self, cluster: u16) -> Option<u32> {
|
||||
let index = (cluster as u32).checked_sub(2)?;
|
||||
Some(self.data_start() + index * (self.sectors_per_cluster as u32))
|
||||
}
|
||||
|
||||
pub(super) fn cluster_size(&self) -> usize {
|
||||
self.sectors_per_cluster as usize * SECTOR_SIZE
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
//! Root directory entries and 8.3 names.
|
||||
|
||||
use super::Fat16;
|
||||
use super::SECTOR_SIZE;
|
||||
use crate::filesystem::FileName;
|
||||
use port_block_device::{BlockDevice, SectorId};
|
||||
|
||||
impl<B: BlockDevice> Fat16<B> {
|
||||
pub(super) fn find_in_root_dir(&mut self, name: &[u8]) -> Option<(u16, usize)> {
|
||||
let mut buf = [0u8; SECTOR_SIZE];
|
||||
let fat_name = to_fat_name(name);
|
||||
for s in 0..self.bpb.root_dir_sectors() {
|
||||
if !self
|
||||
.device
|
||||
.read_sector(SectorId::new(self.bpb.root_dir_start() + s), &mut buf)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for i in 0..(SECTOR_SIZE / 32) {
|
||||
let off = i * 32;
|
||||
if buf[off] == 0x00 || buf[off] == 0xE5 {
|
||||
continue;
|
||||
}
|
||||
if buf[off..off + 11] == fat_name {
|
||||
let cluster = u16::from_le_bytes([buf[off + 26], buf[off + 27]]);
|
||||
let size = u32::from_le_bytes([
|
||||
buf[off + 28],
|
||||
buf[off + 29],
|
||||
buf[off + 30],
|
||||
buf[off + 31],
|
||||
]);
|
||||
return Some((cluster, size as usize));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn create_dir_entry(&mut self, name: &[u8], cluster: u16) -> bool {
|
||||
let mut buf = [0u8; SECTOR_SIZE];
|
||||
let fat_name = to_fat_name(name);
|
||||
for s in 0..self.bpb.root_dir_sectors() {
|
||||
let sector = self.bpb.root_dir_start() + s;
|
||||
if !self.device.read_sector(SectorId::new(sector), &mut buf) {
|
||||
continue;
|
||||
}
|
||||
for i in 0..(SECTOR_SIZE / 32) {
|
||||
let off = i * 32;
|
||||
if buf[off] == 0x00 || buf[off] == 0xE5 {
|
||||
buf[off..off + 11].copy_from_slice(&fat_name);
|
||||
buf[off + 11] = 0x20;
|
||||
for b in &mut buf[off + 12..off + 26] {
|
||||
*b = 0;
|
||||
}
|
||||
buf[off + 26] = cluster as u8;
|
||||
buf[off + 27] = (cluster >> 8) as u8;
|
||||
for b in &mut buf[off + 28..off + 32] {
|
||||
*b = 0;
|
||||
}
|
||||
self.device.write_sector(SectorId::new(sector), &buf);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub(super) fn delete_dir_entry(&mut self, name: &[u8]) -> Option<u16> {
|
||||
let mut buf = [0u8; SECTOR_SIZE];
|
||||
let fat_name = to_fat_name(name);
|
||||
for s in 0..self.bpb.root_dir_sectors() {
|
||||
let sector = self.bpb.root_dir_start() + s;
|
||||
if !self.device.read_sector(SectorId::new(sector), &mut buf) {
|
||||
continue;
|
||||
}
|
||||
for i in 0..(SECTOR_SIZE / 32) {
|
||||
let off = i * 32;
|
||||
if buf[off..off + 11] == fat_name {
|
||||
let cluster = u16::from_le_bytes([buf[off + 26], buf[off + 27]]);
|
||||
buf[off] = 0xE5;
|
||||
self.device.write_sector(SectorId::new(sector), &buf);
|
||||
return Some(cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn update_dir_size(&mut self, name: &[u8], size: u32, cluster: u16) {
|
||||
let mut buf = [0u8; SECTOR_SIZE];
|
||||
let fat_name = to_fat_name(name);
|
||||
for s in 0..self.bpb.root_dir_sectors() {
|
||||
let sector = self.bpb.root_dir_start() + s;
|
||||
if !self.device.read_sector(SectorId::new(sector), &mut buf) {
|
||||
continue;
|
||||
}
|
||||
for i in 0..(SECTOR_SIZE / 32) {
|
||||
let off = i * 32;
|
||||
if buf[off..off + 11] == fat_name {
|
||||
buf[off + 28..off + 32].copy_from_slice(&size.to_le_bytes());
|
||||
buf[off + 26] = cluster as u8;
|
||||
buf[off + 27] = (cluster >> 8) as u8;
|
||||
self.device.write_sector(SectorId::new(sector), &buf);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn to_fat_name(name: &[u8]) -> [u8; 11] {
|
||||
let mut fat = [b' '; 11];
|
||||
let dot_pos = name.iter().position(|&b| b == b'.').unwrap_or(name.len());
|
||||
for i in 0..dot_pos.min(8) {
|
||||
fat[i] = name[i].to_ascii_uppercase();
|
||||
}
|
||||
if dot_pos < name.len() {
|
||||
for i in 0..(name.len() - dot_pos - 1).min(3) {
|
||||
fat[8 + i] = name[dot_pos + 1 + i].to_ascii_uppercase();
|
||||
}
|
||||
}
|
||||
fat
|
||||
}
|
||||
|
||||
pub(super) fn from_fat_name(raw: &[u8]) -> FileName {
|
||||
let mut buf = [0u8; 32];
|
||||
let mut pos = 0;
|
||||
let name_end = raw[..8]
|
||||
.iter()
|
||||
.rposition(|&b| b != b' ')
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
for i in 0..name_end {
|
||||
buf[pos] = raw[i].to_ascii_lowercase();
|
||||
pos += 1;
|
||||
}
|
||||
let ext_end = raw[8..11]
|
||||
.iter()
|
||||
.rposition(|&b| b != b' ')
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(0);
|
||||
if ext_end > 0 {
|
||||
buf[pos] = b'.';
|
||||
pos += 1;
|
||||
for i in 0..ext_end {
|
||||
buf[pos] = raw[8 + i].to_ascii_lowercase();
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
FileName::from_bytes(&buf[..pos])
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
//! FAT table: cluster allocation and chains.
|
||||
|
||||
use super::Fat16;
|
||||
use super::SECTOR_SIZE;
|
||||
use port_block_device::{BlockDevice, SectorId};
|
||||
|
||||
impl<B: BlockDevice> Fat16<B> {
|
||||
pub(super) fn read_fat_entry(&mut self, cluster: u16) -> u16 {
|
||||
let fat_offset = (cluster as u32) * 2;
|
||||
let sector = self.bpb.fat_start() + fat_offset / (SECTOR_SIZE as u32);
|
||||
let offset = (fat_offset % (SECTOR_SIZE as u32)) as usize;
|
||||
let mut buf = [0u8; SECTOR_SIZE];
|
||||
if !self.device.read_sector(SectorId::new(sector), &mut buf) {
|
||||
return 0xFFFF;
|
||||
}
|
||||
u16::from_le_bytes([buf[offset], buf[offset + 1]])
|
||||
}
|
||||
|
||||
pub(super) fn write_fat_entry(&mut self, cluster: u16, value: u16) {
|
||||
let fat_offset = (cluster as u32) * 2;
|
||||
let sector = self.bpb.fat_start() + fat_offset / (SECTOR_SIZE as u32);
|
||||
let offset = (fat_offset % (SECTOR_SIZE as u32)) as usize;
|
||||
let mut buf = [0u8; SECTOR_SIZE];
|
||||
self.device.read_sector(SectorId::new(sector), &mut buf);
|
||||
buf[offset] = value as u8;
|
||||
buf[offset + 1] = (value >> 8) as u8;
|
||||
self.device.write_sector(SectorId::new(sector), &buf);
|
||||
}
|
||||
|
||||
pub(super) fn alloc_cluster(&mut self) -> Option<u16> {
|
||||
let max = self.bpb.total_sectors as u16 / self.bpb.sectors_per_cluster as u16;
|
||||
for i in 2..max {
|
||||
if self.read_fat_entry(i) == 0 {
|
||||
self.write_fat_entry(i, 0xFFFF);
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(super) fn free_chain(&mut self, start: u16) {
|
||||
let mut cluster = start;
|
||||
loop {
|
||||
if cluster < 2 || cluster >= 0xFFF8 {
|
||||
break;
|
||||
}
|
||||
let next = self.read_fat_entry(cluster);
|
||||
self.write_fat_entry(cluster, 0);
|
||||
cluster = next;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn follow_chain(&mut self, start: u16, skip_clusters: usize) -> u16 {
|
||||
let mut cluster = start;
|
||||
for _ in 0..skip_clusters {
|
||||
let next = self.read_fat_entry(cluster);
|
||||
if next >= 0xFFF8 {
|
||||
return cluster;
|
||||
}
|
||||
cluster = next;
|
||||
}
|
||||
cluster
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
//! FAT16 filesystem over a block device (root directory only).
|
||||
|
||||
mod bpb;
|
||||
mod dir;
|
||||
mod fat;
|
||||
|
||||
use crate::filesystem::{DirEntry, FileHandle, FileName, FileSystem};
|
||||
use bpb::Bpb;
|
||||
use dir::from_fat_name;
|
||||
use port_block_device::{BlockDevice, SectorId};
|
||||
|
||||
const SECTOR_SIZE: usize = 512;
|
||||
const MAX_OPEN_FILES: usize = 8;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct OpenFile {
|
||||
first_cluster: u16,
|
||||
size: usize,
|
||||
offset: usize,
|
||||
name: FileName,
|
||||
}
|
||||
|
||||
pub struct Fat16<B: BlockDevice> {
|
||||
device: B,
|
||||
bpb: Bpb,
|
||||
open_files: [Option<OpenFile>; MAX_OPEN_FILES],
|
||||
}
|
||||
|
||||
impl<B: BlockDevice> Fat16<B> {
|
||||
pub fn mount(mut device: B) -> Option<Self> {
|
||||
let mut buf = [0u8; SECTOR_SIZE];
|
||||
if !device.read_sector(SectorId::new(0), &mut buf) {
|
||||
return None;
|
||||
}
|
||||
let bpb = Bpb::parse(&buf)?;
|
||||
Some(Self {
|
||||
device,
|
||||
bpb,
|
||||
open_files: [const { None }; MAX_OPEN_FILES],
|
||||
})
|
||||
}
|
||||
|
||||
/// The open-file slot `handle` names; `None` for handles past the
|
||||
/// table, which come straight from clients.
|
||||
fn slot(&mut self, handle: FileHandle) -> Option<&mut Option<OpenFile>> {
|
||||
self.open_files.get_mut(handle.raw() as usize)
|
||||
}
|
||||
|
||||
fn alloc_handle(&self) -> Option<FileHandle> {
|
||||
for i in 0..MAX_OPEN_FILES {
|
||||
if self.open_files[i].is_none() {
|
||||
return Some(FileHandle::new(i as u16));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: BlockDevice> FileSystem for Fat16<B> {
|
||||
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
|
||||
let (cluster, size) = self.find_in_root_dir(path)?;
|
||||
let handle = self.alloc_handle()?;
|
||||
*self.slot(handle)? = Some(OpenFile {
|
||||
first_cluster: cluster,
|
||||
size,
|
||||
offset: 0,
|
||||
name: FileName::from_bytes(path),
|
||||
});
|
||||
Some(handle)
|
||||
}
|
||||
|
||||
fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> Option<usize> {
|
||||
let file = (*self.slot(handle)?)?;
|
||||
if file.offset >= file.size {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
let remaining = file.size - file.offset;
|
||||
let to_read = buf.len().min(remaining);
|
||||
let cs = self.bpb.cluster_size();
|
||||
|
||||
let skip_clusters = file.offset / cs;
|
||||
let mut cluster = self.follow_chain(file.first_cluster, skip_clusters);
|
||||
let mut skip = file.offset % cs;
|
||||
let mut read = 0;
|
||||
let mut sector_buf = [0u8; SECTOR_SIZE];
|
||||
|
||||
while read < to_read {
|
||||
let sector_in_cluster = skip / SECTOR_SIZE;
|
||||
let byte_in_sector = skip % SECTOR_SIZE;
|
||||
let Some(base) = self.bpb.cluster_to_sector(cluster) else {
|
||||
break;
|
||||
};
|
||||
let sector = base + sector_in_cluster as u32;
|
||||
if !self
|
||||
.device
|
||||
.read_sector(SectorId::new(sector), &mut sector_buf)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
let chunk = (SECTOR_SIZE - byte_in_sector).min(to_read - read);
|
||||
buf[read..read + chunk]
|
||||
.copy_from_slice(§or_buf[byte_in_sector..byte_in_sector + chunk]);
|
||||
read += chunk;
|
||||
skip += chunk;
|
||||
|
||||
if skip >= cs {
|
||||
let next = self.read_fat_entry(cluster);
|
||||
if next >= 0xFFF8 {
|
||||
break;
|
||||
}
|
||||
cluster = next;
|
||||
skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Some(f)) = self.slot(handle) {
|
||||
f.offset += read;
|
||||
}
|
||||
Some(read)
|
||||
}
|
||||
|
||||
fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option<usize> {
|
||||
let mut file = (*self.slot(handle)?)?;
|
||||
let cs = self.bpb.cluster_size();
|
||||
|
||||
if file.first_cluster < 2 {
|
||||
let c = self.alloc_cluster()?;
|
||||
file.first_cluster = c;
|
||||
}
|
||||
|
||||
let skip_clusters = file.offset / cs;
|
||||
let mut cluster = self.follow_chain(file.first_cluster, skip_clusters);
|
||||
let mut pos = file.offset % cs;
|
||||
let mut written = 0;
|
||||
let mut sector_buf = [0u8; SECTOR_SIZE];
|
||||
|
||||
while written < buf.len() {
|
||||
let sector_in_cluster = pos / SECTOR_SIZE;
|
||||
let byte_in_sector = pos % SECTOR_SIZE;
|
||||
let Some(base) = self.bpb.cluster_to_sector(cluster) else {
|
||||
break;
|
||||
};
|
||||
let sector = base + sector_in_cluster as u32;
|
||||
if !self.device.read_sector(SectorId::new(sector), &mut sector_buf) {
|
||||
break;
|
||||
}
|
||||
|
||||
let chunk = (SECTOR_SIZE - byte_in_sector).min(buf.len() - written);
|
||||
sector_buf[byte_in_sector..byte_in_sector + chunk]
|
||||
.copy_from_slice(&buf[written..written + chunk]);
|
||||
// Bytes count as written only once they reached the disk.
|
||||
if !self.device.write_sector(SectorId::new(sector), §or_buf) {
|
||||
break;
|
||||
}
|
||||
|
||||
written += chunk;
|
||||
pos += chunk;
|
||||
|
||||
if pos >= cs {
|
||||
let next = self.read_fat_entry(cluster);
|
||||
if next >= 0xFFF8 {
|
||||
if written < buf.len() {
|
||||
if let Some(new_c) = self.alloc_cluster() {
|
||||
self.write_fat_entry(cluster, new_c);
|
||||
cluster = new_c;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cluster = next;
|
||||
}
|
||||
pos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
file.offset += written;
|
||||
if file.offset > file.size {
|
||||
file.size = file.offset;
|
||||
}
|
||||
*self.slot(handle)? = Some(file);
|
||||
Some(written)
|
||||
}
|
||||
|
||||
fn close(&mut self, handle: FileHandle) {
|
||||
let Some(file) = self.slot(handle).and_then(Option::take) else {
|
||||
return;
|
||||
};
|
||||
let mut name_buf = [0u8; 32];
|
||||
let name_bytes = file.name.as_bytes();
|
||||
let len = name_bytes.len().min(32);
|
||||
name_buf[..len].copy_from_slice(&name_bytes[..len]);
|
||||
self.update_dir_size(&name_buf[..len], file.size as u32, file.first_cluster);
|
||||
}
|
||||
|
||||
fn list_dir(&mut self, _path: &[u8], entries: &mut [DirEntry]) -> Option<usize> {
|
||||
let mut buf = [0u8; SECTOR_SIZE];
|
||||
let mut count = 0;
|
||||
for s in 0..self.bpb.root_dir_sectors() {
|
||||
if !self
|
||||
.device
|
||||
.read_sector(SectorId::new(self.bpb.root_dir_start() + s), &mut buf)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for i in 0..(SECTOR_SIZE / 32) {
|
||||
if count >= entries.len() {
|
||||
return Some(count);
|
||||
}
|
||||
let off = i * 32;
|
||||
if buf[off] == 0x00 {
|
||||
return Some(count);
|
||||
}
|
||||
if buf[off] == 0xE5 || buf[off + 11] & 0x08 != 0 {
|
||||
continue;
|
||||
}
|
||||
let size = u32::from_le_bytes([
|
||||
buf[off + 28],
|
||||
buf[off + 29],
|
||||
buf[off + 30],
|
||||
buf[off + 31],
|
||||
]);
|
||||
entries[count] = DirEntry {
|
||||
name: from_fat_name(&buf[off..off + 11]),
|
||||
size: size as usize,
|
||||
is_dir: buf[off + 11] & 0x10 != 0,
|
||||
};
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
Some(count)
|
||||
}
|
||||
|
||||
fn create(&mut self, path: &[u8]) -> Option<FileHandle> {
|
||||
if self.find_in_root_dir(path).is_some() {
|
||||
return None;
|
||||
}
|
||||
let cluster = self.alloc_cluster()?;
|
||||
if !self.create_dir_entry(path, cluster) {
|
||||
self.write_fat_entry(cluster, 0);
|
||||
return None;
|
||||
}
|
||||
let handle = self.alloc_handle()?;
|
||||
*self.slot(handle)? = Some(OpenFile {
|
||||
first_cluster: cluster,
|
||||
size: 0,
|
||||
offset: 0,
|
||||
name: FileName::from_bytes(path),
|
||||
});
|
||||
Some(handle)
|
||||
}
|
||||
|
||||
fn delete(&mut self, path: &[u8]) -> bool {
|
||||
if let Some(cluster) = self.delete_dir_entry(path) {
|
||||
self.free_chain(cluster);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn file_size(&mut self, handle: FileHandle) -> Option<usize> {
|
||||
self.slot(handle)?.as_ref().map(|f| f.size)
|
||||
}
|
||||
|
||||
fn mkdir(&mut self, _path: &[u8]) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn writable(&self) -> bool {
|
||||
self.device.writable()
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
pub mod ext4;
|
||||
pub mod fat16;
|
||||
|
||||
@@ -48,11 +48,10 @@ pub fn run() -> ! {
|
||||
libkos::println!("storage: no block device, exiting");
|
||||
syscall::exit();
|
||||
};
|
||||
let Some(fat16) = fs::fat16::Fat16::mount(device) else {
|
||||
libkos::println!("storage: FAT16 mount failed, exiting");
|
||||
let Some(ext4) = fs::ext4::Ext4::mount(device) else {
|
||||
libkos::println!("storage: no ext4 filesystem it can read, exiting");
|
||||
syscall::exit();
|
||||
};
|
||||
libkos::println!("storage: FAT16 mounted");
|
||||
let mut srv = server::StorageServer::new(fat16, RegionBuffers);
|
||||
let mut srv = server::StorageServer::new(ext4, RegionBuffers);
|
||||
server_framework::serve(&mut srv, ep)
|
||||
}
|
||||
|
||||
172
crates/servers/storage-server/src/tests/ext4_real.rs
Normal file
172
crates/servers/storage-server/src/tests/ext4_real.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//! The ext4 driver against images made by the real `mkfs.ext4`, checked by
|
||||
//! the real `e2fsck`. Skipped (with a note) where e2fsprogs is missing.
|
||||
|
||||
extern crate std;
|
||||
|
||||
use crate::filesystem::{DirEntry, FileSystem};
|
||||
use crate::fs::ext4::Ext4;
|
||||
use port_block_device::{BlockDevice, SectorId};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::vec::Vec;
|
||||
|
||||
/// The features the driver does not handle, switched off; see
|
||||
/// scripts/make-disk-image.sh, which formats the real disk the same way.
|
||||
pub const MKFS_FEATURES: &str = "^has_journal,^metadata_csum,^64bit,^dir_index,^huge_file,^orphan_file,^metadata_csum_seed";
|
||||
|
||||
struct FileDisk(Vec<u8>);
|
||||
|
||||
impl BlockDevice for FileDisk {
|
||||
fn sector_size(&self) -> usize {
|
||||
512
|
||||
}
|
||||
fn sector_count(&self) -> usize {
|
||||
self.0.len() / 512
|
||||
}
|
||||
fn read_sector(&mut self, s: SectorId, buf: &mut [u8]) -> bool {
|
||||
let at = s.raw() as usize * 512;
|
||||
match self.0.get(at..at + 512) {
|
||||
Some(src) => {
|
||||
buf[..512].copy_from_slice(src);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
fn write_sector(&mut self, s: SectorId, buf: &[u8]) -> bool {
|
||||
let at = s.raw() as usize * 512;
|
||||
match self.0.get_mut(at..at + 512) {
|
||||
Some(dst) => {
|
||||
dst.copy_from_slice(&buf[..512]);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scratch(name: &str) -> PathBuf {
|
||||
let dir = std::env::temp_dir().join(std::format!("kos-ext4-{}-{}", name, std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(dir.join("root/docs")).unwrap();
|
||||
dir
|
||||
}
|
||||
|
||||
/// An 8 MiB image of `root`, or `None` without mkfs.ext4.
|
||||
fn mkfs(dir: &Path, features: &str) -> Option<Vec<u8>> {
|
||||
let img = dir.join("disk.img");
|
||||
let ok = Command::new("mkfs.ext4")
|
||||
.args(["-q", "-F", "-b", "4096", "-O", features, "-d"])
|
||||
.arg(dir.join("root"))
|
||||
.arg(&img)
|
||||
.arg("8M")
|
||||
.status()
|
||||
.ok()?
|
||||
.success();
|
||||
assert!(ok, "mkfs.ext4 failed");
|
||||
Some(std::fs::read(img).unwrap())
|
||||
}
|
||||
|
||||
/// e2fsck's verdict on `image`: true when it finds nothing to fix.
|
||||
fn fsck_clean(dir: &Path, image: &[u8]) -> (bool, std::string::String) {
|
||||
let img = dir.join("after.img");
|
||||
std::fs::write(&img, image).unwrap();
|
||||
let out = Command::new("e2fsck").args(["-fn"]).arg(&img).output().unwrap();
|
||||
(out.status.success(), std::string::String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn setup(name: &str) -> Option<(PathBuf, Vec<u8>)> {
|
||||
let dir = scratch(name);
|
||||
std::fs::write(dir.join("root/hello.txt"), b"hello ext4\n").unwrap();
|
||||
let numbers: std::string::String = (1..=3000).map(|i| std::format!("{}\n", i)).collect();
|
||||
std::fs::write(dir.join("root/docs/numbers.txt"), numbers).unwrap();
|
||||
match mkfs(&dir, MKFS_FEATURES) {
|
||||
Some(img) => Some((dir, img)),
|
||||
None => {
|
||||
std::eprintln!("mkfs.ext4 not found; skipping");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn names(fs: &mut Ext4<FileDisk>, path: &[u8]) -> Vec<std::string::String> {
|
||||
let mut entries = [DirEntry::empty(); 32];
|
||||
let n = fs.list_dir(path, &mut entries).expect("list");
|
||||
entries[..n].iter().map(|e| std::string::String::from_utf8_lossy(e.name.as_bytes()).into_owned()).collect()
|
||||
}
|
||||
|
||||
fn read_all(fs: &mut Ext4<FileDisk>, path: &[u8]) -> Vec<u8> {
|
||||
let h = fs.open(path).expect("open");
|
||||
let mut out = Vec::new();
|
||||
let mut buf = [0u8; 1000];
|
||||
loop {
|
||||
let n = fs.read(h, &mut buf).expect("read");
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
out.extend_from_slice(&buf[..n]);
|
||||
}
|
||||
fs.close(h);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_what_mkfs_wrote() {
|
||||
let Some((_, img)) = setup("read") else { return };
|
||||
let mut fs = Ext4::mount(FileDisk(img)).expect("mount");
|
||||
let root = names(&mut fs, b"/");
|
||||
assert!(root.iter().any(|n| n == "hello.txt") && root.iter().any(|n| n == "docs"), "{root:?}");
|
||||
assert_eq!(read_all(&mut fs, b"/hello.txt"), b"hello ext4\n");
|
||||
let numbers = read_all(&mut fs, b"/docs/numbers.txt");
|
||||
assert_eq!(numbers.len(), (1..=3000).map(|i: u32| std::format!("{i}").len() + 1).sum::<usize>());
|
||||
assert!(numbers.ends_with(b"2999\n3000\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn what_it_writes_passes_e2fsck() {
|
||||
let Some((dir, img)) = setup("write") else { return };
|
||||
let mut fs = Ext4::mount(FileDisk(img)).expect("mount");
|
||||
assert!(fs.mkdir(b"/notes"));
|
||||
let h = fs.create(b"/notes/today.txt").expect("create");
|
||||
let text: Vec<u8> = (0..9000).map(|i| b'a' + (i % 26) as u8).collect();
|
||||
assert_eq!(fs.write(h, &text), Some(text.len()));
|
||||
fs.close(h);
|
||||
assert!(fs.delete(b"/hello.txt"));
|
||||
assert_eq!(read_all(&mut fs, b"/notes/today.txt"), text);
|
||||
let image = fs.into_device().0;
|
||||
let (clean, report) = fsck_clean(&dir, &image);
|
||||
assert!(clean, "e2fsck found problems:\n{report}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disk_with_features_it_cannot_handle_is_refused() {
|
||||
let dir = scratch("refuse");
|
||||
std::fs::write(dir.join("root/x"), b"x").unwrap();
|
||||
let Some(img) = mkfs(&dir, "metadata_csum,64bit") else { return };
|
||||
assert!(Ext4::mount(FileDisk(img)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_disk_it_could_read_but_not_keep_consistent_is_read_only() {
|
||||
let dir = scratch("readonly");
|
||||
std::fs::write(dir.join("root/x"), b"x").unwrap();
|
||||
// Checksums it would not update, and a journal.
|
||||
let Some(img) = mkfs(&dir, "^64bit,^metadata_csum_seed,metadata_csum,has_journal") else { return };
|
||||
let mut fs = Ext4::mount(FileDisk(img)).expect("readable");
|
||||
assert!(!fs.writable());
|
||||
assert_eq!(read_all(&mut fs, b"/x"), b"x");
|
||||
let Some((_, img)) = setup("writable") else { return };
|
||||
assert!(Ext4::mount(FileDisk(img)).unwrap().writable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_read_fills_the_buffer_across_blocks() {
|
||||
let Some((_, img)) = setup("bigread") else { return };
|
||||
let mut fs = Ext4::mount(FileDisk(img)).expect("mount");
|
||||
let h = fs.open(b"docs/numbers.txt").expect("open");
|
||||
let mut buf = std::vec![0u8; 10_000];
|
||||
assert_eq!(fs.read(h, &mut buf), Some(10_000));
|
||||
let total = fs.file_size(h).unwrap();
|
||||
assert_eq!(fs.read(h, &mut buf), Some(total - 10_000));
|
||||
assert_eq!(fs.read(h, &mut buf), Some(0));
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
use crate::adapters::mock_block::MockBlockDevice;
|
||||
use crate::filesystem::{DirEntry, FileSystem};
|
||||
use crate::fs::fat16::Fat16;
|
||||
use port_block_device::{BlockDevice, SectorId};
|
||||
|
||||
fn format_fat16_disk(sectors: usize) -> MockBlockDevice {
|
||||
let mut dev = MockBlockDevice::new(sectors);
|
||||
let mut boot = [0u8; 512];
|
||||
|
||||
// BPB
|
||||
boot[11] = 0x00;
|
||||
boot[12] = 0x02; // bytes_per_sector = 512
|
||||
boot[13] = 1; // sectors_per_cluster = 1
|
||||
boot[14] = 1;
|
||||
boot[15] = 0; // reserved_sectors = 1
|
||||
boot[16] = 1; // num_fats = 1
|
||||
boot[17] = 16;
|
||||
boot[18] = 0; // root_entry_count = 16
|
||||
boot[19] = sectors as u8; // total_sectors
|
||||
boot[20] = (sectors >> 8) as u8;
|
||||
boot[22] = 1;
|
||||
boot[23] = 0; // sectors_per_fat = 1
|
||||
|
||||
// boot signature
|
||||
boot[510] = 0x55;
|
||||
boot[511] = 0xAA;
|
||||
|
||||
dev.write_sector(SectorId::new(0), &boot);
|
||||
|
||||
// FAT sector (sector 1) — all zeros = all free
|
||||
let fat = [0u8; 512];
|
||||
dev.write_sector(SectorId::new(1), &fat);
|
||||
|
||||
dev
|
||||
}
|
||||
|
||||
fn mount_test_disk() -> Fat16<MockBlockDevice> {
|
||||
let dev = format_fat16_disk(32);
|
||||
Fat16::mount(dev).expect("mount failed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_valid_disk() {
|
||||
let dev = format_fat16_disk(32);
|
||||
assert!(Fat16::mount(dev).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_invalid_disk() {
|
||||
let dev = MockBlockDevice::new(32); // no boot signature
|
||||
assert!(Fat16::mount(dev).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_open_file() {
|
||||
let mut fs = mount_test_disk();
|
||||
let h = fs.create(b"hello.txt").expect("create");
|
||||
fs.close(h);
|
||||
|
||||
let h2 = fs.open(b"hello.txt").expect("open");
|
||||
assert_eq!(fs.file_size(h2), Some(0));
|
||||
fs.close(h2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_and_read_file() {
|
||||
let mut fs = mount_test_disk();
|
||||
let h = fs.create(b"data.bin").expect("create");
|
||||
|
||||
let data = b"Hello, FAT16!";
|
||||
let written = fs.write(h, data).expect("write");
|
||||
assert_eq!(written, data.len());
|
||||
|
||||
fs.close(h);
|
||||
|
||||
let h2 = fs.open(b"data.bin").expect("open");
|
||||
assert_eq!(fs.file_size(h2), Some(data.len()));
|
||||
|
||||
let mut buf = [0u8; 32];
|
||||
let read = fs.read(h2, &mut buf).expect("read");
|
||||
assert_eq!(read, data.len());
|
||||
assert_eq!(&buf[..read], data);
|
||||
fs.close(h2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_file() {
|
||||
let mut fs = mount_test_disk();
|
||||
fs.create(b"temp.txt").map(|h| fs.close(h));
|
||||
assert!(fs.delete(b"temp.txt"));
|
||||
assert!(fs.open(b"temp.txt").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_directory() {
|
||||
let mut fs = mount_test_disk();
|
||||
fs.create(b"a.txt").map(|h| fs.close(h));
|
||||
fs.create(b"b.txt").map(|h| fs.close(h));
|
||||
|
||||
let mut entries = [DirEntry::empty(); 16];
|
||||
let count = fs.list_dir(b"/", &mut entries).expect("list");
|
||||
assert_eq!(count, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_nonexistent_returns_none() {
|
||||
let mut fs = mount_test_disk();
|
||||
assert!(fs.open(b"nope.txt").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_duplicate_returns_none() {
|
||||
let mut fs = mount_test_disk();
|
||||
fs.create(b"dup.txt").map(|h| fs.close(h));
|
||||
assert!(fs.create(b"dup.txt").is_none());
|
||||
}
|
||||
|
||||
// ---- hostile input: handles from clients, geometry and chains from disk ----
|
||||
|
||||
fn with_boot_byte(sectors: usize, index: usize, value: u8) -> MockBlockDevice {
|
||||
let mut dev = format_fat16_disk(sectors);
|
||||
let mut boot = [0u8; 512];
|
||||
dev.read_sector(SectorId::new(0), &mut boot);
|
||||
boot[index] = value;
|
||||
dev.write_sector(SectorId::new(0), &boot);
|
||||
dev
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mount_rejects_geometry_the_driver_cannot_use() {
|
||||
// sectors_per_cluster 0 or 3, bytes_per_sector 513 or 0, no FATs, no
|
||||
// FAT sectors
|
||||
for (index, value) in [(13, 0), (13, 3), (11, 1), (12, 0), (16, 0), (22, 0)] {
|
||||
let dev = with_boot_byte(32, index, value);
|
||||
assert!(
|
||||
Fat16::mount(dev).is_none(),
|
||||
"byte {index} = {value} accepted"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_past_the_table_are_rejected_not_indexed() {
|
||||
use crate::filesystem::FileHandle;
|
||||
let mut fs = mount_test_disk();
|
||||
let mut buf = [0u8; 16];
|
||||
for raw in [8, 9, u16::MAX] {
|
||||
let h = FileHandle::new(raw);
|
||||
assert_eq!(fs.read(h, &mut buf), None);
|
||||
assert_eq!(fs.write(h, b"x"), None);
|
||||
assert_eq!(fs.file_size(h), None);
|
||||
fs.close(h);
|
||||
}
|
||||
}
|
||||
|
||||
/// Points a root-directory entry for `name` at `cluster` with `size`.
|
||||
fn plant_entry(dev: &mut MockBlockDevice, name: &[u8; 11], cluster: u16, size: u32) {
|
||||
// root dir starts after 1 reserved + 1 FAT sector
|
||||
let mut dir = [0u8; 512];
|
||||
dir[..11].copy_from_slice(name);
|
||||
dir[11] = 0x20;
|
||||
dir[26..28].copy_from_slice(&cluster.to_le_bytes());
|
||||
dir[28..32].copy_from_slice(&size.to_le_bytes());
|
||||
dev.write_sector(SectorId::new(2), &dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_pointing_at_cluster_zero_reads_nothing() {
|
||||
let mut dev = format_fat16_disk(32);
|
||||
plant_entry(&mut dev, b"BAD BIN", 0, 4096);
|
||||
let mut fs = Fat16::mount(dev).unwrap();
|
||||
let h = fs.open(b"bad.bin").expect("open");
|
||||
let mut buf = [0u8; 64];
|
||||
assert_eq!(fs.read(h, &mut buf), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_a_file_with_a_cyclic_chain_terminates() {
|
||||
let mut dev = format_fat16_disk(32);
|
||||
// cluster 2 -> 3 -> 2 -> ...
|
||||
let mut fat = [0u8; 512];
|
||||
fat[4..6].copy_from_slice(&3u16.to_le_bytes());
|
||||
fat[6..8].copy_from_slice(&2u16.to_le_bytes());
|
||||
dev.write_sector(SectorId::new(1), &fat);
|
||||
plant_entry(&mut dev, b"LOOP BIN", 2, 4096);
|
||||
let mut fs = Fat16::mount(dev).unwrap();
|
||||
assert!(fs.delete(b"loop.bin"));
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
mod ext4;
|
||||
mod fat16;
|
||||
mod ext4_real;
|
||||
mod protocol;
|
||||
mod server;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
use crate::budget::Limits;
|
||||
|
||||
pub const SPAWN: usize = 0;
|
||||
/// Program file names: FAT 8.3, with room to spare.
|
||||
/// The longest program file name a spawn request carries.
|
||||
pub const MAX_NAME: usize = 16;
|
||||
/// Where init puts the child's stack top (its stack ends at the image).
|
||||
pub const STACK_TOP: usize = 0x10000;
|
||||
|
||||
@@ -76,7 +76,10 @@ A thread made by `SpawnThread`/`SpawnThreadIn` starts with the caller's
|
||||
`arg` in `a0`. A process's first thread reads it as `types::start_arg`:
|
||||
the slots of the console and log capabilities it was handed.
|
||||
|
||||
Boot loads only init and storage (init needs it to read anything else):
|
||||
Boot loads only init and storage, both packed into the kernel image (they
|
||||
are what reads the disk, so they cannot come from it). The disk is one ext4
|
||||
filesystem; storage refuses one with features it does not understand, and
|
||||
mounts read-only one it could not keep consistent.
|
||||
init gets storage's process and endpoint in slots 1 and 2, before the
|
||||
board capabilities. Everything else init starts from its manifest
|
||||
(`init/src/manifest.rs`), handing each program only the endpoints, device
|
||||
|
||||
@@ -1,45 +1,43 @@
|
||||
#!/bin/bash
|
||||
# Builds the k-os disk: one ext4 filesystem holding every program init may
|
||||
# start. Init and storage themselves are packed into the kernel image.
|
||||
set -euo pipefail
|
||||
|
||||
IMG="${IMG:-boot.img}"
|
||||
IMG_SIZE_KB=4096
|
||||
SIZE="${SIZE:-16M}"
|
||||
TARGET="riscv64gc-unknown-none-elf"
|
||||
BUILD_DIR="target/${TARGET}/release"
|
||||
# What the storage server's ext4 driver does not maintain, switched off so it
|
||||
# can write to the disk (see storage-server's tests/ext4_real.rs).
|
||||
FEATURES="^has_journal,^metadata_csum,^64bit,^dir_index,^huge_file,^orphan_file,^metadata_csum_seed"
|
||||
|
||||
echo "=== Creating k-os boot disk image ==="
|
||||
ROOT="$(mktemp -d)"
|
||||
trap 'rm -rf "${ROOT}"' EXIT
|
||||
|
||||
dd if=/dev/zero of="${IMG}" bs=1K count=${IMG_SIZE_KB} 2>/dev/null
|
||||
mformat -i "${IMG}" -t 8 -h 2 -s 32 -r 16 -c 1 -M 512 ::
|
||||
echo "Created and formatted ${IMG} (${IMG_SIZE_KB}KB, FAT16)"
|
||||
|
||||
# Copy with 8.3 names (no long filenames, no tilde mangling)
|
||||
copy() {
|
||||
local src="$1" dst="$2"
|
||||
if [ -f "${BUILD_DIR}/${src}" ]; then
|
||||
mcopy -i "${IMG}" "${BUILD_DIR}/${src}" "::${dst}"
|
||||
echo " ${dst}"
|
||||
cp "${BUILD_DIR}/${src}" "${ROOT}/${dst}"
|
||||
fi
|
||||
}
|
||||
|
||||
copy init "init.elf"
|
||||
copy echo "echo.elf"
|
||||
copy snake "snake.elf"
|
||||
copy display-qemu "display.elf"
|
||||
copy input-qemu "input.elf"
|
||||
copy log-server "log.elf"
|
||||
copy permission-manager "permis.elf"
|
||||
copy storage-qemu "storage.elf"
|
||||
copy session-server "session.elf"
|
||||
copy terminal-server "terminal.elf"
|
||||
copy shell "shell.elf"
|
||||
# Only the test image: its presence makes init run the SMP self-test.
|
||||
# Only the test image: its presence makes init run the self-test.
|
||||
if [ -n "${SMPTEST:-}" ]; then
|
||||
copy smptest "smptest.elf"
|
||||
copy spawntest "spawntst.elf"
|
||||
copy hello "hello.elf"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Disk contents:"
|
||||
mdir -i "${IMG}" ::
|
||||
echo "=== Done: ${IMG} ==="
|
||||
rm -f "${IMG}"
|
||||
mkfs.ext4 -q -F -b 4096 -L k-os -O "${FEATURES}" -d "${ROOT}" "${IMG}" "${SIZE}"
|
||||
echo "=== ${IMG}: ext4, ${SIZE} ==="
|
||||
ls -1 "${ROOT}"
|
||||
|
||||
@@ -17,7 +17,19 @@ timeout ${QT:-120} qemu-system-riscv64 -machine virt -smp "${SMP}" -bios default
|
||||
status=$?
|
||||
|
||||
grep -a "smptest:\|spawntest:\|Hello\|SMPTEST" "${LOG}"
|
||||
if grep -aq "SMPTEST PASS" "${LOG}"; then
|
||||
# What k-os wrote must be ext4 that the real tools accept.
|
||||
disk_ok=1
|
||||
if command -v e2fsck >/dev/null && command -v debugfs >/dev/null; then
|
||||
if ! e2fsck -fn "${IMAGE}" >"${LOG}.fsck" 2>&1; then
|
||||
echo "test-qemu: e2fsck rejects the disk:"; cat "${LOG}.fsck"; disk_ok=0
|
||||
elif [ "$(debugfs -R 'cat /selftest.txt' "${IMAGE}" 2>/dev/null)" != "written by k-os on ext4" ]; then
|
||||
echo "test-qemu: /selftest.txt is not on the disk"; disk_ok=0
|
||||
else
|
||||
echo "test-qemu: e2fsck clean, file on disk"
|
||||
fi
|
||||
rm -f "${LOG}.fsck"
|
||||
fi
|
||||
if [ "${disk_ok}" -eq 1 ] && grep -aq "SMPTEST PASS" "${LOG}"; then
|
||||
echo "test-qemu: pass (${SMP} harts)"
|
||||
rm -f "${LOG}"
|
||||
exit 0
|
||||
|
||||
Reference in New Issue
Block a user