1use crate::{JournalEntry, JournalError, JournalHead, StoredDatumRef};
2use sha2::{Digest, Sha256};
3use sim_kernel::{ContentId, Datum, Symbol};
4use sim_storage_port::HostDirPort;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct BackendCapabilities {
9 pub linearizable_cas: bool,
11 pub durable_publish: bool,
13}
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum NativeFormatId {
18 V1,
20 V2,
22}
23
24#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
26pub struct EntryNamespace(pub String);
27
28#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
30pub struct EntryLocation {
31 pub namespace: EntryNamespace,
32 pub sequence: u64,
33 pub entry: ContentId,
34 pub storage: ContentId,
35}
36
37#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct VerifiedNativePrefixRef {
40 pub entries: u64,
41 pub physical_head: JournalHead,
42 pub canonical_head: JournalHead,
43 pub descriptor: ContentId,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
48pub struct NativeStateEnvelope {
49 pub format: NativeFormatId,
50 pub fence: u64,
51 pub namespace: EntryNamespace,
52 pub prefix: Option<VerifiedNativePrefixRef>,
53 pub head: Option<JournalHead>,
54 pub head_location: Option<EntryLocation>,
55}
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq)]
59pub enum Failpoint {
60 BeforePrefixDescriptor,
61 AfterPrefixDescriptor,
62 AfterNamespaceReservation,
63 BeforeFormatCas,
64 AfterFormatCas,
65 BeforeObjectPublish,
66 AfterObjectPublish,
67 AfterDurabilityReceipt,
68 BeforeCas,
69 AfterCas,
70 BeforeAcknowledgement,
71}
72
73impl Failpoint {
74 pub(crate) fn label(self) -> &'static str {
75 match self {
76 Self::BeforePrefixDescriptor => "before-prefix-descriptor",
77 Self::AfterPrefixDescriptor => "after-prefix-descriptor",
78 Self::AfterNamespaceReservation => "after-namespace-reservation",
79 Self::BeforeFormatCas => "before-format-cas",
80 Self::AfterFormatCas => "after-format-cas",
81 Self::BeforeObjectPublish => "before-object-publish",
82 Self::AfterObjectPublish => "after-object-publish",
83 Self::AfterDurabilityReceipt => "after-durability-receipt",
84 Self::BeforeCas => "before-cas",
85 Self::AfterCas => "after-cas",
86 Self::BeforeAcknowledgement => "before-acknowledgement",
87 }
88 }
89}
90
91#[derive(Clone)]
92pub(crate) struct PhysicalEntry {
93 pub(crate) entry: JournalEntry,
94 pub(crate) previous_location: Option<EntryLocation>,
95 pub(crate) payloads: Vec<StoredDatumRef>,
96}
97
98#[derive(Clone)]
99pub(crate) struct V1Entry {
100 pub(crate) id: ContentId,
101 pub(crate) sequence: u64,
102 pub(crate) previous: Option<ContentId>,
103 pub(crate) kind: Symbol,
104 pub(crate) payloads: Vec<ContentId>,
105}
106
107impl V1Entry {
108 pub(crate) fn canonical_id(&self) -> ContentId {
109 let mut hasher = Sha256::new();
110 hasher.update(b"sim-journal-entry-v1\0");
111 hasher.update(self.sequence.to_be_bytes());
112 put_v1_optional_id_hash(&mut hasher, self.previous.as_ref());
113 put_v1_symbol_hash(&mut hasher, &self.kind);
114 hasher.update((self.payloads.len() as u64).to_be_bytes());
115 for id in &self.payloads {
116 put_v1_id_hash(&mut hasher, id);
117 }
118 ContentId::from_bytes(
119 Symbol::qualified("journal", "sha256-entry-v1"),
120 hasher.finalize().into(),
121 )
122 }
123}
124
125pub(crate) fn encode_envelope(value: &NativeStateEnvelope) -> Vec<u8> {
126 let mut out = b"SIMJSTATE2".to_vec();
127 out.extend(value.fence.to_be_bytes());
128 put_text(&mut out, &value.namespace.0);
129 match &value.prefix {
130 Some(prefix) => {
131 out.push(1);
132 out.extend(prefix.entries.to_be_bytes());
133 put_head(&mut out, &prefix.physical_head);
134 put_head(&mut out, &prefix.canonical_head);
135 put_id(&mut out, &prefix.descriptor);
136 }
137 None => out.push(0),
138 }
139 put_optional_head(&mut out, value.head.as_ref());
140 put_optional_location(&mut out, value.head_location.as_ref());
141 out
142}
143
144pub(crate) fn decode_envelope(bytes: &[u8]) -> Result<NativeStateEnvelope, JournalError> {
145 let mut cursor = Cursor::new(bytes);
146 if cursor.take(10)? != b"SIMJSTATE2" {
147 return Err(JournalError::CorruptState("state format"));
148 }
149 let fence = cursor.u64()?;
150 let namespace = EntryNamespace(cursor.text()?);
151 let prefix = match cursor.byte()? {
152 0 => None,
153 1 => Some(VerifiedNativePrefixRef {
154 entries: cursor.u64()?,
155 physical_head: cursor.head()?,
156 canonical_head: cursor.head()?,
157 descriptor: cursor.id()?,
158 }),
159 _ => return Err(JournalError::CorruptState("prefix tag")),
160 };
161 let head = cursor.optional_head()?;
162 let head_location = cursor.optional_location()?;
163 cursor.end()?;
164 if prefix.is_none() && head.is_some() && head_location.is_none() {
165 return Err(JournalError::CorruptState("head without locator"));
166 }
167 Ok(NativeStateEnvelope {
168 format: NativeFormatId::V2,
169 fence,
170 namespace,
171 prefix,
172 head,
173 head_location,
174 })
175}
176
177pub(crate) fn encode_descriptor(old_state: &[u8], canonical_head: &JournalHead) -> Vec<u8> {
178 let mut out = b"SIMJPREFIX1".to_vec();
179 out.extend((old_state.len() as u64).to_be_bytes());
180 out.extend(old_state);
181 put_head(&mut out, canonical_head);
182 out
183}
184
185pub(crate) fn decode_descriptor(bytes: &[u8]) -> Result<(Vec<u8>, JournalHead), JournalError> {
186 let mut cursor = Cursor::new(bytes);
187 if cursor.take(11)? != b"SIMJPREFIX1" {
188 return Err(JournalError::CorruptState("prefix descriptor"));
189 }
190 let len = usize::try_from(cursor.u64()?).map_err(|_| JournalError::WorkBoundExceeded)?;
191 let state = cursor.take(len)?.to_vec();
192 let canonical_head = cursor.head()?;
193 cursor.end()?;
194 Ok((state, canonical_head))
195}
196
197pub(crate) fn encode_v2_entry(value: &PhysicalEntry) -> Vec<u8> {
198 let mut out = b"SIMJENTRY2".to_vec();
199 let datum = crate::datum_codec::encode(&value.entry.canonical_datum())
200 .expect("canonical entry datum encodes");
201 out.extend((datum.len() as u64).to_be_bytes());
202 out.extend(datum);
203 put_optional_location(&mut out, value.previous_location.as_ref());
204 out.extend((value.payloads.len() as u64).to_be_bytes());
205 for reference in &value.payloads {
206 put_id(&mut out, &reference.meaning);
207 put_id(&mut out, &reference.storage);
208 }
209 out
210}
211
212pub(crate) fn decode_v2_entry(bytes: &[u8]) -> Result<PhysicalEntry, JournalError> {
213 let mut cursor = Cursor::new(bytes);
214 if cursor.take(10)? != b"SIMJENTRY2" {
215 return Err(JournalError::CorruptState("entry format"));
216 }
217 let datum_len = usize::try_from(cursor.u64()?).map_err(|_| JournalError::WorkBoundExceeded)?;
218 let datum = crate::datum_codec::decode(cursor.take(datum_len)?)?;
219 let entry = entry_from_datum(datum)?;
220 let previous_location = cursor.optional_location()?;
221 let count = usize::try_from(cursor.u64()?).map_err(|_| JournalError::WorkBoundExceeded)?;
222 let mut payloads = Vec::with_capacity(count);
223 for _ in 0..count {
224 payloads.push(StoredDatumRef {
225 meaning: cursor.id()?,
226 storage: cursor.id()?,
227 });
228 }
229 cursor.end()?;
230 Ok(PhysicalEntry {
231 entry,
232 previous_location,
233 payloads,
234 })
235}
236
237pub(crate) fn entry_from_datum(datum: Datum) -> Result<JournalEntry, JournalError> {
238 let id = datum.content_id().map_err(|_| JournalError::CorruptEntry)?;
239 let Datum::Node { tag, fields } = datum else {
240 return Err(JournalError::CorruptEntry);
241 };
242 if tag != Symbol::qualified("journal", "entry-v2") || fields.len() != 4 {
243 return Err(JournalError::CorruptEntry);
244 }
245 let field = |name: &str| {
246 fields
247 .iter()
248 .find(|(field, _)| *field == Symbol::new(name))
249 .map(|(_, value)| value)
250 .ok_or(JournalError::CorruptEntry)
251 };
252 let sequence = match field("sequence")? {
253 Datum::Number(number)
254 if number.domain == Symbol::qualified("numbers", "u64")
255 && number
256 .canonical
257 .parse::<u64>()
258 .ok()
259 .is_some_and(|value| value.to_string() == number.canonical) =>
260 {
261 number
262 .canonical
263 .parse()
264 .map_err(|_| JournalError::CorruptEntry)?
265 }
266 _ => return Err(JournalError::CorruptEntry),
267 };
268 let previous = match field("previous")? {
269 Datum::Nil => None,
270 value => Some(id_from_datum(value)?),
271 };
272 let kind = match field("kind")? {
273 Datum::Symbol(kind) => kind.clone(),
274 _ => return Err(JournalError::CorruptEntry),
275 };
276 let payloads = match field("payloads")? {
277 Datum::Vector(values) => values.iter().map(id_from_datum).collect::<Result<_, _>>()?,
278 _ => return Err(JournalError::CorruptEntry),
279 };
280 let entry = JournalEntry {
281 id,
282 sequence,
283 previous,
284 kind,
285 payloads,
286 };
287 if entry.canonical_id()? != entry.id {
288 return Err(JournalError::CorruptEntry);
289 }
290 Ok(entry)
291}
292
293pub(crate) fn id_from_datum(value: &Datum) -> Result<ContentId, JournalError> {
294 let Datum::Node { tag, fields } = value else {
295 return Err(JournalError::CorruptEntry);
296 };
297 if *tag != Symbol::qualified("journal", "content-id-v1") || fields.len() != 2 {
298 return Err(JournalError::CorruptEntry);
299 }
300 let algorithm = fields
301 .iter()
302 .find_map(|(name, value)| (*name == Symbol::new("algorithm")).then_some(value));
303 let digest = fields
304 .iter()
305 .find_map(|(name, value)| (*name == Symbol::new("digest")).then_some(value));
306 let (Some(Datum::Symbol(algorithm)), Some(Datum::Bytes(bytes))) = (algorithm, digest) else {
307 return Err(JournalError::CorruptEntry);
308 };
309 let digest: [u8; 32] = bytes
310 .as_slice()
311 .try_into()
312 .map_err(|_| JournalError::CorruptEntry)?;
313 Ok(ContentId::from_bytes(algorithm.clone(), digest))
314}
315
316pub(crate) fn decode_v1_state(bytes: &[u8]) -> Result<(u64, Option<JournalHead>), JournalError> {
317 let mut cursor = Cursor::new(bytes);
318 if cursor.take(10)? != b"SIMJSTATE1" {
319 return Err(JournalError::CorruptState("state format"));
320 }
321 let fence = cursor.u64()?;
322 let head = cursor.optional_head()?;
323 cursor.end()?;
324 Ok((fence, head))
325}
326
327pub(crate) fn decode_v1_entry(bytes: &[u8]) -> Result<V1Entry, JournalError> {
328 let mut cursor = Cursor::new(bytes);
329 if cursor.take(10)? != b"SIMJENTRY1" {
330 return Err(JournalError::CorruptState("entry format"));
331 }
332 let id = cursor.id()?;
333 let sequence = cursor.u64()?;
334 let previous = match cursor.byte()? {
335 0 => None,
336 1 => Some(cursor.id()?),
337 _ => return Err(JournalError::CorruptState("entry tag")),
338 };
339 let kind = parse_symbol(&cursor.text()?)?;
340 let count = cursor.u32()? as usize;
341 let mut payloads = Vec::with_capacity(count);
342 for _ in 0..count {
343 payloads.push(cursor.id()?);
344 }
345 cursor.end()?;
346 Ok(V1Entry {
347 id,
348 sequence,
349 previous,
350 kind,
351 payloads,
352 })
353}
354
355pub(crate) fn v1_object_id(bytes: &[u8]) -> ContentId {
356 ContentId::from_bytes(
357 Symbol::qualified("journal", "sha256-bytes-v1"),
358 Sha256::digest(bytes).into(),
359 )
360}
361
362pub(crate) fn put_v1_optional_id_hash(hasher: &mut Sha256, id: Option<&ContentId>) {
363 match id {
364 Some(id) => {
365 hasher.update([1]);
366 put_v1_id_hash(hasher, id);
367 }
368 None => hasher.update([0]),
369 }
370}
371
372pub(crate) fn put_v1_id_hash(hasher: &mut Sha256, id: &ContentId) {
373 put_v1_symbol_hash(hasher, &id.algorithm);
374 hasher.update(id.bytes);
375}
376
377pub(crate) fn put_v1_symbol_hash(hasher: &mut Sha256, symbol: &Symbol) {
378 let text = symbol.as_qualified_str();
379 hasher.update((text.len() as u64).to_be_bytes());
380 hasher.update(text.as_bytes());
381}
382
383pub(crate) fn namespace_root(namespace: &EntryNamespace) -> Vec<String> {
384 vec!["namespaces-v2".into(), namespace.0.clone()]
385}
386
387pub(crate) fn ensure_entry_dirs(
388 port: &dyn HostDirPort,
389 location: &EntryLocation,
390) -> Result<(), JournalError> {
391 let root = [namespace_root(&location.namespace), vec!["entries".into()]].concat();
392 let sequence = [root, vec![format!("{:016x}", location.sequence)]].concat();
393 port.create_dir(&sequence).map_err(port_error)?;
394 port.create_dir(&[sequence, vec![id_key(&location.entry)]].concat())
395 .map_err(port_error)
396}
397
398pub(crate) fn entry_path(location: &EntryLocation) -> Vec<String> {
399 vec![
400 "namespaces-v2".into(),
401 location.namespace.0.clone(),
402 "entries".into(),
403 format!("{:016x}", location.sequence),
404 id_key(&location.entry),
405 id_key(&location.storage),
406 ]
407}
408
409pub(crate) fn object_path(reference: &StoredDatumRef) -> Vec<String> {
410 vec![
411 "objects-v2".into(),
412 id_key(&reference.meaning),
413 id_key(&reference.storage),
414 ]
415}
416
417pub(crate) fn descriptor_path(id: &ContentId) -> Vec<String> {
418 vec!["compat-v1".into(), id_key(id)]
419}
420
421pub(crate) fn v1_entry_path(sequence: u64) -> Vec<String> {
422 vec!["entries".into(), format!("{sequence:016x}")]
423}
424
425pub(crate) fn v1_object_path(id: &ContentId) -> Vec<String> {
426 vec!["objects".into(), hex(&id.bytes)]
427}
428
429pub(crate) fn id_key(id: &ContentId) -> String {
430 format!(
431 "{}-{}",
432 hex(id.algorithm.as_qualified_str().as_bytes()),
433 hex(&id.bytes)
434 )
435}
436
437pub(crate) fn parse_id_key(text: &str) -> Result<ContentId, JournalError> {
438 let (algorithm, digest) = text
439 .split_once('-')
440 .ok_or(JournalError::CorruptState("id path"))?;
441 let algorithm = String::from_utf8(unhex(algorithm)?)
442 .map_err(|_| JournalError::CorruptState("id path utf8"))?;
443 let digest = unhex(digest)?;
444 let bytes: [u8; 32] = digest
445 .try_into()
446 .map_err(|_| JournalError::CorruptState("id path digest"))?;
447 Ok(ContentId::from_bytes(parse_symbol(&algorithm)?, bytes))
448}
449
450pub(crate) fn hex(bytes: &[u8]) -> String {
451 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
452}
453
454pub(crate) fn unhex(text: &str) -> Result<Vec<u8>, JournalError> {
455 if !text.len().is_multiple_of(2) {
456 return Err(JournalError::CorruptState("hex path"));
457 }
458 text.as_bytes()
459 .chunks_exact(2)
460 .map(|pair| {
461 let high = (pair[0] as char)
462 .to_digit(16)
463 .ok_or(JournalError::CorruptState("hex path"))?;
464 let low = (pair[1] as char)
465 .to_digit(16)
466 .ok_or(JournalError::CorruptState("hex path"))?;
467 Ok(((high << 4) | low) as u8)
468 })
469 .collect()
470}
471
472pub(crate) fn put_head(out: &mut Vec<u8>, head: &JournalHead) {
473 out.extend(head.sequence.to_be_bytes());
474 put_id(out, &head.entry);
475}
476
477pub(crate) fn put_optional_head(out: &mut Vec<u8>, head: Option<&JournalHead>) {
478 match head {
479 Some(head) => {
480 out.push(1);
481 put_head(out, head);
482 }
483 None => out.push(0),
484 }
485}
486
487pub(crate) fn put_optional_location(out: &mut Vec<u8>, location: Option<&EntryLocation>) {
488 match location {
489 Some(location) => {
490 out.push(1);
491 put_text(out, &location.namespace.0);
492 out.extend(location.sequence.to_be_bytes());
493 put_id(out, &location.entry);
494 put_id(out, &location.storage);
495 }
496 None => out.push(0),
497 }
498}
499
500pub(crate) fn put_id(out: &mut Vec<u8>, id: &ContentId) {
501 put_text(out, &id.algorithm.as_qualified_str());
502 out.extend(id.bytes);
503}
504
505pub(crate) fn put_text(out: &mut Vec<u8>, text: &str) {
506 out.extend((text.len() as u32).to_be_bytes());
507 out.extend(text.as_bytes());
508}
509
510pub(crate) fn parse_symbol(text: &str) -> Result<Symbol, JournalError> {
511 match text.split_once('/') {
512 Some((namespace, name)) if !namespace.is_empty() && !name.is_empty() => {
513 Ok(Symbol::qualified(namespace, name))
514 }
515 None => Symbol::checked(text).map_err(|_| JournalError::CorruptState("symbol")),
516 _ => Err(JournalError::CorruptState("symbol")),
517 }
518}
519
520pub(crate) fn port_error(error: sim_storage_port::HostDirError) -> JournalError {
521 JournalError::Backend(error.to_string())
522}
523
524struct Cursor<'a> {
525 bytes: &'a [u8],
526 at: usize,
527}
528
529impl<'a> Cursor<'a> {
530 fn new(bytes: &'a [u8]) -> Self {
531 Self { bytes, at: 0 }
532 }
533 fn take(&mut self, len: usize) -> Result<&'a [u8], JournalError> {
534 let end = self
535 .at
536 .checked_add(len)
537 .ok_or(JournalError::CorruptState("length"))?;
538 let value = self
539 .bytes
540 .get(self.at..end)
541 .ok_or(JournalError::CorruptState("truncated"))?;
542 self.at = end;
543 Ok(value)
544 }
545 fn byte(&mut self) -> Result<u8, JournalError> {
546 Ok(self.take(1)?[0])
547 }
548 fn u32(&mut self) -> Result<u32, JournalError> {
549 Ok(u32::from_be_bytes(
550 self.take(4)?
551 .try_into()
552 .map_err(|_| JournalError::CorruptState("u32"))?,
553 ))
554 }
555 fn u64(&mut self) -> Result<u64, JournalError> {
556 Ok(u64::from_be_bytes(
557 self.take(8)?
558 .try_into()
559 .map_err(|_| JournalError::CorruptState("u64"))?,
560 ))
561 }
562 fn text(&mut self) -> Result<String, JournalError> {
563 let len = self.u32()? as usize;
564 String::from_utf8(self.take(len)?.to_vec()).map_err(|_| JournalError::CorruptState("utf8"))
565 }
566 fn id(&mut self) -> Result<ContentId, JournalError> {
567 let algorithm = parse_symbol(&self.text()?)?;
568 let bytes = self
569 .take(32)?
570 .try_into()
571 .map_err(|_| JournalError::CorruptState("id"))?;
572 Ok(ContentId::from_bytes(algorithm, bytes))
573 }
574 fn head(&mut self) -> Result<JournalHead, JournalError> {
575 Ok(JournalHead {
576 sequence: self.u64()?,
577 entry: self.id()?,
578 })
579 }
580 fn optional_head(&mut self) -> Result<Option<JournalHead>, JournalError> {
581 match self.byte()? {
582 0 => Ok(None),
583 1 => Ok(Some(self.head()?)),
584 _ => Err(JournalError::CorruptState("head tag")),
585 }
586 }
587 fn optional_location(&mut self) -> Result<Option<EntryLocation>, JournalError> {
588 match self.byte()? {
589 0 => Ok(None),
590 1 => Ok(Some(EntryLocation {
591 namespace: EntryNamespace(self.text()?),
592 sequence: self.u64()?,
593 entry: self.id()?,
594 storage: self.id()?,
595 })),
596 _ => Err(JournalError::CorruptState("location tag")),
597 }
598 }
599 fn end(&self) -> Result<(), JournalError> {
600 if self.at == self.bytes.len() {
601 Ok(())
602 } else {
603 Err(JournalError::CorruptState("trailing bytes"))
604 }
605 }
606}