1use alloc::vec::Vec;
29
30use xxhash_rust::xxh3::xxh3_64;
31
32use crate::error::Error;
33
34const U32_BYTES: usize = core::mem::size_of::<u32>();
36const U64_BYTES: usize = core::mem::size_of::<u64>();
38const F32_BYTES: usize = core::mem::size_of::<f32>();
40
41const HEADER: usize = U32_BYTES + U32_BYTES;
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize))]
47pub struct JournalEntry<'a> {
48 pub op: u8,
50 pub payload: &'a [u8],
52}
53
54#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct JournalScan<'a> {
57 pub entries: Vec<JournalEntry<'a>>,
59 pub truncated_tail: bool,
62}
63
64fn body_checksum(body: &[u8]) -> u32 {
67 xxh3_64(body) as u32
68}
69
70pub fn encode_entry(out: &mut Vec<u8>, op: u8, payload: &[u8]) {
73 let len = 1 + payload.len();
74 let len32 = u32::try_from(len).expect("journal payload fits u32 by construction");
75 out.reserve(HEADER + len);
76 out.extend_from_slice(&len32.to_le_bytes());
77 let check_pos = out.len();
80 out.extend_from_slice(&[0u8; U32_BYTES]);
81 out.push(op);
82 out.extend_from_slice(payload);
83 let check = body_checksum(&out[check_pos + U32_BYTES..]);
84 out[check_pos..check_pos + U32_BYTES].copy_from_slice(&check.to_le_bytes());
85}
86
87#[derive(Clone, Debug, PartialEq)]
94#[cfg_attr(feature = "serde", derive(serde::Serialize))]
95pub enum Op<'a> {
96 Remember {
98 now: u64,
100 valid_from: u64,
103 entity: Option<&'a str>,
105 text: &'a str,
107 tags: Vec<&'a str>,
109 links: Vec<(&'a str, &'a str)>,
111 vector: Vec<f32>,
115 metadata: Vec<(&'a str, &'a str)>,
119 revises: crate::id::FactId,
121 assigned: crate::id::FactId,
124 },
125 Forget {
127 now: u64,
129 fact: crate::id::FactId,
131 },
132 Link {
134 now: u64,
136 src: &'a str,
138 rel: &'a str,
140 dst: &'a str,
142 provenance: crate::id::FactId,
144 },
145 Maintain {
147 now: u64,
149 mode: u8,
151 max_hnsw_inserts: u32,
153 },
154 Unlink {
156 now: u64,
158 src: &'a str,
160 rel: &'a str,
162 dst: &'a str,
164 },
165}
166
167fn put_str(out: &mut Vec<u8>, s: &str) {
169 out.extend_from_slice(&(s.len() as u32).to_le_bytes());
170 out.extend_from_slice(s.as_bytes());
171}
172
173fn take_str<'a>(bytes: &'a [u8], at: &mut usize) -> Result<&'a str, Error> {
175 let len = take_u32(bytes, at)? as usize;
176 let end = at
177 .checked_add(len)
178 .filter(|&e| e <= bytes.len())
179 .ok_or(Error::Corrupt("journal string overruns its record"))?;
180 let s = core::str::from_utf8(&bytes[*at..end])
181 .map_err(|_| Error::Corrupt("journal string is not UTF-8"))?;
182 *at = end;
183 Ok(s)
184}
185
186fn take_u32(bytes: &[u8], at: &mut usize) -> Result<u32, Error> {
188 let end = *at + U32_BYTES;
189 if end > bytes.len() {
190 return Err(Error::Corrupt("journal record truncated inside a field"));
191 }
192 let v = u32::from_le_bytes(bytes[*at..end].try_into().unwrap());
193 *at = end;
194 Ok(v)
195}
196
197fn take_u64(bytes: &[u8], at: &mut usize) -> Result<u64, Error> {
199 let end = *at + U64_BYTES;
200 if end > bytes.len() {
201 return Err(Error::Corrupt("journal record truncated inside a field"));
202 }
203 let v = u64::from_le_bytes(bytes[*at..end].try_into().unwrap());
204 *at = end;
205 Ok(v)
206}
207
208fn take_vec_f32(bytes: &[u8], at: &mut usize) -> Result<Vec<f32>, Error> {
210 let count = take_u32(bytes, at)? as usize;
211 let end = *at as u64 + count as u64 * F32_BYTES as u64;
217 if end > bytes.len() as u64 {
218 return Err(Error::Corrupt("journal vector overruns its record"));
219 }
220 let end = end as usize;
221 let mut v = Vec::with_capacity(count);
222 let mut p = *at;
223 while p < end {
224 v.push(f32::from_le_bytes(
225 bytes[p..p + F32_BYTES].try_into().unwrap(),
226 ));
227 p += F32_BYTES;
228 }
229 *at = end;
230 Ok(v)
231}
232
233impl<'a> Op<'a> {
234 pub fn encode(&self, out: &mut Vec<u8>) {
237 let mut payload = Vec::new();
238 let op = match self {
239 Op::Remember {
240 now,
241 valid_from,
242 entity,
243 text,
244 tags,
245 links,
246 vector,
247 metadata,
248 revises,
249 assigned,
250 } => {
251 payload.extend_from_slice(&now.to_le_bytes());
252 payload.extend_from_slice(&valid_from.to_le_bytes());
253 payload.extend_from_slice(&revises.0.to_le_bytes());
254 payload.extend_from_slice(&assigned.0.to_le_bytes());
255 match entity {
256 Some(name) => {
257 payload.push(1);
258 put_str(&mut payload, name);
259 }
260 None => payload.push(0),
261 }
262 put_str(&mut payload, text);
263 payload.push(tags.len() as u8);
264 for tag in tags {
265 put_str(&mut payload, tag);
266 }
267 payload.push(links.len() as u8);
268 for (rel, dst) in links {
269 put_str(&mut payload, rel);
270 put_str(&mut payload, dst);
271 }
272 payload.extend_from_slice(&(vector.len() as u32).to_le_bytes());
273 for &x in vector {
274 payload.extend_from_slice(&x.to_le_bytes());
275 }
276 payload.extend_from_slice(&(metadata.len() as u32).to_le_bytes());
277 for (k, v) in metadata {
278 put_str(&mut payload, k);
279 put_str(&mut payload, v);
280 }
281 if revises.is_none() { 1 } else { 2 }
282 }
283 Op::Forget { now, fact } => {
284 payload.extend_from_slice(&now.to_le_bytes());
285 payload.extend_from_slice(&fact.0.to_le_bytes());
286 3
287 }
288 Op::Link {
289 now,
290 src,
291 rel,
292 dst,
293 provenance,
294 } => {
295 payload.extend_from_slice(&now.to_le_bytes());
296 payload.extend_from_slice(&provenance.0.to_le_bytes());
297 put_str(&mut payload, src);
298 put_str(&mut payload, rel);
299 put_str(&mut payload, dst);
300 4
301 }
302 Op::Unlink { now, src, rel, dst } => {
303 payload.extend_from_slice(&now.to_le_bytes());
304 put_str(&mut payload, src);
305 put_str(&mut payload, rel);
306 put_str(&mut payload, dst);
307 6
308 }
309 Op::Maintain {
310 now,
311 mode,
312 max_hnsw_inserts,
313 } => {
314 payload.extend_from_slice(&now.to_le_bytes());
315 payload.push(*mode);
316 payload.extend_from_slice(&max_hnsw_inserts.to_le_bytes());
317 5
318 }
319 };
320 encode_entry(out, op, &payload);
321 }
322
323 pub fn decode(op: u8, payload: &'a [u8]) -> Result<Op<'a>, Error> {
328 use crate::id::FactId;
329 let at = &mut 0usize;
330 let decoded = match op {
331 1 | 2 => {
332 let now = take_u64(payload, at)?;
333 let valid_from = take_u64(payload, at)?;
334 let revises = FactId(take_u32(payload, at)?);
335 let assigned = FactId(take_u32(payload, at)?);
336 if (op == 2) == revises.is_none() {
337 return Err(Error::Corrupt("journal revises field disagrees with op"));
338 }
339 let entity = match payload.get(*at) {
340 Some(0) => {
341 *at += 1;
342 None
343 }
344 Some(1) => {
345 *at += 1;
346 Some(take_str(payload, at)?)
347 }
348 _ => return Err(Error::Corrupt("journal entity flag is invalid")),
349 };
350 let text = take_str(payload, at)?;
351 let tag_cnt = *payload
352 .get(*at)
353 .ok_or(Error::Corrupt("journal record truncated inside a field"))?;
354 *at += 1;
355 let mut tags = Vec::with_capacity(tag_cnt as usize);
356 for _ in 0..tag_cnt {
357 tags.push(take_str(payload, at)?);
358 }
359 let link_cnt = *payload
360 .get(*at)
361 .ok_or(Error::Corrupt("journal record truncated inside a field"))?;
362 *at += 1;
363 let mut links = Vec::with_capacity(link_cnt as usize);
364 for _ in 0..link_cnt {
365 let rel = take_str(payload, at)?;
366 let dst = take_str(payload, at)?;
367 links.push((rel, dst));
368 }
369 let vector = take_vec_f32(payload, at)?;
370 let meta_cnt = take_u32(payload, at)?;
371 let mut metadata = Vec::new();
372 for _ in 0..meta_cnt {
373 let k = take_str(payload, at)?;
374 let v = take_str(payload, at)?;
375 metadata.push((k, v));
376 }
377 Op::Remember {
378 now,
379 valid_from,
380 entity,
381 text,
382 tags,
383 links,
384 vector,
385 metadata,
386 revises,
387 assigned,
388 }
389 }
390 3 => Op::Forget {
391 now: take_u64(payload, at)?,
392 fact: FactId(take_u32(payload, at)?),
393 },
394 4 => {
395 let now = take_u64(payload, at)?;
396 let provenance = FactId(take_u32(payload, at)?);
397 let src = take_str(payload, at)?;
398 let rel = take_str(payload, at)?;
399 let dst = take_str(payload, at)?;
400 Op::Link {
401 now,
402 src,
403 rel,
404 dst,
405 provenance,
406 }
407 }
408 5 => {
409 let now = take_u64(payload, at)?;
410 let (mode, max_hnsw_inserts) = if *at == payload.len() {
411 (0, u32::MAX)
412 } else {
413 let mode = *payload
414 .get(*at)
415 .ok_or(Error::Corrupt("journal record truncated inside a field"))?;
416 *at += 1;
417 let max_hnsw_inserts = take_u32(payload, at)?;
418 (mode, max_hnsw_inserts)
419 };
420 Op::Maintain {
421 now,
422 mode,
423 max_hnsw_inserts,
424 }
425 }
426 6 => {
427 let now = take_u64(payload, at)?;
428 let src = take_str(payload, at)?;
429 let rel = take_str(payload, at)?;
430 let dst = take_str(payload, at)?;
431 Op::Unlink { now, src, rel, dst }
432 }
433 _ => return Err(Error::Corrupt("unknown journal op")),
434 };
435 if *at != payload.len() {
436 return Err(Error::Corrupt("journal record has trailing bytes"));
437 }
438 Ok(decoded)
439 }
440}
441
442pub fn scan(journal: &[u8]) -> Result<JournalScan<'_>, Error> {
445 let mut entries = Vec::new();
446 let mut pos = 0usize;
447 while pos < journal.len() {
448 let rest = &journal[pos..];
449 if rest.len() < HEADER {
450 return Ok(JournalScan {
451 entries,
452 truncated_tail: true,
453 });
454 }
455 let len = u32::from_le_bytes(rest[..U32_BYTES].try_into().unwrap()) as usize;
456 if len == 0 {
457 return Err(Error::Corrupt("journal record with zero length"));
458 }
459 let Some(body) = HEADER
465 .checked_add(len)
466 .and_then(|end| rest.get(HEADER..end))
467 else {
468 return Ok(JournalScan {
469 entries,
470 truncated_tail: true,
471 });
472 };
473 let want = u32::from_le_bytes(rest[U32_BYTES..HEADER].try_into().unwrap());
474 if body_checksum(body) != want {
475 if pos + HEADER + len == journal.len() {
476 return Ok(JournalScan {
477 entries,
478 truncated_tail: true,
479 });
480 }
481 return Err(Error::Corrupt("journal checksum mismatch mid-stream"));
482 }
483 entries.push(JournalEntry {
484 op: body[0],
485 payload: &body[1..],
486 });
487 pos += HEADER + len;
488 }
489 Ok(JournalScan {
490 entries,
491 truncated_tail: false,
492 })
493}