feat(storage-server): add StorageServer<F: FileSystem> with BlockDevice trait, mock adapters, and tests

This commit is contained in:
2026-10-18 03:37:40 +02:00
parent b7bc223a79
commit 7a4600b3a2
13 changed files with 591 additions and 12 deletions

View File

@@ -4,3 +4,5 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
types = { workspace = true }
server-framework = { workspace = true }

View File

@@ -0,0 +1,46 @@
use crate::block_device::BlockDevice;
const SECTOR_SIZE: usize = 512;
const MAX_SECTORS: usize = 64;
pub struct MockBlockDevice {
data: [[u8; SECTOR_SIZE]; MAX_SECTORS],
sectors: usize,
}
impl MockBlockDevice {
pub fn new(sectors: usize) -> Self {
Self {
data: [[0; SECTOR_SIZE]; MAX_SECTORS],
sectors: sectors.min(MAX_SECTORS),
}
}
}
impl BlockDevice for MockBlockDevice {
fn sector_size(&self) -> usize {
SECTOR_SIZE
}
fn sector_count(&self) -> usize {
self.sectors
}
fn read_sector(&mut self, sector: u32, buf: &mut [u8]) -> bool {
let idx = sector as usize;
if idx >= self.sectors || buf.len() < SECTOR_SIZE {
return false;
}
buf[..SECTOR_SIZE].copy_from_slice(&self.data[idx]);
true
}
fn write_sector(&mut self, sector: u32, buf: &[u8]) -> bool {
let idx = sector as usize;
if idx >= self.sectors || buf.len() < SECTOR_SIZE {
return false;
}
self.data[idx].copy_from_slice(&buf[..SECTOR_SIZE]);
true
}
}

View File

@@ -0,0 +1,127 @@
use crate::filesystem::{DirEntry, FileHandle, FileSystem};
const MAX_FILES: usize = 16;
const MAX_FILE_SIZE: usize = 256;
struct FileEntry {
name: [u8; 32],
name_len: u8,
data: [u8; MAX_FILE_SIZE],
size: usize,
open: bool,
}
impl FileEntry {
fn empty() -> Self {
Self {
name: [0; 32],
name_len: 0,
data: [0; MAX_FILE_SIZE],
size: 0,
open: false,
}
}
}
pub struct MockFileSystem {
files: [Option<FileEntry>; MAX_FILES],
count: usize,
}
impl MockFileSystem {
pub fn new() -> Self {
Self {
files: [const { None }; MAX_FILES],
count: 0,
}
}
fn find_by_name(&self, path: &[u8]) -> Option<usize> {
for i in 0..self.count {
if let Some(entry) = &self.files[i] {
if &entry.name[..entry.name_len as usize] == path {
return Some(i);
}
}
}
None
}
}
impl FileSystem for MockFileSystem {
fn open(&mut self, path: &[u8]) -> Option<FileHandle> {
let idx = self.find_by_name(path)?;
if let Some(entry) = &mut self.files[idx] {
entry.open = true;
Some(FileHandle::new(idx as u16))
} else {
None
}
}
fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> Option<usize> {
let entry = self.files[handle.raw() as usize].as_ref()?;
let len = buf.len().min(entry.size);
buf[..len].copy_from_slice(&entry.data[..len]);
Some(len)
}
fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option<usize> {
let entry = self.files[handle.raw() as usize].as_mut()?;
let len = buf.len().min(MAX_FILE_SIZE - entry.size);
entry.data[entry.size..entry.size + len].copy_from_slice(&buf[..len]);
entry.size += len;
Some(len)
}
fn close(&mut self, handle: FileHandle) {
if let Some(entry) = &mut self.files[handle.raw() as usize] {
entry.open = false;
}
}
fn list_dir(&mut self, _path: &[u8], entries: &mut [DirEntry]) -> Option<usize> {
let mut count = 0;
for i in 0..self.count {
if count >= entries.len() { break; }
if let Some(file) = &self.files[i] {
let mut entry = DirEntry::empty();
let len = file.name_len as usize;
entry.name[..len].copy_from_slice(&file.name[..len]);
entry.name_len = file.name_len;
entry.size = file.size as u32;
entry.is_dir = false;
entries[count] = entry;
count += 1;
}
}
Some(count)
}
fn create(&mut self, path: &[u8]) -> Option<FileHandle> {
if self.count >= MAX_FILES || path.len() > 32 {
return None;
}
let idx = self.count;
let mut entry = FileEntry::empty();
entry.name[..path.len()].copy_from_slice(path);
entry.name_len = path.len() as u8;
entry.open = true;
self.files[idx] = Some(entry);
self.count += 1;
Some(FileHandle::new(idx as u16))
}
fn delete(&mut self, path: &[u8]) -> bool {
if let Some(idx) = self.find_by_name(path) {
self.files[idx] = None;
true
} else {
false
}
}
fn file_size(&mut self, handle: FileHandle) -> Option<usize> {
self.files[handle.raw() as usize].as_ref().map(|e| e.size)
}
}

