feat(ds-stream): v2.0-2.3 composable codec pipeline

v2.0 — Pipeline Architecture
  - Frame, CodecResult, Codec trait, Pipeline builder
  - 6 adapters: Passthrough, Dedup, Compress, Pacer, Slicer, Stats

v2.1 — Multi-frame & new codecs
  - CodecOutput::Many fan-out, EncryptCodec, FilterCodec
  - Codec::reset(), encode_all/decode_all, real SlicerCodec chunking

v2.2 — Observability & reassembly
  - PipelineResult (frames+errors+consumed), StageMetric
  - ReassemblyCodec, ConditionalCodec, Pipeline presets & metrics

v2.3 — Integrity & rate control
  - ChecksumCodec (CRC32), RateLimitCodec (token bucket), TagCodec
  - Pipeline::chain(), Pipeline::describe()

13 codec adapters, 474 tests (all green, 0 regressions)
This commit is contained in:
enzotar 2026-03-11 23:50:35 -07:00
parent fbbdeb0bc4
commit 35b39a1cf1
15 changed files with 9830 additions and 47 deletions

View file

@ -4,6 +4,135 @@ All notable changes to this project will be documented in this file.
## [Unreleased]
### 🏗️ Engine v2.3.0 — ds-stream Pipeline v4 — 2026-03-11
- **`ChecksumCodec`** — CRC32 integrity: append on encode, verify+strip on decode, Error on mismatch
- **`RateLimitCodec`** — Token bucket algorithm (burst-tolerant, refills over time)
- **`TagCodec`** — Attach channel ID for mux routing, drop wrong-channel frames on decode
- **`Pipeline::chain()`** — Compose two pipelines into one
- **`Pipeline::describe()`** — Human-readable dump (`dedup → compress → encrypt`)
- **Error propagation tested** — Corrupt checksum → `PipelineResult.errors` collects the error
- 474 total ds-stream tests (+13 new)
### 🏗️ Engine v2.2.0 — ds-stream Pipeline v3 — 2026-03-11
- **`PipelineResult`** — returns frames + collected errors + consumed count
- **`StageMetric`** — per-stage frames_in/frames_out/consumed/errors observability
- **`ReassemblyCodec`** — reassemble chunked frames (counterpart to SlicerCodec)
- **`ConditionalCodec`** — wrap any codec with runtime enable/disable toggle
- **`Pipeline::signal(key)`** — preset: dedup→compress→encrypt
- **`Pipeline::media(mtu)`** — preset: compress→slicer
- **`Pipeline::metrics()`** — per-stage counter snapshot
- **Slicer↔Reassembly roundtrip** — verified end-to-end chunk→reassemble
- 461 total ds-stream tests (+12 new)
### 🏗️ Engine v2.1.0 — ds-stream Pipeline v2 — 2026-03-11
- **`CodecOutput::Many`** — codecs can fan-out (1 frame → N frames)
- **`EncryptCodec`** — XOR cipher encrypt/decrypt adapter
- **`FilterCodec`** — drop frames by type (`FilterCodec::drop_control()`)
- **`Codec::reset()`** — clear internal state on reconnect
- **`Pipeline::encode_all/decode_all`** — batch frame processing
- **`SlicerCodec`** rewritten — real MTU chunking via `Many` (no more truncation)
- **Full roundtrip test** — Compress→Encrypt→encode→decode→verify original
- 449 total ds-stream tests (+11 new)
### 🏗️ Engine v2.0.0 — ds-stream Pipeline Architecture — 2026-03-11
- **[NEW] `pipeline.rs`** — `Frame`, `CodecResult`, `Codec` trait, `Pipeline` builder
- **6 codec adapters**`PassthroughCodec`, `DedupCodec`, `CompressCodec`, `PacerCodec`, `SlicerCodec`, `StatsCodec`
- **15 pipeline tests** — frame roundtrip, pipeline composition, per-codec verification (438 total ds-stream tests)
### 🚀 Engine v1.15.0 — 2026-03-11
- **ds-stream**`StreamStats1150`, `PacketCoalescer`, `ErrorBudget` (423 tests)
- **ds-stream-wasm**`stats_v1150`, `coal_v1150`, `ebudget_v1150` WASM bindings (281 tests)
- **ds-physics**`find_fastest_body_v1150`, `get_total_mass_v1150`, `apply_magnet_v1150`, `get_body_angle_v1150` (350 tests)
### 🚀 Engine v1.14.0 — 2026-03-11
- **ds-stream**`PriorityQueue1140`, `LatencyTracker1140`, `FrameWindow` (413 tests)
- **ds-stream-wasm**`pq_v1140`, `lat_v1140`, `fw_v1140` WASM bindings (274 tests)
- **ds-physics**`get_body_aabb_v1140`, `get_max_speed_v1140`, `apply_buoyancy_v1140`, `is_body_colliding_v1140` (340 tests)
### 🎯 Engine v1.13.0 — 2026-03-11 — **1,000 TESTS MILESTONE**
- **ds-stream**`ChannelMux1130`, `FrameSlicer`, `BandwidthProbe` (403 tests)
- **ds-stream-wasm**`mux_v1130`, `slicer_v1130`, `probe_v1130` WASM bindings (267 tests)
- **ds-physics**`get_body_distance_v1130`, `get_world_centroid_v1130`, `apply_wind_v1130`, `count_bodies_in_radius_v1130` (330 tests)
### 🚀 Engine v1.12.0 — 2026-03-11
- **ds-stream**`FrameFingerprint`, `AdaptiveBitrate1120`, `JitterBuffer1120` (393 tests)
- **ds-stream-wasm**`fingerprint_v1120`, `abr_v1120`, `jitter_v1120` WASM bindings (260 tests)
- **ds-physics**`get_contact_count_v1120`, `get_world_momentum_v1120`, `apply_vortex_v1120`, `get_body_inertia_v1120` (320 tests)
### 🚀 Engine v1.11.0 — 2026-03-11
- **ds-stream**`FrameDropPolicy`, `StreamTimeline`, `PacketPacer` (383 tests)
- **ds-stream-wasm**`drop_policy_v1110`, `timeline_v1110`, `pacer_v1110` WASM bindings (253 tests)
- **ds-physics**`get_world_aabb_v1110`, `get_total_angular_ke_v1110`, `apply_drag_field_v1110`, `get_body_speed_v1110` (310 tests)
### 🚀 Engine v1.10.0 — 2026-03-11
- **ds-stream**`FrameRateLimiter`, `DeltaAccumulator`, `ConnectionGrade` (373 tests)
- **ds-stream-wasm**`rate_limit_v1100`, `delta_accum_v1100`, `conn_grade_v1100` WASM bindings (246 tests)
- **ds-physics**`get_body_rotation_v1100`, `get_energy_ratio_v1100`, `apply_explosion_v1100`, `get_nearest_body_v1100` (300 tests)
### 🚀 Engine v1.9.0 — 2026-03-11
- **ds-stream**`SequenceValidator`, `FrameTagMap`, `BurstDetector` (363 tests)
- **ds-stream-wasm**`seq_v190`, `burst_v190`, `tag_v190` WASM bindings (239 tests)
- **ds-physics**`get_gravity_v190`, `get_body_mass_v190`, `apply_central_impulse_v190`, `get_total_potential_energy_v190` (290 tests)
### 🚀 Engine v1.8.0 — 2026-03-11
- **ds-stream**`RingMetric180`, `FrameCompactor`, `RetransmitQueue` (353 tests)
- **ds-stream-wasm**`ring_v180`, `compactor_v180`, `retransmit_v180` WASM bindings (232 tests)
- **ds-physics**`set/get_angular_damping_v180`, `set_restitution_v180`, `sleep/wake_body_v180`, `count_sleeping_v180` (280 tests)
### 🚀 Engine v1.7.0 — 2026-03-11
- **ds-stream**`DelayBuffer170`, `PacketInterleaver`, `HeartbeatWatchdog` (343 tests)
- **ds-stream-wasm**`loss_v170`, `watchdog_v170`, `interleave_v170` WASM bindings (225 tests)
- **ds-physics**`set/get_linear_damping_v170`, `is_out_of_bounds_v170`, `clamp_velocities_v170`, `count_dynamic_bodies_v170` (270 tests)
### 🚀 Engine v1.6.0 — 2026-03-11
- **ds-stream**`AesFrameCipher`, `LossInjector`, `StreamCheckpoint` (333 tests)
- **ds-stream-wasm**`throttle_v160`, `cipher_v160`, `checkpoint_v160` WASM bindings (218 tests)
- **ds-physics**`get_body_type_v160`, `get_world_center_of_mass_v160`, `apply_gravity_well_v160`, `get_total_kinetic_energy_v160` (260 tests)
### 🚀 Engine v1.5.0 — 2026-03-11
- **ds-stream**`ContentDedup`, `StreamHealthTracker`, `FrameThrottler` (323 tests)
- **ds-stream-wasm**`bw_limiter_v150`, `dedup_v150`, `metrics_v150` WASM bindings (211 tests)
- **ds-physics**`apply_force_field_v150`, `freeze/unfreeze_body_v150`, `get_distance_v150`, `get_angular_momentum_v150` (250 tests)
### 🚀 Engine v1.4.0 — 2026-03-11
- **ds-stream**`SessionRecorder`, `PriorityScheduler`, `BandwidthLimiter` (313 tests)
- **ds-stream-wasm**`compositor_v140`, `recorder_v140`, `priority_v140` WASM bindings (204 tests)
- **ds-physics**`raycast_v140`, `get_collision_events_v140`, `time_scale_v140`, `get_body_momentum_v140` (240 tests)
### 🚀 Engine v1.3.0 — 2026-03-11
- **ds-stream**`QualityPolicy`, `FrameLayerCompositor`, `ReorderBuffer`, `QualityDecision`+`FrameMode` (303 tests)
- **ds-stream-wasm**`quality_decide_v130`, `reorder_push/drain_v130`, `channel_auth_check_v130` (197 tests)
- **ds-physics**`snapshot_scene_v130`, `restore_scene_v130`, `get_body_aabb_v130`, `apply_torque_v130` (230 tests)
### 🚀 Engine v1.2.0 — 2026-03-11
- **ds-stream**`HapticPayload`, `haptic_frame()`, `FrameBatch`, `StreamDigest`, `ChannelAuth` (291 tests)
- **ds-stream-wasm**`haptic_message_v120`, `batch_frames_v120`, `digest_v120` (190 tests)
- **ds-physics**`create_spring_joint_v120`, `get_joint_info_v120`, `set_body_rotation_v120`, `get_body_energy_v120` (220 tests)
### 🚀 Engine v1.1.0 — 2026-03-11
- **ds-stream**`FrameType::Error`, `ErrorPayload`, `ProtocolInfo`, `encrypt_frame/decrypt_frame`, `FrameRouter`, `error_frame()` builder (277 tests)
- **ds-stream-wasm**`error_frame_v110`, `decode_error_payload`, `protocol_info_v110`, `encrypt_frame_v110/decrypt_frame_v110` (183 tests)
- **ds-physics**`get_body_velocity_v110`, `set_body_position_v110`, `get_contact_pairs_v110`, `engine_version_v110` (210 tests)
### 🚀 Features
- V2 phase 1 — array access, timer, string interpolation

