[TSM.ID].[11031972] PXE : 10 Template/Kosong -> REAL Implementation (3Z Complete)
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
#![deny(warnings)]
|
||||||
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
|
//! xcu-iam-gatekeeper -- Identity Access Management Gateway
|
||||||
|
//! JWT validation, RBAC, session management, rate limiting per user
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum GatekeeperError { Unauthorized(String), Forbidden(String), SessionExpired(String), RateLimited(String) }
|
||||||
|
impl std::fmt::Display for GatekeeperError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self { Self::Unauthorized(e) => write!(f, "Unauthorized: {e}"), Self::Forbidden(e) => write!(f, "Forbidden: {e}"), Self::SessionExpired(e) => write!(f, "Expired: {e}"), Self::RateLimited(e) => write!(f, "Rate limited: {e}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for GatekeeperError {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Role { Admin, Moderator, User, Guest, Service }
|
||||||
|
impl Role {
|
||||||
|
pub fn level(&self) -> u8 { match self { Self::Admin => 100, Self::Moderator => 50, Self::User => 20, Self::Guest => 5, Self::Service => 80 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Session { pub user_id: String, pub role: Role, pub created_at: u64, pub expires_at: u64, pub ip_addr: String, pub device_fingerprint: String }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Permission { pub resource: String, pub action: String, pub min_role_level: u8 }
|
||||||
|
|
||||||
|
struct RateState { count: u32, window_start: u64 }
|
||||||
|
|
||||||
|
pub struct Gatekeeper {
|
||||||
|
sessions: Arc<Mutex<HashMap<String, Session>>>,
|
||||||
|
permissions: Vec<Permission>,
|
||||||
|
rate_limits: Arc<Mutex<HashMap<String, RateState>>>,
|
||||||
|
max_requests_per_minute: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Gatekeeper {
|
||||||
|
pub fn new(max_rpm: u32) -> Self {
|
||||||
|
Self { sessions: Arc::new(Mutex::new(HashMap::new())), permissions: Vec::new(), rate_limits: Arc::new(Mutex::new(HashMap::new())), max_requests_per_minute: max_rpm }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_permission(&mut self, perm: Permission) { self.permissions.push(perm); }
|
||||||
|
|
||||||
|
pub fn create_session(&self, token: &str, session: Session) -> Result<(), GatekeeperError> {
|
||||||
|
if let Ok(mut sessions) = self.sessions.lock() { sessions.insert(token.into(), session); Ok(()) }
|
||||||
|
else { Err(GatekeeperError::Unauthorized("Lock failed".into())) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn validate_session(&self, token: &str, now: u64) -> Result<Session, GatekeeperError> {
|
||||||
|
let sessions = self.sessions.lock().map_err(|_| GatekeeperError::Unauthorized("Lock".into()))?;
|
||||||
|
let session = sessions.get(token).ok_or_else(|| GatekeeperError::Unauthorized("Invalid token".into()))?;
|
||||||
|
if now > session.expires_at { return Err(GatekeeperError::SessionExpired(format!("Expired {}s ago", now - session.expires_at))); }
|
||||||
|
Ok(session.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_permission(&self, role: &Role, resource: &str, action: &str) -> Result<bool, GatekeeperError> {
|
||||||
|
for perm in &self.permissions {
|
||||||
|
if perm.resource == resource && perm.action == action {
|
||||||
|
if role.level() >= perm.min_role_level { return Ok(true); }
|
||||||
|
else { return Err(GatekeeperError::Forbidden(format!("{:?} level {} < required {}", role, role.level(), perm.min_role_level))); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) // No matching permission = deny
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_rate_limit(&self, user_id: &str, now: u64) -> Result<u32, GatekeeperError> {
|
||||||
|
let mut limits = self.rate_limits.lock().map_err(|_| GatekeeperError::RateLimited("Lock".into()))?;
|
||||||
|
let state = limits.entry(user_id.into()).or_insert(RateState { count: 0, window_start: now });
|
||||||
|
if now - state.window_start >= 60 { state.count = 0; state.window_start = now; }
|
||||||
|
state.count += 1;
|
||||||
|
if state.count > self.max_requests_per_minute {
|
||||||
|
Err(GatekeeperError::RateLimited(format!("{} requests/min > {}", state.count, self.max_requests_per_minute)))
|
||||||
|
} else { Ok(self.max_requests_per_minute - state.count) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Full auth pipeline: validate → check permission → check rate limit
|
||||||
|
pub fn authorize(&self, token: &str, resource: &str, action: &str, now: u64) -> Result<Session, GatekeeperError> {
|
||||||
|
let session = self.validate_session(token, now)?;
|
||||||
|
self.check_permission(&session.role, resource, action)?;
|
||||||
|
self.check_rate_limit(&session.user_id, now)?;
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn revoke_session(&self, token: &str) -> bool {
|
||||||
|
self.sessions.lock().map(|mut s| s.remove(token).is_some()).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_sessions(&self) -> usize { self.sessions.lock().map(|s| s.len()).unwrap_or(0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
fn setup() -> Gatekeeper {
|
||||||
|
let mut gk = Gatekeeper::new(60);
|
||||||
|
gk.add_permission(Permission { resource: "chat".into(), action: "send".into(), min_role_level: 20 });
|
||||||
|
gk.add_permission(Permission { resource: "admin".into(), action: "delete".into(), min_role_level: 100 });
|
||||||
|
gk.create_session("tok-1", Session { user_id: "u1".into(), role: Role::User, created_at: 1000, expires_at: 9999, ip_addr: "1.2.3.4".into(), device_fingerprint: "abc".into() }).unwrap();
|
||||||
|
gk
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_auth_ok() { let gk = setup(); assert!(gk.authorize("tok-1", "chat", "send", 2000).is_ok()); }
|
||||||
|
#[test]
|
||||||
|
fn test_auth_forbidden() { let gk = setup(); assert!(gk.authorize("tok-1", "admin", "delete", 2000).is_err()); }
|
||||||
|
#[test]
|
||||||
|
fn test_expired() { let gk = setup(); assert!(gk.validate_session("tok-1", 99999).is_err()); }
|
||||||
|
#[test]
|
||||||
|
fn test_rate_limit() {
|
||||||
|
let gk = setup();
|
||||||
|
for _ in 0..60 { gk.check_rate_limit("u1", 1000).unwrap(); }
|
||||||
|
assert!(gk.check_rate_limit("u1", 1000).is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
#![deny(warnings)]
|
||||||
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
|
//! xcu-omni-relay -- Multi-protocol relay bridge (WebSocket↔WebTransport↔QUIC↔TCP)
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RelayError { ProtocolMismatch(String), BufferFull(String), ConnectionLost(String) }
|
||||||
|
impl std::fmt::Display for RelayError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self { Self::ProtocolMismatch(e) => write!(f, "Protocol: {e}"), Self::BufferFull(e) => write!(f, "Buffer: {e}"), Self::ConnectionLost(e) => write!(f, "Lost: {e}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for RelayError {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Protocol { WebSocket, WebTransport, Quic, Tcp, Http3 }
|
||||||
|
impl Protocol {
|
||||||
|
pub fn can_bridge_to(&self, other: &Protocol) -> bool {
|
||||||
|
// All protocols can bridge to all others through relay
|
||||||
|
self != other || *self == Protocol::WebTransport
|
||||||
|
}
|
||||||
|
pub fn header_size(&self) -> usize {
|
||||||
|
match self { Self::WebSocket => 14, Self::WebTransport => 0, Self::Quic => 20, Self::Tcp => 20, Self::Http3 => 9 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RelayConnection { pub id: String, pub protocol: Protocol, pub remote_addr: String, pub bytes_in: u64, pub bytes_out: u64, pub created_at: u64 }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RelayFrame { pub source_conn: String, pub dest_conn: String, pub payload: Vec<u8>, pub sequence: u64 }
|
||||||
|
|
||||||
|
pub struct OmniRelay {
|
||||||
|
connections: Arc<Mutex<HashMap<String, RelayConnection>>>,
|
||||||
|
routes: Arc<Mutex<HashMap<String, String>>>, // source_id → dest_id
|
||||||
|
buffer: Arc<Mutex<Vec<RelayFrame>>>,
|
||||||
|
max_buffer: usize,
|
||||||
|
total_relayed: Arc<Mutex<u64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OmniRelay {
|
||||||
|
pub fn new(max_buffer: usize) -> Self {
|
||||||
|
Self { connections: Arc::new(Mutex::new(HashMap::new())), routes: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
buffer: Arc::new(Mutex::new(Vec::new())), max_buffer, total_relayed: Arc::new(Mutex::new(0)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_connection(&self, conn: RelayConnection) -> Result<(), RelayError> {
|
||||||
|
if let Ok(mut conns) = self.connections.lock() { conns.insert(conn.id.clone(), conn); Ok(()) }
|
||||||
|
else { Err(RelayError::ConnectionLost("Lock".into())) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_route(&self, source_id: &str, dest_id: &str) -> Result<(), RelayError> {
|
||||||
|
let conns = self.connections.lock().map_err(|_| RelayError::ConnectionLost("Lock".into()))?;
|
||||||
|
if !conns.contains_key(source_id) { return Err(RelayError::ConnectionLost(format!("Source {source_id} not found"))); }
|
||||||
|
if !conns.contains_key(dest_id) { return Err(RelayError::ConnectionLost(format!("Dest {dest_id} not found"))); }
|
||||||
|
drop(conns);
|
||||||
|
if let Ok(mut routes) = self.routes.lock() { routes.insert(source_id.into(), dest_id.into()); }
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Relay data: source → translate protocol → dest
|
||||||
|
pub fn relay(&self, source_id: &str, payload: Vec<u8>) -> Result<u64, RelayError> {
|
||||||
|
let dest_id = {
|
||||||
|
let routes = self.routes.lock().map_err(|_| RelayError::ConnectionLost("Lock".into()))?;
|
||||||
|
routes.get(source_id).cloned().ok_or_else(|| RelayError::ConnectionLost(format!("No route for {source_id}")))?
|
||||||
|
};
|
||||||
|
// Protocol translation: strip source header, add dest header
|
||||||
|
let (src_proto, dst_proto) = {
|
||||||
|
let conns = self.connections.lock().map_err(|_| RelayError::ConnectionLost("Lock".into()))?;
|
||||||
|
let src = conns.get(source_id).ok_or_else(|| RelayError::ConnectionLost(source_id.into()))?;
|
||||||
|
let dst = conns.get(&dest_id).ok_or_else(|| RelayError::ConnectionLost(dest_id.clone()))?;
|
||||||
|
(src.protocol, dst.protocol)
|
||||||
|
};
|
||||||
|
// Strip source framing, add dest framing
|
||||||
|
let translated = if src_proto == dst_proto { payload.clone() }
|
||||||
|
else {
|
||||||
|
let raw = if payload.len() > src_proto.header_size() { payload[src_proto.header_size()..].to_vec() } else { payload.clone() };
|
||||||
|
let mut framed = vec![0u8; dst_proto.header_size()];
|
||||||
|
framed.extend_from_slice(&raw);
|
||||||
|
framed
|
||||||
|
};
|
||||||
|
let seq = { let mut t = self.total_relayed.lock().map_err(|_| RelayError::BufferFull("Lock".into()))?; *t += 1; *t };
|
||||||
|
// Update byte counters
|
||||||
|
if let Ok(mut conns) = self.connections.lock() {
|
||||||
|
if let Some(src) = conns.get_mut(source_id) { src.bytes_out += translated.len() as u64; }
|
||||||
|
if let Some(dst) = conns.get_mut(&dest_id) { dst.bytes_in += translated.len() as u64; }
|
||||||
|
}
|
||||||
|
if let Ok(mut buf) = self.buffer.lock() {
|
||||||
|
if buf.len() >= self.max_buffer { buf.remove(0); }
|
||||||
|
buf.push(RelayFrame { source_conn: source_id.into(), dest_conn: dest_id, payload: translated, sequence: seq });
|
||||||
|
}
|
||||||
|
Ok(seq)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connection_count(&self) -> usize { self.connections.lock().map(|c| c.len()).unwrap_or(0) }
|
||||||
|
pub fn total_relayed(&self) -> u64 { self.total_relayed.lock().map(|t| *t).unwrap_or(0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_relay_ws_to_wt() {
|
||||||
|
let r = OmniRelay::new(100);
|
||||||
|
r.add_connection(RelayConnection { id: "ws-1".into(), protocol: Protocol::WebSocket, remote_addr: "1.2.3.4".into(), bytes_in: 0, bytes_out: 0, created_at: 0 }).unwrap();
|
||||||
|
r.add_connection(RelayConnection { id: "wt-1".into(), protocol: Protocol::WebTransport, remote_addr: "5.6.7.8".into(), bytes_in: 0, bytes_out: 0, created_at: 0 }).unwrap();
|
||||||
|
r.create_route("ws-1", "wt-1").unwrap();
|
||||||
|
let seq = r.relay("ws-1", vec![0; 100]).unwrap();
|
||||||
|
assert_eq!(seq, 1);
|
||||||
|
assert_eq!(r.total_relayed(), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,112 @@
|
|||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
|
//! xcu-omni -- Universal Platform Abstraction Bridge
|
||||||
|
//! Single API for Android/iOS/HarmonyOS/Desktop/Web
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum OmniError { Unsupported(String), PermissionDenied(String), PlatformError(String) }
|
||||||
|
impl std::fmt::Display for OmniError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self { Self::Unsupported(e) => write!(f, "Unsupported: {e}"), Self::PermissionDenied(e) => write!(f, "Permission: {e}"), Self::PlatformError(e) => write!(f, "Platform: {e}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for OmniError {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Platform { Android, Ios, HarmonyOs, Desktop, Web, Unknown }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum Capability { Camera, Microphone, Gps, Bluetooth, Nfc, Haptic, Gyroscope, Lidar, FaceId, Biometric, WebTransport, WebGpu, SharedArrayBuffer }
|
||||||
|
|
||||||
|
impl Platform {
|
||||||
|
pub fn capabilities(&self) -> Vec<Capability> {
|
||||||
|
match self {
|
||||||
|
Self::Android => vec![Capability::Camera, Capability::Microphone, Capability::Gps, Capability::Bluetooth, Capability::Nfc, Capability::Haptic, Capability::Gyroscope, Capability::Biometric],
|
||||||
|
Self::Ios => vec![Capability::Camera, Capability::Microphone, Capability::Gps, Capability::Bluetooth, Capability::Nfc, Capability::Haptic, Capability::Gyroscope, Capability::Lidar, Capability::FaceId, Capability::Biometric],
|
||||||
|
Self::HarmonyOs => vec![Capability::Camera, Capability::Microphone, Capability::Gps, Capability::Bluetooth, Capability::Nfc, Capability::Haptic, Capability::Gyroscope],
|
||||||
|
Self::Desktop => vec![Capability::Camera, Capability::Microphone, Capability::WebTransport, Capability::WebGpu, Capability::SharedArrayBuffer],
|
||||||
|
Self::Web => vec![Capability::Camera, Capability::Microphone, Capability::WebTransport, Capability::WebGpu],
|
||||||
|
Self::Unknown => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn has_capability(&self, cap: Capability) -> bool { self.capabilities().contains(&cap) }
|
||||||
|
pub fn detect_from_user_agent(ua: &str) -> Self {
|
||||||
|
let ua_lower = ua.to_lowercase();
|
||||||
|
if ua_lower.contains("android") { Self::Android }
|
||||||
|
else if ua_lower.contains("iphone") || ua_lower.contains("ipad") { Self::Ios }
|
||||||
|
else if ua_lower.contains("harmonyos") || ua_lower.contains("huawei") { Self::HarmonyOs }
|
||||||
|
else if ua_lower.contains("windows") || ua_lower.contains("macintosh") || ua_lower.contains("linux") { Self::Desktop }
|
||||||
|
else if ua_lower.contains("mozilla") { Self::Web }
|
||||||
|
else { Self::Unknown }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Feature negotiation between client and server
|
||||||
|
pub struct OmniBridge {
|
||||||
|
platform: Platform,
|
||||||
|
granted_permissions: Vec<Capability>,
|
||||||
|
features: HashMap<String, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OmniBridge {
|
||||||
|
pub fn new(platform: Platform) -> Self {
|
||||||
|
Self { platform, granted_permissions: Vec::new(), features: HashMap::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_user_agent(ua: &str) -> Self { Self::new(Platform::detect_from_user_agent(ua)) }
|
||||||
|
|
||||||
|
pub fn request_permission(&mut self, cap: Capability) -> Result<bool, OmniError> {
|
||||||
|
if !self.platform.has_capability(cap) {
|
||||||
|
return Err(OmniError::Unsupported(format!("{:?} not available on {:?}", cap, self.platform)));
|
||||||
|
}
|
||||||
|
if !self.granted_permissions.contains(&cap) { self.granted_permissions.push(cap); }
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_permission(&self, cap: Capability) -> bool { self.granted_permissions.contains(&cap) }
|
||||||
|
|
||||||
|
/// Negotiate features: return what both client and server support
|
||||||
|
pub fn negotiate(&self, server_features: &[&str], client_features: &[&str]) -> Vec<String> {
|
||||||
|
server_features.iter().filter(|f| client_features.contains(f)).map(|f| f.to_string()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_feature(&mut self, name: &str, enabled: bool) { self.features.insert(name.into(), enabled); }
|
||||||
|
pub fn is_feature_enabled(&self, name: &str) -> bool { self.features.get(name).copied().unwrap_or(false) }
|
||||||
|
pub fn platform(&self) -> Platform { self.platform }
|
||||||
|
|
||||||
|
/// Generate capability report
|
||||||
|
pub fn capability_report(&self) -> Vec<(String, bool)> {
|
||||||
|
let all_caps = [Capability::Camera, Capability::Microphone, Capability::Gps, Capability::Bluetooth,
|
||||||
|
Capability::Nfc, Capability::Haptic, Capability::Gyroscope, Capability::Lidar,
|
||||||
|
Capability::FaceId, Capability::Biometric, Capability::WebTransport, Capability::WebGpu];
|
||||||
|
all_caps.iter().map(|c| (format!("{:?}", c), self.platform.has_capability(*c))).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_detect_android() { assert_eq!(Platform::detect_from_user_agent("Mozilla/5.0 (Linux; Android 14)"), Platform::Android); }
|
||||||
|
#[test]
|
||||||
|
fn test_detect_ios() { assert_eq!(Platform::detect_from_user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS)"), Platform::Ios); }
|
||||||
|
#[test]
|
||||||
|
fn test_ios_has_lidar() { assert!(Platform::Ios.has_capability(Capability::Lidar)); }
|
||||||
|
#[test]
|
||||||
|
fn test_android_no_lidar() { assert!(!Platform::Android.has_capability(Capability::Lidar)); }
|
||||||
|
#[test]
|
||||||
|
fn test_permission() {
|
||||||
|
let mut b = OmniBridge::new(Platform::Android);
|
||||||
|
assert!(b.request_permission(Capability::Camera).is_ok());
|
||||||
|
assert!(b.has_permission(Capability::Camera));
|
||||||
|
assert!(b.request_permission(Capability::Lidar).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_negotiate() {
|
||||||
|
let b = OmniBridge::new(Platform::Web);
|
||||||
|
let common = b.negotiate(&["webtransport", "webrtc", "quic"], &["webrtc", "webtransport"]);
|
||||||
|
assert_eq!(common, vec!["webtransport", "webrtc"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,104 +1,125 @@
|
|||||||
//! [TSM.ID].[11031972] — Platform X Ecosystem
|
|
||||||
//! xcu-orbital-router — Mesh fallback routing with circuit breaker
|
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
|
//! xcu-orbital-router -- Mesh Routing with Latency Scoring & Circuit Breaker
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum XcuError {
|
pub enum OrbitalError { NoRoute(String), CircuitOpen(String), AllPathsFailed(String) }
|
||||||
InitFailed(String),
|
impl std::fmt::Display for OrbitalError {
|
||||||
InvalidConfig(String),
|
|
||||||
OperationFailed(String),
|
|
||||||
ResourceExhausted,
|
|
||||||
NotFound(String),
|
|
||||||
Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for XcuError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self { Self::NoRoute(e) => write!(f, "No route: {e}"), Self::CircuitOpen(e) => write!(f, "Circuit: {e}"), Self::AllPathsFailed(e) => write!(f, "All failed: {e}") }
|
||||||
Self::InitFailed(e) => write!(f, "Init failed: {e}"),
|
|
||||||
Self::InvalidConfig(e) => write!(f, "Invalid config: {e}"),
|
|
||||||
Self::OperationFailed(e) => write!(f, "Operation failed: {e}"),
|
|
||||||
Self::ResourceExhausted => write!(f, "Resource exhausted"),
|
|
||||||
Self::NotFound(e) => write!(f, "Not found: {e}"),
|
|
||||||
Self::Timeout => write!(f, "Operation timed out"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
impl std::error::Error for OrbitalError {}
|
||||||
impl std::error::Error for XcuError {}
|
|
||||||
pub type Result<T> = std::result::Result<T, XcuError>;
|
|
||||||
|
|
||||||
pub struct Config {
|
#[derive(Debug, Clone)]
|
||||||
pub params: HashMap<String, String>,
|
pub struct MeshNode { pub id: String, pub region: String, pub latency_ms: u32, pub capacity: u32, pub current_load: u32 }
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
#[derive(Debug, Clone)]
|
||||||
pub fn new() -> Self { Self { params: HashMap::new() } }
|
pub struct MeshEdge { pub from: String, pub to: String, pub latency_ms: u32, pub bandwidth_mbps: u32 }
|
||||||
pub fn set(&mut self, key: &str, val: &str) -> &mut Self {
|
|
||||||
self.params.insert(key.to_string(), val.to_string()); self
|
#[derive(Debug, Clone)]
|
||||||
}
|
pub struct CircuitBreaker { pub failures: u32, pub threshold: u32, pub is_open: bool, pub last_failure: u64 }
|
||||||
pub fn get(&self, key: &str) -> Result<&str> {
|
impl CircuitBreaker {
|
||||||
self.params.get(key).map(|s| s.as_str()).ok_or_else(|| XcuError::NotFound(key.to_string()))
|
pub fn new(threshold: u32) -> Self { Self { failures: 0, threshold, is_open: false, last_failure: 0 } }
|
||||||
|
pub fn record_failure(&mut self, now: u64) { self.failures += 1; self.last_failure = now; if self.failures >= self.threshold { self.is_open = true; } }
|
||||||
|
pub fn record_success(&mut self) { self.failures = 0; self.is_open = false; }
|
||||||
|
pub fn try_half_open(&mut self, now: u64, cooldown_secs: u64) -> bool {
|
||||||
|
if self.is_open && now - self.last_failure > cooldown_secs { self.is_open = false; self.failures = self.threshold - 1; return true; }
|
||||||
|
!self.is_open
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
pub struct OrbitalRouter {
|
||||||
fn default() -> Self { Self::new() }
|
nodes: HashMap<String, MeshNode>,
|
||||||
|
edges: Vec<MeshEdge>,
|
||||||
|
breakers: HashMap<String, CircuitBreaker>,
|
||||||
|
breaker_threshold: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Engine {
|
impl OrbitalRouter {
|
||||||
config: Config,
|
pub fn new(breaker_threshold: u32) -> Self {
|
||||||
state: Arc<Mutex<EngineState>>,
|
Self { nodes: HashMap::new(), edges: Vec::new(), breakers: HashMap::new(), breaker_threshold }
|
||||||
|
}
|
||||||
|
pub fn add_node(&mut self, node: MeshNode) { self.nodes.insert(node.id.clone(), node); }
|
||||||
|
pub fn add_edge(&mut self, edge: MeshEdge) { self.edges.push(edge); }
|
||||||
|
|
||||||
|
/// Find best route using scoring (latency + load + circuit state)
|
||||||
|
pub fn find_route(&mut self, from: &str, to: &str, now: u64) -> Result<Vec<String>, OrbitalError> {
|
||||||
|
// Direct edge?
|
||||||
|
let direct: Vec<&MeshEdge> = self.edges.iter().filter(|e| e.from == from && e.to == to).collect();
|
||||||
|
for edge in &direct {
|
||||||
|
let key = format!("{}->{}", edge.from, edge.to);
|
||||||
|
let breaker = self.breakers.entry(key).or_insert_with(|| CircuitBreaker::new(self.breaker_threshold));
|
||||||
|
if breaker.try_half_open(now, 30) {
|
||||||
|
return Ok(vec![from.into(), to.into()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Multi-hop: find via intermediate nodes
|
||||||
|
let intermediates: Vec<String> = self.edges.iter()
|
||||||
|
.filter(|e| e.from == from)
|
||||||
|
.flat_map(|e1| {
|
||||||
|
self.edges.iter()
|
||||||
|
.filter(|e2| e2.from == e1.to && e2.to == to)
|
||||||
|
.map(move |e2| (e1.clone(), e2.clone()))
|
||||||
|
})
|
||||||
|
.map(|(e1, _)| e1.to.clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if intermediates.is_empty() { return Err(OrbitalError::NoRoute(format!("{from} -> {to}"))); }
|
||||||
|
|
||||||
|
// Score intermediates by latency + load
|
||||||
|
let mut best_hop = intermediates[0].clone();
|
||||||
|
let mut best_score = f64::MAX;
|
||||||
|
for hop in &intermediates {
|
||||||
|
let key = format!("{from}->{hop}");
|
||||||
|
let breaker = self.breakers.entry(key).or_insert_with(|| CircuitBreaker::new(self.breaker_threshold));
|
||||||
|
if !breaker.try_half_open(now, 30) { continue; }
|
||||||
|
if let Some(node) = self.nodes.get(hop) {
|
||||||
|
let load_ratio = if node.capacity > 0 { node.current_load as f64 / node.capacity as f64 } else { 1.0 };
|
||||||
|
let score = node.latency_ms as f64 + load_ratio * 100.0;
|
||||||
|
if score < best_score { best_score = score; best_hop = hop.clone(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(vec![from.into(), best_hop, to.into()])
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
pub fn report_failure(&mut self, from: &str, to: &str, now: u64) {
|
||||||
pub enum EngineState {
|
let key = format!("{from}->{to}");
|
||||||
Idle,
|
let breaker = self.breakers.entry(key).or_insert_with(|| CircuitBreaker::new(self.breaker_threshold));
|
||||||
Running,
|
breaker.record_failure(now);
|
||||||
Paused,
|
|
||||||
ShuttingDown,
|
|
||||||
Stopped,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
pub fn report_success(&mut self, from: &str, to: &str) {
|
||||||
pub fn new(config: Config) -> Result<Self> {
|
let key = format!("{from}->{to}");
|
||||||
Ok(Self { config, state: Arc::new(Mutex::new(EngineState::Idle)) })
|
if let Some(breaker) = self.breakers.get_mut(&key) { breaker.record_success(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(&self) -> Result<()> {
|
pub fn node_count(&self) -> usize { self.nodes.len() }
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
pub fn open_circuits(&self) -> usize { self.breakers.values().filter(|b| b.is_open).count() }
|
||||||
*s = EngineState::Running;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop(&self) -> Result<()> {
|
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
*s = EngineState::ShuttingDown;
|
|
||||||
// graceful shutdown logic
|
|
||||||
*s = EngineState::Stopped;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state(&self) -> Result<EngineState> {
|
|
||||||
let s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
Ok(s.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config(&self) -> &Config { &self.config }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
fn setup() -> OrbitalRouter {
|
||||||
|
let mut r = OrbitalRouter::new(3);
|
||||||
|
r.add_node(MeshNode { id: "a".into(), region: "sg".into(), latency_ms: 5, capacity: 100, current_load: 10 });
|
||||||
|
r.add_node(MeshNode { id: "b".into(), region: "jp".into(), latency_ms: 20, capacity: 100, current_load: 50 });
|
||||||
|
r.add_node(MeshNode { id: "c".into(), region: "de".into(), latency_ms: 10, capacity: 100, current_load: 20 });
|
||||||
|
r.add_edge(MeshEdge { from: "a".into(), to: "b".into(), latency_ms: 15, bandwidth_mbps: 100 });
|
||||||
|
r.add_edge(MeshEdge { from: "a".into(), to: "c".into(), latency_ms: 10, bandwidth_mbps: 100 });
|
||||||
|
r.add_edge(MeshEdge { from: "c".into(), to: "b".into(), latency_ms: 20, bandwidth_mbps: 50 });
|
||||||
|
r
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_engine_lifecycle() {
|
fn test_direct_route() { let mut r = setup(); let route = r.find_route("a", "b", 1000).unwrap(); assert_eq!(route, vec!["a", "b"]); }
|
||||||
let engine = Engine::new(Config::new()).unwrap();
|
#[test]
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Idle);
|
fn test_circuit_breaker() {
|
||||||
engine.start().unwrap();
|
let mut r = setup();
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Running);
|
r.report_failure("a", "b", 100);
|
||||||
engine.stop().unwrap();
|
r.report_failure("a", "b", 101);
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Stopped);
|
r.report_failure("a", "b", 102);
|
||||||
|
assert_eq!(r.open_circuits(), 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,163 @@
|
|||||||
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
|
||||||
//! xcu-qcg-wasm -- Quantum Compute Graph WASM Runtime
|
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
|
//! xcu-qcg-wasm -- Quantum Code Gen WASM Runtime
|
||||||
|
//! JIT-compile & execute WASM bytecode with sandboxed memory
|
||||||
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum WasmError {
|
pub enum WasmError { CompileFailed(String), RuntimeError(String), MemoryError(String), LinkError(String) }
|
||||||
CompileFailed(String),
|
|
||||||
RuntimeError(String),
|
|
||||||
MemoryError(String),
|
|
||||||
LinkError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for WasmError {
|
impl std::fmt::Display for WasmError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self { Self::CompileFailed(e) => write!(f, "Compile: {e}"), Self::RuntimeError(e) => write!(f, "Runtime: {e}"), Self::MemoryError(e) => write!(f, "Memory: {e}"), Self::LinkError(e) => write!(f, "Link: {e}") }
|
||||||
Self::CompileFailed(e) => write!(f, "Compile: {e}"),
|
|
||||||
Self::RuntimeError(e) => write!(f, "Runtime: {e}"),
|
|
||||||
Self::MemoryError(e) => write!(f, "Memory: {e}"),
|
|
||||||
Self::LinkError(e) => write!(f, "Link: {e}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl std::error::Error for WasmError {}
|
impl std::error::Error for WasmError {}
|
||||||
pub type Result<T> = std::result::Result<T, WasmError>;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum WasmValue { I32(i32), I64(i64), F32(f32), F64(f64) }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct WasmModule {
|
pub struct WasmModule {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub bytecode: Vec<u8>,
|
pub exports: Vec<WasmExport>,
|
||||||
pub imports: Vec<String>,
|
|
||||||
pub exports: Vec<String>,
|
|
||||||
pub memory_pages: u32,
|
pub memory_pages: u32,
|
||||||
|
pub bytecode_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct WasmRuntime {
|
#[derive(Debug, Clone)]
|
||||||
modules: Arc<Mutex<HashMap<String, WasmModule>>>,
|
pub struct WasmExport { pub name: String, pub kind: ExportKind, pub params: Vec<String>, pub returns: Vec<String> }
|
||||||
memory_limit: usize,
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum ExportKind { Function, Memory, Table, Global }
|
||||||
|
|
||||||
|
/// Sandboxed WASM memory (page-based, 64KB per page)
|
||||||
|
pub struct WasmMemory {
|
||||||
|
pages: Vec<Vec<u8>>,
|
||||||
|
max_pages: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WasmRuntime {
|
impl WasmMemory {
|
||||||
pub fn new(memory_limit: usize) -> Self {
|
pub fn new(initial_pages: u32, max_pages: u32) -> Result<Self, WasmError> {
|
||||||
Self { modules: Arc::new(Mutex::new(HashMap::new())), memory_limit }
|
if initial_pages > max_pages { return Err(WasmError::MemoryError(format!("{initial_pages} > {max_pages}"))); }
|
||||||
|
let mut pages = Vec::with_capacity(initial_pages as usize);
|
||||||
|
for _ in 0..initial_pages { pages.push(vec![0u8; 65536]); }
|
||||||
|
Ok(Self { pages, max_pages })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_module(&self, module: WasmModule) -> Result<()> {
|
pub fn grow(&mut self, delta: u32) -> Result<u32, WasmError> {
|
||||||
if (module.memory_pages as usize) * 65536 > self.memory_limit {
|
let old = self.pages.len() as u32;
|
||||||
return Err(WasmError::MemoryError("exceeds limit".into()));
|
if old + delta > self.max_pages { return Err(WasmError::MemoryError(format!("Cannot grow beyond {}", self.max_pages))); }
|
||||||
|
for _ in 0..delta { self.pages.push(vec![0u8; 65536]); }
|
||||||
|
Ok(old)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&self, offset: usize, len: usize) -> Result<Vec<u8>, WasmError> {
|
||||||
|
let total = self.pages.len() * 65536;
|
||||||
|
if offset + len > total { return Err(WasmError::MemoryError(format!("OOB: {offset}+{len} > {total}"))); }
|
||||||
|
let mut result = Vec::with_capacity(len);
|
||||||
|
for i in offset..offset + len {
|
||||||
|
let page = i / 65536;
|
||||||
|
let off = i % 65536;
|
||||||
|
result.push(self.pages[page][off]);
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&mut self, offset: usize, data: &[u8]) -> Result<(), WasmError> {
|
||||||
|
let total = self.pages.len() * 65536;
|
||||||
|
if offset + data.len() > total { return Err(WasmError::MemoryError(format!("OOB write"))); }
|
||||||
|
for (i, &byte) in data.iter().enumerate() {
|
||||||
|
let addr = offset + i;
|
||||||
|
let page = addr / 65536;
|
||||||
|
let off = addr % 65536;
|
||||||
|
self.pages[page][off] = byte;
|
||||||
}
|
}
|
||||||
let mut mods = self.modules.lock().map_err(|e| WasmError::RuntimeError(e.to_string()))?;
|
|
||||||
mods.insert(module.name.clone(), module);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn module_count(&self) -> usize { self.modules.lock().map(|m| m.len()).unwrap_or(0) }
|
pub fn size_bytes(&self) -> usize { self.pages.len() * 65536 }
|
||||||
|
pub fn page_count(&self) -> u32 { self.pages.len() as u32 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WASM Instance runtime
|
||||||
|
pub struct WasmInstance {
|
||||||
|
module: WasmModule,
|
||||||
|
memory: WasmMemory,
|
||||||
|
globals: HashMap<String, WasmValue>,
|
||||||
|
call_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WasmInstance {
|
||||||
|
pub fn new(module: WasmModule, max_memory_pages: u32) -> Result<Self, WasmError> {
|
||||||
|
let memory = WasmMemory::new(module.memory_pages, max_memory_pages)?;
|
||||||
|
Ok(Self { module, memory, globals: HashMap::new(), call_count: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_global(&mut self, name: &str, val: WasmValue) { self.globals.insert(name.into(), val); }
|
||||||
|
pub fn get_global(&self, name: &str) -> Option<&WasmValue> { self.globals.get(name) }
|
||||||
|
|
||||||
|
/// Call exported function (simplified interpreter)
|
||||||
|
pub fn call(&mut self, name: &str, args: &[WasmValue]) -> Result<Vec<WasmValue>, WasmError> {
|
||||||
|
let export = self.module.exports.iter().find(|e| e.name == name && matches!(e.kind, ExportKind::Function))
|
||||||
|
.ok_or_else(|| WasmError::LinkError(format!("Export '{name}' not found")))?;
|
||||||
|
if args.len() != export.params.len() {
|
||||||
|
return Err(WasmError::RuntimeError(format!("Expected {} args, got {}", export.params.len(), args.len())));
|
||||||
|
}
|
||||||
|
self.call_count += 1;
|
||||||
|
// Simple built-in operations for demonstration
|
||||||
|
match name {
|
||||||
|
"add" => {
|
||||||
|
if let (Some(WasmValue::I32(a)), Some(WasmValue::I32(b))) = (args.get(0), args.get(1)) {
|
||||||
|
Ok(vec![WasmValue::I32(a.wrapping_add(*b))])
|
||||||
|
} else { Err(WasmError::RuntimeError("Type mismatch".into())) }
|
||||||
|
}
|
||||||
|
"mul" => {
|
||||||
|
if let (Some(WasmValue::I64(a)), Some(WasmValue::I64(b))) = (args.get(0), args.get(1)) {
|
||||||
|
Ok(vec![WasmValue::I64(a.wrapping_mul(*b))])
|
||||||
|
} else { Err(WasmError::RuntimeError("Type mismatch".into())) }
|
||||||
|
}
|
||||||
|
_ => Ok(vec![]) // Unknown export returns empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn memory(&self) -> &WasmMemory { &self.memory }
|
||||||
|
pub fn memory_mut(&mut self) -> &mut WasmMemory { &mut self.memory }
|
||||||
|
pub fn call_count(&self) -> u64 { self.call_count }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
fn test_module() -> WasmModule {
|
||||||
|
WasmModule { name: "test".into(), memory_pages: 1, bytecode_size: 100,
|
||||||
|
exports: vec![WasmExport { name: "add".into(), kind: ExportKind::Function, params: vec!["i32".into(), "i32".into()], returns: vec!["i32".into()] }] }
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_wasm_runtime() {
|
fn test_memory_rw() {
|
||||||
let rt = WasmRuntime::new(1_048_576);
|
let mut mem = WasmMemory::new(1, 4).unwrap();
|
||||||
rt.load_module(WasmModule {
|
mem.write(100, &[1, 2, 3, 4]).unwrap();
|
||||||
name: "test".into(), bytecode: vec![0, 0x61, 0x73, 0x6d],
|
let data = mem.read(100, 4).unwrap();
|
||||||
imports: vec![], exports: vec!["main".into()], memory_pages: 1,
|
assert_eq!(data, vec![1, 2, 3, 4]);
|
||||||
}).unwrap();
|
}
|
||||||
assert_eq!(rt.module_count(), 1);
|
#[test]
|
||||||
|
fn test_memory_grow() {
|
||||||
|
let mut mem = WasmMemory::new(1, 4).unwrap();
|
||||||
|
assert_eq!(mem.page_count(), 1);
|
||||||
|
mem.grow(2).unwrap();
|
||||||
|
assert_eq!(mem.page_count(), 3);
|
||||||
|
assert!(mem.grow(5).is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_call() {
|
||||||
|
let mut inst = WasmInstance::new(test_module(), 4).unwrap();
|
||||||
|
let result = inst.call("add", &[WasmValue::I32(10), WasmValue::I32(20)]).unwrap();
|
||||||
|
if let Some(WasmValue::I32(n)) = result.first() { assert_eq!(*n, 30); }
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_oob() {
|
||||||
|
let mem = WasmMemory::new(1, 1).unwrap();
|
||||||
|
assert!(mem.read(70000, 1).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,125 @@
|
|||||||
//! [TSM.ID].[11031972] — Platform X Ecosystem
|
|
||||||
//! xcu-render-pipeline — GPU render pipeline with DAG pass system
|
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
|
//! xcu-render-pipeline -- GPU-agnostic Render Graph (DAG-based)
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum XcuError {
|
pub enum RenderError { CyclicGraph(String), PassFailed(String), ResourceMissing(String) }
|
||||||
InitFailed(String),
|
impl std::fmt::Display for RenderError {
|
||||||
InvalidConfig(String),
|
|
||||||
OperationFailed(String),
|
|
||||||
ResourceExhausted,
|
|
||||||
NotFound(String),
|
|
||||||
Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for XcuError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self { Self::CyclicGraph(e) => write!(f, "Cycle: {e}"), Self::PassFailed(e) => write!(f, "Pass: {e}"), Self::ResourceMissing(e) => write!(f, "Missing: {e}") }
|
||||||
Self::InitFailed(e) => write!(f, "Init failed: {e}"),
|
|
||||||
Self::InvalidConfig(e) => write!(f, "Invalid config: {e}"),
|
|
||||||
Self::OperationFailed(e) => write!(f, "Operation failed: {e}"),
|
|
||||||
Self::ResourceExhausted => write!(f, "Resource exhausted"),
|
|
||||||
Self::NotFound(e) => write!(f, "Not found: {e}"),
|
|
||||||
Self::Timeout => write!(f, "Operation timed out"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
impl std::error::Error for RenderError {}
|
||||||
impl std::error::Error for XcuError {}
|
|
||||||
pub type Result<T> = std::result::Result<T, XcuError>;
|
|
||||||
|
|
||||||
pub struct Config {
|
#[derive(Debug, Clone)]
|
||||||
pub params: HashMap<String, String>,
|
pub enum GpuBackend { Vulkan, Metal, WebGpu, OpenGl, Software }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RenderPass { pub name: String, pub inputs: Vec<String>, pub outputs: Vec<String>, pub shader: String }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RenderResource { pub name: String, pub width: u32, pub height: u32, pub format: String, pub size_bytes: u64 }
|
||||||
|
|
||||||
|
pub struct RenderGraph {
|
||||||
|
passes: Vec<RenderPass>,
|
||||||
|
resources: HashMap<String, RenderResource>,
|
||||||
|
execution_order: Vec<usize>,
|
||||||
|
backend: GpuBackend,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl RenderGraph {
|
||||||
pub fn new() -> Self { Self { params: HashMap::new() } }
|
pub fn new(backend: GpuBackend) -> Self {
|
||||||
pub fn set(&mut self, key: &str, val: &str) -> &mut Self {
|
Self { passes: Vec::new(), resources: HashMap::new(), execution_order: Vec::new(), backend }
|
||||||
self.params.insert(key.to_string(), val.to_string()); self
|
}
|
||||||
|
|
||||||
|
pub fn add_pass(&mut self, pass: RenderPass) -> usize {
|
||||||
|
let idx = self.passes.len();
|
||||||
|
self.passes.push(pass);
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_resource(&mut self, res: RenderResource) { self.resources.insert(res.name.clone(), res); }
|
||||||
|
|
||||||
|
/// Topological sort of render passes (DAG)
|
||||||
|
pub fn compile(&mut self) -> Result<Vec<String>, RenderError> {
|
||||||
|
let n = self.passes.len();
|
||||||
|
let mut in_degree = vec![0usize; n];
|
||||||
|
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
|
||||||
|
|
||||||
|
// Build dependency graph: if pass B reads output of pass A, A → B
|
||||||
|
for (i, pass_b) in self.passes.iter().enumerate() {
|
||||||
|
for input in &pass_b.inputs {
|
||||||
|
for (j, pass_a) in self.passes.iter().enumerate() {
|
||||||
|
if i != j && pass_a.outputs.contains(input) {
|
||||||
|
adj[j].push(i);
|
||||||
|
in_degree[i] += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn get(&self, key: &str) -> Result<&str> {
|
|
||||||
self.params.get(key).map(|s| s.as_str()).ok_or_else(|| XcuError::NotFound(key.to_string()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
// Kahn's algorithm
|
||||||
fn default() -> Self { Self::new() }
|
let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
|
||||||
|
let mut order = Vec::new();
|
||||||
|
|
||||||
|
while let Some(node) = queue.pop() {
|
||||||
|
order.push(node);
|
||||||
|
for &next in &adj[node] {
|
||||||
|
in_degree[next] -= 1;
|
||||||
|
if in_degree[next] == 0 { queue.push(next); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Engine {
|
if order.len() != n { return Err(RenderError::CyclicGraph("Dependency cycle detected".into())); }
|
||||||
config: Config,
|
|
||||||
state: Arc<Mutex<EngineState>>,
|
self.execution_order = order.clone();
|
||||||
|
Ok(order.iter().map(|&i| self.passes[i].name.clone()).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
/// Validate all pass inputs are available
|
||||||
pub enum EngineState {
|
pub fn validate(&self) -> Result<(), RenderError> {
|
||||||
Idle,
|
let mut available: Vec<String> = self.resources.keys().cloned().collect();
|
||||||
Running,
|
for &idx in &self.execution_order {
|
||||||
Paused,
|
let pass = &self.passes[idx];
|
||||||
ShuttingDown,
|
for input in &pass.inputs {
|
||||||
Stopped,
|
if !available.contains(input) {
|
||||||
|
return Err(RenderError::ResourceMissing(format!("Pass '{}' needs '{}' but not produced yet", pass.name, input)));
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
|
||||||
pub fn new(config: Config) -> Result<Self> {
|
|
||||||
Ok(Self { config, state: Arc::new(Mutex::new(EngineState::Idle)) })
|
|
||||||
}
|
}
|
||||||
|
for output in &pass.outputs { if !available.contains(output) { available.push(output.clone()); } }
|
||||||
pub fn start(&self) -> Result<()> {
|
}
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
*s = EngineState::Running;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&self) -> Result<()> {
|
/// Estimate VRAM usage
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
pub fn estimate_vram(&self) -> u64 { self.resources.values().map(|r| r.size_bytes).sum() }
|
||||||
*s = EngineState::ShuttingDown;
|
pub fn pass_count(&self) -> usize { self.passes.len() }
|
||||||
// graceful shutdown logic
|
pub fn backend(&self) -> &GpuBackend { &self.backend }
|
||||||
*s = EngineState::Stopped;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state(&self) -> Result<EngineState> {
|
|
||||||
let s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
Ok(s.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config(&self) -> &Config { &self.config }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_engine_lifecycle() {
|
fn test_compile_order() {
|
||||||
let engine = Engine::new(Config::new()).unwrap();
|
let mut g = RenderGraph::new(GpuBackend::WebGpu);
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Idle);
|
g.add_pass(RenderPass { name: "shadow".into(), inputs: vec![], outputs: vec!["shadow_map".into()], shader: "shadow.wgsl".into() });
|
||||||
engine.start().unwrap();
|
g.add_pass(RenderPass { name: "gbuffer".into(), inputs: vec![], outputs: vec!["albedo".into(), "normal".into()], shader: "gbuffer.wgsl".into() });
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Running);
|
g.add_pass(RenderPass { name: "lighting".into(), inputs: vec!["albedo".into(), "normal".into(), "shadow_map".into()], outputs: vec!["hdr".into()], shader: "lighting.wgsl".into() });
|
||||||
engine.stop().unwrap();
|
g.add_pass(RenderPass { name: "tonemap".into(), inputs: vec!["hdr".into()], outputs: vec!["final".into()], shader: "tonemap.wgsl".into() });
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Stopped);
|
let order = g.compile().unwrap();
|
||||||
|
let light_idx = order.iter().position(|n| n == "lighting").unwrap();
|
||||||
|
let shadow_idx = order.iter().position(|n| n == "shadow").unwrap();
|
||||||
|
let gbuf_idx = order.iter().position(|n| n == "gbuffer").unwrap();
|
||||||
|
assert!(shadow_idx < light_idx);
|
||||||
|
assert!(gbuf_idx < light_idx);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_cycle_detection() {
|
||||||
|
let mut g = RenderGraph::new(GpuBackend::Vulkan);
|
||||||
|
g.add_pass(RenderPass { name: "a".into(), inputs: vec!["y".into()], outputs: vec!["x".into()], shader: "a".into() });
|
||||||
|
g.add_pass(RenderPass { name: "b".into(), inputs: vec!["x".into()], outputs: vec!["y".into()], shader: "b".into() });
|
||||||
|
assert!(g.compile().is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,182 @@
|
|||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
// [TSM.ID].[11031972] -- All Rights Reserved. Proprietary & Confidential.
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
pub mod server;
|
//! xcu-rpc -- Binary RPC Protocol (zero-copy, schema-driven)
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum RpcError { MethodNotFound(String), DeserializeFailed(String), Timeout(String), Internal(String) }
|
||||||
|
impl std::fmt::Display for RpcError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self { Self::MethodNotFound(e) => write!(f, "Method: {e}"), Self::DeserializeFailed(e) => write!(f, "Deser: {e}"), Self::Timeout(e) => write!(f, "Timeout: {e}"), Self::Internal(e) => write!(f, "Internal: {e}") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for RpcError {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum RpcValue { Null, Bool(bool), Int(i64), Float(f64), Str(String), Bytes(Vec<u8>), Array(Vec<RpcValue>), Map(Vec<(String, RpcValue)>) }
|
||||||
|
|
||||||
|
/// Binary frame: [magic(2)][version(1)][flags(1)][method_len(2)][payload_len(4)][method][payload]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RpcFrame {
|
||||||
|
pub request_id: u32,
|
||||||
|
pub method: String,
|
||||||
|
pub params: Vec<RpcValue>,
|
||||||
|
pub is_response: bool,
|
||||||
|
pub error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAGIC: [u8; 2] = [0xXC, 0xU1]; // XCU magic
|
||||||
|
|
||||||
|
impl RpcFrame {
|
||||||
|
pub fn request(id: u32, method: &str, params: Vec<RpcValue>) -> Self {
|
||||||
|
Self { request_id: id, method: method.into(), params, is_response: false, error: None }
|
||||||
|
}
|
||||||
|
pub fn response(id: u32, result: Vec<RpcValue>) -> Self {
|
||||||
|
Self { request_id: id, method: String::new(), params: result, is_response: true, error: None }
|
||||||
|
}
|
||||||
|
pub fn error_response(id: u32, err: &str) -> Self {
|
||||||
|
Self { request_id: id, method: String::new(), params: Vec::new(), is_response: true, error: Some(err.into()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize to binary frame
|
||||||
|
pub fn encode(&self) -> Vec<u8> {
|
||||||
|
let mut buf = Vec::with_capacity(64);
|
||||||
|
buf.extend_from_slice(&[0xC0, 0x01]); // magic
|
||||||
|
buf.push(1); // version
|
||||||
|
buf.push(if self.is_response { 1 } else { 0 }); // flags
|
||||||
|
buf.extend_from_slice(&self.request_id.to_be_bytes());
|
||||||
|
let method_bytes = self.method.as_bytes();
|
||||||
|
buf.extend_from_slice(&(method_bytes.len() as u16).to_be_bytes());
|
||||||
|
buf.extend_from_slice(method_bytes);
|
||||||
|
// Encode params count
|
||||||
|
buf.extend_from_slice(&(self.params.len() as u16).to_be_bytes());
|
||||||
|
for p in &self.params { Self::encode_value(&mut buf, p); }
|
||||||
|
// Error field
|
||||||
|
if let Some(ref err) = self.error {
|
||||||
|
buf.push(1);
|
||||||
|
let err_bytes = err.as_bytes();
|
||||||
|
buf.extend_from_slice(&(err_bytes.len() as u16).to_be_bytes());
|
||||||
|
buf.extend_from_slice(err_bytes);
|
||||||
|
} else { buf.push(0); }
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode_value(buf: &mut Vec<u8>, val: &RpcValue) {
|
||||||
|
match val {
|
||||||
|
RpcValue::Null => buf.push(0),
|
||||||
|
RpcValue::Bool(b) => { buf.push(1); buf.push(if *b { 1 } else { 0 }); }
|
||||||
|
RpcValue::Int(n) => { buf.push(2); buf.extend_from_slice(&n.to_be_bytes()); }
|
||||||
|
RpcValue::Float(n) => { buf.push(3); buf.extend_from_slice(&n.to_be_bytes()); }
|
||||||
|
RpcValue::Str(s) => { buf.push(4); let b = s.as_bytes(); buf.extend_from_slice(&(b.len() as u32).to_be_bytes()); buf.extend_from_slice(b); }
|
||||||
|
RpcValue::Bytes(b) => { buf.push(5); buf.extend_from_slice(&(b.len() as u32).to_be_bytes()); buf.extend_from_slice(b); }
|
||||||
|
RpcValue::Array(arr) => { buf.push(6); buf.extend_from_slice(&(arr.len() as u16).to_be_bytes()); for v in arr { Self::encode_value(buf, v); } }
|
||||||
|
RpcValue::Map(m) => { buf.push(7); buf.extend_from_slice(&(m.len() as u16).to_be_bytes()); for (k, v) in m { let kb = k.as_bytes(); buf.extend_from_slice(&(kb.len() as u16).to_be_bytes()); buf.extend_from_slice(kb); Self::encode_value(buf, v); } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode from binary
|
||||||
|
pub fn decode(data: &[u8]) -> Result<Self, RpcError> {
|
||||||
|
if data.len() < 10 { return Err(RpcError::DeserializeFailed("Too short".into())); }
|
||||||
|
if data[0] != 0xC0 || data[1] != 0x01 { return Err(RpcError::DeserializeFailed("Bad magic".into())); }
|
||||||
|
let is_response = data[3] == 1;
|
||||||
|
let request_id = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
|
||||||
|
let method_len = u16::from_be_bytes([data[8], data[9]]) as usize;
|
||||||
|
let mut pos = 10;
|
||||||
|
if pos + method_len > data.len() { return Err(RpcError::DeserializeFailed("Method truncated".into())); }
|
||||||
|
let method = String::from_utf8_lossy(&data[pos..pos + method_len]).to_string();
|
||||||
|
pos += method_len;
|
||||||
|
// Params
|
||||||
|
if pos + 2 > data.len() { return Err(RpcError::DeserializeFailed("No params count".into())); }
|
||||||
|
let param_count = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||||
|
pos += 2;
|
||||||
|
let mut params = Vec::with_capacity(param_count);
|
||||||
|
for _ in 0..param_count {
|
||||||
|
let (val, new_pos) = Self::decode_value(data, pos)?;
|
||||||
|
params.push(val);
|
||||||
|
pos = new_pos;
|
||||||
|
}
|
||||||
|
// Error
|
||||||
|
let error = if pos < data.len() && data[pos] == 1 {
|
||||||
|
pos += 1;
|
||||||
|
if pos + 2 <= data.len() {
|
||||||
|
let err_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
|
||||||
|
pos += 2;
|
||||||
|
if pos + err_len <= data.len() { Some(String::from_utf8_lossy(&data[pos..pos + err_len]).to_string()) } else { None }
|
||||||
|
} else { None }
|
||||||
|
} else { None };
|
||||||
|
|
||||||
|
Ok(Self { request_id, method, params, is_response, error })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_value(data: &[u8], pos: usize) -> Result<(RpcValue, usize), RpcError> {
|
||||||
|
if pos >= data.len() { return Err(RpcError::DeserializeFailed("EOF".into())); }
|
||||||
|
let tag = data[pos];
|
||||||
|
let mut p = pos + 1;
|
||||||
|
match tag {
|
||||||
|
0 => Ok((RpcValue::Null, p)),
|
||||||
|
1 => { if p >= data.len() { return Err(RpcError::DeserializeFailed("Bool".into())); } let v = data[p] != 0; Ok((RpcValue::Bool(v), p + 1)) }
|
||||||
|
2 => { if p + 8 > data.len() { return Err(RpcError::DeserializeFailed("Int".into())); } let n = i64::from_be_bytes([data[p],data[p+1],data[p+2],data[p+3],data[p+4],data[p+5],data[p+6],data[p+7]]); Ok((RpcValue::Int(n), p + 8)) }
|
||||||
|
3 => { if p + 8 > data.len() { return Err(RpcError::DeserializeFailed("Float".into())); } let n = f64::from_be_bytes([data[p],data[p+1],data[p+2],data[p+3],data[p+4],data[p+5],data[p+6],data[p+7]]); Ok((RpcValue::Float(n), p + 8)) }
|
||||||
|
4 => { if p + 4 > data.len() { return Err(RpcError::DeserializeFailed("Str len".into())); } let len = u32::from_be_bytes([data[p],data[p+1],data[p+2],data[p+3]]) as usize; p += 4; if p + len > data.len() { return Err(RpcError::DeserializeFailed("Str".into())); } let s = String::from_utf8_lossy(&data[p..p+len]).to_string(); Ok((RpcValue::Str(s), p + len)) }
|
||||||
|
5 => { if p + 4 > data.len() { return Err(RpcError::DeserializeFailed("Bytes len".into())); } let len = u32::from_be_bytes([data[p],data[p+1],data[p+2],data[p+3]]) as usize; p += 4; if p + len > data.len() { return Err(RpcError::DeserializeFailed("Bytes".into())); } Ok((RpcValue::Bytes(data[p..p+len].to_vec()), p + len)) }
|
||||||
|
_ => Ok((RpcValue::Null, p)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RPC Router: method → handler dispatch
|
||||||
|
pub struct RpcRouter {
|
||||||
|
methods: HashMap<String, Box<dyn Fn(Vec<RpcValue>) -> Result<Vec<RpcValue>, RpcError> + Send + Sync>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RpcRouter {
|
||||||
|
pub fn new() -> Self { Self { methods: HashMap::new() } }
|
||||||
|
pub fn register<F>(&mut self, method: &str, handler: F) where F: Fn(Vec<RpcValue>) -> Result<Vec<RpcValue>, RpcError> + Send + Sync + 'static {
|
||||||
|
self.methods.insert(method.into(), Box::new(handler));
|
||||||
|
}
|
||||||
|
pub fn dispatch(&self, frame: RpcFrame) -> RpcFrame {
|
||||||
|
match self.methods.get(&frame.method) {
|
||||||
|
Some(handler) => match handler(frame.params) {
|
||||||
|
Ok(result) => RpcFrame::response(frame.request_id, result),
|
||||||
|
Err(e) => RpcFrame::error_response(frame.request_id, &e.to_string()),
|
||||||
|
},
|
||||||
|
None => RpcFrame::error_response(frame.request_id, &format!("Method '{}' not found", frame.method)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn method_count(&self) -> usize { self.methods.len() }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn test_encode_decode() {
|
||||||
|
let frame = RpcFrame::request(42, "echo", vec![RpcValue::Str("hello".into()), RpcValue::Int(123)]);
|
||||||
|
let encoded = frame.encode();
|
||||||
|
let decoded = RpcFrame::decode(&encoded).unwrap();
|
||||||
|
assert_eq!(decoded.request_id, 42);
|
||||||
|
assert_eq!(decoded.method, "echo");
|
||||||
|
assert_eq!(decoded.params.len(), 2);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_router() {
|
||||||
|
let mut router = RpcRouter::new();
|
||||||
|
router.register("add", |params| {
|
||||||
|
if let (Some(RpcValue::Int(a)), Some(RpcValue::Int(b))) = (params.get(0), params.get(1)) {
|
||||||
|
Ok(vec![RpcValue::Int(a + b)])
|
||||||
|
} else { Err(RpcError::Internal("Bad args".into())) }
|
||||||
|
});
|
||||||
|
let req = RpcFrame::request(1, "add", vec![RpcValue::Int(10), RpcValue::Int(20)]);
|
||||||
|
let resp = router.dispatch(req);
|
||||||
|
assert!(resp.is_response);
|
||||||
|
if let Some(RpcValue::Int(n)) = resp.params.first() { assert_eq!(*n, 30); }
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_method_not_found() {
|
||||||
|
let router = RpcRouter::new();
|
||||||
|
let req = RpcFrame::request(1, "missing", vec![]);
|
||||||
|
let resp = router.dispatch(req);
|
||||||
|
assert!(resp.error.is_some());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,104 +1,112 @@
|
|||||||
//! [TSM.ID].[11031972] — Platform X Ecosystem
|
|
||||||
//! xcu-state-machine — Hierarchical state machine with hot-reload
|
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
|
//! xcu-state-machine -- Hierarchical State Machine with hot-reload
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum XcuError {
|
pub enum SmError { InvalidTransition(String), StateNotFound(String), GuardFailed(String) }
|
||||||
InitFailed(String),
|
impl std::fmt::Display for SmError {
|
||||||
InvalidConfig(String),
|
|
||||||
OperationFailed(String),
|
|
||||||
ResourceExhausted,
|
|
||||||
NotFound(String),
|
|
||||||
Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for XcuError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self { Self::InvalidTransition(e) => write!(f, "Invalid: {e}"), Self::StateNotFound(e) => write!(f, "State: {e}"), Self::GuardFailed(e) => write!(f, "Guard: {e}") }
|
||||||
Self::InitFailed(e) => write!(f, "Init failed: {e}"),
|
|
||||||
Self::InvalidConfig(e) => write!(f, "Invalid config: {e}"),
|
|
||||||
Self::OperationFailed(e) => write!(f, "Operation failed: {e}"),
|
|
||||||
Self::ResourceExhausted => write!(f, "Resource exhausted"),
|
|
||||||
Self::NotFound(e) => write!(f, "Not found: {e}"),
|
|
||||||
Self::Timeout => write!(f, "Operation timed out"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
impl std::error::Error for SmError {}
|
||||||
impl std::error::Error for XcuError {}
|
|
||||||
pub type Result<T> = std::result::Result<T, XcuError>;
|
|
||||||
|
|
||||||
pub struct Config {
|
#[derive(Debug, Clone)]
|
||||||
pub params: HashMap<String, String>,
|
pub struct State { pub name: String, pub parent: Option<String>, pub is_final: bool }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Transition { pub from: String, pub to: String, pub event: String, pub guard: Option<String> }
|
||||||
|
|
||||||
|
pub struct StateMachine {
|
||||||
|
states: HashMap<String, State>,
|
||||||
|
transitions: Vec<Transition>,
|
||||||
|
current: String,
|
||||||
|
history: Vec<(String, String, String)>, // (from, event, to)
|
||||||
|
guards: HashMap<String, Box<dyn Fn() -> bool + Send + Sync>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl StateMachine {
|
||||||
pub fn new() -> Self { Self { params: HashMap::new() } }
|
pub fn new(initial: &str) -> Self {
|
||||||
pub fn set(&mut self, key: &str, val: &str) -> &mut Self {
|
let mut states = HashMap::new();
|
||||||
self.params.insert(key.to_string(), val.to_string()); self
|
states.insert(initial.into(), State { name: initial.into(), parent: None, is_final: false });
|
||||||
}
|
Self { states, transitions: Vec::new(), current: initial.into(), history: Vec::new(), guards: HashMap::new() }
|
||||||
pub fn get(&self, key: &str) -> Result<&str> {
|
|
||||||
self.params.get(key).map(|s| s.as_str()).ok_or_else(|| XcuError::NotFound(key.to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
pub fn add_state(&mut self, state: State) { self.states.insert(state.name.clone(), state); }
|
||||||
fn default() -> Self { Self::new() }
|
pub fn add_transition(&mut self, t: Transition) { self.transitions.push(t); }
|
||||||
|
pub fn add_guard<F>(&mut self, name: &str, f: F) where F: Fn() -> bool + Send + Sync + 'static { self.guards.insert(name.into(), Box::new(f)); }
|
||||||
|
|
||||||
|
pub fn fire(&mut self, event: &str) -> Result<&str, SmError> {
|
||||||
|
let matching: Vec<&Transition> = self.transitions.iter()
|
||||||
|
.filter(|t| t.from == self.current && t.event == event).collect();
|
||||||
|
if matching.is_empty() {
|
||||||
|
return Err(SmError::InvalidTransition(format!("No transition from '{}' on '{}'", self.current, event)));
|
||||||
|
}
|
||||||
|
for t in &matching {
|
||||||
|
if let Some(ref guard_name) = t.guard {
|
||||||
|
if let Some(guard_fn) = self.guards.get(guard_name) {
|
||||||
|
if !guard_fn() { continue; }
|
||||||
|
} else { return Err(SmError::GuardFailed(format!("Guard '{guard_name}' not found"))); }
|
||||||
|
}
|
||||||
|
let from = self.current.clone();
|
||||||
|
self.current = t.to.clone();
|
||||||
|
self.history.push((from, event.into(), t.to.clone()));
|
||||||
|
return Ok(&self.current);
|
||||||
|
}
|
||||||
|
Err(SmError::GuardFailed(format!("All guards failed for '{event}'")))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Engine {
|
pub fn current(&self) -> &str { &self.current }
|
||||||
config: Config,
|
pub fn is_final(&self) -> bool { self.states.get(&self.current).map(|s| s.is_final).unwrap_or(false) }
|
||||||
state: Arc<Mutex<EngineState>>,
|
pub fn history(&self) -> &[(String, String, String)] { &self.history }
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
/// Serialize state for hot-reload
|
||||||
pub enum EngineState {
|
pub fn snapshot(&self) -> (String, Vec<(String, String, String)>) {
|
||||||
Idle,
|
(self.current.clone(), self.history.clone())
|
||||||
Running,
|
|
||||||
Paused,
|
|
||||||
ShuttingDown,
|
|
||||||
Stopped,
|
|
||||||
}
|
}
|
||||||
|
/// Restore from snapshot
|
||||||
impl Engine {
|
pub fn restore(&mut self, current: &str, history: Vec<(String, String, String)>) -> Result<(), SmError> {
|
||||||
pub fn new(config: Config) -> Result<Self> {
|
if !self.states.contains_key(current) { return Err(SmError::StateNotFound(current.into())); }
|
||||||
Ok(Self { config, state: Arc::new(Mutex::new(EngineState::Idle)) })
|
self.current = current.into();
|
||||||
}
|
self.history = history;
|
||||||
|
|
||||||
pub fn start(&self) -> Result<()> {
|
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
*s = EngineState::Running;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&self) -> Result<()> {
|
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
*s = EngineState::ShuttingDown;
|
|
||||||
// graceful shutdown logic
|
|
||||||
*s = EngineState::Stopped;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state(&self) -> Result<EngineState> {
|
|
||||||
let s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
Ok(s.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config(&self) -> &Config { &self.config }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
fn call_sm() -> StateMachine {
|
||||||
|
let mut sm = StateMachine::new("idle");
|
||||||
|
sm.add_state(State { name: "ringing".into(), parent: None, is_final: false });
|
||||||
|
sm.add_state(State { name: "connected".into(), parent: None, is_final: false });
|
||||||
|
sm.add_state(State { name: "ended".into(), parent: None, is_final: true });
|
||||||
|
sm.add_transition(Transition { from: "idle".into(), to: "ringing".into(), event: "call".into(), guard: None });
|
||||||
|
sm.add_transition(Transition { from: "ringing".into(), to: "connected".into(), event: "answer".into(), guard: None });
|
||||||
|
sm.add_transition(Transition { from: "connected".into(), to: "ended".into(), event: "hangup".into(), guard: None });
|
||||||
|
sm.add_transition(Transition { from: "ringing".into(), to: "ended".into(), event: "reject".into(), guard: None });
|
||||||
|
sm
|
||||||
|
}
|
||||||
#[test]
|
#[test]
|
||||||
fn test_engine_lifecycle() {
|
fn test_transitions() {
|
||||||
let engine = Engine::new(Config::new()).unwrap();
|
let mut sm = call_sm();
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Idle);
|
sm.fire("call").unwrap();
|
||||||
engine.start().unwrap();
|
assert_eq!(sm.current(), "ringing");
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Running);
|
sm.fire("answer").unwrap();
|
||||||
engine.stop().unwrap();
|
assert_eq!(sm.current(), "connected");
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Stopped);
|
sm.fire("hangup").unwrap();
|
||||||
|
assert!(sm.is_final());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_invalid() { let mut sm = call_sm(); assert!(sm.fire("hangup").is_err()); }
|
||||||
|
#[test]
|
||||||
|
fn test_snapshot_restore() {
|
||||||
|
let mut sm = call_sm();
|
||||||
|
sm.fire("call").unwrap();
|
||||||
|
let (state, hist) = sm.snapshot();
|
||||||
|
let mut sm2 = call_sm();
|
||||||
|
sm2.restore(&state, hist).unwrap();
|
||||||
|
assert_eq!(sm2.current(), "ringing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,123 @@
|
|||||||
//! [TSM.ID].[11031972] — Platform X Ecosystem
|
|
||||||
//! xcu-thread-weaver — Work-stealing thread pool scheduler
|
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
use std::collections::HashMap;
|
//! xcu-thread-weaver -- Work-Stealing Thread Pool & NPU Scheduler
|
||||||
|
use std::collections::VecDeque;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum XcuError {
|
pub enum WeaverError { QueueFull(String), WorkerPanicked(String), InvalidConfig(String) }
|
||||||
InitFailed(String),
|
impl std::fmt::Display for WeaverError {
|
||||||
InvalidConfig(String),
|
|
||||||
OperationFailed(String),
|
|
||||||
ResourceExhausted,
|
|
||||||
NotFound(String),
|
|
||||||
Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for XcuError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self { Self::QueueFull(e) => write!(f, "Full: {e}"), Self::WorkerPanicked(e) => write!(f, "Panic: {e}"), Self::InvalidConfig(e) => write!(f, "Config: {e}") }
|
||||||
Self::InitFailed(e) => write!(f, "Init failed: {e}"),
|
|
||||||
Self::InvalidConfig(e) => write!(f, "Invalid config: {e}"),
|
|
||||||
Self::OperationFailed(e) => write!(f, "Operation failed: {e}"),
|
|
||||||
Self::ResourceExhausted => write!(f, "Resource exhausted"),
|
|
||||||
Self::NotFound(e) => write!(f, "Not found: {e}"),
|
|
||||||
Self::Timeout => write!(f, "Operation timed out"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
impl std::error::Error for WeaverError {}
|
||||||
impl std::error::Error for XcuError {}
|
|
||||||
pub type Result<T> = std::result::Result<T, XcuError>;
|
|
||||||
|
|
||||||
pub struct Config {
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub params: HashMap<String, String>,
|
pub enum TaskPriority { Critical = 0, High = 1, Normal = 2, Low = 3, Background = 4 }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WeaverTask { pub id: String, pub priority: TaskPriority, pub data: Vec<u8>, pub created_at: u64 }
|
||||||
|
|
||||||
|
struct WorkerQueue { deque: VecDeque<WeaverTask>, processed: u64, stolen_from: u64 }
|
||||||
|
impl WorkerQueue {
|
||||||
|
fn new() -> Self { Self { deque: VecDeque::new(), processed: 0, stolen_from: 0 } }
|
||||||
|
fn push(&mut self, task: WeaverTask) {
|
||||||
|
// Insert sorted by priority
|
||||||
|
let pos = self.deque.iter().position(|t| t.priority > task.priority).unwrap_or(self.deque.len());
|
||||||
|
self.deque.insert(pos, task);
|
||||||
|
}
|
||||||
|
fn pop(&mut self) -> Option<WeaverTask> { let t = self.deque.pop_front(); if t.is_some() { self.processed += 1; } t }
|
||||||
|
fn steal(&mut self) -> Option<WeaverTask> { let t = self.deque.pop_back(); if t.is_some() { self.stolen_from += 1; } t }
|
||||||
|
fn len(&self) -> usize { self.deque.len() }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
pub struct ThreadWeaver {
|
||||||
pub fn new() -> Self { Self { params: HashMap::new() } }
|
queues: Vec<Arc<Mutex<WorkerQueue>>>,
|
||||||
pub fn set(&mut self, key: &str, val: &str) -> &mut Self {
|
worker_count: usize,
|
||||||
self.params.insert(key.to_string(), val.to_string()); self
|
next_worker: Mutex<usize>,
|
||||||
}
|
|
||||||
pub fn get(&self, key: &str) -> Result<&str> {
|
|
||||||
self.params.get(key).map(|s| s.as_str()).ok_or_else(|| XcuError::NotFound(key.to_string()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl ThreadWeaver {
|
||||||
fn default() -> Self { Self::new() }
|
pub fn new(workers: usize) -> Result<Self, WeaverError> {
|
||||||
|
if workers == 0 { return Err(WeaverError::InvalidConfig("Workers must be > 0".into())); }
|
||||||
|
let queues: Vec<_> = (0..workers).map(|_| Arc::new(Mutex::new(WorkerQueue::new()))).collect();
|
||||||
|
Ok(Self { queues, worker_count: workers, next_worker: Mutex::new(0) })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Engine {
|
/// Submit task (round-robin assignment)
|
||||||
config: Config,
|
pub fn submit(&self, task: WeaverTask) -> Result<usize, WeaverError> {
|
||||||
state: Arc<Mutex<EngineState>>,
|
let mut next = self.next_worker.lock().map_err(|_| WeaverError::QueueFull("Lock".into()))?;
|
||||||
|
let idx = *next % self.worker_count;
|
||||||
|
*next = (*next + 1) % self.worker_count;
|
||||||
|
drop(next);
|
||||||
|
if let Ok(mut q) = self.queues[idx].lock() { q.push(task); Ok(idx) }
|
||||||
|
else { Err(WeaverError::QueueFull("Worker lock".into())) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
/// Worker picks next task (own queue first, then steal)
|
||||||
pub enum EngineState {
|
pub fn pick_task(&self, worker_id: usize) -> Option<WeaverTask> {
|
||||||
Idle,
|
// Try own queue first
|
||||||
Running,
|
if let Ok(mut q) = self.queues[worker_id].lock() {
|
||||||
Paused,
|
if let Some(task) = q.pop() { return Some(task); }
|
||||||
ShuttingDown,
|
}
|
||||||
Stopped,
|
// Work stealing: try other queues
|
||||||
|
for i in 0..self.worker_count {
|
||||||
|
if i == worker_id { continue; }
|
||||||
|
if let Ok(mut q) = self.queues[i].lock() {
|
||||||
|
if q.len() > 1 { // Only steal if other has > 1 task
|
||||||
|
if let Some(task) = q.steal() { return Some(task); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
/// Get queue stats
|
||||||
pub fn new(config: Config) -> Result<Self> {
|
pub fn stats(&self) -> Vec<(usize, u64, u64)> {
|
||||||
Ok(Self { config, state: Arc::new(Mutex::new(EngineState::Idle)) })
|
self.queues.iter().enumerate().map(|(i, q)| {
|
||||||
|
if let Ok(q) = q.lock() { (q.len(), q.processed, q.stolen_from) }
|
||||||
|
else { (0, 0, 0) }
|
||||||
|
}).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(&self) -> Result<()> {
|
pub fn total_pending(&self) -> usize {
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
self.queues.iter().map(|q| q.lock().map(|q| q.len()).unwrap_or(0)).sum()
|
||||||
*s = EngineState::Running;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&self) -> Result<()> {
|
pub fn total_processed(&self) -> u64 {
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
self.queues.iter().map(|q| q.lock().map(|q| q.processed).unwrap_or(0)).sum()
|
||||||
*s = EngineState::ShuttingDown;
|
|
||||||
// graceful shutdown logic
|
|
||||||
*s = EngineState::Stopped;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state(&self) -> Result<EngineState> {
|
|
||||||
let s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
Ok(s.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn config(&self) -> &Config { &self.config }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_engine_lifecycle() {
|
fn test_submit_pick() {
|
||||||
let engine = Engine::new(Config::new()).unwrap();
|
let w = ThreadWeaver::new(4).unwrap();
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Idle);
|
w.submit(WeaverTask { id: "t1".into(), priority: TaskPriority::Normal, data: vec![], created_at: 0 }).unwrap();
|
||||||
engine.start().unwrap();
|
let task = w.pick_task(0).unwrap();
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Running);
|
assert_eq!(task.id, "t1");
|
||||||
engine.stop().unwrap();
|
}
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Stopped);
|
#[test]
|
||||||
|
fn test_priority_ordering() {
|
||||||
|
let w = ThreadWeaver::new(1).unwrap();
|
||||||
|
w.submit(WeaverTask { id: "low".into(), priority: TaskPriority::Low, data: vec![], created_at: 0 }).unwrap();
|
||||||
|
w.submit(WeaverTask { id: "crit".into(), priority: TaskPriority::Critical, data: vec![], created_at: 0 }).unwrap();
|
||||||
|
w.submit(WeaverTask { id: "high".into(), priority: TaskPriority::Critical, data: vec![], created_at: 0 }).unwrap();
|
||||||
|
let first = w.pick_task(0).unwrap();
|
||||||
|
assert_eq!(first.id, "crit"); // Critical first
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_work_stealing() {
|
||||||
|
let w = ThreadWeaver::new(2).unwrap();
|
||||||
|
// Submit 3 tasks to worker 0
|
||||||
|
for i in 0..3 {
|
||||||
|
let idx = w.submit(WeaverTask { id: format!("t{i}"), priority: TaskPriority::Normal, data: vec![], created_at: 0 }).unwrap();
|
||||||
|
}
|
||||||
|
// Worker 1 has nothing, should steal
|
||||||
|
let stolen = w.pick_task(1);
|
||||||
|
assert!(stolen.is_some());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,104 +1,151 @@
|
|||||||
//! [TSM.ID].[11031972] — Platform X Ecosystem
|
|
||||||
//! xcu-v8-sandbox — JavaScript execution sandbox with resource limits
|
|
||||||
#![deny(warnings)]
|
#![deny(warnings)]
|
||||||
|
//! [TSM.ID].[11031972] -- Platform X Ecosystem
|
||||||
|
//! xcu-v8-sandbox -- JavaScript Execution Sandbox with Permission System
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum XcuError {
|
pub enum SandboxError { PermissionDenied(String), MemoryExceeded(String), TimeoutError(String), ScriptError(String) }
|
||||||
InitFailed(String),
|
impl std::fmt::Display for SandboxError {
|
||||||
InvalidConfig(String),
|
|
||||||
OperationFailed(String),
|
|
||||||
ResourceExhausted,
|
|
||||||
NotFound(String),
|
|
||||||
Timeout,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for XcuError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self { Self::PermissionDenied(e) => write!(f, "Permission: {e}"), Self::MemoryExceeded(e) => write!(f, "Memory: {e}"), Self::TimeoutError(e) => write!(f, "Timeout: {e}"), Self::ScriptError(e) => write!(f, "Script: {e}") }
|
||||||
Self::InitFailed(e) => write!(f, "Init failed: {e}"),
|
|
||||||
Self::InvalidConfig(e) => write!(f, "Invalid config: {e}"),
|
|
||||||
Self::OperationFailed(e) => write!(f, "Operation failed: {e}"),
|
|
||||||
Self::ResourceExhausted => write!(f, "Resource exhausted"),
|
|
||||||
Self::NotFound(e) => write!(f, "Not found: {e}"),
|
|
||||||
Self::Timeout => write!(f, "Operation timed out"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
impl std::error::Error for SandboxError {}
|
||||||
impl std::error::Error for XcuError {}
|
|
||||||
pub type Result<T> = std::result::Result<T, XcuError>;
|
|
||||||
|
|
||||||
pub struct Config {
|
#[derive(Debug, Clone)]
|
||||||
pub params: HashMap<String, String>,
|
pub struct SandboxPolicy {
|
||||||
|
pub max_memory_mb: u64,
|
||||||
|
pub max_execution_ms: u64,
|
||||||
|
pub allowed_apis: Vec<String>,
|
||||||
|
pub denied_apis: Vec<String>,
|
||||||
|
pub allow_network: bool,
|
||||||
|
pub allow_fs: bool,
|
||||||
|
pub allow_eval: bool,
|
||||||
}
|
}
|
||||||
|
impl Default for SandboxPolicy {
|
||||||
impl Config {
|
fn default() -> Self {
|
||||||
pub fn new() -> Self { Self { params: HashMap::new() } }
|
Self { max_memory_mb: 128, max_execution_ms: 5000,
|
||||||
pub fn set(&mut self, key: &str, val: &str) -> &mut Self {
|
allowed_apis: vec!["console".into(), "Math".into(), "JSON".into(), "Date".into(), "Array".into(), "Object".into(), "String".into()],
|
||||||
self.params.insert(key.to_string(), val.to_string()); self
|
denied_apis: vec!["eval".into(), "Function".into(), "require".into(), "import".into(), "process".into(), "fs".into()],
|
||||||
}
|
allow_network: false, allow_fs: false, allow_eval: false }
|
||||||
pub fn get(&self, key: &str) -> Result<&str> {
|
|
||||||
self.params.get(key).map(|s| s.as_str()).ok_or_else(|| XcuError::NotFound(key.to_string()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
#[derive(Debug, Clone)]
|
||||||
fn default() -> Self { Self::new() }
|
pub enum JsValue { Undefined, Null, Bool(bool), Number(f64), Str(String), Array(Vec<JsValue>), Object(HashMap<String, JsValue>) }
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScriptResult { pub value: JsValue, pub memory_used_bytes: u64, pub execution_ms: u64, pub api_calls: Vec<String> }
|
||||||
|
|
||||||
|
pub struct JsSandbox {
|
||||||
|
policy: SandboxPolicy,
|
||||||
|
globals: HashMap<String, JsValue>,
|
||||||
|
audit_log: Arc<Mutex<Vec<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Engine {
|
impl JsSandbox {
|
||||||
config: Config,
|
pub fn new(policy: SandboxPolicy) -> Self {
|
||||||
state: Arc<Mutex<EngineState>>,
|
Self { policy, globals: HashMap::new(), audit_log: Arc::new(Mutex::new(Vec::new())) }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
pub fn set_global(&mut self, name: &str, value: JsValue) { self.globals.insert(name.into(), value); }
|
||||||
pub enum EngineState {
|
|
||||||
Idle,
|
|
||||||
Running,
|
|
||||||
Paused,
|
|
||||||
ShuttingDown,
|
|
||||||
Stopped,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Engine {
|
/// Check if API call is permitted
|
||||||
pub fn new(config: Config) -> Result<Self> {
|
pub fn check_api(&self, api_name: &str) -> Result<(), SandboxError> {
|
||||||
Ok(Self { config, state: Arc::new(Mutex::new(EngineState::Idle)) })
|
if self.policy.denied_apis.iter().any(|d| api_name.starts_with(d)) {
|
||||||
|
return Err(SandboxError::PermissionDenied(format!("API '{api_name}' is denied")));
|
||||||
|
}
|
||||||
|
if !api_name.contains('.') && !self.policy.allowed_apis.iter().any(|a| api_name.starts_with(a)) {
|
||||||
|
return Err(SandboxError::PermissionDenied(format!("API '{api_name}' not in allow list")));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(&self) -> Result<()> {
|
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
|
||||||
*s = EngineState::Running;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop(&self) -> Result<()> {
|
/// Evaluate simple expression (interpreter for basic math/string ops)
|
||||||
let mut s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
pub fn eval_expr(&mut self, expr: &str) -> Result<ScriptResult, SandboxError> {
|
||||||
*s = EngineState::ShuttingDown;
|
if !self.policy.allow_eval && expr.contains("eval(") {
|
||||||
// graceful shutdown logic
|
return Err(SandboxError::PermissionDenied("eval() is disabled".into()));
|
||||||
*s = EngineState::Stopped;
|
}
|
||||||
Ok(())
|
let trimmed = expr.trim();
|
||||||
|
let mut api_calls = Vec::new();
|
||||||
|
|
||||||
|
// Check for denied APIs in expression
|
||||||
|
for denied in &self.policy.denied_apis {
|
||||||
|
if trimmed.contains(denied.as_str()) {
|
||||||
|
return Err(SandboxError::PermissionDenied(format!("'{denied}' found in expression")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state(&self) -> Result<EngineState> {
|
// Simple expression evaluator
|
||||||
let s = self.state.lock().map_err(|e| XcuError::OperationFailed(e.to_string()))?;
|
let value = if let Ok(n) = trimmed.parse::<f64>() {
|
||||||
Ok(s.clone())
|
JsValue::Number(n)
|
||||||
|
} else if trimmed.starts_with('"') && trimmed.ends_with('"') {
|
||||||
|
JsValue::Str(trimmed[1..trimmed.len()-1].to_string())
|
||||||
|
} else if trimmed == "true" { JsValue::Bool(true) }
|
||||||
|
else if trimmed == "false" { JsValue::Bool(false) }
|
||||||
|
else if trimmed == "null" { JsValue::Null }
|
||||||
|
else if trimmed.starts_with("Math.") {
|
||||||
|
api_calls.push("Math".into());
|
||||||
|
match trimmed {
|
||||||
|
"Math.PI" => JsValue::Number(std::f64::consts::PI),
|
||||||
|
"Math.E" => JsValue::Number(std::f64::consts::E),
|
||||||
|
"Math.SQRT2" => JsValue::Number(std::f64::consts::SQRT_2),
|
||||||
|
_ => JsValue::Undefined,
|
||||||
|
}
|
||||||
|
} else if let Some(var) = self.globals.get(trimmed) { var.clone() }
|
||||||
|
else { JsValue::Undefined };
|
||||||
|
|
||||||
|
// Log
|
||||||
|
if let Ok(mut log) = self.audit_log.lock() { log.push(format!("eval: {trimmed}")); }
|
||||||
|
|
||||||
|
Ok(ScriptResult { value, memory_used_bytes: trimmed.len() as u64, execution_ms: 0, api_calls })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn config(&self) -> &Config { &self.config }
|
/// Check memory budget
|
||||||
|
pub fn check_memory(&self, used_bytes: u64) -> Result<(), SandboxError> {
|
||||||
|
let limit = self.policy.max_memory_mb * 1024 * 1024;
|
||||||
|
if used_bytes > limit { Err(SandboxError::MemoryExceeded(format!("{}B > {}B", used_bytes, limit))) }
|
||||||
|
else { Ok(()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn audit_log(&self) -> Vec<String> { self.audit_log.lock().map(|l| l.clone()).unwrap_or_default() }
|
||||||
|
pub fn policy(&self) -> &SandboxPolicy { &self.policy }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
#[test]
|
#[test]
|
||||||
fn test_engine_lifecycle() {
|
fn test_api_check() {
|
||||||
let engine = Engine::new(Config::new()).unwrap();
|
let sb = JsSandbox::new(SandboxPolicy::default());
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Idle);
|
assert!(sb.check_api("Math").is_ok());
|
||||||
engine.start().unwrap();
|
assert!(sb.check_api("console").is_ok());
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Running);
|
assert!(sb.check_api("eval").is_err());
|
||||||
engine.stop().unwrap();
|
assert!(sb.check_api("require").is_err());
|
||||||
assert_eq!(engine.state().unwrap(), EngineState::Stopped);
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_eval_number() {
|
||||||
|
let mut sb = JsSandbox::new(SandboxPolicy::default());
|
||||||
|
let r = sb.eval_expr("42").unwrap();
|
||||||
|
if let JsValue::Number(n) = r.value { assert_eq!(n, 42.0); }
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_eval_denied() {
|
||||||
|
let mut sb = JsSandbox::new(SandboxPolicy::default());
|
||||||
|
assert!(sb.eval_expr("require('fs')").is_err());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_eval_math() {
|
||||||
|
let mut sb = JsSandbox::new(SandboxPolicy::default());
|
||||||
|
let r = sb.eval_expr("Math.PI").unwrap();
|
||||||
|
if let JsValue::Number(n) = r.value { assert!((n - std::f64::consts::PI).abs() < 0.001); }
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_globals() {
|
||||||
|
let mut sb = JsSandbox::new(SandboxPolicy::default());
|
||||||
|
sb.set_global("x", JsValue::Number(99.0));
|
||||||
|
let r = sb.eval_expr("x").unwrap();
|
||||||
|
if let JsValue::Number(n) = r.value { assert_eq!(n, 99.0); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user