feat(init): spawn service over IPC with per-client allowance refunded on child death; up to 4 death watchers; spawntest on test image

This commit is contained in:
2026-10-27 16:21:08 +01:00
parent 817a38993b
commit 8587661684
23 changed files with 564 additions and 28 deletions

View File

@@ -0,0 +1,13 @@
[package]
name = "spawntest"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "spawntest"
test = false
bench = false
[dependencies]
types = { workspace = true }
libkos = { workspace = true }

View File

@@ -0,0 +1,61 @@
//! The test image's last check, started by init with a spawn allowance:
//! programs run through init, the allowance holds, and it comes back when
//! a child dies. Reports the whole self-test and halts.
#![no_std]
#![no_main]
extern crate libkos;
use libkos::spawn::{SpawnError, spawn};
use libkos::syscall;
use types::bounded::CapSlot;
use types::budget::Limits;
/// Where init put them (see init's manifest: spawn endpoint, then control).
const INIT: CapSlot = CapSlot::const_new(0);
const CONTROL: CapSlot = CapSlot::const_new(1);
/// Over half the allowance (128 pages), so a second one fits only after
/// the first has died and its limits came back.
const HELLO: Limits = Limits { pages: 80, endpoints: 0, notifications: 0, regions: 0 };
/// Spawns hello, runs it, and waits for it to die.
fn run_hello(died: CapSlot) -> Result<(), &'static str> {
let child = spawn(INIT, b"hello.elf", HELLO).map_err(|_| "init would not spawn hello")?;
syscall::watch_death(child.process, died).map_err(|_| "cannot watch hello")?;
if !child.start() {
return Err("cannot start hello");
}
syscall::wait(died).map_err(|_| "wait failed")?;
Ok(())
}
fn check() -> Result<(), &'static str> {
let died = syscall::create_notification().map_err(|_| "no notification")?;
run_hello(died)?;
libkos::println!("spawntest: spawned program runs and its death is heard: ok");
let greedy = Limits { pages: 200, ..HELLO };
if spawn(INIT, b"hello.elf", greedy).err() != Some(SpawnError::OverAllowance) {
return Err("init spawned past the allowance");
}
let tiny = Limits { pages: 1, ..HELLO };
if spawn(INIT, b"nothere.elf", tiny).err() != Some(SpawnError::NotFound) {
return Err("a missing program was not reported");
}
libkos::println!("spawntest: allowance and missing files refused: ok");
run_hello(died)?;
libkos::println!("spawntest: allowance comes back when a child dies: ok");
Ok(())
}
#[unsafe(no_mangle)]
extern "C" fn main() {
match check() {
Ok(()) => libkos::println!("SMPTEST PASS"),
Err(why) => libkos::println!("SMPTEST FAIL: {}", why),
}
syscall::halt(CONTROL);
}

View File

