1use std::error::Error;
9use std::fmt::{self, Display, Formatter};
10
11use serde::{Deserialize, Serialize};
12
13use crate::{CollectionId, 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 #[must_use]
178 pub fn encode(&self) -> Vec<u8> {
179 let mut bytes = Vec::with_capacity(OPERATION_LOCATOR_BYTES);
180 bytes.extend_from_slice(OPERATION_MAGIC);
181 bytes.extend_from_slice(&OPERATION_LOCATOR_FORMAT_VERSION.to_be_bytes());
182 bytes.push(self.authority.tag());
183 bytes.push(0);
184 bytes.extend_from_slice(&self.collection_id.to_be_bytes());
185 bytes.extend_from_slice(&self.authority_identity);
186 bytes.extend_from_slice(&self.operation_identity);
187 bytes.extend_from_slice(&self.recovery_era.to_be_bytes());
188 bytes.extend_from_slice(&self.authority_revision.to_be_bytes());
189 bytes.extend_from_slice(&self.receipt_ordinal.to_be_bytes());
190 bytes.extend_from_slice(&self.semantic_digest);
191 bytes.extend_from_slice(&self.submitted_at.to_be_bytes());
192 bytes.extend_from_slice(&self.expires_at.to_be_bytes());
193 bytes.extend_from_slice(&self.resource_digest);
194 let checksum = crc32c(&bytes);
195 bytes.extend_from_slice(&checksum.to_be_bytes());
196 bytes
197 }
198
199 pub fn decode(bytes: &[u8]) -> Result<Self, OperationLocatorError> {
200 require_fixed_envelope(
201 bytes,
202 OPERATION_MAGIC,
203 OPERATION_LOCATOR_BYTES,
204 OPERATION_LOCATOR_BODY_BYTES,
205 )?;
206 let format_version = read_u16(bytes, 8)?;
207 if format_version != OPERATION_LOCATOR_FORMAT_VERSION {
208 return Err(OperationLocatorError::UnsupportedOperationVersion(format_version));
209 }
210 if read_byte(bytes, 11)? != 0 {
211 return Err(OperationLocatorError::NonCanonicalReservedByte);
212 }
213 let locator = Self {
214 format_version,
215 authority: OperationAuthorityKindV1::from_tag(read_byte(bytes, 10)?)?,
216 collection_id: CollectionId::from_be_bytes(read_array(bytes, 12)?)
217 .map_err(|_| OperationLocatorError::InvalidCollection)?,
218 authority_identity: read_array(bytes, 28)?,
219 operation_identity: read_array(bytes, 60)?,
220 recovery_era: RecoveryEra::from_be_bytes(read_array(bytes, 92)?)
221 .map_err(|_| OperationLocatorError::InvalidRecoveryEra)?,
222 authority_revision: read_u64(bytes, 108)?,
223 receipt_ordinal: read_u64(bytes, 116)?,
224 semantic_digest: read_array(bytes, 124)?,
225 submitted_at: read_u64(bytes, 156)?,
226 expires_at: read_u64(bytes, 164)?,
227 resource_digest: read_array(bytes, 172)?,
228 };
229 locator.validate()?;
230 Ok(locator)
231 }
232
233 pub fn validate_current(&self) -> Result<(), OperationLocatorError> {
234 self.validate()
235 }
236
237 fn validate(&self) -> Result<(), OperationLocatorError> {
238 if self.format_version != OPERATION_LOCATOR_FORMAT_VERSION {
239 return Err(OperationLocatorError::UnsupportedOperationVersion(self.format_version));
240 }
241 if self.authority_identity == [0; 32] || self.operation_identity == [0; 32] {
242 return Err(OperationLocatorError::InvalidOperationIdentity);
243 }
244 if self.authority_revision == 0 || self.receipt_ordinal == 0 {
245 return Err(OperationLocatorError::InvalidAuthorityPosition);
246 }
247 if self.semantic_digest == [0; 32] || self.resource_digest == [0; 32] {
248 return Err(OperationLocatorError::InvalidDigest);
249 }
250 if self.expires_at <= self.submitted_at {
251 return Err(OperationLocatorError::InvalidExpiry);
252 }
253 Ok(())
254 }
255}
256
257#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
259#[serde(deny_unknown_fields)]
260pub struct ArtifactLocatorV1 {
261 format_version: u16,
262 authority: OperationAuthorityKindV1,
263 collection_id: CollectionId,
264 authority_identity: [u8; 32],
265 recovery_era: RecoveryEra,
266 operation_identity: [u8; 32],
267 operation_binding_digest: [u8; 32],
268 artifact_identity: [u8; 32],
269 bytes: u64,
270 sha256: [u8; 32],
271 expires_at: u64,
272}
273
274impl ArtifactLocatorV1 {
275 pub fn new(
276 operation: &OperationLocatorV1,
277 artifact_identity: [u8; 32],
278 bytes: u64,
279 sha256: [u8; 32],
280 expires_at: u64,
281 ) -> Result<Self, OperationLocatorError> {
282 operation.validate()?;
283 let locator = Self {
284 format_version: ARTIFACT_LOCATOR_FORMAT_VERSION,
285 authority: operation.authority,
286 collection_id: operation.collection_id,
287 authority_identity: operation.authority_identity,
288 recovery_era: operation.recovery_era,
289 operation_identity: operation.operation_identity,
290 operation_binding_digest: operation.binding_digest(),
291 artifact_identity,
292 bytes,
293 sha256,
294 expires_at,
295 };
296 locator.validate()?;
297 Ok(locator)
298 }
299
300 #[must_use]
301 pub const fn authority(&self) -> OperationAuthorityKindV1 {
302 self.authority
303 }
304
305 #[must_use]
306 pub const fn collection_id(&self) -> CollectionId {
307 self.collection_id
308 }
309
310 #[must_use]
311 pub const fn authority_identity(&self) -> [u8; 32] {
312 self.authority_identity
313 }
314
315 #[must_use]
316 pub const fn recovery_era(&self) -> RecoveryEra {
317 self.recovery_era
318 }
319
320 #[must_use]
321 pub const fn operation_identity(&self) -> [u8; 32] {
322 self.operation_identity
323 }
324
325 #[must_use]
326 pub const fn operation_binding_digest(&self) -> [u8; 32] {
327 self.operation_binding_digest
328 }
329
330 #[must_use]
331 pub const fn artifact_identity(&self) -> [u8; 32] {
332 self.artifact_identity
333 }
334
335 #[must_use]
336 pub const fn bytes(&self) -> u64 {
337 self.bytes
338 }
339
340 #[must_use]
341 pub const fn sha256(&self) -> [u8; 32] {
342 self.sha256
343 }
344
345 #[must_use]
346 pub const fn expires_at(&self) -> u64 {
347 self.expires_at
348 }
349
350 pub fn require_operation(&self, operation: &OperationLocatorV1) -> Result<(), OperationLocatorError> {
351 operation.validate()?;
352 if self.authority != operation.authority
353 || self.collection_id != operation.collection_id
354 || self.authority_identity != operation.authority_identity
355 || self.recovery_era != operation.recovery_era
356 || self.operation_identity != operation.operation_identity
357 || self.operation_binding_digest != operation.binding_digest()
358 {
359 return Err(OperationLocatorError::OperationBindingMismatch);
360 }
361 Ok(())
362 }
363
364 #[must_use]
365 pub fn encode(&self) -> Vec<u8> {
366 let mut bytes = Vec::with_capacity(ARTIFACT_LOCATOR_BYTES);
367 bytes.extend_from_slice(ARTIFACT_MAGIC);
368 bytes.extend_from_slice(&ARTIFACT_LOCATOR_FORMAT_VERSION.to_be_bytes());
369 bytes.push(self.authority.tag());
370 bytes.push(0);
371 bytes.extend_from_slice(&self.collection_id.to_be_bytes());
372 bytes.extend_from_slice(&self.authority_identity);
373 bytes.extend_from_slice(&self.recovery_era.to_be_bytes());
374 bytes.extend_from_slice(&self.operation_identity);
375 bytes.extend_from_slice(&self.operation_binding_digest);
376 bytes.extend_from_slice(&self.artifact_identity);
377 bytes.extend_from_slice(&self.bytes.to_be_bytes());
378 bytes.extend_from_slice(&self.sha256);
379 bytes.extend_from_slice(&self.expires_at.to_be_bytes());
380 let checksum = crc32c(&bytes);
381 bytes.extend_from_slice(&checksum.to_be_bytes());
382 bytes
383 }
384
385 pub fn decode(bytes: &[u8]) -> Result<Self, OperationLocatorError> {
386 require_fixed_envelope(
387 bytes,
388 ARTIFACT_MAGIC,
389 ARTIFACT_LOCATOR_BYTES,
390 ARTIFACT_LOCATOR_BODY_BYTES,
391 )?;
392 let format_version = read_u16(bytes, 8)?;
393 if format_version != ARTIFACT_LOCATOR_FORMAT_VERSION {
394 return Err(OperationLocatorError::UnsupportedArtifactVersion(format_version));
395 }
396 if read_byte(bytes, 11)? != 0 {
397 return Err(OperationLocatorError::NonCanonicalReservedByte);
398 }
399 let locator = Self {
400 format_version,
401 authority: OperationAuthorityKindV1::from_tag(read_byte(bytes, 10)?)?,
402 collection_id: CollectionId::from_be_bytes(read_array(bytes, 12)?)
403 .map_err(|_| OperationLocatorError::InvalidCollection)?,
404 authority_identity: read_array(bytes, 28)?,
405 recovery_era: RecoveryEra::from_be_bytes(read_array(bytes, 60)?)
406 .map_err(|_| OperationLocatorError::InvalidRecoveryEra)?,
407 operation_identity: read_array(bytes, 76)?,
408 operation_binding_digest: read_array(bytes, 108)?,
409 artifact_identity: read_array(bytes, 140)?,
410 bytes: read_u64(bytes, 172)?,
411 sha256: read_array(bytes, 180)?,
412 expires_at: read_u64(bytes, 212)?,
413 };
414 locator.validate()?;
415 Ok(locator)
416 }
417
418 pub fn validate_current(&self) -> Result<(), OperationLocatorError> {
419 self.validate()
420 }
421
422 fn validate(&self) -> Result<(), OperationLocatorError> {
423 if self.format_version != ARTIFACT_LOCATOR_FORMAT_VERSION {
424 return Err(OperationLocatorError::UnsupportedArtifactVersion(self.format_version));
425 }
426 if self.authority_identity == [0; 32]
427 || self.operation_identity == [0; 32]
428 || self.operation_binding_digest == [0; 32]
429 || self.artifact_identity == [0; 32]
430 || self.sha256 == [0; 32]
431 {
432 return Err(OperationLocatorError::InvalidDigest);
433 }
434 if self.bytes == 0 || self.expires_at == 0 {
435 return Err(OperationLocatorError::InvalidArtifactBounds);
436 }
437 Ok(())
438 }
439}
440
441#[derive(Clone, Copy, Debug, Eq, PartialEq)]
442pub struct ArtifactReadRangeV1 {
443 pub offset: u64,
444 pub length: u32,
445}
446
447impl ArtifactReadRangeV1 {
448 pub fn new(offset: u64, length: u32, total_bytes: u64) -> Result<Self, OperationLocatorError> {
449 if !offset.is_multiple_of(ARTIFACT_CHUNK_ALIGNMENT) || !(1..=MAX_ARTIFACT_CHUNK_BYTES).contains(&length) {
450 return Err(OperationLocatorError::InvalidArtifactRange);
451 }
452 let end = offset
453 .checked_add(u64::from(length))
454 .ok_or(OperationLocatorError::ArtifactRangeOverflow)?;
455 if offset >= total_bytes
456 || end > total_bytes
457 || (end < total_bytes && !u64::from(length).is_multiple_of(ARTIFACT_CHUNK_ALIGNMENT))
458 {
459 return Err(OperationLocatorError::InvalidArtifactRange);
460 }
461 Ok(Self { offset, length })
462 }
463
464 pub fn end(self) -> Result<u64, OperationLocatorError> {
465 self.offset
466 .checked_add(u64::from(self.length))
467 .ok_or(OperationLocatorError::ArtifactRangeOverflow)
468 }
469}
470
471#[derive(Clone, Debug, Eq, PartialEq)]
472pub struct VerifiedArtifactChunkV1 {
473 pub offset: u64,
474 pub bytes: Vec<u8>,
475 pub chunk_sha256: [u8; 32],
476 pub artifact_sha256: [u8; 32],
477 pub next_offset: Option<u64>,
478 pub complete: bool,
479}
480
481pub fn verify_artifact_chunk_v1(
482 artifact: &ArtifactLocatorV1,
483 range: ArtifactReadRangeV1,
484 bytes: Vec<u8>,
485 authority_artifact_sha256: [u8; 32],
486) -> Result<VerifiedArtifactChunkV1, OperationLocatorError> {
487 artifact.validate()?;
488 let canonical_range = ArtifactReadRangeV1::new(range.offset, range.length, artifact.bytes)?;
489 if bytes.len() != range.length as usize || authority_artifact_sha256 != artifact.sha256 {
490 return Err(OperationLocatorError::ArtifactDigestMismatch);
491 }
492 let chunk_sha256: [u8; 32] = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, &bytes)
493 .as_ref()
494 .try_into()
495 .map_err(|_| OperationLocatorError::ArtifactDigestMismatch)?;
496 let end = canonical_range.end()?;
497 let complete = end == artifact.bytes;
498 if range.offset == 0 && complete && chunk_sha256 != artifact.sha256 {
499 return Err(OperationLocatorError::ArtifactDigestMismatch);
500 }
501 Ok(VerifiedArtifactChunkV1 {
502 offset: range.offset,
503 bytes,
504 chunk_sha256,
505 artifact_sha256: artifact.sha256,
506 next_offset: (!complete).then_some(end),
507 complete,
508 })
509}
510
511fn require_fixed_envelope(
512 bytes: &[u8],
513 magic: &[u8; 8],
514 expected_length: usize,
515 body_length: usize,
516) -> Result<(), OperationLocatorError> {
517 if bytes.len() != expected_length {
518 return Err(OperationLocatorError::InvalidLength {
519 expected: expected_length,
520 actual: bytes.len(),
521 });
522 }
523 if bytes.get(..8) != Some(magic) {
524 return Err(OperationLocatorError::BadMagic);
525 }
526 let expected = read_u32(bytes, body_length)?;
527 let actual = crc32c(bytes.get(..body_length).ok_or(OperationLocatorError::Truncated)?);
528 if expected != actual {
529 return Err(OperationLocatorError::Checksum { expected, actual });
530 }
531 Ok(())
532}
533
534fn read_byte(bytes: &[u8], offset: usize) -> Result<u8, OperationLocatorError> {
535 bytes.get(offset).copied().ok_or(OperationLocatorError::Truncated)
536}
537
538fn read_array<const N: usize>(bytes: &[u8], offset: usize) -> Result<[u8; N], OperationLocatorError> {
539 bytes
540 .get(offset..offset.checked_add(N).ok_or(OperationLocatorError::Truncated)?)
541 .ok_or(OperationLocatorError::Truncated)?
542 .try_into()
543 .map_err(|_| OperationLocatorError::Truncated)
544}
545
546fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, OperationLocatorError> {
547 Ok(u16::from_be_bytes(read_array(bytes, offset)?))
548}
549
550fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, OperationLocatorError> {
551 Ok(u32::from_be_bytes(read_array(bytes, offset)?))
552}
553
554fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, OperationLocatorError> {
555 Ok(u64::from_be_bytes(read_array(bytes, offset)?))
556}
557
558#[derive(Clone, Debug, Eq, PartialEq)]
559pub enum OperationLocatorError {
560 InvalidLength { expected: usize, actual: usize },
561 BadMagic,
562 Truncated,
563 Checksum { expected: u32, actual: u32 },
564 UnsupportedOperationVersion(u16),
565 UnsupportedArtifactVersion(u16),
566 UnsupportedAuthority(u8),
567 NonCanonicalReservedByte,
568 InvalidCollection,
569 InvalidRecoveryEra,
570 InvalidOperationIdentity,
571 InvalidAuthorityPosition,
572 InvalidDigest,
573 InvalidExpiry,
574 InvalidArtifactBounds,
575 OperationBindingMismatch,
576 InvalidArtifactRange,
577 ArtifactRangeOverflow,
578 ArtifactDigestMismatch,
579}
580
581impl Display for OperationLocatorError {
582 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
583 match self {
584 Self::InvalidLength { expected, actual } => {
585 write!(
586 formatter,
587 "locator length {actual} does not match current length {expected}"
588 )
589 }
590 Self::BadMagic => formatter.write_str("locator magic is not current"),
591 Self::Truncated => formatter.write_str("locator is truncated"),
592 Self::Checksum { expected, actual } => {
593 write!(formatter, "locator checksum {actual} does not match {expected}")
594 }
595 Self::UnsupportedOperationVersion(version) => {
596 write!(formatter, "operation locator version {version} is unsupported")
597 }
598 Self::UnsupportedArtifactVersion(version) => {
599 write!(formatter, "artifact locator version {version} is unsupported")
600 }
601 Self::UnsupportedAuthority(authority) => {
602 write!(formatter, "operation authority tag {authority} is unsupported")
603 }
604 Self::NonCanonicalReservedByte => formatter.write_str("locator reserved byte is nonzero"),
605 Self::InvalidCollection => formatter.write_str("locator collection identity is invalid"),
606 Self::InvalidRecoveryEra => formatter.write_str("locator recovery era is invalid"),
607 Self::InvalidOperationIdentity => formatter.write_str("operation identity must be nonzero"),
608 Self::InvalidAuthorityPosition => {
609 formatter.write_str("authority revision and receipt ordinal must be nonzero")
610 }
611 Self::InvalidDigest => formatter.write_str("locator digest must be nonzero"),
612 Self::InvalidExpiry => formatter.write_str("operation expiry must follow submission"),
613 Self::InvalidArtifactBounds => formatter.write_str("artifact size or expiry is invalid"),
614 Self::OperationBindingMismatch => formatter.write_str("artifact is not bound to the supplied operation"),
615 Self::InvalidArtifactRange => {
616 formatter.write_str("artifact range is unaligned, empty, oversized, or outside the artifact")
617 }
618 Self::ArtifactRangeOverflow => formatter.write_str("artifact range overflows u64"),
619 Self::ArtifactDigestMismatch => {
620 formatter.write_str("artifact chunk or full digest does not match authoritative evidence")
621 }
622 }
623 }
624}
625
626impl Error for OperationLocatorError {}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 fn operation() -> Result<OperationLocatorV1, Box<dyn Error>> {
633 Ok(OperationLocatorV1::new(
634 OperationAuthorityKindV1::Diagnostic,
635 CollectionId::new(1)?,
636 [2; 32],
637 [3; 32],
638 RecoveryEra::new(4)?,
639 5,
640 6,
641 [7; 32],
642 8,
643 70_000,
644 [9; 32],
645 )?)
646 }
647
648 #[test]
649 fn operation_and_artifact_locators_round_trip_exactly() -> Result<(), Box<dyn Error>> {
650 let operation = operation()?;
651 assert_eq!(OperationLocatorV1::decode(&operation.encode())?, operation);
652 let artifact = ArtifactLocatorV1::new(&operation, [10; 32], 8_192, [11; 32], 60_000)?;
653 assert_eq!(ArtifactLocatorV1::decode(&artifact.encode())?, artifact);
654 artifact.require_operation(&operation)?;
655 Ok(())
656 }
657
658 #[test]
659 fn malformed_locators_and_ranges_fail_closed() -> Result<(), Box<dyn Error>> {
660 let operation = operation()?;
661 let mut bytes = operation.encode();
662 bytes[40] ^= 1;
663 assert!(OperationLocatorV1::decode(&bytes).is_err());
664 let artifact = ArtifactLocatorV1::new(&operation, [10; 32], 8_193, [11; 32], 60_000)?;
665 assert!(ArtifactReadRangeV1::new(1, 4_096, artifact.bytes()).is_err());
666 assert!(ArtifactReadRangeV1::new(0, 4_095, artifact.bytes()).is_err());
667 let final_range = ArtifactReadRangeV1::new(8_192, 1, artifact.bytes())?;
668 assert_eq!(final_range.end()?, 8_193);
669 Ok(())
670 }
671}