Skip to main content

libfw_core/
ws.rs

1//! WebSocket transport protocol shared by the server and the browser client.
2//!
3//! Both **upload and download use the exact same block-transfer engine** over
4//! a single WebSocket connection per file:
5//!
6//! - The sender pipelines fixed-size blocks without waiting for a per-block
7//!   acknowledgment, so blocks may be sent out of order and
8//!   throughput is bounded by bandwidth instead of `block_size / RTT`.
9//! - The receiver verifies **every** block in real time (CRC32 + bounds) and
10//!   marks bad blocks with a [`FRAME_NAK`]; the sender re-adds those indices
11//!   to its transfer queue.
12//! - A wave boundary ([`FRAME_WAVE_DONE`]) triggers a reconciliation round:
13//!   the receiver replies with a [`FRAME_REQ`] listing every block still not
14//!   verified, which the sender re-queues and re-sends. This repeats until
15//!   the receiver has verified all blocks, at which point it sends
16//!   [`FRAME_COMPLETE`] (for downloads the client is the receiver and sends
17//!   it; for uploads the server is the receiver, commits the file and sends
18//!   it).
19//!
20//! All control commands (handshake, directory listing, metadata) travel over
21//! the same WebSocket; nothing uses separate HTTP requests anymore on the
22//! client transfer path.
23//!
24//! ## Frame layout
25//!
26//! Every frame is `[type: u8][payload: ...]`. Control frames carry a JSON
27//! payload; data frames carry a compact binary payload (see the individual
28//! builders/parsers below).
29
30use serde::{Deserialize, Serialize};
31
32use crate::constants::protocol_header_value;
33
34// ---------------------------------------------------------------------------
35// Frame type constants
36// ---------------------------------------------------------------------------
37
38/// Client → server handshake (`{"protocol","token"}`).
39pub const FRAME_HELLO: u8 = 0x01;
40/// Server → client handshake acknowledgment (`{"ok":true}`).
41pub const FRAME_HELLO_OK: u8 = 0x02;
42/// Client → server directory listing request (`{"path"}`).
43pub const FRAME_LIST_REQ: u8 = 0x10;
44/// Server → client directory listing reply (`{"path","entries":[...]}`).
45pub const FRAME_LIST_REPLY: u8 = 0x11;
46/// Client → server file metadata request (`{"path"}`).
47pub const FRAME_META_REQ: u8 = 0x12;
48/// Server → client file metadata reply (`{"path","size","mtime","etag"}`).
49pub const FRAME_META_REPLY: u8 = 0x13;
50/// Client → server transfer start (`StartRequest`).
51pub const FRAME_START: u8 = 0x20;
52/// Server → client transfer ready (`ReadyReply`).
53pub const FRAME_READY: u8 = 0x21;
54/// Block payload (binary): `[index:u32][crc:u32][raw_len:u32][data]`.
55pub const FRAME_BLOCK: u8 = 0x30;
56/// Receiver marks a block bad → sender re-queues it (binary `[index:u32]`).
57pub const FRAME_NAK: u8 = 0x31;
58/// Receiver asks the sender to re-send a set of blocks
59/// (binary `[count:u32][index:u32 ...]`).
60pub const FRAME_REQ: u8 = 0x32;
61/// Sender finished a wave of blocks (empty payload).
62pub const FRAME_WAVE_DONE: u8 = 0x33;
63/// Receiver completed the transfer (`CompleteMessage`, JSON).
64pub const FRAME_COMPLETE: u8 = 0x34;
65/// Protocol error (`{"code","message"}`, JSON).
66pub const FRAME_ERROR: u8 = 0xFF;
67
68/// Read the frame type from the first byte.
69pub fn frame_type(frame: &[u8]) -> Option<u8> {
70    frame.first().copied()
71}
72
73/// Strip the type byte and return the payload slice.
74pub fn frame_payload(frame: &[u8]) -> &[u8] {
75    frame.get(1..).unwrap_or(&[])
76}
77
78// ---------------------------------------------------------------------------
79// Control frames (JSON payload)
80// ---------------------------------------------------------------------------
81
82/// Build a control frame from a serializable JSON payload.
83pub fn control_frame<T: Serialize>(kind: u8, payload: &T) -> Vec<u8> {
84    let json = serde_json::to_vec(payload).unwrap_or_default();
85    let mut out = Vec::with_capacity(1 + json.len());
86    out.push(kind);
87    out.extend_from_slice(&json);
88    out
89}
90
91/// Parse a control frame's JSON payload into `T`.
92pub fn parse_control<'a, T: Deserialize<'a>>(frame: &'a [u8], kind: u8) -> Option<T> {
93    if frame.first() != Some(&kind) {
94        return None;
95    }
96    serde_json::from_slice(frame.get(1..)?).ok()
97}
98
99/// The `FRAME_HELLO` payload.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct Hello {
102    /// Protocol handshake value (must equal [`protocol_header_value`]).
103    pub protocol: String,
104    /// Bearer token.
105    pub token: String,
106}
107
108impl Hello {
109    /// Build a well-formed hello payload.
110    pub fn new(token: &str) -> Self {
111        Hello {
112            protocol: protocol_header_value().to_string(),
113            token: token.to_string(),
114        }
115    }
116}
117
118/// Direction of a transfer.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
120#[serde(rename_all = "lowercase")]
121pub enum TransferKind {
122    Upload,
123    Download,
124}
125
126/// The `FRAME_START` payload (client → server).
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct StartRequest {
129    /// Upload or download.
130    pub kind: TransferKind,
131    /// Virtual path.
132    pub path: String,
133    /// Declared size (upload; the download side ignores it).
134    #[serde(default)]
135    pub size: u64,
136    /// Last-modified unix time (upload).
137    #[serde(default)]
138    pub mtime: u64,
139    /// Deterministic content id (upload; server derives the session temp from
140    /// it so an interrupted upload resumes the same shared temp).
141    #[serde(default)]
142    pub etag: String,
143    /// Whether to compress each block payload.
144    #[serde(default)]
145    pub compress: bool,
146    /// `"create"` | `"overwrite"` (upload only).
147    #[serde(default)]
148    pub mode: String,
149    /// Resume offset (download only): the first block covers `offset..`.
150    #[serde(default)]
151    pub offset: u64,
152    /// Block size the sender will use.
153    #[serde(default)]
154    pub block_size: u64,
155    /// In-flight block window the sender should use (0 = server default).
156    ///
157    /// The download sender on the server pipelines up to this many blocks
158    /// per wave before a reconciliation round, so the browser's
159    /// `downloadWindow`/`uploadWindow` knobs stay meaningful.
160    #[serde(default)]
161    pub window: u32,
162}
163
164/// The `FRAME_READY` payload (server → client).
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct ReadyReply {
167    /// Upload or download.
168    pub kind: TransferKind,
169    /// Virtual path.
170    pub path: String,
171    /// Authoritative size (download: real file size; upload: declared size).
172    #[serde(default)]
173    pub size: u64,
174    /// Last-modified unix time.
175    #[serde(default)]
176    pub mtime: u64,
177    /// Authoritative ETag.
178    #[serde(default)]
179    pub etag: String,
180    /// Whether this transfer is compressed.
181    #[serde(default)]
182    pub compress: bool,
183    /// Block size in bytes.
184    pub block_size: u64,
185    /// Number of blocks in this transfer (indexes `0..total_blocks`).
186    pub total_blocks: u32,
187    /// Download only: absolute file offset the first block begins at.
188    #[serde(default)]
189    pub offset: u64,
190    /// Upload only: byte ranges the server already holds (resume), as
191    /// `[[start, end], ...]`.
192    #[serde(default)]
193    pub received: Vec<[u64; 2]>,
194}
195
196/// The `FRAME_COMPLETE` payload (receiver → sender).
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct CompleteMessage {
199    /// Whether the receiver accepted the whole transfer.
200    pub ok: bool,
201    /// Final size (download: bytes received; upload: committed size).
202    #[serde(default)]
203    pub size: u64,
204    /// Human-readable error when `ok` is false.
205    #[serde(default, skip_serializing_if = "Option::is_none")]
206    pub error: Option<String>,
207}
208
209impl CompleteMessage {
210    /// A successful completion.
211    pub fn ok(size: u64) -> Self {
212        CompleteMessage {
213            ok: true,
214            size,
215            error: None,
216        }
217    }
218
219    /// A failed completion.
220    pub fn err(message: impl Into<String>) -> Self {
221        CompleteMessage {
222            ok: false,
223            size: 0,
224            error: Some(message.into()),
225        }
226    }
227}
228
229/// The `FRAME_ERROR` payload.
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct ErrorMessage {
232    /// Machine-readable category.
233    pub code: String,
234    /// Human-readable detail.
235    pub message: String,
236}
237
238// ---------------------------------------------------------------------------
239// Block data frames (binary)
240// ---------------------------------------------------------------------------
241
242/// A decoded `FRAME_BLOCK` payload.
243#[derive(Debug, Clone)]
244pub struct Block {
245    /// Zero-based block index.
246    pub index: u32,
247    /// CRC32 of the on-wire `data` payload.
248    pub crc: u32,
249    /// Decompressed length of `data` (== `data.len()` when uncompressed).
250    pub raw_len: u32,
251    /// The block bytes (possibly a compressed frame).
252    pub data: Vec<u8>,
253}
254
255/// Build a `FRAME_BLOCK` frame.
256pub fn block_frame(index: u32, crc: u32, raw_len: u32, data: &[u8]) -> Vec<u8> {
257    let mut out = Vec::with_capacity(1 + 12 + data.len());
258    out.push(FRAME_BLOCK);
259    out.extend_from_slice(&index.to_be_bytes());
260    out.extend_from_slice(&crc.to_be_bytes());
261    out.extend_from_slice(&raw_len.to_be_bytes());
262    out.extend_from_slice(data);
263    out
264}
265
266/// Parse a `FRAME_BLOCK` frame.
267pub fn parse_block(frame: &[u8]) -> Option<Block> {
268    if frame.first() != Some(&FRAME_BLOCK) || frame.len() < 13 {
269        return None;
270    }
271    Some(Block {
272        index: u32::from_be_bytes(frame[1..5].try_into().ok()?),
273        crc: u32::from_be_bytes(frame[5..9].try_into().ok()?),
274        raw_len: u32::from_be_bytes(frame[9..13].try_into().ok()?),
275        data: frame[13..].to_vec(),
276    })
277}
278
279/// Build a `FRAME_NAK` frame for `index`.
280pub fn nak_frame(index: u32) -> Vec<u8> {
281    let mut out = vec![FRAME_NAK];
282    out.extend_from_slice(&index.to_be_bytes());
283    out
284}
285
286/// Build a `FRAME_REQ` frame for a set of indices.
287pub fn req_frame(indices: &[u32]) -> Vec<u8> {
288    let mut out = Vec::with_capacity(5 + indices.len() * 4);
289    out.push(FRAME_REQ);
290    out.extend_from_slice(&(indices.len() as u32).to_be_bytes());
291    for i in indices {
292        out.extend_from_slice(&i.to_be_bytes());
293    }
294    out
295}
296
297/// Parse a `FRAME_REQ` frame into the requested indices.
298pub fn parse_req(frame: &[u8]) -> Option<Vec<u32>> {
299    if frame.first() != Some(&FRAME_REQ) || frame.len() < 5 {
300        return None;
301    }
302    let count = u32::from_be_bytes(frame[1..5].try_into().ok()?) as usize;
303    // A REQ frame carries exactly `count` 4-byte indices after the 5-byte
304    // header. Validate the count against the ACTUAL payload length BEFORE
305    // allocating: a malformed frame claiming billions of indices must not be
306    // able to force a ~`count * 4` GiB pre-allocation (memory-exhaustion DoS,
307    // reachable from both the server and the WASM client).
308    if count > (frame.len() - 5) / 4 {
309        return None;
310    }
311    let mut out = Vec::with_capacity(count);
312    let mut off = 5usize;
313    for _ in 0..count {
314        out.push(u32::from_be_bytes(frame[off..off + 4].try_into().ok()?));
315        off += 4;
316    }
317    Some(out)
318}
319
320/// Parse the block index out of a `FRAME_NAK` frame.
321pub fn parse_nak(frame: &[u8]) -> Option<u32> {
322    if frame.first() != Some(&FRAME_NAK) || frame.len() < 5 {
323        return None;
324    }
325    Some(u32::from_be_bytes(frame[1..5].try_into().ok()?))
326}
327
328/// A `FRAME_WAVE_DONE` frame (empty payload).
329pub fn wave_done_frame() -> Vec<u8> {
330    vec![FRAME_WAVE_DONE]
331}
332
333// ---------------------------------------------------------------------------
334// Checksums
335// ---------------------------------------------------------------------------
336
337/// CRC32 of a byte slice, used to verify every block in real time.
338pub fn crc32(data: &[u8]) -> u32 {
339    crc32fast::hash(data)
340}
341
342// ---------------------------------------------------------------------------
343// Block math
344// ---------------------------------------------------------------------------
345
346/// The number of blocks covering `size` bytes at `block_size`.
347pub fn block_count(size: u64, block_size: u64) -> u32 {
348    let bs = block_size.max(1);
349    if size == 0 {
350        1
351    } else {
352        // Saturate instead of truncating: `div_ceil == 2³²` (e.g. 4 GiB at
353        // 1-byte blocks) would otherwise wrap to 0 and silently produce an
354        // empty, forever-pending transfer.
355        size.div_ceil(bs).min(u32::MAX as u64) as u32
356    }
357}
358
359/// The absolute `[start, end)` byte range of block `index`.
360pub fn block_bounds(index: u32, block_size: u64, size: u64) -> (u64, u64) {
361    let bs = block_size.max(1);
362    let start = index as u64 * bs;
363    // Out-of-range index: return an empty range rather than letting
364    // `end - start` underflow (a caller doing `vec![0; end - start]` would
365    // then try to allocate ~u64::MAX bytes and crash).
366    if start >= size {
367        return (size, size);
368    }
369    let end = start.saturating_add(bs).min(size);
370    (start, end)
371}
372
373/// The absolute byte offset where block `index` of a transfer that begins at
374/// file `offset` lands.
375pub fn block_offset(index: u32, block_size: u64, offset: u64) -> u64 {
376    offset.saturating_add(index as u64 * block_size)
377}
378
379// ---------------------------------------------------------------------------
380// Verified-block set (receiver side)
381// ---------------------------------------------------------------------------
382
383/// A compact bitset tracking which blocks the receiver has verified good.
384///
385/// Used identically by the upload receiver (server) and the download
386/// receiver (client) so both directions share the same "mark good / mark bad
387/// / ask for missing" logic.
388#[derive(Debug, Clone, Default)]
389pub struct BlockSet {
390    bits: Vec<u64>,
391    total: u32,
392    count: u32,
393}
394
395impl BlockSet {
396    /// A fresh, empty set covering `total` blocks.
397    pub fn new(total: u32) -> Self {
398        BlockSet {
399            bits: vec![0; (total as usize).div_ceil(64)],
400            total,
401            count: 0,
402        }
403    }
404
405    /// Mark `index` as verified.
406    pub fn insert(&mut self, index: u32) {
407        if index >= self.total {
408            return;
409        }
410        let word = (index / 64) as usize;
411        let bit = 1u64 << (index % 64);
412        if self.bits[word] & bit == 0 {
413            self.bits[word] |= bit;
414            self.count += 1;
415        }
416    }
417
418    /// Whether `index` has been verified.
419    pub fn contains(&self, index: u32) -> bool {
420        index < self.total && (self.bits[(index / 64) as usize] & (1u64 << (index % 64))) != 0
421    }
422
423    /// Number of verified blocks.
424    pub fn count(&self) -> u32 {
425        self.count
426    }
427
428    /// Total blocks this set covers.
429    pub fn total(&self) -> u32 {
430        self.total
431    }
432
433    /// All indices still not verified, in ascending order.
434    pub fn missing(&self) -> Vec<u32> {
435        let mut out = Vec::new();
436        for i in 0..self.total {
437            if !self.contains(i) {
438                out.push(i);
439            }
440        }
441        out
442    }
443
444    /// Seed from previously-received byte ranges (resume): every block
445    /// overlapping `[start, end)` is marked verified.
446    pub fn seed_from_ranges(&mut self, block_size: u64, ranges: &[(u64, u64)]) {
447        let bs = block_size.max(1);
448        for &(start, end) in ranges {
449            if end <= start {
450                continue;
451            }
452            let first = (start / bs) as u32;
453            let last = ((end - 1) / bs) as u32; // inclusive
454            for i in first..=last {
455                self.insert(i);
456            }
457        }
458    }
459}
460
461/// The block indices of `size`-byte file at `block_size` that are NOT covered
462/// by `received` ranges — the "transfer queue" a sender seeds on resume so it
463/// only retransmits the broken/lost parts.
464pub fn missing_blocks(size: u64, block_size: u64, received: &[(u64, u64)]) -> Vec<u32> {
465    let total = block_count(size, block_size);
466    let mut set = BlockSet::new(total);
467    set.seed_from_ranges(block_size, received);
468    set.missing()
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn block_count_and_bounds() {
477        assert_eq!(block_count(0, 4), 1);
478        assert_eq!(block_count(10, 4), 3);
479        assert_eq!(block_count(8, 4), 2);
480        assert_eq!(block_bounds(1, 4, 10), (4, 8));
481        assert_eq!(block_bounds(2, 4, 10), (8, 10));
482        assert_eq!(block_offset(2, 4, 100), 108);
483    }
484
485    #[test]
486    fn block_math_is_saturating_for_adversarial_inputs() {
487        // Out-of-range index → empty range, never `end < start` (underflow).
488        assert_eq!(block_bounds(u32::MAX, 1, 10), (10, 10));
489        assert_eq!(block_bounds(5, 4, 10), (10, 10));
490        // A block count that would exceed u32 saturates instead of wrapping
491        // to 0 (which would silently make the transfer never complete).
492        assert_eq!(block_count(u64::MAX, 1), u32::MAX);
493        assert_eq!(block_count(4u64 * 1024 * 1024 * 1024, 1), u32::MAX);
494    }
495
496    #[test]
497    fn crc_roundtrip_detects_corruption() {
498        let data = b"hello libfw block";
499        let crc = crc32(data);
500        let mut bad = data.to_vec();
501        bad[0] ^= 0xFF;
502        assert_ne!(crc, crc32(&bad));
503        assert_eq!(crc, crc32(data));
504    }
505
506    #[test]
507    fn block_frame_roundtrip() {
508        let data = vec![7u8; 100];
509        let frame = block_frame(42, 12345, 100, &data);
510        let parsed = parse_block(&frame).unwrap();
511        assert_eq!(parsed.index, 42);
512        assert_eq!(parsed.crc, 12345);
513        assert_eq!(parsed.raw_len, 100);
514        assert_eq!(parsed.data, data);
515        assert_eq!(frame_type(&frame), Some(FRAME_BLOCK));
516    }
517
518    #[test]
519    fn req_and_nak_roundtrip() {
520        let req = req_frame(&[0, 3, 7, 9]);
521        assert_eq!(parse_req(&req), Some(vec![0, 3, 7, 9]));
522        let nak = nak_frame(5);
523        assert_eq!(parse_nak(&nak), Some(5));
524    }
525
526    #[test]
527    fn parse_req_rejects_oversized_count_before_allocating() {
528        // A frame that CLAIMS more indices than its payload actually holds
529        // must be rejected up front (not after `Vec::with_capacity(count)`),
530        // so a crafted 9-byte frame cannot force a huge allocation.
531        let mut frame = vec![FRAME_REQ];
532        frame.extend_from_slice(&10_000_000u32.to_be_bytes());
533        for i in 0..3u32 {
534            frame.extend_from_slice(&i.to_be_bytes());
535        }
536        assert_eq!(parse_req(&frame), None);
537
538        // A well-formed frame still parses exactly.
539        let mut good = vec![FRAME_REQ];
540        good.extend_from_slice(&2u32.to_be_bytes());
541        good.extend_from_slice(&7u32.to_be_bytes());
542        good.extend_from_slice(&9u32.to_be_bytes());
543        assert_eq!(parse_req(&good), Some(vec![7, 9]));
544
545        // Truncated/empty frames are rejected.
546        assert_eq!(parse_req(&[FRAME_REQ]), None);
547        assert_eq!(parse_req(&[FRAME_REQ, 0, 0, 0, 1]), None);
548    }
549
550    #[test]
551    fn block_set_tracks_verified_and_missing() {
552        let mut set = BlockSet::new(10);
553        assert_eq!(set.total(), 10);
554        set.insert(0);
555        set.insert(3);
556        set.insert(3); // idempotent
557        assert_eq!(set.count(), 2);
558        assert!(set.contains(0));
559        assert!(!set.contains(1));
560        assert_eq!(set.missing(), vec![1, 2, 4, 5, 6, 7, 8, 9]);
561    }
562
563    #[test]
564    fn seed_from_ranges_marks_overlapping_blocks() {
565        // size 10, block 4 → blocks 0 (0..4), 1 (4..8), 2 (8..10)
566        let mut set = BlockSet::new(3);
567        set.seed_from_ranges(4, &[(0, 4)]);
568        assert!(set.contains(0));
569        assert!(!set.contains(1));
570        let mut set = BlockSet::new(3);
571        set.seed_from_ranges(4, &[(4, 10)]);
572        assert!(set.contains(1));
573        assert!(set.contains(2));
574        assert!(!set.contains(0));
575    }
576
577    #[test]
578    fn missing_blocks_from_received_ranges() {
579        // size 10, block 4 → blocks 0,1,2; received 0..4 → only 1,2 missing
580        assert_eq!(missing_blocks(10, 4, &[(0, 4)]), vec![1, 2]);
581        // nothing received → all missing
582        assert_eq!(missing_blocks(10, 4, &[]), vec![0, 1, 2]);
583        // everything received → none
584        assert_eq!(missing_blocks(10, 4, &[(0, 10)]), Vec::<u32>::new());
585    }
586
587    #[test]
588    fn control_frame_roundtrip() {
589        let hello = Hello::new("tok");
590        let frame = control_frame(FRAME_HELLO, &hello);
591        assert_eq!(frame_type(&frame), Some(FRAME_HELLO));
592        let parsed: Hello = parse_control(&frame, FRAME_HELLO).unwrap();
593        assert_eq!(parsed.token, "tok");
594        assert_eq!(parsed.protocol, protocol_header_value());
595    }
596}