@@ -93,7 +93,7 @@ pub enum Syscall {
region: CapSlot,
},
/// Signals `notification` when the process behind `process` dies, by
/// exit, fault or kill. One watcher per process; a new one replaces it.
/// exit, fault or kill. Up to `timer::MAX_WATCHERS` per process.
WatchDeath {
process: CapSlot,
notification: CapSlot,

View File

@@ -18,6 +18,9 @@ pub struct Target {
pub badge: Badge,
}
/// How many may watch one process die: its parent and init, with room.
pub const MAX_WATCHERS: usize = 4;
pub struct Timers {
deadlines: [Option<(Ticks, Target)>; MAX_PROCESSES],
}

View File

@@ -14,3 +14,4 @@ hw-virtio = { workspace = true }
types = { workspace = true }
libkos = { workspace = true }
server-framework = { workspace = true }

View File

@@ -5,12 +5,14 @@ use libkos::device::Device as BoardDevice;
use libkos::fs::Storage;
use libkos::syscall;
use types::address::StackPointer;
use types::badge::Badge;
use types::bounded::CapSlot;
use types::permission::{Permission, PermissionSet};
use crate::devices::Devices;
use crate::load::{self, LoadedProcess};
use crate::manifest::{Device, Program};
use crate::spawn::Spawner;
const USER_STACK_TOP: StackPointer = StackPointer::from_raw(0x10000);
const MAX_SERVED: usize = 8;
@@ -25,11 +27,22 @@ pub(crate) struct Launcher<'a> {
devices: Devices,
/// The endpoint of every server started so far, by name.
served: [Option<(&'static str, CapSlot)>; MAX_SERVED],
/// Init's own endpoint, for the spawn service.
pub(crate) spawn_ep: CapSlot,
/// Bound to init's thread: spawned children's deaths, a bit each.
pub(crate) deaths: CapSlot,
pub(crate) spawner: Spawner,
}
impl<'a> Launcher<'a> {
pub(crate) fn new(storage: &'a mut Storage, scratch: &'a mut [u8], devices: Devices) -> Self {
Self { storage, scratch, devices, served: [None; MAX_SERVED] }
pub(crate) fn new(
storage: &'a mut Storage,
scratch: &'a mut [u8],
devices: Devices,
) -> Result<Self, &'static str> {
let spawn_ep = syscall::create_endpoint().ok_or("no spawn endpoint")?;
let deaths = syscall::create_notification().map_err(|_| "no death notification")?;
Ok(Self { storage, scratch, devices, served: [None; MAX_SERVED], spawn_ep, deaths, spawner: Spawner::new() })
}
pub(crate) fn storage(&mut self) -> (&mut Storage, &mut [u8]) {
@@ -61,6 +74,13 @@ impl<'a> Launcher<'a> {
let ep = self.endpoint(name).ok_or("needs a server that is not running")?;
syscall::delegate(ep, child, perms(&[Permission::Execute])).ok_or("cannot give an endpoint")?;
}
if let Some(allowance) = p.spawns {
self.give_spawn(child, allowance)?;
}
if p.system_control {
let control = crate::devices::system_control().ok_or("no SystemControl to give")?;
syscall::delegate(control, child, perms(&[Permission::Execute])).ok_or("cannot give SystemControl")?;
}
let console = libkos::start::console()
.filter(|_| p.console)
.and_then(|c| syscall::delegate(c, child, perms(&[Permission::Write])));
@@ -82,6 +102,17 @@ impl<'a> Launcher<'a> {
Ok(note)
}
/// A badged copy of init's endpoint: the badge names the client and so
/// its allowance.
fn give_spawn(&mut self, child: CapSlot, allowance: types::budget::Limits) -> Result<(), &'static str> {
let badge: Badge = self.spawner.add_client(allowance).ok_or("too many spawn clients")?;
let minted = perms(&[Permission::Execute, Permission::Delegate]);
let copy = syscall::mint(self.spawn_ep, badge, minted).map_err(|_| "cannot mint")?;
let given = syscall::delegate(copy, child, perms(&[Permission::Execute]));
syscall::drop_cap(copy);
given.map(|_| ()).ok_or("cannot give the spawn endpoint")
}
/// The driver waits for its device before serving, and runs without
/// one (headless) if the board has none.
fn give(&self, device: Device, ep: CapSlot) -> &'static str {

View File

@@ -7,6 +7,7 @@ mod launch;
mod load;
mod manifest;
mod selftest;
mod spawn;
use libkos::fs::Storage;
use libkos::syscall;
@@ -39,21 +40,31 @@ pub fn run() -> ! {
idle();
};
let mut launcher = launch::Launcher::new(&mut storage, scratch, found);
let mut launcher = match launch::Launcher::new(&mut storage, scratch, found) {
Ok(l) => l,
Err(why) => {
libkos::println!("init: {}", why);
idle();
}
};
launch_all(&mut launcher, manifest::SERVERS);
give_storage_the_log();
// The test image carries the SMP self-test; it runs instead of the
// desktop and halts the machine.
// The test image carries the SMP self-test; it and the tests run
// instead of the desktop, and the last one halts the machine.
let (storage, scratch) = launcher.storage();
if storage.open(b"smptest.elf").is_ok_and(|h| storage.close(h).is_ok()) {
libkos::println!("init: running the SMP self-test");
selftest::run(storage, scratch);
launch_all(&mut launcher, manifest::TESTS);
} else {
launch_all(&mut launcher, manifest::APPS);
}
launch_all(&mut launcher, manifest::APPS);
libkos::println!("init: all loaded, supervising");
idle();
libkos::println!("init: all loaded, serving spawns");
let _ = syscall::bind_notification(launcher.deaths);
let ep = launcher.spawn_ep;
server_framework::serve(&mut launcher, ep)
}
fn launch_all(launcher: &mut launch::Launcher, programs: &[manifest::Program]) {

View File

@@ -20,6 +20,10 @@ pub struct Program {
pub uses: &'static [&'static str],
/// Takes the console over from init; everyone else prints through it.
pub console: bool,
/// May have init spawn programs, whose limits together stay within this.
pub spawns: Option<Limits>,
/// May halt the machine.
pub system_control: bool,
}
/// Enough for a small program: its image, stack and a little heap.
@@ -31,7 +35,17 @@ pub const SMALL: Limits = Limits {
};
const fn server(name: &'static str, file: &'static [u8], limits: Limits) -> Program {
Program { name, file, limits, serves: true, device: None, uses: &[], console: false }
Program {
name,
file,
limits,
serves: true,
device: None,
uses: &[],
console: false,
spawns: None,
system_control: false,
}
}
/// Started on every boot. The log server comes first so the rest can print.
@@ -47,6 +61,15 @@ pub const SERVERS: &[Program] = &[
server("session", b"session.elf", SMALL),
];
/// Run on the test image after the SMP self-test, instead of the desktop:
/// spawns through init, then reports and halts.
pub const TESTS: &[Program] = &[Program {
serves: false,
spawns: Some(Limits { pages: 128, endpoints: 2, notifications: 2, regions: 2 }),
system_control: true,
..server("spawntest", b"spawntst.elf", SMALL)
}];
/// The desktop; the test image runs the SMP self-test instead.
pub const APPS: &[Program] = &[Program {
serves: false,

View File

@@ -121,12 +121,13 @@ fn timer_is_on_time(events: CapSlot) -> Result<(), &'static str> {
Ok(())
}
/// Runs the test, reports, and halts the machine.
pub fn run(storage: &mut Storage, scratch: &mut [u8]) -> ! {
match run_phases(storage, scratch) {
Ok(()) => libkos::println!("SMPTEST PASS"),
Err(why) => libkos::println!("SMPTEST FAIL: {}", why),
}
/// Runs the hart checks; on failure reports and halts the machine, else
/// returns so the spawn test can run (and report the whole result).
pub fn run(storage: &mut Storage, scratch: &mut [u8]) {
let Err(why) = run_phases(storage, scratch) else {
return;
};
libkos::println!("SMPTEST FAIL: {}", why);
match crate::devices::system_control() {
Some(control) => syscall::halt(control),
None => {

178
crates/init/src/spawn.rs Normal file
View File

@@ -0,0 +1,178 @@
//! Init's spawn service (`types::spawn`): loads a program for a client and
//! hands it the process, unstarted. Each client has an allowance from the
//! manifest; a child's limits come out of it and return when the child dies.
use libkos::syscall;
use server_framework::{Decode, Encode, MALFORMED, Server};
use types::badge::Badge;
use types::bounded::CapSlot;
use types::budget::Limits;
use types::permission::{Permission, PermissionSet};
use types::spawn::{MAX_NAME, NONE, reply};
use crate::launch::Launcher;
use crate::load;
pub(crate) const MAX_CLIENTS: usize = 4;
/// Death signals carry the child's index as a bit.
const MAX_CHILDREN: usize = 8;
struct Child {
client: usize,
limits: Limits,
process: CapSlot,
}
pub(crate) struct Spawner {
/// What each client (badge = index + 1) may still give its children.
allowances: [Option<Limits>; MAX_CLIENTS],
children: [Option<Child>; MAX_CHILDREN],
}
fn fits(want: Limits, left: Limits) -> bool {
want.pages <= left.pages
&& want.endpoints <= left.endpoints
&& want.notifications <= left.notifications
&& want.regions <= left.regions
}
fn add(a: Limits, b: Limits) -> Limits {
Limits {
pages: a.pages + b.pages,
endpoints: a.endpoints + b.endpoints,
notifications: a.notifications + b.notifications,
regions: a.regions + b.regions,
}
}
fn sub(a: Limits, b: Limits) -> Limits {
Limits {
pages: a.pages - b.pages,
endpoints: a.endpoints - b.endpoints,
notifications: a.notifications - b.notifications,
regions: a.regions - b.regions,
}
}
impl Spawner {
pub(crate) const fn new() -> Self {
Self { allowances: [None; MAX_CLIENTS], children: [const { None }; MAX_CHILDREN] }
}
/// The badge for a new client with `allowance`.
pub(crate) fn add_client(&mut self, allowance: Limits) -> Option<Badge> {
let i = self.allowances.iter().position(Option::is_none)?;
self.allowances[i] = Some(allowance);
Some(Badge::new(i + 1))
}
}
pub(crate) struct Request {
name: [u8; MAX_NAME],
len: usize,
limits: Limits,
}
impl Decode for Request {
fn decode(words: [usize; 4]) -> Option<Self> {
types::spawn::parse(words).map(|(name, len, limits)| Self { name, len, limits })
}
}
pub(crate) struct Reply([usize; 4]);
impl Encode for Reply {
fn encode(&self) -> [usize; 4] {
self.0
}
}
fn refused(tag: usize) -> ([usize; 4], Option<CapSlot>) {
([tag, 0, 0, 0], None)
}
impl Launcher<'_> {
fn spawn(&mut self, badge: usize, r: &Request) -> ([usize; 4], Option<CapSlot>) {
let Some(client) = badge.checked_sub(1).filter(|c| *c < MAX_CLIENTS) else {
return refused(reply::ERROR);
};
let Some(left) = self.spawner.allowances[client] else {
return refused(reply::ERROR);
};
if !fits(r.limits, left) {
return refused(reply::OVER_ALLOWANCE);
}
let Some(index) = self.spawner.children.iter().position(Option::is_none) else {
return refused(reply::ERROR);
};
let name = &r.name[..r.len];
let (storage, scratch) = self.storage();
match storage.open(name) {
Ok(h) => {
let _ = storage.close(h);
}
Err(_) => return refused(reply::NOT_FOUND),
}
let Some(loaded) = load::load_binary(storage, scratch, name, r.limits) else {
return refused(reply::ERROR);
};
if !self.watch(loaded.process_cap, index) {
let _ = syscall::kill(loaded.process_cap);
syscall::drop_cap(loaded.process_cap);
return refused(reply::ERROR);
}
let call_only = PermissionSet::empty().with(Permission::Execute);
let log = libkos::start::log().and_then(|l| syscall::delegate(l, loaded.process_cap, call_only));
self.spawner.allowances[client] = Some(sub(left, r.limits));
self.spawner.children[index] = Some(Child { client, limits: r.limits, process: loaded.process_cap });
let log = log.map_or(NONE, |s| s.value() as usize);
([reply::OK, loaded.entry_point.raw(), log, 0], Some(loaded.process_cap))
}
/// Has the child's death signal bit `index` on init's death notification.
fn watch(&self, process: CapSlot, index: usize) -> bool {
let signal = PermissionSet::empty().with(Permission::Write);
let Ok(copy) = syscall::mint(self.deaths, Badge::new(1 << index), signal) else {
return false;
};
let watched = syscall::watch_death(process, copy).is_ok();
syscall::drop_cap(copy);
watched
}
/// Children behind `bits` died: their limits go back to their clients.
fn reap(&mut self, bits: usize) {
for (i, slot) in self.spawner.children.iter_mut().enumerate() {
if bits & (1 << i) == 0 {
continue;
}
if let Some(child) = slot.take() {
if let Some(Some(left)) = self.spawner.allowances.get_mut(child.client) {
*left = add(*left, child.limits);
}
syscall::drop_cap(child.process);
}
}
}
}
impl Server for Launcher<'_> {
type Request = Request;
type Response = Reply;
/// Without a badge there is no client, so no allowance.
fn handle(&mut self, _request: Request) -> Reply {
Reply(refused(reply::ERROR).0)
}
fn handle_call(&mut self, badge: usize, words: [usize; 4], _cap: Option<CapSlot>) -> ([usize; 4], Option<CapSlot>) {
match Request::decode(words) {
Some(r) => self.spawn(badge, &r),
None => ([MALFORMED, 0, 0, 0], None),
}
}
fn signal(&mut self, bits: usize) {
self.reap(bits);
}
}

View File

@@ -78,7 +78,7 @@ pub struct Kernel<P: Platform> {
/// Where each process's next heap search starts (see memory.rs).
heap_cursors: [usize; MAX_PROCESSES],
/// Who to signal when each process dies (see syscall/ipc/events.rs).
death_watchers: [Option<domain::timer::Target>; MAX_PROCESSES],
death_watchers: [[Option<domain::timer::Target>; domain::timer::MAX_WATCHERS]; MAX_PROCESSES],
timers: domain::timer::Timers,
}
@@ -102,7 +102,7 @@ impl<P: Platform> Kernel<P> {
homes: [types::id::CoreId::from_raw(0); MAX_THREADS],
tree: domain::capability::tree::CapTree::new(),
heap_cursors: [0; MAX_PROCESSES],
death_watchers: [None; MAX_PROCESSES],
death_watchers: [[None; domain::timer::MAX_WATCHERS]; MAX_PROCESSES],
timers: domain::timer::Timers::new(),
}
}
@@ -138,7 +138,8 @@ impl<P: Platform> Kernel<P> {
addr_of_mut!((*slot).homes).write([types::id::CoreId::from_raw(0); MAX_THREADS]);
addr_of_mut!((*slot).tree).write(domain::capability::tree::CapTree::new());
addr_of_mut!((*slot).heap_cursors).write([0; MAX_PROCESSES]);
addr_of_mut!((*slot).death_watchers).write([None; MAX_PROCESSES]);
addr_of_mut!((*slot).death_watchers)
.write([[None; domain::timer::MAX_WATCHERS]; MAX_PROCESSES]);
addr_of_mut!((*slot).timers).write(domain::timer::Timers::new());
}
}

View File

@@ -40,22 +40,27 @@ impl<P: Platform> Kernel<P> {
_ => return Err(KernelError::WrongObject),
};
let target = self.target(pid, notification)?;
let slot = self
let watchers = self
.death_watchers
.get_mut(watched.raw() as usize)
.ok_or(KernelError::InvalidProcess(watched))?;
let slot = watchers
.iter_mut()
.find(|w| w.is_none_or(|w| w == target))
.ok_or(KernelError::NoFreeSlot)?;
*slot = Some(target);
Ok(())
}
/// A process is dying: tell its watcher, forget its own timer.
/// A process is dying: tell its watchers, forget its own timer.
pub(crate) fn report_death(&mut self, pid: ProcessId) {
self.timers.clear(pid);
if let Some(target) = self
let watchers = self
.death_watchers
.get_mut(pid.raw() as usize)
.and_then(Option::take)
{
.map(core::mem::take)
.unwrap_or_default();
for target in watchers.into_iter().flatten() {
self.fire(target);
}
}

View File

@@ -161,3 +161,21 @@ fn notification_id(
_ => panic!("not a notification"),
}
}
#[test]
fn every_watcher_hears_and_there_is_room_for_a_few() {
let (mut k, init, proc_cap, first) = watching();
let second = k.create_notification_cap(init).unwrap();
k.watch_death(init, proc_cap, second).unwrap();
// The same notification again takes no new place.
k.watch_death(init, proc_cap, second).unwrap();
for _ in 2..domain::timer::MAX_WATCHERS {
let n = k.create_notification_cap(init).unwrap();
k.watch_death(init, proc_cap, n).unwrap();
}
let extra = k.create_notification_cap(init).unwrap();
assert!(k.watch_death(init, proc_cap, extra).is_err());
k.handle_syscall(Syscall::Kill { process: proc_cap });
assert_eq!(k.poll(init, first).unwrap(), 1);
assert_eq!(k.poll(init, second).unwrap(), 1);
}

View File

@@ -10,6 +10,7 @@ pub mod fs;
pub mod input;
pub mod io;
mod rt;
pub mod spawn;
pub mod start;
pub mod syscall;

View File

@@ -0,0 +1,53 @@
//! Running a program through init (protocol: `types::spawn`). Init loads it
//! and gives it a log endpoint; the caller hands it any other capabilities
//! (`delegate` to the process) and starts it.
use crate::syscall;
use types::address::{EntryPoint, StackPointer};
use types::bounded::CapSlot;
use types::budget::Limits;
use types::spawn::{STACK_TOP, reply, request};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SpawnError {
NotFound,
/// More than what is left of this process's spawn allowance.
OverAllowance,
Failed,
}
/// A loaded program, not yet running.
pub struct Spawned {
pub process: CapSlot,
entry: EntryPoint,
log: Option<CapSlot>,
}
pub fn spawn(init: CapSlot, name: &[u8], limits: Limits) -> Result<Spawned, SpawnError> {
let words = request(name, limits).ok_or(SpawnError::Failed)?;
let reply = syscall::call(init, words, None).map_err(|_| SpawnError::Failed)?;
let [tag, entry, log, _] = reply.words();
let cap = match reply {
syscall::Received::Message { cap, .. } | syscall::Received::Call { cap, .. } => cap,
syscall::Received::Signal { .. } => None,
};
match (tag, cap) {
(reply::OK, Some(process)) => Ok(Spawned {
process,
entry: EntryPoint::from_raw(entry),
log: u8::try_from(log).ok().and_then(CapSlot::new),
}),
(reply::NOT_FOUND, _) => Err(SpawnError::NotFound),
(reply::OVER_ALLOWANCE, _) => Err(SpawnError::OverAllowance),
_ => Err(SpawnError::Failed),
}
}
impl Spawned {
/// Runs the program, telling it where its log endpoint is.
pub fn start(&self) -> bool {
let arg = types::start_arg::encode(None, self.log);
let stack = StackPointer::from_raw(STACK_TOP);
syscall::spawn_thread_in(self.process, self.entry, stack, arg) != usize::MAX
}
}

View File

@@ -14,6 +14,7 @@ pub mod mapping;
pub mod permission;
#[cfg(kani)]
pub mod proofs;
pub mod spawn;
pub mod start_arg;
pub mod storage;
pub mod syscall_abi;

110
crates/types/src/spawn.rs Normal file
View File

@@ -0,0 +1,110 @@
//! Init's spawn protocol, shared by init and `libkos::spawn`: a client asks
//! init to load a program into a new, not yet running process; init replies
//! with the process capability and the client wires and starts it.
//!
//! Request: `[SPAWN | name_len << 8, packed limits, name bytes 0..8, 8..16]`.
//! Reply: `[reply::OK, entry point, child's log slot or NONE, 0]` carrying
//! the process capability, or `[error tag, 0, 0, 0]`.
use crate::budget::Limits;
pub const SPAWN: usize = 0;
/// Program file names: FAT 8.3, with room to spare.
pub const MAX_NAME: usize = 16;
/// Where init puts the child's stack top (its stack ends at the image).
pub const STACK_TOP: usize = 0x10000;
/// A slot word meaning "none".
pub const NONE: usize = usize::MAX;
pub mod reply {
pub const OK: usize = 0;
pub const NOT_FOUND: usize = 1;
/// The limits do not fit in what is left of the client's allowance.
pub const OVER_ALLOWANCE: usize = 2;
pub const ERROR: usize = 3;
}
/// Pages in bits 0..32, endpoints, notifications and regions a byte each
/// above; `None` if a part does not fit.
fn pack(l: Limits) -> Option<usize> {
let byte = |v: usize| u8::try_from(v).ok().map(usize::from);
let pages = u32::try_from(l.pages).ok()? as u64;
let rest = (byte(l.endpoints)? | byte(l.notifications)? << 8 | byte(l.regions)? << 16) as u64;
usize::try_from(pages | rest << 32).ok()
}
fn unpack(w: usize) -> Limits {
let w = w as u64;
let byte = |shift: u32| ((w >> shift) & 0xFF) as usize;
Limits {
pages: (w & 0xFFFF_FFFF) as usize,
endpoints: byte(32),
notifications: byte(40),
regions: byte(48),
}
}
pub fn request(name: &[u8], limits: Limits) -> Option<[usize; 4]> {
if name.is_empty() || name.len() > MAX_NAME {
return None;
}
let mut bytes = [0u8; MAX_NAME];
bytes[..name.len()].copy_from_slice(name);
let (lo, hi) = bytes.as_chunks::<8>().0.split_at(1);
Some([
SPAWN | name.len() << 8,
pack(limits)?,
u64::from_le_bytes(lo[0]) as usize,
u64::from_le_bytes(hi[0]) as usize,
])
}
/// The name (and how much of it counts) and limits of a spawn request.
pub fn parse(words: [usize; 4]) -> Option<([u8; MAX_NAME], usize, Limits)> {
let len = words[0] >> 8;
if words[0] & 0xFF != SPAWN || len == 0 || len > MAX_NAME {
return None;
}
let mut name = [0u8; MAX_NAME];
for (b, w) in name.as_chunks_mut::<8>().0.iter_mut().zip(&words[2..]) {
*b = (*w as u64).to_le_bytes();
}
Some((name, len, unpack(words[1])))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn requests_round_trip() {
let l = Limits {
pages: 4096,
endpoints: 2,
notifications: 3,
regions: 255,
};
let (name, len, back) = parse(request(b"hello.elf", l).unwrap()).unwrap();
assert_eq!(&name[..len], b"hello.elf");
assert_eq!(back, l);
}
#[test]
fn what_cannot_be_said_is_refused() {
let l = Limits::default();
assert!(request(b"", l).is_none());
assert!(request(&[b'a'; 17], l).is_none());
assert!(
request(
b"x",
Limits {
endpoints: 256,
..l
}
)
.is_none()
);
assert!(parse([SPAWN | 17 << 8, 0, 0, 0]).is_none());
assert!(parse([1 | 3 << 8, 0, 0, 0]).is_none());
}
}