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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user