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    let mut out = Vec::with_capacity(count);
304    let mut off = 5usize;
305    for _ in 0..count {
306        if off + 4 > frame.len() {
307            return None;
308        }
309        out.push(u32::from_be_bytes(frame[off..off + 4].try_into().ok()?));
310        off += 4;
311    }
312    Some(out)
313}
314
315/// Parse the block index out of a `FRAME_NAK` frame.
316pub fn parse_nak(frame: &[u8]) -> Option<u32> {
317    if frame.first() != Some(&FRAME_NAK) || frame.len() < 5 {
318        return None;
319    }
320    Some(u32::from_be_bytes(frame[1..5].try_into().ok()?))
321}
322
323/// A `FRAME_WAVE_DONE` frame (empty payload).
324pub fn wave_done_frame() -> Vec<u8> {
325    vec![FRAME_WAVE_DONE]
326}
327
328// ---------------------------------------------------------------------------
329// Checksums
330// ---------------------------------------------------------------------------
331
332/// CRC32 of a byte slice, used to verify every block in real time.
333pub fn crc32(data: &[u8]) -> u32 {
334    crc32fast::hash(data)
335}
336
337// ---------------------------------------------------------------------------
338// Block math
339// ---------------------------------------------------------------------------
340
341/// The number of blocks covering `size` bytes at `block_size`.
342pub fn block_count(size: u64, block_size: u64) -> u32 {
343    let bs = block_size.max(1);
344    if size == 0 {
345        1
346    } else {
347        (size.div_ceil(bs)) as u32
348    }
349}
350
351/// The absolute `[start, end)` byte range of block `index`.
352pub fn block_bounds(index: u32, block_size: u64, size: u64) -> (u64, u64) {
353    let start = index as u64 * block_size;
354    let end = (start + block_size).min(size);
355    (start, end)
356}
357
358/// The absolute byte offset where block `index` of a transfer that begins at
359/// file `offset` lands.
360pub fn block_offset(index: u32, block_size: u64, offset: u64) -> u64 {
361    offset.saturating_add(index as u64 * block_size)
362}
363
364// ---------------------------------------------------------------------------
365// Verified-block set (receiver side)
366// ---------------------------------------------------------------------------
367
368/// A compact bitset tracking which blocks the receiver has verified good.
369///
370/// Used identically by the upload receiver (server) and the download
371/// receiver (client) so both directions share the same "mark good / mark bad
372/// / ask for missing" logic.
373#[derive(Debug, Clone, Default)]
374pub struct BlockSet {
375    bits: Vec<u64>,
376    total: u32,
377    count: u32,
378}
379
380impl BlockSet {
381    /// A fresh, empty set covering `total` blocks.
382    pub fn new(total: u32) -> Self {
383        BlockSet {
384            bits: vec![0; (total as usize).div_ceil(64)],
385            total,
386            count: 0,
387        }
388    }
389
390    /// Mark `index` as verified.
391    pub fn insert(&mut self, index: u32) {
392        if index >= self.total {
393            return;
394        }
395        let word = (index / 64) as usize;
396        let bit = 1u64 << (index % 64);
397        if self.bits[word] & bit == 0 {
398            self.bits[word] |= bit;
399            self.count += 1;
400        }
401    }
402
403    /// Whether `index` has been verified.
404    pub fn contains(&self, index: u32) -> bool {
405        index < self.total && (self.bits[(index / 64) as usize] & (1u64 << (index % 64))) != 0
406    }
407
408    /// Number of verified blocks.
409    pub fn count(&self) -> u32 {
410        self.count
411    }
412
413    /// Total blocks this set covers.
414    pub fn total(&self) -> u32 {
415        self.total
416    }
417
418    /// All indices still not verified, in ascending order.
419    pub fn missing(&self) -> Vec<u32> {
420        let mut out = Vec::new();
421        for i in 0..self.total {
422            if !self.contains(i) {
423                out.push(i);
424            }
425        }
426        out
427    }
428
429    /// Seed from previously-received byte ranges (resume): every block
430    /// overlapping `[start, end)` is marked verified.
431    pub fn seed_from_ranges(&mut self, block_size: u64, ranges: &[(u64, u64)]) {
432        let bs = block_size.max(1);
433        for &(start, end) in ranges {
434            if end <= start {
435                continue;
436            }
437            let first = (start / bs) as u32;
438            let last = ((end - 1) / bs) as u32; // inclusive
439            for i in first..=last {
440                self.insert(i);
441            }
442        }
443    }
444}
445
446/// The block indices of `size`-byte file at `block_size` that are NOT covered
447/// by `received` ranges — the "transfer queue" a sender seeds on resume so it
448/// only retransmits the broken/lost parts.
449pub fn missing_blocks(size: u64, block_size: u64, received: &[(u64, u64)]) -> Vec<u32> {
450    let total = block_count(size, block_size);
451    let mut set = BlockSet::new(total);
452    set.seed_from_ranges(block_size, received);
453    set.missing()
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    #[test]
461    fn block_count_and_bounds() {
462        assert_eq!(block_count(0, 4), 1);
463        assert_eq!(block_count(10, 4), 3);
464        assert_eq!(block_count(8, 4), 2);
465        assert_eq!(block_bounds(1, 4, 10), (4, 8));
466        assert_eq!(block_bounds(2, 4, 10), (8, 10));
467        assert_eq!(block_offset(2, 4, 100), 108);
468    }
469
470    #[test]
471    fn crc_roundtrip_detects_corruption() {
472        let data = b"hello libfw block";
473        let crc = crc32(data);
474        let mut bad = data.to_vec();
475        bad[0] ^= 0xFF;
476        assert_ne!(crc, crc32(&bad));
477        assert_eq!(crc, crc32(data));
478    }
479
480    #[test]
481    fn block_frame_roundtrip() {
482        let data = vec![7u8; 100];
483        let frame = block_frame(42, 12345, 100, &data);
484        let parsed = parse_block(&frame).unwrap();
485        assert_eq!(parsed.index, 42);
486        assert_eq!(parsed.crc, 12345);
487        assert_eq!(parsed.raw_len, 100);
488        assert_eq!(parsed.data, data);
489        assert_eq!(frame_type(&frame), Some(FRAME_BLOCK));
490    }
491
492    #[test]
493    fn req_and_nak_roundtrip() {
494        let req = req_frame(&[0, 3, 7, 9]);
495        assert_eq!(parse_req(&req), Some(vec![0, 3, 7, 9]));
496        let nak = nak_frame(5);
497        assert_eq!(parse_nak(&nak), Some(5));
498    }
499
500    #[test]
501    fn block_set_tracks_verified_and_missing() {
502        let mut set = BlockSet::new(10);
503        assert_eq!(set.total(), 10);
504        set.insert(0);
505        set.insert(3);
506        set.insert(3); // idempotent
507        assert_eq!(set.count(), 2);
508        assert!(set.contains(0));
509        assert!(!set.contains(1));
510        assert_eq!(set.missing(), vec![1, 2, 4, 5, 6, 7, 8, 9]);
511    }
512
513    #[test]
514    fn seed_from_ranges_marks_overlapping_blocks() {
515        // size 10, block 4 → blocks 0 (0..4), 1 (4..8), 2 (8..10)
516        let mut set = BlockSet::new(3);
517        set.seed_from_ranges(4, &[(0, 4)]);
518        assert!(set.contains(0));
519        assert!(!set.contains(1));
520        let mut set = BlockSet::new(3);
521        set.seed_from_ranges(4, &[(4, 10)]);
522        assert!(set.contains(1));
523        assert!(set.contains(2));
524        assert!(!set.contains(0));
525    }
526
527    #[test]
528    fn missing_blocks_from_received_ranges() {
529        // size 10, block 4 → blocks 0,1,2; received 0..4 → only 1,2 missing
530        assert_eq!(missing_blocks(10, 4, &[(0, 4)]), vec![1, 2]);
531        // nothing received → all missing
532        assert_eq!(missing_blocks(10, 4, &[]), vec![0, 1, 2]);
533        // everything received → none
534        assert_eq!(missing_blocks(10, 4, &[(0, 10)]), Vec::<u32>::new());
535    }
536
537    #[test]
538    fn control_frame_roundtrip() {
539        let hello = Hello::new("tok");
540        let frame = control_frame(FRAME_HELLO, &hello);
541        assert_eq!(frame_type(&frame), Some(FRAME_HELLO));
542        let parsed: Hello = parse_control(&frame, FRAME_HELLO).unwrap();
543        assert_eq!(parsed.token, "tok");
544        assert_eq!(parsed.protocol, protocol_header_value());
545    }
546}