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;
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#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
115#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
116#[serde(deny_unknown_fields)]
117pub struct Checksum {
118    /// Algorithm that produced `value`.
119    pub algorithm: ChecksumAlgorithm,
120    /// Lowercase hex of the raw checksum bytes.
121    ///
122    /// The algorithm is its own field, so the value carries no prefix.
123    /// Provider APIs that report base64 are converted at the adapter.
124    pub value: String,
125}
126
127impl Checksum {
128    /// Builds the `algorithm` checksum for these complete bytes.
129    ///
130    /// The one-shot forms below are this with the algorithm spelled out, so
131    /// a payload held whole and one delivered in pieces cannot drift: both
132    /// close the same digest.
133    pub fn compute(algorithm: ChecksumAlgorithm, bytes: &[u8]) -> Self {
134        let mut digest = StreamingChecksum::for_algorithm(algorithm);
135        digest.update(bytes);
136        digest.finish()
137    }
138
139    /// Builds the SHA-256 checksum for these bytes.
140    pub fn sha256(bytes: &[u8]) -> Self {
141        Self::compute(ChecksumAlgorithm::Sha256, bytes)
142    }
143
144    /// Builds the CRC-64/NVME checksum for these bytes.
145    pub fn crc64nvme(bytes: &[u8]) -> Self {
146        Self::compute(ChecksumAlgorithm::Crc64nvme, bytes)
147    }
148
149    /// Builds the CRC-32C checksum for these bytes.
150    pub fn crc32c(bytes: &[u8]) -> Self {
151        Self::compute(ChecksumAlgorithm::Crc32c, bytes)
152    }
153
154    /// Reports whether these bytes produce this exact checksum.
155    pub fn matches(&self, bytes: &[u8]) -> bool {
156        Self::compute(self.algorithm, bytes).value == self.value
157    }
158
159    /// Validates the exact width and lowercase-hex alphabet for `algorithm`.
160    pub fn validate(&self) -> Result<(), ChecksumValidationError> {
161        let expected_len = self.algorithm.value_bytes() * 2;
162        if self.value.len() != expected_len {
163            return Err(ChecksumValidationError::InvalidWidth {
164                algorithm: self.algorithm,
165                expected_len,
166                actual_len: self.value.len(),
167            });
168        }
169        if !self
170            .value
171            .bytes()
172            .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
173        {
174            return Err(ChecksumValidationError::InvalidAlphabet {
175                algorithm: self.algorithm,
176            });
177        }
178        Ok(())
179    }
180}
181
182/// Describes why a checksum is not in its canonical wire form.
183#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
184pub enum ChecksumValidationError {
185    /// The checksum value does not have the exact width for its algorithm.
186    #[error(
187        "checksum for algorithm `{algorithm}` must be {expected_len} hex characters, got {actual_len}"
188    )]
189    InvalidWidth {
190        /// Algorithm whose checksum width was required.
191        algorithm: ChecksumAlgorithm,
192        /// Required number of hexadecimal characters.
193        expected_len: usize,
194        /// Number of characters supplied.
195        actual_len: usize,
196    },
197    /// The checksum value contains a character outside lowercase hexadecimal.
198    #[error("checksum for algorithm `{algorithm}` must be lowercase hex")]
199    InvalidAlphabet {
200        /// Algorithm whose checksum value was rejected.
201        algorithm: ChecksumAlgorithm,
202    },
203}
204
205/// Incremental checksum for streamed reads and writes.
206///
207/// It supports every [`ChecksumAlgorithm`] used by buffered verification, so
208/// buffered and streaming paths differ only in how bytes are supplied.
209#[derive(Debug)]
210pub enum StreamingChecksum {
211    /// SHA-256 folded over the payload.
212    Sha256(Sha256),
213    /// CRC-64/NVME folded over the payload.
214    Crc64nvme(Crc64Nvme),
215    /// CRC-32C folded over the payload.
216    Crc32c(Crc32c),
217}
218
219impl StreamingChecksum {
220    /// Starts an empty digest for `algorithm`.
221    pub fn for_algorithm(algorithm: ChecksumAlgorithm) -> Self {
222        match algorithm {
223            ChecksumAlgorithm::Sha256 => Self::Sha256(Sha256::new()),
224            ChecksumAlgorithm::Crc64nvme => Self::Crc64nvme(Crc64Nvme::new()),
225            ChecksumAlgorithm::Crc32c => Self::Crc32c(Crc32c::new()),
226        }
227    }
228
229    /// Folds the next piece of the payload in, in order.
230    pub fn update(&mut self, bytes: &[u8]) {
231        match self {
232            Self::Sha256(digest) => digest.update(bytes),
233            Self::Crc64nvme(digest) => digest.update(bytes),
234            Self::Crc32c(digest) => digest.update(bytes),
235        }
236    }
237
238    /// Closes the digest over everything fed so far.
239    pub fn finish(self) -> Checksum {
240        match self {
241            Self::Sha256(digest) => digest.finish(),
242            Self::Crc64nvme(digest) => digest.finish(),
243            Self::Crc32c(digest) => digest.finish(),
244        }
245    }
246}
247
248/// CRC-64/NVME over a payload delivered in pieces.
249///
250/// A direct multipart upload needs this digest twice over the same bytes:
251/// once per part, for the header the provider enforces on the way in, and
252/// once over the whole stream, for the reference completion verifies. Parts
253/// fed in order produce both without the object ever being held whole.
254#[derive(Default)]
255pub struct Crc64Nvme {
256    digest: crc64fast_nvme::Digest,
257}
258
259impl Crc64Nvme {
260    /// Starts an empty digest.
261    pub fn new() -> Self {
262        Self {
263            digest: crc64fast_nvme::Digest::new(),
264        }
265    }
266
267    /// Folds the next piece of the payload in, in order.
268    pub fn update(&mut self, bytes: &[u8]) {
269        self.digest.write(bytes);
270    }
271
272    /// Closes the digest over everything fed so far.
273    ///
274    /// The value is the big-endian spelling of the 64-bit result, which is
275    /// what the raw checksum bytes are on the wire and therefore what the
276    /// hex here has to be.
277    pub fn finish(self) -> Checksum {
278        Checksum {
279            algorithm: ChecksumAlgorithm::Crc64nvme,
280            value: hex_encode_bytes(&self.digest.sum64().to_be_bytes()),
281        }
282    }
283}
284
285impl fmt::Debug for Crc64Nvme {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        f.debug_struct("Crc64Nvme").finish_non_exhaustive()
288    }
289}
290
291/// Incremental CRC-32C (Castagnoli) checksum.
292///
293/// Google Cloud Storage reports this checksum for direct transfers. Resumed
294/// reads first add the retained prefix and then the fetched remainder, which
295/// produces the same full-object checksum as an uninterrupted read.
296#[derive(Default)]
297pub struct Crc32c {
298    crc: u32,
299}
300
301impl Crc32c {
302    /// Starts an empty digest.
303    pub fn new() -> Self {
304        Self { crc: 0 }
305    }
306
307    /// Folds the next piece of the payload in, in order.
308    pub fn update(&mut self, bytes: &[u8]) {
309        self.crc = crc32c::crc32c_append(self.crc, bytes);
310    }
311
312    /// Closes the digest over everything fed so far.
313    ///
314    /// The value is the big-endian spelling of the 32-bit result, which is
315    /// what the raw checksum bytes are on the wire and therefore what the
316    /// hex here has to be.
317    pub fn finish(self) -> Checksum {
318        Checksum {
319            algorithm: ChecksumAlgorithm::Crc32c,
320            value: hex_encode_bytes(&self.crc.to_be_bytes()),
321        }
322    }
323}
324
325impl fmt::Debug for Crc32c {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        f.debug_struct("Crc32c").finish_non_exhaustive()
328    }
329}
330
331/// SHA-256 over a payload delivered in pieces.
332///
333/// The proxied write path folds this over the request body as it forwards
334/// it to object storage, so a reference's full-object checksum exists without
335/// the payload ever being held whole. Pieces must be fed in order.
336#[derive(Default)]
337pub struct Sha256 {
338    digest: Sha2Sha256,
339}
340
341impl Sha256 {
342    /// Starts an empty digest.
343    pub fn new() -> Self {
344        Self {
345            digest: Sha2Sha256::new(),
346        }
347    }
348
349    /// Folds the next piece of the payload in, in order.
350    pub fn update(&mut self, bytes: &[u8]) {
351        self.digest.update(bytes);
352    }
353
354    /// Closes the digest over everything fed so far.
355    pub fn finish(self) -> Checksum {
356        Checksum {
357            algorithm: ChecksumAlgorithm::Sha256,
358            value: hex_encode_bytes(&self.digest.finalize()),
359        }
360    }
361}
362
363impl fmt::Debug for Sha256 {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        f.debug_struct("Sha256").finish_non_exhaustive()
366    }
367}
368
369/// Describes why a content reference cannot be part of a durable commit.
370#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
371pub enum ContentRefValidationError {
372    /// The reference names a content strategy this build cannot write.
373    #[error("unsupported content ref kind `{kind}`")]
374    UnsupportedKind {
375        /// Kind spelling carried by the rejected reference.
376        kind: String,
377    },
378    /// The checksum is not in the algorithm's canonical form.
379    #[error("invalid content ref checksum: {0}")]
380    InvalidChecksum(ChecksumValidationError),
381}
382
383/// Pointer to one immutable content object.
384///
385/// `content_id` is identity — *which* object — and the checksum is
386/// evidence about its bytes. Separating the two is what lets the final
387/// object key exist before the first byte is read.
388///
389/// A `ContentRef` is safe to publish only after the referenced bytes are
390/// durable in the namespace's content store.
391#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
392#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
393#[serde(deny_unknown_fields)]
394pub struct ContentRef {
395    /// Content strategy used by the referenced object.
396    #[cfg_attr(feature = "openapi", schema(value_type = String))]
397    pub kind: ContentRefKind,
398    /// Immutable identity of the referenced object.
399    pub content_id: ContentId,
400    /// Complete byte length of the referenced content.
401    pub size_bytes: u64,
402    /// Mandatory checksum over the complete object.
403    pub checksum: Checksum,
404}
405
406/// Available proof that a payload matches a committed content reference.
407#[derive(Debug, Clone, Copy)]
408pub enum ContentEvidence<'a> {
409    /// Bytes that can be hashed with the committed reference's algorithm.
410    Bytes(&'a [u8]),
411    /// A reference carrying checksum evidence about its payload.
412    ContentRef(&'a ContentRef),
413}
414
415impl ContentRef {
416    /// Builds a reference to a freshly minted content object holding these bytes.
417    ///
418    /// Every caller of this constructor moves the bytes through the LoonFS
419    /// write path, so the checksum is trusted by construction.
420    pub fn blob_v1(content_id: ContentId, bytes: &[u8]) -> Self {
421        Self {
422            kind: ContentRefKind::BlobV1,
423            content_id,
424            size_bytes: bytes.len() as u64,
425            checksum: Checksum::sha256(bytes),
426        }
427    }
428
429    /// Builds a content reference from a SHA-256 computed while streaming the
430    /// payload.
431    ///
432    /// Accepting the digest object, rather than an arbitrary checksum string,
433    /// ensures that the checksum came from the LoonFS write path.
434    pub fn blob_v1_streamed(content_id: ContentId, size_bytes: u64, digest: Sha256) -> Self {
435        Self {
436            kind: ContentRefKind::BlobV1,
437            content_id,
438            size_bytes,
439            checksum: digest.finish(),
440        }
441    }
442
443    /// Whether `evidence` proves that a payload has the same bytes as this
444    /// reference.
445    ///
446    /// Reference evidence returns `false` when the size or checksum algorithm
447    /// differs. A checksum that was never computed is not evidence of a match.
448    pub fn matches_evidence(&self, evidence: ContentEvidence<'_>) -> bool {
449        match evidence {
450            ContentEvidence::Bytes(bytes) => {
451                self.size_bytes == bytes.len() as u64 && self.checksum.matches(bytes)
452            }
453            ContentEvidence::ContentRef(reference) => {
454                self.size_bytes == reference.size_bytes && self.checksum == reference.checksum
455            }
456        }
457    }
458
459    /// Reports whether the reference is well formed enough to publish.
460    ///
461    /// This is a shape check on the reference itself; proving that the
462    /// object exists and matches is the storage layer's job.
463    pub fn validate(&self) -> Result<(), ContentRefValidationError> {
464        if self.kind != ContentRefKind::BlobV1 {
465            return Err(ContentRefValidationError::UnsupportedKind {
466                kind: self.kind.as_str().to_owned(),
467            });
468        }
469        self.checksum
470            .validate()
471            .map_err(ContentRefValidationError::InvalidChecksum)?;
472        Ok(())
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::{
479        Checksum, ChecksumAlgorithm, ChecksumValidationError, ContentEvidence, ContentRef,
480        ContentRefKind, ContentRefValidationError, Crc32c, Crc64Nvme, StreamingChecksum,
481    };
482    use crate::ids::ContentId;
483
484    fn content_id() -> ContentId {
485        ContentId::parse("con_0123456789abcdef0123456789abcdef").expect("valid content id")
486    }
487
488    #[test]
489    fn known_kind_round_trips_as_snake_case_string() {
490        let encoded = serde_json::to_string(&ContentRefKind::BlobV1).expect("encode");
491        assert_eq!(encoded, "\"blob_v1\"");
492        let decoded: ContentRefKind = serde_json::from_str(&encoded).expect("decode");
493        assert_eq!(decoded, ContentRefKind::BlobV1);
494    }
495
496    #[test]
497    fn unknown_kind_is_preserved_verbatim_through_a_round_trip() {
498        let decoded: ContentRefKind =
499            serde_json::from_str("\"sparse_file_v9\"").expect("decode unknown kind");
500        assert_eq!(
501            decoded,
502            ContentRefKind::Unsupported("sparse_file_v9".to_owned())
503        );
504        let reencoded = serde_json::to_string(&decoded).expect("encode unknown kind");
505        assert_eq!(reencoded, "\"sparse_file_v9\"");
506    }
507
508    #[test]
509    fn every_checksum_algorithm_round_trips() {
510        for (algorithm, wire) in [
511            (ChecksumAlgorithm::Sha256, "\"sha256\""),
512            (ChecksumAlgorithm::Crc64nvme, "\"crc64nvme\""),
513            (ChecksumAlgorithm::Crc32c, "\"crc32c\""),
514        ] {
515            let encoded = serde_json::to_string(&algorithm).expect("encode algorithm");
516            assert_eq!(encoded, wire);
517            let decoded: ChecksumAlgorithm =
518                serde_json::from_str(&encoded).expect("decode algorithm");
519            assert_eq!(decoded, algorithm);
520        }
521    }
522
523    #[test]
524    fn every_checksum_shape_round_trips() {
525        for checksum in [
526            Checksum::sha256(b"hello"),
527            Checksum::crc64nvme(b"hello"),
528            Checksum::crc32c(b"hello"),
529        ] {
530            let encoded = serde_json::to_string(&checksum).expect("encode checksum");
531            let decoded: Checksum = serde_json::from_str(&encoded).expect("decode checksum");
532            assert_eq!(decoded, checksum);
533        }
534    }
535
536    /// Unknown checksum algorithms are rejected during decoding.
537    ///
538    /// Content kinds may be preserved by readers without interpretation, but a
539    /// checksum is accepted only when this build can verify it. This guarantees
540    /// that every decoded `ChecksumAlgorithm` is supported by buffered, streamed,
541    /// and resumed verification.
542    #[test]
543    fn an_unknown_checksum_algorithm_fails_to_decode() {
544        assert!(serde_json::from_str::<ChecksumAlgorithm>("\"md5\"").is_err());
545
546        let json = r#"{
547            "kind": "blob_v1",
548            "content_id": "con_0123456789abcdef0123456789abcdef",
549            "size_bytes": 5,
550            "checksum": {"algorithm": "md5", "value": "00000000000000000000000000000000"}
551        }"#;
552        assert!(serde_json::from_str::<ContentRef>(json).is_err());
553    }
554
555    #[test]
556    fn a_content_ref_rejects_unknown_fields() {
557        let json = r#"{
558            "kind": "blob_v1",
559            "content_id": "con_0123456789abcdef0123456789abcdef",
560            "size_bytes": 5,
561            "checksum": {"algorithm": "sha256", "value": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"},
562            "checksum_type": "full_object"
563        }"#;
564        assert!(serde_json::from_str::<ContentRef>(json).is_err());
565    }
566
567    #[test]
568    fn a_content_ref_uses_only_the_checksum_shape() {
569        let content_ref = ContentRef::blob_v1(content_id(), b"hello");
570
571        assert_eq!(content_ref.kind, ContentRefKind::BlobV1);
572        assert_eq!(content_ref.size_bytes, 5);
573        assert_eq!(content_ref.checksum.algorithm, ChecksumAlgorithm::Sha256);
574        content_ref.validate().expect("produced refs validate");
575
576        let document = serde_json::to_value(&content_ref).expect("encode content ref");
577        let object = document.as_object().expect("content ref object");
578        assert_eq!(object.len(), 4);
579        assert!(object.contains_key("checksum"));
580        assert!(!object.contains_key("storage_checksum"));
581        assert!(!object.contains_key("whole_file_sha256"));
582    }
583
584    #[test]
585    fn validation_rejects_unsupported_kinds_and_malformed_checksums() {
586        let mut content_ref = ContentRef::blob_v1(content_id(), b"hello");
587        content_ref.kind = ContentRefKind::Unsupported("sparse_file_v9".to_owned());
588        assert!(matches!(
589            content_ref.validate(),
590            Err(ContentRefValidationError::UnsupportedKind { .. })
591        ));
592
593        let mut content_ref = ContentRef::blob_v1(content_id(), b"hello");
594        content_ref.checksum = Checksum {
595            algorithm: ChecksumAlgorithm::Crc64nvme,
596            value: content_ref.checksum.value.clone(),
597        };
598        assert!(matches!(
599            content_ref.validate(),
600            Err(ContentRefValidationError::InvalidChecksum(_))
601        ));
602
603        let mut content_ref = ContentRef::blob_v1(content_id(), b"hello");
604        content_ref.checksum.value = content_ref.checksum.value.to_uppercase();
605        assert!(matches!(
606            content_ref.validate(),
607            Err(ContentRefValidationError::InvalidChecksum(
608                ChecksumValidationError::InvalidAlphabet { .. }
609            ))
610        ));
611    }
612
613    #[test]
614    fn checksum_validation_enforces_exact_widths_and_lowercase_hex() {
615        for (algorithm, width) in [
616            (ChecksumAlgorithm::Sha256, 64),
617            (ChecksumAlgorithm::Crc64nvme, 16),
618            (ChecksumAlgorithm::Crc32c, 8),
619        ] {
620            Checksum {
621                algorithm,
622                value: "a".repeat(width),
623            }
624            .validate()
625            .expect("exact lowercase width");
626
627            assert!(matches!(
628                Checksum {
629                    algorithm,
630                    value: "a".repeat(width - 1),
631                }
632                .validate(),
633                Err(ChecksumValidationError::InvalidWidth { .. })
634            ));
635            assert!(matches!(
636                Checksum {
637                    algorithm,
638                    value: "a".repeat(width + 1),
639                }
640                .validate(),
641                Err(ChecksumValidationError::InvalidWidth { .. })
642            ));
643            assert!(matches!(
644                Checksum {
645                    algorithm,
646                    value: "A".repeat(width),
647                }
648                .validate(),
649                Err(ChecksumValidationError::InvalidAlphabet { .. })
650            ));
651        }
652    }
653
654    /// The catalog check value for CRC-64/NVME. This is the one thing that
655    /// has to agree with the provider bit for bit: a completion compares our
656    /// value against the one S3 computed over the assembled object, so a
657    /// wrong polynomial or byte order would fail every multipart upload.
658    #[test]
659    fn crc64nvme_matches_its_catalog_check_value() {
660        assert_eq!(Checksum::crc64nvme(b"123456789").value, "ae8b14860a799888");
661        assert_eq!(
662            Checksum::crc64nvme(b"").value,
663            "0000000000000000",
664            "the empty payload is the identity"
665        );
666    }
667
668    /// The catalog check value for CRC-32C (Castagnoli), the one the RFC
669    /// 3720 iSCSI CRC and Google Cloud Storage both mean by "crc32c". Like
670    /// the CRC-64/NVME vector above it is an external anchor: it fixes the
671    /// polynomial and the big-endian byte order of the canonical hex against
672    /// a published value rather than against whatever this build happens to
673    /// compute, so a GCS-minted reference and one produced here agree.
674    #[test]
675    fn crc32c_matches_its_catalog_check_value() {
676        assert_eq!(Checksum::crc32c(b"123456789").value, "e3069283");
677        assert_eq!(
678            Checksum::crc32c(b"").value,
679            "00000000",
680            "the empty payload is the identity"
681        );
682    }
683
684    /// The streaming form exists so parts can be hashed on the way past
685    /// without the whole object ever being held, so it must agree with the
686    /// one-shot form over the same bytes.
687    #[test]
688    fn a_streamed_crc64nvme_equals_the_whole_payload_at_once() {
689        let payload: Vec<u8> = (0..4096u32).map(|byte| byte as u8).collect();
690        let mut streamed = Crc64Nvme::new();
691        for chunk in payload.chunks(97) {
692            streamed.update(chunk);
693        }
694
695        assert_eq!(streamed.finish(), Checksum::crc64nvme(&payload));
696    }
697
698    /// A resumed read folds a prefix it already holds and then the bytes it
699    /// fetches, so a CRC has to close over the two halves exactly as it
700    /// closes over the whole.
701    #[test]
702    fn a_crc32c_folded_over_a_prefix_and_the_rest_equals_the_whole_payload() {
703        let payload: Vec<u8> = (0..4096u32).map(|byte| byte as u8).collect();
704        let mut streamed = Crc32c::new();
705        streamed.update(&payload[..1500]);
706        streamed.update(&payload[1500..]);
707
708        assert_eq!(streamed.finish(), Checksum::crc32c(&payload));
709    }
710
711    /// A verifying reader folds the same checksum the one-shot check
712    /// computes, for every algorithm the vocabulary has — a reader that
713    /// disagreed with [`Checksum::matches`] would accept or reject
714    /// objects the buffered read would not.
715    #[test]
716    fn a_streamed_checksum_agrees_with_the_whole_payload_at_once() {
717        let payload: Vec<u8> = (0..4096u32).map(|byte| byte as u8).collect();
718        for expected in [
719            Checksum::sha256(&payload),
720            Checksum::crc64nvme(&payload),
721            Checksum::crc32c(&payload),
722        ] {
723            let mut streaming = StreamingChecksum::for_algorithm(expected.algorithm);
724            for chunk in payload.chunks(97) {
725                streaming.update(chunk);
726            }
727            assert_eq!(streaming.finish(), expected);
728        }
729    }
730
731    /// Every algorithm in the vocabulary is comparable, so a checksum in
732    /// hand is always a question with an answer.
733    #[test]
734    fn every_algorithm_compares_bytes_against_the_checksum_they_produce() {
735        for algorithm in [
736            ChecksumAlgorithm::Sha256,
737            ChecksumAlgorithm::Crc64nvme,
738            ChecksumAlgorithm::Crc32c,
739        ] {
740            let expected = Checksum::compute(algorithm, b"hello");
741            assert_eq!(expected.algorithm, algorithm);
742            assert!(expected.matches(b"hello"));
743            assert!(!expected.matches(b"other"));
744        }
745    }
746
747    #[test]
748    fn a_reference_compares_bytes_using_its_checksum_and_size() {
749        let bytes = b"retried payload";
750        let reference = ContentRef {
751            kind: ContentRefKind::BlobV1,
752            content_id: content_id(),
753            size_bytes: bytes.len() as u64,
754            checksum: Checksum::crc32c(bytes),
755        };
756
757        assert!(reference.matches_evidence(ContentEvidence::Bytes(bytes)));
758        assert!(!reference.matches_evidence(ContentEvidence::Bytes(b"different payload")));
759        let mut wrong_size = reference.clone();
760        wrong_size.size_bytes += 1;
761        assert!(!wrong_size.matches_evidence(ContentEvidence::Bytes(bytes)));
762    }
763
764    #[test]
765    fn a_reference_requires_the_other_reference_to_carry_its_checksum_algorithm() {
766        let bytes = b"retried payload";
767        let crc_reference = ContentRef {
768            kind: ContentRefKind::BlobV1,
769            content_id: content_id(),
770            size_bytes: bytes.len() as u64,
771            checksum: Checksum::crc32c(bytes),
772        };
773        let sha_reference = ContentRef::blob_v1(content_id(), bytes);
774        let matching_crc_reference = ContentRef {
775            content_id: content_id(),
776            ..crc_reference.clone()
777        };
778        let different_size = ContentRef {
779            size_bytes: crc_reference.size_bytes + 1,
780            ..crc_reference.clone()
781        };
782
783        assert!(!crc_reference.matches_evidence(ContentEvidence::ContentRef(&sha_reference)));
784        assert!(
785            crc_reference.matches_evidence(ContentEvidence::ContentRef(&matching_crc_reference))
786        );
787        assert!(!crc_reference.matches_evidence(ContentEvidence::ContentRef(&different_size)));
788        assert!(sha_reference.matches_evidence(ContentEvidence::ContentRef(&sha_reference)));
789    }
790
791    #[test]
792    fn sha_and_crc_references_round_trip() {
793        for content_ref in [
794            ContentRef::blob_v1(content_id(), b"hello"),
795            ContentRef {
796                kind: ContentRefKind::BlobV1,
797                content_id: content_id(),
798                size_bytes: 11_534_336,
799                checksum: Checksum {
800                    algorithm: ChecksumAlgorithm::Crc64nvme,
801                    value: "bbb7305bdf118bcb".to_owned(),
802                },
803            },
804            ContentRef {
805                kind: ContentRefKind::BlobV1,
806                content_id: content_id(),
807                size_bytes: 5,
808                checksum: Checksum::crc32c(b"hello"),
809            },
810        ] {
811            content_ref.validate().expect("content ref is valid");
812            let encoded = serde_json::to_string(&content_ref).expect("encode");
813            let decoded: ContentRef = serde_json::from_str(&encoded).expect("decode");
814            assert_eq!(decoded, content_ref);
815        }
816    }
817}