diff --git a/Cargo.toml b/Cargo.toml index 5d6cacd..da4c61f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "crates/servers/gatekeeper", "crates/servers/gps-server", "crates/servers/input-server", + "crates/servers/log-server", "crates/servers/keychain-server", "crates/servers/media-index-server", "crates/servers/network-server", diff --git a/crates/servers/log-server/Cargo.toml b/crates/servers/log-server/Cargo.toml new file mode 100644 index 0000000..b477c0b --- /dev/null +++ b/crates/servers/log-server/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "log-server" +version = "0.1.0" +edition = "2024" + +[dependencies] +types = { workspace = true } +server-framework = { workspace = true } diff --git a/crates/servers/log-server/src/lib.rs b/crates/servers/log-server/src/lib.rs new file mode 100644 index 0000000..08873ca --- /dev/null +++ b/crates/servers/log-server/src/lib.rs @@ -0,0 +1,8 @@ +#![no_std] + +pub mod protocol; +pub mod server; + +#[cfg(test)] +#[path = "tests/mod.rs"] +mod tests; diff --git a/crates/servers/log-server/src/protocol.rs b/crates/servers/log-server/src/protocol.rs new file mode 100644 index 0000000..916e128 --- /dev/null +++ b/crates/servers/log-server/src/protocol.rs @@ -0,0 +1,99 @@ +use server_framework::{Decode, Encode}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LogLevel { + Error, + Warn, + Info, + Debug, +} + +#[derive(Debug, PartialEq)] +pub enum LogRequest { + Write { level: LogLevel, tag: u16, payload: usize }, + GetCount, + GetEntry { index: usize }, +} + +#[derive(Debug, PartialEq)] +pub enum LogResponse { + Ok, + Count(usize), + Entry { level: LogLevel, tag: u16, payload: usize }, + NotFound, +} + +fn encode_level(level: LogLevel) -> usize { + match level { + LogLevel::Error => 0, + LogLevel::Warn => 1, + LogLevel::Info => 2, + LogLevel::Debug => 3, + } +} + +fn decode_level(v: usize) -> Option { + match v { + 0 => Some(LogLevel::Error), + 1 => Some(LogLevel::Warn), + 2 => Some(LogLevel::Info), + 3 => Some(LogLevel::Debug), + _ => None, + } +} + +impl Encode for LogRequest { + fn encode(&self) -> [usize; 4] { + match self { + LogRequest::Write { level, tag, payload } => { + [0, encode_level(*level) | ((*tag as usize) << 8), *payload, 0] + } + LogRequest::GetCount => [1, 0, 0, 0], + LogRequest::GetEntry { index } => [2, *index, 0, 0], + } + } +} + +impl Decode for LogRequest { + fn decode(words: [usize; 4]) -> Option { + match words[0] { + 0 => { + let level = decode_level(words[1] & 0xFF)?; + let tag = ((words[1] >> 8) & 0xFFFF) as u16; + Some(LogRequest::Write { level, tag, payload: words[2] }) + } + 1 => Some(LogRequest::GetCount), + 2 => Some(LogRequest::GetEntry { index: words[1] }), + _ => None, + } + } +} + +impl Encode for LogResponse { + fn encode(&self) -> [usize; 4] { + match self { + LogResponse::Ok => [0, 0, 0, 0], + LogResponse::Count(n) => [1, *n, 0, 0], + LogResponse::Entry { level, tag, payload } => { + [2, encode_level(*level) | ((*tag as usize) << 8), *payload, 0] + } + LogResponse::NotFound => [3, 0, 0, 0], + } + } +} + +impl Decode for LogResponse { + fn decode(words: [usize; 4]) -> Option { + match words[0] { + 0 => Some(LogResponse::Ok), + 1 => Some(LogResponse::Count(words[1])), + 2 => { + let level = decode_level(words[1] & 0xFF)?; + let tag = ((words[1] >> 8) & 0xFFFF) as u16; + Some(LogResponse::Entry { level, tag, payload: words[2] }) + } + 3 => Some(LogResponse::NotFound), + _ => None, + } + } +} diff --git a/crates/servers/log-server/src/server.rs b/crates/servers/log-server/src/server.rs new file mode 100644 index 0000000..635f02c --- /dev/null +++ b/crates/servers/log-server/src/server.rs @@ -0,0 +1,68 @@ +use server_framework::Server; + +use crate::protocol::{LogLevel, LogRequest, LogResponse}; + +const MAX_ENTRIES: usize = 128; + +struct LogEntry { + level: LogLevel, + tag: u16, + payload: usize, +} + +pub struct LogServer { + entries: [Option; MAX_ENTRIES], + count: usize, + write_pos: usize, +} + +impl LogServer { + pub fn new() -> Self { + Self { + entries: [const { None }; MAX_ENTRIES], + count: 0, + write_pos: 0, + } + } + + pub fn count(&self) -> usize { + self.count + } +} + +impl Server for LogServer { + type Request = LogRequest; + type Response = LogResponse; + + fn handle(&mut self, request: LogRequest) -> LogResponse { + match request { + LogRequest::Write { level, tag, payload } => { + self.entries[self.write_pos] = Some(LogEntry { level, tag, payload }); + self.write_pos = (self.write_pos + 1) % MAX_ENTRIES; + if self.count < MAX_ENTRIES { + self.count += 1; + } + LogResponse::Ok + } + LogRequest::GetCount => LogResponse::Count(self.count), + LogRequest::GetEntry { index } => { + if index >= self.count { + return LogResponse::NotFound; + } + let actual = if self.count < MAX_ENTRIES { + index + } else { + (self.write_pos + index) % MAX_ENTRIES + }; + match &self.entries[actual] { + Some(entry) => LogResponse::Entry { + level: entry.level, + tag: entry.tag, + payload: entry.payload, + }, + None => LogResponse::NotFound, + } + } + } + } +} diff --git a/crates/servers/log-server/src/tests/mod.rs b/crates/servers/log-server/src/tests/mod.rs new file mode 100644 index 0000000..aa77244 --- /dev/null +++ b/crates/servers/log-server/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod protocol; +mod server; diff --git a/crates/servers/log-server/src/tests/protocol.rs b/crates/servers/log-server/src/tests/protocol.rs new file mode 100644 index 0000000..5ec0d2b --- /dev/null +++ b/crates/servers/log-server/src/tests/protocol.rs @@ -0,0 +1,39 @@ +use crate::protocol::*; +use server_framework::{Decode, Encode}; + +#[test] +fn write_request_roundtrip() { + let req = LogRequest::Write { level: LogLevel::Error, tag: 42, payload: 0xDEAD }; + assert_eq!(LogRequest::decode(req.encode()), Some(req)); +} + +#[test] +fn get_count_roundtrip() { + let req = LogRequest::GetCount; + assert_eq!(LogRequest::decode(req.encode()), Some(req)); +} + +#[test] +fn get_entry_roundtrip() { + let req = LogRequest::GetEntry { index: 7 }; + assert_eq!(LogRequest::decode(req.encode()), Some(req)); +} + +#[test] +fn entry_response_roundtrip() { + let resp = LogResponse::Entry { level: LogLevel::Warn, tag: 10, payload: 123 }; + assert_eq!(LogResponse::decode(resp.encode()), Some(resp)); +} + +#[test] +fn all_levels_roundtrip() { + for level in [LogLevel::Error, LogLevel::Warn, LogLevel::Info, LogLevel::Debug] { + let req = LogRequest::Write { level, tag: 0, payload: 0 }; + assert_eq!(LogRequest::decode(req.encode()), Some(req)); + } +} + +#[test] +fn invalid_request_returns_none() { + assert!(LogRequest::decode([99, 0, 0, 0]).is_none()); +} diff --git a/crates/servers/log-server/src/tests/server.rs b/crates/servers/log-server/src/tests/server.rs new file mode 100644 index 0000000..f0921c4 --- /dev/null +++ b/crates/servers/log-server/src/tests/server.rs @@ -0,0 +1,43 @@ +use crate::protocol::*; +use crate::server::LogServer; +use server_framework::Server; + +#[test] +fn empty_server_has_zero_count() { + let mut server = LogServer::new(); + let resp = server.handle(LogRequest::GetCount); + assert_eq!(resp, LogResponse::Count(0)); +} + +#[test] +fn write_increments_count() { + let mut server = LogServer::new(); + server.handle(LogRequest::Write { level: LogLevel::Info, tag: 0, payload: 1 }); + server.handle(LogRequest::Write { level: LogLevel::Info, tag: 0, payload: 2 }); + let resp = server.handle(LogRequest::GetCount); + assert_eq!(resp, LogResponse::Count(2)); +} + +#[test] +fn get_entry_returns_written_data() { + let mut server = LogServer::new(); + server.handle(LogRequest::Write { level: LogLevel::Error, tag: 5, payload: 0xBEEF }); + let resp = server.handle(LogRequest::GetEntry { index: 0 }); + assert_eq!(resp, LogResponse::Entry { level: LogLevel::Error, tag: 5, payload: 0xBEEF }); +} + +#[test] +fn get_entry_out_of_range_returns_not_found() { + let mut server = LogServer::new(); + let resp = server.handle(LogRequest::GetEntry { index: 0 }); + assert_eq!(resp, LogResponse::NotFound); +} + +#[test] +fn ring_buffer_wraps() { + let mut server = LogServer::new(); + for i in 0..200 { + server.handle(LogRequest::Write { level: LogLevel::Debug, tag: 0, payload: i }); + } + assert_eq!(server.count(), 128); +} diff --git a/crates/servers/permission-manager/Cargo.toml b/crates/servers/permission-manager/Cargo.toml index 58d0f56..cd8f722 100644 --- a/crates/servers/permission-manager/Cargo.toml +++ b/crates/servers/permission-manager/Cargo.toml @@ -4,3 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] +types = { workspace = true } +server-framework = { workspace = true } diff --git a/crates/servers/permission-manager/src/lib.rs b/crates/servers/permission-manager/src/lib.rs index b93cf3f..08873ca 100644 --- a/crates/servers/permission-manager/src/lib.rs +++ b/crates/servers/permission-manager/src/lib.rs @@ -1,14 +1,8 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +#![no_std] + +pub mod protocol; +pub mod server; #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +#[path = "tests/mod.rs"] +mod tests; diff --git a/crates/servers/permission-manager/src/protocol.rs b/crates/servers/permission-manager/src/protocol.rs new file mode 100644 index 0000000..e453ed9 --- /dev/null +++ b/crates/servers/permission-manager/src/protocol.rs @@ -0,0 +1,127 @@ +use server_framework::{Decode, Encode}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CapabilityKind { + Display, + Input, + AudioPlayback, + AudioCapture, + Camera, + Storage, + Network, + Gps, + Bluetooth, +} + +#[derive(Debug, PartialEq)] +pub enum PermissionRequest { + RequestCapability { process: u16, kind: CapabilityKind }, + RevokeCapability { process: u16, kind: CapabilityKind }, + CheckCapability { process: u16, kind: CapabilityKind }, + ListGranted { process: u16 }, +} + +#[derive(Debug, PartialEq)] +pub enum PermissionResponse { + Granted, + Denied, + Revoked, + HasCapability(bool), + GrantedList(u16), +} + +fn encode_kind(kind: CapabilityKind) -> usize { + match kind { + CapabilityKind::Display => 0, + CapabilityKind::Input => 1, + CapabilityKind::AudioPlayback => 2, + CapabilityKind::AudioCapture => 3, + CapabilityKind::Camera => 4, + CapabilityKind::Storage => 5, + CapabilityKind::Network => 6, + CapabilityKind::Gps => 7, + CapabilityKind::Bluetooth => 8, + } +} + +fn decode_kind(v: usize) -> Option { + match v { + 0 => Some(CapabilityKind::Display), + 1 => Some(CapabilityKind::Input), + 2 => Some(CapabilityKind::AudioPlayback), + 3 => Some(CapabilityKind::AudioCapture), + 4 => Some(CapabilityKind::Camera), + 5 => Some(CapabilityKind::Storage), + 6 => Some(CapabilityKind::Network), + 7 => Some(CapabilityKind::Gps), + 8 => Some(CapabilityKind::Bluetooth), + _ => None, + } +} + +impl Encode for PermissionRequest { + fn encode(&self) -> [usize; 4] { + match self { + PermissionRequest::RequestCapability { process, kind } => { + [0, *process as usize, encode_kind(*kind), 0] + } + PermissionRequest::RevokeCapability { process, kind } => { + [1, *process as usize, encode_kind(*kind), 0] + } + PermissionRequest::CheckCapability { process, kind } => { + [2, *process as usize, encode_kind(*kind), 0] + } + PermissionRequest::ListGranted { process } => { + [3, *process as usize, 0, 0] + } + } + } +} + +impl Decode for PermissionRequest { + fn decode(words: [usize; 4]) -> Option { + match words[0] { + 0 => Some(PermissionRequest::RequestCapability { + process: words[1] as u16, + kind: decode_kind(words[2])?, + }), + 1 => Some(PermissionRequest::RevokeCapability { + process: words[1] as u16, + kind: decode_kind(words[2])?, + }), + 2 => Some(PermissionRequest::CheckCapability { + process: words[1] as u16, + kind: decode_kind(words[2])?, + }), + 3 => Some(PermissionRequest::ListGranted { + process: words[1] as u16, + }), + _ => None, + } + } +} + +impl Encode for PermissionResponse { + fn encode(&self) -> [usize; 4] { + match self { + PermissionResponse::Granted => [0, 0, 0, 0], + PermissionResponse::Denied => [1, 0, 0, 0], + PermissionResponse::Revoked => [2, 0, 0, 0], + PermissionResponse::HasCapability(has) => [3, *has as usize, 0, 0], + PermissionResponse::GrantedList(bits) => [4, *bits as usize, 0, 0], + } + } +} + +impl Decode for PermissionResponse { + fn decode(words: [usize; 4]) -> Option { + match words[0] { + 0 => Some(PermissionResponse::Granted), + 1 => Some(PermissionResponse::Denied), + 2 => Some(PermissionResponse::Revoked), + 3 => Some(PermissionResponse::HasCapability(words[1] != 0)), + 4 => Some(PermissionResponse::GrantedList(words[1] as u16)), + _ => None, + } + } +} diff --git a/crates/servers/permission-manager/src/server.rs b/crates/servers/permission-manager/src/server.rs new file mode 100644 index 0000000..e5c52ec --- /dev/null +++ b/crates/servers/permission-manager/src/server.rs @@ -0,0 +1,82 @@ +use server_framework::Server; + +use crate::protocol::{CapabilityKind, PermissionRequest, PermissionResponse}; + +const MAX_PROCESSES: usize = 64; +const NUM_KINDS: usize = 9; + +pub struct PermissionManager { + grants: [[bool; NUM_KINDS]; MAX_PROCESSES], +} + +impl PermissionManager { + pub fn new() -> Self { + Self { + grants: [[false; NUM_KINDS]; MAX_PROCESSES], + } + } + + fn kind_index(kind: CapabilityKind) -> usize { + match kind { + CapabilityKind::Display => 0, + CapabilityKind::Input => 1, + CapabilityKind::AudioPlayback => 2, + CapabilityKind::AudioCapture => 3, + CapabilityKind::Camera => 4, + CapabilityKind::Storage => 5, + CapabilityKind::Network => 6, + CapabilityKind::Gps => 7, + CapabilityKind::Bluetooth => 8, + } + } + + fn granted_bits(&self, process: u16) -> u16 { + let idx = process as usize; + if idx >= MAX_PROCESSES { + return 0; + } + let mut bits = 0u16; + for i in 0..NUM_KINDS { + if self.grants[idx][i] { + bits |= 1 << i; + } + } + bits + } +} + +impl Server for PermissionManager { + type Request = PermissionRequest; + type Response = PermissionResponse; + + fn handle(&mut self, request: PermissionRequest) -> PermissionResponse { + match request { + PermissionRequest::RequestCapability { process, kind } => { + let idx = process as usize; + if idx >= MAX_PROCESSES { + return PermissionResponse::Denied; + } + self.grants[idx][Self::kind_index(kind)] = true; + PermissionResponse::Granted + } + PermissionRequest::RevokeCapability { process, kind } => { + let idx = process as usize; + if idx >= MAX_PROCESSES { + return PermissionResponse::Denied; + } + self.grants[idx][Self::kind_index(kind)] = false; + PermissionResponse::Revoked + } + PermissionRequest::CheckCapability { process, kind } => { + let idx = process as usize; + if idx >= MAX_PROCESSES { + return PermissionResponse::HasCapability(false); + } + PermissionResponse::HasCapability(self.grants[idx][Self::kind_index(kind)]) + } + PermissionRequest::ListGranted { process } => { + PermissionResponse::GrantedList(self.granted_bits(process)) + } + } + } +} diff --git a/crates/servers/permission-manager/src/tests/mod.rs b/crates/servers/permission-manager/src/tests/mod.rs new file mode 100644 index 0000000..aa77244 --- /dev/null +++ b/crates/servers/permission-manager/src/tests/mod.rs @@ -0,0 +1,2 @@ +mod protocol; +mod server; diff --git a/crates/servers/permission-manager/src/tests/protocol.rs b/crates/servers/permission-manager/src/tests/protocol.rs new file mode 100644 index 0000000..f0cd095 --- /dev/null +++ b/crates/servers/permission-manager/src/tests/protocol.rs @@ -0,0 +1,59 @@ +use crate::protocol::*; +use server_framework::{Decode, Encode}; + +#[test] +fn request_capability_roundtrip() { + let req = PermissionRequest::RequestCapability { process: 5, kind: CapabilityKind::Camera }; + assert_eq!(PermissionRequest::decode(req.encode()), Some(req)); +} + +#[test] +fn revoke_capability_roundtrip() { + let req = PermissionRequest::RevokeCapability { process: 3, kind: CapabilityKind::Network }; + assert_eq!(PermissionRequest::decode(req.encode()), Some(req)); +} + +#[test] +fn check_capability_roundtrip() { + let req = PermissionRequest::CheckCapability { process: 1, kind: CapabilityKind::Storage }; + assert_eq!(PermissionRequest::decode(req.encode()), Some(req)); +} + +#[test] +fn list_granted_roundtrip() { + let req = PermissionRequest::ListGranted { process: 7 }; + assert_eq!(PermissionRequest::decode(req.encode()), Some(req)); +} + +#[test] +fn all_kinds_roundtrip() { + let kinds = [ + CapabilityKind::Display, CapabilityKind::Input, CapabilityKind::AudioPlayback, + CapabilityKind::AudioCapture, CapabilityKind::Camera, CapabilityKind::Storage, + CapabilityKind::Network, CapabilityKind::Gps, CapabilityKind::Bluetooth, + ]; + for kind in kinds { + let req = PermissionRequest::RequestCapability { process: 0, kind }; + assert_eq!(PermissionRequest::decode(req.encode()), Some(req)); + } +} + +#[test] +fn response_roundtrips() { + let responses = [ + PermissionResponse::Granted, + PermissionResponse::Denied, + PermissionResponse::Revoked, + PermissionResponse::HasCapability(true), + PermissionResponse::HasCapability(false), + PermissionResponse::GrantedList(0b101), + ]; + for resp in responses { + assert_eq!(PermissionResponse::decode(resp.encode()), Some(resp)); + } +} + +#[test] +fn invalid_request_returns_none() { + assert!(PermissionRequest::decode([99, 0, 0, 0]).is_none()); +} diff --git a/crates/servers/permission-manager/src/tests/server.rs b/crates/servers/permission-manager/src/tests/server.rs new file mode 100644 index 0000000..4c7c1bd --- /dev/null +++ b/crates/servers/permission-manager/src/tests/server.rs @@ -0,0 +1,85 @@ +use crate::protocol::*; +use crate::server::PermissionManager; +use server_framework::Server; + +#[test] +fn new_manager_has_no_grants() { + let mut mgr = PermissionManager::new(); + let resp = mgr.handle(PermissionRequest::CheckCapability { + process: 0, + kind: CapabilityKind::Camera, + }); + assert_eq!(resp, PermissionResponse::HasCapability(false)); +} + +#[test] +fn grant_then_check() { + let mut mgr = PermissionManager::new(); + mgr.handle(PermissionRequest::RequestCapability { + process: 1, + kind: CapabilityKind::Network, + }); + let resp = mgr.handle(PermissionRequest::CheckCapability { + process: 1, + kind: CapabilityKind::Network, + }); + assert_eq!(resp, PermissionResponse::HasCapability(true)); +} + +#[test] +fn grant_does_not_affect_other_process() { + let mut mgr = PermissionManager::new(); + mgr.handle(PermissionRequest::RequestCapability { + process: 1, + kind: CapabilityKind::Camera, + }); + let resp = mgr.handle(PermissionRequest::CheckCapability { + process: 2, + kind: CapabilityKind::Camera, + }); + assert_eq!(resp, PermissionResponse::HasCapability(false)); +} + +#[test] +fn revoke_removes_grant() { + let mut mgr = PermissionManager::new(); + mgr.handle(PermissionRequest::RequestCapability { + process: 0, + kind: CapabilityKind::Storage, + }); + mgr.handle(PermissionRequest::RevokeCapability { + process: 0, + kind: CapabilityKind::Storage, + }); + let resp = mgr.handle(PermissionRequest::CheckCapability { + process: 0, + kind: CapabilityKind::Storage, + }); + assert_eq!(resp, PermissionResponse::HasCapability(false)); +} + +#[test] +fn list_granted_returns_bitfield() { + let mut mgr = PermissionManager::new(); + mgr.handle(PermissionRequest::RequestCapability { + process: 0, + kind: CapabilityKind::Display, + }); + mgr.handle(PermissionRequest::RequestCapability { + process: 0, + kind: CapabilityKind::Network, + }); + let resp = mgr.handle(PermissionRequest::ListGranted { process: 0 }); + // Display=bit 0, Network=bit 6 → 0b1000001 = 65 + assert_eq!(resp, PermissionResponse::GrantedList(0b1000001)); +} + +#[test] +fn out_of_range_process_denied() { + let mut mgr = PermissionManager::new(); + let resp = mgr.handle(PermissionRequest::RequestCapability { + process: 999, + kind: CapabilityKind::Camera, + }); + assert_eq!(resp, PermissionResponse::Denied); +}