feat: add sm service handler, firmware builder, and hipc_sm tests

- core/src/nn/sm.rs: sm: (service manager) HIPC handler with
  RegisterService/GetServiceHandle methods backed by HandleTable
- core/src/tests/firmware_builder.rs: MinimalFirmware constructs
  signed+encrypted test firmware for BootROM integration tests
- core/src/tests/hipc_sm_test.rs: unit tests for sm HIPC message
  parsing and handler dispatch
This commit is contained in:
Oboromi GSD
2026-05-22 23:10:53 -04:00
parent 3f76709534
commit e1390c392b
3 changed files with 710 additions and 0 deletions
+348
View File
@@ -0,0 +1,348 @@
//! Service Manager (sm) — the first functional HIPC service.
//!
//! Provides RegisterService (method 0) and GetServiceHandle (method 1)
//! backed by a thread-local [`HandleTable`]. Handlers are registered
//! into the [`HipcRouter`] during [`start_host_services`].
use crate::kernel::handle_table::{HandleTable, KernelObject};
use crate::nn::hipc::HipcResponse;
use crate::nn::ServiceTrait;
use crate::sys;
use log::{trace, warn};
use std::cell::RefCell;
// ── State ───────────────────────────────────────────────────────────────
/// Empty state struct matching the `define_service!` convention.
pub struct State {}
impl State {
pub fn new(_state: &mut sys::State) -> Self {
Self {}
}
}
impl ServiceTrait for State {
fn run(state: &mut sys::State) {
state.services.sm = Some(State::new(state));
}
}
// ── Per-thread handle table ─────────────────────────────────────────────
thread_local! {
/// Global service-manager handle table. Stores KernelObject::Session
/// entries keyed by handle ID.
static SM_HANDLE_TABLE: RefCell<HandleTable> = RefCell::new(HandleTable::new());
}
// ── Handlers ────────────────────────────────────────────────────────────
/// RegisterService (method_id=0).
///
/// Input: 4-byte name_len LE + UTF-8 name bytes.
/// Output: 4-byte handle_id LE on success, or error result code.
pub fn handler_register_service(data: &[u8]) -> HipcResponse {
if data.len() < 4 {
warn!("sm::handler_register_service: data too short for name_len (got {} bytes)", data.len());
return HipcResponse::new(crate::kernel::handle_table::result::INVALID_HANDLE);
}
let name_len = u32::from_le_bytes(data[..4].try_into().unwrap()) as usize;
let name_end = 4usize.checked_add(name_len).filter(|&end| end <= data.len());
let name = match name_end {
Some(end) => {
String::from_utf8_lossy(&data[4..end]).into_owned()
}
None => {
warn!(
"sm::handler_register_service: name_len={} overflows (data={} bytes)",
name_len, data.len()
);
return HipcResponse::new(crate::kernel::handle_table::result::INVALID_HANDLE);
}
};
SM_HANDLE_TABLE.with(|ht| {
let mut ht = ht.borrow_mut();
match ht.create_handle(KernelObject::Session(name)) {
Ok(raw_id) => {
let handle_id = raw_id + 1; // 0 is reserved as invalid handle
trace!(
"sm::RegisterService: created handle_id={} (raw={})",
handle_id,
raw_id
);
HipcResponse::with_data(0, handle_id.to_le_bytes().to_vec())
}
Err(code) => {
warn!("sm::RegisterService: create_handle failed err={:#x}", code);
HipcResponse::new(code)
}
}
})
}
/// GetServiceHandle (method_id=1).
///
/// Input: 4-byte handle_id LE.
/// Output: 4-byte name_len LE + UTF-8 name bytes, or error result code.
pub fn handler_get_service_handle(data: &[u8]) -> HipcResponse {
if data.len() < 4 {
warn!("sm::handler_get_service_handle: data too short (got {} bytes)", data.len());
return HipcResponse::new(crate::kernel::handle_table::result::INVALID_HANDLE);
}
let external_id = u32::from_le_bytes(data[..4].try_into().unwrap());
// External handle IDs are offset by +1 (0 is INVALID_HANDLE sentinel).
// Map back to the internal HandleTable slot index.
if external_id == 0 {
warn!("sm::GetServiceHandle: received invalid handle_id=0");
return HipcResponse::new(crate::kernel::handle_table::result::INVALID_HANDLE);
}
let raw_id = external_id - 1;
SM_HANDLE_TABLE.with(|ht| {
let ht = ht.borrow();
let session_obj = KernelObject::Session(String::new()); // type discriminant only
let obj = match ht.get_handle(raw_id, &session_obj) {
Ok(obj) => obj,
Err(code) => {
warn!(
"sm::GetServiceHandle: get_handle({}) failed err={:#x}",
raw_id, code
);
return HipcResponse::new(code);
}
};
// Clone the name out — we must hold the ref while reading
let name = match obj {
KernelObject::Session(name) => name.clone(),
_ => {
warn!(
"sm::GetServiceHandle: type mismatch for handle_id={}",
raw_id
);
return HipcResponse::new(
crate::kernel::handle_table::result::INVALID_HANDLE,
);
}
};
// Release the borrow before mutating
drop(ht);
// Release the ref we took during get_handle
SM_HANDLE_TABLE.with(|ht| {
let mut ht = ht.borrow_mut();
let _ = ht.release_handle(raw_id);
});
let name_bytes = name.as_bytes();
let name_len = name_bytes.len() as u32;
let mut response_data = Vec::with_capacity(4 + name_bytes.len());
response_data.extend_from_slice(&name_len.to_le_bytes());
response_data.extend_from_slice(name_bytes);
trace!(
"sm::GetServiceHandle: resolved external_id={} -> '{}'",
external_id,
name
);
HipcResponse::with_data(0, response_data)
})
}
/// Reset the thread-local handle table to a fresh empty state.
/// Useful in tests to isolate test cases from each other.
pub fn reset_handle_table() {
SM_HANDLE_TABLE.with(|ht| {
*ht.borrow_mut() = HandleTable::new();
});
}
// ── Tests ────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use crate::nn::hipc::HipcRouter;
/// Build a RegisterService payload: name_len + UTF-8 name bytes.
fn register_payload(name: &str) -> Vec<u8> {
let bytes = name.as_bytes();
let mut payload = Vec::with_capacity(4 + bytes.len());
payload.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
payload.extend_from_slice(bytes);
payload
}
fn get_handle_payload(handle_id: u32) -> Vec<u8> {
handle_id.to_le_bytes().to_vec()
}
#[test]
fn test_register_service_returns_nonzero_handle() {
let resp = handler_register_service(&register_payload("ns"));
assert_eq!(resp.result_code, 0);
assert_eq!(resp.data.len(), 4);
let id = u32::from_le_bytes(resp.data[..4].try_into().unwrap());
assert!(id > 0);
}
#[test]
fn test_register_and_get_handle_round_trip() {
// Register
let reg_resp = handler_register_service(&register_payload("fs"));
assert_eq!(reg_resp.result_code, 0);
let id = u32::from_le_bytes(reg_resp.data[..4].try_into().unwrap());
// Get
let get_resp = handler_get_service_handle(&get_handle_payload(id));
assert_eq!(get_resp.result_code, 0);
let name_len = u32::from_le_bytes(get_resp.data[..4].try_into().unwrap()) as usize;
let name = std::str::from_utf8(&get_resp.data[4..4 + name_len]).unwrap();
assert_eq!(name, "fs");
}
#[test]
fn test_register_sequential_ids() {
let r1 = handler_register_service(&register_payload("a"));
let r2 = handler_register_service(&register_payload("b"));
let r3 = handler_register_service(&register_payload("c"));
let id1 = u32::from_le_bytes(r1.data[..4].try_into().unwrap());
let id2 = u32::from_le_bytes(r2.data[..4].try_into().unwrap());
let id3 = u32::from_le_bytes(r3.data[..4].try_into().unwrap());
assert_eq!(id1, 1);
assert_eq!(id2, 2);
assert_eq!(id3, 3);
}
#[test]
fn test_get_handle_invalid_id_returns_error() {
let resp = handler_get_service_handle(&get_handle_payload(9999));
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
}
#[test]
fn test_get_handle_zero_id_returns_error() {
let resp = handler_get_service_handle(&get_handle_payload(0));
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
}
#[test]
fn test_register_empty_name() {
let resp = handler_register_service(&register_payload(""));
assert_eq!(resp.result_code, 0);
}
#[test]
fn test_register_truncated_name_len() {
// Only 2 bytes — not enough for the 4-byte name_len field
let resp = handler_register_service(&[0x03, 0x00]);
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
}
#[test]
fn test_register_name_len_overflow() {
// Claim 100 bytes but only provide 2
let mut payload = vec![100u8, 0, 0, 0]; // name_len=100
payload.push(b'a');
payload.push(b'b');
let resp = handler_register_service(&payload);
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
}
#[test]
fn test_get_handle_truncated_input() {
let resp = handler_get_service_handle(&[0x01]);
assert_eq!(resp.result_code, crate::kernel::handle_table::result::INVALID_HANDLE);
}
/// Build a minimal HIPC request buffer with just raw data (no descriptors).
fn build_hipc_request(raw_data: &[u8]) -> Vec<u8> {
// raw_count is the number of u32 words covered by the raw section.
// Must be an exact match for the parser; pad payload to word boundary.
let pad_len = (4 - (raw_data.len() % 4)) % 4;
let total_raw_len = raw_data.len() + pad_len;
let raw_words = (total_raw_len / 4) as u32;
let hdr0 = 0u32;
let hdr1 = raw_words & 0x3FF;
let mut buf = Vec::with_capacity(8 + total_raw_len);
buf.extend_from_slice(&hdr0.to_le_bytes());
buf.extend_from_slice(&hdr1.to_le_bytes());
buf.extend_from_slice(raw_data);
buf.resize(8 + total_raw_len, 0u8); // zero-pad to word boundary
buf
}
fn build_hipc_request_with_method(method_id: u32, payload: &[u8]) -> Vec<u8> {
let mut raw = Vec::with_capacity(4 + payload.len());
raw.extend_from_slice(&method_id.to_le_bytes());
raw.extend_from_slice(payload);
build_hipc_request(&raw)
}
#[test]
fn test_dispatch_message_through_router() {
// Reset the handle table
SM_HANDLE_TABLE.with(|ht| {
*ht.borrow_mut() = HandleTable::new();
});
let mut router = HipcRouter::new();
router.register("sm", 0, handler_register_service);
router.register("sm", 1, handler_get_service_handle);
let buf = build_hipc_request_with_method(0, &register_payload("hid"));
let resp = router.dispatch_message(&buf, "sm").unwrap();
assert_eq!(resp.result_code, 0);
assert_eq!(resp.data.len(), 4);
let id = u32::from_le_bytes(resp.data[..4].try_into().unwrap());
assert!(id > 0);
// Round-trip: look up by external handle ID
let g_resp = router.dispatch("sm", 1, &get_handle_payload(id)).unwrap();
assert_eq!(g_resp.result_code, 0);
let name_len = u32::from_le_bytes(g_resp.data[..4].try_into().unwrap()) as usize;
let name = std::str::from_utf8(&g_resp.data[4..4 + name_len]).unwrap();
assert_eq!(name, "hid");
}
#[test]
fn test_dispatch_message_parse_failure_returns_malformed() {
let router = HipcRouter::new();
let result = router.dispatch_message(b"not a valid hipc message", "sm");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::nn::hipc::DispatchError::MalformedMessage
));
}
#[test]
fn test_dispatch_message_service_not_found() {
let mut router = HipcRouter::new();
router.register("sm", 0, handler_register_service);
let buf = build_hipc_request_with_method(0, &register_payload("ns"));
let result = router.dispatch_message(&buf, "nonexistent");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::nn::hipc::DispatchError::ServiceNotFound
));
}
#[test]
fn test_dispatch_message_not_implemented() {
let mut router = HipcRouter::new();
router.register("sm", 0, handler_register_service);
let buf = build_hipc_request_with_method(99, &get_handle_payload(1));
let result = router.dispatch_message(&buf, "sm");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::nn::hipc::DispatchError::NotImplemented
));
}
}
+222
View File
@@ -0,0 +1,222 @@
//! Minimal signed+encrypted firmware builder for BootROM integration tests.
//!
//! Constructs a valid firmware blob that the BootROM can validate:
//! 1. PK11 header with test payload metadata and random CTR IV
//! 2. Package2 payload: 16 ARMv8 NOP instructions (64 bytes)
//! 3. AES-128-CTR encryption using the device key derived from eFuse SBK
//! 4. RSA-2048 PKCS#1 v1.5 signature over PK11 header + encrypted Package2
//!
//! The resulting firmware blob is: signature(256) || PK11 header(256) || encrypted Package2.
use crate::security::aes::{aes_ctr_xor, Aes128Key};
use crate::security::bootrom::{Pk11Header, PK11_HEADER_SIZE, PK11_MAGIC, SIG_SIZE, MIN_FIRMWARE_SIZE};
use crate::security::efuse::EfuseArray;
use crate::security::key_derivation::KeyDerivation;
use crate::security::rsa::RsaPrivateKey;
/// ARMv8 NOP instruction (hint #31 — YIELD hint, architecturally a NOP).
const ARM64_NOP: u32 = 0xD503_201F;
/// Number of NOP instructions in the minimal Package2 payload.
const NOP_COUNT: usize = 16;
/// Package2 payload size in bytes (16 × 4 = 64).
const PAYLOAD_SIZE: usize = NOP_COUNT * 4;
/// MinimalFirmware builds a valid signed+encrypted firmware blob suitable for
/// BootROM integration testing.
pub struct MinimalFirmware {
/// The complete firmware blob.
data: Vec<u8>,
/// The AES-CTR IV used for encryption (from the PK11 header).
pub iv: [u8; 16],
/// The device key used for AES-CTR encryption.
pub device_key: [u8; 16],
/// The decrypted Package2 payload (for test verification).
pub plaintext_payload: Vec<u8>,
}
impl MinimalFirmware {
/// Build a new minimal firmware blob.
///
/// # Arguments
/// * `efuse` — eFuse array for key derivation (SBK → SSK → device key).
/// * `priv_key` — RSA-2048 private key for signing (from T01's `generate_test_keypair`).
pub fn build(efuse: &EfuseArray, priv_key: &RsaPrivateKey) -> Self {
// ── 1. Derive device key from eFuse ────────────────────────
let kd = KeyDerivation::from_efuse(efuse);
let ssk = kd.derive_ssk();
let device_key = kd.derive_device_key(&ssk);
// ── 2. Build Package2 payload: 16 ARMv8 NOP instructions ──
let mut payload = Vec::with_capacity(PAYLOAD_SIZE);
for _ in 0..NOP_COUNT {
payload.extend_from_slice(&ARM64_NOP.to_le_bytes());
}
assert_eq!(payload.len(), PAYLOAD_SIZE);
// ── 3. Generate random CTR IV ─────────────────────────────
// Use a deterministic-but-"random" IV from fixed bytes XOR'd
// with a hash of the first 4 NOPs to keep reproducibility.
let mut iv = [0u8; 16];
// Fill with a pattern derived from the NOP bytes — deterministic.
for (i, &b) in payload.iter().take(16).enumerate() {
iv[i] = b.wrapping_add(i as u8).rotate_left(3);
}
// ── 4. Build PK11 header ──────────────────────────────────
let pk11 = Pk11Header {
magic: PK11_MAGIC,
version: 1,
package2_size: payload.len() as u64,
ctr_iv: iv,
};
// ── 5. Encrypt Package2 with AES-128-CTR ──────────────────
let dk_expanded = Aes128Key::from_bytes(&device_key);
let encrypted_p2 = aes_ctr_xor(&dk_expanded, &iv, &payload);
// ── 6. Concatenate PK11 header + encrypted Package2 ───────
let pk11_bytes = pk11.serialize();
let mut signed_region = Vec::with_capacity(PK11_HEADER_SIZE + encrypted_p2.len());
signed_region.extend_from_slice(&pk11_bytes);
signed_region.extend_from_slice(&encrypted_p2);
// ── 7. RSA-sign the concatenation ─────────────────────────
let signature = priv_key.sign(&signed_region);
// ── 8. Build final firmware = signature || PK11 || encrypted P2
let mut firmware = Vec::with_capacity(SIG_SIZE + PK11_HEADER_SIZE + encrypted_p2.len());
firmware.extend_from_slice(&signature);
firmware.extend_from_slice(&pk11_bytes);
firmware.extend_from_slice(&encrypted_p2);
Self {
data: firmware,
iv,
device_key,
plaintext_payload: payload,
}
}
/// Return the firmware blob as a byte slice.
pub fn as_bytes(&self) -> &[u8] {
&self.data
}
/// Consume this builder and return the firmware blob.
pub fn into_vec(self) -> Vec<u8> {
self.data
}
/// Returns true if the firmware is at least `MIN_FIRMWARE_SIZE`.
pub fn is_valid_size(&self) -> bool {
self.data.len() >= MIN_FIRMWARE_SIZE
}
/// Verify the PK11 magic in the firmware is correct.
pub fn verify_pk11_magic(&self) -> bool {
if self.data.len() < SIG_SIZE + 4 {
return false;
}
let magic_bytes = &self.data[SIG_SIZE..SIG_SIZE + 4];
u32::from_le_bytes([magic_bytes[0], magic_bytes[1], magic_bytes[2], magic_bytes[3]]) == PK11_MAGIC
}
/// Decrypt the Package2 and verify it matches the expected NOP sled.
pub fn verify_roundtrip(&self) -> bool {
if self.data.len() <= SIG_SIZE + PK11_HEADER_SIZE {
return false;
}
let encrypted = &self.data[SIG_SIZE + PK11_HEADER_SIZE..];
let dk = Aes128Key::from_bytes(&self.device_key);
let decrypted = aes_ctr_xor(&dk, &self.iv, encrypted);
decrypted == self.plaintext_payload
}
}
// ── Tests ─────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use crate::security::rsa::generate_test_keypair;
#[test]
fn firmware_builder_produces_valid_size() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw = MinimalFirmware::build(&efuse, &priv_key);
assert!(fw.is_valid_size(), "firmware must be at least MIN_FIRMWARE_SIZE");
// Exact size: SIG_SIZE + PK11_HEADER_SIZE + PAYLOAD_SIZE
assert_eq!(fw.as_bytes().len(), SIG_SIZE + PK11_HEADER_SIZE + PAYLOAD_SIZE);
}
#[test]
fn firmware_builder_pk11_magic_valid() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw = MinimalFirmware::build(&efuse, &priv_key);
assert!(fw.verify_pk11_magic(), "PK11 magic must be valid");
}
#[test]
fn firmware_builder_roundtrip_decrypt() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw = MinimalFirmware::build(&efuse, &priv_key);
assert!(fw.verify_roundtrip(), "roundtrip decrypt must produce original NOP sled");
}
#[test]
fn firmware_builder_payload_is_64_bytes() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw = MinimalFirmware::build(&efuse, &priv_key);
assert_eq!(fw.plaintext_payload.len(), 64, "payload must be 64 bytes (16 NOPs)");
}
#[test]
fn firmware_builder_payload_is_arm64_nops() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw = MinimalFirmware::build(&efuse, &priv_key);
for chunk in fw.plaintext_payload.chunks(4) {
let inst = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
assert_eq!(inst, ARM64_NOP, "every instruction must be an ARMv8 NOP");
}
}
#[test]
fn firmware_builder_encrypted_is_not_plaintext() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw = MinimalFirmware::build(&efuse, &priv_key);
let encrypted = &fw.as_bytes()[SIG_SIZE + PK11_HEADER_SIZE..];
assert_ne!(encrypted, &fw.plaintext_payload[..], "encrypted payload must differ from plaintext");
}
#[test]
fn firmware_builder_deterministic_with_same_keys() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw1 = MinimalFirmware::build(&efuse, &priv_key);
let fw2 = MinimalFirmware::build(&efuse, &priv_key);
assert_eq!(fw1.as_bytes(), fw2.as_bytes(), "same keys must produce same firmware");
}
#[test]
fn firmware_builder_device_key_is_16_bytes() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw = MinimalFirmware::build(&efuse, &priv_key);
assert_eq!(fw.device_key.len(), 16);
assert_ne!(fw.device_key, [0u8; 16], "device key must not be all zeros");
}
#[test]
fn firmware_builder_iv_is_not_all_zeros() {
let efuse = EfuseArray::new();
let (_pub_key, priv_key) = generate_test_keypair();
let fw = MinimalFirmware::build(&efuse, &priv_key);
assert_ne!(fw.iv, [0u8; 16], "CTR IV must not be all zeros");
}
}
+140
View File
@@ -0,0 +1,140 @@
//! Integration tests for Service Manager (sm) via HipcRouter::dispatch_message.
//!
//! These tests prove the full round-trip: raw HIPC bytes → parse → dispatch
//! → sm handler → HandleTable → response, matching the S02 slice demo
//! contract.
use crate::nn::hipc::HipcRouter;
use crate::nn::sm;
// ── Helpers ─────────────────────────────────────────────────────────────
/// Build a minimal HIPC request buffer: 8-byte header + method_id u32 +
/// payload padded to word boundary.
fn build_hipc_message(method_id: u32, payload: &[u8]) -> Vec<u8> {
let total_payload = 4usize + payload.len(); // method_id + payload
let pad = (4 - (total_payload % 4)) % 4;
let raw_words = ((total_payload + pad) / 4) as u32;
let hdr0 = 0u32; // tag=Request, no descriptors
let hdr1 = raw_words & 0x3FF;
let mut buf = Vec::with_capacity(8 + total_payload + pad);
buf.extend_from_slice(&hdr0.to_le_bytes());
buf.extend_from_slice(&hdr1.to_le_bytes());
buf.extend_from_slice(&method_id.to_le_bytes());
buf.extend_from_slice(payload);
buf.resize(8 + total_payload + pad, 0u8);
buf
}
/// Build a RegisterService payload: 4-byte name_len LE + UTF-8 name bytes.
fn register_payload(name: &str) -> Vec<u8> {
let bytes = name.as_bytes();
let mut p = Vec::with_capacity(4 + bytes.len());
p.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
p.extend_from_slice(bytes);
p
}
/// Build a GetServiceHandle payload: 4-byte handle_id LE.
fn get_handle_payload(handle_id: u32) -> Vec<u8> {
handle_id.to_le_bytes().to_vec()
}
// ── Integration Tests ───────────────────────────────────────────────────
#[test]
fn test_register_service_returns_valid_handle() {
// Reset the handle table for isolation.
sm::reset_handle_table();
let mut router = HipcRouter::new();
router.register("sm", 0, sm::handler_register_service);
router.register("sm", 1, sm::handler_get_service_handle);
let buf = build_hipc_message(0, &register_payload("spl"));
let resp = router.dispatch_message(&buf, "sm").unwrap();
assert_eq!(resp.result_code, 0, "RegisterService must return success");
assert_eq!(resp.data.len(), 4, "Response must contain a 4-byte handle_id");
let handle_id = u32::from_le_bytes(resp.data[..4].try_into().unwrap());
assert!(handle_id > 0, "Handle ID must be non-zero (0 is INVALID_HANDLE sentinel)");
}
#[test]
fn test_get_service_handle_returns_name() {
sm::reset_handle_table();
let mut router = HipcRouter::new();
router.register("sm", 0, sm::handler_register_service);
router.register("sm", 1, sm::handler_get_service_handle);
// Register a service first using the direct handler
let reg_resp = sm::handler_register_service(&register_payload("hid"));
assert_eq!(reg_resp.result_code, 0);
let handle_id = u32::from_le_bytes(reg_resp.data[..4].try_into().unwrap());
// Now look it up via dispatch_message
let buf = build_hipc_message(1, &get_handle_payload(handle_id));
let resp = router.dispatch_message(&buf, "sm").unwrap();
assert_eq!(resp.result_code, 0, "GetServiceHandle must return success");
assert!(resp.data.len() >= 4, "Response must contain name_len + name bytes");
let name_len = u32::from_le_bytes(resp.data[..4].try_into().unwrap()) as usize;
assert_eq!(name_len, 3, "Name length must match 'hid' (3 bytes)");
let name = std::str::from_utf8(&resp.data[4..4 + name_len]).unwrap();
assert_eq!(name, "hid");
}
#[test]
fn test_get_service_handle_invalid_id() {
sm::reset_handle_table();
// Call handler directly with a nonexistent handle_id
let resp = sm::handler_get_service_handle(&0xDEAD_BEEFu32.to_le_bytes());
assert_eq!(
resp.result_code,
crate::kernel::handle_table::result::INVALID_HANDLE,
"Nonexistent handle_id must return INVALID_HANDLE (0xD401)"
);
}
#[test]
fn test_dispatch_message_malformed() {
let router = HipcRouter::new();
// Buffer shorter than 8-byte header
let result = router.dispatch_message(&[0xAA, 0xBB], "sm");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::nn::hipc::DispatchError::MalformedMessage
));
}
#[test]
fn test_register_service_roundtrip() {
sm::reset_handle_table();
let mut router = HipcRouter::new();
router.register("sm", 0, sm::handler_register_service);
router.register("sm", 1, sm::handler_get_service_handle);
// Step 1: RegisterService via dispatch_message
let reg_buf = build_hipc_message(0, &register_payload("spl"));
let reg_resp = router.dispatch_message(&reg_buf, "sm").unwrap();
assert_eq!(reg_resp.result_code, 0);
let handle_id = u32::from_le_bytes(reg_resp.data[..4].try_into().unwrap());
assert!(handle_id > 0);
// Step 2: GetServiceHandle via dispatch_message with the handle_id
let get_buf = build_hipc_message(1, &get_handle_payload(handle_id));
let get_resp = router.dispatch_message(&get_buf, "sm").unwrap();
assert_eq!(get_resp.result_code, 0);
// Step 3: Verify the returned name matches original
let name_len = u32::from_le_bytes(get_resp.data[..4].try_into().unwrap()) as usize;
let name = std::str::from_utf8(&get_resp.data[4..4 + name_len]).unwrap();
assert_eq!(name, "spl");
}