#![cfg(feature = "http")]
use cid::Cid;
use multihash::Multihash;
use sha2::{Digest, Sha256};
use crate::resolver::ResolverError;
const RAW_CODEC: u64 = 0x55;
const SHA256_CODE: u64 = 0x12;
#[must_use]
#[allow(
clippy::expect_used,
reason = "Multihash::wrap on a 32-byte SHA-256 input is infallible by construction"
)]
pub fn compute_style_cid(bytes: &[u8]) -> String {
let digest: [u8; 32] = Sha256::digest(bytes).into();
let mh = Multihash::<64>::wrap(SHA256_CODE, &digest)
.expect("32-byte SHA-256 digest always fits in Multihash<64>");
let cid = Cid::new_v1(RAW_CODEC, mh);
cid.to_string()
}
pub fn verify_cid(uri: &str, expected_cid: &str, bytes: &[u8]) -> Result<(), ResolverError> {
let actual = compute_style_cid(bytes);
let expected_canonical = canonicalize_cid(expected_cid)?;
if actual == expected_canonical {
Ok(())
} else {
Err(ResolverError::IntegrityFailure {
uri: uri.to_string(),
expected: expected_canonical,
actual,
})
}
}
pub fn canonicalize_cid(s: &str) -> Result<String, ResolverError> {
let trimmed = s.strip_prefix("cid:").unwrap_or(s);
let cid: Cid = trimmed.parse().map_err(|err: cid::Error| {
ResolverError::InvalidStyle(format!("invalid CID '{s}': {err}").into())
})?;
Ok(cid.to_string())
}
#[must_use]
pub fn strip_cid_scheme(uri: &str) -> &str {
uri.strip_prefix("cid:").unwrap_or(uri)
}
#[must_use]
pub fn is_cid_uri(uri: &str) -> bool {
uri.starts_with("cid:")
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
reason = "Panicking is acceptable and often desired in tests."
)]
mod tests {
use super::*;
const HELLO_WORLD_CID: &str = "bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e";
#[test]
fn compute_cid_is_stable_for_known_input() {
let cid = compute_style_cid(b"hello world");
assert_eq!(cid, HELLO_WORLD_CID, "canonical CIDv1 raw/sha256");
}
#[test]
fn compute_cid_changes_when_bytes_change() {
let a = compute_style_cid(b"alpha");
let b = compute_style_cid(b"beta");
assert_ne!(a, b);
}
#[test]
fn verify_cid_accepts_matching_bytes() {
let bytes = b"some style content";
let cid = compute_style_cid(bytes);
assert!(verify_cid("test://uri", &cid, bytes).is_ok());
}
#[test]
fn verify_cid_accepts_cid_scheme_prefix() {
let bytes = b"some style content";
let cid = compute_style_cid(bytes);
let scheme = format!("cid:{cid}");
assert!(verify_cid("test://uri", &scheme, bytes).is_ok());
}
#[test]
fn verify_cid_rejects_tampered_bytes() {
let original = b"trusted content";
let cid = compute_style_cid(original);
let tampered = b"untrusted content";
let err = verify_cid("test://uri", &cid, tampered).expect_err("must reject");
match err {
ResolverError::IntegrityFailure {
expected, actual, ..
} => {
assert_eq!(expected, cid);
assert_ne!(expected, actual);
}
other => panic!("expected IntegrityFailure, got {other:?}"),
}
}
#[test]
fn verify_cid_rejects_invalid_cid_string() {
let err = verify_cid("test://uri", "not-a-cid", b"x").expect_err("must reject");
assert!(matches!(err, ResolverError::InvalidStyle(_)));
}
#[test]
fn is_cid_uri_detects_scheme() {
assert!(is_cid_uri("cid:bafkreiabc"));
assert!(!is_cid_uri("https://example.org/x.yaml"));
assert!(!is_cid_uri("bafkreiabc"));
}
#[test]
fn strip_cid_scheme_handles_both_forms() {
assert_eq!(strip_cid_scheme("cid:bafkreiabc"), "bafkreiabc");
assert_eq!(strip_cid_scheme("bafkreiabc"), "bafkreiabc");
}
}