cube-store-raw: write a v4 image
The addressed layout, with each index entry carrying the class mask its writer stamped, and the round trip asserted record for record — a writer and a reader that disagree about a stride agree about nothing, and every value lands two bytes off while every record still looks like a record. `entries` must be in (space, key) order, which is not advice: the space table says where each space's records *begin* and a fixed stride is what turns `index_off + i * stride` into a place, so an unsorted index answers the wrong question without failing. A `BTreeMap<(SpaceId, Key), _>` iterates in exactly that order, which is why the caller does not have to sort.
This commit is contained in:
@@ -821,6 +821,77 @@ use alloc::vec::Vec;
|
||||
/// Each entry is `(SpaceId, 24-byte key, value)`. The kernel driver does NOT
|
||||
/// use this (it has no heap); `cube-store` does. Provided behind the `alloc`
|
||||
/// feature so the no_std build stays clean.
|
||||
/// `alloc`-only: serialize a v4 image — the addressed layout, with each index entry carrying the
|
||||
/// class mask its writer stamped.
|
||||
///
|
||||
/// ```text
|
||||
/// [header 46] magic, version, curve, extents, counts
|
||||
/// [space table: space_count x 48] space | first index | records
|
||||
/// [index: record_count x 42] key | flags(u16) | value offset | value length
|
||||
/// [values: packed, in index order]
|
||||
/// ```
|
||||
///
|
||||
/// `entries` must be in `(space, key)` order. That is not advice: the space table says where each
|
||||
/// space's records *begin*, and a fixed stride is what turns `index_off + i * stride` into a place,
|
||||
/// so an unsorted index is a store that answers the wrong question without failing. A
|
||||
/// `BTreeMap<(SpaceId, Key), _>` iterates in exactly that order, which is why the caller does not
|
||||
/// have to sort.
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn serialize_v4(
|
||||
curve_tag: u8,
|
||||
entries: &[(SpaceId, [u8; RAW_KEY_LEN], u16, &[u8])],
|
||||
) -> Vec<u8> {
|
||||
// The space table 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: Vec<(&[u8; SPACE_ID_LEN], u64, u64)> = Vec::new();
|
||||
for (at, (space, _, _, _)) in entries.iter().enumerate() {
|
||||
match spaces.last_mut() {
|
||||
Some((last, _, records)) if *last == space.as_bytes() => *records += 1,
|
||||
_ => spaces.push((space.as_bytes(), at as u64, 1)),
|
||||
}
|
||||
}
|
||||
|
||||
let values_total: usize = entries.iter().map(|(_, _, _, v)| v.len()).sum();
|
||||
let index_off = HEADER_LEN_V3 + spaces.len() * SPACE_ENTRY;
|
||||
let values_off = index_off + entries.len() * INDEX_ENTRY_V4;
|
||||
let image_bytes = values_off + values_total;
|
||||
|
||||
let mut out = vec![0u8; image_bytes];
|
||||
out[0..4].copy_from_slice(MAGIC);
|
||||
out[4] = VERSION_V4;
|
||||
out[5] = curve_tag;
|
||||
out[6..14].copy_from_slice(&(image_bytes as u64).to_le_bytes());
|
||||
out[14..22].copy_from_slice(&(entries.len() as u64).to_le_bytes());
|
||||
out[22..30].copy_from_slice(&(spaces.len() as u64).to_le_bytes());
|
||||
out[30..38].copy_from_slice(&(index_off as u64).to_le_bytes());
|
||||
out[38..46].copy_from_slice(&(values_off as u64).to_le_bytes());
|
||||
|
||||
let mut at = HEADER_LEN_V3;
|
||||
for (space, first, records) in &spaces {
|
||||
out[at..at + SPACE_ID_LEN].copy_from_slice(*space);
|
||||
out[at + SPACE_ID_LEN..at + SPACE_ID_LEN + 8].copy_from_slice(&first.to_le_bytes());
|
||||
out[at + SPACE_ID_LEN + 8..at + SPACE_ENTRY].copy_from_slice(&records.to_le_bytes());
|
||||
at += SPACE_ENTRY;
|
||||
}
|
||||
|
||||
let mut value_at = values_off;
|
||||
for (i, (_, key, flags, value)) in entries.iter().enumerate() {
|
||||
let at = index_off + i * INDEX_ENTRY_V4;
|
||||
out[at..at + RAW_KEY_LEN].copy_from_slice(key);
|
||||
out[at + RAW_KEY_LEN..at + RAW_KEY_LEN + FLAGS_LEN].copy_from_slice(&flags.to_le_bytes());
|
||||
// The length is written as eight bytes even though a value's length fits in four: the
|
||||
// stride is fixed and `values_off` is computed from it, so a shorter field here would leave
|
||||
// the last entries overlapping the values.
|
||||
out[at + RAW_KEY_LEN + FLAGS_LEN..at + RAW_KEY_LEN + FLAGS_LEN + 8]
|
||||
.copy_from_slice(&(value_at as u64).to_le_bytes());
|
||||
out[at + RAW_KEY_LEN + FLAGS_LEN + 8..at + INDEX_ENTRY_V4]
|
||||
.copy_from_slice(&(value.len() as u64).to_le_bytes());
|
||||
out[value_at..value_at + value.len()].copy_from_slice(value);
|
||||
value_at += value.len();
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub fn serialize(curve_tag: u8, entries: &[(SpaceId, [u8; RAW_KEY_LEN], &[u8])]) -> Vec<u8> {
|
||||
let values_total: usize = entries.iter().map(|(_, _, v)| v.len()).sum();
|
||||
@@ -1024,6 +1095,44 @@ mod tests {
|
||||
assert_eq!(read, expected, "a v4 image read differently from its records");
|
||||
}
|
||||
|
||||
/// What the v4 writer writes, the readers read: the same records, in the same order, with the
|
||||
/// masks they were written under.
|
||||
///
|
||||
/// A writer and a reader that disagree about a stride agree about nothing, and the failure is
|
||||
/// silent — every value lands two bytes off and every record still looks like a record. So the
|
||||
/// round trip is asserted record for record rather than by a length.
|
||||
#[test]
|
||||
#[cfg(feature = "alloc")]
|
||||
fn the_v4_writer_and_its_readers_agree() {
|
||||
let root = SpaceId::ROOT;
|
||||
let other = SpaceId::from_bytes([5u8; SPACE_ID_LEN]);
|
||||
let entries: [(SpaceId, [u8; RAW_KEY_LEN], u16, &[u8]); 4] = [
|
||||
(root, [0u8; RAW_KEY_LEN], 0, b""),
|
||||
(root, [1u8; RAW_KEY_LEN], 0x0080, b"an event"),
|
||||
(root, [2u8; RAW_KEY_LEN], 0, b"plain"),
|
||||
(other, [3u8; RAW_KEY_LEN], 0x0022, b"mem|error"),
|
||||
];
|
||||
let image = serialize_v4(0, &entries);
|
||||
|
||||
assert_eq!(parse(&image).unwrap().version, VERSION_V4);
|
||||
let read: std::vec::Vec<_> = iter_records(&image)
|
||||
.map(|r| {
|
||||
r.map(|r| (r.space.to_vec(), r.key.to_vec(), r.flags, r.value.to_vec()))
|
||||
})
|
||||
.collect::<Result<_, _>>()
|
||||
.unwrap();
|
||||
let expected: std::vec::Vec<_> = entries
|
||||
.iter()
|
||||
.map(|(s, k, f, v)| (s.as_bytes().to_vec(), k.to_vec(), *f, v.to_vec()))
|
||||
.collect();
|
||||
assert_eq!(read, expected, "the v4 writer and the reader disagree");
|
||||
|
||||
// And the geometry says what it wrote, so a reader computes the same addresses.
|
||||
let geometry = V3::decode(&image).expect("the tables are where the header says");
|
||||
assert_eq!(geometry.index_stride(), INDEX_ENTRY_V4);
|
||||
assert_eq!(geometry.record_count, entries.len() as u64);
|
||||
}
|
||||
|
||||
/// A v1 image still reads, and still stops at its zero padding — the rule v2 retired.
|
||||
#[test]
|
||||
fn v1_images_still_read() {
|
||||
|
||||
Reference in New Issue
Block a user