View File

@@ -0,0 +1,2 @@
pub mod mock_block;
pub mod mock_fs;

View File

@@ -0,0 +1,6 @@
pub trait BlockDevice {
fn sector_size(&self) -> usize;
fn sector_count(&self) -> usize;
fn read_sector(&mut self, sector: u32, buf: &mut [u8]) -> bool;
fn write_sector(&mut self, sector: u32, buf: &[u8]) -> bool;
}

View File

@@ -0,0 +1,46 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileHandle(u16);
impl FileHandle {
pub const fn new(id: u16) -> Self {
Self(id)
}
pub const fn raw(&self) -> u16 {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct DirEntry {
pub name: [u8; 32],
pub name_len: u8,
pub size: u32,
pub is_dir: bool,
}
impl DirEntry {
pub fn empty() -> Self {
Self {
name: [0; 32],
name_len: 0,
size: 0,
is_dir: false,
}
}
pub fn name_bytes(&self) -> &[u8] {
&self.name[..self.name_len as usize]
}
}
pub trait FileSystem {
fn open(&mut self, path: &[u8]) -> Option<FileHandle>;
fn read(&mut self, handle: FileHandle, buf: &mut [u8]) -> Option<usize>;
fn write(&mut self, handle: FileHandle, buf: &[u8]) -> Option<usize>;
fn close(&mut self, handle: FileHandle);
fn list_dir(&mut self, path: &[u8], entries: &mut [DirEntry]) -> Option<usize>;
fn create(&mut self, path: &[u8]) -> Option<FileHandle>;
fn delete(&mut self, path: &[u8]) -> bool;
fn file_size(&mut self, handle: FileHandle) -> Option<usize>;
}

View File

@@ -0,0 +1,3 @@
// Filesystem implementations go here.
// Each implements FileSystem using a BlockDevice.
// e.g., fat16.rs: pub struct Fat16<B: BlockDevice> { ... }

View File

@@ -1,14 +1,12 @@
pub fn add(left: u64, right: u64) -> u64 { #![no_std]
left + right
} pub mod adapters;
pub mod block_device;
pub mod filesystem;
pub mod fs;
pub mod protocol;
pub mod server;
#[cfg(test)] #[cfg(test)]
mod tests { #[path = "tests/mod.rs"]
use super::*; mod tests;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

View File

@@ -0,0 +1,88 @@
use server_framework::{Decode, Encode};
use crate::filesystem::FileHandle;
#[derive(Debug, PartialEq)]
pub enum StorageRequest {
Open { path_word0: usize, path_word1: usize },
Read { handle: FileHandle, max_bytes: usize },
Write { handle: FileHandle, data_word: usize },
Close { handle: FileHandle },
ListDir { path_word0: usize, path_word1: usize },
Create { path_word0: usize, path_word1: usize },
Delete { path_word0: usize, path_word1: usize },
FileSize { handle: FileHandle },
}
#[derive(Debug, PartialEq)]
pub enum StorageResponse {
Handle(FileHandle),
Data { bytes_read: usize },
Written { bytes_written: usize },
Ok,
DirEntries { count: usize },
Size(usize),
NotFound,
Error,
}
impl Encode for StorageRequest {
fn encode(&self) -> [usize; 4] {
match self {
StorageRequest::Open { path_word0, path_word1 } => [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_word0, path_word1 } => [4, *path_word0, *path_word1, 0],
StorageRequest::Create { path_word0, path_word1 } => [5, *path_word0, *path_word1, 0],
StorageRequest::Delete { path_word0, path_word1 } => [6, *path_word0, *path_word1, 0],
StorageRequest::FileSize { handle } => [7, handle.raw() as usize, 0, 0],
}
}
}
impl Decode for StorageRequest {
fn decode(words: [usize; 4]) -> Option<Self> {
match words[0] {
0 => Some(StorageRequest::Open { path_word0: words[1], path_word1: 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_word0: words[1], path_word1: words[2] }),
5 => Some(StorageRequest::Create { path_word0: words[1], path_word1: words[2] }),
6 => Some(StorageRequest::Delete { path_word0: words[1], path_word1: words[2] }),
7 => Some(StorageRequest::FileSize { handle: FileHandle::new(words[1] as u16) }),
_ => 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],
}
}
}
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,
}
}
}

