Skip to main content

loonfs_api/
content.rs

1//! [`ContentRef`]: the durable reference a file revision points at, naming
2//! one immutable content object and carrying the integrity evidence for it.
3
4use crate::hex::{hex_encode_bytes, is_lower_hex_byte};
5use crate::ids::ContentId;
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256 as Sha2Sha256};
8use std::fmt;
9use thiserror::Error;
10
11/// Kind of content reference.
12///
13/// Serializes as a plain string (`"blob_v1"`). Kinds unknown to this build
14/// decode as [`ContentRefKind::Unsupported`] carrying the original string,
15/// and re-serialize to that same string — so a reader that merely relays or
16/// rewrites rows it does not fully understand can never destroy a newer
17/// kind. Writers must not *create* references with an unsupported kind;
18/// commit validation rejects them (format spec, "Validation and logical commits").
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
20pub enum ContentRefKind {
21    /// One immutable content object, addressed by its random content id.
22    BlobV1,
23    /// A content kind unknown to this build, preserved verbatim.
24    Unsupported(String),
25}
26
27impl ContentRefKind {
28    const BLOB_V1: &'static str = "blob_v1";
29
30    /// Returns the frozen wire spelling, including an unknown spelling preserved by a reader.
31    pub fn as_str(&self) -> &str {
32        match self {
33            Self::BlobV1 => Self::BLOB_V1,
34            Self::Unsupported(other) => other,
35        }
36    }
37}
38
39impl fmt::Display for ContentRefKind {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        f.write_str(self.as_str())
42    }
43}
44
45impl Serialize for ContentRefKind {
46    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
47    where
48        S: serde::Serializer,
49    {
50        serializer.serialize_str(self.as_str())
51    }
52}
53
54impl<'de> Deserialize<'de> for ContentRefKind {
55    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56    where
57        D: serde::Deserializer<'de>,
58    {
59        let value = String::deserialize(deserializer)?;
60        Ok(match value.as_str() {
61            Self::BLOB_V1 => Self::BlobV1,
62            _ => Self::Unsupported(value),
63        })
64    }
65}
66
67/// Supported checksum algorithms.
68///
69/// The enclosing value defines which bytes a checksum covers. Unknown
70/// algorithms fail to decode because every in-memory `ChecksumAlgorithm` must
71/// be recomputable by this build. Adding a variant also requires adding its
72/// implementation.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
74#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
75#[serde(rename_all = "snake_case")]
76pub enum ChecksumAlgorithm {
77    /// SHA-256.
78    Sha256,
79    /// CRC-64/NVME.
80    Crc64nvme,
81    /// CRC-32C.
82    Crc32c,
83}
84
85impl ChecksumAlgorithm {
86    /// Returns the frozen wire spelling.
87    pub fn as_str(self) -> &'static str {
88        match self {
89            Self::Sha256 => "sha256",
90            Self::Crc64nvme => "crc64nvme",
91            Self::Crc32c => "crc32c",
92        }
93    }
94
95    /// Returns the raw checksum width in bytes.
96    pub fn value_bytes(self) -> usize {
97        match self {
98            Self::Sha256 => 32,
99            Self::Crc64nvme => 8,
100            Self::Crc32c => 4,
101        }
102    }
103}
104
105impl fmt::Display for ChecksumAlgorithm {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        f.write_str(self.as_str())
108    }
109}
110
111/// An algorithm and its canonical lowercase-hex checksum value.
112///
113/// The enclosing value defines which bytes the checksum covers.
114// This type also appears in request bodies, so it rejects unknown fields in
115// every context. Add new algorithms instead of new fields. This is not
116// rustdoc because it describes storage behavior, not the public API.
117#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
118#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
119#[serde(deny_unknown_fields)]
120pub struct Checksum {
121    /// Algorithm that produced `value`.
122    pub algorithm: ChecksumAlgorithm,
123    /// Lowercase hex of the raw checksum bytes.
124    ///
125    /// The algorithm is its own field, so the value carries no prefix.
126    /// Provider APIs that report base64 are converted at the adapter.
127    pub value: String,
128}
129
130impl Checksum {
131    /// Builds the `algorithm` checksum for these complete bytes.
132    ///
133    /// The one-shot forms below are this with the algorithm spelled out, so
134    /// a payload held whole and one delivered in pieces cannot drift: both
135    /// close the same digest.
136    pub fn compute(algorithm: ChecksumAlgorithm, bytes: &[u8]) -> Self {
137        let mut digest = StreamingChecksum::for_algorithm(algorithm);
138        digest.update(bytes);
139        digest.finish()
140    }
141
142    /// Builds the SHA-256 checksum for these bytes.
143    pub fn sha256(bytes: &[u8]) -> Self {
144        Self::compute(ChecksumAlgorithm::Sha256, bytes)
145    }
146
147    /// Builds the CRC-64/NVME checksum for these bytes.
148    pub fn crc64nvme(bytes: &[u8]) -> Self {
149        Self::compute(ChecksumAlgorithm::Crc64nvme, bytes)
150    }
151
152    /// Builds the CRC-32C checksum for these bytes.
153    pub fn crc32c(bytes: &[u8]) -> Self {
154        Self::compute(ChecksumAlgorithm::Crc32c, bytes)
155    }
156
157    /// Reports whether these bytes produce this exact checksum.
158    pub fn matches(&self, bytes: &[u8]) -> bool {
159        Self::compute(self.algorithm, bytes).value == self.value
160    }
161
162    /// Validates the exact width and lowercase-hex alphabet for `algorithm`.
163    pub fn validate(&self) -> Result<(), ChecksumValidationError> {
164        let expected_len = self.algorithm.value_bytes() * 2;
165        if self.value.len() != expected_len {
166            return Err(ChecksumValidationError::InvalidWidth {
167                algorithm: self.algorithm,
168                expected_len,
169                actual_len: self.value.len(),
170            });
171        }
172        if !self.value.bytes().all(is_lower_hex_byte) {
173            return Err(ChecksumValidationError::InvalidAlphabet {
174                algorithm: self.algorithm,
175            });
176        }
177        Ok(())
178    }
179}
180
181/// Describes why a checksum is not in its canonical wire form.
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
183pub enum ChecksumValidationError {
184    /// The checksum value does not have the exact width for its algorithm.
185    #[error(
186        "checksum for algorithm `{algorithm}` must be {expected_len} hex characters, got {actual_len}"
187    )]
188    InvalidWidth {
189        /// Algorithm whose checksum width was required.
190        algorithm: ChecksumAlgorithm,
191        /// Required number of hexadecimal characters.
192        expected_len: usize,
193        /// Number of characters supplied.
194        actual_len: usize,
195    },
196    /// The checksum value contains a character outside lowercase hexadecimal.
197    #[error("checksum for algorithm `{algorithm}` must be lowercase hex")]
198    InvalidAlphabet {
199        /// Algorithm whose checksum value was rejected.
200        algorithm: ChecksumAlgorithm,
201    },
202}
203
204/// Incremental checksum for streamed reads and writes.
205///
206/// It supports every [`ChecksumAlgorithm`] used by buffered verification, so
207/// buffered and streaming paths differ only in how bytes are supplied.
208#[derive(Debug)]
209pub enum StreamingChecksum {
210    /// SHA-256 folded over the payload.
211    Sha256(Sha256),
212    /// CRC-64/NVME folded over the payload.
213    Crc64nvme(Crc64Nvme),
214    /// CRC-32C folded over the payload.
215    Crc32c(Crc32c),
216}
217
218impl StreamingChecksum {
219    /// Starts an empty digest for `algorithm`.
220    pub fn for_algorithm(algorithm: ChecksumAlgorithm) -> Self {
221        match algorithm {
222            ChecksumAlgorithm::Sha256 => Self::Sha256(Sha256::new()),
223            ChecksumAlgorithm::Crc64nvme => Self::Crc64nvme(Crc64Nvme::new()),
224            ChecksumAlgorithm::Crc32c => Self::Crc32c(Crc32c::new()),
225        }
226    }
227
228    /// Folds the next piece of the payload in, in order.
229    pub fn update(&mut self, bytes: &[u8]) {
230        match self {
231            Self::Sha256(digest) => digest.update(bytes),
232            Self::Crc64nvme(digest) => digest.update(bytes),
233            Self::Crc32c(digest) => digest.update(bytes),
234        }
235    }
236
237    /// Closes the digest over everything fed so far.
238    pub fn finish(self) -> Checksum {
239        match self {
240            Self::Sha256(digest) => digest.finish(),
241            Self::Crc64nvme(digest) => digest.finish(),
242            Self::Crc32c(digest) => digest.finish(),
243        }
244    }
245}
246
247/// CRC-64/NVME over a payload delivered in pieces.
248///
249/// A direct multipart upload needs this digest twice over the same bytes:
250/// once per part, for the header the provider enforces on the way in, and
251/// once over the whole stream, for the reference completion verifies. Parts
252/// fed in order produce both without the object ever being held whole.
253#[derive(Default)]
254pub struct Crc64Nvme {
255    digest: crc64fast_nvme::Digest,
256}
257
258impl Crc64Nvme {
259    /// Starts an empty digest.
260    pub fn new() -> Self {
261        Self {
262            digest: crc64fast_nvme::Digest::new(),
263        }
264    }
265
266    /// Folds the next piece of the payload in, in order.
267    pub fn update(&mut self, bytes: &[u8]) {
268        self.digest.write(bytes);
269    }
270
271    /// Closes the digest over everything fed so far.
272    ///
273    /// The value is the big-endian spelling of the 64-bit result, which is
274    /// what the raw checksum bytes are on the wire and therefore what the
275    /// hex here has to be.
276    pub fn finish(self) -> Checksum {
277        Checksum {
278            algorithm: ChecksumAlgorithm::Crc64nvme,
279            value: hex_encode_bytes(&self.digest.sum64().to_be_bytes()),
280        }
281    }
282}
283
284impl fmt::Debug for Crc64Nvme {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        f.debug_struct("Crc64Nvme").finish_non_exhaustive()
287    }
288}
289
290/// Incremental CRC-32C (Castagnoli) checksum.
291///
292/// Google Cloud Storage reports this checksum for direct transfers. Resumed
293/// reads first add the retained prefix and then the fetched remainder, which
294/// produces the same full-object checksum as an uninterrupted read.
295#[derive(Default)]
296pub struct Crc32c {
297    crc: u32,
298}
299
300impl Crc32c {
301    /// Starts an empty digest.
302    pub fn new() -> Self {
303        Self { crc: 0 }
304    }
305
306    /// Folds the next piece of the payload in, in order.
307    pub fn update(&mut self, bytes: &[u8]) {
308        self.crc = crc32c::crc32c_append(self.crc, bytes);
309    }
310
311    /// Closes the digest over everything fed so far.
312    ///
313    /// The value is the big-endian spelling of the 32-bit result, which is
314    /// what the raw checksum bytes are on the wire and therefore what the
315    /// hex here has to be.
316    pub fn finish(self) -> Checksum {
317        Checksum {
318            algorithm: ChecksumAlgorithm::Crc32c,
319            value: hex_encode_bytes(&self.crc.to_be_bytes()),
320        }
321    }
322}
323
324impl fmt::Debug for Crc32c {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        f.debug_struct("Crc32c").finish_non_exhaustive()
327    }
328}
329
330/// SHA-256 over a payload delivered in pieces.
331///
332/// The proxied write path folds this over the request body as it forwards
333/// it to object storage, so a reference's full-object checksum exists without
334/// the payload ever being held whole. Pieces must be fed in order.
335#[derive(Default)]
336pub struct Sha256 {
337    digest: Sha2Sha256,
338}
339
340impl Sha256 {
341    /// Starts an empty digest.
342    pub fn new() -> Self {
343        Self {
344            digest: Sha2Sha256::new(),
345        }
346    }
347
348    /// Folds the next piece of the payload in, in order.
349    pub fn update(&mut self, bytes: &[u8]) {
350        self.digest.update(bytes);
351    }
352
353    /// Closes the digest over everything fed so far.
354    pub fn finish(self) -> Checksum {
355        Checksum {
356            algorithm: ChecksumAlgorithm::Sha256,
357            value: hex_encode_bytes(&self.digest.finalize()),
358        }
359    }
360}
361
362impl fmt::Debug for Sha256 {
363    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364        f.debug_struct("Sha256").finish_non_exhaustive()
365    }
366}
367
368/// Describes why a content reference cannot be part of a durable commit.
369#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
370pub enum ContentRefValidationError {
371    /// The reference names a content strategy this build cannot write.
372    #[error("unsupported content ref kind `{kind}`")]
373    UnsupportedKind {
374        /// Kind spelling carried by the rejected reference.
375        kind: String,
376    },
377    /// The checksum is not in the algorithm's canonical form.
378    #[error("invalid content ref checksum: {0}")]
379    InvalidChecksum(ChecksumValidationError),
380}
381
382/// Pointer to one immutable content object.
383///
384/// `content_id` is identity — *which* object — and the checksum is
385/// evidence about its bytes. Separating the two is what lets the final
386/// object key exist before the first byte is read.
387///
388/// A `ContentRef` is safe to publish only after the referenced bytes are
389/// durable in the namespace's content store.
390// This type also appears in request bodies, so it rejects unknown fields in
391// every context. Add new content kinds instead of new fields. This is not
392// rustdoc because it describes storage behavior, not the public API.
393#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
394#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
395#[serde(deny_unknown_fields)]
396pub struct ContentRef {
397    /// Content strategy used by the referenced object.
398    #[cfg_attr(feature = "openapi", schema(value_type = String))]
399    pub kind: ContentRefKind,
400    /// Immutable identity of the referenced object.
401    pub content_id: ContentId,
402    /// Complete byte length of the referenced content.
403    pub size_bytes: u64,
404    /// Mandatory checksum over the complete object.
405    pub checksum: Checksum,
406}
407
408/// Available proof that a payload matches a committed content reference.
409#[derive(Debug, Clone, Copy)]
410pub enum ContentEvidence<'a> {
411    /// Bytes that can be hashed with the committed reference's algorithm.
412    Bytes(&'a [u8]),
413    /// A reference carrying checksum evidence about its payload.
414    ContentRef(&'a ContentRef),
415}
416
417impl ContentRef {
418    /// Builds a reference to a freshly minted content object holding these bytes.
419    ///
420    /// Every caller of this constructor moves the bytes through the LoonFS
421    /// write path, so the checksum is trusted by construction.
422    pub fn blob_v1(content_id: ContentId, bytes: &[u8]) -> Self {
423        Self {
424            kind: ContentRefKind::BlobV1,
425            content_id,
426            size_bytes: bytes.len() as u64,
427            checksum: Checksum::sha256(bytes),
428        }
429    }
430
431    /// Builds a content reference from a SHA-256 computed while streaming the
432    /// payload.
433    ///
434    /// Accepting the digest object, rather than an arbitrary checksum string,
435    /// ensures that the checksum came from the LoonFS write path.
436    pub fn blob_v1_streamed(content_id: ContentId, size_bytes: u64, digest: Sha256) -> Self {
437        Self {
438            kind: ContentRefKind::BlobV1,
439            content_id,
440            size_bytes,
441            checksum: digest.finish(),
442        }
443    }
444
445    /// Whether `evidence` proves that a payload has the same bytes as this
446    /// reference.
447    ///
448    /// Reference evidence returns `false` when the size or checksum algorithm
449    /// differs. A checksum that was never computed is not evidence of a match.
450    pub fn matches_evidence(&self, evidence: ContentEvidence<'_>) -> bool {
451        match evidence {
452            ContentEvidence::Bytes(bytes) => {
453                self.size_bytes == bytes.len() as u64 && self.checksum.matches(bytes)
454            }
455            ContentEvidence::ContentRef(reference) => {
456                self.size_bytes == reference.size_bytes && self.checksum == reference.checksum
457            }
458        }
459    }
460
461    /// Reports whether the reference is well formed enough to publish.
462    ///
463    /// This is a shape check on the reference itself; proving that the
464    /// object exists and matches is the storage layer's job.
465    pub fn validate(&self) -> Result<(), ContentRefValidationError> {
466        if self.kind != ContentRefKind::BlobV1 {
467            return Err(ContentRefValidationError::UnsupportedKind {
468                kind: self.kind.as_str().to_owned(),
469            });
470        }
471        self.checksum
472            .validate()
473            .map_err(ContentRefValidationError::InvalidChecksum)?;
474        Ok(())
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::{
481        Checksum, ChecksumAlgorithm, ChecksumValidationError, ContentEvidence, ContentRef,
482        ContentRefKind, ContentRefValidationError, StreamingChecksum,
483    };
484    use crate::ids::ContentId;
485
486    fn content_id() -> ContentId {
487        ContentId::parse("con_0123456789abcdef0123456789abcdef").expect("valid content id")
488    }
489
490    #[test]
491    fn known_kind_round_trips_as_snake_case_string() {
492        let encoded = serde_json::to_string(&ContentRefKind::BlobV1).expect("encode");
493        assert_eq!(encoded, "\"blob_v1\"");
494        let decoded: ContentRefKind = serde_json::from_str(&encoded).expect("decode");
495        assert_eq!(decoded, ContentRefKind::BlobV1);
496    }
497
498    #[test]
499    fn unknown_kind_is_preserved_verbatim_through_a_round_trip() {
500        let decoded: ContentRefKind =
501            serde_json::from_str("\"sparse_file_v9\"").expect("decode unknown kind");
502        assert_eq!(
503            decoded,
504            ContentRefKind::Unsupported("sparse_file_v9".to_owned())
505        );
506        let reencoded = serde_json::to_string(&decoded).expect("encode unknown kind");
507        assert_eq!(reencoded, "\"sparse_file_v9\"");
508    }
509
510    #[test]
511    fn every_checksum_algorithm_round_trips() {
512        for (algorithm, wire) in [
513            (ChecksumAlgorithm::Sha256, "sha256"),
514            (ChecksumAlgorithm::Crc64nvme, "crc64nvme"),
515            (ChecksumAlgorithm::Crc32c, "crc32c"),
516        ] {
517            let encoded = serde_json::to_string(&algorithm).expect("encode algorithm");
518            assert_eq!(encoded, format!("\"{wire}\""));
519            assert_eq!(
520                algorithm.as_str(),
521                wire,
522                "the hand-written spelling must match the serde tag"
523            );
524            let decoded: ChecksumAlgorithm =
525                serde_json::from_str(&encoded).expect("decode algorithm");
526            assert_eq!(decoded, algorithm);
527        }
528    }
529
530    #[test]
531    fn an_unknown_checksum_algorithm_fails_to_decode() {
532        assert!(serde_json::from_str::<ChecksumAlgorithm>("\"md5\"").is_err());
533
534        let json = r#"{
535            "kind": "blob_v1",
536            "content_id": "con_0123456789abcdef0123456789abcdef",
537            "size_bytes": 5,
538            "checksum": {"algorithm": "md5", "value": "00000000000000000000000000000000"}
539        }"#;
540        assert!(serde_json::from_str::<ContentRef>(json).is_err());
541    }
542
543    #[test]
544    fn a_content_ref_uses_only_the_checksum_shape() {
545        let content_ref = ContentRef::blob_v1(content_id(), b"hello");
546
547        assert_eq!(content_ref.kind, ContentRefKind::BlobV1);
548        assert_eq!(content_ref.size_bytes, 5);
549        assert_eq!(content_ref.checksum.algorithm, ChecksumAlgorithm::Sha256);
550        content_ref.validate().expect("produced refs validate");
551
552        let document = serde_json::to_value(&content_ref).expect("encode content ref");
553        let object = document.as_object().expect("content ref object");
554        assert_eq!(object.len(), 4);
555        assert!(object.contains_key("checksum"));
556        assert!(!object.contains_key("storage_checksum"));
557        assert!(!object.contains_key("whole_file_sha256"));
558    }
559
560    #[test]
561    fn validation_rejects_an_unsupported_kind_and_a_malformed_checksum() {
562        let mut content_ref = ContentRef::blob_v1(content_id(), b"hello");
563        content_ref.kind = ContentRefKind::Unsupported("sparse_file_v9".to_owned());
564        assert!(matches!(
565            content_ref.validate(),
566            Err(ContentRefValidationError::UnsupportedKind { .. })
567        ));
568
569        let mut content_ref = ContentRef::blob_v1(content_id(), b"hello");
570        content_ref.checksum = Checksum {
571            algorithm: ChecksumAlgorithm::Crc64nvme,
572            value: content_ref.checksum.value.clone(),
573        };
574        assert!(matches!(
575            content_ref.validate(),
576            Err(ContentRefValidationError::InvalidChecksum(
577                ChecksumValidationError::InvalidWidth { .. }
578            ))
579        ));
580    }
581
582    #[test]
583    fn checksum_validation_enforces_exact_widths_and_lowercase_hex() {
584        for (algorithm, width) in [
585            (ChecksumAlgorithm::Sha256, 64),
586            (ChecksumAlgorithm::Crc64nvme, 16),
587            (ChecksumAlgorithm::Crc32c, 8),
588        ] {
589            Checksum {
590                algorithm,
591                value: "a".repeat(width),
592            }
593            .validate()
594            .expect("exact lowercase width");
595
596            assert!(matches!(
597                Checksum {
598                    algorithm,
599                    value: "a".repeat(width - 1),
600                }
601                .validate(),
602                Err(ChecksumValidationError::InvalidWidth { .. })
603            ));
604            assert!(matches!(
605                Checksum {
606                    algorithm,
607                    value: "a".repeat(width + 1),
608                }
609                .validate(),
610                Err(ChecksumValidationError::InvalidWidth { .. })
611            ));
612            assert!(matches!(
613                Checksum {
614                    algorithm,
615                    value: "A".repeat(width),
616                }
617                .validate(),
618                Err(ChecksumValidationError::InvalidAlphabet { .. })
619            ));
620        }
621    }
622
623    #[test]
624    fn crc64nvme_matches_its_catalog_check_value() {
625        assert_eq!(Checksum::crc64nvme(b"123456789").value, "ae8b14860a799888");
626        assert_eq!(
627            Checksum::crc64nvme(b"").value,
628            "0000000000000000",
629            "the empty payload is the identity"
630        );
631    }
632
633    #[test]
634    fn crc32c_matches_its_catalog_check_value() {
635        assert_eq!(Checksum::crc32c(b"123456789").value, "e3069283");
636        assert_eq!(
637            Checksum::crc32c(b"").value,
638            "00000000",
639            "the empty payload is the identity"
640        );
641    }
642
643    #[test]
644    fn a_streamed_checksum_agrees_with_the_whole_payload_at_once() {
645        let payload: Vec<u8> = (0..4096u32).map(|byte| byte as u8).collect();
646        for expected in [
647            Checksum::sha256(&payload),
648            Checksum::crc64nvme(&payload),
649            Checksum::crc32c(&payload),
650        ] {
651            let mut streaming = StreamingChecksum::for_algorithm(expected.algorithm);
652            for chunk in payload.chunks(97) {
653                streaming.update(chunk);
654            }
655            assert_eq!(streaming.finish(), expected);
656        }
657    }
658
659    #[test]
660    fn every_algorithm_compares_bytes_against_the_checksum_they_produce() {
661        for algorithm in [
662            ChecksumAlgorithm::Sha256,
663            ChecksumAlgorithm::Crc64nvme,
664            ChecksumAlgorithm::Crc32c,
665        ] {
666            let expected = Checksum::compute(algorithm, b"hello");
667            assert_eq!(expected.algorithm, algorithm);
668            assert!(expected.matches(b"hello"));
669            assert!(!expected.matches(b"other"));
670        }
671    }
672
673    #[test]
674    fn a_reference_compares_bytes_using_its_checksum_and_size() {
675        let bytes = b"retried payload";
676        let reference = ContentRef {
677            kind: ContentRefKind::BlobV1,
678            content_id: content_id(),
679            size_bytes: bytes.len() as u64,
680            checksum: Checksum::crc32c(bytes),
681        };
682
683        assert!(reference.matches_evidence(ContentEvidence::Bytes(bytes)));
684        assert!(!reference.matches_evidence(ContentEvidence::Bytes(b"different payload")));
685        let mut wrong_size = reference.clone();
686        wrong_size.size_bytes += 1;
687        assert!(!wrong_size.matches_evidence(ContentEvidence::Bytes(bytes)));
688    }
689
690    #[test]
691    fn a_reference_requires_the_other_reference_to_carry_its_checksum_algorithm() {
692        let bytes = b"retried payload";
693        let crc_reference = ContentRef {
694            kind: ContentRefKind::BlobV1,
695            content_id: content_id(),
696            size_bytes: bytes.len() as u64,
697            checksum: Checksum::crc32c(bytes),
698        };
699        let sha_reference = ContentRef::blob_v1(content_id(), bytes);
700        let matching_crc_reference = ContentRef {
701            content_id: content_id(),
702            ..crc_reference.clone()
703        };
704        let different_size = ContentRef {
705            size_bytes: crc_reference.size_bytes + 1,
706            ..crc_reference.clone()
707        };
708
709        assert!(!crc_reference.matches_evidence(ContentEvidence::ContentRef(&sha_reference)));
710        assert!(
711            crc_reference.matches_evidence(ContentEvidence::ContentRef(&matching_crc_reference))
712        );
713        assert!(!crc_reference.matches_evidence(ContentEvidence::ContentRef(&different_size)));
714        assert!(sha_reference.matches_evidence(ContentEvidence::ContentRef(&sha_reference)));
715    }
716}