diff --git a/Cargo.lock b/Cargo.lock index 207f8b5..8400a75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -851,11 +851,11 @@ name = "unshell" version = "0.1.0" dependencies = [ "chrono", + "crossbeam-channel", "rkyv", "static_init", "thiserror", "unshell-macros", - "unshell-protocol", ] [[package]] @@ -874,14 +874,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "unshell-protocol" -version = "0.1.0" -dependencies = [ - "crossbeam-channel", - "rkyv", -] - [[package]] name = "ush-obfuscate" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7c0ebd8..b18814b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ "unshell-macros-core", "unshell-macros", - "unshell-protocol", + # "unshell-protocol", "unshell-leaves/leaf-pty", ] @@ -33,7 +33,7 @@ portable-pty = "0.9.0" crossbeam-channel = "0.5.15" unshell = { path = "." } -unshell-protocol = { path = "./unshell-protocol" } +# unshell-protocol = { path = "./unshell-protocol" } unshell-macros-core = { path = "./unshell-macros-core" } unshell-macros = { path = "./unshell-macros" } @@ -70,10 +70,12 @@ chrono = { workspace = true, optional = true } static_init = { workspace = true } unshell-macros = { workspace = true } -unshell-protocol = { workspace = true } +# unshell-protocol = { workspace = true } # unshell-runtime = { workspace = true } # unshell-leaves = { workspace = true } +[dev-dependencies] +crossbeam-channel.workspace = true [profile.minimize] inherits = "release" diff --git a/LEAF_MACRO_INTERFACE.md b/LEAF_MACRO_INTERFACE.md new file mode 100644 index 0000000..9fa798e --- /dev/null +++ b/LEAF_MACRO_INTERFACE.md @@ -0,0 +1,810 @@ +# Macro-Generated Leaf Interface Design + +**Status:** Draft +**Last updated:** 2026-05-28 +**Primary use case:** Remote PTY sessions over hook-backed UnShell packets + +## Summary + +This document proposes a generated leaf interface for UnShell. The goal is to make +stateful leaves, such as a remote PTY leaf, easy to write without forcing every leaf +author to hand-code packet draining, procedure dispatch, session lookup, hook +lifetime handling, and retry-safe final-frame cleanup. + +The user writes the application logic: + +- the leaf state struct +- one or more session types for long-running hook-backed conversations +- one or more procedure types for one-packet operations +- payload encoding and decoding +- OS-specific behavior, such as spawning or polling a PTY + +The macro generates the plumbing: + +- the `Leaf` implementation +- the generated wrapper that owns session stores and retry queues +- inbound packet filtering for the leaf's procedure ids +- per-packet procedure dispatch +- per-hook session dispatch +- retry-safe outbound flushing +- final-frame session removal only after routing succeeds + +The macro should generate ordinary Rust. No runtime registry, no boxed procedure +objects, and no hidden dynamic dispatch in the hot path. + +## Problem + +The current `Leaf` trait is deliberately small: + +```rust +pub trait Leaf { + fn get_id(&self) -> u32; + + fn update(&mut self, endpoint: &mut Endpoint); +} +``` + +That makes the protocol runtime flexible, but it also means every non-trivial leaf +has to solve the same set of problems by hand: + +- select only the inbound packets that belong to this leaf +- distinguish one-shot procedures from long-running sessions +- group session packets by `hook_id` +- keep per-session application state +- build response packets with the right `hook_id`, path, procedure id, and `end_hook` +- retry failed outbound packets without losing stream progress +- remove session state only after final packets route successfully +- avoid consuming packets intended for other leaves + +A remote PTY leaf makes this pain obvious. A PTY session is bidirectional and long +lived. The same hook carries `Open`, `Input`, `Resize`, `StdinEof`, `Output`, `Exit`, +and errors. The endpoint already owns hook authorization, but the leaf still needs a +safe session state machine. + +## Goals + +- Automate the repetitive `Leaf::update` machinery. +- Keep application logic explicit and testable. +- Keep `unshell-protocol` minimal and no_std-friendly. +- Keep OS-specific PTY code outside `unshell-protocol`. +- Preserve the new endpoint hook model: downward packets pave hooks, final packets close hooks. +- Make final-frame retries hard to get wrong. +- Keep generated runtime code static and size-conscious. +- Let sessions and procedures mutate shared leaf state without directly accessing each other. +- Make multiple sessions of the same type cheap and predictable. + +## Non-Goals + +- Do not define a full actor framework. +- Do not add async requirements to the protocol layer. +- Do not make sessions discover or mutate other sessions directly. +- Do not introduce `Vec>` or `Vec>` runtime registries. +- Do not hide PTY business logic inside the macro. +- Do not add source-path fields to `Packet` just for PTY. The PTY `Open` payload can carry the reply path. + +## Current Protocol Assumptions + +The endpoint routing layer now owns hook lifetime: + +```text +validated downward packet, end_hook=false -> open or refresh peer-bound hook +validated downward packet, end_hook=true -> close hook after successful route or delivery +validated upward packet -> require matching hook +validated upward packet, end_hook=true -> close hook after successful route or delivery +``` + +For PTY, this means: + +- `Open` uses `end_hook = false` because it expects returned output. +- `Input`, `Resize`, `StdinEof`, and `Terminate` use `end_hook = false`. +- `StdinEof` is not `end_hook`. EOF closes stdin, not the whole PTY session. +- `Abort` may use `end_hook = true` if no acknowledgement is expected. +- `Output` uses `end_hook = false`. +- `Exit` or fatal `Error` uses `end_hook = true`. + +## Crate Boundary + +```text +unshell-protocol + Endpoint + Packet + Leaf + hook routing rules + filtered inbound drain API + +unshell-runtime or unshell-leaves + real PTY worker implementation + std-only integrations + portable-pty adapter + +unshell-macros + tiny proc-macro shim + +unshell-macros-core + syn, quote, proc-macro2, deluxe or darling + parser and code generator tests +``` + +The generated code runs in the final binary. Macro parsing dependencies do not. +That means `syn`, `quote`, `deluxe`, and `darling` are acceptable in the macro crates, +but not in the protocol runtime. + +## Required Endpoint Addition + +The generated leaf must be able to drain only the packets it owns. The current +`take_inbound_clear` drains a whole local queue, which is unsafe once multiple +application leaves share an endpoint. + +Add a filtered drain API: + +```rust +impl Endpoint { + /// Drains packets from `local_id` that match `predicate`, preserving all other + /// packets in their original relative order. + pub fn take_inbound_matching(&mut self, local_id: u32, predicate: P, f: F) + where + P: FnMut(&Packet) -> bool, + F: FnMut(&Packet); +} +``` + +For generated leaves, matching usually means: + +```rust +packet.procedure_id == PROC_PTY + || packet.procedure_id == PROC_PING + || packet.procedure_id == PROC_CAPABILITIES +``` + +This is the one endpoint API the macro needs before it can be safe in mixed-leaf endpoints. + +## User-Facing Macro + +Use an attribute macro that wraps a user-owned state struct and generates a leaf type. +A derive macro alone cannot add storage fields to the original struct, so the macro +must generate a companion wrapper. + +```rust +#[unshell_leaf( + leaf = RemotePtyLeaf, + id = LEAF_REMOTE_PTY, + sessions(PtySession), + procedures(PingProcedure, CapabilitiesProcedure) +)] +pub struct RemotePtyState { + max_sessions: usize, + default_rows: u16, + default_cols: u16, +} +``` + +The macro emits roughly: + +```rust +pub struct RemotePtyLeaf { + state: RemotePtyState, + pty_sessions: SessionStore, + retry: PacketQueue, +} + +impl RemotePtyLeaf { + pub fn new(state: RemotePtyState) -> Self { ... } +} + +impl Leaf for RemotePtyLeaf { + fn get_id(&self) -> u32 { + LEAF_REMOTE_PTY + } + + fn update(&mut self, endpoint: &mut Endpoint) { + ... generated dispatch ... + } +} +``` + +The leaf wrapper is the object stored in `Endpoint::leaves`. The state struct stays +small and owned by the user. + +## Core Model + +```text +Endpoint inbound queue + | + v ++-------------------------------+ +| generated RemotePtyLeaf | +| | +| state: RemotePtyState | +| sessions: SessionStore | +| retry: PacketQueue | ++-------------------------------+ + | + +--> Procedure packet -> Procedure::handle(...) + | + +--> Session packet -> by hook_id + create or update session + session queues outbound frames + generated flush handles retry +``` + +Procedures and sessions can both mutate `RemotePtyState`. They cannot directly +borrow or mutate each other. Communication between them must happen through leaf +state or through packets. + +## Sessions + +A session is a long-running hook-backed conversation. PTY is the main example. + +```rust +pub trait Session { + /// All packets for this session type use this outer procedure id. + const PROCEDURE_ID: u32; + + /// State owned for one active hook/session. + type State; + + /// Attempts to create a new session from an incoming packet. + fn init( + leaf: &mut L, + packet: Packet, + ctx: &mut SessionInit, + ) -> SessionInitResult; + + /// Advances one session. The generated leaf passes all queued packets for this + /// hook and one context that can enqueue outbound frames. + fn update( + leaf: &mut L, + session: &mut Self::State, + incoming: &mut PacketQueue, + ctx: &mut SessionCtx, + ) -> SessionStatus; +} +``` + +`L` is the user state type, for example `RemotePtyState`. + +### Session Init Context + +```rust +pub struct SessionInit { + hook_id: HookID, + packet_path: Vec, +} + +pub enum SessionInitResult { + Created(S), + Rejected, + RejectedWith(Packet), +} +``` + +`RejectedWith(Packet)` is intended for cases where the initializer can build a +protocol-level failure response, such as "too many PTY sessions". The generated leaf +still owns routing and retry for that packet. + +The PTY `Open` payload should include the caller reply path because `Packet` does +not currently carry source path. + +```text +Open payload: + opcode + reply_path_len + reply_path segments + rows + cols + command/env/options +``` + +The session stores that reply path and uses it for upward output packets. + +### Session Update Context + +Sessions should use a context wrapper rather than directly constructing packets. +The context can still carry restricted endpoint access when absolutely necessary, +but the normal output path should be helper methods. + +```rust +pub struct SessionCtx<'a> { + endpoint: &'a mut Endpoint, + hook_id: HookID, + reply_path: &'a [u32], + procedure_id: u32, + outbox: &'a mut PacketQueue, +} +``` + +Helpers: + +```rust +impl<'a> SessionCtx<'a> { + pub fn send(&mut self, opcode: u8, data: &[u8]); + + pub fn send_final(&mut self, opcode: u8, data: &[u8]); + + pub fn error(&mut self, code: u8, data: &[u8]); + + pub fn error_final(&mut self, code: u8, data: &[u8]); + +} +``` + +These helpers build packets like: + +```rust +Packet { + hook_id: self.hook_id, + end_hook, + path: self.reply_path.to_vec(), + procedure_id: self.procedure_id, + data: encode_frame(opcode, payload), +} +``` + +The helper only queues packets. It does not route them immediately. The generated +leaf owns flushing and retry. + +### Session Status + +```rust +pub enum SessionStatus { + Running, + Closing, + Closed, +} +``` + +`Closed` means the session has no more application work. The generated leaf still +must keep the session until any final packet has routed successfully. + +## Procedures + +A procedure is a one-packet operation. It is appropriate for introspection, ping, +capabilities, and simple state queries. + +```rust +pub trait Procedure { + const PROCEDURE_ID: u32; + + fn handle( + leaf: &mut L, + endpoint: &mut Endpoint, + packet: Packet, + out: &mut ProcedureOut, + ); +} +``` + +Procedure helpers mirror session helpers but operate on one incoming packet: + +```rust +pub struct ProcedureOut { + hook_id: HookID, + reply_path: Vec, + procedure_id: u32, + outbox: PacketQueue, +} + +impl ProcedureOut { + pub fn send(&mut self, data: &[u8]); + + pub fn send_final(&mut self, data: &[u8]); + +} +``` + +Procedures do not directly access sessions. If a procedure needs information about +sessions, that information should be mirrored into leaf state by the session code. + +## PTY Binary Protocol + +One PTY session uses one hook and one outer procedure id. The inner PTY protocol is +a tiny binary frame in `Packet::data`. + +If API names use `end_state` at the session layer, it maps directly to +`Packet::end_hook` at the protocol layer. + +```text +Packet { + hook_id: session hook, + procedure_id: PROC_PTY, + data: [opcode, payload...], + end_hook: session lifetime marker, +} +``` + +Suggested opcodes: + +| Opcode | Direction | Meaning | end_hook | +|---:|---|---|---| +| 0 | Downward | Open PTY | false | +| 1 | Upward | Opened | false | +| 2 | Downward | Input bytes | false | +| 3 | Downward | Resize | false | +| 4 | Downward | Stdin EOF | false | +| 5 | Downward | Terminate process | false | +| 6 | Downward | Abort session without acknowledgement | true | +| 7 | Upward | Output bytes | false | +| 8 | Upward | Exit status | true | +| 9 | Upward | Fatal error | true | + +`StdinEof` must not set `end_hook = true`. The remote process may still emit output +after stdin closes. + +## Generated Update Loop + +The generated `Leaf::update` should follow this shape: + +```text +update(endpoint) + 1. flush retry queue first + 2. drain matching inbound packets only + 3. dispatch procedure packets + 4. group session packets by hook_id + 5. create sessions for open packets + 6. update active sessions + 7. flush outbound frames + 8. remove sessions whose final packet routed successfully +``` + +More concrete flow: + +```text ++----------------------------+ +| generated update | ++----------------------------+ + | + v +flush retry packets + | + v +take_inbound_matching(local_id, owns_packet) + | + +--> procedure id match -> Procedure::handle + | + +--> session id match -> session inbox by hook_id + | + v +for each session inbox + | + +--> existing hook -> Session::update + | + +--> no hook -> Session::init, then Session::update + | + v +flush session/procedure outbox + | + +--> route success -> advance/remove when safe + | + +--> route failure -> keep retry packet and keep state +``` + +The retry rule is the most important generated invariant: + +```text +If endpoint.add_outbound(packet) fails, the generated leaf must not: + - drop the packet + - advance a final frame + - remove the session + - consume state that cannot be reconstructed +``` + +## Generated Dispatch + +The macro should emit static matches, not runtime registries. + +Good: + +```rust +match packet.procedure_id { + PtySession::PROCEDURE_ID => self.dispatch_pty_session(packet), + PingProcedure::PROCEDURE_ID => PingProcedure::handle(...), + CapabilitiesProcedure::PROCEDURE_ID => CapabilitiesProcedure::handle(...), + _ => self.handle_unknown(packet), +} +``` + +Avoid: + +```rust +Vec> +Vec> +``` + +Static dispatch keeps the generated code visible to LLVM and avoids registry setup +costs in constrained binaries. + +## Session Store + +The first implementation can use a small `Vec` or `VecDeque` store. + +```rust +pub struct SessionEntry { + hook_id: HookID, + state: S, + inbox: PacketQueue, + outbox: PacketQueue, + retry: Option, + closing: bool, +} +``` + +For minimal binaries, a later implementation can replace this with a fixed-capacity +table under a feature flag. + +Required operations: + +- find by `hook_id` +- insert if capacity allows +- push incoming packet to inbox +- enqueue outbound packet +- retain session while retry packet exists +- remove only after session is closed and outbox/retry are empty + +## Unknown Packets + +The generated leaf should have configurable unknown-packet behavior. + +Default behavior: + +- unknown procedure id remains in the endpoint queue because `take_inbound_matching` does not drain it +- unknown session opcode for a known session produces a fatal error frame and closes the session +- packet for unknown hook under a session procedure produces a fatal error frame if the payload is not a valid `Open` + +For PTY: + +```text +unknown hook + Open opcode -> create session +unknown hook + non-Open opcode -> Error end_hook=true +known hook + known opcode -> session update +known hook + unknown opcode -> Error end_hook=true +``` + +## Access Boundaries + +Sessions and procedures can mutate leaf state: + +```rust +fn update(leaf: &mut RemotePtyState, ...) +``` + +They should not directly access each other: + +```text +Procedure -> no direct SessionStore access +Session -> no direct Procedure access +``` + +If cross-cutting data is needed, mirror it into `RemotePtyState`: + +```rust +pub struct RemotePtyState { + active_count: usize, + total_spawned: u64, + max_sessions: usize, +} +``` + +This keeps borrowing simple and avoids turning the generated leaf into a shared +mutable object graph. + +## Endpoint Access + +The user proposed passing `&mut Endpoint` to sessions and procedures. The safest +design is slightly narrower: + +- procedures may receive `&mut Endpoint` because they handle one packet and return immediately +- sessions should receive `SessionCtx`, which can expose narrow endpoint helpers +- a raw endpoint escape hatch can be added later if a real leaf proves it needs one + +Rationale: sessions are long-lived and retry-sensitive. If session code calls +`endpoint.add_outbound` directly, it can bypass generated retry handling and lose a +final packet. The helper path keeps the dangerous part centralized. + +## Remote PTY Example + +User code: + +```rust +#[unshell_leaf( + leaf = RemotePtyLeaf, + id = LEAF_REMOTE_PTY, + sessions(PtySession), + procedures(PtyCapabilities) +)] +pub struct RemotePtyState { + max_sessions: usize, + default_rows: u16, + default_cols: u16, +} + +pub struct PtySession; + +pub struct PtyState { + hook_id: HookID, + reply_path: Vec, + worker: PtyWorker, + saw_stdin_eof: bool, +} + +impl Session for PtySession { + const PROCEDURE_ID: u32 = PROC_PTY; + + type State = PtyState; + + fn init( + leaf: &mut RemotePtyState, + packet: Packet, + ctx: &mut SessionInit, + ) -> SessionInitResult { + let open = decode_open(&packet.data)?; + + if leaf.active_count >= leaf.max_sessions { + return SessionInitResult::RejectedWith(pty_error_busy(packet.hook_id)); + } + + let worker = PtyWorker::spawn(open.command, open.rows, open.cols)?; + + SessionInitResult::Created(PtyState { + hook_id: ctx.hook_id(), + reply_path: open.reply_path, + worker, + saw_stdin_eof: false, + }) + } + + fn update( + leaf: &mut RemotePtyState, + session: &mut Self::State, + incoming: &mut PacketQueue, + ctx: &mut SessionCtx, + ) -> SessionStatus { + while let Some(packet) = incoming.pop_front() { + match decode_pty_frame(&packet.data) { + PtyFrame::Input(bytes) => session.worker.write(&bytes), + PtyFrame::Resize { rows, cols } => session.worker.resize(rows, cols), + PtyFrame::StdinEof => { + session.saw_stdin_eof = true; + session.worker.close_stdin(); + } + PtyFrame::Terminate => session.worker.terminate(), + PtyFrame::Abort => return SessionStatus::Closed, + _ => ctx.error_final(ERR_BAD_OPCODE, b"bad pty opcode"), + } + } + + while let Some(bytes) = session.worker.poll_output() { + ctx.send(OP_OUTPUT, &bytes); + } + + if let Some(status) = session.worker.poll_exit() { + ctx.send_final(OP_EXIT, &encode_exit(status)); + leaf.active_count -= 1; + return SessionStatus::Closed; + } + + SessionStatus::Running + } +} +``` + +The exact error handling syntax above is illustrative. The final API should avoid +`?` unless `SessionInitResult` has a clear conversion story. + +## Generated Remote PTY Flow + +```text +Caller Remote endpoint RemotePtyLeaf +------ --------------- ------------- +allocate hook +Open end=false -------------> downward route opens hook ----> init PtyState +Input false -------------> hook remains active -----------> write PTY stdin +Resize false -------------> hook remains active -----------> resize PTY +StdinEof false -------------> hook remains active -----------> close PTY stdin + +Output false <------------- upward route requires hook <---- ctx.send +Output false <------------- upward route requires hook <---- ctx.send +Exit true <------------- upward route closes hook <---- ctx.send_final + +caller cleanup remove session +``` + +## Proc Macro Implementation + +Rust still requires a proc-macro crate for real attribute macros. Keep that crate +thin and put the real logic in a normal testable crate. + +```text +unshell-macros + #[proc_macro_attribute] + pub fn unshell_leaf(attr, item) -> TokenStream + +unshell-macros-core + parse UnshellLeafArgs + parse state struct + generate wrapper + generate Leaf impl + generate dispatch methods + tests over proc_macro2::TokenStream +``` + +Recommended parser helper: + +- `deluxe` for attribute parsing if the desired syntax uses richer Rust tokens +- `darling` if the syntax stays close to normal Rust meta attributes + +Current recommendation: use `deluxe` for attribute parsing and `syn`/`quote` for +code generation. + +## Generated Code Requirements + +- Must compile without requiring std in `unshell-protocol`. +- Must not allocate handler trait objects. +- Must preserve generic parameters and where clauses on the state struct. +- Must emit useful compile errors for duplicate procedure ids. +- Must emit useful compile errors for duplicate session procedure ids. +- Must reject session and procedure id collisions unless explicitly allowed. +- Must preserve user attributes that are not consumed by the macro. +- Must not require users to manually implement `Leaf` for the generated wrapper. + +## Testing Strategy + +Phase 1 tests should use a fake PTY session, not `portable-pty`. + +Protocol tests: + +- `open_pty_paves_hook_and_creates_session` +- `input_and_output_share_one_hook` +- `stdin_eof_keeps_hook_until_exit` +- `exit_end_hook_cleans_route_and_session` +- `failed_final_exit_route_retries_without_losing_session` +- `abort_downward_end_hook_closes_without_ack` +- `unknown_session_input_returns_error_end_hook` +- `two_pty_sessions_interleave_without_crossing_hooks` +- `pty_leaf_does_not_consume_other_leaf_packets` + +Macro tests: + +- generated leaf implements `Leaf` +- duplicate procedure ids fail at compile time +- duplicate session procedure ids fail at compile time +- generic state structs expand correctly +- generated code routes final frames retry-safely +- generated code preserves unmatched inbound packets + +Use `trybuild` for compile-fail macro tests once `unshell-macros` exists. + +## Rollout Plan + +1. Add `Endpoint::take_inbound_matching`. +2. Write a manual fake PTY leaf that follows the exact generated shape. +3. Add PTY session tests against the manual fake leaf. +4. Create `unshell-macros` and `unshell-macros-core`. +5. Generate the same shape as the manual fake leaf. +6. Port fake PTY tests to the generated leaf. +7. Add compile-fail macro tests. +8. Implement the real std-only PTY worker in `unshell-leaves` or `unshell-runtime`. +9. Add integration tests for the real worker where the platform supports PTYs. + +This avoids writing a macro against an unproven design. First make the generated +shape real by hand, then teach the macro to emit it. + +## Open Questions + +- Should `SessionCtx` expose any raw `Endpoint` access, or only narrow helpers? +- Should session stores be `Vec` first, or should fixed-capacity storage be designed immediately? +- Should unknown opcodes produce an error packet by default, or should each session type decide? +- Should `Open` always carry `reply_path`, or should the packet format eventually add source path? +- Should `ProcedureOut` be retry-safe like session output, or are procedures allowed to fail fast? +- Should macro-generated leaves expose counters for active sessions and retry queue depth? + +## Recommendation + +Implement this as static generated code with a narrow context API. + +Do not build a runtime plugin system. Do not give sessions raw access to session +stores. Do not let session code bypass generated output flushing. The macro should +make the correct routing and hook behavior the easiest path, especially for final +frames. + +The first proof point should be fake PTY. If fake PTY works cleanly, real PTY is +mostly OS plumbing. diff --git a/examples/hashtest.rs b/examples/hashtest.rs index 0dd59c7..8520c21 100644 --- a/examples/hashtest.rs +++ b/examples/hashtest.rs @@ -6,7 +6,7 @@ macro_rules! hashtest { }; } -const MAP: [(&'static str, u32); 6] = [ +const MAP: [(&str, u32); 6] = [ hashtest!("abc123"), hashtest!("abc124"), hashtest!("abc125"), diff --git a/src/hash.rs b/src/hash.rs index 70e8d28..a7b6d85 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -1,6 +1,6 @@ //! Temporary hash function -const fn hash_recursive<'a>(state: &mut [u8; 4], input: &'a [u8]) { +const fn hash_recursive(state: &mut [u8; 4], input: &[u8]) { match input.len() { 3 => { state[0] ^= input[0]; diff --git a/src/interface/mod.rs b/src/interface/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/interface/mod.rs @@ -0,0 +1 @@ + diff --git a/src/lib.rs b/src/lib.rs index 3dbfd0d..0d4f202 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ //! ## Architecture //! //! - [`protocol`] - Wire types, framing, stateless validation, and routing/runtime. +//! - [`interface`] - Typed control surfaces used by UI adapters and control leaves. //! //! The library requires `alloc` for path and payload management. @@ -16,12 +17,8 @@ pub extern crate alloc; mod hash; +pub mod interface; pub mod logger; - -pub mod protocol { - pub use unshell_protocol::*; - - pub use unshell_macros::unshell_leaf; -} +pub mod protocol; pub use hash::hash; diff --git a/unshell-protocol/src/endpoint/hooks.rs b/src/protocol/endpoint/hooks.rs similarity index 98% rename from unshell-protocol/src/endpoint/hooks.rs rename to src/protocol/endpoint/hooks.rs index 9447b40..6e14043 100644 --- a/unshell-protocol/src/endpoint/hooks.rs +++ b/src/protocol/endpoint/hooks.rs @@ -1,4 +1,4 @@ -use crate::{Endpoint, EndpointError, EndpointName}; +use crate::protocol::{Endpoint, EndpointError, EndpointName}; /// Compact identifier for one routed return channel. /// diff --git a/unshell-protocol/src/endpoint/mod.rs b/src/protocol/endpoint/mod.rs similarity index 97% rename from unshell-protocol/src/endpoint/mod.rs rename to src/protocol/endpoint/mod.rs index c57772b..eaac715 100644 --- a/unshell-protocol/src/endpoint/mod.rs +++ b/src/protocol/endpoint/mod.rs @@ -5,7 +5,7 @@ pub use hooks::HookID; use alloc::{boxed::Box, vec::Vec}; -use crate::{ConnectionSet, HookMap, Leaf, Packet, Path, RouteMap}; +use crate::protocol::{ConnectionSet, HookMap, Leaf, Packet, Path, RouteMap}; pub struct Endpoint { // This endpoint's identifier diff --git a/unshell-protocol/src/endpoint/routing.rs b/src/protocol/endpoint/routing.rs similarity index 99% rename from unshell-protocol/src/endpoint/routing.rs rename to src/protocol/endpoint/routing.rs index 38a2afa..01af5c9 100644 --- a/unshell-protocol/src/endpoint/routing.rs +++ b/src/protocol/endpoint/routing.rs @@ -1,4 +1,4 @@ -use crate::{Endpoint, EndpointError, Packet, RouteDirection}; +use crate::protocol::{Endpoint, EndpointError, Packet, RouteDirection}; impl Endpoint { /// Register an inbound packet from legacy trusted code. diff --git a/unshell-protocol/src/error.rs b/src/protocol/error.rs similarity index 100% rename from unshell-protocol/src/error.rs rename to src/protocol/error.rs diff --git a/unshell-protocol/src/leaf.rs b/src/protocol/leaf.rs similarity index 99% rename from unshell-protocol/src/leaf.rs rename to src/protocol/leaf.rs index aa4491b..dfd2646 100644 --- a/unshell-protocol/src/leaf.rs +++ b/src/protocol/leaf.rs @@ -1,4 +1,4 @@ -use crate::{Endpoint, HookID, Packet, PacketQueue}; +use crate::protocol::{Endpoint, HookID, Packet, PacketQueue}; use alloc::vec::Vec; diff --git a/unshell-protocol/src/lib.rs b/src/protocol/mod.rs similarity index 94% rename from unshell-protocol/src/lib.rs rename to src/protocol/mod.rs index 1ec332e..dbf6c80 100644 --- a/unshell-protocol/src/lib.rs +++ b/src/protocol/mod.rs @@ -1,7 +1,3 @@ -#![no_std] - -pub extern crate alloc; - mod endpoint; mod error; mod leaf; @@ -11,6 +7,7 @@ pub use endpoint::{Endpoint, HookID}; pub use error::*; pub use leaf::*; pub use packet::Packet; +pub use unshell_macros::unshell_leaf; // Various named types used for brevity use alloc::{ diff --git a/unshell-protocol/src/packet.rs b/src/protocol/packet.rs similarity index 98% rename from unshell-protocol/src/packet.rs rename to src/protocol/packet.rs index f908f3f..3d07ecc 100644 --- a/unshell-protocol/src/packet.rs +++ b/src/protocol/packet.rs @@ -2,7 +2,7 @@ extern crate alloc; use alloc::vec::Vec; -use crate::{DeserializeError, SerializeError}; +use crate::protocol::{DeserializeError, SerializeError}; /// Fully decoded UnShell test packet. /// diff --git a/unshell-protocol/src/tests/merkle_sync/codec.rs b/src/protocol/tests/merkle_sync/codec.rs similarity index 100% rename from unshell-protocol/src/tests/merkle_sync/codec.rs rename to src/protocol/tests/merkle_sync/codec.rs diff --git a/unshell-protocol/src/tests/merkle_sync/constants.rs b/src/protocol/tests/merkle_sync/constants.rs similarity index 100% rename from unshell-protocol/src/tests/merkle_sync/constants.rs rename to src/protocol/tests/merkle_sync/constants.rs diff --git a/unshell-protocol/src/tests/merkle_sync/harness.rs b/src/protocol/tests/merkle_sync/harness.rs similarity index 99% rename from unshell-protocol/src/tests/merkle_sync/harness.rs rename to src/protocol/tests/merkle_sync/harness.rs index 34d544c..120ff9a 100644 --- a/unshell-protocol/src/tests/merkle_sync/harness.rs +++ b/src/protocol/tests/merkle_sync/harness.rs @@ -1,7 +1,7 @@ use alloc::{boxed::Box, rc::Rc, vec}; use core::cell::RefCell; -use crate::Endpoint; +use crate::protocol::Endpoint; use super::{ constants::{ENDPOINT_CALLER, ENDPOINT_RESPONDENT}, diff --git a/unshell-protocol/src/tests/merkle_sync/leaves.rs b/src/protocol/tests/merkle_sync/leaves.rs similarity index 99% rename from unshell-protocol/src/tests/merkle_sync/leaves.rs rename to src/protocol/tests/merkle_sync/leaves.rs index b1b75b9..0c9ddcd 100644 --- a/unshell-protocol/src/tests/merkle_sync/leaves.rs +++ b/src/protocol/tests/merkle_sync/leaves.rs @@ -3,7 +3,7 @@ use core::cell::RefCell; use crossbeam_channel::{Receiver, Sender}; -use crate::{Endpoint, Leaf, Packet}; +use crate::protocol::{Endpoint, Leaf, Packet}; use super::{ codec::{decode_block_chunk, decode_child_summary, decode_u32}, diff --git a/unshell-protocol/src/tests/merkle_sync/mod.rs b/src/protocol/tests/merkle_sync/mod.rs similarity index 100% rename from unshell-protocol/src/tests/merkle_sync/mod.rs rename to src/protocol/tests/merkle_sync/mod.rs diff --git a/unshell-protocol/src/tests/merkle_sync/rpc.rs b/src/protocol/tests/merkle_sync/rpc.rs similarity index 98% rename from unshell-protocol/src/tests/merkle_sync/rpc.rs rename to src/protocol/tests/merkle_sync/rpc.rs index 83ba211..56cefad 100644 --- a/unshell-protocol/src/tests/merkle_sync/rpc.rs +++ b/src/protocol/tests/merkle_sync/rpc.rs @@ -1,6 +1,6 @@ use alloc::{vec, vec::Vec}; -use crate::Packet; +use crate::protocol::Packet; use super::{ codec::{encode_block_chunk, encode_child_summary, encode_u32}, diff --git a/unshell-protocol/src/tests/merkle_sync/state.rs b/src/protocol/tests/merkle_sync/state.rs similarity index 96% rename from unshell-protocol/src/tests/merkle_sync/state.rs rename to src/protocol/tests/merkle_sync/state.rs index 92a8cea..dfcf725 100644 --- a/unshell-protocol/src/tests/merkle_sync/state.rs +++ b/src/protocol/tests/merkle_sync/state.rs @@ -1,5 +1,7 @@ use alloc::vec::Vec; +use crate::protocol::Packet; + use super::{ rpc::OutgoingFrame, tree::{BlockChunk, ChildSummary}, @@ -76,7 +78,7 @@ impl ResponseStream { } /// Builds the next packet without advancing the stream. - pub(super) fn next_packet(&self) -> Option { + pub(super) fn next_packet(&self) -> Option { let frame = self.frames.get(self.next_index)?; Some(frame.to_packet(self.hook_id, self.next_index + 1 == self.frames.len())) } diff --git a/unshell-protocol/src/tests/merkle_sync/tests.rs b/src/protocol/tests/merkle_sync/tests.rs similarity index 100% rename from unshell-protocol/src/tests/merkle_sync/tests.rs rename to src/protocol/tests/merkle_sync/tests.rs diff --git a/unshell-protocol/src/tests/merkle_sync/tree.rs b/src/protocol/tests/merkle_sync/tree.rs similarity index 100% rename from unshell-protocol/src/tests/merkle_sync/tree.rs rename to src/protocol/tests/merkle_sync/tree.rs diff --git a/unshell-protocol/src/tests/oneshot/mod.rs b/src/protocol/tests/oneshot/mod.rs similarity index 98% rename from unshell-protocol/src/tests/oneshot/mod.rs rename to src/protocol/tests/oneshot/mod.rs index d2fae29..310c562 100644 --- a/unshell-protocol/src/tests/oneshot/mod.rs +++ b/src/protocol/tests/oneshot/mod.rs @@ -1,7 +1,7 @@ mod streams; mod support; -use crate::{Endpoint, EndpointError, RouteDirection}; +use crate::protocol::{Endpoint, EndpointError, RouteDirection}; use alloc::{boxed::Box, vec}; @@ -16,7 +16,7 @@ fn test_oneshot() { let (tx_a, rx_a) = crossbeam_channel::unbounded(); let (tx_b, rx_b) = crossbeam_channel::unbounded(); - let mut endpoint_a = crate::endpoint::Endpoint::new( + let mut endpoint_a = Endpoint::new( ENDPOINT_A, vec![ Box::new(ControllerLeaf { has_run: false }), @@ -31,7 +31,7 @@ fn test_oneshot() { ); endpoint_a.path = vec![ENDPOINT_A]; - let mut endpoint_b = crate::endpoint::Endpoint::new( + let mut endpoint_b = Endpoint::new( ENDPOINT_B, vec![ Box::new(ResponderLeaf), diff --git a/unshell-protocol/src/tests/oneshot/streams.rs b/src/protocol/tests/oneshot/streams.rs similarity index 99% rename from unshell-protocol/src/tests/oneshot/streams.rs rename to src/protocol/tests/oneshot/streams.rs index f38e2f8..b5d2180 100644 --- a/unshell-protocol/src/tests/oneshot/streams.rs +++ b/src/protocol/tests/oneshot/streams.rs @@ -1,4 +1,4 @@ -use crate::{Endpoint, Leaf, Packet}; +use crate::protocol::{Endpoint, Leaf, Packet}; use alloc::{boxed::Box, format, vec, vec::Vec}; diff --git a/unshell-protocol/src/tests/oneshot/support.rs b/src/protocol/tests/oneshot/support.rs similarity index 99% rename from unshell-protocol/src/tests/oneshot/support.rs rename to src/protocol/tests/oneshot/support.rs index 06796ae..89fb069 100644 --- a/unshell-protocol/src/tests/oneshot/support.rs +++ b/src/protocol/tests/oneshot/support.rs @@ -1,4 +1,4 @@ -use crate::{Endpoint, Leaf, Packet}; +use crate::protocol::{Endpoint, Leaf, Packet}; use alloc::{vec, vec::Vec}; use crossbeam_channel::{Receiver, Sender}; diff --git a/unshell-protocol/src/tests/packet.rs b/src/protocol/tests/packet.rs similarity index 98% rename from unshell-protocol/src/tests/packet.rs rename to src/protocol/tests/packet.rs index 83280db..fb07dd1 100644 --- a/unshell-protocol/src/tests/packet.rs +++ b/src/protocol/tests/packet.rs @@ -1,6 +1,6 @@ use alloc::{vec, vec::Vec}; -use crate::{DeserializeError, EndpointError, Packet, SerializeError}; +use crate::protocol::{DeserializeError, EndpointError, Packet, SerializeError}; // ── Helpers ─────────────────────────────────────────────────────────────── diff --git a/unshell-macros-core/src/leaf/generator.rs b/unshell-macros-core/src/leaf/generator.rs index d237e55..8a14f05 100644 --- a/unshell-macros-core/src/leaf/generator.rs +++ b/unshell-macros-core/src/leaf/generator.rs @@ -114,7 +114,7 @@ impl LeafGenerator { fn __unshell_parent_reply_path( endpoint: &::unshell::protocol::Endpoint, - ) -> ::unshell::protocol::alloc::vec::Vec { + ) -> ::unshell::alloc::vec::Vec { if endpoint.path.len() > 1 { endpoint.path[..endpoint.path.len() - 1].to_vec() } else { @@ -135,7 +135,7 @@ impl LeafGenerator { return; }; - let mut __unshell_packets = ::unshell::protocol::alloc::vec::Vec::new(); + let mut __unshell_packets = ::unshell::alloc::vec::Vec::new(); endpoint.take_inbound_matching( __unshell_local_id, Self::__unshell_packet_is_owned, @@ -177,7 +177,7 @@ impl LeafGenerator { let field = &store.field; let session_ty = &store.ty; quote! { - #field: ::unshell::protocol::alloc::vec::Vec< + #field: ::unshell::alloc::vec::Vec< ::unshell::protocol::SessionEntry< <#session_ty as ::unshell::protocol::Session<#state_type>>::State > @@ -193,7 +193,7 @@ impl LeafGenerator { .iter() .map(|store| { let field = &store.field; - quote!(#field: ::unshell::protocol::alloc::vec::Vec::new()) + quote!(#field: ::unshell::alloc::vec::Vec::new()) }) .collect() } diff --git a/unshell-protocol/Cargo.toml b/unshell-protocol/Cargo.toml deleted file mode 100644 index 5f56508..0000000 --- a/unshell-protocol/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "unshell-protocol" -version.workspace = true -edition.workspace = true -description = "Wire protocol, framing, validation, and endpoint runtime for UnShell" - -[lib] -doctest = false - -[dependencies] -rkyv = { workspace = true } -# unshell-macros = { path = "../unshell-macros" } - -[dev-dependencies] -crossbeam-channel.workspace = true - -[lints.rust] -elided_lifetimes_in_paths = "warn" -future_incompatible = { level = "warn", priority = -1 } -nonstandard_style = { level = "warn", priority = -1 } -rust_2018_idioms = { level = "warn", priority = -1 } -rust_2021_prelude_collisions = "warn" -semicolon_in_expressions_from_macros = "warn" -unsafe_op_in_unsafe_fn = "warn" -unused_import_braces = "warn" -unused_lifetimes = "warn" -trivial_casts = "allow" -# missing_docs = "warn"