View File

@@ -0,0 +1,100 @@
use server_framework::Server;
use crate::filesystem::{FileHandle, FileSystem};
use crate::protocol::{StorageRequest, StorageResponse};
pub struct StorageServer<F: FileSystem> {
fs: F,
}
impl<F: FileSystem> StorageServer<F> {
pub fn new(fs: F) -> Self {
Self { fs }
}
pub fn filesystem(&self) -> &F {
&self.fs
}
fn path_from_words(w0: usize, w1: usize) -> [u8; 16] {
let mut path = [0u8; 16];
let b0 = w0.to_le_bytes();
let b1 = w1.to_le_bytes();
path[..8].copy_from_slice(&b0);
path[8..16].copy_from_slice(&b1);
path
}
fn path_len(path: &[u8]) -> usize {
path.iter().position(|&b| b == 0).unwrap_or(path.len())
}
}
impl<F: FileSystem> Server for StorageServer<F> {
type Request = StorageRequest;
type Response = StorageResponse;
fn handle(&mut self, request: StorageRequest) -> StorageResponse {
match request {
StorageRequest::Open { path_word0, path_word1 } => {
let path = Self::path_from_words(path_word0, path_word1);
let len = Self::path_len(&path);
match self.fs.open(&path[..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_word0, path_word1 } => {
let path = Self::path_from_words(path_word0, path_word1);
let len = Self::path_len(&path);
let mut entries = [crate::filesystem::DirEntry::empty(); 16];
match self.fs.list_dir(&path[..len], &mut entries) {
Some(count) => StorageResponse::DirEntries { count },
None => StorageResponse::NotFound,
}
}
StorageRequest::Create { path_word0, path_word1 } => {
let path = Self::path_from_words(path_word0, path_word1);
let len = Self::path_len(&path);
match self.fs.create(&path[..len]) {
Some(h) => StorageResponse::Handle(h),
None => StorageResponse::Error,
}
}
StorageRequest::Delete { path_word0, path_word1 } => {
let path = Self::path_from_words(path_word0, path_word1);
let len = Self::path_len(&path);
if self.fs.delete(&path[..len]) {
StorageResponse::Ok
} else {
StorageResponse::NotFound
}
}
StorageRequest::FileSize { handle } => {
match self.fs.file_size(handle) {
Some(size) => StorageResponse::Size(size),
None => StorageResponse::Error,
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
mod protocol;
mod server;

View File

@@ -0,0 +1,64 @@
use crate::filesystem::FileHandle;
use crate::protocol::*;
use server_framework::{Decode, Encode};
#[test]
fn open_roundtrip() {
let req = StorageRequest::Open { path_word0: 0x61626364, path_word1: 0 };
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 = [
StorageResponse::Ok,
StorageResponse::NotFound,
StorageResponse::Error,
StorageResponse::Size(1024),
StorageResponse::DirEntries { count: 3 },
];
for resp in variants {
assert_eq!(StorageResponse::decode(resp.encode()), Some(resp));
}
}
#[test]
fn invalid_request_returns_none() {
assert!(StorageRequest::decode([99, 0, 0, 0]).is_none());
}

View File

@@ -0,0 +1,95 @@
use crate::adapters::mock_fs::MockFileSystem;
use crate::filesystem::FileHandle;
use crate::protocol::*;
use crate::server::StorageServer;
use server_framework::Server;
fn test_server() -> StorageServer<MockFileSystem> {
StorageServer::new(MockFileSystem::new())
}
fn path_words(s: &[u8]) -> (usize, usize) {
let mut w0 = [0u8; 8];
let mut w1 = [0u8; 8];
for (i, &b) in s.iter().enumerate() {
if i < 8 { w0[i] = b; }
else if i < 16 { w1[i - 8] = b; }
}
(usize::from_le_bytes(w0), usize::from_le_bytes(w1))
}
#[test]
fn create_and_open() {
let mut server = test_server();
let (w0, w1) = path_words(b"test.txt");
let resp = server.handle(StorageRequest::Create { path_word0: w0, path_word1: w1 });
assert!(matches!(resp, StorageResponse::Handle(_)));
let resp = server.handle(StorageRequest::Open { path_word0: w0, path_word1: w1 });
assert!(matches!(resp, StorageResponse::Handle(_)));
}
#[test]
fn open_nonexistent_returns_not_found() {
let mut server = test_server();
let (w0, w1) = path_words(b"nope.txt");
let resp = server.handle(StorageRequest::Open { path_word0: w0, path_word1: w1 });
assert_eq!(resp, StorageResponse::NotFound);
}
#[test]
fn write_then_read() {
let mut server = test_server();
let (w0, w1) = path_words(b"data");
let handle = match server.handle(StorageRequest::Create { path_word0: w0, path_word1: w1 }) {
StorageResponse::Handle(h) => h,
_ => panic!("expected handle"),
};
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));
}
#[test]
fn delete_file() {
let mut server = test_server();
let (w0, w1) = path_words(b"del.txt");
server.handle(StorageRequest::Create { path_word0: w0, path_word1: w1 });
let resp = server.handle(StorageRequest::Delete { path_word0: w0, path_word1: w1 });
assert_eq!(resp, StorageResponse::Ok);
let resp = server.handle(StorageRequest::Open { path_word0: w0, path_word1: w1 });
assert_eq!(resp, StorageResponse::NotFound);
}
#[test]
fn list_dir() {
let mut server = test_server();
let (w0a, w1a) = path_words(b"a.txt");
let (w0b, w1b) = path_words(b"b.txt");
server.handle(StorageRequest::Create { path_word0: w0a, path_word1: w1a });
server.handle(StorageRequest::Create { path_word0: w0b, path_word1: w1b });
let (w0r, w1r) = path_words(b"/");
let resp = server.handle(StorageRequest::ListDir { path_word0: w0r, path_word1: w1r });
assert_eq!(resp, StorageResponse::DirEntries { count: 2 });
}
#[test]
fn close_succeeds() {
let mut server = test_server();
let (w0, w1) = path_words(b"c.txt");
let handle = match server.handle(StorageRequest::Create { path_word0: w0, path_word1: w1 }) {
StorageResponse::Handle(h) => h,
_ => panic!("expected handle"),
};
let resp = server.handle(StorageRequest::Close { handle });
assert_eq!(resp, StorageResponse::Ok);
}