use crate::classified::ClassifiedCid;
use crate::error::ContentError;
use crate::raw_id::RawContentId;
use core::fmt;
use core::str::FromStr;
use ipld_core::cid::multihash::Multihash;
use ipld_core::cid::Cid;
#[derive(Debug)]
struct LegacyParseError(String);
impl fmt::Display for LegacyParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for LegacyParseError {}
fn invalid(reason: impl Into<String>) -> ContentError {
let reason = reason.into();
ContentError::InvalidCid {
source: Box::new(LegacyParseError(reason.clone())),
reason,
}
}
fn decode_hex32(hex: &str) -> Result<[u8; 32], ContentError> {
let bytes = hex.as_bytes();
if bytes.len() != 64 {
return Err(invalid(format!(
"expected 64 hex chars for a 32-byte digest, got {}",
bytes.len()
)));
}
let nibble = |c: u8| -> Result<u8, ContentError> {
match c {
b'0'..=b'9' => Ok(c - b'0'),
b'a'..=b'f' => Ok(c - b'a' + 10),
b'A'..=b'F' => Ok(c - b'A' + 10),
_ => Err(invalid(format!("non-hex character {:?}", c as char))),
}
};
let mut out = [0u8; 32];
for (i, pair) in bytes.chunks(2).enumerate() {
out[i] = (nibble(pair[0])? << 4) | nibble(pair[1])?;
}
Ok(out)
}
pub mod kyln {
use super::*;
pub fn parse(envelope_hex: &str) -> Result<RawContentId, ContentError> {
let cid = Cid::from_str(&format!("f{envelope_hex}"))
.map_err(|e| invalid(format!("not a hex-encoded CID envelope: {e}")))?;
RawContentId::try_from(cid)
}
}
pub mod nessie {
use super::*;
pub const SHA2_256_HASH_CODE: u64 = 0x12;
pub fn parse(text: &str) -> Result<ClassifiedCid, ContentError> {
let (algo, hex) = text
.split_once(':')
.ok_or_else(|| invalid("expected \"<algo>:<hex>\""))?;
let digest = decode_hex32(hex)?;
match algo {
"blake3" => Ok(ClassifiedCid::Raw(RawContentId::from_blake3_digest(digest))),
"sha2-256" => {
let mh = Multihash::wrap(SHA2_256_HASH_CODE, &digest)
.expect("32-byte digest fits a 64-byte multihash");
Ok(ClassifiedCid::from_cid(Cid::new_v1(
crate::raw_id::RAW_CODEC,
mh,
)))
}
other => Err(invalid(format!(
"unknown digest algorithm {other:?} (expected \"blake3\" or \"sha2-256\")"
))),
}
}
}
pub mod bare_blake3 {
use super::*;
pub fn parse(digest_hex: &str) -> Result<RawContentId, ContentError> {
Ok(RawContentId::from_blake3_digest(decode_hex32(digest_hex)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn digest() -> [u8; 32] {
*blake3::hash(b"legacy").as_bytes()
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[test]
fn kyln_envelope_hex_is_a_raw_content_id_byte_for_byte() {
let expected = RawContentId::from_blake3_digest(digest());
let mut env = vec![0x01, 0x55, 0x1e, 0x20];
env.extend_from_slice(&digest());
let parsed = kyln::parse(&hex(&env)).unwrap();
assert_eq!(parsed, expected);
assert_eq!(parsed.to_bytes(), env);
assert_eq!(kyln::parse(&hex(&env).to_uppercase()).unwrap(), expected);
}
#[test]
fn kyln_rejects_non_raw_and_garbage() {
let dag = crate::ContentId::from_dag_cbor_digest(digest());
assert!(matches!(
kyln::parse(&hex(&dag.to_bytes())),
Err(ContentError::InvalidCidProfile { .. })
));
assert!(matches!(
kyln::parse("zz"),
Err(ContentError::InvalidCid { .. })
));
}
#[test]
fn nessie_blake3_is_raw_and_sha256_is_foreign() {
let d = digest();
let raw = nessie::parse(&format!("blake3:{}", hex(&d))).unwrap();
assert_eq!(raw, ClassifiedCid::Raw(RawContentId::from_blake3_digest(d)));
let sha = nessie::parse(&format!("sha2-256:{}", hex(&d))).unwrap();
assert!(matches!(sha, ClassifiedCid::Foreign(_)));
assert!(!sha.is_mintable());
assert_eq!(sha.hash_code(), 0x12);
assert_eq!(sha.codec(), 0x55);
let mut mh = vec![0x12, 0x20];
mh.extend_from_slice(&d);
assert_eq!(sha.as_cid().hash().to_bytes(), mh);
assert_ne!(raw, sha);
}
#[test]
fn nessie_rejects_unknown_algo_and_bad_hex() {
assert!(nessie::parse("md5:00").is_err());
assert!(nessie::parse("blake3").is_err());
assert!(nessie::parse(&format!("blake3:{}", "0".repeat(63))).is_err());
assert!(nessie::parse(&format!("blake3:{}", "g".repeat(64))).is_err());
}
#[test]
fn bare_blake3_hex_roundtrips_with_digest_hex() {
let id = RawContentId::from_content(b"bare");
assert_eq!(bare_blake3::parse(&id.digest_hex()).unwrap(), id);
assert!(bare_blake3::parse("abc").is_err());
}
}