bolt_lite/
lib.rs

1//! Minimal read-only BoltDB parser, tailored for containerd metadata usage.
2//! Implements just enough of Bolt's B+tree to walk buckets and read values.
3
4mod btree;
5mod meta;
6mod page;
7
8use std::fs::File;
9use std::io::Read;
10use std::path::Path;
11
12pub use meta::{BRANCH_PAGE_FLAG, BUCKET_VALUE_FLAG, LEAF_PAGE_FLAG, MAGIC, META_PAGE_FLAG, META_STRUCT_OFFSET};
13use meta::{parse_meta_at, parse_page_size, BucketHeader};
14use page::{collect_leaf_entries, LeafEntry};
15use crate::btree::{collect_tree_entries, find_in_page, find_in_tree};
16
17#[derive(Debug, thiserror::Error)]
18pub enum Error {
19    #[error("io: {0}")]
20    Io(#[from] std::io::Error),
21    #[error("invalid bolt magic")]
22    InvalidMagic,
23    #[error("unsupported page size {0}")]
24    InvalidPageSize(u32),
25    #[error("invalid page id {0}")]
26    InvalidPageId(u64),
27    #[error("corrupt bolt structure: {0}")]
28    Corrupt(&'static str),
29}
30
31pub type Result<T> = std::result::Result<T, Error>;
32
33/// Read-only database held in memory (meta.db is small).
34pub struct Bolt {
35    data: Vec<u8>,
36    page_size: usize,
37    root: BucketHeader,
38}
39
40/// Lightweight database stats useful for debugging bounds.
41pub struct Stats {
42    pub page_size: usize,
43    pub page_count: usize,
44    pub bytes: usize,
45}
46
47/// Read-only transaction. Immutable view over the DB bytes.
48pub struct Tx<'a> {
49    db: &'a Bolt,
50}
51
52/// A logical bucket.
53pub struct Bucket<'a> {
54    db: &'a Bolt,
55    root: u64,
56    inline: Option<Vec<u8>>, // inline page bytes when root == 0
57}
58
59/// Cursor to iterate leaf entries without re-parsing the tree.
60pub struct BucketCursor<'a> {
61    entries: Vec<LeafEntry>,
62    idx: usize,
63    _db: &'a Bolt,
64}
65
66impl Bolt {
67    /// Open the database file into memory. We intentionally do not validate checksums
68    /// to tolerate slightly nonstandard meta pages seen in containerd rootless setups.
69    pub fn open_ro<P: AsRef<Path>>(path: P) -> Result<Self> {
70        let mut file = File::open(path)?;
71        let mut data = Vec::new();
72        file.read_to_end(&mut data)?;
73        if data.len() < 4096 {
74            return Err(Error::Corrupt("file too small"));
75        }
76
77        let page_size = parse_page_size(&data)? as usize;
78        let meta0 = parse_meta_at(&data, page_size, 0).ok();
79        let meta1 = if data.len() >= page_size * 2 {
80            parse_meta_at(&data, page_size, page_size).ok()
81        } else {
82            None
83        };
84        let meta = match (meta0, meta1) {
85            (Some(m0), Some(m1)) => if m1.txid >= m0.txid { m1 } else { m0 },
86            (Some(m0), None) => m0,
87            (None, Some(m1)) => m1,
88            (None, None) => return Err(Error::Corrupt("no valid meta pages")),
89        };
90
91        Ok(Self {
92            data,
93            page_size,
94            root: meta.root,
95        })
96    }
97
98    pub fn stats(&self) -> Stats {
99        Stats {
100            page_size: self.page_size,
101            page_count: self.data.len() / self.page_size,
102            bytes: self.data.len(),
103        }
104    }
105
106    pub fn begin(&self) -> Result<Tx<'_>> {
107        Ok(Tx { db: self })
108    }
109
110    fn read_page(&self, pgid: u64) -> Result<&[u8]> {
111        let offset = (pgid as usize)
112            .checked_mul(self.page_size)
113            .ok_or(Error::InvalidPageId(pgid))?;
114        if offset + self.page_size > self.data.len() {
115            return Err(Error::InvalidPageId(pgid));
116        }
117        let base = &self.data[offset..offset + self.page_size];
118        let overflow = u32::from_le_bytes(base[12..16].try_into().unwrap()) as usize;
119        let end = offset + self.page_size * (1 + overflow);
120        if end > self.data.len() {
121            return Err(Error::InvalidPageId(pgid));
122        }
123        Ok(&self.data[offset..end])
124    }
125}
126
127impl<'a> Tx<'a> {
128    pub fn bucket_path(&'a self, parts: &[&[u8]]) -> Option<Bucket<'a>> {
129        let mut current = self.root_bucket();
130        for name in parts {
131            current = current.bucket(name)?;
132        }
133        Some(current)
134    }
135
136    pub fn bucket(&'a self, name: &[u8]) -> Option<Bucket<'a>> {
137        let root = self.root_bucket();
138        root.bucket(name)
139    }
140
141    fn root_bucket(&'a self) -> Bucket<'a> {
142        Bucket {
143            db: self.db,
144            root: self.db.root.root,
145            inline: None,
146        }
147    }
148}
149
150impl<'a> Bucket<'a> {
151    pub fn bucket(&self, name: &[u8]) -> Option<Bucket<'a>> {
152        let entry = self.find_entry(name)?;
153        if entry.flags & BUCKET_VALUE_FLAG == 0 {
154            return None;
155        }
156        let hdr = ok_opt(parse_bucket_header(&entry.value))?;
157        let inline = if hdr.root == 0 && entry.value.len() > 16 {
158            Some(entry.value[16..].to_vec())
159        } else {
160            None
161        };
162        Some(Bucket {
163            db: self.db,
164            root: hdr.root,
165            inline,
166        })
167    }
168
169    pub fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
170        self.find_entry(key).map(|e| e.value)
171    }
172
173    pub fn iter_buckets(&self) -> Vec<(Vec<u8>, Bucket<'a>)> {
174        let mut out = Vec::new();
175        if let Ok(entries) = self.collect_entries() {
176            for e in entries {
177                if e.flags & BUCKET_VALUE_FLAG != 0 {
178                    if let Some(hdr) = ok_opt(parse_bucket_header(&e.value)) {
179                        let inline = if hdr.root == 0 && e.value.len() > 16 {
180                            Some(e.value[16..].to_vec())
181                        } else {
182                            None
183                        };
184                        out.push((e.key, Bucket { db: self.db, root: hdr.root, inline }));
185                    }
186                }
187            }
188        }
189        out
190    }
191
192    fn find_entry(&self, key: &[u8]) -> Option<LeafEntry> {
193        if self.root == 0 {
194            let data = self.inline.as_ref()?;
195            return find_in_page(self.db.page_size, data, key).ok().flatten();
196        }
197        find_in_tree(self.db, self.root, key).ok().flatten()
198    }
199
200    fn collect_entries(&self) -> Result<Vec<LeafEntry>> {
201        if self.root == 0 {
202            let data = self.inline.as_ref().ok_or(Error::Corrupt("inline bucket missing data"))?;
203            return collect_leaf_entries(self.db.page_size, data);
204        }
205        collect_tree_entries(self.db, self.root)
206    }
207
208    pub fn cursor(&self) -> Result<BucketCursor<'a>> {
209        let entries = self.collect_entries()?;
210        Ok(BucketCursor { entries, idx: 0, _db: self.db })
211    }
212}
213
214fn parse_bucket_header(buf: &[u8]) -> Result<BucketHeader> {
215    if buf.len() < 16 {
216        return Err(Error::Corrupt("bucket header too small"));
217    }
218    Ok(BucketHeader {
219        root: u64::from_le_bytes(buf[..8].try_into().unwrap()),
220        _sequence: u64::from_le_bytes(buf[8..16].try_into().unwrap()),
221    })
222}
223
224pub(crate) fn ok_opt<T>(res: Result<T>) -> Option<T> {
225    res.ok()
226}
227
228pub struct CursorEntry {
229    pub key: Vec<u8>,
230    pub value: Vec<u8>,
231    pub flags: u32,
232}
233
234impl<'a> Iterator for BucketCursor<'a> {
235    type Item = CursorEntry;
236
237    fn next(&mut self) -> Option<Self::Item> {
238        if self.idx >= self.entries.len() {
239            return None;
240        }
241        let entry = self.entries[self.idx].clone();
242        self.idx += 1;
243        Some(CursorEntry { key: entry.key, value: entry.value, flags: entry.flags })
244    }
245}
246
247// Tests cover offset handling and cursor basics.
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn ok_opt_maps_result_option() {
254        assert_eq!(ok_opt::<u32>(Ok(5)), Some(5));
255        assert!(ok_opt::<u32>(Err(Error::InvalidMagic)).is_none());
256    }
257
258    #[test]
259    fn stats_reports_page_count() {
260        let mut db = Bolt {
261            data: vec![0u8; 8192],
262            page_size: 4096,
263            root: BucketHeader { root: 0, _sequence: 0 },
264        };
265        let stats = db.stats();
266        assert_eq!(stats.page_size, 4096);
267        assert_eq!(stats.page_count, 2);
268        assert_eq!(stats.bytes, 8192);
269        // silence unused
270        db.data[0] = 0;
271    }
272}