// The CUBE store's format, in one file, used by both sides. // // `cube-store-raw` (and through it `cube-cli`, `cube-image`) and this kernel's driver have to agree // about the store's bytes to the byte: the digest line is diffed between a kernel-written image and // a userspace-written one, a listing taken through `cube(2)` is compared with one taken by // userspace, and a store the kernel folds has to be a store userspace can read. They were two // implementations of one format, and when v3 landed the kernel moved and userspace did not — an // hour of "unsupported-version" and a README's worth of careful duplication. // // Two build systems cannot share a crate: the kernel's Rust build compiles what is in its own // module tree, and a cargo crate is not that. So they share a *file*, instead: this one, which the // driver declares as a module and `crates/cube-format` includes by path. There is no copy to drift // from, which is the only arrangement that cannot go stale. // // Everything here is a pure function over slices: no allocation, no I/O, no `std`, no logging. The // kernel reads into buffers with its own allocator and userspace already has the whole image; what // they must agree on is the layout, and that is what lives here. // // The constants are `pub` because both sides need them (a reader computing a stride, a writer // sizing an image), and `#[allow(dead_code)]` is applied per item where one side does not use it — // an unused `pub` constant in a kernel module is a warning, and a warning-free build is how the // gates stay readable. /// Every image starts with these four bytes. pub const MAGIC: &[u8; 4] = b"CUBE"; /// The original format: magic, version, curve, then records until zero padding. pub const VERSION_V1: u8 = 1; /// The packed format: the same, plus the image's byte extent and record count. pub const VERSION_V2: u8 = 2; /// The addressed format: the same, plus a space table, a fixed-size index, and packed values. pub const VERSION_V3: u8 = 3; /// v3, plus a 16-bit class mask in each index entry, so a scan by flag is a seek rather than a /// walk. The mask is the flag substrate (DESIGN-flag-vocabularies.md): a raw `u16` whose bits are a /// vocabulary's business, written at put time and read by `CUBE_OP_FLAG_SCAN`. pub const VERSION_V4: u8 = 4; pub const HEADER_LEN_V1: usize = 6; pub const HEADER_LEN_V2: usize = 6 + 8 + 8; /// magic(4) version(1) curve(1) image_bytes(8) record_count(8) space_count(8) index_off(8) /// values_off(8). pub const HEADER_LEN_V3: usize = 4 + 1 + 1 + 8 + 8 + 8 + 8 + 8; pub const SPACE_ID_LEN: usize = 32; pub const RAW_KEY_LEN: usize = 24; /// The class mask's width: one `u16` per record. pub const FLAGS_LEN: usize = 2; /// A packed record's fixed part: `space | key | value_len(u64)`, with the value behind it. pub const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8; /// A v3 index entry: `key | value_off(u64) | value_len(u64)`. pub const INDEX_ENTRY: usize = RAW_KEY_LEN + 8 + 8; /// A v4 index entry: `key | flags(u16) | value_off(u64) | value_len(u64)`. pub const INDEX_ENTRY_V4: usize = RAW_KEY_LEN + FLAGS_LEN + 8 + 8; /// A v3 space-table row: `space | first index(u64) | records(u64)`. pub const SPACE_ENTRY: usize = SPACE_ID_LEN + 8 + 8; /// The log's framing, which the fold and the readers both parse. pub const WAL_MAGIC: &[u8; 4] = b"CUBW"; /// The original log entry: no class mask. pub const WAL_VERSION: u8 = 1; /// The flagged log entry: the entry carries a `u16` class mask before its length. pub const WAL_VERSION_V2: u8 = 2; pub const WAL_HEADER_LEN: usize = 6; /// `op(1) | crc(4) | space(32) | key(24) | len(4)`, with the value behind it. pub const ENTRY_FIXED: usize = 1 + 4 + SPACE_ID_LEN + RAW_KEY_LEN + 4; /// v2's entry: the same, with a `flags(2)` field before the length. pub const ENTRY_FIXED_V2: usize = 1 + 4 + SPACE_ID_LEN + RAW_KEY_LEN + FLAGS_LEN + 4; /// `CUBE_OP_PUT`: an entry that stores a value. pub const WAL_OP_WRITE: u8 = 1; /// `CUBE_OP_DEL`: an entry that removes one. pub const WAL_OP_DELETE: u8 = 2; /// What an image's header says, in any version. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Header { pub version: u8, pub curve: u8, /// The image's byte length, header included. `None` in v1, which states no extent. pub image_bytes: Option, /// How many records follow the header. `None` in v1. pub record_count: Option, } impl Header { /// Where the records start. pub fn records_off(&self) -> usize { if self.version == VERSION_V1 { HEADER_LEN_V1 } else if self.version == VERSION_V3 || self.version == VERSION_V4 { HEADER_LEN_V3 } else { HEADER_LEN_V2 } } /// The image's extent within `bytes`: what the header promises, or all of it for v1. pub fn extent(&self, bytes_len: usize) -> usize { match self.image_bytes { Some(n) => core::cmp::min(n as usize, bytes_len), None => bytes_len, } } } /// Why an image was refused. A reader that guesses at an extent can read somebody else's bytes, so /// these are all refusals rather than defaults. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Bad { /// Not a CUBE image at all. Magic, /// A version this build does not know. Version(u8), /// A header that does not fit, or an extent that does not cover its own header. Extent, /// A v3 table that does not sit where the header says it does. Tables, } /// Read and validate an image header. pub fn parse_header(bytes: &[u8]) -> Result { if bytes.len() < 5 || &bytes[0..4] != MAGIC { return Err(Bad::Magic); } let version = bytes[4]; let curve = bytes[5]; match version { VERSION_V1 => Ok(Header { version, curve, image_bytes: None, record_count: None, }), VERSION_V2 => { if bytes.len() < HEADER_LEN_V2 { return Err(Bad::Magic); } let image_bytes = le_u64(bytes, 6); let record_count = le_u64(bytes, 14); if (image_bytes as usize) < HEADER_LEN_V2 { return Err(Bad::Extent); } Ok(Header { version, curve, image_bytes: Some(image_bytes), record_count: Some(record_count), }) } VERSION_V3 | VERSION_V4 => { if bytes.len() < HEADER_LEN_V3 { return Err(Bad::Magic); } let geometry = V3::decode(bytes)?; Ok(Header { version, curve, image_bytes: Some(le_u64(bytes, 6)), record_count: Some(geometry.record_count), }) } other => Err(Bad::Version(other)), } } /// The v3 tables' geometry: where the index is, where the values start, and how many of each. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct V3 { /// The version this geometry belongs to: `VERSION_V3` or `VERSION_V4`, which differ only in /// the index entry's stride (the class mask adds two bytes). pub version: u8, pub record_count: u64, pub space_count: u64, pub index_off: u64, pub values_off: u64, } impl V3 { /// The width of one index entry for this geometry's version. pub fn index_stride(&self) -> usize { if self.version == VERSION_V4 { INDEX_ENTRY_V4 } else { INDEX_ENTRY } } /// Read the tables' geometry, refusing anything that does not add up. /// /// The three equalities here are the whole point of the format: a reader computes a record's /// place as `index_off + i * stride`, and that is only a place if the index really starts /// there and really is that wide. pub fn decode(bytes: &[u8]) -> Result { let version = bytes.get(4).copied().ok_or(Bad::Magic)?; if bytes.len() < HEADER_LEN_V3 || &bytes[0..4] != MAGIC || (version != VERSION_V3 && version != VERSION_V4) { return Err(Bad::Magic); } let geometry = V3 { version, record_count: le_u64(bytes, 14), space_count: le_u64(bytes, 22), index_off: le_u64(bytes, 30), values_off: le_u64(bytes, 38), }; let image_bytes = le_u64(bytes, 6); if image_bytes < HEADER_LEN_V3 as u64 { return Err(Bad::Extent); } let table_end = HEADER_LEN_V3 as u64 + geometry.space_count * SPACE_ENTRY as u64; let index_end = geometry.index_off + geometry.record_count * geometry.index_stride() as u64; if geometry.index_off != table_end || geometry.values_off != index_end || geometry.values_off > image_bytes { return Err(Bad::Tables); } Ok(geometry) } /// Write the header this geometry describes. `image_bytes` is filled in by the caller's /// arithmetic, since only it knows how long the values are. pub fn encode(&self, out: &mut [u8], curve: u8, image_bytes: u64) -> Result { if out.len() < HEADER_LEN_V3 { return Err(Bad::Extent); } out[0..4].copy_from_slice(MAGIC); out[4] = VERSION_V3; out[5] = curve; out[6..14].copy_from_slice(&image_bytes.to_le_bytes()); out[14..22].copy_from_slice(&self.record_count.to_le_bytes()); out[22..30].copy_from_slice(&self.space_count.to_le_bytes()); out[30..38].copy_from_slice(&self.index_off.to_le_bytes()); out[38..46].copy_from_slice(&self.values_off.to_le_bytes()); Ok(HEADER_LEN_V3) } /// One space-table row: the space, where its records start in the index, and how many. pub fn space_row<'a>(&self, bytes: &'a [u8], row: u64) -> Option<(&'a [u8; SPACE_ID_LEN], u64, u64)> { if row >= self.space_count { return None; } let at = HEADER_LEN_V3 + row as usize * SPACE_ENTRY; if at + SPACE_ENTRY > bytes.len() { return None; } let space: &[u8; SPACE_ID_LEN] = bytes[at..at + SPACE_ID_LEN].try_into().ok()?; Some(( space, le_u64(bytes, at + SPACE_ID_LEN), le_u64(bytes, at + SPACE_ID_LEN + 8), )) } /// One index entry, by its position in the index. pub fn index_entry<'a>(&self, bytes: &'a [u8], at: u64) -> Option> { if at >= self.record_count { return None; } let stride = self.index_stride(); let off = self.index_off as usize + at as usize * stride; if off + stride > bytes.len() { return None; } // The flag field exists only in v4; a v3 entry reads as a zero mask, which is honest — // "no class" — and matches nothing in a scan. let flags = if self.version == VERSION_V4 { le_u16(bytes, off + RAW_KEY_LEN) } else { 0 }; let vo = off + RAW_KEY_LEN + if self.version == VERSION_V4 { FLAGS_LEN } else { 0 }; let entry = IndexEntry { key: bytes[off..off + RAW_KEY_LEN].try_into().ok()?, flags, value_off: le_u64(bytes, vo), value_len: le_u64(bytes, vo + 8), }; // A value that is not inside the image is a truncated image, not an empty one. if entry.value_off < self.values_off || entry.value_off + entry.value_len > bytes.len() as u64 { return None; } Some(entry) } /// The value an index entry points at. pub fn value<'a>(&self, bytes: &'a [u8], entry: &IndexEntry<'_>) -> Option<&'a [u8]> { let at = entry.value_off as usize; let len = entry.value_len as usize; if at < self.values_off as usize || at + len > bytes.len() { return None; } Some(&bytes[at..at + len]) } } /// An addressed index entry: the key, its class mask, and where its value lies. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct IndexEntry<'a> { pub key: &'a [u8; RAW_KEY_LEN], /// The class mask (v4), or zero (v3, where no mask was written). pub flags: u16, pub value_off: u64, pub value_len: u64, } /// One packed record, and where the next one starts. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct PackedRecord<'a> { pub space: &'a [u8; SPACE_ID_LEN], pub key: &'a [u8; RAW_KEY_LEN], pub value: &'a [u8], /// The offset of the record after this one. pub next: usize, } /// Read one packed record at `off`, if there is a whole one before `end`. /// /// This is the v1/v2 layout's only parsing rule, and it is shared for the same reason the constants /// are: a reader that disagrees about a frame's length reads every following record wrong. pub fn packed_record(bytes: &[u8], off: usize, end: usize) -> Option> { if off + RECORD_FIXED > end || end > bytes.len() { return None; } let frame = &bytes[off..off + RECORD_FIXED]; let value_len = le_u64(frame, SPACE_ID_LEN + RAW_KEY_LEN) as usize; let value_at = off + RECORD_FIXED; if value_at + value_len > end { return None; } Some(PackedRecord { space: frame[..SPACE_ID_LEN].try_into().ok()?, key: frame[SPACE_ID_LEN..SPACE_ID_LEN + RAW_KEY_LEN].try_into().ok()?, value: &bytes[value_at..value_at + value_len], next: value_at + value_len, }) } /// One log entry, and where the next one starts. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct WalEntry<'a> { pub space: &'a [u8; SPACE_ID_LEN], pub key: &'a [u8; RAW_KEY_LEN], pub op: u8, /// The class mask (WAL version 2), or zero (version 1, where no mask was written). pub flags: u16, pub value: &'a [u8], pub next: usize, } /// Read one log entry at `off`. The log slice includes its header, so the version at `log[4]` /// decides the entry's stride: version 2 carries a two-byte class mask before the length. /// /// The checksum covers space, key, the mask, length and value, so a torn tail is stopped at rather /// than applied — the rule the fold and every reader share. pub fn wal_entry(log: &[u8], off: usize) -> Option> { let version = log.get(4).copied().unwrap_or(WAL_VERSION); let (fixed, len_at) = if version == WAL_VERSION_V2 { (ENTRY_FIXED_V2, RAW_KEY_LEN + FLAGS_LEN + SPACE_ID_LEN + 5) } else { (ENTRY_FIXED, 61) }; if off + fixed > log.len() { return None; } let op = log[off]; if op != WAL_OP_WRITE && op != WAL_OP_DELETE { return None; } let crc = le_u32(log, off + 1); let len = le_u32(log, off + len_at) as usize; let frame_end = off + fixed + len; if frame_end > log.len() { return None; } if crc32(&log[off + 5..frame_end]) != crc { return None; } let flags = if version == WAL_VERSION_V2 { le_u16(log, off + SPACE_ID_LEN + RAW_KEY_LEN + 5) } else { 0 }; Some(WalEntry { space: log[off + 5..off + 37].try_into().ok()?, key: log[off + 37..off + 61].try_into().ok()?, op, flags, value: &log[off + fixed..frame_end], next: frame_end, }) } /// A little-endian `u64` at `at`, or zero if it does not fit — callers bound-check first, and the /// alternative is a panic in a reader that exists to refuse rather than guess. pub fn le_u64(bytes: &[u8], at: usize) -> u64 { let mut w = [0u8; 8]; if at + 8 <= bytes.len() { w.copy_from_slice(&bytes[at..at + 8]); } u64::from_le_bytes(w) } /// A little-endian `u32` at `at`, with the same rule. pub fn le_u32(bytes: &[u8], at: usize) -> u32 { let mut w = [0u8; 4]; if at + 4 <= bytes.len() { w.copy_from_slice(&bytes[at..at + 4]); } u32::from_le_bytes(w) } /// A little-endian `u16` at `at`, with the same rule. pub fn le_u16(bytes: &[u8], at: usize) -> u16 { let mut w = [0u8; 2]; if at + 2 <= bytes.len() { w.copy_from_slice(&bytes[at..at + 2]); } u16::from_le_bytes(w) } /// CRC-32 (IEEE 802.3), bitwise. /// /// A corruption check, not a security check: it catches a torn write or a flipped bit, and says /// nothing about whether anyone tampered with the bytes — that is `cube-crypt`'s job, and it belongs /// on the record rather than on the framing. pub fn crc32(bytes: &[u8]) -> u32 { let mut crc = 0xFFFF_FFFFu32; for byte in bytes { crc ^= *byte as u32; for _ in 0..8 { let mask = (crc & 1).wrapping_neg(); crc = (crc >> 1) ^ (0xEDB8_8320 & mask); } } !crc } /// The store's digest hash: FNV-1a, 64-bit. pub const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; /// Fold more bytes into an FNV-1a hash. pub fn fnv1a64(bytes: &[u8], mut h: u64) -> u64 { for byte in bytes { h ^= *byte as u64; h = h.wrapping_mul(0x0000_0100_0000_01b3); } h } /// The digest line, as both sides print it. /// /// `bytes` is the records' logical size — the same number whatever layout holds them — because this /// line is what a kernel-written store and a userspace-written one are compared by, and a figure /// that changed with the layout would make one store look like two. pub struct Digest { pub version: u8, pub curve: u8, pub records: u64, pub value_bytes: u64, pub hash: u64, pub errors: u64, } impl Digest { /// The logical size of the store these records describe. pub fn logical_bytes(records: u64, value_bytes: u64) -> u64 { records * RECORD_FIXED as u64 + value_bytes } /// Fold one record in. pub fn add(&mut self, space: &[u8], key: &[u8], value: &[u8]) { self.hash = fnv1a64(space, self.hash); self.hash = fnv1a64(key, self.hash); self.hash = fnv1a64(&(value.len() as u64).to_le_bytes(), self.hash); self.hash = fnv1a64(value, self.hash); self.records += 1; self.value_bytes += value.len() as u64; } pub fn new(version: u8, curve: u8) -> Self { Digest { version, curve, records: 0, value_bytes: 0, hash: FNV_OFFSET, errors: 0, } } } // ── Where a coordinate's key sits, and what span a box covers ────────────────────────── // // The key is a three-axis bit interleave: axis `x` owns bit `3i`, `y` owns `3i+1`, `z` owns // `3i+2`, and the 24-byte key is that 192-bit number written big-endian — so comparing two keys // byte-wise from the front is comparing the interleaved integers, and `key_cmp` is the comparison // a sorted index performs. // // This lives in the shared file rather than in the driver because it is the one piece of the // addressing that the two sides must agree on byte for byte: the driver computes a key to look a // record up, and userspace computes the same key to lay an image out. It was written twice before // (`morton_encode` in `cubelinux_store.rs` and the curve in `cube-core`) with the agreement held // by the gates rather than by the compiler; now the kernel's copy is this one. /// The key a coordinate has. /// /// `x` at bit `3i`, `y` at `3i+1`, `z` at `3i+2`, for `i` in `0..64`, big-endian into 24 bytes. /// The correspondence is monotone in every axis — if `a <= a'` on all three then `key(a) <= key(a')` /// — which is what makes [`key_span`] a sound bound rather than a guess. pub fn morton_key(x: u64, y: u64, z: u64) -> [u8; RAW_KEY_LEN] { let mut k = [0u8; RAW_KEY_LEN]; let mut i = 0; while i < 64 { let mut axis = 0; while axis < 3 { let value = match axis { 0 => x, 1 => y, _ => z, }; if (value >> i) & 1 != 0 { let n = 3 * i + axis; k[RAW_KEY_LEN - 1 - (n / 8)] |= 1 << (n % 8); } axis += 1; } i += 1; } k } /// The point a key encodes — the inverse of [`morton_key`]. /// /// Bit `n` of the key is bit `n / 3` of axis `n % 3`, which is the interleave read backwards. /// /// Why the kernel needs it: a region walk is handed a **key span**, not a set of keys. A span is a /// bound, not the set — keys of points *outside* the box also fall inside it (Z-order /// amplification) — so every candidate found in the span has to be turned back into a point and /// tested for membership. Without this, the walk would return records that are outside the region /// the caller asked for, which is exactly the mistake the span's own doc warns about. pub fn morton_decode(k: &[u8; RAW_KEY_LEN]) -> [u64; 3] { let mut out = [0u64; 3]; let mut n = 0; while n < RAW_KEY_LEN * 8 { if k[RAW_KEY_LEN - 1 - (n / 8)] & (1 << (n % 8)) != 0 { out[n % 3] |= 1u64 << (n / 3); } n += 1; } out } /// Compare two keys as the 192-bit numbers they are. `a < b` means `a` sorts first. pub fn key_cmp(a: &[u8; RAW_KEY_LEN], b: &[u8; RAW_KEY_LEN]) -> core::cmp::Ordering { let mut i = 0; while i < RAW_KEY_LEN { if a[i] != b[i] { return if a[i] < b[i] { core::cmp::Ordering::Less } else { core::cmp::Ordering::Greater }; } i += 1; } core::cmp::Ordering::Equal } /// The key span a box covers: `(key(lo), key(hi))`, inclusive. /// /// **This is the bound a seek needs, and it needs no decomposition.** Because the interleave is /// monotone, every point in the box has a key between these two — so a sorted index can be /// binary-searched for the lower one, scanned forward while the key stays under the upper one, and /// each candidate tested for membership. Records may be found in that span that are *not* in the /// box (that is the classic Z-order amplification, and how much depends on the box), but no record /// in the box is outside the span. What is *not* true — and was believed here until it was checked /// — is that the box is one contiguous run: see [`aligned_box_is_one_run`]. The span being sound /// does not make it tight, and confusing the two is how an aligned box of three runs gets /// documented as one. pub fn key_span(lo: [u64; 3], hi: [u64; 3]) -> ([u8; RAW_KEY_LEN], [u8; RAW_KEY_LEN]) { ( morton_key(lo[0], lo[1], lo[2]), morton_key(hi[0], hi[1], hi[2]), ) } /// Whether a power-of-two-aligned box with these per-axis sizes is exactly **one** contiguous run /// of the key. /// /// The condition, derived and then checked against the keys themselves on every properly-aligned /// box with sizes in `{1,2,4,8,16}` per axis and origins swept to 16 — 4,805 boxes, no /// disagreement (pinned in `crates/cube-format`'s tests and in `cube-store`'s): /// /// > `max(k) - min(k) <= 1` **and** the axes at the top level form a prefix of `(x, y, z)` in that /// > order, where `2^k` is the size on an axis. /// /// The free bit positions of such a box are `{3i + a : i < k_a}`, and that set is a prefix of /// `{0,1,2,…}` only when the levels are complete except possibly the last — so a cube qualifies, a /// cube doubled along `x` qualifies, a cube doubled along `x` and `y` qualifies, and **doubling `z` /// alone, or `x` and `z` together, does not**, because `z` cannot be free at a level where `y` is /// not. The asymmetry belongs to the interleave; it is why the property looked plausible. /// /// Sizes are taken as given: a size that is not a power of two is not an aligned box's size, and is /// answered `false` rather than rounded, because a caller that passes one has a bug and the useful /// answer is "no", not a guess. pub fn aligned_box_is_one_run(sx: u64, sy: u64, sz: u64) -> bool { fn k_of(s: u64) -> Option { if s == 0 || !s.is_power_of_two() { return None; } Some(s.trailing_zeros()) } let (kx, ky, kz) = match (k_of(sx), k_of(sy), k_of(sz)) { (Some(a), Some(b), Some(c)) => (a, b, c), _ => return false, }; let (lo, hi) = ( kx.min(ky).min(kz), kx.max(ky).max(kz), ); if hi - lo > 1 { return false; } // The axes at the top level, in the order the interleave gives them: x, then y, then z. let mut top = [false; 3]; if kx == hi { top[0] = true; } if ky == hi { top[1] = true; } if kz == hi { top[2] = true; } // A prefix: x before y before z, with no gaps. if top[2] && !top[1] { return false; } if top[1] && !top[0] { return false; } top[0] }