View file

@ -1,18 +1,50 @@
# Changelog
## [1.6.0] - 2026-03-11
### Added
- **`get_body_type_v160(body)`** — returns 0=dynamic, 1=kinematic, 2=fixed, -1=invalid
- **`get_world_center_of_mass_v160()`** — mass-weighted center of all dynamic bodies
- **`apply_gravity_well_v160(cx, cy, radius, strength)`** — attractive radial force (pulls toward center)
- **`get_total_kinetic_energy_v160()`** — sum of 0.5·m·v² for all dynamic bodies
- **`engine_version_v160()`** — version string
- 10 new tests (260 total)
## [1.5.0] - 2026-03-11
### Added
- `apply_force_field_v150` — repulsive radial force
- `freeze/unfreeze_body_v150` — kinematic toggle
- `get_distance_v150` — center-to-center distance
- `get_angular_momentum_v150` — angular momentum
- 10 new tests (250 total)
## [1.4.0] - 2026-03-11
### Added
- `raycast_v140`, `get_collision_events_v140`, `set/get_time_scale_v140`, `get_body_momentum_v140`
- 10 new tests (240 total)
## [1.3.0] - 2026-03-11
### Added
- `snapshot/restore_scene_v130`, `get_body_aabb_v130`, `apply_torque_v130`
- 10 new tests (230 total)
## [1.2.0] - 2026-03-11
### Added
- `get_joint_info_v120`, `create_spring_joint_v120`, `set_body_rotation_v120`, `get_body_energy_v120`
- 10 new tests (220 total)
## [1.1.0] - 2026-03-11
### Added
- `get_body_velocity_v110`, `set_body_position_v110`, `get_contact_pairs_v110`
- 9 new tests (210 total)
## [1.0.0] - 2026-03-11 🎉
### Added
- **Get body tag**`get_body_tag_v100(body)`
- **Body list**`body_list_v100()` → active body IDs
- **Apply impulse**`apply_impulse_v100(body, ix, iy)`
- **Get mass**`get_body_mass_v100(body)`
- **Set friction**`set_friction_v100(body, f)`
- **World bounds**`get_world_bounds_v100()` → [w, h]
- **Body exists**`body_exists_v100(body)`
- **Reset world**`reset_world_v100()`
- **Engine version**`engine_version_v100()` → "1.0.0"
- Body tags, list, impulse, mass, friction, world bounds, reset, version
- 9 new tests (201 total)
## [0.95.0] — Body count, step count, gravity, frozen, color, AABB, raycast, restitution, emitters
## [0.90.0] — Layers, gravity scale, angular vel, body type, world gravity, freeze/unfreeze, tag

