1use 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#[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#[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}