Skip to main content

blit_remote/
kv.rs

1//! Server KV store wire protocol (docs/design/kv.md).
2//!
3//! A host-local key→value store with CAS writes and prefix-watch
4//! subscriptions. Keys are raw UTF-8 (≤ [`KV_MAX_KEY`] bytes, no NUL,
5//! non-empty); values are opaque bytes, LZ4 on the wire. CAS is
6//! BLAKE3-128 over value bytes with the zero-hash absent sentinel,
7//! [docs/design/fs-write.md]'s conflict model verbatim.
8//!
9//! All integers little-endian, tightly packed, as everywhere in the protocol.
10
11use std::collections::BTreeMap;
12
13/// Subscribe to a prefix: [0x70][nonce:2][flags:1][inline_max:4][prefix_len:2][prefix:N]
14/// The prefix is a literal byte prefix (no glob); empty = whole store.
15pub const C2S_KV_OPEN: u8 = 0x70;
16/// Close a subscription: [0x71][kv_id:2]
17pub const C2S_KV_STOP: u8 = 0x71;
18/// Cumulative acknowledgement: [0x72][kv_id:2][update_id:4]
19pub const C2S_KV_ACK: u8 = 0x72;
20/// CAS put/delete: [0x73][nonce:2][flags:1][base:16][key_len:2][key:N][value:LZ4]
21/// `base` is the CAS precondition on the current value bytes: non-zero =
22/// match-or-CONFLICT, zero = create-exclusive, ignored under `KV_PUT_NO_CAS`.
23pub const C2S_KV_PUT: u8 = 0x73;
24/// Fetch one value: [0x74][nonce:2][key_len:2][key:N]
25pub const C2S_KV_FETCH: u8 = 0x74;
26
27/// Subscription accepted or refused: [0x70][nonce:2][kv_id:2][status:1][detail_len:2][detail:N]
28pub const S2C_KV_OPENED: u8 = 0x70;
29/// Snapshot/live records: [0x71][kv_id:2][update_id:4][flags:1][records:LZ4]
30pub const S2C_KV_UPDATE: u8 = 0x71;
31/// Put result: [0x72][nonce:2][status:1][hash:16][mtime_ns:8] — one per
32/// `KV_PUT`. On success `hash` is the new value hash (zero for a delete);
33/// on `CONFLICT` it carries the current hash so the client rebases without
34/// a round trip.
35pub const S2C_KV_DONE: u8 = 0x72;
36/// Fetch result: [0x73][nonce:2][status:1][hash:16][data:LZ4]
37pub const S2C_KV_VALUE: u8 = 0x73;
38/// Subscription ended server-side: [0x74][kv_id:2][reason:1]
39/// After it the `kv_id` is dead; a client that still wants the prefix
40/// re-opens with `KV_OPEN` and receives a fresh snapshot — lossless,
41/// because updates carry state, not events (docs/design/kv.md § Watch).
42pub const S2C_KV_CLOSED: u8 = 0x74;
43
44/// `S2C_HELLO` feature bit: server supports the `KV_*` family
45/// (docs/design/kv.md). `BLIT_KV=0` refuses every `KV_*` at dispatch with
46/// `PERMISSION` instead of un-advertising.
47pub const FEATURE_KV: u32 = 1 << 9;
48
49/// `kv_id` reported by a failed `KV_OPENED`.
50pub const KV_ID_INVALID: u16 = 0xFFFF;
51
52/// Maximum key length in bytes (fixed, not an env knob).
53pub const KV_MAX_KEY: usize = 256;
54
55// C2S_KV_PUT flags.
56/// Ignore `base`; unconditional put/delete.
57pub const KV_PUT_NO_CAS: u8 = 1 << 0;
58/// Remove the entry (value must be empty). A delete is a put of absence;
59/// `base` zero with DELETE is INVALID (delete-iff-absent is meaningless).
60pub const KV_PUT_DELETE: u8 = 1 << 1;
61/// fsync the store before replying; default trades durability for latency.
62pub const KV_PUT_DURABLE: u8 = 1 << 2;
63
64// S2C_KV_UPDATE flags.
65/// This batch completes the initial snapshot; subsequent updates are live.
66pub const KV_UPDATE_SNAPSHOT_END: u8 = 1 << 0;
67
68// S2C_KV_CLOSED reasons — numbered as the fs family's closed table
69// (docs/design/fs-watch.md § FS_CLOSED); 0-3 (client request, gone,
70// permission lost, backend failed) are reserved until the store can
71// produce them.
72/// Queued-unacked bytes exceeded `BLIT_KV_UNACKED_MAX`
73/// (docs/design/kv.md § Budgets); the subscription was dropped.
74pub const KV_CLOSED_RESOURCE_LIMIT: u8 = 4;
75
76// KV_DONE / KV_OPENED / KV_VALUE status — the unified git/lsp status table
77// (docs/git.md "Statuses") plus fs-write's `11 CONFLICT`. Same numeric
78// values as `FS_DONE_*` / `GIT_STATUS_*` where they overlap.
79pub const KV_STATUS_OK: u8 = 0;
80pub const KV_STATUS_NOT_FOUND: u8 = 2;
81pub const KV_STATUS_PERMISSION: u8 = 4;
82pub const KV_STATUS_TOO_LARGE: u8 = 5;
83pub const KV_STATUS_BUDGET: u8 = 6;
84pub const KV_STATUS_INVALID: u8 = 7;
85pub const KV_STATUS_OTHER: u8 = 9;
86/// A CAS precondition failed (mismatch, or create-exclusive on an existing
87/// key). `KV_DONE.hash` carries the current value hash.
88pub const KV_STATUS_CONFLICT: u8 = 11;
89
90/// Human-readable name for a `KV_*` status code.
91pub fn kv_status_text(status: u8) -> &'static str {
92    match status {
93        KV_STATUS_OK => "ok",
94        KV_STATUS_NOT_FOUND => "not found",
95        KV_STATUS_PERMISSION => "permission denied",
96        KV_STATUS_TOO_LARGE => "too large",
97        KV_STATUS_BUDGET => "budget exhausted",
98        KV_STATUS_INVALID => "invalid request",
99        KV_STATUS_CONFLICT => "conflict",
100        _ => "error",
101    }
102}
103
104/// Wire-key validity: non-empty UTF-8 (guaranteed by `&str`), ≤
105/// [`KV_MAX_KEY`] bytes, no NUL.
106pub fn kv_key_valid(key: &str) -> bool {
107    !key.is_empty() && key.len() <= KV_MAX_KEY && !key.as_bytes().contains(&0)
108}
109
110// Record kinds inside KV_UPDATE.
111pub const KV_RECORD_UPSERT: u8 = 0x01;
112pub const KV_RECORD_DELETE: u8 = 0x02;
113
114// UPSERT content kinds.
115pub const KV_CONTENT_NONE: u8 = 0;
116pub const KV_CONTENT_FULL: u8 = 1;
117
118/// One decoded record from a `KV_UPDATE` payload.
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum KvRecord<'a> {
121    Upsert {
122        key: &'a str,
123        /// BLAKE3-128 of the value bytes.
124        hash: u128,
125        size: u32,
126        mtime_ns: u64,
127        /// Inline value iff `size` ≤ the subscription's `inline_max`;
128        /// `None` = fetch on demand.
129        value: Option<&'a [u8]>,
130    },
131    Delete {
132        key: &'a str,
133    },
134}
135
136/// Append one record to an uncompressed `KV_UPDATE` records buffer.
137pub fn append_kv_record(buf: &mut Vec<u8>, record: &KvRecord<'_>) {
138    let start = buf.len();
139    buf.extend_from_slice(&0u32.to_le_bytes()); // record_len placeholder
140    match record {
141        KvRecord::Upsert {
142            key,
143            hash,
144            size,
145            mtime_ns,
146            value,
147        } => {
148            buf.push(KV_RECORD_UPSERT);
149            let kb = key.as_bytes();
150            buf.extend_from_slice(&(kb.len() as u16).to_le_bytes());
151            buf.extend_from_slice(kb);
152            buf.extend_from_slice(&hash.to_le_bytes());
153            buf.extend_from_slice(&size.to_le_bytes());
154            buf.extend_from_slice(&mtime_ns.to_le_bytes());
155            match value {
156                None => buf.push(KV_CONTENT_NONE),
157                Some(data) => {
158                    buf.push(KV_CONTENT_FULL);
159                    buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
160                    buf.extend_from_slice(data);
161                }
162            }
163        }
164        KvRecord::Delete { key } => {
165            buf.push(KV_RECORD_DELETE);
166            let kb = key.as_bytes();
167            buf.extend_from_slice(&(kb.len() as u16).to_le_bytes());
168            buf.extend_from_slice(kb);
169        }
170    }
171    let len = (buf.len() - start - 4) as u32;
172    buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
173}
174
175/// Iterate records in an uncompressed `KV_UPDATE` payload. Unknown kinds
176/// are skipped via `record_len`; a malformed record ends iteration
177/// (forward-compatible with future record extensions, the fs rule).
178pub struct KvRecordIter<'a> {
179    data: &'a [u8],
180}
181
182pub fn kv_records(data: &[u8]) -> KvRecordIter<'_> {
183    KvRecordIter { data }
184}
185
186fn take_key<'a>(body: &mut &'a [u8]) -> Option<&'a str> {
187    if body.len() < 2 {
188        return None;
189    }
190    let len = u16::from_le_bytes([body[0], body[1]]) as usize;
191    if body.len() < 2 + len {
192        return None;
193    }
194    let s = std::str::from_utf8(&body[2..2 + len]).ok()?;
195    *body = &body[2 + len..];
196    Some(s)
197}
198
199impl<'a> Iterator for KvRecordIter<'a> {
200    type Item = KvRecord<'a>;
201
202    fn next(&mut self) -> Option<KvRecord<'a>> {
203        loop {
204            if self.data.len() < 4 {
205                return None;
206            }
207            let rec_len =
208                u32::from_le_bytes([self.data[0], self.data[1], self.data[2], self.data[3]])
209                    as usize;
210            if self.data.len() < 4 + rec_len || rec_len == 0 {
211                return None;
212            }
213            let mut body = &self.data[4..4 + rec_len];
214            self.data = &self.data[4 + rec_len..];
215            let kind = body[0];
216            body = &body[1..];
217            match kind {
218                KV_RECORD_UPSERT => {
219                    let key = take_key(&mut body)?;
220                    if body.len() < 16 + 4 + 8 + 1 {
221                        return None;
222                    }
223                    let hash = u128::from_le_bytes(body[0..16].try_into().unwrap());
224                    let size = u32::from_le_bytes(body[16..20].try_into().unwrap());
225                    let mtime_ns = u64::from_le_bytes(body[20..28].try_into().unwrap());
226                    let content_kind = body[28];
227                    body = &body[29..];
228                    let value = match content_kind {
229                        KV_CONTENT_NONE => None,
230                        KV_CONTENT_FULL => {
231                            if body.len() < 4 {
232                                return None;
233                            }
234                            let len = u32::from_le_bytes(body[0..4].try_into().unwrap()) as usize;
235                            if body.len() < 4 + len {
236                                return None;
237                            }
238                            Some(&body[4..4 + len])
239                        }
240                        _ => return None,
241                    };
242                    return Some(KvRecord::Upsert {
243                        key,
244                        hash,
245                        size,
246                        mtime_ns,
247                        value,
248                    });
249                }
250                KV_RECORD_DELETE => {
251                    let key = take_key(&mut body)?;
252                    return Some(KvRecord::Delete { key });
253                }
254                _ => continue, // unknown kind: skip via record_len
255            }
256        }
257    }
258}
259
260// ---------------------------------------------------------------------------
261// Message builders and parsers
262// ---------------------------------------------------------------------------
263
264pub fn msg_kv_open(nonce: u16, flags: u8, inline_max: u32, prefix: &str) -> Vec<u8> {
265    let pb = prefix.as_bytes();
266    let mut msg = Vec::with_capacity(10 + pb.len());
267    msg.push(C2S_KV_OPEN);
268    msg.extend_from_slice(&nonce.to_le_bytes());
269    msg.push(flags);
270    msg.extend_from_slice(&inline_max.to_le_bytes());
271    msg.extend_from_slice(&(pb.len() as u16).to_le_bytes());
272    msg.extend_from_slice(pb);
273    msg
274}
275
276/// Parse a `C2S_KV_OPEN` → `(nonce, flags, inline_max, prefix)`.
277pub fn parse_kv_open(msg: &[u8]) -> Option<(u16, u8, u32, String)> {
278    if msg.len() < 10 || msg[0] != C2S_KV_OPEN {
279        return None;
280    }
281    let nonce = u16::from_le_bytes([msg[1], msg[2]]);
282    let flags = msg[3];
283    let inline_max = u32::from_le_bytes(msg[4..8].try_into().unwrap());
284    let prefix_len = u16::from_le_bytes([msg[8], msg[9]]) as usize;
285    let prefix = std::str::from_utf8(msg.get(10..10 + prefix_len)?)
286        .ok()?
287        .to_string();
288    Some((nonce, flags, inline_max, prefix))
289}
290
291pub fn msg_kv_stop(kv_id: u16) -> Vec<u8> {
292    let mut msg = Vec::with_capacity(3);
293    msg.push(C2S_KV_STOP);
294    msg.extend_from_slice(&kv_id.to_le_bytes());
295    msg
296}
297
298/// Parse a `C2S_KV_STOP` → `kv_id`.
299pub fn parse_kv_stop(msg: &[u8]) -> Option<u16> {
300    if msg.len() < 3 || msg[0] != C2S_KV_STOP {
301        return None;
302    }
303    Some(u16::from_le_bytes([msg[1], msg[2]]))
304}
305
306pub fn msg_kv_ack(kv_id: u16, update_id: u32) -> Vec<u8> {
307    let mut msg = Vec::with_capacity(7);
308    msg.push(C2S_KV_ACK);
309    msg.extend_from_slice(&kv_id.to_le_bytes());
310    msg.extend_from_slice(&update_id.to_le_bytes());
311    msg
312}
313
314/// Parse a `C2S_KV_ACK` → `(kv_id, update_id)`.
315pub fn parse_kv_ack(msg: &[u8]) -> Option<(u16, u32)> {
316    if msg.len() < 7 || msg[0] != C2S_KV_ACK {
317        return None;
318    }
319    let kv_id = u16::from_le_bytes([msg[1], msg[2]]);
320    let update_id = u32::from_le_bytes(msg[3..7].try_into().unwrap());
321    Some((kv_id, update_id))
322}
323
324/// A CAS put or delete (`C2S_KV_PUT`).
325#[derive(Clone, Debug, PartialEq, Eq)]
326pub struct KvPut {
327    pub nonce: u16,
328    pub flags: u8,
329    pub base: u128,
330    pub key: String,
331    pub value: Vec<u8>,
332}
333
334pub fn msg_kv_put(p: &KvPut) -> Vec<u8> {
335    let kb = p.key.as_bytes();
336    let compressed = lz4_flex::compress_prepend_size(&p.value);
337    let mut msg = Vec::with_capacity(22 + kb.len() + compressed.len());
338    msg.push(C2S_KV_PUT);
339    msg.extend_from_slice(&p.nonce.to_le_bytes());
340    msg.push(p.flags);
341    msg.extend_from_slice(&p.base.to_le_bytes());
342    msg.extend_from_slice(&(kb.len() as u16).to_le_bytes());
343    msg.extend_from_slice(kb);
344    msg.extend_from_slice(&compressed);
345    msg
346}
347
348/// Parse a `C2S_KV_PUT`. `None` = malformed, a non-UTF-8 key, or a value
349/// whose declared decompressed size exceeds the protocol cap.
350/// The decompressed size a `C2S_KV_PUT` claims for its value, read without
351/// inflating it.
352///
353/// `decompress_size_prepended` allocates the declared size up front, so a
354/// server whose own `value_max` is tighter than [`crate::MAX_DECOMPRESSED`]
355/// can refuse the put before it costs anything. Otherwise rejecting a
356/// 4 MiB-limit violation paid a 64 MiB allocation first — a sixteenfold
357/// amplification available to any client, one message at a time.
358pub fn kv_put_declared_value_len(msg: &[u8]) -> Option<usize> {
359    if msg.len() < 22 || msg[0] != C2S_KV_PUT {
360        return None;
361    }
362    let key_len = u16::from_le_bytes([msg[20], msg[21]]) as usize;
363    let value = msg.get(22 + key_len..)?;
364    let head = value.get(0..4)?;
365    Some(u32::from_le_bytes(head.try_into().unwrap()) as usize)
366}
367
368pub fn parse_kv_put(msg: &[u8]) -> Option<KvPut> {
369    // [nonce:2][flags:1][base:16][key_len:2][key:N][value:LZ4]
370    if msg.len() < 22 || msg[0] != C2S_KV_PUT {
371        return None;
372    }
373    let nonce = u16::from_le_bytes([msg[1], msg[2]]);
374    let flags = msg[3];
375    let base = u128::from_le_bytes(msg[4..20].try_into().unwrap());
376    let key_len = u16::from_le_bytes([msg[20], msg[21]]) as usize;
377    let key = std::str::from_utf8(msg.get(22..22 + key_len)?)
378        .ok()?
379        .to_string();
380    let value = decompress_guarded(&msg[22 + key_len..])?;
381    Some(KvPut {
382        nonce,
383        flags,
384        base,
385        key,
386        value,
387    })
388}
389
390pub fn msg_kv_fetch(nonce: u16, key: &str) -> Vec<u8> {
391    let kb = key.as_bytes();
392    let mut msg = Vec::with_capacity(5 + kb.len());
393    msg.push(C2S_KV_FETCH);
394    msg.extend_from_slice(&nonce.to_le_bytes());
395    msg.extend_from_slice(&(kb.len() as u16).to_le_bytes());
396    msg.extend_from_slice(kb);
397    msg
398}
399
400/// Parse a `C2S_KV_FETCH` → `(nonce, key)`.
401pub fn parse_kv_fetch(msg: &[u8]) -> Option<(u16, String)> {
402    if msg.len() < 5 || msg[0] != C2S_KV_FETCH {
403        return None;
404    }
405    let nonce = u16::from_le_bytes([msg[1], msg[2]]);
406    let key_len = u16::from_le_bytes([msg[3], msg[4]]) as usize;
407    let key = std::str::from_utf8(msg.get(5..5 + key_len)?)
408        .ok()?
409        .to_string();
410    Some((nonce, key))
411}
412
413pub fn msg_kv_opened(nonce: u16, kv_id: u16, status: u8, detail: &str) -> Vec<u8> {
414    let db = detail.as_bytes();
415    let mut msg = Vec::with_capacity(8 + db.len());
416    msg.push(S2C_KV_OPENED);
417    msg.extend_from_slice(&nonce.to_le_bytes());
418    msg.extend_from_slice(&kv_id.to_le_bytes());
419    msg.push(status);
420    msg.extend_from_slice(&(db.len() as u16).to_le_bytes());
421    msg.extend_from_slice(db);
422    msg
423}
424
425/// Parse an `S2C_KV_OPENED` → `(nonce, kv_id, status, detail)`.
426pub fn parse_kv_opened(msg: &[u8]) -> Option<(u16, u16, u8, String)> {
427    if msg.len() < 8 || msg[0] != S2C_KV_OPENED {
428        return None;
429    }
430    let nonce = u16::from_le_bytes([msg[1], msg[2]]);
431    let kv_id = u16::from_le_bytes([msg[3], msg[4]]);
432    let status = msg[5];
433    let detail_len = u16::from_le_bytes([msg[6], msg[7]]) as usize;
434    let detail = String::from_utf8_lossy(msg.get(8..8 + detail_len)?).into_owned();
435    Some((nonce, kv_id, status, detail))
436}
437
438/// Build a `KV_UPDATE` from an uncompressed records buffer.
439pub fn msg_kv_update(kv_id: u16, update_id: u32, flags: u8, records: &[u8]) -> Vec<u8> {
440    let compressed = lz4_flex::compress_prepend_size(records);
441    let mut msg = Vec::with_capacity(8 + compressed.len());
442    msg.push(S2C_KV_UPDATE);
443    msg.extend_from_slice(&kv_id.to_le_bytes());
444    msg.extend_from_slice(&update_id.to_le_bytes());
445    msg.push(flags);
446    msg.extend_from_slice(&compressed);
447    msg
448}
449
450/// Build an `S2C_KV_DONE`. On success `hash` is the new value hash (zero
451/// for a delete); on `CONFLICT` the current hash.
452pub fn msg_kv_done(nonce: u16, status: u8, hash: u128, mtime_ns: u64) -> Vec<u8> {
453    let mut msg = Vec::with_capacity(28);
454    msg.push(S2C_KV_DONE);
455    msg.extend_from_slice(&nonce.to_le_bytes());
456    msg.push(status);
457    msg.extend_from_slice(&hash.to_le_bytes());
458    msg.extend_from_slice(&mtime_ns.to_le_bytes());
459    msg
460}
461
462/// Parse an `S2C_KV_DONE` → `(nonce, status, hash, mtime_ns)`.
463pub fn parse_kv_done(msg: &[u8]) -> Option<(u16, u8, u128, u64)> {
464    if msg.len() < 28 || msg[0] != S2C_KV_DONE {
465        return None;
466    }
467    let nonce = u16::from_le_bytes([msg[1], msg[2]]);
468    let status = msg[3];
469    let hash = u128::from_le_bytes(msg[4..20].try_into().unwrap());
470    let mtime_ns = u64::from_le_bytes(msg[20..28].try_into().unwrap());
471    Some((nonce, status, hash, mtime_ns))
472}
473
474pub fn msg_kv_value(nonce: u16, status: u8, hash: u128, data: &[u8]) -> Vec<u8> {
475    let compressed = lz4_flex::compress_prepend_size(data);
476    let mut msg = Vec::with_capacity(20 + compressed.len());
477    msg.push(S2C_KV_VALUE);
478    msg.extend_from_slice(&nonce.to_le_bytes());
479    msg.push(status);
480    msg.extend_from_slice(&hash.to_le_bytes());
481    msg.extend_from_slice(&compressed);
482    msg
483}
484
485/// Parse an `S2C_KV_VALUE` → `(nonce, status, hash, data)`.
486pub fn parse_kv_value(msg: &[u8]) -> Option<(u16, u8, u128, Vec<u8>)> {
487    if msg.len() < 20 || msg[0] != S2C_KV_VALUE {
488        return None;
489    }
490    let nonce = u16::from_le_bytes([msg[1], msg[2]]);
491    let status = msg[3];
492    let hash = u128::from_le_bytes(msg[4..20].try_into().unwrap());
493    let data = decompress_guarded(&msg[20..])?;
494    Some((nonce, status, hash, data))
495}
496
497pub fn msg_kv_closed(kv_id: u16, reason: u8) -> Vec<u8> {
498    let mut msg = Vec::with_capacity(4);
499    msg.push(S2C_KV_CLOSED);
500    msg.extend_from_slice(&kv_id.to_le_bytes());
501    msg.push(reason);
502    msg
503}
504
505/// Parse an `S2C_KV_CLOSED` → `(kv_id, reason)`.
506pub fn parse_kv_closed(msg: &[u8]) -> Option<(u16, u8)> {
507    if msg.len() < 4 || msg[0] != S2C_KV_CLOSED {
508        return None;
509    }
510    Some((u16::from_le_bytes([msg[1], msg[2]]), msg[3]))
511}
512
513/// Decompress a `compress_prepend_size` payload, refusing declared sizes
514/// over the protocol-wide [`crate::MAX_DECOMPRESSED`] cap.
515fn decompress_guarded(data: &[u8]) -> Option<Vec<u8>> {
516    if data.len() < 4 {
517        return None;
518    }
519    let declared = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
520    if declared > crate::MAX_DECOMPRESSED {
521        return None;
522    }
523    lz4_flex::decompress_size_prepended(data).ok()
524}
525
526// ---------------------------------------------------------------------------
527// Client-side reducer
528// ---------------------------------------------------------------------------
529
530/// One mirrored entry.
531#[derive(Clone, Debug, PartialEq, Eq)]
532pub struct KvEntry {
533    /// BLAKE3-128 of the value bytes.
534    pub hash: u128,
535    pub size: u32,
536    pub mtime_ns: u64,
537    /// Present iff the value arrived inline (`size` ≤ the subscription's
538    /// `inline_max`); `None` = fetch on demand.
539    pub value: Option<Vec<u8>>,
540}
541
542/// The complete watcher obligation: apply updates, read `live`.
543///
544/// One mirror per subscription; a re-established connection means a new
545/// `KV_OPEN`, a new `kv_id`, and a fresh mirror (nothing survives, the
546/// fs-family rule).
547#[derive(Debug, Default)]
548pub struct KvMirror {
549    pub live: BTreeMap<String, KvEntry>,
550    /// True once the initial snapshot is complete (`KV_UPDATE_SNAPSHOT_END`).
551    pub snapshot_done: bool,
552}
553
554impl KvMirror {
555    pub fn new() -> Self {
556        Self::default()
557    }
558
559    /// Apply one `KV_UPDATE` message (starting at the opcode byte).
560    /// Returns `Some(update_id)` to acknowledge, `None` if malformed.
561    pub fn apply_update(&mut self, msg: &[u8]) -> Option<u32> {
562        if msg.len() < 8 || msg[0] != S2C_KV_UPDATE {
563            return None;
564        }
565        let update_id = u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]);
566        let flags = msg[7];
567        let records = decompress_guarded(&msg[8..])?;
568        for record in kv_records(&records) {
569            match record {
570                KvRecord::Upsert {
571                    key,
572                    hash,
573                    size,
574                    mtime_ns,
575                    value,
576                } => {
577                    self.live.insert(
578                        key.to_string(),
579                        KvEntry {
580                            hash,
581                            size,
582                            mtime_ns,
583                            value: value.map(|v| v.to_vec()),
584                        },
585                    );
586                }
587                KvRecord::Delete { key } => {
588                    self.live.remove(key);
589                }
590            }
591        }
592        if flags & KV_UPDATE_SNAPSHOT_END != 0 {
593            self.snapshot_done = true;
594        }
595        Some(update_id)
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602
603    #[test]
604    fn kv_open_roundtrip_and_bytes() {
605        let m = msg_kv_open(7, 0, 4096, "editor/");
606        // Lock the byte layout: [0x70][nonce:2][flags:1][inline_max:4][prefix_len:2][prefix]
607        assert_eq!(
608            m,
609            vec![
610                0x70, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x07, 0x00, 0x65, 0x64, 0x69, 0x74,
611                0x6F, 0x72, 0x2F
612            ]
613        );
614        let (nonce, flags, inline_max, prefix) = parse_kv_open(&m).unwrap();
615        assert_eq!(nonce, 7);
616        assert_eq!(flags, 0);
617        assert_eq!(inline_max, 4096);
618        assert_eq!(prefix, "editor/");
619    }
620
621    #[test]
622    fn kv_stop_ack_roundtrip() {
623        assert_eq!(parse_kv_stop(&msg_kv_stop(3)), Some(3));
624        assert_eq!(parse_kv_ack(&msg_kv_ack(3, 99)), Some((3, 99)));
625    }
626
627    #[test]
628    fn kv_put_roundtrip() {
629        let p = KvPut {
630            nonce: 21,
631            flags: KV_PUT_DURABLE,
632            base: 0xDEAD_BEEF,
633            key: "roots".to_string(),
634            value: b"main = /src/blit\n".to_vec(),
635        };
636        let out = parse_kv_put(&msg_kv_put(&p)).unwrap();
637        assert_eq!(out, p);
638    }
639
640    #[test]
641    fn kv_put_delete_empty_value() {
642        let p = KvPut {
643            nonce: 1,
644            flags: KV_PUT_DELETE,
645            base: 42,
646            key: "editor/buf//tmp/x".to_string(),
647            value: Vec::new(),
648        };
649        let out = parse_kv_put(&msg_kv_put(&p)).unwrap();
650        assert_eq!(out, p);
651    }
652
653    #[test]
654    fn kv_fetch_roundtrip() {
655        let m = msg_kv_fetch(5, "editor/open//x/y.rs");
656        assert_eq!(parse_kv_fetch(&m), Some((5, "editor/open//x/y.rs".into())));
657    }
658
659    #[test]
660    fn kv_opened_roundtrip() {
661        let m = msg_kv_opened(9, 2, KV_STATUS_OK, "");
662        assert_eq!(parse_kv_opened(&m), Some((9, 2, KV_STATUS_OK, "".into())));
663        let m = msg_kv_opened(9, KV_ID_INVALID, KV_STATUS_PERMISSION, "kv disabled");
664        assert_eq!(
665            parse_kv_opened(&m),
666            Some((9, KV_ID_INVALID, KV_STATUS_PERMISSION, "kv disabled".into()))
667        );
668    }
669
670    #[test]
671    fn kv_closed_roundtrip_and_bytes() {
672        let m = msg_kv_closed(3, KV_CLOSED_RESOURCE_LIMIT);
673        // Lock the byte layout: [0x74][kv_id:2][reason:1]
674        assert_eq!(m, vec![0x74, 0x03, 0x00, 0x04]);
675        assert_eq!(parse_kv_closed(&m), Some((3, KV_CLOSED_RESOURCE_LIMIT)));
676        assert_eq!(parse_kv_closed(&m[..3]), None);
677        assert_eq!(parse_kv_closed(&msg_kv_stop(3)), None);
678    }
679
680    #[test]
681    fn kv_done_value_roundtrip() {
682        let m = msg_kv_done(4, KV_STATUS_CONFLICT, 77, 123_456);
683        assert_eq!(
684            parse_kv_done(&m),
685            Some((4, KV_STATUS_CONFLICT, 77, 123_456))
686        );
687        let m = msg_kv_value(6, KV_STATUS_OK, 88, b"payload");
688        assert_eq!(
689            parse_kv_value(&m),
690            Some((6, KV_STATUS_OK, 88, b"payload".to_vec()))
691        );
692    }
693
694    #[test]
695    fn record_roundtrip_and_mirror() {
696        let mut buf = Vec::new();
697        append_kv_record(
698            &mut buf,
699            &KvRecord::Upsert {
700                key: "editor/open//a.rs",
701                hash: 11,
702                size: 2,
703                mtime_ns: 5,
704                value: Some(b"{}"),
705            },
706        );
707        append_kv_record(
708            &mut buf,
709            &KvRecord::Upsert {
710                key: "editor/buf//a.rs",
711                hash: 12,
712                size: 9_999_999,
713                mtime_ns: 6,
714                value: None, // over inline_max: metadata only
715            },
716        );
717        append_kv_record(&mut buf, &KvRecord::Delete { key: "roots" });
718        let records: Vec<_> = kv_records(&buf).collect();
719        assert_eq!(records.len(), 3);
720        assert_eq!(
721            records[0],
722            KvRecord::Upsert {
723                key: "editor/open//a.rs",
724                hash: 11,
725                size: 2,
726                mtime_ns: 5,
727                value: Some(b"{}"),
728            }
729        );
730
731        let mut mirror = KvMirror::new();
732        let msg = msg_kv_update(1, 10, KV_UPDATE_SNAPSHOT_END, &buf);
733        assert_eq!(mirror.apply_update(&msg), Some(10));
734        assert!(mirror.snapshot_done);
735        assert_eq!(mirror.live.len(), 2);
736        assert_eq!(
737            mirror.live.get("editor/open//a.rs").unwrap().value,
738            Some(b"{}".to_vec())
739        );
740        assert_eq!(mirror.live.get("editor/buf//a.rs").unwrap().value, None);
741
742        // A live delete removes the entry.
743        let mut buf2 = Vec::new();
744        append_kv_record(
745            &mut buf2,
746            &KvRecord::Delete {
747                key: "editor/open//a.rs",
748            },
749        );
750        let msg2 = msg_kv_update(1, 11, 0, &buf2);
751        assert_eq!(mirror.apply_update(&msg2), Some(11));
752        assert_eq!(mirror.live.len(), 1);
753    }
754
755    #[test]
756    fn unknown_record_kind_skipped() {
757        let mut buf = Vec::new();
758        // A future record kind (0x7F) with a 3-byte body.
759        buf.extend_from_slice(&4u32.to_le_bytes());
760        buf.push(0x7F);
761        buf.extend_from_slice(&[1, 2, 3]);
762        append_kv_record(&mut buf, &KvRecord::Delete { key: "k" });
763        let records: Vec<_> = kv_records(&buf).collect();
764        assert_eq!(records, vec![KvRecord::Delete { key: "k" }]);
765    }
766
767    #[test]
768    fn key_validity() {
769        assert!(kv_key_valid("roots"));
770        assert!(kv_key_valid("editor/buf//x/y.rs"));
771        assert!(!kv_key_valid(""));
772        assert!(!kv_key_valid(&"k".repeat(KV_MAX_KEY + 1)));
773        assert!(!kv_key_valid("a\0b"));
774    }
775}