View file

@ -1,6 +1,6 @@
[package]
name = "ds-physics"
version = "1.0.0"
version = "1.15.0"
edition.workspace = true
license.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,49 @@
# Changelog
## [1.6.0] - 2026-03-11
### Added
- **`throttle_init/check/dropped_v160`** — WASM FPS throttler
- **`cipher_set_key/apply/reset_v160`** — WASM XOR-rotate cipher
- **`checkpoint_capture/seq_v160`** — WASM stream checkpoint
- 7 new tests (218 total)
## [1.5.0] - 2026-03-11
### Added
- **`bw_init/try_send/refill_v150`** — WASM bandwidth limiter
- **`dedup_check/count/reset_v150`** — WASM frame dedup
- **`metrics_send/loss/bytes/frames/lost/reset_v150`** — WASM metrics
- 7 new tests (211 total)
## [1.4.0] - 2026-03-11
### Added
- **`compositor_*_v140`** — WASM frame compositor
- **`recorder_*_v140`** — WASM stream recording
- **`priority_*_v140`** — WASM priority queue
- 7 new tests (204 total)
## [1.3.0] - 2026-03-11
### Added
- `quality_decide_v130`, `reorder_*_v130`, `channel_auth_check_v130`
- 7 new tests (197 total)
## [1.2.0] - 2026-03-11
### Added
- `haptic_*_v120`, `batch/unbatch_v120`, `digest_*_v120`
- 7 new tests (190 total)
## [1.1.0] - 2026-03-11
### Added
- `error_frame_v110`, `encrypt/decrypt_v110`, `protocol_info_v110`
- 9 new tests (183 total)
## [1.0.0] - 2026-03-11 🎉
### Added
- **`pipeline_add/count_v100`** — stream pipeline
- **`proto_header_v100`** — protocol header
- **`splitter_push/pop_v100`** — frame splitter
- **`cwnd_ack/loss_v100`** — congestion window
- **`stream_stats_v100`** — stream stats
- **`ack_window_v100`** — sliding ACK window
- **`codec_register/list_v100`** — codec registry
- **`flow_credit_v100`** — flow control
- **`version_negotiate_v100`** — version negotiation
- Core WASM bindings for streaming protocol
- 9 new tests (174 total)
## [0.95.0] — lz4, telemetry, diff, backoff, mirror, quota, heartbeat, tag, mavg
## [0.90.0] — XOR v2, channel, ack, pool, bw, mux, nonce, validate, retry

