1use std::fmt;
21use std::time::Duration;
22
23use crate::ModelError;
24
25pub const MAX_SOURCE_PINS: usize = 32;
27pub const INCARNATION_BYTES: usize = 32;
29pub const DIGEST_BYTES: usize = 32;
31pub const COMMIT_ROOT_BYTES: usize = 32;
33pub const ATTESTATION_SIGNATURE_BYTES: usize = 64;
35pub const ATTESTATION_SIGNER_BYTES: usize = 32;
37
38fn push_len_prefixed(buffer: &mut Vec<u8>, bytes: &[u8]) {
39 buffer.extend_from_slice(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_be_bytes());
40 buffer.extend_from_slice(bytes);
41}
42
43const fn non_empty(field: &'static str, value: &str) -> Result<(), ModelError> {
44 if value.is_empty() {
45 return Err(ModelError::Bounds(field));
46 }
47 Ok(())
48}
49
50#[derive(Clone, PartialEq, Eq)]
52pub struct JournalSource {
53 partition: String,
54 incarnation: [u8; INCARNATION_BYTES],
55}
56
57impl JournalSource {
58 pub fn try_new(
64 partition: String,
65 incarnation: [u8; INCARNATION_BYTES],
66 ) -> Result<Self, ModelError> {
67 non_empty("partition", &partition)?;
68 Ok(Self {
69 partition,
70 incarnation,
71 })
72 }
73
74 #[must_use]
76 pub fn partition(&self) -> &str {
77 &self.partition
78 }
79
80 #[must_use]
82 pub const fn incarnation(&self) -> &[u8; INCARNATION_BYTES] {
83 &self.incarnation
84 }
85}
86
87#[derive(Clone, PartialEq, Eq)]
89pub struct JournalAttestation {
90 root: [u8; COMMIT_ROOT_BYTES],
91 leaf_count: u64,
92 signature: [u8; ATTESTATION_SIGNATURE_BYTES],
93 signer: [u8; ATTESTATION_SIGNER_BYTES],
94}
95
96impl JournalAttestation {
97 #[must_use]
99 pub const fn new(
100 root: [u8; COMMIT_ROOT_BYTES],
101 leaf_count: u64,
102 signature: [u8; ATTESTATION_SIGNATURE_BYTES],
103 signer: [u8; ATTESTATION_SIGNER_BYTES],
104 ) -> Self {
105 Self {
106 root,
107 leaf_count,
108 signature,
109 signer,
110 }
111 }
112
113 #[must_use]
115 pub const fn root(&self) -> &[u8; COMMIT_ROOT_BYTES] {
116 &self.root
117 }
118
119 #[must_use]
121 pub const fn leaf_count(&self) -> u64 {
122 self.leaf_count
123 }
124
125 #[must_use]
127 pub const fn signature(&self) -> &[u8; ATTESTATION_SIGNATURE_BYTES] {
128 &self.signature
129 }
130
131 #[must_use]
133 pub const fn signer(&self) -> &[u8; ATTESTATION_SIGNER_BYTES] {
134 &self.signer
135 }
136}
137
138#[derive(Clone, PartialEq, Eq)]
140pub struct SourceCheckpoint {
141 source: JournalSource,
142 feed_position: u64,
143 journal_position: u64,
144 evidence_leaf: u64,
145 covering_attestation: JournalAttestation,
146}
147
148impl SourceCheckpoint {
149 #[must_use]
151 pub const fn new(
152 source: JournalSource,
153 feed_position: u64,
154 journal_position: u64,
155 evidence_leaf: u64,
156 covering_attestation: JournalAttestation,
157 ) -> Self {
158 Self {
159 source,
160 feed_position,
161 journal_position,
162 evidence_leaf,
163 covering_attestation,
164 }
165 }
166
167 #[must_use]
169 pub const fn source(&self) -> &JournalSource {
170 &self.source
171 }
172
173 #[must_use]
175 pub const fn feed_position(&self) -> u64 {
176 self.feed_position
177 }
178
179 #[must_use]
181 pub const fn journal_position(&self) -> u64 {
182 self.journal_position
183 }
184
185 #[must_use]
187 pub const fn evidence_leaf(&self) -> u64 {
188 self.evidence_leaf
189 }
190
191 #[must_use]
193 pub const fn covering_attestation(&self) -> &JournalAttestation {
194 &self.covering_attestation
195 }
196}
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum Retention {
201 For(Duration),
203 UntilReleased,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
209pub enum Classification {
210 Public,
212 Internal,
214 Confidential,
216 Restricted,
218}
219
220#[derive(Clone, PartialEq, Eq)]
222pub struct ObjectDescriptor {
223 object: String,
224 generation: u64,
225 digest: [u8; DIGEST_BYTES],
226 owner: String,
227 classification: Classification,
228 retention: Retention,
229 byte_len: u64,
230 content_reference: String,
231}
232
233impl ObjectDescriptor {
234 #[allow(
241 clippy::too_many_arguments,
242 reason = "every field of the signed descriptor is named explicitly"
243 )]
244 pub fn try_new(
245 object: String,
246 generation: u64,
247 digest: [u8; DIGEST_BYTES],
248 owner: String,
249 classification: Classification,
250 retention: Retention,
251 byte_len: u64,
252 content_reference: String,
253 ) -> Result<Self, ModelError> {
254 non_empty("object", &object)?;
255 non_empty("owner", &owner)?;
256 non_empty("content_reference", &content_reference)?;
257 Ok(Self {
258 object,
259 generation,
260 digest,
261 owner,
262 classification,
263 retention,
264 byte_len,
265 content_reference,
266 })
267 }
268
269 #[must_use]
271 pub fn object(&self) -> &str {
272 &self.object
273 }
274
275 #[must_use]
277 pub const fn generation(&self) -> u64 {
278 self.generation
279 }
280
281 #[must_use]
283 pub const fn digest(&self) -> &[u8; DIGEST_BYTES] {
284 &self.digest
285 }
286
287 #[must_use]
289 pub fn owner(&self) -> &str {
290 &self.owner
291 }
292
293 #[must_use]
295 pub const fn classification(&self) -> Classification {
296 self.classification
297 }
298
299 #[must_use]
301 pub const fn retention(&self) -> Retention {
302 self.retention
303 }
304
305 #[must_use]
307 pub const fn byte_len(&self) -> u64 {
308 self.byte_len
309 }
310
311 #[must_use]
313 pub fn content_reference(&self) -> &str {
314 &self.content_reference
315 }
316}
317
318#[derive(Clone, PartialEq, Eq)]
320pub struct ExactObjectRef {
321 namespace: String,
322 key: String,
323 backend_generation: u64,
324}
325
326impl ExactObjectRef {
327 pub fn try_new(
333 namespace: String,
334 key: String,
335 backend_generation: u64,
336 ) -> Result<Self, ModelError> {
337 non_empty("namespace", &namespace)?;
338 non_empty("key", &key)?;
339 Ok(Self {
340 namespace,
341 key,
342 backend_generation,
343 })
344 }
345
346 #[must_use]
348 pub fn namespace(&self) -> &str {
349 &self.namespace
350 }
351
352 #[must_use]
354 pub fn key(&self) -> &str {
355 &self.key
356 }
357
358 #[must_use]
360 pub const fn backend_generation(&self) -> u64 {
361 self.backend_generation
362 }
363}
364
365#[derive(Clone, PartialEq, Eq)]
367pub struct ProjectionKey {
368 family: String,
369 source_partition: String,
370}
371
372impl ProjectionKey {
373 pub fn try_new(family: String, source_partition: String) -> Result<Self, ModelError> {
379 non_empty("family", &family)?;
380 non_empty("source_partition", &source_partition)?;
381 Ok(Self {
382 family,
383 source_partition,
384 })
385 }
386
387 #[must_use]
389 pub fn family(&self) -> &str {
390 &self.family
391 }
392
393 #[must_use]
395 pub fn source_partition(&self) -> &str {
396 &self.source_partition
397 }
398}
399
400#[derive(Clone, PartialEq, Eq)]
402pub struct PublisherFence {
403 key: ProjectionKey,
404 source_incarnation: [u8; INCARNATION_BYTES],
405 term: u64,
406}
407
408impl PublisherFence {
409 pub fn try_new(
415 key: ProjectionKey,
416 source_incarnation: [u8; INCARNATION_BYTES],
417 term: u64,
418 ) -> Result<Self, ModelError> {
419 if term == 0 {
420 return Err(ModelError::Bounds("term"));
421 }
422 Ok(Self {
423 key,
424 source_incarnation,
425 term,
426 })
427 }
428
429 #[must_use]
431 pub const fn key(&self) -> &ProjectionKey {
432 &self.key
433 }
434
435 #[must_use]
437 pub const fn source_incarnation(&self) -> &[u8; INCARNATION_BYTES] {
438 &self.source_incarnation
439 }
440
441 #[must_use]
443 pub const fn term(&self) -> u64 {
444 self.term
445 }
446}
447
448#[derive(Clone, PartialEq, Eq)]
450pub struct ProjectionManifest {
451 key: ProjectionKey,
452 generation: u64,
453 checkpoint: SourceCheckpoint,
454 schema_version: u32,
455 fact_version: u32,
456 object: ObjectDescriptor,
457 artifact_object: ExactObjectRef,
458 publisher: String,
459 fence: PublisherFence,
460}
461
462impl ProjectionManifest {
463 #[allow(
470 clippy::too_many_arguments,
471 reason = "every field of the signed manifest is named explicitly"
472 )]
473 pub fn try_new(
474 key: ProjectionKey,
475 generation: u64,
476 checkpoint: SourceCheckpoint,
477 schema_version: u32,
478 fact_version: u32,
479 object: ObjectDescriptor,
480 artifact_object: ExactObjectRef,
481 publisher: String,
482 fence: PublisherFence,
483 ) -> Result<Self, ModelError> {
484 if generation == 0 {
485 return Err(ModelError::Bounds("generation"));
486 }
487 if schema_version == 0 {
488 return Err(ModelError::Bounds("schema_version"));
489 }
490 if fact_version == 0 {
491 return Err(ModelError::Bounds("fact_version"));
492 }
493 non_empty("publisher", &publisher)?;
494 Ok(Self {
495 key,
496 generation,
497 checkpoint,
498 schema_version,
499 fact_version,
500 object,
501 artifact_object,
502 publisher,
503 fence,
504 })
505 }
506
507 #[must_use]
509 pub const fn key(&self) -> &ProjectionKey {
510 &self.key
511 }
512
513 #[must_use]
515 pub const fn generation(&self) -> u64 {
516 self.generation
517 }
518
519 #[must_use]
521 pub const fn checkpoint(&self) -> &SourceCheckpoint {
522 &self.checkpoint
523 }
524
525 #[must_use]
527 pub const fn schema_version(&self) -> u32 {
528 self.schema_version
529 }
530
531 #[must_use]
533 pub const fn fact_version(&self) -> u32 {
534 self.fact_version
535 }
536
537 #[must_use]
539 pub const fn object(&self) -> &ObjectDescriptor {
540 &self.object
541 }
542
543 #[must_use]
545 pub const fn artifact_object(&self) -> &ExactObjectRef {
546 &self.artifact_object
547 }
548
549 #[must_use]
551 pub fn publisher(&self) -> &str {
552 &self.publisher
553 }
554
555 #[must_use]
557 pub const fn fence(&self) -> &PublisherFence {
558 &self.fence
559 }
560}
561
562#[derive(Clone, PartialEq, Eq)]
564pub struct JournalAnchor {
565 source: JournalSource,
566 head: u64,
567}
568
569impl JournalAnchor {
570 #[must_use]
572 pub const fn new(source: JournalSource, head: u64) -> Self {
573 Self { source, head }
574 }
575
576 #[must_use]
578 pub const fn source(&self) -> &JournalSource {
579 &self.source
580 }
581
582 #[must_use]
584 pub const fn head(&self) -> u64 {
585 self.head
586 }
587}
588
589#[derive(Clone, PartialEq, Eq)]
591pub enum SourcePin {
592 Projected(Box<ProjectionManifest>),
598 Journal(JournalAnchor),
600 Authoritative(u64),
602}
603
604impl SourcePin {
605 #[must_use]
613 pub fn identity_bytes(&self) -> Vec<u8> {
614 let mut bytes = Vec::new();
615 match self {
616 Self::Projected(manifest) => {
617 bytes.push(0);
618 push_len_prefixed(&mut bytes, manifest.key().family().as_bytes());
619 push_len_prefixed(&mut bytes, manifest.key().source_partition().as_bytes());
620 }
621 Self::Journal(anchor) => {
622 bytes.push(1);
623 push_len_prefixed(&mut bytes, anchor.source().partition().as_bytes());
624 }
625 Self::Authoritative(_) => bytes.push(2),
626 }
627 bytes
628 }
629}
630
631#[derive(Clone, PartialEq, Eq)]
633pub struct SourceEvidence {
634 pins: Vec<SourcePin>,
635}
636
637impl SourceEvidence {
638 pub fn try_new(pins: Vec<SourcePin>) -> Result<Self, ModelError> {
647 if pins.len() > MAX_SOURCE_PINS {
648 return Err(ModelError::Bounds("source_pins"));
649 }
650 for pair in pins.windows(2) {
651 if pair[0].identity_bytes() >= pair[1].identity_bytes() {
652 return Err(ModelError::Order("source_pins"));
653 }
654 }
655 Ok(Self { pins })
656 }
657
658 #[must_use]
660 pub fn pins(&self) -> &[SourcePin] {
661 &self.pins
662 }
663}
664
665impl fmt::Debug for JournalSource {
676 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
677 formatter.write_str("JournalSource")
678 }
679}
680
681impl fmt::Debug for JournalAttestation {
682 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
683 formatter
684 .debug_struct("JournalAttestation")
685 .field("leaf_count", &self.leaf_count)
686 .finish_non_exhaustive()
687 }
688}
689
690impl fmt::Debug for SourceCheckpoint {
691 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
692 formatter
693 .debug_struct("SourceCheckpoint")
694 .field("feed_position", &self.feed_position)
695 .field("journal_position", &self.journal_position)
696 .finish_non_exhaustive()
697 }
698}
699
700impl fmt::Debug for ObjectDescriptor {
701 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
702 formatter
703 .debug_struct("ObjectDescriptor")
704 .field("generation", &self.generation)
705 .field("classification", &self.classification)
706 .field("byte_len", &self.byte_len)
707 .finish_non_exhaustive()
708 }
709}
710
711impl fmt::Debug for ExactObjectRef {
712 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
713 formatter
714 .debug_struct("ExactObjectRef")
715 .field("backend_generation", &self.backend_generation)
716 .finish_non_exhaustive()
717 }
718}
719
720impl fmt::Debug for ProjectionKey {
721 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
722 formatter.write_str("ProjectionKey")
723 }
724}
725
726impl fmt::Debug for PublisherFence {
727 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
728 formatter
729 .debug_struct("PublisherFence")
730 .field("term", &self.term)
731 .finish_non_exhaustive()
732 }
733}
734
735impl fmt::Debug for ProjectionManifest {
736 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
737 formatter
738 .debug_struct("ProjectionManifest")
739 .field("generation", &self.generation)
740 .field("schema_version", &self.schema_version)
741 .field("fact_version", &self.fact_version)
742 .finish_non_exhaustive()
743 }
744}
745
746impl fmt::Debug for JournalAnchor {
747 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
748 formatter
749 .debug_struct("JournalAnchor")
750 .field("head", &self.head)
751 .finish_non_exhaustive()
752 }
753}
754
755impl fmt::Debug for SourcePin {
756 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
757 formatter.write_str(match self {
758 Self::Projected(_) => "projected",
759 Self::Journal(_) => "journal",
760 Self::Authoritative(_) => "authoritative",
761 })
762 }
763}
764
765impl fmt::Debug for SourceEvidence {
766 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
767 formatter
768 .debug_struct("SourceEvidence")
769 .field("pins", &self.pins.len())
770 .finish()
771 }
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 fn journal(partition: &str) -> SourcePin {
779 SourcePin::Journal(JournalAnchor::new(
780 JournalSource::try_new(partition.to_owned(), [7; INCARNATION_BYTES]).unwrap(),
781 4,
782 ))
783 }
784
785 #[test]
786 fn pins_must_be_strictly_ascending_and_bounded() {
787 assert!(SourceEvidence::try_new(vec![journal("a"), journal("b")]).is_ok());
788 assert_eq!(
789 SourceEvidence::try_new(vec![journal("b"), journal("a")]),
790 Err(ModelError::Order("source_pins"))
791 );
792 assert_eq!(
793 SourceEvidence::try_new(vec![journal("a"), journal("a")]),
794 Err(ModelError::Order("source_pins")),
795 "one partition may be anchored once"
796 );
797
798 let overflowing = (0..=MAX_SOURCE_PINS)
799 .map(|index| journal(&format!("p{index:04}")))
800 .collect();
801 assert_eq!(
802 SourceEvidence::try_new(overflowing),
803 Err(ModelError::Bounds("source_pins"))
804 );
805 }
806
807 #[test]
808 fn identity_ignores_everything_except_the_source_it_names() {
809 let first = SourcePin::Journal(JournalAnchor::new(
810 JournalSource::try_new("a".to_owned(), [1; INCARNATION_BYTES]).unwrap(),
811 1,
812 ));
813 let second = SourcePin::Journal(JournalAnchor::new(
814 JournalSource::try_new("a".to_owned(), [2; INCARNATION_BYTES]).unwrap(),
815 9,
816 ));
817 assert_eq!(first.identity_bytes(), second.identity_bytes());
818 }
819
820 #[test]
821 fn structural_bounds_refuse_empty_identifiers() {
822 assert_eq!(
823 JournalSource::try_new(String::new(), [0; INCARNATION_BYTES]),
824 Err(ModelError::Bounds("partition"))
825 );
826 assert_eq!(
827 ProjectionKey::try_new(String::new(), "p".to_owned()),
828 Err(ModelError::Bounds("family"))
829 );
830 assert_eq!(
831 ExactObjectRef::try_new("ns".to_owned(), String::new(), 1),
832 Err(ModelError::Bounds("key"))
833 );
834 }
835}