Skip to main content

cubecl_environment/bundle/
embedded.rs

1//! The flat bundle format, readable anywhere.
2//!
3//! `SQLite` needs a file system, so it can't serve wasm or no-std targets.
4//! This format exists for them: one contiguous blob of bytes, produced on a
5//! development machine by [`export`](super::export) and consumed by
6//! [`EmbeddedBundle`] either from `include_bytes!` or from bytes fetched at
7//! runtime.
8//!
9//! # Layout
10//!
11//! All integers are little-endian `u32`. Offsets are relative to the start of
12//! the data section.
13//!
14//! ```text
15//! magic          8 bytes, MAGIC
16//! format         u32, FORMAT_VERSION
17//! metadata_len   u32
18//! metadata       metadata_len bytes, opaque JSON, only read by native tools
19//! namespace_cnt  u32
20//! namespaces     namespace_cnt × (u32 len + UTF-8 bytes), sorted
21//! entry_cnt      u32
22//! entries        entry_cnt × 20 bytes, sorted by (namespace, key)
23//! data           keys and values, concatenated
24//! ```
25//!
26//! Both tables are sorted, so a lookup is a binary search over `entries` with
27//! no allocation and no deserialization: the format is read in place rather
28//! than parsed into memory. Everything is validated once at
29//! [`open`](EmbeddedBundle::open), so lookups need no bounds handling beyond
30//! ordinary slicing.
31
32use alloc::string::String;
33use alloc::vec::Vec;
34
35use super::Bundle;
36use crate::bytes::Bytes;
37use crate::persistence::NamespaceSummary;
38
39/// Identifies a flat bundle, and catches a file handed over by mistake.
40pub const MAGIC: &[u8; 8] = b"CUBECLB\x01";
41
42/// The flat layout this build reads and writes. Entries of any other version
43/// are ignored rather than misread.
44pub const FORMAT_VERSION: u32 = 1;
45
46/// Size of one entry in the index: namespace id, key span, value span.
47pub(crate) const ENTRY_SIZE: usize = 20;
48
49/// The layout version of the flat bundle starting at `bytes`, if it is one.
50///
51/// Only the header is read, so this tells the two bundle formats apart from a
52/// file's first bytes: enough to pick a reader, or to know that an export may
53/// replace what is already at its output path.
54pub fn flat_bundle_version(bytes: &[u8]) -> Option<u32> {
55    if bytes.len() < MAGIC.len() + 4 || &bytes[..MAGIC.len()] != MAGIC {
56        return None;
57    }
58
59    read_u32(bytes, MAGIC.len())
60}
61
62/// Why a byte blob isn't a readable flat bundle.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum EmbeddedBundleError {
65    /// The blob doesn't start with [`MAGIC`].
66    NotABundle,
67    /// The blob declares a layout this build doesn't read.
68    UnsupportedFormat(u32),
69    /// The blob is truncated or its offsets point outside it.
70    Corrupted(&'static str),
71}
72
73impl core::fmt::Display for EmbeddedBundleError {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        match self {
76            Self::NotABundle => write!(f, "not a cubecl flat bundle"),
77            Self::UnsupportedFormat(found) => write!(
78                f,
79                "flat bundle format {found} is not {FORMAT_VERSION}, its entries are unreadable"
80            ),
81            Self::Corrupted(what) => write!(f, "corrupted flat bundle: {what}"),
82        }
83    }
84}
85
86impl core::error::Error for EmbeddedBundleError {}
87
88/// A bundle held as one blob of bytes, embedded in the binary with
89/// `include_bytes!` or fetched at runtime.
90///
91/// This is the format for targets without a file system. The blob is held as
92/// shared [`Bytes`], so a lookup hands back a zero-copy window into it rather
93/// than a copy: serving a compiled kernel costs a reference count, whatever
94/// its size.
95///
96/// Ignored rather than run: the bundle file is produced by
97/// `cargo xtask bundle export --format flat`, so there is none to include here.
98///
99/// ```ignore
100/// use cubecl_environment::bundle::{self, EmbeddedBundle};
101///
102/// static BUNDLE: &[u8] = include_bytes!("../bundles/h100.ccb");
103///
104/// let bundle = EmbeddedBundle::from_static(BUNDLE).expect("valid bundle");
105/// bundle::import(&bundle);
106/// ```
107#[derive(Debug)]
108pub struct EmbeddedBundle {
109    bytes: Bytes,
110    /// Byte range of the metadata blob.
111    metadata: (usize, usize),
112    /// Byte range of every namespace of the sorted table, resolved once at
113    /// [`parse`](Self::parse) so a lookup is an index rather than a walk.
114    namespaces: Vec<(usize, usize)>,
115    /// Offset of the entry index and how many entries it holds.
116    entries: (usize, usize),
117    /// Offset of the data section, which every span is relative to.
118    data: usize,
119}
120
121impl EmbeddedBundle {
122    /// Reads a flat bundle, validating it once so that later lookups can't
123    /// fail.
124    ///
125    /// The blob is shared on the way in so that lookups can return zero-copy
126    /// windows into it.
127    pub fn open(bytes: Bytes) -> Result<Self, EmbeddedBundleError> {
128        Self::parse(bytes.shared())
129    }
130
131    /// Reads a flat bundle embedded in the binary with `include_bytes!`.
132    ///
133    /// With the `shared-bytes` feature the static blob is adopted as is, so
134    /// the bundle occupies no heap at all. Without it, the bytes are copied
135    /// once at startup.
136    pub fn from_static(blob: &'static [u8]) -> Result<Self, EmbeddedBundleError> {
137        #[cfg(feature = "shared-bytes")]
138        let bytes = Bytes::from_shared(
139            bytes::Bytes::from_static(blob),
140            crate::bytes::AllocationProperty::Other,
141        );
142        #[cfg(not(feature = "shared-bytes"))]
143        let bytes = Bytes::from_bytes_vec(blob.to_vec());
144
145        Self::open(bytes)
146    }
147
148    fn parse(bytes: Bytes) -> Result<Self, EmbeddedBundleError> {
149        use EmbeddedBundleError::{Corrupted, NotABundle, UnsupportedFormat};
150
151        if bytes.len() < MAGIC.len() + 8 || &bytes[..MAGIC.len()] != MAGIC {
152            return Err(NotABundle);
153        }
154
155        let mut cursor = MAGIC.len();
156        let take_u32 = |cursor: &mut usize| -> Option<u32> {
157            let value = read_u32(&bytes, *cursor)?;
158            *cursor += 4;
159            Some(value)
160        };
161
162        let format = take_u32(&mut cursor).ok_or(Corrupted("truncated header"))?;
163        if format != FORMAT_VERSION {
164            return Err(UnsupportedFormat(format));
165        }
166
167        let metadata_len = take_u32(&mut cursor).ok_or(Corrupted("truncated header"))? as usize;
168        let metadata_start = cursor;
169        let metadata_end = metadata_start
170            .checked_add(metadata_len)
171            .ok_or(Corrupted("metadata length overflows"))?;
172        if metadata_end > bytes.len() {
173            return Err(Corrupted("metadata runs past the end"));
174        }
175        cursor = metadata_end;
176
177        let namespace_count = take_u32(&mut cursor).ok_or(Corrupted("truncated header"))? as usize;
178        // Walk the namespace table once, recording where each entry lives:
179        // every later lookup indexes this instead of walking again.
180        let mut namespaces = Vec::with_capacity(namespace_count.min(bytes.len() / 4));
181        for _ in 0..namespace_count {
182            let len =
183                read_u32(&bytes, cursor).ok_or(Corrupted("truncated namespace table"))? as usize;
184            let start = cursor + 4;
185            cursor = start
186                .checked_add(len)
187                .ok_or(Corrupted("namespace length overflows"))?;
188            if cursor > bytes.len() {
189                return Err(Corrupted("namespace table runs past the end"));
190            }
191            namespaces.push((start, len));
192        }
193
194        let entry_count = read_u32(&bytes, cursor).ok_or(Corrupted("truncated header"))? as usize;
195        cursor += 4;
196        let entries_start = cursor;
197        let index_len = entry_count
198            .checked_mul(ENTRY_SIZE)
199            .ok_or(Corrupted("entry count overflows"))?;
200        let data_start = entries_start
201            .checked_add(index_len)
202            .ok_or(Corrupted("entry index overflows"))?;
203        if data_start > bytes.len() {
204            return Err(Corrupted("entry index runs past the end"));
205        }
206
207        let this = Self {
208            bytes,
209            metadata: (metadata_start, metadata_end),
210            namespaces,
211            entries: (entries_start, entry_count),
212            data: data_start,
213        };
214
215        this.validate_entries()?;
216
217        Ok(this)
218    }
219
220    /// Checks every span and the index ordering up front, so lookups never
221    /// have to.
222    fn validate_entries(&self) -> Result<(), EmbeddedBundleError> {
223        use EmbeddedBundleError::Corrupted;
224
225        let available = self.bytes.len() - self.data;
226        let namespace_count = self.namespaces.len();
227        let mut previous: Option<Entry> = None;
228
229        for index in 0..self.entries.1 {
230            let entry = self
231                .entry(index)
232                .ok_or(Corrupted("truncated entry index"))?;
233
234            if entry.namespace as usize >= namespace_count {
235                return Err(Corrupted("entry points at an unknown namespace"));
236            }
237            for (offset, len) in [
238                (entry.key_offset, entry.key_len),
239                (entry.value_offset, entry.value_len),
240            ] {
241                let end = (offset as usize)
242                    .checked_add(len as usize)
243                    .ok_or(Corrupted("entry span overflows"))?;
244                if end > available {
245                    return Err(Corrupted("entry span runs past the end"));
246                }
247            }
248
249            // A blob is untrusted input, and `get`, `first_of` and `scan` all
250            // assume this ordering: an unsorted index would silently answer
251            // misses, and a duplicate key would shadow one of the two values.
252            if let Some(previous) = &previous {
253                let order = (previous.namespace, self.key_of(previous))
254                    .cmp(&(entry.namespace, self.key_of(&entry)));
255                if order != core::cmp::Ordering::Less {
256                    return Err(Corrupted("entry index is not sorted by (namespace, key)"));
257                }
258            }
259            previous = Some(entry);
260        }
261
262        // The namespace table must be readable as UTF-8 to be comparable.
263        for index in 0..namespace_count {
264            self.namespace(index)
265                .ok_or(Corrupted("namespace is not valid UTF-8"))?;
266        }
267
268        Ok(())
269    }
270
271    /// The opaque metadata blob, a JSON manifest when written by
272    /// [`export`](super::export).
273    pub fn metadata(&self) -> &[u8] {
274        &self.bytes[self.metadata.0..self.metadata.1]
275    }
276
277    /// The bundle manifest, parsed from [`metadata`](Self::metadata) and
278    /// validated against the schema this build understands.
279    ///
280    /// The flat format keeps the manifest out of the read path on purpose, so
281    /// it is checked here rather than at [`open`](Self::open): a bundle whose
282    /// manifest this build can't read still serves its entries.
283    pub fn manifest(&self) -> Result<super::BundleManifest, super::BundleError> {
284        let manifest = super::BundleManifest::parse(self.metadata())?;
285        manifest.warn_on_version_mismatch();
286
287        Ok(manifest)
288    }
289
290    /// How many entries the bundle holds, across all namespaces.
291    pub fn len(&self) -> usize {
292        self.entries.1
293    }
294
295    /// Whether the bundle holds no entries at all.
296    pub fn is_empty(&self) -> bool {
297        self.len() == 0
298    }
299
300    /// Entry count and total size per namespace, for reporting.
301    pub fn summary(&self) -> Vec<NamespaceSummary> {
302        let mut summary: Vec<NamespaceSummary> = (0..self.namespaces.len())
303            .filter_map(|index| {
304                Some(NamespaceSummary {
305                    namespace: alloc::string::ToString::to_string(self.namespace(index)?),
306                    entries: 0,
307                    bytes: 0,
308                })
309            })
310            .collect();
311
312        for index in 0..self.entries.1 {
313            let Some(entry) = self.entry(index) else {
314                break;
315            };
316            if let Some(namespace) = summary.get_mut(entry.namespace as usize) {
317                namespace.entries += 1;
318                namespace.bytes += u64::from(entry.key_len) + u64::from(entry.value_len);
319            }
320        }
321
322        summary
323    }
324
325    /// The `index`-th namespace of the sorted namespace table.
326    fn namespace(&self, index: usize) -> Option<&str> {
327        let &(start, len) = self.namespaces.get(index)?;
328        core::str::from_utf8(self.bytes.get(start..start + len)?).ok()
329    }
330
331    /// The id of `namespace`, by binary search over the sorted table.
332    fn namespace_id(&self, namespace: &str) -> Option<u32> {
333        let at = lower_bound(self.namespaces.len(), |index| {
334            Some(self.namespace(index)?.cmp(namespace))
335        })?;
336
337        (self.namespace(at)? == namespace).then_some(at as u32)
338    }
339
340    fn entry(&self, index: usize) -> Option<Entry> {
341        let at = self.entries.0 + index * ENTRY_SIZE;
342
343        Some(Entry {
344            namespace: read_u32(&self.bytes, at)?,
345            key_offset: read_u32(&self.bytes, at + 4)?,
346            key_len: read_u32(&self.bytes, at + 8)?,
347            value_offset: read_u32(&self.bytes, at + 12)?,
348            value_len: read_u32(&self.bytes, at + 16)?,
349        })
350    }
351
352    /// Where a span recorded in the index lives in the blob.
353    ///
354    /// Spans are relative to the start of the data section, and
355    /// [`validate_entries`](Self::validate_entries) checks every one of them
356    /// against that same convention, so slicing here needs no bounds handling.
357    fn span(&self, offset: u32, len: u32) -> core::ops::Range<usize> {
358        let start = self.data + offset as usize;
359        start..start + len as usize
360    }
361
362    fn key_of(&self, entry: &Entry) -> &[u8] {
363        &self.bytes[self.span(entry.key_offset, entry.key_len)]
364    }
365
366    fn value_of(&self, entry: &Entry) -> &[u8] {
367        &self.bytes[self.span(entry.value_offset, entry.value_len)]
368    }
369
370    /// The entry's value as a zero-copy window into the blob.
371    fn value_window(&self, entry: &Entry) -> Option<Bytes> {
372        let span = self.span(entry.value_offset, entry.value_len);
373        self.bytes
374            .view(span.start, span.end)
375            .inspect_err(|err| log::warn!("Embedded bundle: can't view an entry: {err:?}"))
376            .ok()
377    }
378
379    /// The index of the first entry of `namespace`, if it has any.
380    fn first_of(&self, namespace: u32) -> Option<usize> {
381        let at = lower_bound(self.entries.1, |index| {
382            Some(self.entry(index)?.namespace.cmp(&namespace))
383        })?;
384
385        (self.entry(at)?.namespace == namespace).then_some(at)
386    }
387}
388
389impl Bundle for EmbeddedBundle {
390    fn get(&self, namespace: &str, key: &[u8]) -> Option<Bytes> {
391        let namespace = self.namespace_id(namespace)?;
392
393        // The index is sorted by (namespace, key), so one binary search over
394        // the pair finds the entry without materializing anything.
395        let at = lower_bound(self.entries.1, |index| {
396            let entry = self.entry(index)?;
397            Some((entry.namespace, self.key_of(&entry)).cmp(&(namespace, key)))
398        })?;
399
400        let entry = self.entry(at)?;
401        ((entry.namespace, self.key_of(&entry)) == (namespace, key))
402            .then(|| self.value_window(&entry))
403            .flatten()
404    }
405
406    fn scan(&self, namespace: &str, visit: &mut dyn FnMut(&[u8], &[u8])) {
407        let Some(id) = self.namespace_id(namespace) else {
408            return;
409        };
410        let Some(first) = self.first_of(id) else {
411            return;
412        };
413
414        for index in first..self.entries.1 {
415            let Some(entry) = self.entry(index) else {
416                return;
417            };
418            if entry.namespace != id {
419                return;
420            }
421            visit(self.key_of(&entry), self.value_of(&entry));
422        }
423    }
424
425    fn namespaces(&self) -> Vec<String> {
426        (0..self.namespaces.len())
427            .filter_map(|index| Some(alloc::string::ToString::to_string(self.namespace(index)?)))
428            .collect()
429    }
430
431    fn describe(&self) -> String {
432        alloc::format!("embedded bundle ({} entries)", self.entries.1)
433    }
434}
435
436/// One row of the entry index.
437struct Entry {
438    namespace: u32,
439    key_offset: u32,
440    key_len: u32,
441    value_offset: u32,
442    value_len: u32,
443}
444
445/// The index of the first item of `0..len` that `compare` doesn't order before
446/// what is being looked for, by binary search.
447///
448/// Both tables of the format are sorted, so every lookup is this one search:
449/// the caller checks the item it lands on to tell a hit from a miss, and gets
450/// `len` back when everything sorts before it. `compare` answers `None` for an
451/// unreadable item, which gives up rather than guessing.
452fn lower_bound(
453    len: usize,
454    compare: impl Fn(usize) -> Option<core::cmp::Ordering>,
455) -> Option<usize> {
456    let (mut low, mut high) = (0usize, len);
457
458    while low < high {
459        let mid = low + (high - low) / 2;
460        if compare(mid)? == core::cmp::Ordering::Less {
461            low = mid + 1;
462        } else {
463            high = mid;
464        }
465    }
466
467    Some(low)
468}
469
470fn read_u32(bytes: &[u8], at: usize) -> Option<u32> {
471    let raw: [u8; 4] = bytes.get(at..at + 4)?.try_into().ok()?;
472    Some(u32::from_le_bytes(raw))
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use alloc::vec;
479
480    /// Bytes that pass `open` but describe nothing.
481    fn empty_bundle() -> Bytes {
482        let mut bytes = Vec::new();
483        bytes.extend_from_slice(MAGIC);
484        bytes.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
485        bytes.extend_from_slice(&0u32.to_le_bytes()); // metadata
486        bytes.extend_from_slice(&0u32.to_le_bytes()); // namespaces
487        bytes.extend_from_slice(&0u32.to_le_bytes()); // entries
488        Bytes::from_bytes_vec(bytes)
489    }
490
491    #[test]
492    fn an_empty_bundle_reads_as_empty() {
493        let bundle = EmbeddedBundle::open(empty_bundle()).unwrap();
494
495        assert!(bundle.is_empty());
496        assert_eq!(bundle.get("anything", b"key"), None);
497        bundle.scan("anything", &mut |_, _| panic!("no entries to visit"));
498    }
499
500    #[test]
501    fn foreign_bytes_are_rejected() {
502        assert_eq!(
503            EmbeddedBundle::open(Bytes::from_bytes_vec(b"definitely not a bundle".to_vec()))
504                .unwrap_err(),
505            EmbeddedBundleError::NotABundle
506        );
507        assert_eq!(
508            EmbeddedBundle::open(Bytes::from_bytes_vec(vec![])).unwrap_err(),
509            EmbeddedBundleError::NotABundle
510        );
511    }
512
513    #[test]
514    fn another_format_version_is_rejected() {
515        let mut bytes = empty_bundle().to_vec();
516        bytes[MAGIC.len()..MAGIC.len() + 4].copy_from_slice(&99u32.to_le_bytes());
517        let bytes = Bytes::from_bytes_vec(bytes);
518
519        assert_eq!(
520            EmbeddedBundle::open(bytes).unwrap_err(),
521            EmbeddedBundleError::UnsupportedFormat(99)
522        );
523    }
524
525    /// Truncation must be reported, never panic on a slice out of range.
526    #[test]
527    fn truncated_bytes_are_rejected() {
528        let full = empty_bundle();
529
530        for len in MAGIC.len()..full.len() {
531            let result = EmbeddedBundle::open(Bytes::from_bytes_vec(full[..len].to_vec()));
532            assert!(result.is_err(), "a {len}-byte prefix must not open");
533        }
534    }
535
536    /// A blob whose offsets point outside it must be caught at open, since
537    /// lookups slice without checking.
538    #[test]
539    fn out_of_range_spans_are_rejected() {
540        let mut bytes = Vec::new();
541        bytes.extend_from_slice(MAGIC);
542        bytes.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
543        bytes.extend_from_slice(&0u32.to_le_bytes()); // metadata
544        bytes.extend_from_slice(&1u32.to_le_bytes()); // one namespace
545        bytes.extend_from_slice(&2u32.to_le_bytes());
546        bytes.extend_from_slice(b"ns");
547        bytes.extend_from_slice(&1u32.to_le_bytes()); // one entry
548        bytes.extend_from_slice(&0u32.to_le_bytes()); // namespace id
549        bytes.extend_from_slice(&0u32.to_le_bytes()); // key offset
550        bytes.extend_from_slice(&99u32.to_le_bytes()); // key len, way past the end
551        bytes.extend_from_slice(&0u32.to_le_bytes()); // value offset
552        bytes.extend_from_slice(&0u32.to_le_bytes()); // value len
553
554        assert!(matches!(
555            EmbeddedBundle::open(Bytes::from_bytes_vec(bytes)).unwrap_err(),
556            EmbeddedBundleError::Corrupted(_)
557        ));
558    }
559
560    /// Lookups binary-search the index, so an unsorted or duplicated one would
561    /// answer misses rather than fail. It is caught at open instead.
562    #[test]
563    fn an_unsorted_entry_index_is_rejected() {
564        let bundle = |keys: [&[u8]; 2]| {
565            let mut data = Vec::new();
566            let mut index = Vec::new();
567            for key in keys {
568                index.extend_from_slice(&0u32.to_le_bytes()); // namespace id
569                index.extend_from_slice(&(data.len() as u32).to_le_bytes());
570                index.extend_from_slice(&(key.len() as u32).to_le_bytes());
571                data.extend_from_slice(key);
572                index.extend_from_slice(&(data.len() as u32).to_le_bytes());
573                index.extend_from_slice(&0u32.to_le_bytes()); // empty value
574            }
575
576            let mut bytes = Vec::new();
577            bytes.extend_from_slice(MAGIC);
578            bytes.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
579            bytes.extend_from_slice(&0u32.to_le_bytes()); // metadata
580            bytes.extend_from_slice(&1u32.to_le_bytes()); // one namespace
581            bytes.extend_from_slice(&2u32.to_le_bytes());
582            bytes.extend_from_slice(b"ns");
583            bytes.extend_from_slice(&2u32.to_le_bytes()); // two entries
584            bytes.extend_from_slice(&index);
585            bytes.extend_from_slice(&data);
586
587            EmbeddedBundle::open(Bytes::from_bytes_vec(bytes))
588        };
589
590        assert!(bundle([b"a", b"b"]).is_ok());
591        assert!(matches!(
592            bundle([b"b", b"a"]).unwrap_err(),
593            EmbeddedBundleError::Corrupted(_)
594        ));
595        assert!(matches!(
596            bundle([b"a", b"a"]).unwrap_err(),
597            EmbeddedBundleError::Corrupted(_)
598        ));
599    }
600
601    #[test]
602    fn an_unknown_namespace_id_is_rejected() {
603        let mut bytes = Vec::new();
604        bytes.extend_from_slice(MAGIC);
605        bytes.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
606        bytes.extend_from_slice(&0u32.to_le_bytes());
607        bytes.extend_from_slice(&0u32.to_le_bytes()); // no namespaces
608        bytes.extend_from_slice(&1u32.to_le_bytes()); // but one entry
609        bytes.extend_from_slice(&7u32.to_le_bytes()); // pointing at namespace 7
610        bytes.extend_from_slice(&0u32.to_le_bytes());
611        bytes.extend_from_slice(&0u32.to_le_bytes());
612        bytes.extend_from_slice(&0u32.to_le_bytes());
613        bytes.extend_from_slice(&0u32.to_le_bytes());
614
615        assert!(matches!(
616            EmbeddedBundle::open(Bytes::from_bytes_vec(bytes)).unwrap_err(),
617            EmbeddedBundleError::Corrupted(_)
618        ));
619    }
620}