use crate::hex::{hex_encode_bytes, is_lower_hex_byte};
use crate::ids::{ContentId, NamespaceId};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256 as Sha2Sha256};
use std::fmt;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum ContentRefKind {
BlobV1,
}
impl ContentRefKind {
pub fn as_str(&self) -> &str {
match self {
Self::BlobV1 => "blob_v1",
}
}
}
impl fmt::Display for ContentRefKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[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(is_lower_hex_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("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 {
pub kind: ContentRefKind,
pub owner_namespace_id: NamespaceId,
pub content_id: ContentId,
pub size_bytes: u64,
pub checksum: Checksum,
}
impl ContentRef {
pub fn blob_v1(owner_namespace_id: NamespaceId, content_id: ContentId, bytes: &[u8]) -> Self {
Self {
kind: ContentRefKind::BlobV1,
owner_namespace_id,
content_id,
size_bytes: bytes.len() as u64,
checksum: Checksum::sha256(bytes),
}
}
pub fn blob_v1_streamed(
owner_namespace_id: NamespaceId,
content_id: ContentId,
size_bytes: u64,
digest: Sha256,
) -> Self {
Self {
kind: ContentRefKind::BlobV1,
owner_namespace_id,
content_id,
size_bytes,
checksum: digest.finish(),
}
}
pub fn validate(&self) -> Result<(), ContentRefValidationError> {
self.checksum
.validate()
.map_err(ContentRefValidationError::InvalidChecksum)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{
Checksum, ChecksumAlgorithm, ChecksumValidationError, ContentRef, ContentRefKind,
ContentRefValidationError, 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_fails_to_decode() {
let error = serde_json::from_str::<ContentRefKind>("\"sparse_file_v9\"")
.expect_err("unknown content kind must be rejected");
assert_eq!(
error.to_string(),
"unknown variant `sparse_file_v9`, expected `blob_v1` at line 1 column 16"
);
}
#[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, format!("\"{wire}\""));
assert_eq!(
algorithm.as_str(),
wire,
"the hand-written spelling must match the serde tag"
);
let decoded: ChecksumAlgorithm =
serde_json::from_str(&encoded).expect("decode algorithm");
assert_eq!(decoded, algorithm);
}
}
#[test]
fn an_unknown_checksum_algorithm_fails_to_decode() {
assert!(serde_json::from_str::<ChecksumAlgorithm>("\"md5\"").is_err());
let json = r#"{
"kind": "blob_v1",
"owner_namespace_id": "demo",
"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_requires_an_owner_and_one_checksum() {
let content_ref = ContentRef::blob_v1(
crate::NamespaceId::parse("demo").expect("namespace id"),
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(), 5);
assert_eq!(object["owner_namespace_id"], "demo");
let mut missing_owner = document.clone();
missing_owner
.as_object_mut()
.expect("reference")
.remove("owner_namespace_id");
assert!(serde_json::from_value::<ContentRef>(missing_owner).is_err());
assert!(object.contains_key("checksum"));
assert!(!object.contains_key("storage_checksum"));
assert!(!object.contains_key("whole_file_sha256"));
}
#[test]
fn validation_rejects_a_malformed_checksum() {
let mut content_ref = ContentRef::blob_v1(
crate::NamespaceId::parse("demo").expect("namespace id"),
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(
ChecksumValidationError::InvalidWidth { .. }
))
));
}
#[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_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"));
}
}
}