Skip to main content

objstore_r/mirror/
manifest.rs

1// Author: spicyzhug@gmail.com
2// Copyright: lituus-io, all rights reserved
3//
4//! The `HEAD` object: which base and which WAL byte ranges make up the
5//! published store, plus the optional writer lease. A small checksummed
6//! binary record (the same style as graph-r's own files), written only by
7//! generation-conditional puts.
8//!
9//! ```text
10//! magic "GRPM" u32 | version u16 | flags u16 | generation u64
11//! base_len u64 | base_sum u64 | lease_expires_ms u64 | holder_len u16 | holder
12//! seg_count u32 | seg_count × (start u64 | len u64 | sum u64) | xxh3 u64
13//! ```
14
15use super::{Error, Result};
16
17const MAGIC: u32 = 0x4D50_5247; // "GRPM"
18const VERSION: u16 = 1;
19/// Upper bound on segments a manifest may list (publish coalesces long
20/// before this; the bound keeps a hostile HEAD from forcing a huge alloc).
21pub(crate) const MAX_SEGMENTS: usize = 4096;
22/// Upper bound on the lease holder name.
23pub(crate) const MAX_HOLDER: usize = 256;
24
25/// One published WAL byte range: `[start, start + len)` of `graph.wal`.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub struct Segment {
28    /// Offset in `graph.wal` (0 for the first, which carries the header).
29    pub start: u64,
30    /// Length in bytes; always ends on a committed frame boundary.
31    pub len: u64,
32    /// xxh3 of the bytes.
33    pub sum: u64,
34}
35
36impl Segment {
37    pub(crate) fn end(&self) -> u64 {
38        self.start + self.len
39    }
40}
41
42/// The writer lease recorded in `HEAD`.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct Lease {
45    /// Who holds it.
46    pub holder: String,
47    /// When it lapses, Unix ms.
48    pub expires_ms: u64,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, Default)]
52pub(crate) struct Manifest {
53    /// graph-r base generation; 0 = nothing published yet (lease-only HEAD).
54    pub(crate) generation: u64,
55    pub(crate) base_len: u64,
56    pub(crate) base_sum: u64,
57    pub(crate) lease: Option<Lease>,
58    pub(crate) segments: Vec<Segment>,
59}
60
61impl Manifest {
62    pub(crate) fn wal_len(&self) -> u64 {
63        self.segments.last().map_or(0, Segment::end)
64    }
65
66    pub(crate) fn encode(&self) -> Vec<u8> {
67        let holder = self.lease.as_ref().map_or("", |l| l.holder.as_str());
68        let mut o = Vec::with_capacity(48 + holder.len() + self.segments.len() * 24);
69        o.extend_from_slice(&self.generation.to_le_bytes());
70        o.extend_from_slice(&self.base_len.to_le_bytes());
71        o.extend_from_slice(&self.base_sum.to_le_bytes());
72        o.extend_from_slice(&self.lease.as_ref().map_or(0, |l| l.expires_ms).to_le_bytes());
73        o.extend_from_slice(&(holder.len() as u16).to_le_bytes());
74        o.extend_from_slice(holder.as_bytes());
75        o.extend_from_slice(&(self.segments.len() as u32).to_le_bytes());
76        for s in &self.segments {
77            o.extend_from_slice(&s.start.to_le_bytes());
78            o.extend_from_slice(&s.len.to_le_bytes());
79            o.extend_from_slice(&s.sum.to_le_bytes());
80        }
81        crate::fenced::seal(MAGIC, VERSION, &o)
82    }
83
84    /// Strict parse: any deviation is [`Error::Corrupt`], never a panic.
85    pub(crate) fn decode(b: &[u8]) -> Result<Self> {
86        let bad = |m: &str| Error::Corrupt { message: format!("HEAD: {m}") };
87        let body = crate::fenced::unseal(b, MAGIC, VERSION).map_err(|e| bad(&e.to_string()))?;
88        let mut r = graph_r::bytesio::Reader::new(body);
89        let generation = r.u64()?;
90        let base_len = r.u64()?;
91        let base_sum = r.u64()?;
92        let expires_ms = r.u64()?;
93        let holder_len = usize::from(r.u16()?);
94        if holder_len > MAX_HOLDER {
95            return Err(bad("lease holder too long"));
96        }
97        let holder =
98            std::str::from_utf8(r.bytes(holder_len)?).map_err(|_| bad("lease holder not utf-8"))?;
99        let lease = (!holder.is_empty()).then(|| Lease { holder: holder.to_owned(), expires_ms });
100        let count = r.u32()? as usize;
101        if count > MAX_SEGMENTS || count * 24 != r.remaining() {
102            return Err(bad("segment table size"));
103        }
104        let mut segments = Vec::with_capacity(count);
105        let mut end = 0u64;
106        for _ in 0..count {
107            let s = Segment { start: r.u64()?, len: r.u64()?, sum: r.u64()? };
108            if s.start != end || s.len == 0 {
109                return Err(bad("segments are not contiguous"));
110            }
111            end = s.start.checked_add(s.len).ok_or_else(|| bad("segment overflow"))?;
112            segments.push(s);
113        }
114        if generation == 0 && (!segments.is_empty() || base_len != 0) {
115            return Err(bad("data without a generation"));
116        }
117        if generation != 0 && segments.is_empty() {
118            return Err(bad("generation without a log"));
119        }
120        Ok(Self { generation, base_len, base_sum, lease, segments })
121    }
122}
123
124/// Fuzzing entry point: any bytes parse or fail cleanly, and whatever parses
125/// re-encodes to exactly the same bytes.
126#[doc(hidden)]
127pub fn fuzz(b: &[u8]) {
128    if let Ok(m) = Manifest::decode(b) {
129        assert_eq!(m.encode(), b);
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    fn sample() -> Manifest {
138        Manifest {
139            generation: 7,
140            base_len: 1234,
141            base_sum: 99,
142            lease: Some(Lease { holder: "instance-a".into(), expires_ms: 5 }),
143            segments: vec![
144                Segment { start: 0, len: 64, sum: 1 },
145                Segment { start: 64, len: 40, sum: 2 },
146            ],
147        }
148    }
149
150    #[test]
151    fn round_trips_and_rejects_every_corruption() {
152        let m = sample();
153        let b = m.encode();
154        assert_eq!(Manifest::decode(&b).unwrap(), m);
155        assert_eq!(Manifest::decode(&Manifest::default().encode()).unwrap(), Manifest::default());
156        for i in 0..b.len() {
157            let mut c = b.clone();
158            c[i] ^= 0x40;
159            assert!(Manifest::decode(&c).is_err(), "flip at {i}");
160        }
161        for cut in 0..b.len() {
162            assert!(Manifest::decode(&b[..cut]).is_err(), "cut at {cut}");
163        }
164        fuzz(&b);
165    }
166
167    #[test]
168    fn structural_rules_hold_even_with_a_valid_checksum() {
169        let mut m = sample();
170        m.segments[1].start = 65;
171        assert!(Manifest::decode(&m.encode()).is_err(), "gap");
172        let mut m = sample();
173        m.segments.clear();
174        assert!(Manifest::decode(&m.encode()).is_err(), "generation without log");
175        let m = Manifest { generation: 0, base_len: 5, ..Manifest::default() };
176        assert!(Manifest::decode(&m.encode()).is_err());
177    }
178}