Phase 5: Extract shakenfist-spice-usbredir¶
Prompt¶
Before executing any step of this plan, back brief the operator. This is the simplest of the three crate extractions — the usbredir module has zero ryll-internal coupling, a clean API that needs no refactoring, and the fewest consumer files. The work is a single extraction commit (unlike Phases 3 and 4 which each had prerequisite commits for LZ4 relocation and marker-struct refactoring respectively).
I prefer one commit per logical change. Each commit must leave the tree in a working state.
Situation¶
After Phases 1-4 (most recently 9a409fe and 2d3e5a8), the
repository is a four-member Cargo workspace:
ryll-repo/
├── Cargo.toml # workspace manifest
├── ryll/
│ └── src/
│ ├── usbredir/ # the target of Phase 5
│ │ ├── mod.rs # 9 lines
│ │ ├── constants.rs # 150 lines
│ │ ├── messages.rs # 804 lines
│ │ └── parser.rs # 349 lines
│ ├── channels/usbredir.rs # the *channel handler*
│ │ # (stays in ryll)
│ ├── usb/{mod,real,virtual_msc}.rs # USB backends
│ │ # (consume usbredir types)
│ └── ...
├── shakenfist-spice-compression/ # real crate (Phase 3)
├── shakenfist-spice-protocol/ # real crate (Phase 4)
└── shakenfist-spice-usbredir/ # v0.0.0 placeholder
Important naming distinction: ryll/src/usbredir/ is the
protocol parser and types module (what we're extracting).
ryll/src/channels/usbredir.rs is the channel handler that
uses the protocol module (stays in ryll). Both exist because
SPICE's usbredir channel uses the usbredir wire protocol. The
mod usbredir; at ryll/src/main.rs:26
declares the protocol module (the extraction target). The
pub mod usbredir; at
ryll/src/channels/mod.rs:5
declares the channel handler (not touched by Phase 5).
Current state of the usbredir code (from the Phase 5 research pass):
-
ryll/src/usbredir/mod.rs (9 lines): declares
pub mod constants,pub mod messages,pub mod parser. No re-exports, no type definitions. -
ryll/src/usbredir/constants.rs (150 lines):
pub mod msg_type(35 message-type constants),pub mod cap(8 capability constants),pub const RYLL_CAPS,pub enum Status,pub enum UsbSpeed,pub enum EpType,pub fn msg_type_name(). Pure constants and enums, no external crate imports (only stdlib). Has#![allow(dead_code)]at the top. -
ryll/src/usbredir/messages.rs (804 lines): 15 message struct types (each with
pub const SIZE,pub fn read(),pub fn write()),pub fn make_usbredir_message(),pub struct UsbredirMessage,pub enum UsbredirPayloadwith 21 variants. Usesbyteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}. Internaluse super::constants::msg_typefor dispatch. -
ryll/src/usbredir/parser.rs (349 lines):
pub struct UsbredirParserwithpub fn new(),pub fn feed(),pub fn next_message(). Usesanyhow::Result. Internaluse super::constants::msg_typeanduse super::messages::*.
Coupling: zero use crate::* imports in any file. The
module's only dependencies are byteorder, anyhow, and
std::io.
Tests: messages.rs and parser.rs contain unit tests — the research found ~17 test functions total, all pure input/output with no ryll-specific mocking. Exact counts to be verified during pre-flight (the research report was inconsistent on the number). These tests migrate with their files to the new crate.
Consumer files that import from crate::usbredir::*:
| File | Imports |
|---|---|
| ryll/src/channels/usbredir.rs | crate::usbredir::constants::{self, msg_type, msg_type_name, Status, RYLL_CAPS}, crate::usbredir::messages::{...}, crate::usbredir::parser::UsbredirParser |
| ryll/src/usb/mod.rs | crate::usbredir::constants::Status, crate::usbredir::messages::{DeviceConnect, EpInfo, InterfaceInfo} |
| ryll/src/usb/real.rs | crate::usbredir::constants::Status, crate::usbredir::messages::{DeviceConnect, EpInfo, InterfaceInfo} |
| ryll/src/usb/virtual_msc.rs | (likely same pattern as real.rs — verify during execution) |
Inline qualified paths (lesson from Phase 4): the research
found two inline crate::usbredir::messages::InterruptReceivingStatus { ... }
struct instantiations at channels/usbredir.rs:595 and :611.
These must be caught alongside the use statement updates.
Mission and problem statement¶
Replace the Phase 2 shakenfist-spice-usbredir v0.0.0
placeholder with a real crate containing the usbredir protocol
parser, message types, and constants. Switch ryll's 3-4
consumer files to import from the new crate.
After this phase:
- The
shakenfist-spice-usbredircrate exists atshakenfist-spice-usbredir/as a real workspace member. ryll/src/usbredir/is deleted.- The
mod usbredir;declaration inryll/src/main.rsis removed. - All consumer files (
channels/usbredir.rs,usb/mod.rs,usb/real.rs, andusb/virtual_msc.rsif applicable) import fromshakenfist_spice_usbredir::*. - All existing ryll tests continue to pass. The usbredir unit tests migrate to the new crate. Total test count stays at 99.
- The crate is at version
0.0.0. No publish happens. - Per Decision #1's caveat, ryll's path-dep has no
version =qualifier.
Approach¶
This phase is a single extraction commit plus bookkeeping. No prerequisite commit is needed because the usbredir module's API is already clean (confirmed by the research pass — no marker structs, no positional-arg issues, no file relocations needed before extraction).
-
Extraction commit:
git mvthe three files (constants.rs,messages.rs,parser.rs) fromryll/src/usbredir/intoshakenfist-spice-usbredir/src/. Deletemod.rs. Rewrite the placeholderCargo.toml/lib.rs/README.md. Update the 3-4 consumer files. Removemod usbredir;frommain.rs. Add the path-dep toryll/Cargo.toml. -
Status update commit: mark Phase 5 complete, record discoveries.
Pre-flight verification¶
- Working tree is clean on
display-iteration. - CI is green on the most recent push (or Phase 4's commits are pushed and verified).
- All 99 tests pass via
make test. cargo clippy --workspace --all-targets -- -D warningspasses.- The usbredir module structure matches the research:
ls ryll/src/usbredir/shows exactlyconstants.rs,messages.rs,mod.rs,parser.rs. - Count usbredir tests:
grep -c '#\[test\]' ryll/src/usbredir/*.rs— record the exact counts for post-extraction verification. - Grep for ALL consumer references: both
grep -rn 'use crate::usbredir' ryll/src/ANDgrep -rn 'crate::usbredir::' ryll/src/ | grep -v '^.*:use 'to catch inline qualified paths (Phase 4 lesson).
Crate layout¶
shakenfist-spice-usbredir/Cargo.toml¶
[package]
name = "shakenfist-spice-usbredir"
version = "0.0.0"
edition.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
description = "Pure-Rust parser and message types for the SPICE USB redirection (usbredir) protocol, extracted from the ryll SPICE client."
readme = "README.md"
[dependencies]
anyhow = "1"
byteorder = "1"
No tokio, no tracing, no feature flags. The simplest
Cargo.toml of the three extracted crates.
shakenfist-spice-usbredir/src/lib.rs¶
//! Pure-Rust parser and message types for the SPICE USB
//! redirection (usbredir) protocol, suitable for clients,
//! proxies, and protocol analysis tools.
//!
//! - [`constants`] — message types, capabilities, status
//! codes, USB speed and endpoint-type enums.
//! - [`messages`] — wire-format struct definitions with
//! `read` and `write` methods for every usbredir message
//! type, plus `UsbredirMessage` / `UsbredirPayload` for
//! parsed message dispatch.
//! - [`parser`] — `UsbredirParser`, a byte-stream parser
//! that accumulates data and yields complete
//! `UsbredirMessage` values.
//!
//! Extracted from the
//! [ryll](https://github.com/shakenfist/ryll) SPICE client.
pub mod constants;
pub mod messages;
pub mod parser;
No crate-root re-exports needed — the three sub-modules are
the natural API surface, and consumers already access them via
usbredir::constants::*, usbredir::messages::*,
usbredir::parser::*.
ryll/Cargo.toml change¶
# Extracted SPICE USB redirection protocol types (same
# dual-spec caveat: no `version` qualifier until 0.1.0).
shakenfist-spice-usbredir = { path = "../shakenfist-spice-usbredir" }
ryll/src/main.rs change¶
Remove mod usbredir; at line 26. The mod usb; declaration
(which is the USB device backend, not the usbredir protocol)
stays.
Consumer file import updates¶
Each use crate::usbredir::* becomes
use shakenfist_spice_usbredir::*. The two inline qualified
paths at channels/usbredir.rs:595 and :611 become
shakenfist_spice_usbredir::messages::InterruptReceivingStatus { ... }.
Internal super::* references in the moved files¶
After the move, messages.rs and parser.rs use
use super::constants::* and use super::messages::*
respectively. Since super from a lib.rs-declared module
resolves to the crate root, these still compile correctly in
the new crate (same as Phase 4's link.rs experience). No
changes needed.
Execution¶
Step 1: Extract shakenfist-spice-usbredir¶
git mvthe three files:- Delete
ryll/src/usbredir/mod.rs(git rm). - Write the new
Cargo.toml,lib.rs,README.md. - Add the path-dep to
ryll/Cargo.toml. - Remove
mod usbredir;fromryll/src/main.rs. - Update the 3-4 consumer files: change all
use crate::usbredir::*imports touse shakenfist_spice_usbredir::*, and fix the 2 inline qualified paths. - Verify locally:
cargo fmt --all --checkcargo clippy --workspace --all-targets -- -D warningscargo build --workspacecargo test --workspace— total must still be 99. The per-crate split will shift (ryll loses the usbredir tests; the new crate gains them).cargo build -p shakenfist-spice-usbredirsucceeds.cargo build --release -p ryll,cargo deb,cargo generate-rpmall still work.pre-commit run --all-files- Inspect
git status— expected shape: 3 file renames, 1 deleted (mod.rs),Cargo.lockupdated,ryll/Cargo.tomlmodified,ryll/src/main.rs-1 line, 3-4 consumer files modified, plus the three rewritten placeholder files. - Commit.
Step 2: Mark Phase 5 complete¶
Update the master plan's execution table and record execution discoveries. Commit.
Open questions¶
-
Does
usb/virtual_msc.rsimport fromcrate::usbredir? The research showedusb/mod.rsandusb/real.rsdo;virtual_msc.rslikely follows the same pattern but wasn't explicitly confirmed. Verify during execution with the grep in pre-flight step 7. -
Exact test count. The research report was inconsistent (said "14" at the summary but listed 17 test names). The exact count doesn't matter for the plan — just verify during pre-flight that the post-extraction total is 99.
Administration and logistics¶
Success criteria¶
ryll/src/usbredir/no longer exists.shakenfist-spice-usbredir/is a real workspace member.- The crate's
Cargo.tomlstill hasversion = "0.0.0". - All 99 ryll tests pass. The usbredir unit tests now appear
under
shakenfist_spice_usbredirin the test output. cargo build -p shakenfist-spice-usbredirsucceeds.git log --followon each file traces back to its original location.- CI is green.
Future work¶
- Publish
shakenfist-spice-usbredir v0.1.0— same future-work item as the other two crates. - Clean up
#![allow(dead_code)]in constants.rs — the allow may be hiding unused constants that should be removed or documented. API polish for0.1.0. - Add a
README.mdusage example showingUsbredirParser::feed()+next_message()loop.
Bugs fixed during this work¶
(None expected.)
Discoveries during execution¶
-
Open Question #1 answered: yes,
usb/virtual_msc.rsimports fromcrate::usbredir. Same pattern asusb/mod.rsandusb/real.rs:Status,DeviceConnect,EpInfo,InterfaceInfo. Four consumer files total, as the pre-flight grep confirmed. -
Open Question #2 answered: 17 tests. 10 in
messages.rs, 7 inparser.rs. The research report's summary said "14" but listed 17 names — the test function count was correct at -
Post-extraction: 80 ryll + 2 compression + 17 usbredir + 0 protocol = 99 total.
-
Clippy
new_without_defaultlint surfaced onUsbredirParser. When the usbredir code was part of ryll, this lint was either suppressed or not triggered (possibly because the crate-level#![allow(dead_code)]inconstants.rsmasked it, or because the lint only fires on public items in a standalone crate). Once the code became its own crate, clippy flaggedUsbredirParser::new()as needing aDefaultimpl. Fixed by addingimpl Default for UsbredirParserthat delegates toSelf::new(). -
No inline qualified paths beyond the two already known. The pre-flight grep for
crate::usbredir::(withoutuse) found exactly the twoInterruptReceivingStatusstruct instantiations atchannels/usbredir.rs:595and:611. No surprises beyond what the research pass predicted. -
All three moved files registered as proper git renames.
constants.rsandmessages.rsat 100%;parser.rsat 99% because of the addedDefaultimpl (6 lines).git log --followtraces all three files back to their originalryll/src/usbredir/locations.
Back brief¶
Before executing, confirm:
- Single extraction commit (no prerequisite).
mod usbredir;inmain.rsis removed;pub mod usbredir;inchannels/mod.rsis NOT touched (different module).- Both
use crate::usbredirimport lines AND inlinecrate::usbredir::qualified paths will be caught. - No
version =qualifier on the path-dep. - Test count stays at 99 after the per-crate split.