pub const COORDINATE_VERSION: u8 = 1;
pub const COORDINATE_FIXED_LEN: usize = 1 + 8 + 32 + 2;
pub const MAX_SEGMENT_NAME_LEN: usize = 255;
pub const MAX_TRACKED_ACK_PAYLOAD_LEN: usize = COORDINATE_FIXED_LEN + MAX_SEGMENT_NAME_LEN;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RecordCoordinate {
segment: String,
index: u64,
record_id: [u8; 32],
}
impl RecordCoordinate {
pub fn new(segment: String, index: u64, record_id: [u8; 32]) -> Result<Self, CoordinateError> {
if segment.len() > MAX_SEGMENT_NAME_LEN {
return Err(CoordinateError::SegmentTooLong {
len: segment.len(),
cap: MAX_SEGMENT_NAME_LEN,
});
}
Ok(Self {
segment,
index,
record_id,
})
}
#[must_use]
pub fn segment(&self) -> &str {
&self.segment
}
#[must_use]
pub fn index(&self) -> u64 {
self.index
}
#[must_use]
pub fn record_id(&self) -> &[u8; 32] {
&self.record_id
}
#[must_use]
pub fn record_id_hex(&self) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(64);
for b in self.record_id {
let _ = write!(out, "{b:02x}");
}
out
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(COORDINATE_FIXED_LEN + self.segment.len());
out.push(COORDINATE_VERSION);
out.extend_from_slice(&self.index.to_le_bytes());
out.extend_from_slice(&self.record_id);
out.extend_from_slice(&(self.segment.len() as u16).to_le_bytes());
out.extend_from_slice(self.segment.as_bytes());
out
}
pub fn decode(buf: &[u8]) -> Result<Self, CoordinateError> {
if buf.len() < COORDINATE_FIXED_LEN {
return Err(CoordinateError::Truncated {
len: buf.len(),
need: COORDINATE_FIXED_LEN,
});
}
let version = buf[0];
if version != COORDINATE_VERSION {
return Err(CoordinateError::UnsupportedVersion { version });
}
let index = u64::from_le_bytes(buf[1..9].try_into().unwrap());
let record_id: [u8; 32] = buf[9..41].try_into().unwrap();
let segment_len = u16::from_le_bytes(buf[41..43].try_into().unwrap()) as usize;
if segment_len > MAX_SEGMENT_NAME_LEN {
return Err(CoordinateError::SegmentTooLong {
len: segment_len,
cap: MAX_SEGMENT_NAME_LEN,
});
}
let expected = COORDINATE_FIXED_LEN + segment_len;
if buf.len() != expected {
return Err(CoordinateError::LengthMismatch {
declared: expected,
actual: buf.len(),
});
}
let segment = std::str::from_utf8(&buf[COORDINATE_FIXED_LEN..])
.map_err(|_| CoordinateError::SegmentNotUtf8)?
.to_owned();
Ok(Self {
segment,
index,
record_id,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum CoordinateError {
Truncated {
len: usize,
need: usize,
},
UnsupportedVersion {
version: u8,
},
SegmentTooLong {
len: usize,
cap: usize,
},
LengthMismatch {
declared: usize,
actual: usize,
},
SegmentNotUtf8,
}
impl std::fmt::Display for CoordinateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CoordinateError::Truncated { len, need } => write!(
f,
"record coordinate truncated: {len} bytes, need at least {need}"
),
CoordinateError::UnsupportedVersion { version } => write!(
f,
"unsupported record-coordinate version {version:#04x}; this build understands {COORDINATE_VERSION:#04x}"
),
CoordinateError::SegmentTooLong { len, cap } => write!(
f,
"record coordinate segment name is {len} bytes, over the {cap}-byte cap"
),
CoordinateError::LengthMismatch { declared, actual } => write!(
f,
"record coordinate declares {declared} bytes but the payload is {actual}"
),
CoordinateError::SegmentNotUtf8 => {
write!(f, "record coordinate segment name is not valid UTF-8")
}
}
}
}
impl std::error::Error for CoordinateError {}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> RecordCoordinate {
RecordCoordinate::new("shard_00/seg_00000001.wab.sealed".into(), 7, [0xAB; 32]).unwrap()
}
#[test]
fn round_trips() {
let c = sample();
assert_eq!(RecordCoordinate::decode(&c.encode()).unwrap(), c);
}
#[test]
fn round_trips_at_the_boundaries() {
for (segment, index) in [
(String::new(), 0u64),
("a".to_string(), 1),
("x".repeat(MAX_SEGMENT_NAME_LEN), u64::MAX),
] {
let c = RecordCoordinate::new(segment, index, [0x5A; 32]).unwrap();
assert_eq!(
RecordCoordinate::decode(&c.encode()).unwrap(),
c,
"coordinate did not survive a round trip"
);
}
}
#[test]
fn encoded_layout_is_pinned() {
let c = RecordCoordinate::new("ab".into(), 0x0102_0304_0506_0708, [0x11; 32]).unwrap();
let e = c.encode();
assert_eq!(e.len(), COORDINATE_FIXED_LEN + 2);
assert_eq!(e[0], COORDINATE_VERSION);
assert_eq!(&e[1..9], &[0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01]);
assert_eq!(&e[9..41], &[0x11; 32]);
assert_eq!(&e[41..43], &[0x02, 0x00]);
assert_eq!(&e[43..], b"ab");
}
#[test]
fn encoded_length_never_exceeds_the_response_cap() {
let c = RecordCoordinate::new("z".repeat(MAX_SEGMENT_NAME_LEN), 1, [0; 32]).unwrap();
assert_eq!(c.encode().len(), MAX_TRACKED_ACK_PAYLOAD_LEN);
}
#[test]
fn new_rejects_an_oversized_segment_name() {
let err =
RecordCoordinate::new("z".repeat(MAX_SEGMENT_NAME_LEN + 1), 1, [0; 32]).unwrap_err();
assert_eq!(
err,
CoordinateError::SegmentTooLong {
len: MAX_SEGMENT_NAME_LEN + 1,
cap: MAX_SEGMENT_NAME_LEN,
}
);
}
#[test]
fn decode_rejects_a_short_payload() {
let e = sample().encode();
for take in [0usize, 1, COORDINATE_FIXED_LEN - 1] {
assert_eq!(
RecordCoordinate::decode(&e[..take]),
Err(CoordinateError::Truncated {
len: take,
need: COORDINATE_FIXED_LEN,
})
);
}
}
#[test]
fn decode_rejects_an_unknown_version() {
let mut e = sample().encode();
e[0] = COORDINATE_VERSION + 1;
assert_eq!(
RecordCoordinate::decode(&e),
Err(CoordinateError::UnsupportedVersion {
version: COORDINATE_VERSION + 1,
})
);
}
#[test]
fn decode_checks_version_before_length() {
let mut e = sample().encode();
e[0] = 0xFF;
e.push(0x00);
assert_eq!(
RecordCoordinate::decode(&e),
Err(CoordinateError::UnsupportedVersion { version: 0xFF })
);
}
#[test]
fn decode_rejects_a_declared_length_over_the_cap() {
let mut e = sample().encode();
e[41..43].copy_from_slice(&((MAX_SEGMENT_NAME_LEN + 1) as u16).to_le_bytes());
assert_eq!(
RecordCoordinate::decode(&e),
Err(CoordinateError::SegmentTooLong {
len: MAX_SEGMENT_NAME_LEN + 1,
cap: MAX_SEGMENT_NAME_LEN,
})
);
}
#[test]
fn decode_rejects_trailing_bytes_and_a_short_tail() {
let base = sample().encode();
let segment_len = base.len() - COORDINATE_FIXED_LEN;
let mut long = base.clone();
long.push(0x00);
assert_eq!(
RecordCoordinate::decode(&long),
Err(CoordinateError::LengthMismatch {
declared: base.len(),
actual: base.len() + 1,
})
);
let short = &base[..base.len() - 1];
assert_eq!(
RecordCoordinate::decode(short),
Err(CoordinateError::LengthMismatch {
declared: COORDINATE_FIXED_LEN + segment_len,
actual: base.len() - 1,
})
);
}
#[test]
fn decode_rejects_a_non_utf8_segment_name() {
let mut e = sample().encode();
let last = e.len() - 1;
e[last] = 0xFF;
assert_eq!(
RecordCoordinate::decode(&e),
Err(CoordinateError::SegmentNotUtf8)
);
}
#[test]
fn record_id_hex_is_64_lowercase_chars() {
let mut id = [0u8; 32];
id[0] = 0x0A;
id[31] = 0xFF;
let c = RecordCoordinate::new("s".into(), 1, id).unwrap();
let hex = c.record_id_hex();
assert_eq!(hex.len(), 64);
assert!(hex.starts_with("0a"));
assert!(hex.ends_with("ff"));
assert!(
hex.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_uppercase())
);
}
#[test]
fn errors_are_displayable_std_errors() {
for e in [
CoordinateError::Truncated { len: 0, need: 43 },
CoordinateError::UnsupportedVersion { version: 2 },
CoordinateError::SegmentTooLong { len: 300, cap: 255 },
CoordinateError::LengthMismatch {
declared: 43,
actual: 44,
},
CoordinateError::SegmentNotUtf8,
] {
assert!(!e.to_string().is_empty());
let _: &dyn std::error::Error = &e;
}
}
}