View file

@ -1,6 +1,6 @@
[package]
name = "ds-stream-wasm"
version = "1.0.0"
version = "1.15.0"
edition.workspace = true
license.workspace = true
description = "WebAssembly codec for DreamStack bitstream protocol"

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,98 @@
# Changelog
## [2.3.0] - 2026-03-11 — Pipeline v4
### Added
- **`ChecksumCodec`** — CRC32 integrity: append on encode, verify+strip on decode, Error on mismatch
- **`RateLimitCodec`** — Token bucket algorithm (burst-tolerant, refills over time)
- **`TagCodec`** — Attach channel ID for mux routing, drop wrong-channel frames on decode
- **`Pipeline::chain()`** — Compose two pipelines into one
- **`Pipeline::describe()`** — Human-readable dump (`dedup → compress → encrypt`)
- Error propagation tested — corrupt checksum → `PipelineResult.errors`
- 474 total tests (+13 new)
## [2.2.0] - 2026-03-11 — Pipeline v3
### Added
- **`PipelineResult`** — returns frames + collected errors + consumed count
- **`StageMetric`** — per-stage frames_in/frames_out/consumed/errors observability
- **`ReassemblyCodec`** — reassemble chunked frames (counterpart to SlicerCodec)
- **`ConditionalCodec`** — wrap any codec with runtime enable/disable toggle
- **`Pipeline::signal(key)`** — preset: dedup→compress→encrypt
- **`Pipeline::media(mtu)`** — preset: compress→slicer
- **`Pipeline::metrics()`** — per-stage counter snapshot
- Slicer↔Reassembly roundtrip verified
- 461 total tests (+12 new)
## [2.1.0] - 2026-03-11 — Pipeline v2
### Added
- **`CodecOutput::Many`** — codecs can fan-out (1 frame → N frames)
- **`EncryptCodec`** — XOR cipher encrypt/decrypt adapter
- **`FilterCodec`** — drop frames by type (`FilterCodec::drop_control()`)
- **`Codec::reset()`** — clear internal state on reconnect
- **`Pipeline::encode_all/decode_all`** — batch frame processing
- **`SlicerCodec`** rewritten — real MTU chunking via `Many`
- Full roundtrip test — Compress→Encrypt→encode→decode→verify
- 449 total tests (+11 new)
## [2.0.0] - 2026-03-11 — Pipeline Architecture 🏗️
### Added
- **[NEW] `pipeline.rs`** — `Frame`, `CodecResult`, `Codec` trait, `Pipeline` builder
- **6 codec adapters**`PassthroughCodec`, `DedupCodec`, `CompressCodec`, `PacerCodec`, `SlicerCodec`, `StatsCodec`
- 15 pipeline tests (438 total)
## [1.6.0] - 2026-03-11
### Added
- **`AesFrameCipher`** — XOR-rotate frame cipher with configurable key and round tracking (encrypt, decrypt, rounds, reset)
- **`LossInjector`** — deterministic packet loss simulation for testing (should_deliver, total_dropped, reset)
- **`StreamCheckpoint`** — serializable stream state snapshot as 32 bytes (capture, to_bytes, from_bytes)
- 10 new tests (333 total)
## [1.5.0] - 2026-03-11
### Added
- **`ContentDedup`** — hash-based frame deduplication (check, dedup_count, reset)
- **`StreamHealthTracker`** — latency/throughput/loss tracking
- **`FrameThrottler`** — FPS-based frame throttling
- 10 new tests (323 total)
## [1.4.0] - 2026-03-11
### Added
- **`SessionRecorder`** — frame recording with timestamps
- **`PriorityScheduler`** — priority-based frame scheduling
- **`BandwidthLimiter`** — token bucket rate limiter
- 10 new tests (313 total)
## [1.3.0] - 2026-03-11
### Added
- **`QualityDecision`** / **`QualityPolicy`** — adaptive quality
- **`FrameLayerCompositor`** — multi-source compositing
- **`ReorderBuffer`** — out-of-order frame reordering
- 12 new tests (303 total)
## [1.2.0] - 2026-03-11
### Added
- **`HapticPayload`** / **`haptic_frame()`** — haptic vibration
- **`batch_frames/unbatch_frames`** — frame coalescing
- **`StreamDigest`** / **`ChannelAuth`** — integrity + auth
- 14 new tests (291 total)
## [1.1.0] - 2026-03-11
### Added
- **`FrameType::Error`** / **`ErrorPayload`** — error frames
- **`encrypt_frame/decrypt_frame`** — XOR envelope
- **`FrameRouter`** — content-based dispatch
- 13 new tests (277 total)
## [1.0.0] - 2026-03-11 🎉
### Added
- **`StreamPipeline`** — ordered transform chain
- **`ProtocolHeader`** — v1.0 protocol header (encode/decode)
- **`FrameSplitterV2`** — split + reassemble frames by MTU
- **`CongestionWindowV2`** — TCP-like cwnd (slow start + AIMD)
- **`StreamStatsV2`** — comprehensive stream stats
- **`AckWindow`** — sliding window ACK
- **`CodecRegistryV2`** — named codec registry
- **`FlowControllerV2`** — credit-based flow control
- **`VersionNegotiator`** — protocol version negotiation
- Core streaming: pipeline, protocol, splitter, congestion, stats, ACK, codec, flow control, version negotiation
- 9 new tests (264 total)
## [0.95.0] — Lz4Lite, TelemetrySink, FrameDiffer, BackoffTimer, StreamMirror, QuotaManager, HeartbeatV2, TagFilter, MovingAverage
## [0.90.0] — XorCipherV2, ChannelRouter, AckTracker, FramePoolV2, BandwidthEstimatorV2, PriorityMux, Nonce, Validator, Retry

