Skip to main content

lodestone_types/
operation_locator.rs

1//! Opaque typed locators for per-authority operations and immutable artifacts.
2//!
3//! These locators are presentation identities only. They never authorize an
4//! operation, select a collection, or become a global completion ledger. Every
5//! consumer must route to the named existing authority and reauthorize the
6//! authenticated principal before reading or mutating state.
7
8use std::error::Error;
9use std::fmt::{self, Display, Formatter};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{CollectionId, MutationLifecycleError, MutationReplayIdentity, RecoveryEra, crc32c};
14
15pub const OPERATION_LOCATOR_FORMAT_VERSION: u16 = 2;
16pub const ARTIFACT_LOCATOR_FORMAT_VERSION: u16 = 2;
17pub const ARTIFACT_CHUNK_ALIGNMENT: u64 = 4 * 1024;
18pub const MAX_ARTIFACT_CHUNK_BYTES: u32 = 1024 * 1024;
19
20const OPERATION_MAGIC: &[u8; 8] = b"LDOPLOC2";
21const OPERATION_LOCATOR_BYTES: usize = 208;
22const OPERATION_LOCATOR_BODY_BYTES: usize = OPERATION_LOCATOR_BYTES - 4;
23const ARTIFACT_MAGIC: &[u8; 8] = b"LDARLOC2";
24const ARTIFACT_LOCATOR_BYTES: usize = 224;
25const ARTIFACT_LOCATOR_BODY_BYTES: usize = ARTIFACT_LOCATOR_BYTES - 4;
26
27#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
28#[serde(rename_all = "snake_case")]
29pub enum OperationAuthorityKindV1 {
30    Catalog,
31    Administration,
32    Security,
33    Backup,
34    Maintenance,
35    Diagnostic,
36    Portability,
37}
38
39impl OperationAuthorityKindV1 {
40    const fn tag(self) -> u8 {
41        match self {
42            Self::Catalog => 1,
43            Self::Administration => 2,
44            Self::Security => 3,
45            Self::Backup => 4,
46            Self::Maintenance => 5,
47            Self::Diagnostic => 6,
48            Self::Portability => 7,
49        }
50    }
51
52    fn from_tag(tag: u8) -> Result<Self, OperationLocatorError> {
53        match tag {
54            1 => Ok(Self::Catalog),
55            2 => Ok(Self::Administration),
56            3 => Ok(Self::Security),
57            4 => Ok(Self::Backup),
58            5 => Ok(Self::Maintenance),
59            6 => Ok(Self::Diagnostic),
60            7 => Ok(Self::Portability),
61            value => Err(OperationLocatorError::UnsupportedAuthority(value)),
62        }
63    }
64}
65
66/// Opaque-to-clients locator for one receipt owned by one named authority.
67#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
68#[serde(deny_unknown_fields)]
69pub struct OperationLocatorV1 {
70    format_version: u16,
71    authority: OperationAuthorityKindV1,
72    collection_id: CollectionId,
73    authority_identity: [u8; 32],
74    operation_identity: [u8; 32],
75    recovery_era: RecoveryEra,
76    authority_revision: u64,
77    receipt_ordinal: u64,
78    semantic_digest: [u8; 32],
79    submitted_at: u64,
80    expires_at: u64,
81    resource_digest: [u8; 32],
82}
83
84impl OperationLocatorV1 {
85    #[allow(clippy::too_many_arguments)]
86    pub fn new(
87        authority: OperationAuthorityKindV1,
88        collection_id: CollectionId,
89        authority_identity: [u8; 32],
90        operation_identity: [u8; 32],
91        recovery_era: RecoveryEra,
92        authority_revision: u64,
93        receipt_ordinal: u64,
94        semantic_digest: [u8; 32],
95        submitted_at: u64,
96        expires_at: u64,
97        resource_digest: [u8; 32],
98    ) -> Result<Self, OperationLocatorError> {
99        let locator = Self {
100            format_version: OPERATION_LOCATOR_FORMAT_VERSION,
101            authority,
102            collection_id,
103            authority_identity,
104            operation_identity,
105            recovery_era,
106            authority_revision,
107            receipt_ordinal,
108            semantic_digest,
109            submitted_at,
110            expires_at,
111            resource_digest,
112        };
113        locator.validate()?;
114        Ok(locator)
115    }
116
117    #[must_use]
118    pub const fn authority(&self) -> OperationAuthorityKindV1 {
119        self.authority
120    }
121
122    #[must_use]
123    pub const fn collection_id(&self) -> CollectionId {
124        self.collection_id
125    }
126
127    #[must_use]
128    pub const fn authority_identity(&self) -> [u8; 32] {
129        self.authority_identity
130    }
131
132    #[must_use]
133    pub const fn operation_identity(&self) -> [u8; 32] {
134        self.operation_identity
135    }
136
137    #[must_use]
138    pub const fn recovery_era(&self) -> RecoveryEra {
139        self.recovery_era
140    }
141
142    #[must_use]
143    pub const fn authority_revision(&self) -> u64 {
144        self.authority_revision
145    }
146
147    #[must_use]
148    pub const fn receipt_ordinal(&self) -> u64 {
149        self.receipt_ordinal
150    }
151
152    #[must_use]
153    pub const fn semantic_digest(&self) -> [u8; 32] {
154        self.semantic_digest
155    }
156
157    #[must_use]
158    pub const fn submitted_at(&self) -> u64 {
159        self.submitted_at
160    }
161
162    #[must_use]
163    pub const fn expires_at(&self) -> u64 {
164        self.expires_at
165    }
166
167    #[must_use]
168    pub const fn resource_digest(&self) -> [u8; 32] {
169        self.resource_digest
170    }
171
172    #[must_use]
173    pub fn binding_digest(&self) -> [u8; 32] {
174        *blake3::hash(&self.encode()).as_bytes()
175    }
176
177    pub fn mutation_replay_identity(&self) -> Result<MutationReplayIdentity, MutationLifecycleError> {
178        let mut authority = blake3::Hasher::new();
179        authority.update(b"lodestone/coordinated-operation-mutation-authority/v1\0");
180        authority.update(&[self.authority.tag()]);
181        authority.update(&self.collection_id.to_be_bytes());
182        authority.update(&self.authority_identity);
183        authority.update(&self.recovery_era.to_be_bytes());
184        MutationReplayIdentity::new(
185            *authority.finalize().as_bytes(),
186            self.operation_identity,
187            self.semantic_digest,
188        )
189    }
190
191    #[must_use]
192    pub fn encode(&self) -> Vec<u8> {
193        let mut bytes = Vec::with_capacity(OPERATION_LOCATOR_BYTES);
194        bytes.extend_from_slice(OPERATION_MAGIC);
195        bytes.extend_from_slice(&OPERATION_LOCATOR_FORMAT_VERSION.to_be_bytes());
196        bytes.push(self.authority.tag());
197        bytes.push(0);
198        bytes.extend_from_slice(&self.collection_id.to_be_bytes());
199        bytes.extend_from_slice(&self.authority_identity);
200        bytes.extend_from_slice(&self.operation_identity);
201        bytes.extend_from_slice(&self.recovery_era.to_be_bytes());
202        bytes.extend_from_slice(&self.authority_revision.to_be_bytes());
203        bytes.extend_from_slice(&self.receipt_ordinal.to_be_bytes());
204        bytes.extend_from_slice(&self.semantic_digest);
205        bytes.extend_from_slice(&self.submitted_at.to_be_bytes());
206        bytes.extend_from_slice(&self.expires_at.to_be_bytes());
207        bytes.extend_from_slice(&self.resource_digest);
208        let checksum = crc32c(&bytes);
209        bytes.extend_from_slice(&checksum.to_be_bytes());
210        bytes
211    }
212
213    pub fn decode(bytes: &[u8]) -> Result<Self, OperationLocatorError> {
214        require_fixed_envelope(
215            bytes,
216            OPERATION_MAGIC,
217            OPERATION_LOCATOR_BYTES,
218            OPERATION_LOCATOR_BODY_BYTES,
219        )?;
220        let format_version = read_u16(bytes, 8)?;
221        if format_version != OPERATION_LOCATOR_FORMAT_VERSION {
222            return Err(OperationLocatorError::UnsupportedOperationVersion(format_version));
223        }
224        if read_byte(bytes, 11)? != 0 {
225            return Err(OperationLocatorError::NonCanonicalReservedByte);
226        }
227        let locator = Self {
228            format_version,
229            authority: OperationAuthorityKindV1::from_tag(read_byte(bytes, 10)?)?,
230            collection_id: CollectionId::from_be_bytes(read_array(bytes, 12)?)
231                .map_err(|_| OperationLocatorError::InvalidCollection)?,
232            authority_identity: read_array(bytes, 28)?,
233            operation_identity: read_array(bytes, 60)?,
234            recovery_era: RecoveryEra::from_be_bytes(read_array(bytes, 92)?)
235                .map_err(|_| OperationLocatorError::InvalidRecoveryEra)?,
236            authority_revision: read_u64(bytes, 108)?,
237            receipt_ordinal: read_u64(bytes, 116)?,
238            semantic_digest: read_array(bytes, 124)?,
239            submitted_at: read_u64(bytes, 156)?,
240            expires_at: read_u64(bytes, 164)?,
241            resource_digest: read_array(bytes, 172)?,
242        };
243        locator.validate()?;
244        Ok(locator)
245    }
246
247    pub fn validate_current(&self) -> Result<(), OperationLocatorError> {
248        self.validate()
249    }
250
251    fn validate(&self) -> Result<(), OperationLocatorError> {
252        if self.format_version != OPERATION_LOCATOR_FORMAT_VERSION {
253            return Err(OperationLocatorError::UnsupportedOperationVersion(self.format_version));
254        }
255        if self.authority_identity == [0; 32] || self.operation_identity == [0; 32] {
256            return Err(OperationLocatorError::InvalidOperationIdentity);
257        }
258        if self.authority_revision == 0 || self.receipt_ordinal == 0 {
259            return Err(OperationLocatorError::InvalidAuthorityPosition);
260        }
261        if self.semantic_digest == [0; 32] || self.resource_digest == [0; 32] {
262            return Err(OperationLocatorError::InvalidDigest);
263        }
264        if self.expires_at <= self.submitted_at {
265            return Err(OperationLocatorError::InvalidExpiry);
266        }
267        Ok(())
268    }
269}
270
271/// Public artifact locator. It deliberately carries no backend or object key.
272#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
273#[serde(deny_unknown_fields)]
274pub struct ArtifactLocatorV1 {
275    format_version: u16,
276    authority: OperationAuthorityKindV1,
277    collection_id: CollectionId,
278    authority_identity: [u8; 32],
279    recovery_era: RecoveryEra,
280    operation_identity: [u8; 32],
281    operation_binding_digest: [u8; 32],
282    artifact_identity: [u8; 32],
283    bytes: u64,
284    sha256: [u8; 32],
285    expires_at: u64,
286}
287
288impl ArtifactLocatorV1 {
289    pub fn new(
290        operation: &OperationLocatorV1,
291        artifact_identity: [u8; 32],
292        bytes: u64,
293        sha256: [u8; 32],
294        expires_at: u64,
295    ) -> Result<Self, OperationLocatorError> {
296        operation.validate()?;
297        let locator = Self {
298            format_version: ARTIFACT_LOCATOR_FORMAT_VERSION,
299            authority: operation.authority,
300            collection_id: operation.collection_id,
301            authority_identity: operation.authority_identity,
302            recovery_era: operation.recovery_era,
303            operation_identity: operation.operation_identity,
304            operation_binding_digest: operation.binding_digest(),
305            artifact_identity,
306            bytes,
307            sha256,
308            expires_at,
309        };
310        locator.validate()?;
311        Ok(locator)
312    }
313
314    #[must_use]
315    pub const fn authority(&self) -> OperationAuthorityKindV1 {
316        self.authority
317    }
318
319    #[must_use]
320    pub const fn collection_id(&self) -> CollectionId {
321        self.collection_id
322    }
323
324    #[must_use]
325    pub const fn authority_identity(&self) -> [u8; 32] {
326        self.authority_identity
327    }
328
329    #[must_use]
330    pub const fn recovery_era(&self) -> RecoveryEra {
331        self.recovery_era
332    }
333
334    #[must_use]
335    pub const fn operation_identity(&self) -> [u8; 32] {
336        self.operation_identity
337    }
338
339    #[must_use]
340    pub const fn operation_binding_digest(&self) -> [u8; 32] {
341        self.operation_binding_digest
342    }
343
344    #[must_use]
345    pub const fn artifact_identity(&self) -> [u8; 32] {
346        self.artifact_identity
347    }
348
349    #[must_use]
350    pub const fn bytes(&self) -> u64 {
351        self.bytes
352    }
353
354    #[must_use]
355    pub const fn sha256(&self) -> [u8; 32] {
356        self.sha256
357    }
358
359    #[must_use]
360    pub const fn expires_at(&self) -> u64 {
361        self.expires_at
362    }
363
364    pub fn require_operation(&self, operation: &OperationLocatorV1) -> Result<(), OperationLocatorError> {
365        operation.validate()?;
366        if self.authority != operation.authority
367            || self.collection_id != operation.collection_id
368            || self.authority_identity != operation.authority_identity
369            || self.recovery_era != operation.recovery_era
370            || self.operation_identity != operation.operation_identity
371            || self.operation_binding_digest != operation.binding_digest()
372        {
373            return Err(OperationLocatorError::OperationBindingMismatch);
374        }
375        Ok(())
376    }
377
378    #[must_use]
379    pub fn encode(&self) -> Vec<u8> {
380        let mut bytes = Vec::with_capacity(ARTIFACT_LOCATOR_BYTES);
381        bytes.extend_from_slice(ARTIFACT_MAGIC);
382        bytes.extend_from_slice(&ARTIFACT_LOCATOR_FORMAT_VERSION.to_be_bytes());
383        bytes.push(self.authority.tag());
384        bytes.push(0);
385        bytes.extend_from_slice(&self.collection_id.to_be_bytes());
386        bytes.extend_from_slice(&self.authority_identity);
387        bytes.extend_from_slice(&self.recovery_era.to_be_bytes());
388        bytes.extend_from_slice(&self.operation_identity);
389        bytes.extend_from_slice(&self.operation_binding_digest);
390        bytes.extend_from_slice(&self.artifact_identity);
391        bytes.extend_from_slice(&self.bytes.to_be_bytes());
392        bytes.extend_from_slice(&self.sha256);
393        bytes.extend_from_slice(&self.expires_at.to_be_bytes());
394        let checksum = crc32c(&bytes);
395        bytes.extend_from_slice(&checksum.to_be_bytes());
396        bytes
397    }
398
399    pub fn decode(bytes: &[u8]) -> Result<Self, OperationLocatorError> {
400        require_fixed_envelope(
401            bytes,
402            ARTIFACT_MAGIC,
403            ARTIFACT_LOCATOR_BYTES,
404            ARTIFACT_LOCATOR_BODY_BYTES,
405        )?;
406        let format_version = read_u16(bytes, 8)?;
407        if format_version != ARTIFACT_LOCATOR_FORMAT_VERSION {
408            return Err(OperationLocatorError::UnsupportedArtifactVersion(format_version));
409        }
410        if read_byte(bytes, 11)? != 0 {
411            return Err(OperationLocatorError::NonCanonicalReservedByte);
412        }
413        let locator = Self {
414            format_version,
415            authority: OperationAuthorityKindV1::from_tag(read_byte(bytes, 10)?)?,
416            collection_id: CollectionId::from_be_bytes(read_array(bytes, 12)?)
417                .map_err(|_| OperationLocatorError::InvalidCollection)?,
418            authority_identity: read_array(bytes, 28)?,
419            recovery_era: RecoveryEra::from_be_bytes(read_array(bytes, 60)?)
420                .map_err(|_| OperationLocatorError::InvalidRecoveryEra)?,
421            operation_identity: read_array(bytes, 76)?,
422            operation_binding_digest: read_array(bytes, 108)?,
423            artifact_identity: read_array(bytes, 140)?,
424            bytes: read_u64(bytes, 172)?,
425            sha256: read_array(bytes, 180)?,
426            expires_at: read_u64(bytes, 212)?,
427        };
428        locator.validate()?;
429        Ok(locator)
430    }
431
432    pub fn validate_current(&self) -> Result<(), OperationLocatorError> {
433        self.validate()
434    }
435
436    fn validate(&self) -> Result<(), OperationLocatorError> {
437        if self.format_version != ARTIFACT_LOCATOR_FORMAT_VERSION {
438            return Err(OperationLocatorError::UnsupportedArtifactVersion(self.format_version));
439        }
440        if self.authority_identity == [0; 32]
441            || self.operation_identity == [0; 32]
442            || self.operation_binding_digest == [0; 32]
443            || self.artifact_identity == [0; 32]
444            || self.sha256 == [0; 32]
445        {
446            return Err(OperationLocatorError::InvalidDigest);
447        }
448        if self.bytes == 0 || self.expires_at == 0 {
449            return Err(OperationLocatorError::InvalidArtifactBounds);
450        }
451        Ok(())
452    }
453}
454
455#[derive(Clone, Copy, Debug, Eq, PartialEq)]
456pub struct ArtifactReadRangeV1 {
457    pub offset: u64,
458    pub length: u32,
459}
460
461impl ArtifactReadRangeV1 {
462    pub fn new(offset: u64, length: u32, total_bytes: u64) -> Result<Self, OperationLocatorError> {
463        if !offset.is_multiple_of(ARTIFACT_CHUNK_ALIGNMENT) || !(1..=MAX_ARTIFACT_CHUNK_BYTES).contains(&length) {
464            return Err(OperationLocatorError::InvalidArtifactRange);
465        }
466        let end = offset
467            .checked_add(u64::from(length))
468            .ok_or(OperationLocatorError::ArtifactRangeOverflow)?;
469        if offset >= total_bytes
470            || end > total_bytes
471            || (end < total_bytes && !u64::from(length).is_multiple_of(ARTIFACT_CHUNK_ALIGNMENT))
472        {
473            return Err(OperationLocatorError::InvalidArtifactRange);
474        }
475        Ok(Self { offset, length })
476    }
477
478    pub fn end(self) -> Result<u64, OperationLocatorError> {
479        self.offset
480            .checked_add(u64::from(self.length))
481            .ok_or(OperationLocatorError::ArtifactRangeOverflow)
482    }
483}
484
485#[derive(Clone, Debug, Eq, PartialEq)]
486pub struct VerifiedArtifactChunkV1 {
487    pub offset: u64,
488    pub bytes: Vec<u8>,
489    pub chunk_sha256: [u8; 32],
490    pub artifact_sha256: [u8; 32],
491    pub next_offset: Option<u64>,
492    pub complete: bool,
493}
494
495pub fn verify_artifact_chunk_v1(
496    artifact: &ArtifactLocatorV1,
497    range: ArtifactReadRangeV1,
498    bytes: Vec<u8>,
499    authority_artifact_sha256: [u8; 32],
500) -> Result<VerifiedArtifactChunkV1, OperationLocatorError> {
501    artifact.validate()?;
502    let canonical_range = ArtifactReadRangeV1::new(range.offset, range.length, artifact.bytes)?;
503    if bytes.len() != range.length as usize || authority_artifact_sha256 != artifact.sha256 {
504        return Err(OperationLocatorError::ArtifactDigestMismatch);
505    }
506    let chunk_sha256: [u8; 32] = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &bytes)
507        .as_ref()
508        .try_into()
509        .map_err(|_| OperationLocatorError::ArtifactDigestMismatch)?;
510    let end = canonical_range.end()?;
511    let complete = end == artifact.bytes;
512    if range.offset == 0 && complete && chunk_sha256 != artifact.sha256 {
513        return Err(OperationLocatorError::ArtifactDigestMismatch);
514    }
515    Ok(VerifiedArtifactChunkV1 {
516        offset: range.offset,
517        bytes,
518        chunk_sha256,
519        artifact_sha256: artifact.sha256,
520        next_offset: (!complete).then_some(end),
521        complete,
522    })
523}
524
525fn require_fixed_envelope(
526    bytes: &[u8],
527    magic: &[u8; 8],
528    expected_length: usize,
529    body_length: usize,
530) -> Result<(), OperationLocatorError> {
531    if bytes.len() != expected_length {
532        return Err(OperationLocatorError::InvalidLength {
533            expected: expected_length,
534            actual: bytes.len(),
535        });
536    }
537    if bytes.get(..8) != Some(magic) {
538        return Err(OperationLocatorError::BadMagic);
539    }
540    let expected = read_u32(bytes, body_length)?;
541    let actual = crc32c(bytes.get(..body_length).ok_or(OperationLocatorError::Truncated)?);
542    if expected != actual {
543        return Err(OperationLocatorError::Checksum { expected, actual });
544    }
545    Ok(())
546}
547
548fn read_byte(bytes: &[u8], offset: usize) -> Result<u8, OperationLocatorError> {
549    bytes.get(offset).copied().ok_or(OperationLocatorError::Truncated)
550}
551
552fn read_array<const N: usize>(bytes: &[u8], offset: usize) -> Result<[u8; N], OperationLocatorError> {
553    bytes
554        .get(offset..offset.checked_add(N).ok_or(OperationLocatorError::Truncated)?)
555        .ok_or(OperationLocatorError::Truncated)?
556        .try_into()
557        .map_err(|_| OperationLocatorError::Truncated)
558}
559
560fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, OperationLocatorError> {
561    Ok(u16::from_be_bytes(read_array(bytes, offset)?))
562}
563
564fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, OperationLocatorError> {
565    Ok(u32::from_be_bytes(read_array(bytes, offset)?))
566}
567
568fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, OperationLocatorError> {
569    Ok(u64::from_be_bytes(read_array(bytes, offset)?))
570}
571
572#[derive(Clone, Debug, Eq, PartialEq)]
573pub enum OperationLocatorError {
574    InvalidLength { expected: usize, actual: usize },
575    BadMagic,
576    Truncated,
577    Checksum { expected: u32, actual: u32 },
578    UnsupportedOperationVersion(u16),
579    UnsupportedArtifactVersion(u16),
580    UnsupportedAuthority(u8),
581    NonCanonicalReservedByte,
582    InvalidCollection,
583    InvalidRecoveryEra,
584    InvalidOperationIdentity,
585    InvalidAuthorityPosition,
586    InvalidDigest,
587    InvalidExpiry,
588    InvalidArtifactBounds,
589    OperationBindingMismatch,
590    InvalidArtifactRange,
591    ArtifactRangeOverflow,
592    ArtifactDigestMismatch,
593}
594
595impl Display for OperationLocatorError {
596    fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
597        match self {
598            Self::InvalidLength { expected, actual } => {
599                write!(
600                    formatter,
601                    "locator length {actual} does not match current length {expected}"
602                )
603            }
604            Self::BadMagic => formatter.write_str("locator magic is not current"),
605            Self::Truncated => formatter.write_str("locator is truncated"),
606            Self::Checksum { expected, actual } => {
607                write!(formatter, "locator checksum {actual} does not match {expected}")
608            }
609            Self::UnsupportedOperationVersion(version) => {
610                write!(formatter, "operation locator version {version} is unsupported")
611            }
612            Self::UnsupportedArtifactVersion(version) => {
613                write!(formatter, "artifact locator version {version} is unsupported")
614            }
615            Self::UnsupportedAuthority(authority) => {
616                write!(formatter, "operation authority tag {authority} is unsupported")
617            }
618            Self::NonCanonicalReservedByte => formatter.write_str("locator reserved byte is nonzero"),
619            Self::InvalidCollection => formatter.write_str("locator collection identity is invalid"),
620            Self::InvalidRecoveryEra => formatter.write_str("locator recovery era is invalid"),
621            Self::InvalidOperationIdentity => formatter.write_str("operation identity must be nonzero"),
622            Self::InvalidAuthorityPosition => {
623                formatter.write_str("authority revision and receipt ordinal must be nonzero")
624            }
625            Self::InvalidDigest => formatter.write_str("locator digest must be nonzero"),
626            Self::InvalidExpiry => formatter.write_str("operation expiry must follow submission"),
627            Self::InvalidArtifactBounds => formatter.write_str("artifact size or expiry is invalid"),
628            Self::OperationBindingMismatch => formatter.write_str("artifact is not bound to the supplied operation"),
629            Self::InvalidArtifactRange => {
630                formatter.write_str("artifact range is unaligned, empty, oversized, or outside the artifact")
631            }
632            Self::ArtifactRangeOverflow => formatter.write_str("artifact range overflows u64"),
633            Self::ArtifactDigestMismatch => {
634                formatter.write_str("artifact chunk or full digest does not match authoritative evidence")
635            }
636        }
637    }
638}
639
640impl Error for OperationLocatorError {}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645
646    fn operation() -> Result<OperationLocatorV1, Box<dyn Error>> {
647        Ok(OperationLocatorV1::new(
648            OperationAuthorityKindV1::Diagnostic,
649            CollectionId::new(1)?,
650            [2; 32],
651            [3; 32],
652            RecoveryEra::new(4)?,
653            5,
654            6,
655            [7; 32],
656            8,
657            70_000,
658            [9; 32],
659        )?)
660    }
661
662    #[test]
663    fn operation_and_artifact_locators_round_trip_exactly() -> Result<(), Box<dyn Error>> {
664        let operation = operation()?;
665        assert_eq!(OperationLocatorV1::decode(&operation.encode())?, operation);
666        let artifact = ArtifactLocatorV1::new(&operation, [10; 32], 8_192, [11; 32], 60_000)?;
667        assert_eq!(ArtifactLocatorV1::decode(&artifact.encode())?, artifact);
668        artifact.require_operation(&operation)?;
669        Ok(())
670    }
671
672    #[test]
673    fn malformed_locators_and_ranges_fail_closed() -> Result<(), Box<dyn Error>> {
674        let operation = operation()?;
675        let mut bytes = operation.encode();
676        bytes[40] ^= 1;
677        assert!(OperationLocatorV1::decode(&bytes).is_err());
678        let artifact = ArtifactLocatorV1::new(&operation, [10; 32], 8_193, [11; 32], 60_000)?;
679        assert!(ArtifactReadRangeV1::new(1, 4_096, artifact.bytes()).is_err());
680        assert!(ArtifactReadRangeV1::new(0, 4_095, artifact.bytes()).is_err());
681        let final_range = ArtifactReadRangeV1::new(8_192, 1, artifact.bytes())?;
682        assert_eq!(final_range.end()?, 8_193);
683        Ok(())
684    }
685}