use alloc::vec::Vec;
use xxhash_rust::xxh3::xxh3_64;
use crate::error::Error;
const U32_BYTES: usize = core::mem::size_of::<u32>();
const U64_BYTES: usize = core::mem::size_of::<u64>();
const F32_BYTES: usize = core::mem::size_of::<f32>();
const HEADER: usize = U32_BYTES + U32_BYTES;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct JournalEntry<'a> {
pub op: u8,
pub payload: &'a [u8],
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct JournalScan<'a> {
pub entries: Vec<JournalEntry<'a>>,
pub truncated_tail: bool,
}
fn body_checksum(body: &[u8]) -> u32 {
xxh3_64(body) as u32
}
pub fn encode_entry(out: &mut Vec<u8>, op: u8, payload: &[u8]) {
let len = 1 + payload.len();
let len32 = u32::try_from(len).expect("journal payload fits u32 by construction");
out.reserve(HEADER + len);
out.extend_from_slice(&len32.to_le_bytes());
let check_pos = out.len();
out.extend_from_slice(&[0u8; U32_BYTES]);
out.push(op);
out.extend_from_slice(payload);
let check = body_checksum(&out[check_pos + U32_BYTES..]);
out[check_pos..check_pos + U32_BYTES].copy_from_slice(&check.to_le_bytes());
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum Op<'a> {
Remember {
now: u64,
valid_from: u64,
entity: Option<&'a str>,
text: &'a str,
tags: Vec<&'a str>,
links: Vec<(&'a str, &'a str)>,
vector: Vec<f32>,
metadata: Vec<(&'a str, &'a str)>,
revises: crate::id::FactId,
assigned: crate::id::FactId,
},
Forget {
now: u64,
fact: crate::id::FactId,
},
Link {
now: u64,
src: &'a str,
rel: &'a str,
dst: &'a str,
provenance: crate::id::FactId,
},
Maintain {
now: u64,
},
}
fn put_str(out: &mut Vec<u8>, s: &str) {
out.extend_from_slice(&(s.len() as u32).to_le_bytes());
out.extend_from_slice(s.as_bytes());
}
fn take_str<'a>(bytes: &'a [u8], at: &mut usize) -> Result<&'a str, Error> {
let len = take_u32(bytes, at)? as usize;
let end = at
.checked_add(len)
.filter(|&e| e <= bytes.len())
.ok_or(Error::Corrupt("journal string overruns its record"))?;
let s = core::str::from_utf8(&bytes[*at..end])
.map_err(|_| Error::Corrupt("journal string is not UTF-8"))?;
*at = end;
Ok(s)
}
fn take_u32(bytes: &[u8], at: &mut usize) -> Result<u32, Error> {
let end = *at + U32_BYTES;
if end > bytes.len() {
return Err(Error::Corrupt("journal record truncated inside a field"));
}
let v = u32::from_le_bytes(bytes[*at..end].try_into().unwrap());
*at = end;
Ok(v)
}
fn take_u64(bytes: &[u8], at: &mut usize) -> Result<u64, Error> {
let end = *at + U64_BYTES;
if end > bytes.len() {
return Err(Error::Corrupt("journal record truncated inside a field"));
}
let v = u64::from_le_bytes(bytes[*at..end].try_into().unwrap());
*at = end;
Ok(v)
}
fn take_vec_f32(bytes: &[u8], at: &mut usize) -> Result<Vec<f32>, Error> {
let count = take_u32(bytes, at)? as usize;
let end = *at as u64 + count as u64 * F32_BYTES as u64;
if end > bytes.len() as u64 {
return Err(Error::Corrupt("journal vector overruns its record"));
}
let end = end as usize;
let mut v = Vec::with_capacity(count);
let mut p = *at;
while p < end {
v.push(f32::from_le_bytes(
bytes[p..p + F32_BYTES].try_into().unwrap(),
));
p += F32_BYTES;
}
*at = end;
Ok(v)
}
impl<'a> Op<'a> {
pub fn encode(&self, out: &mut Vec<u8>) {
let mut payload = Vec::new();
let op = match self {
Op::Remember {
now,
valid_from,
entity,
text,
tags,
links,
vector,
metadata,
revises,
assigned,
} => {
payload.extend_from_slice(&now.to_le_bytes());
payload.extend_from_slice(&valid_from.to_le_bytes());
payload.extend_from_slice(&revises.0.to_le_bytes());
payload.extend_from_slice(&assigned.0.to_le_bytes());
match entity {
Some(name) => {
payload.push(1);
put_str(&mut payload, name);
}
None => payload.push(0),
}
put_str(&mut payload, text);
payload.push(tags.len() as u8);
for tag in tags {
put_str(&mut payload, tag);
}
payload.push(links.len() as u8);
for (rel, dst) in links {
put_str(&mut payload, rel);
put_str(&mut payload, dst);
}
payload.extend_from_slice(&(vector.len() as u32).to_le_bytes());
for &x in vector {
payload.extend_from_slice(&x.to_le_bytes());
}
payload.extend_from_slice(&(metadata.len() as u32).to_le_bytes());
for (k, v) in metadata {
put_str(&mut payload, k);
put_str(&mut payload, v);
}
if revises.is_none() { 1 } else { 2 }
}
Op::Forget { now, fact } => {
payload.extend_from_slice(&now.to_le_bytes());
payload.extend_from_slice(&fact.0.to_le_bytes());
3
}
Op::Link {
now,
src,
rel,
dst,
provenance,
} => {
payload.extend_from_slice(&now.to_le_bytes());
payload.extend_from_slice(&provenance.0.to_le_bytes());
put_str(&mut payload, src);
put_str(&mut payload, rel);
put_str(&mut payload, dst);
4
}
Op::Maintain { now } => {
payload.extend_from_slice(&now.to_le_bytes());
5
}
};
encode_entry(out, op, &payload);
}
pub fn decode(op: u8, payload: &'a [u8]) -> Result<Op<'a>, Error> {
use crate::id::FactId;
let at = &mut 0usize;
let decoded = match op {
1 | 2 => {
let now = take_u64(payload, at)?;
let valid_from = take_u64(payload, at)?;
let revises = FactId(take_u32(payload, at)?);
let assigned = FactId(take_u32(payload, at)?);
if (op == 2) == revises.is_none() {
return Err(Error::Corrupt("journal revises field disagrees with op"));
}
let entity = match payload.get(*at) {
Some(0) => {
*at += 1;
None
}
Some(1) => {
*at += 1;
Some(take_str(payload, at)?)
}
_ => return Err(Error::Corrupt("journal entity flag is invalid")),
};
let text = take_str(payload, at)?;
let tag_cnt = *payload
.get(*at)
.ok_or(Error::Corrupt("journal record truncated inside a field"))?;
*at += 1;
let mut tags = Vec::with_capacity(tag_cnt as usize);
for _ in 0..tag_cnt {
tags.push(take_str(payload, at)?);
}
let link_cnt = *payload
.get(*at)
.ok_or(Error::Corrupt("journal record truncated inside a field"))?;
*at += 1;
let mut links = Vec::with_capacity(link_cnt as usize);
for _ in 0..link_cnt {
let rel = take_str(payload, at)?;
let dst = take_str(payload, at)?;
links.push((rel, dst));
}
let vector = take_vec_f32(payload, at)?;
let meta_cnt = take_u32(payload, at)?;
let mut metadata = Vec::new();
for _ in 0..meta_cnt {
let k = take_str(payload, at)?;
let v = take_str(payload, at)?;
metadata.push((k, v));
}
Op::Remember {
now,
valid_from,
entity,
text,
tags,
links,
vector,
metadata,
revises,
assigned,
}
}
3 => Op::Forget {
now: take_u64(payload, at)?,
fact: FactId(take_u32(payload, at)?),
},
4 => {
let now = take_u64(payload, at)?;
let provenance = FactId(take_u32(payload, at)?);
let src = take_str(payload, at)?;
let rel = take_str(payload, at)?;
let dst = take_str(payload, at)?;
Op::Link {
now,
src,
rel,
dst,
provenance,
}
}
5 => Op::Maintain {
now: take_u64(payload, at)?,
},
_ => return Err(Error::Corrupt("unknown journal op")),
};
if *at != payload.len() {
return Err(Error::Corrupt("journal record has trailing bytes"));
}
Ok(decoded)
}
}
pub fn scan(journal: &[u8]) -> Result<JournalScan<'_>, Error> {
let mut entries = Vec::new();
let mut pos = 0usize;
while pos < journal.len() {
let rest = &journal[pos..];
if rest.len() < HEADER {
return Ok(JournalScan {
entries,
truncated_tail: true,
});
}
let len = u32::from_le_bytes(rest[..U32_BYTES].try_into().unwrap()) as usize;
if len == 0 {
return Err(Error::Corrupt("journal record with zero length"));
}
let Some(body) = HEADER
.checked_add(len)
.and_then(|end| rest.get(HEADER..end))
else {
return Ok(JournalScan {
entries,
truncated_tail: true,
});
};
let want = u32::from_le_bytes(rest[U32_BYTES..HEADER].try_into().unwrap());
if body_checksum(body) != want {
if pos + HEADER + len == journal.len() {
return Ok(JournalScan {
entries,
truncated_tail: true,
});
}
return Err(Error::Corrupt("journal checksum mismatch mid-stream"));
}
entries.push(JournalEntry {
op: body[0],
payload: &body[1..],
});
pos += HEADER + len;
}
Ok(JournalScan {
entries,
truncated_tail: false,
})
}