use crate::hex::hex_encode_bytes;
use crate::ids::ContentId;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256 as Sha2Sha256};
use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContentRefKind {
BlobV1,
Unsupported(String),
}
impl ContentRefKind {
const BLOB_V1: &'static str = "blob_v1";
pub fn as_str(&self) -> &str {
match self {
Self::BlobV1 => Self::BLOB_V1,
Self::Unsupported(other) => other,
}
}
}
impl fmt::Display for ContentRefKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for ContentRefKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for ContentRefKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
Self::BLOB_V1 => Self::BlobV1,
_ => Self::Unsupported(value),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ChecksumAlgorithm {
Sha256,
Crc64nvme,
Crc32c,
}
impl ChecksumAlgorithm {
pub fn as_str(self) -> &'static str {
match self {
Self::Sha256 => "sha256",
Self::Crc64nvme => "crc64nvme",
Self::Crc32c => "crc32c",
}
}
pub fn value_bytes(self) -> usize {
match self {
Self::Sha256 => 32,
Self::Crc64nvme => 8,
Self::Crc32c => 4,
}
}
}
impl fmt::Display for ChecksumAlgorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct Checksum {
pub algorithm: ChecksumAlgorithm,
pub value: String,
}
impl Checksum {
pub fn compute(algorithm: ChecksumAlgorithm, bytes: &[u8]) -> Self {
let mut digest = StreamingChecksum::for_algorithm(algorithm);
digest.update(bytes);
digest.finish()
}
pub fn sha256(bytes: &[u8]) -> Self {
Self::compute(ChecksumAlgorithm::Sha256, bytes)
}
pub fn crc64nvme(bytes: &[u8]) -> Self {
Self::compute(ChecksumAlgorithm::Crc64nvme, bytes)
}
pub fn crc32c(bytes: &[u8]) -> Self {
Self::compute(ChecksumAlgorithm::Crc32c, bytes)
}
pub fn matches(&self, bytes: &[u8]) -> bool {
Self::compute(self.algorithm, bytes).value == self.value
}
pub fn validate(&self) -> Result<(), ChecksumValidationError> {
let expected_len = self.algorithm.value_bytes() * 2;
if self.value.len() != expected_len {
return Err(ChecksumValidationError::InvalidWidth {
algorithm: self.algorithm,
expected_len,
actual_len: self.value.len(),
});
}
if !self
.value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(ChecksumValidationError::InvalidAlphabet {
algorithm: self.algorithm,
});
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
pub enum ChecksumValidationError {
#[error(
"checksum for algorithm `{algorithm}` must be {expected_len} hex characters, got {actual_len}"
)]
InvalidWidth {
algorithm: ChecksumAlgorithm,
expected_len: usize,
actual_len: usize,
},
#[error("checksum for algorithm `{algorithm}` must be lowercase hex")]
InvalidAlphabet {
algorithm: ChecksumAlgorithm,
},
}
#[derive(Debug)]
pub enum StreamingChecksum {
Sha256(Sha256),
Crc64nvme(Crc64Nvme),
Crc32c(Crc32c),
}
impl StreamingChecksum {
pub fn for_algorithm(algorithm: ChecksumAlgorithm) -> Self {
match algorithm {
ChecksumAlgorithm::Sha256 => Self::Sha256(Sha256::new()),
ChecksumAlgorithm::Crc64nvme => Self::Crc64nvme(Crc64Nvme::new()),
ChecksumAlgorithm::Crc32c => Self::Crc32c(Crc32c::new()),
}
}
pub fn update(&mut self, bytes: &[u8]) {
match self {
Self::Sha256(digest) => digest.update(bytes),
Self::Crc64nvme(digest) => digest.update(bytes),
Self::Crc32c(digest) => digest.update(bytes),
}
}
pub fn finish(self) -> Checksum {
match self {
Self::Sha256(digest) => digest.finish(),
Self::Crc64nvme(digest) => digest.finish(),
Self::Crc32c(digest) => digest.finish(),
}
}
}
#[derive(Default)]
pub struct Crc64Nvme {
digest: crc64fast_nvme::Digest,
}
impl Crc64Nvme {
pub fn new() -> Self {
Self {
digest: crc64fast_nvme::Digest::new(),
}
}
pub fn update(&mut self, bytes: &[u8]) {
self.digest.write(bytes);
}
pub fn finish(self) -> Checksum {
Checksum {
algorithm: ChecksumAlgorithm::Crc64nvme,
value: hex_encode_bytes(&self.digest.sum64().to_be_bytes()),
}
}
}
impl fmt::Debug for Crc64Nvme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Crc64Nvme").finish_non_exhaustive()
}
}
#[derive(Default)]
pub struct Crc32c {
crc: u32,
}
impl Crc32c {
pub fn new() -> Self {
Self { crc: 0 }
}
pub fn update(&mut self, bytes: &[u8]) {
self.crc = crc32c::crc32c_append(self.crc, bytes);
}
pub fn finish(self) -> Checksum {
Checksum {
algorithm: ChecksumAlgorithm::Crc32c,
value: hex_encode_bytes(&self.crc.to_be_bytes()),
}
}
}
impl fmt::Debug for Crc32c {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Crc32c").finish_non_exhaustive()
}
}
#[derive(Default)]
pub struct Sha256 {
digest: Sha2Sha256,
}
impl Sha256 {
pub fn new() -> Self {
Self {
digest: Sha2Sha256::new(),
}
}
pub fn update(&mut self, bytes: &[u8]) {
self.digest.update(bytes);
}
pub fn finish(self) -> Checksum {
Checksum {
algorithm: ChecksumAlgorithm::Sha256,
value: hex_encode_bytes(&self.digest.finalize()),
}
}
}
impl fmt::Debug for Sha256 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Sha256").finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Error)]
pub enum ContentRefValidationError {
#[error("unsupported content ref kind `{kind}`")]
UnsupportedKind {
kind: String,
},
#[error("invalid content ref checksum: {0}")]
InvalidChecksum(ChecksumValidationError),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(deny_unknown_fields)]
pub struct ContentRef {
#[cfg_attr(feature = "openapi", schema(value_type = String))]
pub kind: ContentRefKind,
pub content_id: ContentId,
pub size_bytes: u64,
pub checksum: Checksum,
}
#[derive(Debug, Clone, Copy)]
pub enum ContentEvidence<'a> {
Bytes(&'a [u8]),
ContentRef(&'a ContentRef),
}
impl ContentRef {
pub fn blob_v1(content_id: ContentId, bytes: &[u8]) -> Self {
Self {
kind: ContentRefKind::BlobV1,
content_id,
size_bytes: bytes.len() as u64,
checksum: Checksum::sha256(bytes),
}
}
pub fn blob_v1_streamed(content_id: ContentId, size_bytes: u64, digest: Sha256) -> Self {
Self {
kind: ContentRefKind::BlobV1,
content_id,
size_bytes,
checksum: digest.finish(),
}
}
pub fn matches_evidence(&self, evidence: ContentEvidence<'_>) -> bool {
match evidence {
ContentEvidence::Bytes(bytes) => {
self.size_bytes == bytes.len() as u64 && self.checksum.matches(bytes)
}
ContentEvidence::ContentRef(reference) => {
self.size_bytes == reference.size_bytes && self.checksum == reference.checksum
}
}
}
pub fn validate(&self) -> Result<(), ContentRefValidationError> {
if self.kind != ContentRefKind::BlobV1 {
return Err(ContentRefValidationError::UnsupportedKind {
kind: self.kind.as_str().to_owned(),
});
}
self.checksum
.validate()
.map_err(ContentRefValidationError::InvalidChecksum)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{
Checksum, ChecksumAlgorithm, ChecksumValidationError, ContentEvidence, ContentRef,
ContentRefKind, ContentRefValidationError, Crc32c, Crc64Nvme, StreamingChecksum,
};
use crate::ids::ContentId;
fn content_id() -> ContentId {
ContentId::parse("con_0123456789abcdef0123456789abcdef").expect("valid content id")
}
#[test]
fn known_kind_round_trips_as_snake_case_string() {
let encoded = serde_json::to_string(&ContentRefKind::BlobV1).expect("encode");
assert_eq!(encoded, "\"blob_v1\"");
let decoded: ContentRefKind = serde_json::from_str(&encoded).expect("decode");
assert_eq!(decoded, ContentRefKind::BlobV1);
}
#[test]
fn unknown_kind_is_preserved_verbatim_through_a_round_trip() {
let decoded: ContentRefKind =
serde_json::from_str("\"sparse_file_v9\"").expect("decode unknown kind");
assert_eq!(
decoded,
ContentRefKind::Unsupported("sparse_file_v9".to_owned())
);
let reencoded = serde_json::to_string(&decoded).expect("encode unknown kind");
assert_eq!(reencoded, "\"sparse_file_v9\"");
}
#[test]
fn every_checksum_algorithm_round_trips() {
for (algorithm, wire) in [
(ChecksumAlgorithm::Sha256, "\"sha256\""),
(ChecksumAlgorithm::Crc64nvme, "\"crc64nvme\""),
(ChecksumAlgorithm::Crc32c, "\"crc32c\""),
] {
let encoded = serde_json::to_string(&algorithm).expect("encode algorithm");
assert_eq!(encoded, wire);
let decoded: ChecksumAlgorithm =
serde_json::from_str(&encoded).expect("decode algorithm");
assert_eq!(decoded, algorithm);
}
}
#[test]
fn every_checksum_shape_round_trips() {
for checksum in [
Checksum::sha256(b"hello"),
Checksum::crc64nvme(b"hello"),
Checksum::crc32c(b"hello"),
] {
let encoded = serde_json::to_string(&checksum).expect("encode checksum");
let decoded: Checksum = serde_json::from_str(&encoded).expect("decode checksum");
assert_eq!(decoded, checksum);
}
}
#[test]
fn an_unknown_checksum_algorithm_fails_to_decode() {
assert!(serde_json::from_str::<ChecksumAlgorithm>("\"md5\"").is_err());
let json = r#"{
"kind": "blob_v1",
"content_id": "con_0123456789abcdef0123456789abcdef",
"size_bytes": 5,
"checksum": {"algorithm": "md5", "value": "00000000000000000000000000000000"}
}"#;
assert!(serde_json::from_str::<ContentRef>(json).is_err());
}
#[test]
fn a_content_ref_rejects_unknown_fields() {
let json = r#"{
"kind": "blob_v1",
"content_id": "con_0123456789abcdef0123456789abcdef",
"size_bytes": 5,
"checksum": {"algorithm": "sha256", "value": "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"},
"checksum_type": "full_object"
}"#;
assert!(serde_json::from_str::<ContentRef>(json).is_err());
}
#[test]
fn a_content_ref_uses_only_the_checksum_shape() {
let content_ref = ContentRef::blob_v1(content_id(), b"hello");
assert_eq!(content_ref.kind, ContentRefKind::BlobV1);
assert_eq!(content_ref.size_bytes, 5);
assert_eq!(content_ref.checksum.algorithm, ChecksumAlgorithm::Sha256);
content_ref.validate().expect("produced refs validate");
let document = serde_json::to_value(&content_ref).expect("encode content ref");
let object = document.as_object().expect("content ref object");
assert_eq!(object.len(), 4);
assert!(object.contains_key("checksum"));
assert!(!object.contains_key("storage_checksum"));
assert!(!object.contains_key("whole_file_sha256"));
}
#[test]
fn validation_rejects_unsupported_kinds_and_malformed_checksums() {
let mut content_ref = ContentRef::blob_v1(content_id(), b"hello");
content_ref.kind = ContentRefKind::Unsupported("sparse_file_v9".to_owned());
assert!(matches!(
content_ref.validate(),
Err(ContentRefValidationError::UnsupportedKind { .. })
));
let mut content_ref = ContentRef::blob_v1(content_id(), b"hello");
content_ref.checksum = Checksum {
algorithm: ChecksumAlgorithm::Crc64nvme,
value: content_ref.checksum.value.clone(),
};
assert!(matches!(
content_ref.validate(),
Err(ContentRefValidationError::InvalidChecksum(_))
));
let mut content_ref = ContentRef::blob_v1(content_id(), b"hello");
content_ref.checksum.value = content_ref.checksum.value.to_uppercase();
assert!(matches!(
content_ref.validate(),
Err(ContentRefValidationError::InvalidChecksum(
ChecksumValidationError::InvalidAlphabet { .. }
))
));
}
#[test]
fn checksum_validation_enforces_exact_widths_and_lowercase_hex() {
for (algorithm, width) in [
(ChecksumAlgorithm::Sha256, 64),
(ChecksumAlgorithm::Crc64nvme, 16),
(ChecksumAlgorithm::Crc32c, 8),
] {
Checksum {
algorithm,
value: "a".repeat(width),
}
.validate()
.expect("exact lowercase width");
assert!(matches!(
Checksum {
algorithm,
value: "a".repeat(width - 1),
}
.validate(),
Err(ChecksumValidationError::InvalidWidth { .. })
));
assert!(matches!(
Checksum {
algorithm,
value: "a".repeat(width + 1),
}
.validate(),
Err(ChecksumValidationError::InvalidWidth { .. })
));
assert!(matches!(
Checksum {
algorithm,
value: "A".repeat(width),
}
.validate(),
Err(ChecksumValidationError::InvalidAlphabet { .. })
));
}
}
#[test]
fn crc64nvme_matches_its_catalog_check_value() {
assert_eq!(Checksum::crc64nvme(b"123456789").value, "ae8b14860a799888");
assert_eq!(
Checksum::crc64nvme(b"").value,
"0000000000000000",
"the empty payload is the identity"
);
}
#[test]
fn crc32c_matches_its_catalog_check_value() {
assert_eq!(Checksum::crc32c(b"123456789").value, "e3069283");
assert_eq!(
Checksum::crc32c(b"").value,
"00000000",
"the empty payload is the identity"
);
}
#[test]
fn a_streamed_crc64nvme_equals_the_whole_payload_at_once() {
let payload: Vec<u8> = (0..4096u32).map(|byte| byte as u8).collect();
let mut streamed = Crc64Nvme::new();
for chunk in payload.chunks(97) {
streamed.update(chunk);
}
assert_eq!(streamed.finish(), Checksum::crc64nvme(&payload));
}
#[test]
fn a_crc32c_folded_over_a_prefix_and_the_rest_equals_the_whole_payload() {
let payload: Vec<u8> = (0..4096u32).map(|byte| byte as u8).collect();
let mut streamed = Crc32c::new();
streamed.update(&payload[..1500]);
streamed.update(&payload[1500..]);
assert_eq!(streamed.finish(), Checksum::crc32c(&payload));
}
#[test]
fn a_streamed_checksum_agrees_with_the_whole_payload_at_once() {
let payload: Vec<u8> = (0..4096u32).map(|byte| byte as u8).collect();
for expected in [
Checksum::sha256(&payload),
Checksum::crc64nvme(&payload),
Checksum::crc32c(&payload),
] {
let mut streaming = StreamingChecksum::for_algorithm(expected.algorithm);
for chunk in payload.chunks(97) {
streaming.update(chunk);
}
assert_eq!(streaming.finish(), expected);
}
}
#[test]
fn every_algorithm_compares_bytes_against_the_checksum_they_produce() {
for algorithm in [
ChecksumAlgorithm::Sha256,
ChecksumAlgorithm::Crc64nvme,
ChecksumAlgorithm::Crc32c,
] {
let expected = Checksum::compute(algorithm, b"hello");
assert_eq!(expected.algorithm, algorithm);
assert!(expected.matches(b"hello"));
assert!(!expected.matches(b"other"));
}
}
#[test]
fn a_reference_compares_bytes_using_its_checksum_and_size() {
let bytes = b"retried payload";
let reference = ContentRef {
kind: ContentRefKind::BlobV1,
content_id: content_id(),
size_bytes: bytes.len() as u64,
checksum: Checksum::crc32c(bytes),
};
assert!(reference.matches_evidence(ContentEvidence::Bytes(bytes)));
assert!(!reference.matches_evidence(ContentEvidence::Bytes(b"different payload")));
let mut wrong_size = reference.clone();
wrong_size.size_bytes += 1;
assert!(!wrong_size.matches_evidence(ContentEvidence::Bytes(bytes)));
}
#[test]
fn a_reference_requires_the_other_reference_to_carry_its_checksum_algorithm() {
let bytes = b"retried payload";
let crc_reference = ContentRef {
kind: ContentRefKind::BlobV1,
content_id: content_id(),
size_bytes: bytes.len() as u64,
checksum: Checksum::crc32c(bytes),
};
let sha_reference = ContentRef::blob_v1(content_id(), bytes);
let matching_crc_reference = ContentRef {
content_id: content_id(),
..crc_reference.clone()
};
let different_size = ContentRef {
size_bytes: crc_reference.size_bytes + 1,
..crc_reference.clone()
};
assert!(!crc_reference.matches_evidence(ContentEvidence::ContentRef(&sha_reference)));
assert!(
crc_reference.matches_evidence(ContentEvidence::ContentRef(&matching_crc_reference))
);
assert!(!crc_reference.matches_evidence(ContentEvidence::ContentRef(&different_size)));
assert!(sha_reference.matches_evidence(ContentEvidence::ContentRef(&sha_reference)));
}
#[test]
fn sha_and_crc_references_round_trip() {
for content_ref in [
ContentRef::blob_v1(content_id(), b"hello"),
ContentRef {
kind: ContentRefKind::BlobV1,
content_id: content_id(),
size_bytes: 11_534_336,
checksum: Checksum {
algorithm: ChecksumAlgorithm::Crc64nvme,
value: "bbb7305bdf118bcb".to_owned(),
},
},
ContentRef {
kind: ContentRefKind::BlobV1,
content_id: content_id(),
size_bytes: 5,
checksum: Checksum::crc32c(b"hello"),
},
] {
content_ref.validate().expect("content ref is valid");
let encoded = serde_json::to_string(&content_ref).expect("encode");
let decoded: ContentRef = serde_json::from_str(&encoded).expect("decode");
assert_eq!(decoded, content_ref);
}
}
}