feat(storage): full file protocol over per-client shared region (badged clients, owned handles, clamped lengths, ReadOnly on unwritable disk); init loads via libkos::fs; drop boot shared buffer

This commit is contained in:
2026-10-27 12:42:37 +01:00
parent 694c675a40
commit 909b3f0503
19 changed files with 827 additions and 506 deletions

View File

@@ -24,6 +24,7 @@ impl FileEntry {
pub struct MockFileSystem {
files: [Option<FileEntry>; MAX_FILES],
count: usize,
read_only: bool,
}
impl MockFileSystem {
@@ -31,9 +32,16 @@ impl MockFileSystem {
Self {
files: [const { None }; MAX_FILES],
count: 0,
read_only: false,
}
}
/// The same file system, refusing all writes.
pub fn read_only(mut self) -> Self {
self.read_only = true;
self
}
fn find_by_name(&self, path: &[u8]) -> Option<usize> {
for i in 0..MAX_FILES {
if let Some(entry) = &self.files[i] {
@@ -47,6 +55,10 @@ impl MockFileSystem {
}
impl FileSystem for MockFileSystem {
fn writable(&self) -> bool {
!self.read_only
}
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
let idx = self.find_by_name(path)?;
if let Some(entry) = &mut self.files[idx] {

View File

@@ -74,4 +74,9 @@ pub trait FileSystem {
fn delete(&mut self, path: &[u8]) -> bool;
fn file_size(&mut self, handle: FileHandle) -> Option<usize>;
fn mkdir(&mut self, path: &[u8]) -> bool;
/// Whether anything can be written at all; the server refuses
/// writing requests up front when not.
fn writable(&self) -> bool {
true
}
}

View File

@@ -358,13 +358,17 @@ impl<B: BlockDevice> FileSystem for Fat16<B> {
break;
};
let sector = base + sector_in_cluster as u32;
self.device
.read_sector(SectorId::new(sector), &mut sector_buf);
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]);
self.device.write_sector(SectorId::new(sector), &sector_buf);
// Bytes count as written only once they reached the disk.
if !self.device.write_sector(SectorId::new(sector), &sector_buf) {
break;
}
written += chunk;
pos += chunk;
@@ -479,6 +483,10 @@ impl<B: BlockDevice> FileSystem for Fat16<B> {
fn mkdir(&mut self, _path: &[u8]) -> bool {
false
}
fn writable(&self) -> bool {
self.device.writable()
}
}
fn to_fat_name(name: &[u8]) -> [u8; 11] {

View File

@@ -13,19 +13,33 @@ pub mod server;
mod tests;
use libkos::syscall;
use types::address::VirtualAddress;
use types::bounded::{CapSlot, PageCount};
use types::mapping::PagePerms;
const CMD_READ_FILE: usize = 1;
const SHARED_BUF_VA: usize = 0x30000;
const SHARED_BUF_SIZE: usize = 64 * 1024;
/// Client buffers sit high in storage's window, clear of its heap.
const BUFFERS_VA: usize = 0x0100_0000;
fn shared_buf() -> &'static mut [u8] {
unsafe { core::slice::from_raw_parts_mut(SHARED_BUF_VA as *mut u8, SHARED_BUF_SIZE) }
/// Each client's buffer is a fresh region, mapped here and handed to the
/// client, which maps the same pages in its own space.
struct RegionBuffers;
impl server::Buffers for RegionBuffers {
fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option<CapSlot>)> {
let va = BUFFERS_VA + client * protocol::BUFFER_SIZE;
let region = syscall::create_region(PageCount::new(protocol::BUFFER_PAGES as u16)?).ok()?;
syscall::map_region(region, VirtualAddress::from_raw(va), PagePerms::ReadWrite).ok()?;
// SAFETY: the region is mapped read-write at `va` for as long as
// the server holds `region`, which it never drops.
let buf = unsafe { core::slice::from_raw_parts_mut(va as *mut u8, protocol::BUFFER_SIZE) };
Some((buf, Some(region)))
}
}
pub fn run() -> ! {
libkos::println!("storage: started");
let ep = types::bounded::CapSlot::new(0).unwrap();
let ep = CapSlot::new(0).unwrap();
// Init hands over the disk before asking for anything.
let Some(device) = libkos::device::receive_device(ep)
.mmio
@@ -34,72 +48,11 @@ pub fn run() -> ! {
libkos::println!("storage: no block device, exiting");
syscall::exit();
};
let fat16 = fs::fat16::Fat16::mount(device);
if fat16.is_none() {
let Some(fat16) = fs::fat16::Fat16::mount(device) else {
libkos::println!("storage: FAT16 mount failed, exiting");
syscall::exit();
}
let mut fat16 = fat16.unwrap();
libkos::println!("storage: FAT16 mounted");
let mut next = syscall::receive_msg(ep);
loop {
let message = match next {
Ok(m) => m,
Err(e) => {
libkos::println!("storage: endpoint unusable ({:?}), exiting", e);
syscall::exit();
}
};
let [w0, w1, _, _] = message.words();
let reply = match w0 {
CMD_READ_FILE => {
let filename_len = w1;
let buf = shared_buf();
let filename_len = filename_len.min(buf.len());
let mut name_buf = [0u8; 32];
let name_len = filename_len.min(32);
name_buf[..name_len].copy_from_slice(&buf[..name_len]);
let name = core::str::from_utf8(&name_buf[..name_len]).unwrap_or("?");
libkos::print!("storage: read {}...", name);
let file_size = read_entire_file(&mut fat16, &name_buf[..name_len], buf);
if file_size > 0 {
libkos::println!("ok");
} else {
libkos::println!("NOT FOUND");
}
[file_size, 0, 0, 0]
}
_ => [0, 0, 0, 0],
};
next = match message {
syscall::Received::Call { .. } => syscall::reply_recv(ep, reply, None),
_ => syscall::receive_msg(ep),
};
}
}
fn read_entire_file<B: port_block_device::BlockDevice>(
fat: &mut fs::fat16::Fat16<B>,
filename: &[u8],
out: &mut [u8],
) -> usize {
use filesystem::FileSystem;
let handle = match fat.open(filename) {
Some(h) => h,
None => return 0,
};
let size = fat.file_size(handle).unwrap_or(0);
let to_read = size.min(out.len());
let read = fat.read(handle, &mut out[..to_read]).unwrap_or(0);
fat.close(handle);
read
libkos::println!("storage: FAT16 mounted");
let mut srv = server::StorageServer::new(fat16, RegionBuffers);
server_framework::serve(&mut srv, ep)
}

View File

@@ -1,126 +1,129 @@
//! The storage protocol. A client first `Attach`es: the server makes it a
//! buffer region and replies with a capability to it, which the client
//! maps. Paths, file data and directory listings then travel through that
//! buffer; the message words carry only lengths and handles, and the
//! server clamps every length to the buffer. Clients are told apart by
//! their endpoint badge, and a handle only works for the client that
//! opened it.
use server_framework::{Decode, Encode};
use crate::filesystem::FileHandle;
pub use types::storage::{BUFFER_PAGES, BUFFER_SIZE, DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH};
use types::storage::{op, reply};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PackedPath {
word0: usize,
word1: usize,
}
impl PackedPath {
pub fn from_bytes(src: &[u8]) -> Self {
let mut w0 = [0u8; 8];
let mut w1 = [0u8; 8];
for (i, &b) in src.iter().enumerate() {
if i < 8 { w0[i] = b; }
else if i < 16 { w1[i - 8] = b; }
}
Self {
word0: usize::from_le_bytes(w0),
word1: usize::from_le_bytes(w1),
}
}
pub fn from_words(word0: usize, word1: usize) -> Self {
Self { word0, word1 }
}
pub fn to_bytes(&self) -> [u8; 16] {
let mut buf = [0u8; 16];
buf[..8].copy_from_slice(&self.word0.to_le_bytes());
buf[8..16].copy_from_slice(&self.word1.to_le_bytes());
buf
}
pub fn word0(&self) -> usize { self.word0 }
pub fn word1(&self) -> usize { self.word1 }
}
#[derive(Debug, PartialEq)]
pub enum StorageRequest {
Open { path: PackedPath },
Read { handle: FileHandle, max_bytes: usize },
Write { handle: FileHandle, data_word: usize },
Attach,
/// The path is the first `path_len` bytes of the buffer.
Open { path_len: usize },
/// Reads up to `len` bytes into the buffer.
Read { handle: FileHandle, len: usize },
/// Writes the first `len` bytes of the buffer.
Write { handle: FileHandle, len: usize },
Close { handle: FileHandle },
ListDir { path: PackedPath },
Create { path: PackedPath },
Delete { path: PackedPath },
FileSize { handle: FileHandle },
Mkdir { path: PackedPath },
/// Fills the buffer with `DIR_ENTRY_SIZE` records.
ListDir { path_len: usize },
Create { path_len: usize },
Delete { path_len: usize },
Mkdir { path_len: usize },
}
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorageResponse {
/// Carries the buffer capability; `size` bytes are usable.
Attached { size: usize },
Handle(FileHandle),
Data { bytes_read: usize },
Written { bytes_written: usize },
Data { bytes: usize },
Written { bytes: usize },
Ok,
DirEntries { count: usize },
Size(usize),
NotFound,
Error,
/// The disk cannot be written.
ReadOnly,
/// The client has not attached a buffer yet.
NotAttached,
}
fn handle(word: usize) -> Option<FileHandle> {
u16::try_from(word).ok().map(FileHandle::new)
}
impl Encode for StorageRequest {
fn encode(&self) -> [usize; 4] {
match self {
StorageRequest::Open { path } => [0, path.word0(), path.word1(), 0],
StorageRequest::Read { handle, max_bytes } => [1, handle.raw() as usize, *max_bytes, 0],
StorageRequest::Write { handle, data_word } => [2, handle.raw() as usize, *data_word, 0],
StorageRequest::Close { handle } => [3, handle.raw() as usize, 0, 0],
StorageRequest::ListDir { path } => [4, path.word0(), path.word1(), 0],
StorageRequest::Create { path } => [5, path.word0(), path.word1(), 0],
StorageRequest::Delete { path } => [6, path.word0(), path.word1(), 0],
StorageRequest::FileSize { handle } => [7, handle.raw() as usize, 0, 0],
StorageRequest::Mkdir { path } => [8, path.word0(), path.word1(), 0],
use StorageRequest::*;
match *self {
Attach => [op::ATTACH, 0, 0, 0],
Open { path_len } => [op::OPEN, path_len, 0, 0],
Read { handle, len } => [op::READ, handle.raw() as usize, len, 0],
Write { handle, len } => [op::WRITE, handle.raw() as usize, len, 0],
Close { handle } => [op::CLOSE, handle.raw() as usize, 0, 0],
FileSize { handle } => [op::FILE_SIZE, handle.raw() as usize, 0, 0],
ListDir { path_len } => [op::LIST_DIR, path_len, 0, 0],
Create { path_len } => [op::CREATE, path_len, 0, 0],
Delete { path_len } => [op::DELETE, path_len, 0, 0],
Mkdir { path_len } => [op::MKDIR, path_len, 0, 0],
}
}
}
impl Decode for StorageRequest {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(StorageRequest::Open { path: PackedPath::from_words(words[1], words[2]) }),
1 => Some(StorageRequest::Read { handle: FileHandle::new(words[1] as u16), max_bytes: words[2] }),
2 => Some(StorageRequest::Write { handle: FileHandle::new(words[1] as u16), data_word: words[2] }),
3 => Some(StorageRequest::Close { handle: FileHandle::new(words[1] as u16) }),
4 => Some(StorageRequest::ListDir { path: PackedPath::from_words(words[1], words[2]) }),
5 => Some(StorageRequest::Create { path: PackedPath::from_words(words[1], words[2]) }),
6 => Some(StorageRequest::Delete { path: PackedPath::from_words(words[1], words[2]) }),
7 => Some(StorageRequest::FileSize { handle: FileHandle::new(words[1] as u16) }),
8 => Some(StorageRequest::Mkdir { path: PackedPath::from_words(words[1], words[2]) }),
_ => None,
}
fn decode(w: [usize; 4]) -> Option<Self> {
use StorageRequest::*;
Some(match w[0] {
op::ATTACH => Attach,
op::OPEN => Open { path_len: w[1] },
op::READ => Read { handle: handle(w[1])?, len: w[2] },
op::WRITE => Write { handle: handle(w[1])?, len: w[2] },
op::CLOSE => Close { handle: handle(w[1])? },
op::FILE_SIZE => FileSize { handle: handle(w[1])? },
op::LIST_DIR => ListDir { path_len: w[1] },
op::CREATE => Create { path_len: w[1] },
op::DELETE => Delete { path_len: w[1] },
op::MKDIR => Mkdir { path_len: w[1] },
_ => return None,
})
}
}
impl Encode for StorageResponse {
fn encode(&self) -> [usize; 4] {
match self {
StorageResponse::Handle(h) => [0, h.raw() as usize, 0, 0],
StorageResponse::Data { bytes_read } => [1, *bytes_read, 0, 0],
StorageResponse::Written { bytes_written } => [2, *bytes_written, 0, 0],
StorageResponse::Ok => [3, 0, 0, 0],
StorageResponse::DirEntries { count } => [4, *count, 0, 0],
StorageResponse::Size(s) => [5, *s, 0, 0],
StorageResponse::NotFound => [6, 0, 0, 0],
StorageResponse::Error => [7, 0, 0, 0],
use StorageResponse::*;
match *self {
Attached { size } => [reply::ATTACHED, size, 0, 0],
Handle(h) => [reply::HANDLE, h.raw() as usize, 0, 0],
Data { bytes } => [reply::DATA, bytes, 0, 0],
Written { bytes } => [reply::WRITTEN, bytes, 0, 0],
Ok => [reply::OK, 0, 0, 0],
DirEntries { count } => [reply::DIR_ENTRIES, count, 0, 0],
Size(s) => [reply::SIZE, s, 0, 0],
NotFound => [reply::NOT_FOUND, 0, 0, 0],
Error => [reply::ERROR, 0, 0, 0],
ReadOnly => [reply::READ_ONLY, 0, 0, 0],
NotAttached => [reply::NOT_ATTACHED, 0, 0, 0],
}
}
}
impl Decode for StorageResponse {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(StorageResponse::Handle(FileHandle::new(words[1] as u16))),
1 => Some(StorageResponse::Data { bytes_read: words[1] }),
2 => Some(StorageResponse::Written { bytes_written: words[1] }),
3 => Some(StorageResponse::Ok),
4 => Some(StorageResponse::DirEntries { count: words[1] }),
5 => Some(StorageResponse::Size(words[1])),
6 => Some(StorageResponse::NotFound),
7 => Some(StorageResponse::Error),
_ => None,
}
fn decode(w: [usize; 4]) -> Option<Self> {
use StorageResponse::*;
Some(match w[0] {
reply::ATTACHED => Attached { size: w[1] },
reply::HANDLE => Handle(handle(w[1])?),
reply::DATA => Data { bytes: w[1] },
reply::WRITTEN => Written { bytes: w[1] },
reply::OK => Ok,
reply::DIR_ENTRIES => DirEntries { count: w[1] },
reply::SIZE => Size(w[1]),
reply::NOT_FOUND => NotFound,
reply::ERROR => Error,
reply::READ_ONLY => ReadOnly,
reply::NOT_ATTACHED => NotAttached,
_ => return None,
})
}
}

View File

@@ -1,96 +1,208 @@
use server_framework::Server;
use server_framework::{Decode, Encode, MALFORMED, Server};
use types::bounded::CapSlot;
use crate::filesystem::{FileSystem, DirEntry};
use crate::protocol::{StorageRequest, StorageResponse};
use crate::filesystem::{DirEntry, FileHandle, FileSystem};
use crate::protocol::{
DIR_ENTRY_SIZE, DIR_NAME_SIZE, MAX_PATH, StorageRequest, StorageResponse,
};
pub struct StorageServer<F: FileSystem> {
fs: F,
const MAX_CLIENTS: usize = 8;
/// Handles at or past this are never valid (the file systems hand out
/// fewer), so ownership fits a small table.
const MAX_HANDLES: usize = 16;
const MAX_LISTED: usize = 64;
/// Where client buffers come from: a fresh region per client in the
/// real server, plain memory in tests.
pub trait Buffers {
/// A buffer for a new client and the capability that lets the client
/// map it, if there is one to send.
fn create(&mut self, client: usize) -> Option<(&'static mut [u8], Option<CapSlot>)>;
}
impl<F: FileSystem> StorageServer<F> {
pub fn new(fs: F) -> Self {
Self { fs }
struct Client {
badge: usize,
buf: &'static mut [u8],
}
pub struct StorageServer<F: FileSystem, B: Buffers> {
fs: F,
buffers: B,
clients: [Option<Client>; MAX_CLIENTS],
/// The badge of the client that opened each handle.
owners: [Option<usize>; MAX_HANDLES],
}
impl<F: FileSystem, B: Buffers> StorageServer<F, B> {
pub fn new(fs: F, buffers: B) -> Self {
Self {
fs,
buffers,
clients: [const { None }; MAX_CLIENTS],
owners: [None; MAX_HANDLES],
}
}
pub fn filesystem(&self) -> &F {
&self.fs
}
#[cfg(test)]
pub fn client_buffer_for_test(&mut self, badge: usize) -> Option<&mut [u8]> {
self.client(badge).map(|c| &mut *c.buf)
}
fn client(&mut self, badge: usize) -> Option<&mut Client> {
self.clients.iter_mut().flatten().find(|c| c.badge == badge)
}
fn attach(&mut self, badge: usize) -> (StorageResponse, Option<CapSlot>) {
if self.client(badge).is_some() {
return (StorageResponse::Error, None);
}
let Some(index) = self.clients.iter().position(Option::is_none) else {
return (StorageResponse::Error, None);
};
let Some((buf, cap)) = self.buffers.create(index) else {
return (StorageResponse::Error, None);
};
let size = buf.len();
if let Some(slot) = self.clients.get_mut(index) {
*slot = Some(Client { badge, buf });
}
(StorageResponse::Attached { size }, cap)
}
fn owns(&self, badge: usize, handle: FileHandle) -> bool {
self.owners.get(handle.raw() as usize).copied().flatten() == Some(badge)
}
/// Serves one request from `badge`.
fn serve(&mut self, badge: usize, request: StorageRequest) -> (StorageResponse, Option<CapSlot>) {
use StorageRequest::*;
use StorageResponse as R;
if request == Attach {
return self.attach(badge);
}
let handle_ok = match request {
Read { handle, .. } | Write { handle, .. } | Close { handle } | FileSize { handle } => {
self.owns(badge, handle)
}
_ => true,
};
if !handle_ok {
return (R::Error, None);
}
let mutates = matches!(request, Write { .. } | Create { .. } | Delete { .. } | Mkdir { .. });
if mutates && !self.fs.writable() {
return (R::ReadOnly, None);
}
// Split borrows: the client's buffer, the file system, the owners.
let Self { fs, clients, owners, .. } = self;
let Some(client) = clients.iter_mut().flatten().find(|c| c.badge == badge) else {
return (R::NotAttached, None);
};
let buf = &mut *client.buf;
let room = buf.len();
let path = |len: usize| ..len.min(MAX_PATH).min(room);
let opened = |owners: &mut [Option<usize>; MAX_HANDLES], h: Option<FileHandle>| match h {
Some(h) => match owners.get_mut(h.raw() as usize) {
Some(owner) => {
*owner = Some(badge);
R::Handle(h)
}
// A handle the table cannot track is one we cannot police.
None => R::Error,
},
None => R::NotFound,
};
let response = match request {
Attach => R::Error,
Open { path_len } => {
let h = fs.open(&buf[path(path_len)]);
opened(owners, h)
}
Create { path_len } => match fs.create(&buf[path(path_len)]) {
None => R::Error,
h => opened(owners, h),
},
Read { handle, len } => match fs.read(handle, &mut buf[..len.min(room)]) {
Some(bytes) => R::Data { bytes },
None => R::Error,
},
Write { handle, len } => match fs.write(handle, &buf[..len.min(room)]) {
Some(bytes) => R::Written { bytes },
None => R::Error,
},
Close { handle } => {
fs.close(handle);
if let Some(owner) = owners.get_mut(handle.raw() as usize) {
*owner = None;
}
R::Ok
}
FileSize { handle } => match fs.file_size(handle) {
Some(size) => R::Size(size),
None => R::Error,
},
ListDir { path_len } => {
let mut name = [0u8; MAX_PATH];
let p = &buf[path(path_len)];
name[..p.len()].copy_from_slice(p);
let mut entries = [DirEntry::empty(); MAX_LISTED];
let room = (buf.len() / DIR_ENTRY_SIZE).min(MAX_LISTED);
match fs.list_dir(&name[..p.len()], &mut entries[..room]) {
Some(count) => {
for (entry, record) in entries.iter().take(count).zip(buf.chunks_mut(DIR_ENTRY_SIZE)) {
write_entry(entry, record);
}
R::DirEntries { count }
}
None => R::NotFound,
}
}
Delete { path_len } => {
if fs.delete(&buf[path(path_len)]) { R::Ok } else { R::NotFound }
}
Mkdir { path_len } => {
if fs.mkdir(&buf[path(path_len)]) { R::Ok } else { R::Error }
}
};
(response, None)
}
}
impl<F: FileSystem> Server for StorageServer<F> {
fn write_entry(entry: &DirEntry, record: &mut [u8]) {
record.fill(0);
let name = entry.name.as_bytes();
let n = name.len().min(DIR_NAME_SIZE);
record[..n].copy_from_slice(&name[..n]);
record[DIR_NAME_SIZE..DIR_NAME_SIZE + 8].copy_from_slice(&(entry.size as u64).to_le_bytes());
let flags = if entry.is_dir { types::storage::DIR_FLAG_DIRECTORY } else { 0 };
record[DIR_NAME_SIZE + 8..DIR_ENTRY_SIZE].copy_from_slice(&flags.to_le_bytes());
}
impl<F: FileSystem, B: Buffers> Server for StorageServer<F, B> {
type Request = StorageRequest;
type Response = StorageResponse;
/// Without a badge: as the unbadged client.
fn handle(&mut self, request: StorageRequest) -> StorageResponse {
match request {
StorageRequest::Open { path } => {
let buf = path.to_bytes();
let len = buf.iter().position(|&b| b == 0).unwrap_or(16);
match self.fs.open(&buf[..len]) {
Some(h) => StorageResponse::Handle(h),
None => StorageResponse::NotFound,
}
}
StorageRequest::Read { handle, max_bytes } => {
let mut buf = [0u8; 512];
let read_len = max_bytes.min(buf.len());
match self.fs.read(handle, &mut buf[..read_len]) {
Some(n) => StorageResponse::Data { bytes_read: n },
None => StorageResponse::Error,
}
}
StorageRequest::Write { handle, data_word } => {
let data = data_word.to_le_bytes();
match self.fs.write(handle, &data) {
Some(n) => StorageResponse::Written { bytes_written: n },
None => StorageResponse::Error,
}
}
StorageRequest::Close { handle } => {
self.fs.close(handle);
StorageResponse::Ok
}
StorageRequest::ListDir { path } => {
let buf = path.to_bytes();
let len = buf.iter().position(|&b| b == 0).unwrap_or(16);
let mut entries = [DirEntry::empty(); 16];
match self.fs.list_dir(&buf[..len], &mut entries) {
Some(count) => StorageResponse::DirEntries { count },
None => StorageResponse::NotFound,
}
}
StorageRequest::Create { path } => {
let buf = path.to_bytes();
let len = buf.iter().position(|&b| b == 0).unwrap_or(16);
match self.fs.create(&buf[..len]) {
Some(h) => StorageResponse::Handle(h),
None => StorageResponse::Error,
}
}
StorageRequest::Delete { path } => {
let buf = path.to_bytes();
let len = buf.iter().position(|&b| b == 0).unwrap_or(16);
if self.fs.delete(&buf[..len]) {
StorageResponse::Ok
} else {
StorageResponse::NotFound
}
}
StorageRequest::FileSize { handle } => {
match self.fs.file_size(handle) {
Some(size) => StorageResponse::Size(size),
None => StorageResponse::Error,
}
}
StorageRequest::Mkdir { path } => {
let buf = path.to_bytes();
let len = buf.iter().position(|&b| b == 0).unwrap_or(16);
if self.fs.mkdir(&buf[..len]) {
StorageResponse::Ok
} else {
StorageResponse::Error
}
self.serve(0, request).0
}
fn handle_call(
&mut self,
badge: usize,
words: [usize; 4],
_cap: Option<CapSlot>,
) -> ([usize; 4], Option<CapSlot>) {
match StorageRequest::decode(words) {
Some(request) => {
let (response, cap) = self.serve(badge, request);
(response.encode(), cap)
}
None => ([MALFORMED, 0, 0, 0], None),
}
}
}

View File

@@ -3,78 +3,58 @@ use crate::protocol::*;
use server_framework::{Decode, Encode};
#[test]
fn open_roundtrip() {
let path = PackedPath::from_bytes(b"test.txt");
let req = StorageRequest::Open { path };
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
fn every_request_roundtrips() {
let h = FileHandle::new(3);
for req in [
StorageRequest::Attach,
StorageRequest::Open { path_len: 9 },
StorageRequest::Read { handle: h, len: 100 },
StorageRequest::Write { handle: h, len: 7 },
StorageRequest::Close { handle: h },
StorageRequest::FileSize { handle: h },
StorageRequest::ListDir { path_len: 1 },
StorageRequest::Create { path_len: 5 },
StorageRequest::Delete { path_len: 5 },
StorageRequest::Mkdir { path_len: 4 },
] {
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
}
}
#[test]
fn read_roundtrip() {
let req = StorageRequest::Read { handle: FileHandle::new(3), max_bytes: 512 };
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
}
#[test]
fn write_roundtrip() {
let req = StorageRequest::Write { handle: FileHandle::new(1), data_word: 42 };
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
}
#[test]
fn close_roundtrip() {
let req = StorageRequest::Close { handle: FileHandle::new(5) };
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
}
#[test]
fn file_size_roundtrip() {
let req = StorageRequest::FileSize { handle: FileHandle::new(2) };
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
}
#[test]
fn handle_response_roundtrip() {
let resp = StorageResponse::Handle(FileHandle::new(7));
assert_eq!(StorageResponse::decode(resp.encode()), Some(resp));
}
#[test]
fn data_response_roundtrip() {
let resp = StorageResponse::Data { bytes_read: 256 };
assert_eq!(StorageResponse::decode(resp.encode()), Some(resp));
}
#[test]
fn all_response_variants() {
let variants = [
fn every_response_roundtrips() {
for resp in [
StorageResponse::Attached { size: BUFFER_SIZE },
StorageResponse::Handle(FileHandle::new(2)),
StorageResponse::Data { bytes: 512 },
StorageResponse::Written { bytes: 8 },
StorageResponse::Ok,
StorageResponse::DirEntries { count: 3 },
StorageResponse::Size(4096),
StorageResponse::NotFound,
StorageResponse::Error,
StorageResponse::Size(1024),
StorageResponse::DirEntries { count: 3 },
];
for resp in variants {
StorageResponse::ReadOnly,
StorageResponse::NotAttached,
] {
assert_eq!(StorageResponse::decode(resp.encode()), Some(resp));
}
}
#[test]
fn mkdir_roundtrip() {
let path = PackedPath::from_bytes(b"mydir");
let req = StorageRequest::Mkdir { path };
assert_eq!(StorageRequest::decode(req.encode()), Some(req));
fn handles_that_do_not_fit_are_rejected_not_truncated() {
assert_eq!(StorageRequest::decode([2, 0x1_0003, 10, 0]), None);
assert_eq!(StorageRequest::decode([4, usize::MAX, 0, 0]), None);
}
#[test]
fn invalid_request_returns_none() {
assert!(StorageRequest::decode([99, 0, 0, 0]).is_none());
fn unknown_opcodes_do_not_decode() {
assert_eq!(StorageRequest::decode([99, 0, 0, 0]), None);
assert_eq!(StorageRequest::decode([usize::MAX, 0, 0, 0]), None);
}
#[test]
fn packed_path_roundtrip() {
let path = PackedPath::from_bytes(b"hello");
let buf = path.to_bytes();
assert_eq!(&buf[..5], b"hello");
assert_eq!(buf[5], 0);
fn wire_numbers_are_the_shared_ones() {
use types::storage::{op, reply};
assert_eq!(StorageRequest::Read { handle: FileHandle::new(0), len: 0 }.encode()[0], op::READ);
assert_eq!(StorageResponse::ReadOnly.encode()[0], reply::READ_ONLY);
}

View File

@@ -1,87 +1,182 @@
extern crate std;
use crate::adapters::mock_fs::MockFileSystem;
use crate::filesystem::FileHandle;
use crate::protocol::*;
use crate::server::StorageServer;
use server_framework::Server;
use crate::server::{Buffers, StorageServer};
use server_framework::{Decode, Encode, MALFORMED, Server};
use std::boxed::Box;
use std::vec;
use types::bounded::CapSlot;
fn test_server() -> StorageServer<MockFileSystem> {
StorageServer::new(MockFileSystem::new())
/// Plain memory instead of regions; the "capability" is a fixed slot.
struct TestBuffers;
impl Buffers for TestBuffers {
fn create(&mut self, _client: usize) -> Option<(&'static mut [u8], Option<CapSlot>)> {
Some((Box::leak(vec![0u8; 4096].into_boxed_slice()), CapSlot::new(9)))
}
}
fn path(s: &[u8]) -> PackedPath {
PackedPath::from_bytes(s)
type Srv = StorageServer<MockFileSystem, TestBuffers>;
fn server() -> Srv {
StorageServer::new(MockFileSystem::new(), TestBuffers)
}
/// One request from `badge`, returning the decoded reply and any cap.
fn call(s: &mut Srv, badge: usize, req: StorageRequest) -> (StorageResponse, Option<CapSlot>) {
let (words, cap) = s.handle_call(badge, req.encode(), None);
(StorageResponse::decode(words).expect("valid reply"), cap)
}
struct Client {
badge: usize,
}
impl Client {
fn attach(s: &mut Srv, badge: usize) -> Self {
let (resp, cap) = call(s, badge, StorageRequest::Attach);
assert_eq!(resp, StorageResponse::Attached { size: 4096 });
assert_eq!(cap, CapSlot::new(9), "the reply carries the buffer");
Self { badge }
}
fn put(&self, s: &mut Srv, bytes: &[u8]) {
s.client_buffer_for_test(self.badge).unwrap()[..bytes.len()].copy_from_slice(bytes);
}
fn get(&self, s: &mut Srv, len: usize) -> std::vec::Vec<u8> {
s.client_buffer_for_test(self.badge).unwrap()[..len].to_vec()
}
fn create(&self, s: &mut Srv, path: &[u8]) -> FileHandle {
self.put(s, path);
match call(s, self.badge, StorageRequest::Create { path_len: path.len() }).0 {
StorageResponse::Handle(h) => h,
other => panic!("create: {other:?}"),
}
}
}
#[test]
fn create_and_open() {
let mut server = test_server();
let resp = server.handle(StorageRequest::Create { path: path(b"test.txt") });
assert!(matches!(resp, StorageResponse::Handle(_)));
let resp = server.handle(StorageRequest::Open { path: path(b"test.txt") });
assert!(matches!(resp, StorageResponse::Handle(_)));
fn nothing_works_before_attaching() {
let mut s = server();
let (resp, _) = call(&mut s, 0, StorageRequest::Open { path_len: 3 });
assert_eq!(resp, StorageResponse::NotAttached);
}
#[test]
fn open_nonexistent_returns_not_found() {
let mut server = test_server();
let resp = server.handle(StorageRequest::Open { path: path(b"nope.txt") });
assert_eq!(resp, StorageResponse::NotFound);
fn a_client_attaches_once() {
let mut s = server();
Client::attach(&mut s, 0);
assert_eq!(call(&mut s, 0, StorageRequest::Attach).0, StorageResponse::Error);
}
#[test]
fn write_then_read() {
let mut server = test_server();
fn data_goes_through_the_buffer_both_ways() {
let mut s = server();
let c = Client::attach(&mut s, 0);
let h = c.create(&mut s, b"note.txt");
c.put(&mut s, b"hello, storage");
assert_eq!(
call(&mut s, 0, StorageRequest::Write { handle: h, len: 14 }).0,
StorageResponse::Written { bytes: 14 }
);
assert_eq!(call(&mut s, 0, StorageRequest::Close { handle: h }).0, StorageResponse::Ok);
let handle = match server.handle(StorageRequest::Create { path: path(b"data") }) {
StorageResponse::Handle(h) => h,
_ => panic!("expected handle"),
c.put(&mut s, b"note.txt");
let StorageResponse::Handle(h) = call(&mut s, 0, StorageRequest::Open { path_len: 8 }).0 else {
panic!("open");
};
let resp = server.handle(StorageRequest::Write { handle, data_word: 0xDEADBEEF });
assert!(matches!(resp, StorageResponse::Written { bytes_written: 8 }));
let resp = server.handle(StorageRequest::FileSize { handle });
assert_eq!(resp, StorageResponse::Size(8));
assert_eq!(call(&mut s, 0, StorageRequest::FileSize { handle: h }).0, StorageResponse::Size(14));
c.put(&mut s, &[0; 32]);
assert_eq!(
call(&mut s, 0, StorageRequest::Read { handle: h, len: 64 }).0,
StorageResponse::Data { bytes: 14 }
);
assert_eq!(c.get(&mut s, 14), b"hello, storage");
}
#[test]
fn delete_file() {
let mut server = test_server();
server.handle(StorageRequest::Create { path: path(b"del.txt") });
let resp = server.handle(StorageRequest::Delete { path: path(b"del.txt") });
assert_eq!(resp, StorageResponse::Ok);
let resp = server.handle(StorageRequest::Open { path: path(b"del.txt") });
assert_eq!(resp, StorageResponse::NotFound);
fn a_handle_works_only_for_the_client_that_opened_it() {
let mut s = server();
let a = Client::attach(&mut s, 1);
Client::attach(&mut s, 2);
let h = a.create(&mut s, b"mine");
for req in [
StorageRequest::Read { handle: h, len: 4 },
StorageRequest::Write { handle: h, len: 4 },
StorageRequest::FileSize { handle: h },
StorageRequest::Close { handle: h },
] {
assert_eq!(call(&mut s, 2, req).0, StorageResponse::Error, "{req:?}");
}
assert_eq!(call(&mut s, 1, StorageRequest::FileSize { handle: h }).0, StorageResponse::Size(0));
}
#[test]
fn list_dir() {
let mut server = test_server();
server.handle(StorageRequest::Create { path: path(b"a.txt") });
server.handle(StorageRequest::Create { path: path(b"b.txt") });
let resp = server.handle(StorageRequest::ListDir { path: path(b"/") });
assert_eq!(resp, StorageResponse::DirEntries { count: 2 });
fn hostile_lengths_are_clamped_to_the_buffer() {
let mut s = server();
let c = Client::attach(&mut s, 0);
let h = c.create(&mut s, b"x");
for len in [4097, usize::MAX] {
assert!(matches!(call(&mut s, 0, StorageRequest::Write { handle: h, len }).0, StorageResponse::Written { .. }));
assert!(matches!(call(&mut s, 0, StorageRequest::Read { handle: h, len }).0, StorageResponse::Data { .. }));
assert_eq!(call(&mut s, 0, StorageRequest::Open { path_len: len }).0, StorageResponse::NotFound);
assert!(matches!(call(&mut s, 0, StorageRequest::ListDir { path_len: len }).0, StorageResponse::DirEntries { .. }));
}
}
#[test]
fn mkdir_returns_ok() {
let mut server = test_server();
let resp = server.handle(StorageRequest::Mkdir { path: path(b"subdir") });
assert_eq!(resp, StorageResponse::Ok);
fn handles_never_opened_are_refused() {
let mut s = server();
Client::attach(&mut s, 0);
for raw in [0, 15, 16, u16::MAX] {
let h = FileHandle::new(raw);
assert_eq!(call(&mut s, 0, StorageRequest::Read { handle: h, len: 1 }).0, StorageResponse::Error);
}
}
#[test]
fn close_succeeds() {
let mut server = test_server();
let handle = match server.handle(StorageRequest::Create { path: path(b"c.txt") }) {
StorageResponse::Handle(h) => h,
_ => panic!("expected handle"),
};
let resp = server.handle(StorageRequest::Close { handle });
assert_eq!(resp, StorageResponse::Ok);
fn a_read_only_disk_refuses_every_write_up_front() {
let mut s = StorageServer::new(MockFileSystem::new().read_only(), TestBuffers);
let c = Client::attach(&mut s, 0);
c.put(&mut s, b"f");
for req in [
StorageRequest::Create { path_len: 1 },
StorageRequest::Delete { path_len: 1 },
StorageRequest::Mkdir { path_len: 1 },
StorageRequest::Write { handle: FileHandle::new(0), len: 1 },
] {
let expect = match req {
// A handle nobody opened is refused before the disk is asked.
StorageRequest::Write { .. } => StorageResponse::Error,
_ => StorageResponse::ReadOnly,
};
assert_eq!(call(&mut s, 0, req).0, expect, "{req:?}");
}
}
#[test]
fn listings_are_records_in_the_buffer() {
let mut s = server();
let c = Client::attach(&mut s, 0);
for name in [b"a.txt".as_slice(), b"bee.bin"] {
let h = c.create(&mut s, name);
call(&mut s, 0, StorageRequest::Close { handle: h });
}
c.put(&mut s, b"/");
assert_eq!(call(&mut s, 0, StorageRequest::ListDir { path_len: 1 }).0, StorageResponse::DirEntries { count: 2 });
let records = c.get(&mut s, 2 * DIR_ENTRY_SIZE);
assert_eq!(&records[..5], b"a.txt");
assert_eq!(records[5], 0);
assert_eq!(&records[DIR_ENTRY_SIZE..DIR_ENTRY_SIZE + 7], b"bee.bin");
}
#[test]
fn garbage_gets_a_malformed_reply() {
let mut s = server();
let (words, cap) = s.handle_call(0, [99, 0, 0, 0], None);
assert_eq!((words[0], cap), (MALFORMED, None));
}