feat(storage-server): add StorageServer<F: FileSystem> with BlockDevice trait, mock adapters, and tests
This commit is contained in:
@@ -4,3 +4,5 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
types = { workspace = true }
|
||||||
|
server-framework = { workspace = true }
|
||||||
|
|||||||
46
crates/servers/storage-server/src/adapters/mock_block.rs
Normal file
46
crates/servers/storage-server/src/adapters/mock_block.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
127
crates/servers/storage-server/src/adapters/mock_fs.rs
Normal file
127
crates/servers/storage-server/src/adapters/mock_fs.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/servers/storage-server/src/adapters/mod.rs
Normal file
2
crates/servers/storage-server/src/adapters/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod mock_block;
|
||||||
|
pub mod mock_fs;
|
||||||
6
crates/servers/storage-server/src/block_device.rs
Normal file
6
crates/servers/storage-server/src/block_device.rs
Normal 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;
|
||||||
|
}
|
||||||
46
crates/servers/storage-server/src/filesystem.rs
Normal file
46
crates/servers/storage-server/src/filesystem.rs
Normal 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>;
|
||||||
|
}
|
||||||
3
crates/servers/storage-server/src/fs/mod.rs
Normal file
3
crates/servers/storage-server/src/fs/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Filesystem implementations go here.
|
||||||
|
// Each implements FileSystem using a BlockDevice.
|
||||||
|
// e.g., fat16.rs: pub struct Fat16<B: BlockDevice> { ... }
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
88
crates/servers/storage-server/src/protocol.rs
Normal file
88
crates/servers/storage-server/src/protocol.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
crates/servers/storage-server/src/server.rs
Normal file
100
crates/servers/storage-server/src/server.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/servers/storage-server/src/tests/mod.rs
Normal file
2
crates/servers/storage-server/src/tests/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod protocol;
|
||||||
|
mod server;
|
||||||
64
crates/servers/storage-server/src/tests/protocol.rs
Normal file
64
crates/servers/storage-server/src/tests/protocol.rs
Normal 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());
|
||||||
|
}
|
||||||
95
crates/servers/storage-server/src/tests/server.rs
Normal file
95
crates/servers/storage-server/src/tests/server.rs
Normal 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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user