use crate::error::CowError;
const CID_VERSION: u8 = 0x01;
const MULTICODEC_RAW: u8 = 0x55;
const HASH_KECCAK256: u8 = 0x1b;
const HASH_LEN: u8 = 0x20;
const MULTICODEC_DAG_PB: u8 = 0x70;
const HASH_SHA2_256: u8 = 0x12;
pub fn appdata_hex_to_cid(app_data_hex: &str) -> Result<String, CowError> {
let hex = app_data_hex.strip_prefix("0x").map_or(app_data_hex, |s| s);
let bytes = alloy_primitives::hex::decode(hex)
.map_err(|e| CowError::AppData(format!("invalid hex: {e}")))?;
if bytes.len() != HASH_LEN as usize {
return Err(CowError::AppData(format!(
"appDataHex must be {} bytes, got {}",
HASH_LEN,
bytes.len()
)));
}
let mut cid = Vec::with_capacity(4 + HASH_LEN as usize);
cid.push(CID_VERSION);
cid.push(MULTICODEC_RAW);
cid.push(HASH_KECCAK256);
cid.push(HASH_LEN);
cid.extend_from_slice(&bytes);
Ok(format!("f{}", alloy_primitives::hex::encode(&cid)))
}
pub fn cid_to_appdata_hex(cid: &str) -> Result<String, CowError> {
let lower = cid.to_ascii_lowercase();
let hex = lower
.strip_prefix('f')
.ok_or_else(|| CowError::AppData("only base16 CIDs are supported (prefix 'f')".into()))?;
let bytes = alloy_primitives::hex::decode(hex)
.map_err(|e| CowError::AppData(format!("invalid CID hex: {e}")))?;
if bytes.len() < 4 + 32 {
return Err(CowError::AppData("CID too short".into()));
}
let digest = &bytes[4..4 + 32];
Ok(format!("0x{}", alloy_primitives::hex::encode(digest)))
}
fn to_cid_bytes(
version: u8,
multicodec: u8,
hashing_algorithm: u8,
hashing_length: u8,
multihash_hex: &str,
) -> Result<Vec<u8>, CowError> {
let hex = multihash_hex.strip_prefix("0x").map_or(multihash_hex, |s| s);
let hash_bytes = alloy_primitives::hex::decode(hex)
.map_err(|e| CowError::AppData(format!("invalid hex: {e}")))?;
let mut cid = Vec::with_capacity(4 + hash_bytes.len());
cid.push(version);
cid.push(multicodec);
cid.push(hashing_algorithm);
cid.push(hashing_length);
cid.extend_from_slice(&hash_bytes);
Ok(cid)
}
fn app_data_hex_to_cid_legacy_aux(app_data_hex: &str) -> Result<String, CowError> {
let cid_bytes =
to_cid_bytes(CID_VERSION, MULTICODEC_DAG_PB, HASH_SHA2_256, HASH_LEN, app_data_hex)?;
Ok(format!("f{}", alloy_primitives::hex::encode(&cid_bytes)))
}
pub fn assert_cid(cid: &str, app_data_hex: &str) -> Result<(), CowError> {
if cid.is_empty() {
return Err(CowError::AppData(format!("Error getting CID from appDataHex: {app_data_hex}")));
}
Ok(())
}
#[deprecated(
note = "Use appdata_hex_to_cid instead — legacy CID encoding is no longer used by CoW Protocol"
)]
pub fn app_data_hex_to_cid_legacy(app_data_hex: &str) -> Result<String, CowError> {
let cid = app_data_hex_to_cid_legacy_aux(app_data_hex)?;
assert_cid(&cid, app_data_hex)?;
Ok(cid)
}
#[derive(Debug, Clone)]
pub struct CidComponents {
pub version: u8,
pub codec: u8,
pub hash_function: u8,
pub hash_length: u8,
pub digest: Vec<u8>,
}
pub fn parse_cid(ipfs_hash: &str) -> Result<CidComponents, CowError> {
let lower = ipfs_hash.to_ascii_lowercase();
let hex = lower
.strip_prefix('f')
.ok_or_else(|| CowError::AppData("only base16 CIDs are supported (prefix 'f')".into()))?;
let bytes = alloy_primitives::hex::decode(hex)
.map_err(|e| CowError::AppData(format!("invalid CID hex: {e}")))?;
if bytes.len() < 4 {
return Err(CowError::AppData("CID too short".into()));
}
let version = bytes[0];
let codec = bytes[1];
let hash_function = bytes[2];
let hash_length = bytes[3];
let digest = bytes[4..].to_vec();
Ok(CidComponents { version, codec, hash_function, hash_length, digest })
}
pub fn decode_cid(bytes: &[u8]) -> Result<CidComponents, CowError> {
if bytes.len() < 4 {
return Err(CowError::AppData("CID bytes too short".into()));
}
Ok(CidComponents {
version: bytes[0],
codec: bytes[1],
hash_function: bytes[2],
hash_length: bytes[3],
digest: bytes[4..].to_vec(),
})
}
pub fn extract_digest(cid: &str) -> Result<String, CowError> {
let components = parse_cid(cid)?;
Ok(format!("0x{}", alloy_primitives::hex::encode(&components.digest)))
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_HEX: &str = "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
#[test]
fn appdata_hex_to_cid_produces_base16_cid() {
let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
assert!(cid.starts_with('f'));
assert_eq!(cid.len(), 1 + 72);
}
#[test]
fn appdata_hex_to_cid_without_0x_prefix() {
let hex = SAMPLE_HEX.strip_prefix("0x").unwrap_or_else(|| SAMPLE_HEX);
let cid = appdata_hex_to_cid(hex).unwrap_or_default();
assert!(cid.starts_with('f'));
}
#[test]
fn cid_to_appdata_hex_roundtrip() {
let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap();
let recovered = cid_to_appdata_hex(&cid).unwrap();
assert!(recovered.starts_with("0x"));
assert_eq!(recovered.len(), 66);
assert_eq!(recovered, SAMPLE_HEX);
}
#[test]
fn appdata_hex_to_cid_uses_input_as_digest() {
let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap();
let components = parse_cid(&cid).unwrap();
let expected = alloy_primitives::hex::decode(SAMPLE_HEX.trim_start_matches("0x")).unwrap();
assert_eq!(components.digest, expected);
}
#[test]
fn appdata_hex_to_cid_rejects_wrong_length() {
assert!(appdata_hex_to_cid("0xdeadbeef").is_err());
}
#[test]
fn cid_to_appdata_hex_rejects_non_base16() {
assert!(cid_to_appdata_hex("Qmabc123").is_err());
assert!(cid_to_appdata_hex("babc123").is_err());
}
#[test]
fn cid_to_appdata_hex_rejects_too_short() {
assert!(cid_to_appdata_hex("f0155").is_err());
}
#[test]
fn parse_cid_components() {
let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
let c = parse_cid(&cid).unwrap_or_else(|_| CidComponents {
version: 0,
codec: 0,
hash_function: 0,
hash_length: 0,
digest: vec![],
});
assert_eq!(c.version, CID_VERSION);
assert_eq!(c.codec, MULTICODEC_RAW);
assert_eq!(c.hash_function, HASH_KECCAK256);
assert_eq!(c.hash_length, HASH_LEN);
assert_eq!(c.digest.len(), 32);
}
#[test]
fn parse_cid_rejects_non_base16() {
assert!(parse_cid("not_a_cid").is_err());
}
#[test]
fn parse_cid_rejects_too_short() {
assert!(parse_cid("f01").is_err());
}
#[test]
fn decode_cid_from_bytes() {
let mut bytes = vec![0x01, 0x55, 0x1b, 0x20];
bytes.extend_from_slice(&[0xaa; 32]);
let c = decode_cid(&bytes).unwrap_or_else(|_| CidComponents {
version: 0,
codec: 0,
hash_function: 0,
hash_length: 0,
digest: vec![],
});
assert_eq!(c.version, 1);
assert_eq!(c.codec, 0x55);
assert_eq!(c.digest.len(), 32);
}
#[test]
fn decode_cid_rejects_short_bytes() {
assert!(decode_cid(&[0x01, 0x02, 0x03]).is_err());
assert!(decode_cid(&[]).is_err());
}
#[test]
fn extract_digest_returns_0x_prefixed() {
let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
let digest = extract_digest(&cid).unwrap_or_default();
assert!(digest.starts_with("0x"));
assert_eq!(digest.len(), 66);
}
#[test]
fn assert_cid_accepts_nonempty() {
assert!(assert_cid("f01234", "0xabc").is_ok());
}
#[test]
fn assert_cid_rejects_empty() {
assert!(assert_cid("", "0xabc").is_err());
}
#[test]
#[allow(deprecated, reason = "testing legacy API surface")]
fn legacy_cid_produces_base16_string() {
let cid = app_data_hex_to_cid_legacy(SAMPLE_HEX).unwrap_or_default();
assert!(cid.starts_with('f'));
}
#[test]
fn appdata_hex_to_cid_invalid_hex() {
assert!(appdata_hex_to_cid("0xZZZZ").is_err());
}
#[test]
fn deterministic_output() {
let cid1 = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
let cid2 = appdata_hex_to_cid(SAMPLE_HEX).unwrap_or_default();
assert_eq!(cid1, cid2);
}
#[test]
fn cid_to_appdata_hex_invalid_hex() {
assert!(cid_to_appdata_hex("fZZZZinvalid").is_err());
}
#[test]
fn parse_cid_uppercase_f_prefix() {
let cid = appdata_hex_to_cid(SAMPLE_HEX).unwrap();
let upper = format!("F{}", &cid[1..]);
let c = parse_cid(&upper).unwrap();
assert_eq!(c.version, CID_VERSION);
}
#[test]
fn to_cid_bytes_without_0x() {
let hex = SAMPLE_HEX.strip_prefix("0x").unwrap();
let bytes = to_cid_bytes(CID_VERSION, MULTICODEC_RAW, HASH_KECCAK256, HASH_LEN, hex);
assert!(bytes.is_ok());
}
#[test]
fn to_cid_bytes_invalid_hex() {
let result = to_cid_bytes(CID_VERSION, MULTICODEC_RAW, HASH_KECCAK256, HASH_LEN, "ZZZZ");
assert!(result.is_err());
}
}