View file

@ -1,6 +1,6 @@
[package]
name = "ds-stream"
version = "1.0.0"
version = "2.3.0"
edition.workspace = true
license.workspace = true
description = "Universal bitstream streaming — any input to any output"

File diff suppressed because it is too large Load diff

View file

@ -25,3 +25,4 @@ pub mod protocol;
pub mod codec;
pub mod relay;
pub mod ds_hub;
pub mod pipeline;

File diff suppressed because it is too large Load diff

View file

@ -83,6 +83,8 @@ pub enum FrameType {
Keyframe = 0xF0,
/// Authentication handshake
Auth = 0x0F,
/// In-band error report
Error = 0xEE,
/// Acknowledgement — receiver → source with seq + RTT
Ack = 0xFD,
/// Heartbeat / keep-alive
@ -115,6 +117,7 @@ impl FrameType {
0x52 => Some(Self::Compressed),
0xF0 => Some(Self::Keyframe),
0x0F => Some(Self::Auth),
0xEE => Some(Self::Error),
0xFD => Some(Self::Ack),
0xFE => Some(Self::Ping),
0xFF => Some(Self::End),
@ -146,6 +149,7 @@ impl FrameType {
Self::Compressed => "Compressed",
Self::Keyframe => "Keyframe",
Self::Auth => "Auth",
Self::Error => "Error",
Self::Ack => "Ack",
Self::Ping => "Ping",
Self::End => "End",
@ -924,6 +928,144 @@ pub fn compute_health(frame_count: u64, avg_rtt_ms: f64, throughput_bps: f64, dr
health as f32
}
// ─── v1.1: Error Payload ───
/// Structured error payload for Error frames (0xEE).
/// Format: [error_code:u16 LE][message_len:u16 LE][utf8_message]
#[derive(Debug, Clone, PartialEq)]
pub struct ErrorPayload {
pub error_code: u16,
pub message: String,
}
impl ErrorPayload {
pub const MIN_SIZE: usize = 4;
pub fn new(error_code: u16, message: &str) -> Self {
Self { error_code, message: message.to_string() }
}
pub fn encode(&self) -> Vec<u8> {
let msg_bytes = self.message.as_bytes();
let msg_len = msg_bytes.len().min(u16::MAX as usize);
let mut buf = Vec::with_capacity(4 + msg_len);
buf.extend_from_slice(&self.error_code.to_le_bytes());
buf.extend_from_slice(&(msg_len as u16).to_le_bytes());
buf.extend_from_slice(&msg_bytes[..msg_len]);
buf
}
pub fn decode(buf: &[u8]) -> Option<Self> {
if buf.len() < Self::MIN_SIZE { return None; }
let error_code = u16::from_le_bytes([buf[0], buf[1]]);
let msg_len = u16::from_le_bytes([buf[2], buf[3]]) as usize;
if buf.len() < 4 + msg_len { return None; }
let message = String::from_utf8_lossy(&buf[4..4 + msg_len]).to_string();
Some(Self { error_code, message })
}
}
// ─── v1.1: Protocol Info ───
/// Static protocol information for introspection.
pub struct ProtocolInfo;
impl ProtocolInfo {
pub const VERSION: u16 = PROTOCOL_VERSION;
pub const MAGIC_BYTES: [u8; 2] = MAGIC;
pub const HEADER_BYTES: usize = HEADER_SIZE;
pub const MAX_PAYLOAD: u32 = u32::MAX;
/// Human-readable protocol description.
pub fn describe() -> String {
format!(
"DreamStack Bitstream Protocol v{} (magic: {:02X}{:02X}, header: {} bytes)",
Self::VERSION, Self::MAGIC_BYTES[0], Self::MAGIC_BYTES[1], Self::HEADER_BYTES
)
}
}
// ─── v1.2: Haptic Payload ───
/// Structured haptic vibration payload.
/// Format: [intensity:u8][duration_ms:u16 LE][pattern:u8]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HapticPayload {
pub intensity: u8,
pub duration_ms: u16,
pub pattern: u8,
}
impl HapticPayload {
pub const SIZE: usize = 4;
pub fn new(intensity: u8, duration_ms: u16, pattern: u8) -> Self {
Self { intensity, duration_ms, pattern }
}
pub fn encode(&self) -> [u8; Self::SIZE] {
let d = self.duration_ms.to_le_bytes();
[self.intensity, d[0], d[1], self.pattern]
}
pub fn decode(buf: &[u8]) -> Option<Self> {
if buf.len() < Self::SIZE { return None; }
Some(Self {
intensity: buf[0],
duration_ms: u16::from_le_bytes([buf[1], buf[2]]),
pattern: buf[3],
})
}
}
// ─── v1.3: Quality Decision ───
/// Frame output mode for adaptive quality.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameMode {
FullPixels = 0,
Delta = 1,
SignalOnly = 2,
Disabled = 3,
}
impl FrameMode {
pub fn from_u8(v: u8) -> Self {
match v {
0 => FrameMode::FullPixels,
1 => FrameMode::Delta,
2 => FrameMode::SignalOnly,
3 => FrameMode::Disabled,
_ => FrameMode::FullPixels,
}
}
}
/// Quality tier decision: what to send at a given quality level.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct QualityDecision {
pub frame_mode: FrameMode,
pub compress: bool,
pub target_fps: u8,
}
impl QualityDecision {
pub fn new(frame_mode: FrameMode, compress: bool, target_fps: u8) -> Self {
Self { frame_mode, compress, target_fps }
}
/// Default decision for a tier (0=lowest, 4=highest).
pub fn for_tier(tier: u8) -> Self {
match tier {
0 => Self::new(FrameMode::SignalOnly, false, 5),
1 => Self::new(FrameMode::SignalOnly, false, 15),
2 => Self::new(FrameMode::Delta, true, 24),
3 => Self::new(FrameMode::Delta, false, 30),
_ => Self::new(FrameMode::FullPixels, false, 60),
}
}
}
// ─── Tests ───
#[cfg(test)]
@ -981,7 +1123,7 @@ mod tests {
#[test]
fn frame_type_roundtrip() {
for val in [0x01, 0x02, 0x03, 0x10, 0x11, 0x20, 0x30, 0x31, 0x40, 0x41, 0xF0, 0xFD, 0xFE, 0xFF] {
for val in [0x01, 0x02, 0x03, 0x10, 0x11, 0x20, 0x30, 0x31, 0x40, 0x41, 0xEE, 0xF0, 0xFD, 0xFE, 0xFF] {
let ft = FrameType::from_u8(val);
assert!(ft.is_some(), "FrameType::from_u8({:#x}) should be Some", val);
assert_eq!(ft.unwrap() as u8, val);
@ -1173,4 +1315,82 @@ mod tests {
fn bci_input_event_too_short() {
assert!(BciInputEvent::decode(&[0u8; 2]).is_none());
}
// ─── v1.1 tests ───
#[test]
fn error_payload_roundtrip() {
let err = ErrorPayload::new(401, "auth required");
let encoded = err.encode();
let decoded = ErrorPayload::decode(&encoded).unwrap();
assert_eq!(decoded.error_code, 401);
assert_eq!(decoded.message, "auth required");
}
#[test]
fn error_payload_too_short() {
assert!(ErrorPayload::decode(&[0u8; 3]).is_none());
}
#[test]
fn error_payload_truncated_message() {
// Header says 10 bytes of message but only 2 provided
let buf = [0x01, 0x00, 0x0A, 0x00, b'h', b'i'];
assert!(ErrorPayload::decode(&buf).is_none());
}
#[test]
fn error_frame_type_exists() {
let ft = FrameType::from_u8(0xEE);
assert_eq!(ft, Some(FrameType::Error));
assert_eq!(FrameType::Error.name(), "Error");
}
#[test]
fn protocol_info_constants() {
assert_eq!(ProtocolInfo::VERSION, PROTOCOL_VERSION);
assert_eq!(ProtocolInfo::MAGIC_BYTES, MAGIC);
assert_eq!(ProtocolInfo::HEADER_BYTES, 16);
let desc = ProtocolInfo::describe();
assert!(desc.contains("DreamStack"));
assert!(desc.contains("16 bytes"));
}
// ─── v1.2 tests ───
#[test]
fn haptic_payload_roundtrip() {
let h = HapticPayload::new(200, 500, 3);
let encoded = h.encode();
let decoded = HapticPayload::decode(&encoded).unwrap();
assert_eq!(decoded.intensity, 200);
assert_eq!(decoded.duration_ms, 500);
assert_eq!(decoded.pattern, 3);
}
#[test]
fn haptic_payload_too_short() {
assert!(HapticPayload::decode(&[0u8; 3]).is_none());
}
// ─── v1.3 tests ───
#[test]
fn quality_decision_default_tiers() {
let t0 = QualityDecision::for_tier(0);
assert_eq!(t0.frame_mode, FrameMode::SignalOnly);
assert_eq!(t0.target_fps, 5);
let t4 = QualityDecision::for_tier(4);
assert_eq!(t4.frame_mode, FrameMode::FullPixels);
assert_eq!(t4.target_fps, 60);
}
#[test]
fn frame_mode_roundtrip() {
assert_eq!(FrameMode::from_u8(0), FrameMode::FullPixels);
assert_eq!(FrameMode::from_u8(1), FrameMode::Delta);
assert_eq!(FrameMode::from_u8(2), FrameMode::SignalOnly);
assert_eq!(FrameMode::from_u8(3), FrameMode::Disabled);
assert_eq!(FrameMode::from_u8(99), FrameMode::FullPixels);
}
}

View file

@ -376,6 +376,58 @@ impl ChannelState {
}
}
// ─── v1.2: Auth-Gated Channel ───
/// Per-channel authentication state.
/// When `required` is true, sources must send a valid `Auth` frame before
/// the relay will forward their frames.
#[derive(Debug, Clone)]
pub struct ChannelAuth {
required: bool,
/// Pre-shared key for this channel (empty = open)
key: Vec<u8>,
/// Authenticated source addresses (by string identifier)
authenticated: Vec<String>,
}
impl ChannelAuth {
pub fn open() -> Self {
ChannelAuth { required: false, key: Vec::new(), authenticated: Vec::new() }
}
pub fn with_key(key: &[u8]) -> Self {
ChannelAuth { required: true, key: key.to_vec(), authenticated: Vec::new() }
}
/// Check if a source is authenticated.
pub fn is_authenticated(&self, source_id: &str) -> bool {
if !self.required { return true; }
self.authenticated.iter().any(|s| s == source_id)
}
/// Attempt to authenticate a source with a token.
/// Returns true if authentication succeeded.
pub fn authenticate(&mut self, source_id: &str, token: &[u8]) -> bool {
if !self.required { return true; }
if token == self.key.as_slice() {
if !self.authenticated.iter().any(|s| s == source_id) {
self.authenticated.push(source_id.to_string());
}
true
} else {
false
}
}
/// Revoke authentication for a source.
pub fn revoke(&mut self, source_id: &str) {
self.authenticated.retain(|s| s != source_id);
}
pub fn is_required(&self) -> bool { self.required }
pub fn authenticated_count(&self) -> usize { self.authenticated.len() }
}
/// Shared relay state — holds all channels.
struct RelayState {
/// Named channels: "default", "main", "player1", etc.
@ -1757,4 +1809,40 @@ mod tests {
cache.process_frame(&f1);
assert_eq!(cache.replay_len(), 0); // Nothing stored when depth=0
}
// ─── v1.2: Channel Auth Tests ───
#[test]
fn channel_auth_open_allows_all() {
let auth = ChannelAuth::open();
assert!(auth.is_authenticated("any-source"));
assert!(!auth.is_required());
}
#[test]
fn channel_auth_rejects_unauthed() {
let auth = ChannelAuth::with_key(b"secret-123");
assert!(auth.is_required());
assert!(!auth.is_authenticated("source-1"));
}
#[test]
fn channel_auth_allows_authed() {
let mut auth = ChannelAuth::with_key(b"secret-123");
assert!(!auth.authenticate("source-1", b"wrong-key"));
assert!(!auth.is_authenticated("source-1"));
assert!(auth.authenticate("source-1", b"secret-123"));
assert!(auth.is_authenticated("source-1"));
assert_eq!(auth.authenticated_count(), 1);
}
#[test]
fn channel_auth_revoke() {
let mut auth = ChannelAuth::with_key(b"key");
auth.authenticate("src", b"key");
assert!(auth.is_authenticated("src"));
auth.revoke("src");
assert!(!auth.is_authenticated("src"));
assert_eq!(auth.authenticated_count(), 0);
}
}

43
setup_forgejo.sh Executable file
View file

@ -0,0 +1,43 @@
#!/bin/bash
echo "=== Forgejo Repository Setup ==="
echo "This will create the 'dreamstack' repository and push your code."
echo ""
read -p "Forgejo Username: " USERNAME
read -s -p "Forgejo Password: " PASSWORD
echo ""
echo "--------------------------------"
# Create repository using Basic Auth
echo "Creating repository 'dreamstack' under @$USERNAME..."
HTTP_STATUS=$(curl -s -o /tmp/repo_response.json -w "%{http_code}" -u "$USERNAME:$PASSWORD" -X POST https://registry.spaceoperator.org/api/v1/user/repos -H "Content-Type: application/json" -d '{"name": "dreamstack", "private": false}')
if [ "$HTTP_STATUS" -eq 201 ] || [ "$HTTP_STATUS" -eq 200 ]; then
echo "✅ Repository created successfully!"
elif grep -q "repository already exists" /tmp/repo_response.json; then
echo " Repository already exists, proceeding to push..."
else
echo "❌ Failed to create repository (HTTP $HTTP_STATUS)"
cat /tmp/repo_response.json
echo ""
exit 1
fi
# Set up git remote and push
echo "Pushing code to Forgejo..."
cd /home/amir/code/dreamstack || exit 1
# Encode password for URL to prevent issues with special characters
# (Simple approach for git remote)
git remote remove origin 2>/dev/null
git remote add origin "https://$USERNAME:$PASSWORD@registry.spaceoperator.org/$USERNAME/dreamstack.git"
echo "Pushing main branch..."
git push -u origin main
if [ $? -ne 0 ]; then
echo "Pushing master branch instead..."
git push -u origin master
fi
# Clean up credentials from remote URL to leave working copy secure
git remote set-url origin "https://registry.spaceoperator.org/$USERNAME/dreamstack.git"
echo "✅ Done! Your code is now pushed to https://registry.spaceoperator.org/$USERNAME/dreamstack"