Skip to main content

kevy_persist/
lib.rs

1//! kevy-persist — durability for a [`kevy_store::Store`].
2//!
3//! Two mechanisms, both zero-dependency pure Rust over `std::fs`:
4//!
5//! - **Snapshot (RDB-style):** [`save_snapshot`] dumps a whole store to a temp
6//!   file then atomically renames it (fsync before rename); [`load_snapshot`]
7//!   restores it. A compact, type-tagged binary format.
8//! - **AOF:** an [`Aof`] append-only command log with a configurable fsync
9//!   policy; [`replay_aof`] re-applies it on startup, tolerating a truncated
10//!   trailing frame from a crash mid-write.
11//!
12//! In a shared-nothing runtime each shard persists its own store to its own
13//! file, so there is no cross-core coordination. Part of the [kevy] server.
14//!
15//! [kevy]: https://crates.io/crates/kevy
16//!
17//! # Example (AOF)
18//!
19//! ```
20//! use kevy_persist::{Aof, Argv, Fsync, replay_aof};
21//!
22//! # fn main() -> std::io::Result<()> {
23//! let path = std::env::temp_dir().join("kevy-persist-doctest.aof");
24//! # let _ = std::fs::remove_file(&path);
25//! {
26//!     let mut aof = Aof::open(&path, Fsync::No)?;
27//!     aof.append(&Argv::from(vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]))?;
28//! } // flushed on drop
29//!
30//! let mut replayed: Vec<Argv> = Vec::new();
31//! replay_aof(&path, |args| replayed.push(args))?;
32//! assert_eq!(replayed, vec![vec![b"SET".to_vec(), b"k".to_vec(), b"v".to_vec()]]);
33//! # std::fs::remove_file(&path).ok();
34//! # Ok(())
35//! # }
36//! ```
37#![forbid(unsafe_code)]
38
39mod aof;
40pub mod feed_meta;
41pub mod layout;
42mod replay;
43pub mod reshard;
44mod rewrite_fmt;
45mod shards_meta;
46mod snapshot_payload;
47
48pub use aof::{Aof, Fsync, RewritePlan, RewriteStats, write_aof_base};
49pub use replay::replay_aof;
50pub use shards_meta::{Routing, ShardsMeta, read_shards_meta, write_shards_meta};
51pub use kevy_resp::{Argv, ArgvView};
52pub use rewrite_fmt::dump_aof;
53pub(crate) use rewrite_fmt::{dump_store_to_buf, estimate_multibulk_bytes, write_multibulk};
54use kevy_store::Store;
55use kevy_store::Value;
56// ZSet snapshot iterates ordered (member, score) pairs via `Value::ZSet`.
57use std::fs::File;
58use std::io::{self, BufReader, BufWriter, Read, Write};
59use std::path::Path;
60
61/// File magic + format version. Bump `VERSION` on any layout change.
62///
63/// v2 stored each entry's TTL as **remaining millis** (relative), so a load
64/// re-anchored the deadline to load-time — a restart reset every key to a
65/// fresh full TTL (INC-2026-06-09). v3 stores the **absolute** Unix-ms
66/// deadline, so a load reconstructs the original instant. v4 appends a
67/// consumer-group section to each `OP_STREAM` payload (groups + consumers
68/// plus PEL) — before that, SAVE/reshard silently dropped group state. The
69/// loader still accepts v2 (relative TTL) and v3 (no group section).
70const MAGIC: &[u8; 8] = b"KEVYSNAP";
71const VERSION: u8 = 4;
72/// v2.3: version 5 carries a 16-byte feed cursor (`gen u64 LE` +
73/// `offset u64 LE`) right after the version byte — the snapshot half
74/// of the recovery-point contract (docs/cdc.md): snapshot S + feed
75/// frames from S's cursor = exact restore. Writers emit v5 only when
76/// a cursor is supplied; cursor-less writes stay at v4 so every
77/// existing path is byte-identical.
78const VERSION_FEED_CURSOR: u8 = 5;
79/// v2.4: version 6 additionally carries `OP_HFTTL` hash field-TTL
80/// records after the entry stream. Written only when field TTLs
81/// exist; the header still carries the (possibly zero) feed cursor.
82const VERSION_HASH_TTL: u8 = 6;
83const VERSION_RELATIVE_TTL: u8 = 2;
84const VERSION_ABSOLUTE_TTL: u8 = 3;
85
86// Record opcodes (one per value type). Each record is:
87//   [op][ttl: u8 flag + optional u64][key][type payload]
88const OP_EOF: u8 = 0;
89const OP_STR: u8 = 1;
90const OP_HASH: u8 = 2;
91const OP_LIST: u8 = 3;
92const OP_SET: u8 = 4;
93const OP_ZSET: u8 = 5;
94const OP_STREAM: u8 = 6;
95/// v2.4 hash field TTL record: `[key][field][deadline_ms: u64 LE]`.
96/// Appears only in format v6+ snapshots, after the entry stream's
97/// records (before OP_EOF).
98const OP_HFTTL: u8 = 7;
99
100/// BufWriter capacity for bulk snapshot / AOF-rewrite writes. The 8 KiB
101/// default made SAVE ~12 % of disk bandwidth (tens of thousands of small
102/// `write(2)`s); 1 MiB amortizes the syscalls toward disk speed.
103pub(crate) const SNAPSHOT_BUF_CAP: usize = 1 << 20;
104
105/// Anything that can enumerate `(key, &Value, ttl_ms)` triples for
106/// serialization: a live [`Store`] (its `snapshot_each`, the synchronous
107/// paths) or a frozen [`kevy_store::SnapshotView`] (the COW paths — collect
108/// on the owning thread, serialize on a background one).
109pub trait SnapshotSource {
110    /// Visit every live entry as `(key, &value, remaining_ttl_ms)`.
111    fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>));
112
113    /// v2.4: visit every live hash field TTL as `(key, field,
114    /// absolute_unix_ms)`. Default = none (sources predating the
115    /// feature).
116    fn for_each_hash_ttl(&self, _f: impl FnMut(&[u8], &[u8], u64)) {}
117}
118
119impl SnapshotSource for Store {
120    fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>)) {
121        self.snapshot_each(f);
122    }
123    fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
124        self.hash_ttl_each(f);
125    }
126}
127
128impl SnapshotSource for kevy_store::SnapshotView {
129    fn for_each_entry(&self, f: impl FnMut(&[u8], &Value, Option<u64>)) {
130        self.each(f);
131    }
132    fn for_each_hash_ttl(&self, f: impl FnMut(&[u8], &[u8], u64)) {
133        self.each_hash_ttl(f);
134    }
135}
136
137/// Write a point-in-time snapshot of `src` (a live [`Store`] or a frozen
138/// [`kevy_store::SnapshotView`]) to `path`, atomically: data is written to
139/// `<path>.tmp`, fsynced, then renamed over `path`.
140pub fn save_snapshot<S: SnapshotSource>(src: &S, path: &Path) -> io::Result<()> {
141    let tmp = write_snapshot_tmp(src, path)?;
142    std::fs::rename(&tmp, path)
143}
144
145/// Serialize a point-in-time snapshot of `src` into any `Write` sink.
146/// Used by both [`write_snapshot_tmp`] (sink = `BufWriter<File>` +
147/// extra fsync after) and the v3-cluster replication path (sink =
148/// `&mut Vec<u8>` for in-memory snapshot ship, see
149/// `kevy-replicate/docs/snapshot.md`).
150///
151/// On-disk bytes are identical regardless of sink — the same magic +
152/// version header, same entry stream, same `OP_EOF` trailer. Callers
153/// that need durability (disk) wrap in `BufWriter<File>` and call
154/// `sync_all` themselves; callers that need bytes (network ship)
155/// pass a `Vec<u8>`.
156pub fn write_snapshot_to<S: SnapshotSource, W: Write>(src: &S, sink: &mut W) -> io::Result<()> {
157    write_snapshot_to_with_cursor(src, sink, None)
158}
159
160/// [`write_snapshot_to`] with the v2.3 recovery-point header: when
161/// `cursor = Some((generation, offset))` the snapshot records the feed
162/// position it was taken at (format v5); `None` writes the legacy v4
163/// stream unchanged.
164pub fn write_snapshot_to_with_cursor<S: SnapshotSource, W: Write>(
165    src: &S,
166    sink: &mut W,
167    cursor: Option<(u64, u64)>,
168) -> io::Result<()> {
169    // v2.4: field-TTL records force format v6; collect them first so
170    // the header version is known before anything is written.
171    let mut fttl: Vec<(Vec<u8>, Vec<u8>, u64)> = Vec::new();
172    src.for_each_hash_ttl(|k, f, d| fttl.push((k.to_vec(), f.to_vec(), d)));
173    let mut w = BufWriter::with_capacity(SNAPSHOT_BUF_CAP, sink);
174    w.write_all(MAGIC)?;
175    let version = if !fttl.is_empty() {
176        VERSION_HASH_TTL
177    } else if cursor.is_some() {
178        VERSION_FEED_CURSOR
179    } else {
180        VERSION
181    };
182    w.write_all(&[version])?;
183    if version >= VERSION_FEED_CURSOR {
184        let (generation, offset) = cursor.unwrap_or((0, 0));
185        w.write_all(&generation.to_le_bytes())?;
186        w.write_all(&offset.to_le_bytes())?;
187    }
188    // The source yields *remaining* ms; v3 persists the absolute
189    // Unix-ms deadline (now + remaining) so the TTL survives a restart.
190    let now = kevy_store::now_unix_ms();
191    // Enumeration is infallible; capture the first write error to surface.
192    let mut err: Option<io::Error> = None;
193    src.for_each_entry(|key, value, ttl| {
194        let deadline = ttl.map(|ms| now.saturating_add(ms));
195        if err.is_none()
196            && let Err(e) = write_entry(&mut w, key, value, deadline)
197        {
198            err = Some(e);
199        }
200    });
201    if let Some(e) = err {
202        return Err(e);
203    }
204    for (k, f, d) in &fttl {
205        w.write_all(&[OP_HFTTL])?;
206        write_bytes(&mut w, k)?;
207        write_bytes(&mut w, f)?;
208        w.write_all(&d.to_le_bytes())?;
209    }
210    w.write_all(&[OP_EOF])?;
211    w.flush()?;
212    Ok(())
213}
214
215/// The write half of [`save_snapshot`]: produce the durable (fsynced)
216/// `<path>.tmp` and return its path **without** the final rename. For the
217/// COW background-save flow: the serializer thread writes the temp file at
218/// leisure, then the store-owning thread renames it in the same critical
219/// section that resets the AOF — keeping the snapshot/AOF commit adjacent
220/// instead of seconds apart.
221pub fn write_snapshot_tmp<S: SnapshotSource>(src: &S, path: &Path) -> io::Result<std::path::PathBuf> {
222    let tmp = tmp_path(path);
223    {
224        let mut file = File::create(&tmp)?;
225        write_snapshot_to(src, &mut file)?;
226        file.sync_all()?; // durably on disk before the rename
227    }
228    Ok(tmp)
229}
230
231/// Read the v2.3 recovery-point cursor from a snapshot's header:
232/// `Some((generation, offset))` for format v5+, `None` for older
233/// (cursor-less) snapshots. Does not load entries.
234pub fn read_snapshot_cursor(path: &Path) -> io::Result<Option<(u64, u64)>> {
235    let mut r = BufReader::new(File::open(path)?);
236    let mut magic = [0u8; 8];
237    r.read_exact(&mut magic)?;
238    if &magic != MAGIC {
239        return Err(io::Error::new(io::ErrorKind::InvalidData, "kevy snapshot: bad magic"));
240    }
241    let version = read_u8(&mut r)?;
242    if version < VERSION_FEED_CURSOR {
243        return Ok(None);
244    }
245    let mut cur = [0u8; 16];
246    r.read_exact(&mut cur)?;
247    let generation = u64::from_le_bytes(cur[..8].try_into().expect("8 bytes"));
248    let offset = u64::from_le_bytes(cur[8..].try_into().expect("8 bytes"));
249    Ok(Some((generation, offset)))
250}
251
252/// Load a snapshot from `path` into `store` (entries are inserted, not cleared
253/// first — call on a fresh store). Errors on a bad magic/version or truncation.
254pub fn load_snapshot(store: &mut Store, path: &Path) -> io::Result<()> {
255    let r = BufReader::new(File::open(path)?);
256    load_snapshot_from(store, r)
257}
258
259/// Load a snapshot from any [`std::io::Read`] sink into `store` —
260/// symmetric to [`write_snapshot_to`]. Used by the v3-cluster
261/// replication path (sink = `&[u8]` wrapped in `std::io::Cursor`) to
262/// apply a primary-shipped snapshot to a fresh local store without
263/// touching disk. Entries are inserted, not cleared first — call on
264/// a fresh store. Errors on bad magic/version or truncation.
265pub fn load_snapshot_from<R: Read>(store: &mut Store, r: R) -> io::Result<()> {
266    load_snapshot_filtered(store, r, |_| true)
267}
268
269/// v3.2 — [`load_snapshot_from`] with a key predicate: only records
270/// whose key satisfies `keep` are loaded (skipped records are still
271/// parsed to stay in frame). The single-source replica path broadcasts
272/// one snapshot payload to every shard and each loads its own hash
273/// slice — no intermediate store, no re-serialization.
274pub fn load_snapshot_filtered<R: Read>(
275    store: &mut Store,
276    mut r: R,
277    keep: impl Fn(&[u8]) -> bool,
278) -> io::Result<()> {
279    let mut magic = [0u8; 8];
280    r.read_exact(&mut magic)?;
281    if &magic != MAGIC {
282        return Err(io::Error::new(
283            io::ErrorKind::InvalidData,
284            "kevy snapshot: bad magic",
285        ));
286    }
287    let version = read_u8(&mut r)?;
288    if !(VERSION_RELATIVE_TTL..=VERSION_HASH_TTL).contains(&version) {
289        return Err(io::Error::new(
290            io::ErrorKind::InvalidData,
291            "kevy snapshot: bad version",
292        ));
293    }
294    if version >= VERSION_FEED_CURSOR {
295        // Loader-side the cursor is advisory (read via
296        // [`read_snapshot_cursor`] by restore tooling); skip it here.
297        let mut cur = [0u8; 16];
298        r.read_exact(&mut cur)?;
299    }
300    // v3+ stores absolute Unix-ms deadlines; convert each to remaining ms
301    // against one `now` read so the load is internally consistent. A deadline
302    // already past becomes `Some(0)` → loaded then immediately reaped (lazy
303    // get / active reaper), matching "expired key is gone". v2 ttls are
304    // already remaining, so pass them through.
305    let absolute_ttl = version >= VERSION_ABSOLUTE_TTL;
306    let now = kevy_store::now_unix_ms();
307
308    loop {
309        let op = read_u8(&mut r)?;
310        if op == OP_EOF {
311            return Ok(());
312        }
313        // v2.4 field-TTL records: no ttl/value framing of their own.
314        if op == OP_HFTTL {
315            let key = read_bytes(&mut r)?;
316            let field = read_bytes(&mut r)?;
317            let mut d = [0u8; 8];
318            r.read_exact(&mut d)?;
319            if keep(&key) {
320                store.load_hash_field_ttl(&key, &field, u64::from_le_bytes(d));
321            }
322            continue;
323        }
324        let raw_ttl = read_ttl(&mut r)?;
325        let ttl = if absolute_ttl {
326            raw_ttl.map(|deadline| deadline.saturating_sub(now))
327        } else {
328            raw_ttl
329        };
330        let key = read_bytes(&mut r)?;
331        match op {
332            OP_STR => {
333                let val = read_bytes(&mut r)?;
334                if keep(&key) {
335                    store.load_str(key, val, ttl);
336                }
337            }
338            OP_HASH => {
339                let n = read_u32(&mut r)? as usize;
340                let mut fields = Vec::with_capacity(n);
341                for _ in 0..n {
342                    let f = read_bytes(&mut r)?;
343                    let v = read_bytes(&mut r)?;
344                    fields.push((f, v));
345                }
346                if keep(&key) {
347                    store.load_hash(key, fields, ttl);
348                }
349            }
350            OP_LIST => {
351                let n = read_u32(&mut r)? as usize;
352                let mut items = Vec::with_capacity(n);
353                for _ in 0..n {
354                    items.push(read_bytes(&mut r)?);
355                }
356                if keep(&key) {
357                    store.load_list(key, items, ttl);
358                }
359            }
360            OP_SET => {
361                let n = read_u32(&mut r)? as usize;
362                let mut members = Vec::with_capacity(n);
363                for _ in 0..n {
364                    members.push(read_bytes(&mut r)?);
365                }
366                if keep(&key) {
367                    store.load_set(key, members, ttl);
368                }
369            }
370            OP_ZSET => {
371                let n = read_u32(&mut r)? as usize;
372                let mut pairs = Vec::with_capacity(n);
373                for _ in 0..n {
374                    let m = read_bytes(&mut r)?;
375                    let score = f64::from_bits(read_u64(&mut r)?);
376                    pairs.push((m, score));
377                }
378                if keep(&key) {
379                    store.load_zset(key, pairs, ttl);
380                }
381            }
382            OP_STREAM => {
383                let last_ms = read_u64(&mut r)?;
384                let last_seq = read_u64(&mut r)?;
385                let mxd_ms = read_u64(&mut r)?;
386                let mxd_seq = read_u64(&mut r)?;
387                let entries_added = read_u64(&mut r)?;
388                let n = read_u32(&mut r)? as usize;
389                let mut entries = Vec::with_capacity(n);
390                for _ in 0..n {
391                    let ms = read_u64(&mut r)?;
392                    let seq = read_u64(&mut r)?;
393                    let nf = read_u32(&mut r)? as usize;
394                    let mut fv = Vec::with_capacity(nf);
395                    for _ in 0..nf {
396                        let f = read_bytes(&mut r)?;
397                        let v = read_bytes(&mut r)?;
398                        fv.push((f, v));
399                    }
400                    entries.push((ms, seq, fv));
401                }
402                // v4 appends the consumer-group section; v2/v3 files
403                // predate groups-in-snapshot, so they load with none.
404                let groups = if version >= VERSION {
405                    read_stream_groups(&mut r)?
406                } else {
407                    Vec::new()
408                };
409                if keep(&key) {
410                    store.load_stream(
411                        key,
412                        entries,
413                        (last_ms, last_seq),
414                        (mxd_ms, mxd_seq),
415                        entries_added,
416                        groups,
417                        ttl,
418                    );
419                }
420            }
421            other => {
422                return Err(io::Error::new(
423                    io::ErrorKind::InvalidData,
424                    format!("kevy snapshot: unknown opcode {other}"),
425                ));
426            }
427        }
428    }
429}
430
431/// Serialize one entry: `[op][ttl][key][payload]`.
432fn write_entry<W: Write>(w: &mut W, key: &[u8], value: &Value, ttl: Option<u64>) -> io::Result<()> {
433    let op = match value {
434        Value::Str(_) | Value::Int(_) | Value::ArcBulk(_) => OP_STR, // L1/L2: all reuse OP_STR.
435
436        Value::Hash(_) | Value::SmallHashInline(_) => OP_HASH,
437        Value::List(_) | Value::SmallListInline(_) => OP_LIST,
438        // A.7 O5: both Set encodings share the OP_SET wire format —
439        // payload is `[len: u32 LE][bulk: len-prefixed bytes]*`, agnostic
440        // of whether the in-memory representation is `SmallSetInline` or
441        // `Arc<KevySet>`.
442        Value::Set(_) | Value::SmallSetInline(_) => OP_SET,
443        Value::ZSet(_) | Value::SmallZSetInline(_) => OP_ZSET,
444        Value::Stream(_) => OP_STREAM,
445    };
446    w.write_all(&[op])?;
447    write_ttl(w, ttl)?;
448    write_bytes(w, key)?;
449    match value {
450        Value::Str(v) => write_bytes(w, v.as_slice()),
451        Value::Int(n) => write_bytes(w, n.to_string().as_bytes()),
452        Value::ArcBulk(a) => write_bytes(w, a.as_ref()),
453        Value::Hash(h) => snapshot_payload::write_hash_payload(w, h),
454        Value::SmallHashInline(h) => snapshot_payload::write_small_hash_payload(w, h),
455        Value::List(l) => snapshot_payload::write_list_payload(w, l),
456        Value::SmallListInline(l) => snapshot_payload::write_small_list_payload(w, l),
457        Value::Set(set) => snapshot_payload::write_set_payload(w, set),
458        Value::SmallSetInline(s) => snapshot_payload::write_small_set_payload(w, s),
459        Value::ZSet(z) => snapshot_payload::write_zset_payload(w, z),
460        Value::SmallZSetInline(z) => snapshot_payload::write_small_zset_payload(w, z),
461        Value::Stream(s) => snapshot_payload::write_stream_payload(w, s),
462    }
463}
464
465/// v4 consumer-group section: `[n_groups][per group: name, last_delivered,
466/// consumers (name + last_seen_ms), PEL rows]`. Tombstone PEL rows are kept
467/// — the snapshot path is the full-fidelity one (the AOF rewrite can't
468/// re-create them via XCLAIM, see `rewrite_fmt`).
469pub(crate) fn write_stream_groups<W: Write>(w: &mut W, groups: &[kevy_store::LoadedGroup]) -> io::Result<()> {
470    w.write_all(&(groups.len() as u32).to_le_bytes())?;
471    for g in groups {
472        write_bytes(w, &g.name)?;
473        w.write_all(&g.last_delivered.0.to_le_bytes())?;
474        w.write_all(&g.last_delivered.1.to_le_bytes())?;
475        w.write_all(&(g.consumers.len() as u32).to_le_bytes())?;
476        for (name, last_seen_ms) in &g.consumers {
477            write_bytes(w, name)?;
478            w.write_all(&last_seen_ms.to_le_bytes())?;
479        }
480        w.write_all(&(g.pel.len() as u32).to_le_bytes())?;
481        for (ms, seq, consumer, delivery_time_ms, delivery_count) in &g.pel {
482            w.write_all(&ms.to_le_bytes())?;
483            w.write_all(&seq.to_le_bytes())?;
484            write_bytes(w, consumer)?;
485            w.write_all(&delivery_time_ms.to_le_bytes())?;
486            w.write_all(&delivery_count.to_le_bytes())?;
487        }
488    }
489    Ok(())
490}
491
492/// Loader-side twin of [`write_stream_groups`].
493fn read_stream_groups<R: Read>(r: &mut R) -> io::Result<Vec<kevy_store::LoadedGroup>> {
494    let n = read_u32(r)? as usize;
495    let mut groups = Vec::with_capacity(n);
496    for _ in 0..n {
497        let name = read_bytes(r)?;
498        let last_delivered = (read_u64(r)?, read_u64(r)?);
499        let nc = read_u32(r)? as usize;
500        let mut consumers = Vec::with_capacity(nc);
501        for _ in 0..nc {
502            let cname = read_bytes(r)?;
503            consumers.push((cname, read_u64(r)?));
504        }
505        let np = read_u32(r)? as usize;
506        let mut pel = Vec::with_capacity(np);
507        for _ in 0..np {
508            let ms = read_u64(r)?;
509            let seq = read_u64(r)?;
510            let consumer = read_bytes(r)?;
511            let delivery_time_ms = read_u64(r)?;
512            let delivery_count = read_u32(r)?;
513            pel.push((ms, seq, consumer, delivery_time_ms, delivery_count));
514        }
515        groups.push(kevy_store::LoadedGroup { name, last_delivered, consumers, pel });
516    }
517    Ok(groups)
518}
519
520fn write_ttl<W: Write>(w: &mut W, ttl: Option<u64>) -> io::Result<()> {
521    match ttl {
522        Some(ms) => {
523            w.write_all(&[1u8])?;
524            w.write_all(&ms.to_le_bytes())?;
525        }
526        None => w.write_all(&[0u8])?,
527    }
528    Ok(())
529}
530
531fn read_ttl<R: Read>(r: &mut R) -> io::Result<Option<u64>> {
532    if read_u8(r)? == 1 {
533        Ok(Some(read_u64(r)?))
534    } else {
535        Ok(None)
536    }
537}
538
539fn tmp_path(path: &Path) -> std::path::PathBuf {
540    let mut s = path.as_os_str().to_owned();
541    s.push(".tmp");
542    s.into()
543}
544
545pub(crate) fn write_bytes<W: Write>(w: &mut W, b: &[u8]) -> io::Result<()> {
546    w.write_all(&(b.len() as u32).to_le_bytes())?;
547    w.write_all(b)
548}
549
550fn read_bytes<R: Read>(r: &mut R) -> io::Result<Vec<u8>> {
551    let len = read_u32(r)? as usize;
552    let mut buf = vec![0u8; len];
553    r.read_exact(&mut buf)?;
554    Ok(buf)
555}
556
557fn read_u8<R: Read>(r: &mut R) -> io::Result<u8> {
558    let mut b = [0u8; 1];
559    r.read_exact(&mut b)?;
560    Ok(b[0])
561}
562
563fn read_u32<R: Read>(r: &mut R) -> io::Result<u32> {
564    let mut b = [0u8; 4];
565    r.read_exact(&mut b)?;
566    Ok(u32::from_le_bytes(b))
567}
568
569fn read_u64<R: Read>(r: &mut R) -> io::Result<u64> {
570    let mut b = [0u8; 8];
571    r.read_exact(&mut b)?;
572    Ok(u64::from_le_bytes(b))
573}
574
575#[cfg(test)]
576mod tests;
577#[cfg(test)]
578mod tests_aof;
579#[cfg(test)]
580mod tests_rewrite;