Storage
For now only a key-value store is supported for persistent storage. Eventually, additional facilities may be added: a cyclic logging journal, a file-system, raw flash access, etc.
In this section, we will illustrate the key-value store usage by walking through the store
example
which provides direct store access with a text-based interface through the platform protocol applet
RPC mechanism.
The key-value store has 2 restrictions:
- Keys must be between 0 and 4095
- Values must be at most 1023 bytes
The second restriction can be worked around by storing large entries as multiple fragments of at most 1023 bytes each using multiple keys. To support those large entries, we define an abstract notion of keys. An abstract key is either exactly one key, or a contiguous range of keys.
enum Key {
Exact(usize),
Range(Range<usize>),
}
The store provides 3 operations: insert
, find
, and remove
. Each of those operation comes with
a fragmented variant (working on a range of keys for large values). We define helpers to dispatch to
the regular or fragmented version based on the abstract key.
fn insert(key: &Key, value: &[u8]) -> Result<(), Error> {
match key {
Key::Exact(key) => store::insert(*key, value),
Key::Range(keys) => store::fragment::insert(keys.clone(), value),
}
}
fn find(key: &Key) -> Result<Option<Box<[u8]>>, Error> {
match key {
Key::Exact(key) => store::find(*key),
Key::Range(keys) => store::fragment::find(keys.clone()),
}
}
fn remove(key: &Key) -> Result<(), Error> {
match key {
Key::Exact(key) => store::remove(*key),
Key::Range(keys) => store::fragment::remove(keys.clone()),
}
}
To ease parsing and processing of RPC requests, we define a straightforward type for commands.
enum Command<'a> {
Help,
Insert { key: Key, value: &'a str },
Find { key: Key },
Remove { key: Key },
}
The parsing function is also straightforward.
impl<'a> Command<'a> {
fn parse(input: &'a str) -> Result<Self, String> {
Ok(match *input.split_whitespace().collect::<Vec<_>>().as_slice() {
[] | ["help"] => Command::Help,
["insert", key, value] => Command::Insert { key: Key::parse(key)?, value },
["find", key] => Command::Find { key: Key::parse(key)? },
["remove", key] => Command::Remove { key: Key::parse(key)? },
[command, ..] => return Err(format!("Invalid command {command:?}")),
})
}
The process function is more interesting as we'll describe how the store operations behave. When processing a command, we may either succeed with an output, or fail with an error. We use strings because we implement a text-based interface.
fn process(&self) -> Result<String, String> {
For the help command, we simply output the grammar for commands.
match self {
Command::Help => Ok("\
Usage: insert <key>[..<key>] <value>
Usage: find <key>[..<key>]
Usage: remove <key>[..<key>]"
.to_string()),
For insert commands, we use our helper function for abstract keys and format the output and error appropriately. When inserting to the store, if the key (or range of keys) is already present, then its value is overwritten.
Command::Insert { key, value } => match insert(key, value.as_bytes()) {
Ok(()) => Ok("Done".to_string()),
Err(error) => Err(format!("{error}")),
},
Remove commands are similar. When removing from the store, it is not an error if the key (or range of keys) is absent.
Command::Remove { key } => match remove(key) {
Ok(()) => Ok("Done".to_string()),
Err(error) => Err(format!("{error}")),
},
Find commands also use the helper function, however they have 2 possible outputs (outside errors):
- If the key is absent from the store, then
None
is returned. - If the key is present, then
Some
is returned with the bytes of the value. Because we implement a text-based interface, we try to convert the byte slice to a string slice for the output.
Command::Find { key } => match find(key) {
Ok(None) => Ok("Not found".to_string()),
Ok(Some(value)) => match core::str::from_utf8(&value) {
Ok(value) => Ok(format!("Found: {value}")),
Err(_) => Ok(format!("Found (not UTF-8): {value:02x?}")),
},
Err(error) => Err(format!("{error}")),
},
We can finally write our handler function taking a request as argument and returning a response.
fn handler(request: Vec<u8>) -> Vec<u8> {
// Parse and process the request.
let result: Result<String, String> = try {
let request = String::from_utf8(request).map_err(|_| "Request is not UTF-8")?;
Command::parse(&request)?.process()?
};
// Format output including error and next prompt.
let mut output = result.unwrap_or_else(|error| format!("Error: {error}"));
output.push_str("\n> ");
output.into_bytes()
}
The main function simply registers an RPC listener with the handler above.
fn main() {
rpc::Listener::new(&platform::protocol::RpcProtocol, handler).leak();
}
The final code looks like this:
#![no_std] #![feature(try_blocks)] wasefire::applet!(); use alloc::boxed::Box; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::num::ParseIntError; use core::ops::Range; use core::str::FromStr; fn main() { rpc::Listener::new(&platform::protocol::RpcProtocol, handler).leak(); } fn handler(request: Vec<u8>) -> Vec<u8> { // Parse and process the request. let result: Result<String, String> = try { let request = String::from_utf8(request).map_err(|_| "Request is not UTF-8")?; Command::parse(&request)?.process()? }; // Format output including error and next prompt. let mut output = result.unwrap_or_else(|error| format!("Error: {error}")); output.push_str("\n> "); output.into_bytes() } enum Command<'a> { Help, Insert { key: Key, value: &'a str }, Find { key: Key }, Remove { key: Key }, } impl<'a> Command<'a> { fn parse(input: &'a str) -> Result<Self, String> { Ok(match *input.split_whitespace().collect::<Vec<_>>().as_slice() { [] | ["help"] => Command::Help, ["insert", key, value] => Command::Insert { key: Key::parse(key)?, value }, ["find", key] => Command::Find { key: Key::parse(key)? }, ["remove", key] => Command::Remove { key: Key::parse(key)? }, [command, ..] => return Err(format!("Invalid command {command:?}")), }) } fn process(&self) -> Result<String, String> { match self { Command::Help => Ok("\ Usage: insert <key>[..<key>] <value> Usage: find <key>[..<key>] Usage: remove <key>[..<key>]" .to_string()), Command::Insert { key, value } => match insert(key, value.as_bytes()) { Ok(()) => Ok("Done".to_string()), Err(error) => Err(format!("{error}")), }, Command::Find { key } => match find(key) { Ok(None) => Ok("Not found".to_string()), Ok(Some(value)) => match core::str::from_utf8(&value) { Ok(value) => Ok(format!("Found: {value}")), Err(_) => Ok(format!("Found (not UTF-8): {value:02x?}")), }, Err(error) => Err(format!("{error}")), }, Command::Remove { key } => match remove(key) { Ok(()) => Ok("Done".to_string()), Err(error) => Err(format!("{error}")), }, } } } enum Key { Exact(usize), Range(Range<usize>), } impl FromStr for Key { type Err = ParseIntError; fn from_str(s: &str) -> Result<Self, Self::Err> { match s.split_once("..") { Some((start, end)) => Ok(Key::Range(start.parse()? .. end.parse()?)), None => Ok(Key::Exact(s.parse()?)), } } } impl Key { fn parse(key: &str) -> Result<Self, String> { let key: Key = key.parse().map_err(|_| "Failed to parse key")?; let valid = match &key { Key::Exact(key) => *key < 4096, Key::Range(keys) => !keys.is_empty() && keys.end < 4096, }; if !valid { return Err("Invalid key".to_string()); } Ok(key) } } fn insert(key: &Key, value: &[u8]) -> Result<(), Error> { match key { Key::Exact(key) => store::insert(*key, value), Key::Range(keys) => store::fragment::insert(keys.clone(), value), } } fn find(key: &Key) -> Result<Option<Box<[u8]>>, Error> { match key { Key::Exact(key) => store::find(*key), Key::Range(keys) => store::fragment::find(keys.clone()), } } fn remove(key: &Key) -> Result<(), Error> { match key { Key::Exact(key) => store::remove(*key), Key::Range(keys) => store::fragment::remove(keys.clone()), } }
Testing
We can use a one-liner REPL in shell around wasefire applet-rpc
:
while read line; do echo "$line" | wasefire applet-rpc; done
An example interaction could look like this:
help
Usage: insert <key>[..<key>] <value>
Usage: find <key>[..<key>]
Usage: remove <key>[..<key>]
> insert 0 hello world
Error: Invalid command "insert"
> insert 0 hello-world
Done
> find 0
Found: hello-world
> find 1
Not found
> remove 0
Done
> find 0
Not found