use crate::get_u32;
use yo_common::{Code, Error, Result};
pub const DOC_HEADER_LEN: usize = 4;
pub const DOC_COUNT_SHIFT: u32 = 8;
pub const DOC_COUNT_MAX: usize = (1 << 24) - 1;
pub mod doc_flags {
pub const ARRAY: u32 = 1 << 3;
pub const SORTED: u32 = 1 << 4;
pub const OFFSETS: u32 = 1 << 5;
pub const INTERNED: u32 = 1 << 6;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ValueTag {
Null = 0,
False = 1,
True = 3,
Int = 4,
Float = 5,
Text = 6,
Container = 7,
}
impl ValueTag {
#[must_use]
pub const fn as_u8(self) -> u8 {
self as u8
}
#[must_use]
pub const fn from_u8(b: u8) -> Option<ValueTag> {
match b {
0 => Some(ValueTag::Null),
1 => Some(ValueTag::False),
3 => Some(ValueTag::True),
4 => Some(ValueTag::Int),
5 => Some(ValueTag::Float),
6 => Some(ValueTag::Text),
7 => Some(ValueTag::Container),
_ => None,
}
}
#[must_use]
pub const fn is_container(self) -> bool {
matches!(self, ValueTag::Container)
}
}
#[derive(Debug, Clone, Copy)]
pub struct DocumentBody<'a> {
head: u32,
tag: ValueTag,
bytes: &'a [u8],
}
impl<'a> DocumentBody<'a> {
pub fn decode(value: &'a [u8]) -> Result<DocumentBody<'a>> {
if value.len() < DOC_HEADER_LEN {
return Err(Error::new(
Code::Corrupt,
"a document record is shorter than a value header",
)
.with_detail(format!("len={}", value.len())));
}
let head = get_u32(value, 0);
let Some(tag) = ValueTag::from_u8((head & 0b111) as u8) else {
return Err(
Error::new(Code::Corrupt, "a document record has an unknown tag")
.with_detail(format!("head={head:#010x}")),
);
};
let count = (head >> DOC_COUNT_SHIFT) as usize;
if !tag.is_container() {
let want = DOC_HEADER_LEN + count;
if value.len() != want {
return Err(Error::new(
Code::Corrupt,
"a scalar document is not the length its header says",
)
.with_detail(format!("len={} want={want}", value.len())));
}
let ok = match tag {
ValueTag::Null | ValueTag::False | ValueTag::True => count == 0,
ValueTag::Int => matches!(count, 1 | 2 | 4 | 8),
ValueTag::Float => count == 8,
ValueTag::Text => true,
ValueTag::Container => unreachable!("checked above"),
};
if !ok {
return Err(Error::new(
Code::Corrupt,
"a scalar document has a payload its type cannot have",
)
.with_detail(format!("tag={tag:?} payload={count}")));
}
}
Ok(DocumentBody {
head,
tag,
bytes: value,
})
}
#[must_use]
pub fn head(self) -> u32 {
self.head
}
#[must_use]
pub fn tag(self) -> ValueTag {
self.tag
}
#[must_use]
pub fn count(self) -> usize {
(self.head >> DOC_COUNT_SHIFT) as usize
}
#[must_use]
pub fn is_array(self) -> bool {
self.tag.is_container() && self.head & doc_flags::ARRAY != 0
}
#[must_use]
pub fn is_interned(self) -> bool {
self.tag.is_container()
&& self.head & doc_flags::ARRAY == 0
&& self.head & doc_flags::INTERNED != 0
}
#[must_use]
pub fn bytes(self) -> &'a [u8] {
self.bytes
}
}
#[cfg(test)]
mod tests {
use super::*;
fn head(tag: ValueTag, flags: u32, count: usize) -> [u8; 4] {
(u32::from(tag.as_u8()) | flags | ((count as u32) << DOC_COUNT_SHIFT)).to_le_bytes()
}
#[test]
fn a_scalar_is_its_header_and_its_payload() {
let mut v = head(ValueTag::Int, 0, 8).to_vec();
v.extend_from_slice(&41_920i64.to_le_bytes());
let d = DocumentBody::decode(&v).unwrap();
assert_eq!(d.tag(), ValueTag::Int);
assert_eq!(d.count(), 8);
assert!(!d.is_array());
assert!(!d.is_interned());
assert_eq!(d.bytes(), &v[..]);
}
#[test]
fn a_scalar_that_lost_bytes_is_corrupt() {
let mut v = head(ValueTag::Int, 0, 8).to_vec();
v.extend_from_slice(&41_920i64.to_le_bytes());
for cut in 1..=8 {
let short = &v[..v.len() - cut];
assert!(
DocumentBody::decode(short).is_err(),
"an int missing {cut} bytes was accepted"
);
}
}
#[test]
fn a_scalar_of_a_length_its_type_cannot_have_is_corrupt() {
let mut v = head(ValueTag::Int, 0, 3).to_vec();
v.extend_from_slice(&[1, 2, 3]);
assert!(DocumentBody::decode(&v).is_err());
let mut v = head(ValueTag::Float, 0, 4).to_vec();
v.extend_from_slice(&[1, 2, 3, 4]);
assert!(DocumentBody::decode(&v).is_err());
let mut v = head(ValueTag::Null, 0, 1).to_vec();
v.push(0);
assert!(DocumentBody::decode(&v).is_err());
}
#[test]
fn a_container_is_not_walked_here() {
let mut v = head(
ValueTag::Container,
doc_flags::OFFSETS | doc_flags::SORTED,
4,
)
.to_vec();
v.extend_from_slice(&[0; 5]);
let d = DocumentBody::decode(&v).unwrap();
assert_eq!(d.tag(), ValueTag::Container);
assert_eq!(d.count(), 4);
assert!(!d.is_array());
}
#[test]
fn an_array_and_an_interned_object_say_so() {
let v = head(
ValueTag::Container,
doc_flags::ARRAY | doc_flags::OFFSETS,
0,
)
.to_vec();
let d = DocumentBody::decode(&v).unwrap();
assert!(d.is_array());
assert!(!d.is_interned(), "an array has no keys to intern");
let v = head(
ValueTag::Container,
doc_flags::INTERNED | doc_flags::OFFSETS,
0,
)
.to_vec();
let d = DocumentBody::decode(&v).unwrap();
assert!(!d.is_array());
assert!(d.is_interned());
}
#[test]
fn an_unknown_tag_is_corrupt_rather_than_a_guess() {
let v = 2u32.to_le_bytes().to_vec();
assert!(DocumentBody::decode(&v).is_err());
assert_eq!(ValueTag::from_u8(2), None);
}
#[test]
fn a_value_shorter_than_a_header_is_corrupt() {
for n in 0..DOC_HEADER_LEN {
assert!(DocumentBody::decode(&vec![0u8; n]).is_err(), "{n} bytes");
}
}
#[test]
fn every_tag_round_trips_through_its_byte() {
for tag in [
ValueTag::Null,
ValueTag::False,
ValueTag::True,
ValueTag::Int,
ValueTag::Float,
ValueTag::Text,
ValueTag::Container,
] {
assert_eq!(ValueTag::from_u8(tag.as_u8()), Some(tag));
}
}
}