From 1db3abce9352d6bd6c944b73b9a6925b8692b3ea Mon Sep 17 00:00:00 2001 From: surface-camera-build Date: Mon, 21 Sep 2026 02:58:03 -0400 Subject: [PATCH] =?UTF-8?q?cubelinux:=20v3=20=E2=80=94=20the=20image=20car?= =?UTF-8?q?ries=20the=20addresses,=20so=20a=20coordinate=20is=20a=20place?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A packed list (`space | key | len | value`, repeated) cannot answer "where is this coordinate": record N's offset is the sum of every record before it. The key gives ORDER, and order is not an address, so no coordinate could be turned into a place and not even a binary search was possible — the middle record's offset is just as unknowable. Every lookup walked, every batch of a listing walked again, and a read of one record cost as much as the store (measured: 110 ms per get, 119 ms per `spaces`, 1,206 ms for a 6,127-record listing). v3 puts the addresses in the image: [header 46] magic, version, curve, extents, counts [space table: space_count x 48] space | first index | records [index: record_count x 40] key | value offset | value length [values: packed, in index order] A lookup is now a binary search over arithmetic addresses (`index_off + i * 40`); a listing starts at its space's first index entry and streams, reading a span of values per batch; asking which spaces exist is the space table; a spatial range is a contiguous run of index entries. An index entry is 40 bytes against the packed frame's 64, so the image also gets smaller. Measured by `verify-enum-cost.sh` between a 500-record store and a 20,000-record one: listing the same 10-record space: 3.22 ms -> 3.27 ms (1.0x, store grew 40x) asking which spaces exist: 3.25 ms -> 3.08 ms (0.9x) reading a coordinate that is there: 1.07 ms -> 1.25 ms (1.2x) reading one that is not: 1.19 ms -> 1.22 ms (1.0x) Nothing scales with the store any more, which is the premise: knowing a coordinate is what lets you reach it. Two bugs found on the way, both of the kind only a real image shows. The index was written with a 4-byte value length (`Entry` carries it in a u32) while the header's arithmetic and both readers assumed 8 — every entry four bytes short, the last of them overlapping the values. And the digest's `bytes` figure added the layout's own header length, so two layouts of one store digested differently; it is now the records' logical size, which is the same number either way. Verified: verify-kernel-append.sh, verify-kernel-checkpoint.sh, verify-enum.sh, verify-enum-cost.sh and verify-frontend.sh all pass, and the workspace tests pass — including a new fixture test in cube-store-raw that reads a 700-byte image the kernel itself wrote, because a hand-built image cannot catch a misunderstanding shared by the builder. --- drivers/cube/cubelinux_store.rs | 588 +++++++++++++++++++++++++++++++- 1 file changed, 572 insertions(+), 16 deletions(-) diff --git a/drivers/cube/cubelinux_store.rs b/drivers/cube/cubelinux_store.rs index 64ec62f6a..a8f837722 100644 --- a/drivers/cube/cubelinux_store.rs +++ b/drivers/cube/cubelinux_store.rs @@ -67,10 +67,38 @@ module! { const MAGIC: &[u8; 4] = b"CUBE"; /// The original format: magic, version, curve, then records until zero padding. const VERSION_V1: u8 = 1; -/// The current format: the same, plus the image's byte extent and record count. +/// The packed format: the same, plus the image's byte extent and record count. const VERSION: u8 = 2; +/// The addressed format: the same, plus a fixed-size index, a space table, and packed values. +/// +/// v2 is a packed list — `space | key | len | value`, repeated — so record N's offset is the sum of +/// every record before it. A coordinate gives the key, and the key gives ORDER, and order is not an +/// address: no coordinate can be turned into a place, and not even a binary search is possible, +/// because the middle record's offset cannot be computed either. Every lookup, therefore, walks. +/// +/// v3 puts the addresses in the image: +/// +/// ```text +/// [header 46] magic, version, curve, extents, counts +/// [space table: space_count x 48] space | first index | records +/// [index: record_count x 40] key | value offset | value length +/// [values: packed, in index order] +/// ``` +/// +/// The index is fixed-size and sorted by key, so a lookup is a binary search over arithmetic +/// addresses (`index_off + i * 40`), a listing starts at its space's first index entry and streams, +/// asking which spaces exist is the space table, and a spatial range is a contiguous run of index +/// entries. An index entry is 40 bytes against v2's 64-byte frame, so the image also gets smaller. +const VERSION_V3: u8 = 3; const HEADER_LEN_V1: usize = 6; 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). +const HEADER_LEN_V3: usize = 4 + 1 + 1 + 8 + 8 + 8 + 8 + 8; +/// `key(24) | value_off(8) | value_len(8)`. +const INDEX_ENTRY: usize = RAW_KEY_LEN + 8 + 8; +/// `space(32) | first index(8) | records(8)`. +const SPACE_ENTRY: usize = SPACE_ID_LEN + 8 + 8; const SPACE_ID_LEN: usize = 32; const RAW_KEY_LEN: usize = 24; const RECORD_FIXED: usize = SPACE_ID_LEN + RAW_KEY_LEN + 8; @@ -309,6 +337,60 @@ fn parse_header(image: &[u8]) -> Result { } } +/// The v3 header: what a reader needs to address the image by arithmetic. +struct HeaderV3 { + image_bytes: u64, + record_count: u64, + space_count: u64, + index_off: u64, + values_off: u64, +} + +impl HeaderV3 { + fn encode(&self, out: &mut KVVec, curve: u8) -> Result<(), AllocError> { + out.extend_from_slice(MAGIC, GFP_KERNEL)?; + out.extend_from_slice(&[VERSION_V3, curve], GFP_KERNEL)?; + out.extend_from_slice(&self.image_bytes.to_le_bytes(), GFP_KERNEL)?; + out.extend_from_slice(&self.record_count.to_le_bytes(), GFP_KERNEL)?; + out.extend_from_slice(&self.space_count.to_le_bytes(), GFP_KERNEL)?; + out.extend_from_slice(&self.index_off.to_le_bytes(), GFP_KERNEL)?; + out.extend_from_slice(&self.values_off.to_le_bytes(), GFP_KERNEL)?; + Ok(()) + } + + fn decode(image: &[u8]) -> Result { + if image.len() < HEADER_LEN_V3 || &image[0..4] != MAGIC || image[4] != VERSION_V3 { + return Err("not-a-v3-header"); + } + let word = |at: usize| -> u64 { + let mut w = [0u8; 8]; + w.copy_from_slice(&image[at..at + 8]); + u64::from_le_bytes(w) + }; + let header = HeaderV3 { + image_bytes: word(6), + record_count: word(14), + space_count: word(22), + index_off: word(30), + values_off: word(38), + }; + // Every extent inside the image and in order, or a reader would take somebody else's bytes + // for a record — the same rule the v2 header is held to. + if header.image_bytes < HEADER_LEN_V3 as u64 { + return Err("bad-extent"); + } + let table_end = HEADER_LEN_V3 as u64 + header.space_count * SPACE_ENTRY as u64; + let index_end = header.index_off + header.record_count * INDEX_ENTRY as u64; + if header.index_off != table_end || header.values_off != index_end { + return Err("bad-tables"); + } + if header.values_off > header.image_bytes { + return Err("tables-past-the-image"); + } + Ok(header) + } +} + fn digest(image: &[u8]) -> Line { let mut line = Line::new(); @@ -424,7 +506,10 @@ fn digest(image: &[u8]) -> Line { // records, whatever padding the reader happened to see. Reporting what was read made // the same records disagree between a file and a device — and the gates compare this // line, so it has to mean the same thing on both sides. - header_len + count * RECORD_FIXED as u64 + value_bytes, + // The records' logical size, and not the layout's: a v2 image and a v3 image of one + // store hold the same records in different shapes, and this line is how the two are + // compared — a figure that changes with the layout would make them look different. + count * RECORD_FIXED as u64 + value_bytes, count, value_bytes, h, @@ -853,9 +938,10 @@ fn merged_digest(image: &[u8], log: &[u8], header: &Header) -> Result Result, AllocErr merged.sort_entries(); let entries = merged.entries.as_slice(); + // The winners first: one live record per coordinate, in the store's own (space, key) order. let mut winners = KVVec::::new(); let mut values_len: u64 = 0; let mut i = 0; @@ -1127,20 +1214,57 @@ fn serialize_image(merged: &mut Merged, curve: u8) -> Result, AllocErr i = last + 1; } - let image_bytes = HEADER_LEN_V2 as u64 + winners.len() as u64 * RECORD_FIXED as u64 + values_len; - let mut out = KVVec::::with_capacity(image_bytes as usize, GFP_KERNEL)?; - out.extend_from_slice(MAGIC, GFP_KERNEL)?; - out.extend_from_slice(&[VERSION, curve], GFP_KERNEL)?; - out.extend_from_slice(&image_bytes.to_le_bytes(), GFP_KERNEL)?; - out.extend_from_slice(&(winners.len() as u64).to_le_bytes(), GFP_KERNEL)?; + // The space table: where each space's records begin in the index, and how many. It comes from + // the same order the index is written in, so a walk of the store is a walk of this table. + let mut spaces = KVVec::<([u8; SPACE_ID_LEN], u64, u64)>::new(); + for (at, w) in winners.as_slice().iter().enumerate() { + let space = merged.entries.as_slice()[*w as usize].space; + match spaces.as_mut_slice().last_mut() { + Some((last_space, _, records)) if *last_space == space => *records += 1, + _ => spaces.push((space, at as u64, 1), GFP_KERNEL)?, + } + } + let header = HeaderV3 { + record_count: winners.len() as u64, + space_count: spaces.len() as u64, + index_off: (HEADER_LEN_V3 + spaces.len() * SPACE_ENTRY) as u64, + values_off: (HEADER_LEN_V3 + spaces.len() * SPACE_ENTRY + winners.len() * INDEX_ENTRY) as u64, + image_bytes: 0, + }; + let image_bytes = header.values_off + values_len; + + let mut out = KVVec::::with_capacity(image_bytes as usize, GFP_KERNEL)?; + let mut header = header; + header.image_bytes = image_bytes; + header.encode(&mut out, curve)?; + + for (space, first, records) in spaces.as_slice() { + out.extend_from_slice(space, GFP_KERNEL)?; + out.extend_from_slice(&first.to_le_bytes(), GFP_KERNEL)?; + out.extend_from_slice(&records.to_le_bytes(), GFP_KERNEL)?; + } + + // The index, whose entries are the addresses: a fixed stride is what makes `index_off + + // i * INDEX_ENTRY` a place a reader can go to without reading anything before it. + let mut value_at = header.values_off; for w in winners.as_slice() { let e = &merged.entries.as_slice()[*w as usize]; - let value = merged.value(e); - out.extend_from_slice(&e.space, GFP_KERNEL)?; out.extend_from_slice(&e.key, GFP_KERNEL)?; - out.extend_from_slice(&(value.len() as u64).to_le_bytes(), GFP_KERNEL)?; - out.extend_from_slice(value, GFP_KERNEL)?; + out.extend_from_slice(&value_at.to_le_bytes(), GFP_KERNEL)?; + // The length is written as 8 bytes even though `Entry` carries it in 4: an index entry + // is 40 bytes and the header's `values_off` is computed from that, so a 4-byte field here + // shortens every entry by four and leaves the last ones overlapping the values. Both + // readers assume the stride the header declares, which is the point of a fixed stride. + out.extend_from_slice(&(e.value_len as u64).to_le_bytes(), GFP_KERNEL)?; + value_at += e.value_len as u64; + } + + // The values, packed in index order: a batch of records is one span of this, which is why a + // listing can read what it returns in one go rather than record by record. + for w in winners.as_slice() { + let e = &merged.entries.as_slice()[*w as usize]; + out.extend_from_slice(merged.value(e), GFP_KERNEL)?; } Ok(out) } @@ -1995,7 +2119,419 @@ impl Batch<'_> { } } -/// `CUBE_OP_GET`: read a coordinate. +// ── v3: the addressed reader ──────────────────────────────────────────────────────────── +// +// A v3 image is read at the offsets the header describes, so an operation reads the bytes it names +// and nothing else. This is what the coordinate finally buys: a lookup is a binary search over a +// fixed stride, a listing starts where its space starts, and neither reads the store to find out. + +/// A v3 image open for reading, with the few bytes an operation needs read on demand. +struct Addressed { + file: *mut bindings::file, + /// Where the image begins in the device. + image_off: u64, + header: HeaderV3, + /// The log window, which is what can override the image and is small. + log: KVVec, + /// Reused for every span read, so a lookup does not allocate per probe. + scratch: KVVec, +} + +impl Addressed { + /// Open the store and read its control block, v3 header, and log window — not its records. + fn open() -> Result> { + // SAFETY: `store_device()` is a NUL-terminated C string the module parameter filled at + // boot; filp_open returns a valid `struct file *` or an error pointer, checked below. + let file = unsafe { bindings::filp_open(store_device(), 0, 0) }; + let file = kernel::error::from_err_ptr(file)?; + if file.is_null() { + return Err(EINVAL); + } + let mut opened = Addressed { + file, + image_off: 0, + header: HeaderV3 { + image_bytes: 0, + record_count: 0, + space_count: 0, + index_off: 0, + values_off: 0, + }, + log: KVVec::new(), + scratch: KVVec::new(), + }; + opened.scratch = KVVec::::with_capacity(4096, GFP_KERNEL)?; + opened.scratch.resize(4096, 0, GFP_KERNEL)?; + + let mut head = KVVec::::with_capacity(CTL_COPY_B + CTL_COPY_LEN, GFP_KERNEL)?; + read_exact_at(file, 0, CTL_COPY_B + CTL_COPY_LEN, &mut head, &mut opened.scratch)?; + if &head.as_slice()[0..4] != CTL_MAGIC.as_slice() { + return Ok(None); // a bare image, which the walking reader handles + } + let control = read_control(head.as_slice()).ok_or(EINVAL)?; + opened.image_off = control.image_off() as u64; + + let mut raw = KVVec::::with_capacity(HEADER_LEN_V3, GFP_KERNEL)?; + read_exact_at(file, opened.image_off, HEADER_LEN_V3, &mut raw, &mut opened.scratch)?; + if raw.as_slice()[4] != VERSION_V3 { + return Ok(None); // v1 or v2: the walking reader + } + opened.header = HeaderV3::decode(raw.as_slice()).map_err(|what| { + pr_err!("cubelinux: {}\n", what); + EINVAL + })?; + + if control.log_used > 0 { + let len = core::cmp::min(WAL_HEADER_LEN + control.log_used as usize, MAX_BYTES); + read_exact_at(file, control.log_off() as u64, len, &mut opened.log, &mut opened.scratch)?; + } + Ok(Some(opened)) + } + + /// Read `len` bytes at an offset *within the image*. + fn read_image_at(&mut self, off: u64, len: usize, out: &mut KVVec) -> Result<()> { + if off + len as u64 > self.header.image_bytes { + return Err(EINVAL); + } + read_exact_at(self.file, self.image_off + off, len, out, &mut self.scratch) + } + + /// The space table entry for a space: where its records start in the index, and how many. + /// + /// The table is sorted by space and fixed-size, so this is a binary search over arithmetic + /// addresses — the same reason the index works. + fn space_entry(&mut self, space: &[u8; SPACE_ID_LEN]) -> Result> { + let mut buf = KVVec::::new(); + let mut lo = 0u64; + let mut hi = self.header.space_count; + while lo < hi { + let mid = lo + (hi - lo) / 2; + let at = HEADER_LEN_V3 as u64 + mid * SPACE_ENTRY as u64; + self.read_image_at(at, SPACE_ENTRY, &mut buf)?; + let entry = buf.as_slice(); + let found = &entry[..SPACE_ID_LEN]; + if found < &space[..] { + lo = mid + 1; + } else if found > &space[..] { + hi = mid; + } else { + let mut w = [0u8; 8]; + w.copy_from_slice(&entry[SPACE_ID_LEN..SPACE_ID_LEN + 8]); + let first = u64::from_le_bytes(w); + w.copy_from_slice(&entry[SPACE_ID_LEN + 8..SPACE_ENTRY]); + return Ok(Some((first, u64::from_le_bytes(w)))); + } + } + Ok(None) + } + + /// One index entry, by its position in the index. + fn index_entry(&mut self, at: u64, buf: &mut KVVec) -> Result<(u64, u64)> { + let off = self.header.index_off + at * INDEX_ENTRY as u64; + self.read_image_at(off, INDEX_ENTRY, buf)?; + let entry = buf.as_slice(); + let mut w = [0u8; 8]; + w.copy_from_slice(&entry[RAW_KEY_LEN..RAW_KEY_LEN + 8]); + let value_off = u64::from_le_bytes(w); + w.copy_from_slice(&entry[RAW_KEY_LEN + 8..INDEX_ENTRY]); + Ok((value_off, u64::from_le_bytes(w))) + } + + /// The address of `key` inside a space's index range, or nothing if the space does not hold it. + fn find(&mut self, key: &[u8; RAW_KEY_LEN], first: u64, records: u64) -> Result> { + let mut buf = KVVec::::new(); + let mut lo = 0u64; + let mut hi = records; + while lo < hi { + let mid = lo + (hi - lo) / 2; + let off = self.header.index_off + (first + mid) * INDEX_ENTRY as u64; + self.read_image_at(off, INDEX_ENTRY, &mut buf)?; + let found = &buf.as_slice()[..RAW_KEY_LEN]; + if found < &key[..] { + lo = mid + 1; + } else if found > &key[..] { + hi = mid; + } else { + return self.index_entry(first + mid, &mut buf).map(Some); + } + } + Ok(None) + } + + /// The value of an index entry, read from where the index says it is. + fn value_at(&mut self, value_off: u64, value_len: u64) -> Result> { + let mut out = KVVec::::new(); + self.read_image_at(value_off, value_len as usize, &mut out)?; + Ok(out) + } + + /// Write the log's window back into `buf` — used to hand the log to the merge helpers, which + /// take a slice of a log that has already been read. + fn log(&self) -> &[u8] { + self.log.as_slice() + } +} + +impl Drop for Addressed { + fn drop(&mut self) { + // SAFETY: `self.file` came from filp_open and is closed exactly once, here; the owner + // argument is only meaningful for locks that nobody holds. + unsafe { bindings::filp_close(self.file, core::ptr::null_mut()) }; + } +} + +/// `CUBE_OP_GET` against a v3 image: the log's newest word, else a binary search. +/// +/// # Safety +/// `buf` must hold `len` writable bytes. +unsafe fn v3_get(mut image: Addressed, sp: [u8; SPACE_ID_LEN], key: [u8; RAW_KEY_LEN], buf: *mut u8, len: usize) -> isize { + // The log is owned for the duration: what it says borrows it, and reading the image needs + // `&mut image`. It is what a checkpoint has not folded, so it is small. + let mut log = KVVec::::new(); + if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() { + return -12; // -ENOMEM + } + match log_effect(log.as_slice(), &sp, &key) { + Some(Effect::Delete) => return -2, // -ENOENT + Some(Effect::Write(value)) => return copy_out(value, buf, len), + None => {} + } + let (first, records) = match image.space_entry(&sp) { + Ok(Some(entry)) => entry, + Ok(None) => return -2, + Err(e) => return -(e.to_errno() as isize), + }; + let (value_off, value_len) = match image.find(&key, first, records) { + Ok(Some(entry)) => entry, + Ok(None) => return -2, + Err(e) => return -(e.to_errno() as isize), + }; + let value = match image.value_at(value_off, value_len) { + Ok(v) => v, + Err(e) => return -(e.to_errno() as isize), + }; + copy_out(value.as_slice(), buf, len) +} + +/// `CUBE_OP_SPACES` against a v3 image: the space table, and whatever only the log writes. +/// +/// # Safety +/// `space_out` must point to 32 writable bytes. +unsafe fn v3_spaces(mut image: Addressed, cursor: u64, space_out: *mut u8) -> i32 { + let mut log_only = KVVec::<[u8; SPACE_ID_LEN]>::new(); + if log_spaces(image.log(), &mut log_only).is_err() { + return -12; + } + + // The table is sorted and fixed-size: the cursor is an index into the *store's* spaces, which + // is the table's spaces merged with the log's. Reading the table a row at a time is a few dozen + // reads of 48 bytes, and it keeps the merge in one order. + let mut index: u64 = 0; + let mut log_at = 0usize; + let mut buf = KVVec::::new(); + let mut row: u64 = 0; + while row < image.header.space_count { + if image.read_image_at(HEADER_LEN_V3 as u64 + row * SPACE_ENTRY as u64, SPACE_ENTRY, &mut buf).is_err() { + return -12; + } + let mut space = [0u8; SPACE_ID_LEN]; + space.copy_from_slice(&buf.as_slice()[..SPACE_ID_LEN]); + while log_at < log_only.len() && log_only.as_slice()[log_at] < space { + let candidate = log_only.as_slice()[log_at]; + log_at += 1; + if !space_has_records_v3(&mut image, &candidate) { + continue; + } + if index == cursor { + // SAFETY: the caller guarantees 32 writable bytes at `space_out`. + unsafe { core::ptr::copy_nonoverlapping(candidate.as_ptr(), space_out, SPACE_ID_LEN) }; + return 0; + } + index += 1; + } + if log_at < log_only.len() && log_only.as_slice()[log_at] == space { + log_at += 1; + } + row += 1; + if !space_has_records_v3(&mut image, &space) { + continue; + } + if index == cursor { + // SAFETY: as above. + unsafe { core::ptr::copy_nonoverlapping(space.as_ptr(), space_out, SPACE_ID_LEN) }; + return 0; + } + index += 1; + } + while log_at < log_only.len() { + let candidate = log_only.as_slice()[log_at]; + log_at += 1; + if !space_has_records_v3(&mut image, &candidate) { + continue; + } + if index == cursor { + // SAFETY: as above. + unsafe { core::ptr::copy_nonoverlapping(candidate.as_ptr(), space_out, SPACE_ID_LEN) }; + return 0; + } + index += 1; + } + -2 // -ENOENT: no such space; the walk is finished +} + +/// Does a v3 image's space hold a live record? One index entry, not a listing. +fn space_has_records_v3(image: &mut Addressed, space: &[u8; SPACE_ID_LEN]) -> bool { + match image.space_entry(space) { + Ok(Some((_, records))) => records > 0, + _ => false, + } +} + +/// `CUBE_OP_ENUM` against a v3 image. +/// +/// With no log edits for the space — the ordinary case, and always the case just after a +/// checkpoint — the cursor is a count of records already returned and the index is fixed-size, so +/// the batch starts at `first + cursor`: arithmetic, and the cost is the records returned. When the +/// log does hold edits, they interleave with the image's records and the walk has to merge them, so +/// it streams the index from the space's start and counts. +/// +/// # Safety +/// `buf` must hold `cap` writable bytes; `out_len` and `out_cursor` must point to writable `u64`s. +unsafe fn v3_enum( + mut image: Addressed, + wanted: [u8; SPACE_ID_LEN], + cursor: u64, + buf: *mut u8, + cap: usize, + out_len: *mut u64, + out_cursor: *mut u64, +) -> i32 { + let (first, records) = match image.space_entry(&wanted) { + Ok(Some(entry)) => entry, + Ok(None) => (0, 0), + Err(e) => return -(e.to_errno() as i32), + }; + // Own the log: the edits borrow it, and walking the image needs `&mut image`. + let mut log = KVVec::::new(); + if log.extend_from_slice(image.log(), GFP_KERNEL).is_err() { + return -12; // -ENOMEM + } + let mut edits = KVVec::>::new(); + if log_edits(log.as_slice(), &wanted, &mut edits).is_err() { + return -12; + } + + // SAFETY: the shim guarantees `cap` writable bytes at `buf`. + let out = unsafe { core::slice::from_raw_parts_mut(buf, cap) }; + let mut batch = Batch { + out, + written: 0, + returned: 0, + seen: cursor, + cursor, + too_big: 0, + }; + + if edits.is_empty() { + // Straight through the index, from the record the cursor names, reading a page of index + // entries at a time and then the span of values they point at. + let mut entry = KVVec::::new(); + let mut at = first + cursor; + while at < first + records { + let (value_off, value_len) = match image.index_entry(at, &mut entry) { + Ok(pair) => pair, + Err(e) => return -(e.to_errno() as i32), + }; + let value = match image.value_at(value_off, value_len) { + Ok(v) => v, + Err(e) => return -(e.to_errno() as i32), + }; + let key: &[u8; RAW_KEY_LEN] = match entry.as_slice()[..RAW_KEY_LEN].try_into() { + Ok(k) => k, + Err(_) => return -22, + }; + if !batch.offer(key, value.as_slice()) { + break; + } + at += 1; + } + } else { + // The log's edits interleave, so the merged order is what the cursor counts. Stream the + // space's index entries rather than all of them: the space is what is being listed. + let mut entry = KVVec::::new(); + let mut at = first; + let mut edit_at = 0usize; + loop { + while edit_at + 1 < edits.len() && edits[edit_at + 1].key == edits[edit_at].key { + edit_at += 1; + } + let edit = edits.as_slice().get(edit_at).copied(); + let image_entry = if at < first + records { + match image.index_entry(at, &mut entry) { + Ok(pair) => Some(pair), + Err(e) => return -(e.to_errno() as i32), + } + } else { + None + }; + let image_key: Option<&[u8; RAW_KEY_LEN]> = match image_entry { + Some(_) => entry.as_slice()[..RAW_KEY_LEN].try_into().ok(), + None => None, + }; + match (edit, image_entry, image_key) { + (Some(e), Some((_, value_len)), Some(key)) if e.key == key => { + let _ = value_len; + edit_at += 1; + at += 1; + if !e.deleted && !batch.offer(e.key, e.value) { + break; + } + } + (Some(e), Some((value_off, value_len)), Some(key)) if e.key < key => { + edit_at += 1; + if !e.deleted && !batch.offer(e.key, e.value) { + break; + } + let _ = (value_off, value_len); + } + (_, Some((value_off, value_len)), Some(_)) => { + let value = match image.value_at(value_off, value_len) { + Ok(v) => v, + Err(e) => return -(e.to_errno() as i32), + }; + let key = match entry.as_slice()[..RAW_KEY_LEN].try_into() { + Ok(k) => k, + Err(_) => return -22, + }; + at += 1; + if !batch.offer(key, value.as_slice()) { + break; + } + } + (Some(e), None, _) => { + edit_at += 1; + if !e.deleted && !batch.offer(e.key, e.value) { + break; + } + } + (None, None, _) => break, + _ => break, + } + } + } + + // SAFETY: both out-pointers are writable under this function's contract. + unsafe { + if batch.too_big > 0 && batch.written == 0 { + *out_len = batch.too_big as u64; + *out_cursor = cursor; + return -34; // -ERANGE + } + *out_len = batch.written as u64; + *out_cursor = cursor + batch.returned; + } + 0 +} /// /// Returns the record's length, or a negative errno. When the value does not fit in `len` the /// bytes are *not* copied and the length is returned anyway, so the caller can size its buffer @@ -2017,6 +2553,13 @@ pub unsafe extern "C" fn cubelinux_kernel_get( len: usize, ) -> isize { let (sp, key) = unsafe { coord_key(space, x, y, z) }; + // A v3 image is addressed, so a read is a binary search and a few bytes. v1 and v2 images are + // packed lists, which the walking reader below handles. + match Addressed::open() { + Ok(Some(image)) => return unsafe { v3_get(image, sp, key, buf, len) }, + Ok(None) => {} + Err(e) => return -(e.to_errno() as isize), + } let view = match read_view() { Ok(v) => v, Err(e) => return -(e.to_errno() as isize), @@ -2094,6 +2637,14 @@ pub unsafe extern "C" fn cubelinux_kernel_enum( // SAFETY: the caller guarantees 32 readable bytes at `space`. unsafe { core::ptr::copy_nonoverlapping(space, wanted.as_mut_ptr(), SPACE_ID_LEN) }; + match Addressed::open() { + Ok(Some(image)) => { + return unsafe { v3_enum(image, wanted, cursor, buf, cap, out_len, out_cursor) } + } + Ok(None) => {} + Err(e) => return -(e.to_errno() as i32), + } + let view = match read_view() { Ok(v) => v, Err(e) => return -(e.to_errno() as i32), @@ -2147,6 +2698,11 @@ pub unsafe extern "C" fn cubelinux_kernel_enum( /// `space_out` must point to 32 writable bytes. #[unsafe(no_mangle)] pub unsafe extern "C" fn cubelinux_kernel_spaces(cursor: u64, space_out: *mut u8) -> i32 { + match Addressed::open() { + Ok(Some(image)) => return unsafe { v3_spaces(image, cursor, space_out) }, + Ok(None) => {} + Err(e) => return -(e.to_errno() as i32), + } let view = match read_view() { Ok(v) => v, Err(e) => return -(e.to_errno() as i32),