mirror of
https://github.com/Astatin3/unshell.git
synced 2026-06-08 22:38:01 -06:00
Reorganize protocol.
This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
//! Endpoint runtime and traits.
|
||||
|
||||
use alloc::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
string::String,
|
||||
vec,
|
||||
vec::Vec,
|
||||
};
|
||||
use core::fmt;
|
||||
use rkyv::{rancor::Error as RkyvError, to_bytes};
|
||||
|
||||
use crate::protocol::{
|
||||
CallMessage, DataMessage, EndpointIntrospection, FaultMessage, FrameBytes, FrameError,
|
||||
HookTarget, LeafIntrospection, LeafIntrospectionSummary, PacketHeader, PacketType,
|
||||
ProtocolFault, ValidationError, decode_frame, encode_packet,
|
||||
introspection::INTROSPECTION_PROCEDURE_ID, validate_call, validate_header,
|
||||
validate_procedure_id,
|
||||
};
|
||||
|
||||
use super::{ActiveHook, HookKey, HookTable, PendingHook, RouteDecision, route_destination};
|
||||
|
||||
/// Local connection state.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnectionState {
|
||||
Unregistered,
|
||||
Registered,
|
||||
}
|
||||
|
||||
/// Registered child route.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ChildRoute {
|
||||
pub path: Vec<String>,
|
||||
pub state: ConnectionState,
|
||||
}
|
||||
|
||||
impl ChildRoute {
|
||||
pub fn registered(path: Vec<String>) -> Self {
|
||||
Self {
|
||||
path,
|
||||
state: ConnectionState::Registered,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Leaf behavior for test runtime.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LeafBehavior {
|
||||
Echo,
|
||||
}
|
||||
|
||||
/// Static leaf description.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LeafSpec {
|
||||
pub name: String,
|
||||
pub procedures: Vec<String>,
|
||||
pub behavior: LeafBehavior,
|
||||
}
|
||||
|
||||
/// Arrival side.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Ingress {
|
||||
Parent,
|
||||
Child(Vec<String>),
|
||||
Local,
|
||||
}
|
||||
|
||||
/// Local events.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LocalEvent {
|
||||
Call {
|
||||
header: PacketHeader,
|
||||
message: CallMessage,
|
||||
},
|
||||
Data {
|
||||
header: PacketHeader,
|
||||
message: DataMessage,
|
||||
},
|
||||
Fault {
|
||||
header: PacketHeader,
|
||||
message: FaultMessage,
|
||||
},
|
||||
}
|
||||
|
||||
/// Processing outcome.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct EndpointOutcome {
|
||||
pub forwards: Vec<(RouteDecision, FrameBytes)>,
|
||||
pub events: Vec<LocalEvent>,
|
||||
pub dropped: bool,
|
||||
}
|
||||
|
||||
/// Processing error.
|
||||
#[derive(Debug)]
|
||||
pub enum EndpointError {
|
||||
Frame(FrameError),
|
||||
Validation(ValidationError),
|
||||
}
|
||||
|
||||
impl fmt::Display for EndpointError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Frame(error) => write!(f, "{error}"),
|
||||
Self::Validation(error) => write!(f, "{error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for EndpointError {}
|
||||
|
||||
impl From<FrameError> for EndpointError {
|
||||
fn from(value: FrameError) -> Self {
|
||||
Self::Frame(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ValidationError> for EndpointError {
|
||||
fn from(value: ValidationError) -> Self {
|
||||
Self::Validation(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Core trait for a protocol endpoint.
|
||||
pub trait Endpoint {
|
||||
fn path(&self) -> &[String];
|
||||
fn receive(
|
||||
&mut self,
|
||||
ingress: &Ingress,
|
||||
frame: FrameBytes,
|
||||
) -> Result<EndpointOutcome, EndpointError>;
|
||||
}
|
||||
|
||||
/// Default endpoint implementation.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProtocolEndpoint {
|
||||
path: Vec<String>,
|
||||
parent_path: Option<Vec<String>>,
|
||||
children: Vec<ChildRoute>,
|
||||
leaves: BTreeMap<String, LeafSpec>,
|
||||
endpoint_procedures: BTreeSet<String>,
|
||||
hooks: HookTable,
|
||||
}
|
||||
|
||||
impl ProtocolEndpoint {
|
||||
pub fn new(
|
||||
path: Vec<String>,
|
||||
parent_path: Option<Vec<String>>,
|
||||
children: Vec<ChildRoute>,
|
||||
leaves: Vec<LeafSpec>,
|
||||
) -> Self {
|
||||
Self {
|
||||
path,
|
||||
parent_path,
|
||||
children,
|
||||
leaves: leaves
|
||||
.into_iter()
|
||||
.map(|leaf| (leaf.name.clone(), leaf))
|
||||
.collect(),
|
||||
endpoint_procedures: BTreeSet::new(),
|
||||
hooks: HookTable::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_endpoint_procedure(
|
||||
&mut self,
|
||||
procedure_id: impl Into<String>,
|
||||
) -> Result<(), EndpointError> {
|
||||
let procedure_id = procedure_id.into();
|
||||
validate_procedure_id(&procedure_id)?;
|
||||
self.endpoint_procedures.insert(procedure_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn allocate_hook_id(&self) -> u64 {
|
||||
self.hooks.allocate_hook_id(&self.path)
|
||||
}
|
||||
|
||||
pub fn make_call(
|
||||
&mut self,
|
||||
dst_path: Vec<String>,
|
||||
dst_leaf: Option<String>,
|
||||
procedure_id: impl Into<String>,
|
||||
response_hook_id: Option<u64>,
|
||||
data: Vec<u8>,
|
||||
) -> Result<FrameBytes, EndpointError> {
|
||||
let procedure_id = procedure_id.into();
|
||||
validate_procedure_id(&procedure_id)?;
|
||||
let response_hook = response_hook_id.map(|hook_id| HookTarget {
|
||||
hook_id,
|
||||
return_path: self.path.clone(),
|
||||
});
|
||||
let header = PacketHeader {
|
||||
packet_type: PacketType::Call,
|
||||
src_path: self.path.clone(),
|
||||
dst_path: dst_path.clone(),
|
||||
dst_leaf: dst_leaf.clone(),
|
||||
hook_id: None,
|
||||
};
|
||||
let call = CallMessage {
|
||||
procedure_id: procedure_id.clone(),
|
||||
data,
|
||||
response_hook,
|
||||
};
|
||||
validate_header(&header)?;
|
||||
validate_call(&header, &call)?;
|
||||
|
||||
if let Some(hook) = &call.response_hook {
|
||||
self.hooks.insert_active(ActiveHook {
|
||||
return_path: hook.return_path.clone(),
|
||||
hook_id: hook.hook_id,
|
||||
peer_path: dst_path,
|
||||
procedure_id,
|
||||
dst_leaf,
|
||||
peer_finished: false,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(encode_packet(&header, &call)?)
|
||||
}
|
||||
|
||||
pub fn make_data(
|
||||
&self,
|
||||
dst_path: Vec<String>,
|
||||
hook_id: u64,
|
||||
procedure_id: impl Into<String>,
|
||||
data: Vec<u8>,
|
||||
end_hook: bool,
|
||||
) -> Result<FrameBytes, EndpointError> {
|
||||
let procedure_id = procedure_id.into();
|
||||
validate_procedure_id(&procedure_id)?;
|
||||
let header = PacketHeader {
|
||||
packet_type: PacketType::Data,
|
||||
src_path: self.path.clone(),
|
||||
dst_path,
|
||||
dst_leaf: None,
|
||||
hook_id: Some(hook_id),
|
||||
};
|
||||
let message = DataMessage {
|
||||
procedure_id,
|
||||
data,
|
||||
end_hook,
|
||||
};
|
||||
validate_header(&header)?;
|
||||
Ok(encode_packet(&header, &message)?)
|
||||
}
|
||||
|
||||
fn handle_local_call(
|
||||
&mut self,
|
||||
header: PacketHeader,
|
||||
message: CallMessage,
|
||||
) -> Result<EndpointOutcome, EndpointError> {
|
||||
let key = message
|
||||
.response_hook
|
||||
.as_ref()
|
||||
.map(|hook| HookKey::new(hook.return_path.clone(), hook.hook_id));
|
||||
|
||||
if let Some(hook) = &message.response_hook {
|
||||
self.hooks.insert_pending(PendingHook {
|
||||
caller_src_path: header.src_path.clone(),
|
||||
return_path: hook.return_path.clone(),
|
||||
hook_id: hook.hook_id,
|
||||
procedure_id: message.procedure_id.clone(),
|
||||
dst_leaf: header.dst_leaf.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
if message.procedure_id == INTROSPECTION_PROCEDURE_ID {
|
||||
return self.handle_introspection(&header, key);
|
||||
}
|
||||
|
||||
let supported = match &header.dst_leaf {
|
||||
Some(leaf_name) => self
|
||||
.leaves
|
||||
.get(leaf_name)
|
||||
.map(|leaf| leaf.procedures.iter().any(|p| p == &message.procedure_id))
|
||||
.unwrap_or(false),
|
||||
None => self.endpoint_procedures.contains(&message.procedure_id),
|
||||
};
|
||||
|
||||
if !supported {
|
||||
let fault = if header
|
||||
.dst_leaf
|
||||
.as_ref()
|
||||
.is_some_and(|name| !self.leaves.contains_key(name))
|
||||
{
|
||||
ProtocolFault::UnknownLeaf
|
||||
} else {
|
||||
ProtocolFault::UnknownProcedure
|
||||
};
|
||||
return self.emit_fault_if_possible(key, fault);
|
||||
}
|
||||
|
||||
if let Some(key) = &key {
|
||||
self.hooks.activate_pending(key, header.src_path.clone());
|
||||
}
|
||||
|
||||
match header
|
||||
.dst_leaf
|
||||
.as_ref()
|
||||
.and_then(|name| self.leaves.get(name))
|
||||
{
|
||||
Some(leaf) if leaf.behavior == LeafBehavior::Echo && key.is_some() => {
|
||||
let hook = message.response_hook.expect("synchronized");
|
||||
let response = DataMessage {
|
||||
procedure_id: message.procedure_id.clone(),
|
||||
data: message.data,
|
||||
end_hook: true,
|
||||
};
|
||||
let response_header = PacketHeader {
|
||||
packet_type: PacketType::Data,
|
||||
src_path: self.path.clone(),
|
||||
dst_path: hook.return_path.clone(),
|
||||
dst_leaf: None,
|
||||
hook_id: Some(hook.hook_id),
|
||||
};
|
||||
let frame = encode_packet(&response_header, &response)?;
|
||||
self.hooks
|
||||
.remove_active(&HookKey::new(hook.return_path, hook.hook_id));
|
||||
Ok(EndpointOutcome {
|
||||
forwards: vec![(RouteDecision::Parent, frame)],
|
||||
..EndpointOutcome::default()
|
||||
})
|
||||
}
|
||||
_ => Ok(EndpointOutcome {
|
||||
events: vec![LocalEvent::Call { header, message }],
|
||||
..EndpointOutcome::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_introspection(
|
||||
&mut self,
|
||||
header: &PacketHeader,
|
||||
key: Option<HookKey>,
|
||||
) -> Result<EndpointOutcome, EndpointError> {
|
||||
let Some(key) = key else {
|
||||
return Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
});
|
||||
};
|
||||
self.hooks.activate_pending(&key, header.src_path.clone());
|
||||
|
||||
let payload = if let Some(leaf_name) = &header.dst_leaf {
|
||||
let Some(leaf) = self.leaves.get(leaf_name) else {
|
||||
return self.emit_fault_if_possible(Some(key), ProtocolFault::UnknownLeaf);
|
||||
};
|
||||
to_bytes::<RkyvError>(&LeafIntrospection {
|
||||
leaf_name: leaf_name.clone(),
|
||||
procedures: leaf.procedures.clone(),
|
||||
})
|
||||
.expect("serialize")
|
||||
.to_vec()
|
||||
} else {
|
||||
to_bytes::<RkyvError>(&EndpointIntrospection {
|
||||
leaves: self
|
||||
.leaves
|
||||
.values()
|
||||
.map(|leaf| LeafIntrospectionSummary {
|
||||
leaf_name: leaf.name.clone(),
|
||||
procedures: leaf.procedures.clone(),
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.expect("serialize")
|
||||
.to_vec()
|
||||
};
|
||||
|
||||
let response_header = PacketHeader {
|
||||
packet_type: PacketType::Data,
|
||||
src_path: self.path.clone(),
|
||||
dst_path: key.return_path.clone(),
|
||||
dst_leaf: None,
|
||||
hook_id: Some(key.hook_id),
|
||||
};
|
||||
let response = DataMessage {
|
||||
procedure_id: String::new(),
|
||||
data: payload,
|
||||
end_hook: true,
|
||||
};
|
||||
let frame = encode_packet(&response_header, &response)?;
|
||||
self.hooks.remove_active(&key);
|
||||
Ok(EndpointOutcome {
|
||||
forwards: vec![(RouteDecision::Parent, frame)],
|
||||
..EndpointOutcome::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_local_data(
|
||||
&mut self,
|
||||
header: PacketHeader,
|
||||
message: DataMessage,
|
||||
) -> Result<EndpointOutcome, EndpointError> {
|
||||
let key = HookKey::new(self.path.clone(), header.hook_id.expect("validated"));
|
||||
|
||||
if self.hooks.active(&key).is_none() {
|
||||
let matches = self.hooks.pending(&key).is_some_and(|p| {
|
||||
p.caller_src_path == header.src_path && p.procedure_id == message.procedure_id
|
||||
});
|
||||
if matches {
|
||||
self.hooks.activate_pending(&key, header.src_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let Some(active) = self.hooks.active(&key).cloned() else {
|
||||
return Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
});
|
||||
};
|
||||
|
||||
if active.peer_path != header.src_path || active.procedure_id != message.procedure_id {
|
||||
self.hooks.remove_active(&key);
|
||||
self.hooks.remove_pending(&key);
|
||||
return Ok(EndpointOutcome {
|
||||
events: vec![LocalEvent::Fault {
|
||||
header: PacketHeader {
|
||||
packet_type: PacketType::Fault,
|
||||
src_path: header.src_path,
|
||||
dst_path: self.path.clone(),
|
||||
dst_leaf: None,
|
||||
hook_id: Some(key.hook_id),
|
||||
},
|
||||
message: FaultMessage {
|
||||
fault: ProtocolFault::InvalidHookPeer,
|
||||
},
|
||||
}],
|
||||
..EndpointOutcome::default()
|
||||
});
|
||||
}
|
||||
|
||||
if message.end_hook {
|
||||
self.hooks.remove_active(&key);
|
||||
}
|
||||
Ok(EndpointOutcome {
|
||||
events: vec![LocalEvent::Data { header, message }],
|
||||
..EndpointOutcome::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_local_fault(
|
||||
&mut self,
|
||||
header: PacketHeader,
|
||||
message: FaultMessage,
|
||||
) -> Result<EndpointOutcome, EndpointError> {
|
||||
let key = HookKey::new(self.path.clone(), header.hook_id.expect("validated"));
|
||||
let matches = self
|
||||
.hooks
|
||||
.active(&key)
|
||||
.is_some_and(|a| a.peer_path == header.src_path)
|
||||
|| self
|
||||
.hooks
|
||||
.pending(&key)
|
||||
.is_some_and(|p| p.caller_src_path == header.src_path);
|
||||
if !matches {
|
||||
return Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
});
|
||||
}
|
||||
self.hooks.remove_active(&key);
|
||||
self.hooks.remove_pending(&key);
|
||||
Ok(EndpointOutcome {
|
||||
events: vec![LocalEvent::Fault { header, message }],
|
||||
..EndpointOutcome::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn emit_fault_if_possible(
|
||||
&mut self,
|
||||
key: Option<HookKey>,
|
||||
fault: ProtocolFault,
|
||||
) -> Result<EndpointOutcome, EndpointError> {
|
||||
let Some(key) = key else {
|
||||
return Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
});
|
||||
};
|
||||
self.hooks.remove_pending(&key);
|
||||
self.hooks.remove_active(&key);
|
||||
let header = PacketHeader {
|
||||
packet_type: PacketType::Fault,
|
||||
src_path: self.path.clone(),
|
||||
dst_path: key.return_path.clone(),
|
||||
dst_leaf: None,
|
||||
hook_id: Some(key.hook_id),
|
||||
};
|
||||
let frame = encode_packet(&header, &FaultMessage { fault })?;
|
||||
Ok(EndpointOutcome {
|
||||
forwards: vec![(RouteDecision::Parent, frame)],
|
||||
..EndpointOutcome::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn decide_route(&self, dst_path: &[String]) -> RouteDecision {
|
||||
let child_paths: Vec<Vec<String>> = self
|
||||
.children
|
||||
.iter()
|
||||
.filter(|c| c.state == ConnectionState::Registered)
|
||||
.map(|c| c.path.clone())
|
||||
.collect();
|
||||
route_destination(
|
||||
&self.path,
|
||||
&child_paths,
|
||||
self.parent_path.is_some(),
|
||||
dst_path,
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_source_for_ingress(&self, ingress: &Ingress, src_path: &[String]) -> bool {
|
||||
match ingress {
|
||||
Ingress::Parent => self
|
||||
.parent_path
|
||||
.as_ref()
|
||||
.map_or(self.path.is_empty(), |p| p == src_path),
|
||||
Ingress::Child(path) => path == src_path,
|
||||
Ingress::Local => src_path == self.path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Endpoint for ProtocolEndpoint {
|
||||
fn path(&self) -> &[String] {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn receive(
|
||||
&mut self,
|
||||
ingress: &Ingress,
|
||||
frame: FrameBytes,
|
||||
) -> Result<EndpointOutcome, EndpointError> {
|
||||
let parsed = decode_frame(&frame)?;
|
||||
let header = parsed.deserialize_header();
|
||||
validate_header(&header)?;
|
||||
if !self.valid_source_for_ingress(ingress, &header.src_path) {
|
||||
return Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
});
|
||||
}
|
||||
|
||||
match header.packet_type {
|
||||
PacketType::Call => {
|
||||
let message = parsed.deserialize_call()?;
|
||||
if !matches!(ingress, Ingress::Parent | Ingress::Local) {
|
||||
return Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
});
|
||||
}
|
||||
validate_call(&header, &message)?;
|
||||
match self.decide_route(&header.dst_path) {
|
||||
RouteDecision::Child(idx) => Ok(EndpointOutcome {
|
||||
forwards: vec![(RouteDecision::Child(idx), frame)],
|
||||
..EndpointOutcome::default()
|
||||
}),
|
||||
RouteDecision::Parent => Ok(EndpointOutcome {
|
||||
forwards: vec![(RouteDecision::Parent, frame)],
|
||||
..EndpointOutcome::default()
|
||||
}),
|
||||
RouteDecision::Drop => Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
}),
|
||||
RouteDecision::Local => self.handle_local_call(header, message),
|
||||
}
|
||||
}
|
||||
PacketType::Data => {
|
||||
let message = parsed.deserialize_data()?;
|
||||
match self.decide_route(&header.dst_path) {
|
||||
RouteDecision::Local => self.handle_local_data(header, message),
|
||||
_ => Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
PacketType::Fault => {
|
||||
let message = parsed.deserialize_fault()?;
|
||||
match self.decide_route(&header.dst_path) {
|
||||
RouteDecision::Local => self.handle_local_fault(header, message),
|
||||
_ => Ok(EndpointOutcome {
|
||||
dropped: true,
|
||||
..EndpointOutcome::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
//! Hook state for pending and active protocol flows.
|
||||
|
||||
use alloc::{collections::BTreeMap, string::String, vec::Vec};
|
||||
|
||||
/// Hook table key scoped to the hook host path.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct HookKey {
|
||||
/// Path of the endpoint hosting the hook.
|
||||
pub return_path: Vec<String>,
|
||||
/// Hook identifier scoped to `return_path`.
|
||||
pub hook_id: u64,
|
||||
}
|
||||
|
||||
impl HookKey {
|
||||
pub fn new(return_path: Vec<String>, hook_id: u64) -> Self {
|
||||
Self {
|
||||
return_path,
|
||||
hook_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pending hook context created by a received call.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PendingHook {
|
||||
pub caller_src_path: Vec<String>,
|
||||
pub return_path: Vec<String>,
|
||||
pub hook_id: u64,
|
||||
pub procedure_id: String,
|
||||
pub dst_leaf: Option<String>,
|
||||
}
|
||||
|
||||
/// Active hook context used for ordinary data traffic.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ActiveHook {
|
||||
pub return_path: Vec<String>,
|
||||
pub hook_id: u64,
|
||||
pub peer_path: Vec<String>,
|
||||
pub procedure_id: String,
|
||||
pub dst_leaf: Option<String>,
|
||||
pub peer_finished: bool,
|
||||
}
|
||||
|
||||
/// Durable hook state tables.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct HookTable {
|
||||
pending: BTreeMap<HookKey, PendingHook>,
|
||||
active: BTreeMap<HookKey, ActiveHook>,
|
||||
}
|
||||
|
||||
impl HookTable {
|
||||
pub fn allocate_hook_id(&self, return_path: &[String]) -> u64 {
|
||||
let mut hook_id = 0u64;
|
||||
loop {
|
||||
let key = HookKey::new(return_path.to_vec(), hook_id);
|
||||
if !self.pending.contains_key(&key) && !self.active.contains_key(&key) {
|
||||
return hook_id;
|
||||
}
|
||||
hook_id = hook_id.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_pending(&mut self, pending: PendingHook) {
|
||||
// WARNING: hook tables intentionally own their path and procedure strings.
|
||||
// Hook state must outlive any individual frame buffer.
|
||||
let key = HookKey::new(pending.return_path.clone(), pending.hook_id);
|
||||
self.pending.insert(key, pending);
|
||||
}
|
||||
|
||||
pub fn insert_active(&mut self, active: ActiveHook) {
|
||||
let key = HookKey::new(active.return_path.clone(), active.hook_id);
|
||||
self.active.insert(key, active);
|
||||
}
|
||||
|
||||
pub fn activate_pending(&mut self, key: &HookKey, peer_path: Vec<String>) -> Option<()> {
|
||||
let pending = self.pending.remove(key)?;
|
||||
self.active.insert(
|
||||
key.clone(),
|
||||
ActiveHook {
|
||||
return_path: pending.return_path,
|
||||
hook_id: pending.hook_id,
|
||||
peer_path,
|
||||
procedure_id: pending.procedure_id,
|
||||
dst_leaf: pending.dst_leaf,
|
||||
peer_finished: false,
|
||||
},
|
||||
);
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn remove_pending(&mut self, key: &HookKey) -> Option<PendingHook> {
|
||||
self.pending.remove(key)
|
||||
}
|
||||
|
||||
pub fn remove_active(&mut self, key: &HookKey) -> Option<ActiveHook> {
|
||||
self.active.remove(key)
|
||||
}
|
||||
|
||||
pub fn pending(&self, key: &HookKey) -> Option<&PendingHook> {
|
||||
self.pending.get(key)
|
||||
}
|
||||
|
||||
pub fn active(&self, key: &HookKey) -> Option<&ActiveHook> {
|
||||
self.active.get(key)
|
||||
}
|
||||
|
||||
pub fn active_mut(&mut self, key: &HookKey) -> Option<&mut ActiveHook> {
|
||||
self.active.get_mut(key)
|
||||
}
|
||||
|
||||
pub fn pending_len(&self) -> usize {
|
||||
self.pending.len()
|
||||
}
|
||||
|
||||
pub fn active_len(&self) -> usize {
|
||||
self.active.len()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//! Explicit tree declaration, routing, and a small endpoint runtime.
|
||||
|
||||
mod endpoint;
|
||||
mod hook;
|
||||
mod routing;
|
||||
|
||||
pub use endpoint::{
|
||||
ChildRoute, ConnectionState, Endpoint, EndpointError, EndpointOutcome, Ingress, LeafBehavior,
|
||||
LeafSpec, LocalEvent, ProtocolEndpoint,
|
||||
};
|
||||
pub use hook::{ActiveHook, HookKey, HookTable, PendingHook};
|
||||
pub use routing::{
|
||||
DefaultRouteProvider, LeafNode, RouteDecision, RouteProvider, TreeNode, is_prefix,
|
||||
route_destination,
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Path routing helpers and explicit enum tree declarations.
|
||||
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
/// Explicit test tree declaration used for configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TreeNode {
|
||||
/// The tree root.
|
||||
Root { children: Vec<Self> },
|
||||
/// A concrete endpoint in the tree.
|
||||
Endpoint {
|
||||
segment: String,
|
||||
leaves: Vec<LeafNode>,
|
||||
children: Vec<Self>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Leaf declaration used inside the explicit tree enum.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LeafNode {
|
||||
/// Local leaf name.
|
||||
pub name: String,
|
||||
/// Supported procedures.
|
||||
pub procedures: Vec<String>,
|
||||
}
|
||||
|
||||
impl TreeNode {
|
||||
/// Flattens the tree into absolute endpoint paths.
|
||||
pub fn paths(&self) -> Vec<Vec<String>> {
|
||||
let mut output = Vec::new();
|
||||
self.collect_paths(&[], &mut output);
|
||||
output
|
||||
}
|
||||
|
||||
fn collect_paths(&self, prefix: &[String], output: &mut Vec<Vec<String>>) {
|
||||
match self {
|
||||
Self::Root { children } => {
|
||||
output.push(Vec::new());
|
||||
for child in children {
|
||||
child.collect_paths(&[], output);
|
||||
}
|
||||
}
|
||||
Self::Endpoint {
|
||||
segment, children, ..
|
||||
} => {
|
||||
let mut next = prefix.to_vec();
|
||||
next.push(segment.clone());
|
||||
output.push(next.clone());
|
||||
for child in children {
|
||||
child.collect_paths(&next, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Longest-prefix route decision.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RouteDecision {
|
||||
/// Forward to the child at the given index.
|
||||
Child(usize),
|
||||
/// Deliver locally.
|
||||
Local,
|
||||
/// Forward upward toward the parent.
|
||||
Parent,
|
||||
/// Silently drop.
|
||||
Drop,
|
||||
}
|
||||
|
||||
/// Returns `true` if `prefix` is a path prefix of `path`.
|
||||
pub fn is_prefix(prefix: &[String], path: &[String]) -> bool {
|
||||
prefix.len() <= path.len()
|
||||
&& prefix
|
||||
.iter()
|
||||
.zip(path.iter())
|
||||
.all(|(left, right)| left == right)
|
||||
}
|
||||
|
||||
/// Trait for resolving a destination path to a routing decision.
|
||||
pub trait RouteProvider {
|
||||
/// Computes the routing decision for a destination path.
|
||||
fn route_destination(
|
||||
&self,
|
||||
local_path: &[String],
|
||||
child_paths: &[Vec<String>],
|
||||
has_parent: bool,
|
||||
dst_path: &[String],
|
||||
) -> RouteDecision;
|
||||
}
|
||||
|
||||
/// Default routing implementation using the protocol's longest-prefix rule.
|
||||
pub struct DefaultRouteProvider;
|
||||
|
||||
impl RouteProvider for DefaultRouteProvider {
|
||||
fn route_destination(
|
||||
&self,
|
||||
local_path: &[String],
|
||||
child_paths: &[Vec<String>],
|
||||
has_parent: bool,
|
||||
dst_path: &[String],
|
||||
) -> RouteDecision {
|
||||
let child = child_paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, child_path)| is_prefix(child_path, dst_path))
|
||||
.max_by_key(|(_, child_path)| child_path.len())
|
||||
.map(|(index, _)| index);
|
||||
|
||||
if let Some(index) = child {
|
||||
return RouteDecision::Child(index);
|
||||
}
|
||||
if local_path == dst_path {
|
||||
return RouteDecision::Local;
|
||||
}
|
||||
if has_parent && !is_prefix(local_path, dst_path) {
|
||||
return RouteDecision::Parent;
|
||||
}
|
||||
RouteDecision::Drop
|
||||
}
|
||||
}
|
||||
|
||||
pub fn route_destination(
|
||||
local_path: &[String],
|
||||
child_paths: &[Vec<String>],
|
||||
has_parent: bool,
|
||||
dst_path: &[String],
|
||||
) -> RouteDecision {
|
||||
DefaultRouteProvider.route_destination(local_path, child_paths, has_parent, dst_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::{string::String, vec};
|
||||
|
||||
#[test]
|
||||
fn longest_prefix_wins() {
|
||||
let provider = DefaultRouteProvider;
|
||||
let children = vec![
|
||||
vec![String::from("a")],
|
||||
vec![String::from("a"), String::from("b")],
|
||||
];
|
||||
assert_eq!(
|
||||
provider.route_destination(
|
||||
&Vec::<String>::new(),
|
||||
&children,
|
||||
false,
|
||||
&[String::from("a"), String::from("b"), String::from("c")]
|
||||
),
|
||||
RouteDecision::Child(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_enum_flattens_paths() {
|
||||
let tree = TreeNode::Root {
|
||||
children: vec![TreeNode::Endpoint {
|
||||
segment: String::from("a"),
|
||||
leaves: Vec::new(),
|
||||
children: vec![TreeNode::Endpoint {
|
||||
segment: String::from("b"),
|
||||
leaves: Vec::new(),
|
||||
children: Vec::new(),
|
||||
}],
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
tree.paths(),
|
||||
vec![
|
||||
Vec::<String>::new(),
|
||||
vec![String::from("a")],
|
||||
vec![String::from("a"), String::from("b")],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user