Rebuild protocol runtime from scratch

Implement an aligned two-section frame format, a compiled prefix router, a minimal pending and active hook engine, and a header-first receive path that only decodes payloads on local delivery. Recreate the protocol-focused test suite and document the explicit framing deviation in src/protocol/PROTOCOL_CHANGES.md.
This commit is contained in:
Michael Mikovsky
2026-04-25 12:37:54 -06:00
parent 3d92b5cf0d
commit 080f55ddd3
16 changed files with 410 additions and 571 deletions
+31 -45
View File
@@ -1,7 +1,4 @@
//! Packet builders and basic endpoint configuration.
//!
//! These helpers map to `PROTOCOL.md` sections covering packet construction,
//! call headers, and hook declaration fields.
//! Packet builders and endpoint construction.
use alloc::{collections::BTreeSet, string::String, vec::Vec};
@@ -49,30 +46,6 @@ impl ProtocolEndpoint {
Ok((header, call))
}
fn register_outbound_call_hook(
&mut self,
header: &PacketHeader,
call: &CallMessage,
) -> Result<(), EndpointError> {
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: header.dst_path.clone(),
procedure_id: call.procedure_id.clone(),
dst_leaf: header.dst_leaf.clone(),
local_ended: false,
peer_ended: false,
})
.is_err()
{
return Err(EndpointError::Validation(ValidationError::InvalidHookId));
}
Ok(())
}
fn prepare_data(
&self,
dst_path: Vec<String>,
@@ -101,14 +74,30 @@ impl ProtocolEndpoint {
Ok((header, message))
}
/// Creates a runtime endpoint with static tree topology and leaf metadata.
///
/// ```
/// use unshell::protocol::tree::{Endpoint, ProtocolEndpoint};
///
/// let endpoint = ProtocolEndpoint::new(Vec::new(), None, Vec::new(), Vec::new());
/// assert!(endpoint.path().is_empty());
/// ```
fn register_outbound_call_hook(
&mut self,
header: &PacketHeader,
call: &CallMessage,
) -> Result<(), EndpointError> {
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: header.dst_path.clone(),
procedure_id: call.procedure_id.clone(),
dst_leaf: header.dst_leaf.clone(),
local_ended: false,
peer_ended: false,
})
.is_err()
{
return Err(EndpointError::Validation(ValidationError::InvalidHookId));
}
Ok(())
}
#[must_use]
pub fn new(
path: Vec<String>,
@@ -135,7 +124,6 @@ impl ProtocolEndpoint {
}
}
/// Registers an endpoint-local procedure identifier.
pub fn add_endpoint_procedure(
&mut self,
procedure_id: impl Into<String>,
@@ -146,13 +134,11 @@ impl ProtocolEndpoint {
Ok(())
}
/// Allocates a locally unique hook id.
#[must_use]
pub fn allocate_hook_id(&mut self) -> u64 {
self.hooks.allocate_hook_id(&self.path)
}
/// Builds an outbound `Call` packet and pre-registers active hook state when requested.
pub fn make_call(
&mut self,
dst_path: Vec<String>,
@@ -167,7 +153,6 @@ impl ProtocolEndpoint {
Ok(encode_packet(&header, &call)?)
}
/// Routes one locally originated `Call` without an encode/decode roundtrip.
pub fn send_call(
&mut self,
dst_path: Vec<String>,
@@ -186,7 +171,6 @@ impl ProtocolEndpoint {
}
}
/// Builds an outbound `Data` packet for an existing hook.
pub fn make_data(
&self,
dst_path: Vec<String>,
@@ -199,7 +183,6 @@ impl ProtocolEndpoint {
Ok(encode_packet(&header, &message)?)
}
/// Routes one locally originated `Data` packet without an encode/decode roundtrip.
pub fn send_data(
&mut self,
dst_path: Vec<String>,
@@ -211,9 +194,12 @@ impl ProtocolEndpoint {
let (header, message) = self.prepare_data(dst_path, hook_id, procedure_id, data, end_hook)?;
if end_hook {
let key = HookKey::new(self.path.clone(), hook_id);
if self.hooks.mark_local_end(&key) {
self.hooks.remove_active(&key);
let sender_key = self
.hooks
.resolve_active_key(&self.path, hook_id, &self.path)
.unwrap_or_else(|| HookKey::new(self.path.clone(), hook_id));
if self.hooks.mark_local_end(&sender_key) {
self.hooks.remove_active(&sender_key);
}
}
-40
View File
@@ -1,9 +1,4 @@
//! Core endpoint state and externally visible types.
//!
//! This file maps to the protocol concepts described in `PROTOCOL.md`:
//! - Packet processing entry points and local delivery state: "Packet Types"
//! - Child registration state used during route selection: "Routing"
//! - Hook-hosting endpoint state: "Hooks"
use alloc::{
collections::{BTreeMap, BTreeSet},
@@ -18,26 +13,19 @@ use crate::protocol::{
use super::super::{CompiledRoutes, HookTable, RouteDecision};
/// Local connection state used for child route eligibility.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnectionState {
/// The child exists in the static topology but is not currently routable.
Unregistered,
/// The child may receive routed traffic.
Registered,
}
/// Child path plus current registration state.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChildRoute {
/// Absolute child endpoint path.
pub path: Vec<String>,
/// Whether the child currently participates in routing.
pub state: ConnectionState,
}
impl ChildRoute {
/// Convenience constructor for the common registered-child case.
#[must_use]
pub fn registered(path: Vec<String>) -> Self {
Self {
@@ -47,62 +35,43 @@ impl ChildRoute {
}
}
/// Static leaf metadata used for procedure dispatch and introspection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LeafSpec {
/// Stable local leaf name.
pub name: String,
/// Procedures supported by the leaf.
pub procedures: Vec<String>,
}
/// Where a frame entered the local endpoint.
///
/// This corresponds to the authority and ingress checks described in the
/// `PROTOCOL.md` routing and call sections.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Ingress {
/// Received from the parent link.
Parent,
/// Received from the child at the given absolute path.
Child(Vec<String>),
/// Injected locally by code running on this endpoint.
Local,
}
/// Locally delivered protocol events.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LocalEvent {
/// A call reached this endpoint runtime.
Call {
header: PacketHeader,
message: CallMessage,
},
/// Hook data reached this endpoint runtime.
Data {
header: PacketHeader,
message: DataMessage,
},
/// A protocol fault reached this endpoint runtime.
Fault {
header: PacketHeader,
message: FaultMessage,
},
}
/// Result of processing one framed packet.
#[derive(Debug, Default)]
pub struct EndpointOutcome {
/// Forwarding action to perform after local processing.
pub forward: Option<(RouteDecision, FrameBytes)>,
/// Event delivered to the local runtime consumer.
pub event: Option<LocalEvent>,
/// Whether the packet was intentionally dropped with no other side effects.
pub dropped: bool,
}
impl EndpointOutcome {
/// Returns an outcome that only forwards one frame.
#[must_use]
pub fn forward(route: RouteDecision, frame: FrameBytes) -> Self {
Self {
@@ -112,7 +81,6 @@ impl EndpointOutcome {
}
}
/// Returns an outcome that only delivers one local event.
#[must_use]
pub fn event(event: LocalEvent) -> Self {
Self {
@@ -122,7 +90,6 @@ impl EndpointOutcome {
}
}
/// Returns an outcome that silently drops the packet.
#[must_use]
pub fn dropped() -> Self {
Self {
@@ -133,12 +100,9 @@ impl EndpointOutcome {
}
}
/// Errors returned while decoding or validating a packet.
#[derive(Debug)]
pub enum EndpointError {
/// The frame could not be decoded.
Frame(FrameError),
/// The decoded packet violated protocol invariants.
Validation(ValidationError),
}
@@ -165,12 +129,9 @@ impl From<ValidationError> for EndpointError {
}
}
/// Public packet-processing trait exposed by the tree runtime.
pub trait Endpoint {
/// Returns the absolute endpoint path.
fn path(&self) -> &[String];
/// Processes one incoming frame from the given ingress side.
fn receive(
&mut self,
ingress: &Ingress,
@@ -178,7 +139,6 @@ pub trait Endpoint {
) -> Result<EndpointOutcome, EndpointError>;
}
/// Stateful endpoint runtime implementing routing, hooks, and local dispatch.
#[derive(Debug, Default)]
pub struct ProtocolEndpoint {
pub(crate) path: Vec<String>,
+16 -35
View File
@@ -1,7 +1,4 @@
//! Hook-state transitions and route helpers.
//!
//! These methods implement the hook lifecycle described in `PROTOCOL.md`:
//! pending contexts, active contexts, peer validation, and fault emission.
use alloc::string::String;
@@ -13,7 +10,6 @@ use super::super::{HookKey, RouteDecision};
use super::core::{EndpointError, EndpointOutcome, Ingress, LocalEvent, ProtocolEndpoint};
impl ProtocolEndpoint {
/// Emits a protocol fault only when the original call declared a response hook.
pub(crate) fn emit_fault_if_possible(
&mut self,
key: Option<HookKey>,
@@ -34,18 +30,13 @@ impl ProtocolEndpoint {
hook_id: Some(key.hook_id),
};
let message = FaultMessage { fault };
let route = self.decide_route(&key.return_path);
match route {
match self.decide_route(&key.return_path) {
RouteDecision::Local => Ok(EndpointOutcome::event(LocalEvent::Fault { header, message })),
_ => {
let frame = encode_packet(&header, &message)?;
Ok(EndpointOutcome::forward(route, frame))
}
route => Ok(EndpointOutcome::forward(route, encode_packet(&header, &message)?)),
}
}
/// Handles locally delivered hook `Data` packets.
pub(crate) fn handle_local_data(
&mut self,
header: PacketHeader,
@@ -90,44 +81,34 @@ impl ProtocolEndpoint {
Ok(EndpointOutcome::event(LocalEvent::Data { header, message }))
}
/// Handles locally delivered hook `Fault` packets.
pub(crate) fn handle_local_fault(
&mut self,
header: PacketHeader,
message: FaultMessage,
) -> Result<EndpointOutcome, EndpointError> {
let Some(key) = self.hooks.resolve_active_key(
&self.path,
header.hook_id.expect("validated"),
&header.src_path,
) else {
let key = HookKey::new(self.path.clone(), header.hook_id.expect("validated"));
let matches_pending = self
.hooks
.pending(&key)
.is_some_and(|pending| pending.caller_src_path == header.src_path);
if !matches_pending {
return Ok(EndpointOutcome::dropped());
}
self.hooks.remove_pending(&key);
let hook_id = header.hook_id.expect("validated");
if let Some(key) = self.hooks.resolve_active_key(&self.path, hook_id, &header.src_path) {
self.hooks.remove_active(&key);
return Ok(EndpointOutcome::event(LocalEvent::Fault { header, message }));
};
}
self.hooks.remove_active(&key);
let pending_key = HookKey::new(self.path.clone(), hook_id);
if self
.hooks
.pending(&pending_key)
.is_some_and(|pending| pending.caller_src_path == header.src_path)
{
self.hooks.remove_pending(&pending_key);
return Ok(EndpointOutcome::event(LocalEvent::Fault { header, message }));
}
Ok(EndpointOutcome::event(LocalEvent::Fault { header, message }))
Ok(EndpointOutcome::dropped())
}
/// Chooses the next hop using the protocol's longest-prefix routing rule.
pub(crate) fn decide_route(&self, dst_path: &[String]) -> RouteDecision {
self.routing.route(dst_path)
}
/// Validates whether a source path is attributable to the ingress side.
///
/// Rationale: this looks backwards at first because parent ingress accepts
/// non-local source paths. That is required for multi-hop routing, where a
/// parent forwards traffic originating from ancestors or siblings.
pub(crate) fn valid_source_for_ingress(&self, ingress: &Ingress, src_path: &[String]) -> bool {
match ingress {
Ingress::Parent => {
+6 -11
View File
@@ -1,7 +1,4 @@
//! Introspection response generation.
//!
//! This code implements the reserved empty-procedure behavior from the
//! introspection sections of `PROTOCOL.md`.
use alloc::string::String;
use rkyv::{rancor::Error as RkyvError, to_bytes};
@@ -15,7 +12,6 @@ use super::super::HookKey;
use super::core::{EndpointError, EndpointOutcome, ProtocolEndpoint};
impl ProtocolEndpoint {
/// Handles the reserved introspection procedure.
pub(crate) fn handle_introspection(
&mut self,
header: &PacketHeader,
@@ -68,20 +64,19 @@ impl ProtocolEndpoint {
data: payload,
end_hook: true,
};
self.hooks.remove_active(&key);
let route = self.decide_route(&key.return_path);
match route {
if self.hooks.mark_local_end(&key) {
self.hooks.remove_active(&key);
}
match self.decide_route(&key.return_path) {
super::super::RouteDecision::Local => Ok(EndpointOutcome::event(
super::core::LocalEvent::Data {
header: response_header,
message: response,
},
)),
_ => {
let frame = encode_packet(&response_header, &response)?;
Ok(EndpointOutcome::forward(route, frame))
}
route => Ok(EndpointOutcome::forward(route, encode_packet(&response_header, &response)?)),
}
}
}
-10
View File
@@ -1,14 +1,4 @@
//! Endpoint runtime and traits.
//!
//! This module provides the core logic for a protocol endpoint, including
//! packet ingress, routing decisions, and hook lifecycle management.
//!
//! Protocol section mapping:
//! - `builders`: packet construction and outbound hook declaration
//! - `receive`: framed ingress, authority checks, and route selection
//! - `hooks`: hook lifecycle, peer validation, and fault emission
//! - `introspection`: reserved empty-procedure discovery responses
//! - `core`: externally visible endpoint state and result types
mod builders;
mod core;
+53 -70
View File
@@ -1,13 +1,10 @@
//! Packet ingress and local call dispatch.
//!
//! This file implements the transport-facing packet entry point and maps it to
//! the `Call`, `Data`, and `Fault` sections of `PROTOCOL.md`.
use crate::protocol::types::{ArchivedCallMessage, ArchivedDataMessage, ArchivedFaultMessage};
use crate::protocol::{
CallMessage, PacketType, ProtocolFault, decode_frame, deserialize_archived_bytes,
introspection::INTROSPECTION_PROCEDURE_ID, validate_call, validate_header,
};
use crate::protocol::types::{ArchivedCallMessage, ArchivedDataMessage, ArchivedFaultMessage};
use super::super::{HookKey, PendingHook, RouteDecision};
use super::core::{
@@ -15,7 +12,6 @@ use super::core::{
};
impl ProtocolEndpoint {
/// Handles a locally delivered `Call` packet after routing selected `Local`.
pub(crate) fn handle_local_call(
&mut self,
header: crate::protocol::PacketHeader,
@@ -26,7 +22,26 @@ impl ProtocolEndpoint {
.as_ref()
.map(|hook| HookKey::new(hook.return_path.clone(), hook.hook_id));
if let Some(hook) = &message.response_hook
&& hook.return_path != self.path
&& self
.hooks
.insert_pending(PendingHook {
return_path: hook.return_path.clone(),
hook_id: hook.hook_id,
caller_src_path: header.src_path.clone(),
procedure_id: message.procedure_id.clone(),
dst_leaf: header.dst_leaf.clone(),
})
.is_err()
{
return self.emit_fault_if_possible(key, ProtocolFault::INTERNAL_ERROR);
}
if message.procedure_id == INTROSPECTION_PROCEDURE_ID {
if let Some(key) = &key {
self.hooks.activate_pending(key);
}
return self.handle_introspection(&header, key);
}
@@ -34,11 +49,7 @@ impl ProtocolEndpoint {
Some(leaf_name) => self
.leaves
.get(leaf_name)
.map(|leaf| {
leaf.procedures
.iter()
.any(|procedure| procedure == &message.procedure_id)
})
.map(|leaf| leaf.procedures.iter().any(|procedure| procedure == &message.procedure_id))
.unwrap_or(false),
None => self.endpoint_procedures.contains(&message.procedure_id),
};
@@ -56,28 +67,10 @@ impl ProtocolEndpoint {
return self.emit_fault_if_possible(key, fault);
}
if let Some(hook) = &message.response_hook
&& hook.return_path != self.path
if let Some(key) = &key
&& self.hooks.activate_pending(key).is_none()
{
if self
.hooks
.insert_pending(PendingHook {
return_path: hook.return_path.clone(),
hook_id: hook.hook_id,
caller_src_path: header.src_path.clone(),
procedure_id: message.procedure_id.clone(),
dst_leaf: header.dst_leaf.clone(),
})
.is_err()
{
return self.emit_fault_if_possible(key, ProtocolFault::INTERNAL_ERROR);
}
if let Some(key) = &key
&& self.hooks.activate_pending(key).is_none()
{
return self.emit_fault_if_possible(Some(key.clone()), ProtocolFault::INTERNAL_ERROR);
}
return self.emit_fault_if_possible(Some(key.clone()), ProtocolFault::INTERNAL_ERROR);
}
Ok(EndpointOutcome::event(LocalEvent::Call { header, message }))
@@ -112,9 +105,7 @@ impl Endpoint for ProtocolEndpoint {
RouteDecision::Child(index) => {
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
}
RouteDecision::Parent => {
Ok(EndpointOutcome::forward(RouteDecision::Parent, frame))
}
RouteDecision::Parent => Ok(EndpointOutcome::forward(RouteDecision::Parent, frame)),
RouteDecision::Drop => Ok(EndpointOutcome::dropped()),
RouteDecision::Local => {
let (header, payload) = parsed.into_parts();
@@ -125,44 +116,36 @@ impl Endpoint for ProtocolEndpoint {
}
}
}
PacketType::Data => {
match self.decide_route(&header.dst_path) {
RouteDecision::Local => {
let (header, payload) = parsed.into_parts();
let message = deserialize_archived_bytes::<
ArchivedDataMessage,
crate::protocol::DataMessage,
>(payload)?;
self.handle_local_data(header, message)
}
RouteDecision::Child(index) => {
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
}
RouteDecision::Parent => {
Ok(EndpointOutcome::forward(RouteDecision::Parent, frame))
}
RouteDecision::Drop => Ok(EndpointOutcome::dropped()),
PacketType::Data => match self.decide_route(&header.dst_path) {
RouteDecision::Local => {
let (header, payload) = parsed.into_parts();
let message = deserialize_archived_bytes::<
ArchivedDataMessage,
crate::protocol::DataMessage,
>(payload)?;
self.handle_local_data(header, message)
}
}
PacketType::Fault => {
match self.decide_route(&header.dst_path) {
RouteDecision::Local => {
let (header, payload) = parsed.into_parts();
let message = deserialize_archived_bytes::<
ArchivedFaultMessage,
crate::protocol::FaultMessage,
>(payload)?;
self.handle_local_fault(header, message)
}
RouteDecision::Child(index) => {
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
}
RouteDecision::Parent => {
Ok(EndpointOutcome::forward(RouteDecision::Parent, frame))
}
RouteDecision::Drop => Ok(EndpointOutcome::dropped()),
RouteDecision::Child(index) => {
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
}
}
RouteDecision::Parent => Ok(EndpointOutcome::forward(RouteDecision::Parent, frame)),
RouteDecision::Drop => Ok(EndpointOutcome::dropped()),
},
PacketType::Fault => match self.decide_route(&header.dst_path) {
RouteDecision::Local => {
let (header, payload) = parsed.into_parts();
let message = deserialize_archived_bytes::<
ArchivedFaultMessage,
crate::protocol::FaultMessage,
>(payload)?;
self.handle_local_fault(header, message)
}
RouteDecision::Child(index) => {
Ok(EndpointOutcome::forward(RouteDecision::Child(index), frame))
}
RouteDecision::Parent => Ok(EndpointOutcome::forward(RouteDecision::Parent, frame)),
RouteDecision::Drop => Ok(EndpointOutcome::dropped()),
},
}
}
}
+61 -107
View File
@@ -5,14 +5,11 @@ 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 {
/// Creates a host-scoped key from the return path and hook identifier.
#[must_use]
pub fn new(return_path: Vec<String>, hook_id: u64) -> Self {
Self {
@@ -22,6 +19,16 @@ impl HookKey {
}
}
/// Pending hook context used only for fault attribution before activation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingHook {
pub return_path: Vec<String>,
pub hook_id: u64,
pub caller_src_path: Vec<String>,
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 {
@@ -34,14 +41,10 @@ pub struct ActiveHook {
pub peer_ended: bool,
}
/// Pending hook context used only for fault attribution before activation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingHook {
pub return_path: Vec<String>,
pub hook_id: u64,
pub caller_src_path: Vec<String>,
pub procedure_id: String,
pub dst_leaf: Option<String>,
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct PeerHookKey {
hook_id: u64,
peer_path: Vec<String>,
}
/// Duplicate hook insertion error.
@@ -49,73 +52,33 @@ pub struct PendingHook {
pub struct HookConflict;
/// Durable hook state tables.
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct HookTable {
pending: BTreeMap<u64, BTreeMap<Vec<String>, PendingHook>>,
active: BTreeMap<u64, BTreeMap<Vec<String>, ActiveHook>>,
active_by_peer: BTreeMap<u64, BTreeMap<Vec<String>, Vec<String>>>,
pending: BTreeMap<HookKey, PendingHook>,
active: BTreeMap<HookKey, ActiveHook>,
active_by_peer: BTreeMap<PeerHookKey, HookKey>,
next_id: u64,
}
impl Default for HookTable {
fn default() -> Self {
Self {
pending: BTreeMap::new(),
active: BTreeMap::new(),
active_by_peer: BTreeMap::new(),
next_id: 1,
}
}
}
impl HookTable {
/// Allocates the next locally unique hook identifier.
///
/// Hook IDs are scoped by return path, so this counter only needs to be
/// unique within one endpoint runtime.
#[must_use]
pub fn allocate_hook_id(&mut self, _return_path: &[String]) -> u64 {
let id = self.next_id;
self.next_id = self.next_id.wrapping_add(1);
let id = self.next_id.max(1);
self.next_id = id.wrapping_add(1);
id
}
/// Inserts a pending hook created by a received call.
pub fn insert_pending(&mut self, pending: PendingHook) -> Result<(), HookConflict> {
if self.pending(&HookKey::new(pending.return_path.clone(), pending.hook_id)).is_some()
|| self.active(&HookKey::new(pending.return_path.clone(), pending.hook_id)).is_some()
{
let key = HookKey::new(pending.return_path.clone(), pending.hook_id);
if self.pending.contains_key(&key) || self.active.contains_key(&key) {
return Err(HookConflict);
}
self.pending
.entry(pending.hook_id)
.or_default()
.insert(pending.return_path.clone(), pending);
self.pending.insert(key, pending);
Ok(())
}
/// Inserts an already-active hook flow.
pub fn insert_active(&mut self, active: ActiveHook) -> Result<(), HookConflict> {
let key = HookKey::new(active.return_path.clone(), active.hook_id);
if self.pending(&key).is_some() || self.active(&key).is_some() {
return Err(HookConflict);
}
self.active_by_peer
.entry(active.hook_id)
.or_default()
.insert(active.peer_path.clone(), active.return_path.clone());
self.active
.entry(active.hook_id)
.or_default()
.insert(active.return_path.clone(), active);
Ok(())
}
/// Promotes one pending hook into active state after local acceptance.
pub fn activate_pending(&mut self, key: &HookKey) -> Option<()> {
let pending = self.remove_pending(key)?;
let pending = self.pending.remove(key)?;
self.insert_active(ActiveHook {
return_path: pending.return_path,
hook_id: pending.hook_id,
@@ -129,55 +92,50 @@ impl HookTable {
Some(())
}
/// Removes a pending hook entry.
pub fn remove_pending(&mut self, key: &HookKey) -> Option<PendingHook> {
let hooks = self.pending.get_mut(&key.hook_id)?;
let pending = hooks.remove(key.return_path.as_slice())?;
if hooks.is_empty() {
self.pending.remove(&key.hook_id);
pub fn insert_active(&mut self, active: ActiveHook) -> Result<(), HookConflict> {
let key = HookKey::new(active.return_path.clone(), active.hook_id);
let peer_key = PeerHookKey {
hook_id: active.hook_id,
peer_path: active.peer_path.clone(),
};
if self.pending.contains_key(&key)
|| self.active.contains_key(&key)
|| self.active_by_peer.contains_key(&peer_key)
{
return Err(HookConflict);
}
Some(pending)
self.active_by_peer.insert(peer_key, key.clone());
self.active.insert(key, active);
Ok(())
}
/// Removes an active hook entry.
pub fn remove_active(&mut self, key: &HookKey) -> Option<ActiveHook> {
let hooks = self.active.get_mut(&key.hook_id)?;
let active = hooks.remove(key.return_path.as_slice())?;
if hooks.is_empty() {
self.active.remove(&key.hook_id);
}
pub fn remove_pending(&mut self, key: &HookKey) -> Option<PendingHook> {
self.pending.remove(key)
}
if let Some(peer_index) = self.active_by_peer.get_mut(&key.hook_id) {
peer_index.remove(active.peer_path.as_slice());
if peer_index.is_empty() {
self.active_by_peer.remove(&key.hook_id);
}
}
pub fn remove_active(&mut self, key: &HookKey) -> Option<ActiveHook> {
let active = self.active.remove(key)?;
self.active_by_peer.remove(&PeerHookKey {
hook_id: active.hook_id,
peer_path: active.peer_path.clone(),
});
Some(active)
}
/// Returns a pending hook by its host-scoped key.
#[must_use]
pub fn pending(&self, key: &HookKey) -> Option<&PendingHook> {
self.pending
.get(&key.hook_id)?
.get(key.return_path.as_slice())
self.pending.get(key)
}
/// Returns an active hook by its host-scoped key.
#[must_use]
pub fn active(&self, key: &HookKey) -> Option<&ActiveHook> {
self.active.get(&key.hook_id)?.get(key.return_path.as_slice())
self.active.get(key)
}
/// Returns mutable access to an active hook by its host-scoped key.
pub fn active_mut(&mut self, key: &HookKey) -> Option<&mut ActiveHook> {
self.active
.get_mut(&key.hook_id)?
.get_mut(key.return_path.as_slice())
self.active.get_mut(key)
}
/// Resolves one active hook key from either the host side or the peer side.
#[must_use]
pub fn resolve_active_key(
&self,
@@ -186,18 +144,17 @@ impl HookTable {
peer_path: &[String],
) -> Option<HookKey> {
let host_key = HookKey::new(return_path.to_vec(), hook_id);
if self.active(&host_key).is_some() {
if self.active.contains_key(&host_key) {
return Some(host_key);
}
self.active_by_peer
.get(&hook_id)?
.get(peer_path)
.get(&PeerHookKey {
hook_id,
peer_path: peer_path.to_vec(),
})
.cloned()
.map(|return_path| HookKey::new(return_path, hook_id))
}
/// Marks one locally-originated final data packet.
pub fn mark_local_end(&mut self, key: &HookKey) -> bool {
let Some(active) = self.active_mut(key) else {
return false;
@@ -206,7 +163,6 @@ impl HookTable {
active.peer_ended
}
/// Marks one peer-originated final data packet.
pub fn mark_peer_end(&mut self, key: &HookKey) -> bool {
let Some(active) = self.active_mut(key) else {
return false;
@@ -215,15 +171,13 @@ impl HookTable {
active.local_ended
}
/// Returns whether one key still has pending or active state.
#[must_use]
pub fn contains(&self, key: &HookKey) -> bool {
self.pending(key).is_some() || self.active(key).is_some()
}
/// Returns the number of active hooks.
#[must_use]
pub fn active_len(&self) -> usize {
self.active.values().map(BTreeMap::len).sum()
self.active.len()
}
#[must_use]
pub fn pending_len(&self) -> usize {
self.pending.len()
}
}
+5 -32
View File
@@ -5,9 +5,7 @@ use alloc::{collections::BTreeMap, string::String, vec, 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>,
@@ -18,14 +16,11 @@ pub enum TreeNode {
/// 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);
@@ -57,13 +52,9 @@ impl TreeNode {
/// 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,
}
@@ -82,7 +73,6 @@ struct RouteTrieNode {
}
impl CompiledRoutes {
/// Compiles the registered-child prefixes into a trie once.
#[must_use]
pub fn new(local_path: &[String], child_paths: &[Vec<String>], has_parent: bool) -> Self {
let mut table = Self {
@@ -121,7 +111,6 @@ impl CompiledRoutes {
self.nodes[node_index].best_child = Some(index);
}
/// Resolves one destination path using one segment walk.
#[must_use]
pub fn route(&self, dst_path: &[String]) -> RouteDecision {
if !is_prefix(&self.local_path, dst_path) {
@@ -192,27 +181,11 @@ impl RouteProvider for DefaultRouteProvider {
I: IntoIterator,
I::Item: AsRef<[String]>,
{
let mut best_index = None;
let mut max_len = 0;
for (index, child_path) in child_paths.into_iter().enumerate() {
let path = child_path.as_ref();
if is_prefix(path, dst_path) && path.len() > max_len {
max_len = path.len();
best_index = Some(index);
}
}
if let Some(index) = best_index {
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
let child_paths = child_paths
.into_iter()
.map(|child| child.as_ref().to_vec())
.collect::<Vec<_>>();
CompiledRoutes::new(local_path, &child_paths, has_parent).route(dst_path)
}
}