#![allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
const TAG_HOT: u8 = 0x00;
const TAG_COLD: u8 = 0x01;
#[derive(Debug, PartialEq, Eq)]
pub enum RowLocatorError {
TooShort { got: usize, need: usize },
BadTag { got: u8 },
TruncatedCold { got: usize, need: usize },
LegacyIndexOverflow(String),
}
impl fmt::Display for RowLocatorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TooShort { got, need } => {
write!(f, "row_locator: too short, got {got} bytes, need {need}")
}
Self::BadTag { got } => write!(
f,
"row_locator: bad tag 0x{got:02x}, expected 0x00 (Hot) or 0x01 (Cold)"
),
Self::TruncatedCold { got, need } => write!(
f,
"row_locator: cold variant truncated, got {got} bytes, need {need}"
),
Self::LegacyIndexOverflow(s) => write!(f, "row_locator: legacy v8 index overflow: {s}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum RowLocator {
Hot(usize),
Cold { segment_id: u32, page_offset: u32 },
}
impl RowLocator {
#[must_use]
pub const fn is_hot(&self) -> bool {
matches!(self, Self::Hot(_))
}
#[must_use]
pub const fn is_cold(&self) -> bool {
matches!(self, Self::Cold { .. })
}
#[must_use]
pub const fn as_hot(&self) -> Option<usize> {
match self {
Self::Hot(i) => Some(*i),
Self::Cold { .. } => None,
}
}
#[must_use]
pub const fn as_cold(&self) -> Option<(u32, u32)> {
match self {
Self::Cold {
segment_id,
page_offset,
} => Some((*segment_id, *page_offset)),
Self::Hot(_) => None,
}
}
#[must_use]
pub const fn encoded_len(&self) -> usize {
9
}
pub fn write_le(&self, out: &mut Vec<u8>) {
match self {
Self::Hot(i) => {
out.push(TAG_HOT);
out.extend_from_slice(&(*i as u64).to_le_bytes());
}
Self::Cold {
segment_id,
page_offset,
} => {
out.push(TAG_COLD);
out.extend_from_slice(&segment_id.to_le_bytes());
out.extend_from_slice(&page_offset.to_le_bytes());
}
}
}
pub fn read_le(input: &[u8]) -> Result<(Self, usize), RowLocatorError> {
if input.is_empty() {
return Err(RowLocatorError::TooShort { got: 0, need: 1 });
}
let tag = input[0];
match tag {
TAG_HOT => {
if input.len() < 9 {
return Err(RowLocatorError::TooShort {
got: input.len(),
need: 9,
});
}
let idx = u64::from_le_bytes([
input[1], input[2], input[3], input[4], input[5], input[6], input[7], input[8],
]);
let idx_usize = usize::try_from(idx).map_err(|_| {
RowLocatorError::LegacyIndexOverflow(format!(
"Hot row index {idx} exceeds usize on this target"
))
})?;
Ok((Self::Hot(idx_usize), 9))
}
TAG_COLD => {
if input.len() < 9 {
return Err(RowLocatorError::TruncatedCold {
got: input.len(),
need: 9,
});
}
let segment_id = u32::from_le_bytes([input[1], input[2], input[3], input[4]]);
let page_offset = u32::from_le_bytes([input[5], input[6], input[7], input[8]]);
Ok((
Self::Cold {
segment_id,
page_offset,
},
9,
))
}
other => Err(RowLocatorError::BadTag { got: other }),
}
}
pub fn from_legacy_v8_u64(idx: u64) -> Result<Self, RowLocatorError> {
let idx_usize = usize::try_from(idx).map_err(|_| {
RowLocatorError::LegacyIndexOverflow(format!(
"Hot row index {idx} exceeds usize on this target"
))
})?;
Ok(Self::Hot(idx_usize))
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn hot_constructs_and_inspects() {
let l = RowLocator::Hot(42);
assert!(l.is_hot());
assert!(!l.is_cold());
assert_eq!(l.as_hot(), Some(42));
assert_eq!(l.as_cold(), None);
}
#[test]
fn cold_constructs_and_inspects() {
let l = RowLocator::Cold {
segment_id: 7,
page_offset: 4096 * 9,
};
assert!(l.is_cold());
assert!(!l.is_hot());
assert_eq!(l.as_hot(), None);
assert_eq!(l.as_cold(), Some((7, 36_864)));
}
#[test]
fn encoded_len_is_constant() {
assert_eq!(RowLocator::Hot(0).encoded_len(), 9);
assert_eq!(RowLocator::Hot(usize::MAX).encoded_len(), 9);
assert_eq!(
RowLocator::Cold {
segment_id: u32::MAX,
page_offset: u32::MAX,
}
.encoded_len(),
9
);
}
#[test]
fn roundtrip_hot_via_write_le_read_le() {
for &idx in &[0_usize, 1, 42, 1_000_000, usize::MAX] {
let l = RowLocator::Hot(idx);
let mut buf = Vec::new();
l.write_le(&mut buf);
assert_eq!(buf.len(), 9);
let (parsed, consumed) = RowLocator::read_le(&buf).expect("hot roundtrip parses");
assert_eq!(parsed, l);
assert_eq!(consumed, 9);
}
}
#[test]
fn roundtrip_cold_via_write_le_read_le() {
for &(s, p) in &[
(0_u32, 0_u32),
(1, 4096),
(42, 4096 * 7),
(u32::MAX, u32::MAX),
] {
let l = RowLocator::Cold {
segment_id: s,
page_offset: p,
};
let mut buf = Vec::new();
l.write_le(&mut buf);
assert_eq!(buf.len(), 9);
let (parsed, consumed) = RowLocator::read_le(&buf).expect("cold roundtrip parses");
assert_eq!(parsed, l);
assert_eq!(consumed, 9);
}
}
#[test]
fn mixed_concat_decodes_in_sequence() {
let entries = [
RowLocator::Hot(7),
RowLocator::Cold {
segment_id: 2,
page_offset: 4096,
},
RowLocator::Hot(99),
];
let mut buf = Vec::new();
for e in &entries {
e.write_le(&mut buf);
}
assert_eq!(buf.len(), 27);
let mut offset = 0;
let mut decoded = Vec::new();
while offset < buf.len() {
let (l, n) = RowLocator::read_le(&buf[offset..]).expect("decode succeeds");
decoded.push(l);
offset += n;
}
assert_eq!(offset, buf.len());
assert_eq!(decoded.as_slice(), entries.as_slice());
}
#[test]
fn read_le_rejects_empty_input() {
match RowLocator::read_le(&[]) {
Err(RowLocatorError::TooShort { got: 0, need: 1 }) => {}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn read_le_rejects_bad_tag() {
let mut buf = vec![0xff_u8];
buf.extend_from_slice(&0_u64.to_le_bytes());
match RowLocator::read_le(&buf) {
Err(RowLocatorError::BadTag { got: 0xff }) => {}
other => panic!("expected BadTag, got {other:?}"),
}
}
#[test]
fn read_le_rejects_truncated_hot() {
let buf = [TAG_HOT, 0x01, 0x02, 0x03, 0x04];
match RowLocator::read_le(&buf) {
Err(RowLocatorError::TooShort { got: 5, need: 9 }) => {}
other => panic!("expected TooShort, got {other:?}"),
}
}
#[test]
fn read_le_rejects_truncated_cold() {
let buf = [TAG_COLD, 0x01, 0x02, 0x03];
match RowLocator::read_le(&buf) {
Err(RowLocatorError::TruncatedCold { got: 4, need: 9 }) => {}
other => panic!("expected TruncatedCold, got {other:?}"),
}
}
#[test]
fn from_legacy_v8_u64_wraps_as_hot() {
for &idx in &[0_u64, 1, 1_000_000, u64::from(u32::MAX)] {
let l = RowLocator::from_legacy_v8_u64(idx).expect("fits usize on 64-bit");
assert_eq!(l.as_hot(), Some(idx as usize));
}
}
#[test]
fn size_is_16_bytes() {
assert_eq!(core::mem::size_of::<RowLocator>(), 16);
}
}