Skip to main content

mkit_core/pack/window/
cursor.rs

1//! Canonical v1 cursor: version byte; four u64s (pack length, window size,
2//! position, payload sum); u32 version/count/index; optional first-non-raw u32;
3//! completed-window u64; optional expected id, trailer anchor, and current-window
4//! prefix commitment; two trees (u8 depth, CVs, optional root); checksum. Optional
5//! fields have a 0/1 tag. All integers LE. The tagged prefix adds at most 33 bytes;
6//! the complete encoding remains bounded by 4 KiB.
7use super::{MAX_ENTRIES, MAX_TOTAL_PAYLOAD, PackError, Tree, geometry};
8use crate::hash::{self, Hash};
9
10/// Trusted-at-rest, checksummed entry-boundary state (at most 4 KiB encoded).
11#[derive(Debug, Clone)]
12pub struct WindowCursor {
13    pub(super) pack_len: u64,
14    pub(super) window_size: u64,
15    pub(super) pos: u64,
16    pub(super) payload_sum: u64,
17    pub(super) version: u32,
18    pub(super) count: u32,
19    pub(super) index: u32,
20    pub(super) first_non_raw: Option<u32>,
21    pub(super) completed: u64,
22    pub(super) expected: Option<Hash>,
23    pub(super) anchor: Option<Hash>,
24    pub(super) window_prefix: Option<Hash>,
25    pub(super) trailer_tree: Tree,
26    pub(super) id_tree: Tree,
27}
28
29impl WindowCursor {
30    pub(super) fn initial(pack_len: u64, window_size: u64, expected: Option<Hash>) -> Self {
31        Self {
32            pack_len,
33            window_size,
34            pos: 12,
35            payload_sum: 0,
36            version: 0,
37            count: 0,
38            index: 0,
39            first_non_raw: None,
40            completed: 0,
41            expected,
42            anchor: None,
43            window_prefix: None,
44            trailer_tree: Tree::new(),
45            id_tree: Tree::new(),
46        }
47    }
48    pub(super) fn split(&self) -> u64 {
49        self.pack_len - 32
50    } // validated geometry
51    pub(super) fn validate(&self) -> Result<(), PackError> {
52        let bad = PackError::PackfileCorrupted;
53        geometry(self.pack_len, self.window_size).map_err(|_| PackError::PackfileCorrupted)?;
54        let framing = u64::from(self.index)
55            .checked_mul(5)
56            .and_then(|n| n.checked_add(12))
57            .and_then(|n| n.checked_add(self.payload_sum));
58        let max_len = MAX_TOTAL_PAYLOAD
59            .checked_add(u64::from(self.count) * 5)
60            .and_then(|n| n.checked_add(44));
61        let window_start = self.completed.checked_mul(self.window_size);
62        if window_start.is_none_or(|start| self.window_prefix.is_some() != (self.pos > start))
63            || (self.expected.is_none() && self.anchor.is_none())
64            || !matches!(self.version, 1 | 2)
65            || self.count > MAX_ENTRIES
66            || self.index > self.count
67            || !(12..=self.split()).contains(&self.pos)
68            || framing != Some(self.pos)
69            || (self.index == 0 && self.payload_sum != 0)
70            || self.payload_sum > MAX_TOTAL_PAYLOAD
71            || max_len.is_none_or(|n| self.pack_len > n)
72            || self.completed != self.pos / self.window_size
73            || self.first_non_raw.is_some_and(|i| i >= self.index)
74            || !self
75                .trailer_tree
76                .validate(self.completed, self.split(), self.window_size)
77            || (self.expected.is_some()
78                && !self
79                    .id_tree
80                    .validate(self.completed, self.pack_len, self.window_size))
81            || (self.expected.is_none() && self.id_tree != Tree::new())
82        {
83            return Err(bad);
84        }
85        Ok(())
86    }
87
88    /// Encode canonical little-endian v1 fields followed by BLAKE3 checksum.
89    #[must_use]
90    pub fn to_bytes(&self) -> Vec<u8> {
91        let mut out = vec![1];
92        for n in [self.pack_len, self.window_size, self.pos, self.payload_sum] {
93            out.extend_from_slice(&n.to_le_bytes());
94        }
95        for n in [self.version, self.count, self.index] {
96            out.extend_from_slice(&n.to_le_bytes());
97        }
98        out.push(u8::from(self.first_non_raw.is_some()));
99        if let Some(index) = self.first_non_raw {
100            out.extend_from_slice(&index.to_le_bytes());
101        }
102        out.extend_from_slice(&self.completed.to_le_bytes());
103        write_option(&mut out, self.expected);
104        write_option(&mut out, self.anchor);
105        write_option(&mut out, self.window_prefix);
106        for tree in [&self.trailer_tree, &self.id_tree] {
107            // Full pack geometry needs fewer than 64 CVs (and at most 17 under
108            // the format caps), so this is lossless for every internal cursor.
109            out.push(u8::try_from(tree.stack.len()).unwrap_or(u8::MAX));
110            for cv in &tree.stack {
111                out.extend_from_slice(cv);
112            }
113            write_option(&mut out, tree.root);
114        }
115        out.extend_from_slice(&hash::hash(&out));
116        out
117    }
118
119    /// Decode a canonical cursor and validate its checksum and field geometry.
120    ///
121    /// # Errors
122    /// Oversized, truncated, unsupported, corrupt, or inconsistent encodings
123    /// return `PackfileCorrupted`.
124    pub fn from_bytes(bytes: &[u8]) -> Result<Self, PackError> {
125        if bytes.len() > 4096 || bytes.len() < 32 {
126            return Err(PackError::PackfileCorrupted);
127        }
128        let (body, checksum) = bytes.split_at(bytes.len() - 32);
129        if hash::hash(body).as_slice() != checksum {
130            return Err(PackError::PackfileCorrupted);
131        }
132        let mut r = Input(body);
133        if r.byte()? != 1 {
134            return Err(PackError::PackfileCorrupted);
135        }
136        let mut state = Self {
137            pack_len: r.u64()?,
138            window_size: r.u64()?,
139            pos: r.u64()?,
140            payload_sum: r.u64()?,
141            version: r.u32()?,
142            count: r.u32()?,
143            index: r.u32()?,
144            first_non_raw: None,
145            completed: 0,
146            expected: None,
147            anchor: None,
148            window_prefix: None,
149            trailer_tree: Tree::new(),
150            id_tree: Tree::new(),
151        };
152        state.first_non_raw = if r.tag()? { Some(r.u32()?) } else { None };
153        state.completed = r.u64()?;
154        state.expected = r.optional_hash()?;
155        state.anchor = r.optional_hash()?;
156        state.window_prefix = r.optional_hash()?;
157        state.trailer_tree = r.tree()?;
158        state.id_tree = r.tree()?;
159        if !r.0.is_empty() {
160            return Err(PackError::PackfileCorrupted);
161        }
162        state.validate()?;
163        Ok(state)
164    }
165}
166fn write_option(out: &mut Vec<u8>, value: Option<Hash>) {
167    out.push(u8::from(value.is_some()));
168    if let Some(hash) = value {
169        out.extend_from_slice(&hash);
170    }
171}
172struct Input<'a>(&'a [u8]);
173impl Input<'_> {
174    fn array<const N: usize>(&mut self) -> Result<[u8; N], PackError> {
175        let bytes = self.0.get(..N).ok_or(PackError::PackfileCorrupted)?;
176        let out = bytes.try_into().map_err(|_| PackError::PackfileCorrupted)?;
177        self.0 = &self.0[N..];
178        Ok(out)
179    }
180    fn byte(&mut self) -> Result<u8, PackError> {
181        Ok(self.array::<1>()?[0])
182    }
183    fn tag(&mut self) -> Result<bool, PackError> {
184        match self.byte()? {
185            0 => Ok(false),
186            1 => Ok(true),
187            _ => Err(PackError::PackfileCorrupted),
188        }
189    }
190    fn u64(&mut self) -> Result<u64, PackError> {
191        Ok(u64::from_le_bytes(self.array()?))
192    }
193    fn u32(&mut self) -> Result<u32, PackError> {
194        Ok(u32::from_le_bytes(self.array()?))
195    }
196    fn optional_hash(&mut self) -> Result<Option<Hash>, PackError> {
197        if self.tag()? {
198            Ok(Some(self.array()?))
199        } else {
200            Ok(None)
201        }
202    }
203    fn tree(&mut self) -> Result<Tree, PackError> {
204        let depth = usize::from(self.byte()?);
205        if depth > 64 {
206            return Err(PackError::PackfileCorrupted);
207        }
208        let mut stack = Vec::new();
209        stack
210            .try_reserve_exact(depth)
211            .map_err(|_| PackError::PackfileTooLarge)?;
212        for _ in 0..depth {
213            stack.push(self.array()?);
214        }
215        Ok(Tree {
216            stack,
217            root: self.optional_hash()?,
218        })
219    }
220}