use crate::error::{Error, Result};
pub use ::cid::Cid;
use multibase::Base;
use multihash_codetable::{Code, MultihashDigest};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HashAlgorithm {
#[default]
Sha256,
Sha512,
Sha3_256,
Sha3_512,
Blake2b256,
Blake2b512,
Blake2s256,
Blake3,
}
impl HashAlgorithm {
#[inline]
pub fn code(&self) -> Code {
match self {
HashAlgorithm::Sha256 => Code::Sha2_256,
HashAlgorithm::Sha512 => Code::Sha2_512,
HashAlgorithm::Sha3_256 => Code::Sha3_256,
HashAlgorithm::Sha3_512 => Code::Sha3_512,
HashAlgorithm::Blake2b256 => Code::Blake2b256,
HashAlgorithm::Blake2b512 => Code::Blake2b512,
HashAlgorithm::Blake2s256 => Code::Blake2s256,
HashAlgorithm::Blake3 => Code::Blake3_256,
}
}
#[inline]
pub const fn name(&self) -> &'static str {
match self {
HashAlgorithm::Sha256 => "SHA2-256",
HashAlgorithm::Sha512 => "SHA2-512",
HashAlgorithm::Sha3_256 => "SHA3-256",
HashAlgorithm::Sha3_512 => "SHA3-512",
HashAlgorithm::Blake2b256 => "BLAKE2b-256",
HashAlgorithm::Blake2b512 => "BLAKE2b-512",
HashAlgorithm::Blake2s256 => "BLAKE2s-256",
HashAlgorithm::Blake3 => "BLAKE3",
}
}
#[inline]
pub const fn hash_size(&self) -> usize {
match self {
HashAlgorithm::Sha256 => 32,
HashAlgorithm::Sha512 => 64,
HashAlgorithm::Sha3_256 => 32,
HashAlgorithm::Sha3_512 => 64,
HashAlgorithm::Blake2b256 => 32,
HashAlgorithm::Blake2b512 => 64,
HashAlgorithm::Blake2s256 => 32,
HashAlgorithm::Blake3 => 32,
}
}
#[inline]
pub const fn is_sha(&self) -> bool {
matches!(
self,
HashAlgorithm::Sha256
| HashAlgorithm::Sha512
| HashAlgorithm::Sha3_256
| HashAlgorithm::Sha3_512
)
}
#[inline]
pub const fn is_blake(&self) -> bool {
matches!(
self,
HashAlgorithm::Blake2b256
| HashAlgorithm::Blake2b512
| HashAlgorithm::Blake2s256
| HashAlgorithm::Blake3
)
}
#[inline]
pub const fn is_256_bit(&self) -> bool {
matches!(
self,
HashAlgorithm::Sha256
| HashAlgorithm::Sha3_256
| HashAlgorithm::Blake2b256
| HashAlgorithm::Blake2s256
| HashAlgorithm::Blake3
)
}
#[inline]
pub const fn is_512_bit(&self) -> bool {
matches!(
self,
HashAlgorithm::Sha512 | HashAlgorithm::Sha3_512 | HashAlgorithm::Blake2b512
)
}
#[inline]
pub fn has_simd_support(&self) -> bool {
match self {
HashAlgorithm::Sha256 | HashAlgorithm::Sha512 => {
cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64")
}
HashAlgorithm::Sha3_256 | HashAlgorithm::Sha3_512 => true,
HashAlgorithm::Blake2b256 | HashAlgorithm::Blake2b512 | HashAlgorithm::Blake2s256 => {
true
}
HashAlgorithm::Blake3 => true,
}
}
pub fn all() -> &'static [HashAlgorithm] {
&[
HashAlgorithm::Sha256,
HashAlgorithm::Sha512,
HashAlgorithm::Sha3_256,
HashAlgorithm::Sha3_512,
HashAlgorithm::Blake2b256,
HashAlgorithm::Blake2b512,
HashAlgorithm::Blake2s256,
HashAlgorithm::Blake3,
]
}
}
impl std::fmt::Display for HashAlgorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
impl std::str::FromStr for HashAlgorithm {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s.to_uppercase().as_str() {
"SHA256" | "SHA2-256" | "SHA-256" => Ok(HashAlgorithm::Sha256),
"SHA512" | "SHA2-512" | "SHA-512" => Ok(HashAlgorithm::Sha512),
"SHA3-256" | "SHA3_256" => Ok(HashAlgorithm::Sha3_256),
"SHA3-512" | "SHA3_512" => Ok(HashAlgorithm::Sha3_512),
"BLAKE2B256" | "BLAKE2B-256" | "BLAKE2B_256" => Ok(HashAlgorithm::Blake2b256),
"BLAKE2B512" | "BLAKE2B-512" | "BLAKE2B_512" => Ok(HashAlgorithm::Blake2b512),
"BLAKE2S256" | "BLAKE2S-256" | "BLAKE2S_256" => Ok(HashAlgorithm::Blake2s256),
"BLAKE3" | "BLAKE3-256" | "BLAKE3_256" => Ok(HashAlgorithm::Blake3),
_ => Err(Error::InvalidData(format!("Unknown hash algorithm: {}", s))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MultibaseEncoding {
#[default]
Base32Lower,
Base58Btc,
Base64,
Base64Url,
Base32Upper,
}
impl MultibaseEncoding {
#[inline]
pub fn base(&self) -> Base {
match self {
MultibaseEncoding::Base32Lower => Base::Base32Lower,
MultibaseEncoding::Base58Btc => Base::Base58Btc,
MultibaseEncoding::Base64 => Base::Base64,
MultibaseEncoding::Base64Url => Base::Base64Url,
MultibaseEncoding::Base32Upper => Base::Base32Upper,
}
}
#[inline]
pub const fn prefix(&self) -> char {
match self {
MultibaseEncoding::Base32Lower => 'b',
MultibaseEncoding::Base58Btc => 'z',
MultibaseEncoding::Base64 => 'm',
MultibaseEncoding::Base64Url => 'u',
MultibaseEncoding::Base32Upper => 'B',
}
}
#[inline]
pub const fn name(&self) -> &'static str {
match self {
MultibaseEncoding::Base32Lower => "base32 (lowercase)",
MultibaseEncoding::Base58Btc => "base58btc",
MultibaseEncoding::Base64 => "base64",
MultibaseEncoding::Base64Url => "base64url",
MultibaseEncoding::Base32Upper => "base32 (uppercase)",
}
}
#[inline]
pub const fn from_prefix(c: char) -> Option<Self> {
match c {
'b' => Some(MultibaseEncoding::Base32Lower),
'z' => Some(MultibaseEncoding::Base58Btc),
'm' => Some(MultibaseEncoding::Base64),
'u' => Some(MultibaseEncoding::Base64Url),
'B' => Some(MultibaseEncoding::Base32Upper),
_ => None,
}
}
pub const fn all() -> &'static [MultibaseEncoding] {
&[
MultibaseEncoding::Base32Lower,
MultibaseEncoding::Base58Btc,
MultibaseEncoding::Base64,
MultibaseEncoding::Base64Url,
MultibaseEncoding::Base32Upper,
]
}
}
impl std::fmt::Display for MultibaseEncoding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone)]
pub struct CidBuilder {
version: cid::Version,
codec: u64,
hash_algorithm: HashAlgorithm,
}
impl Default for CidBuilder {
fn default() -> Self {
Self {
version: cid::Version::V1,
codec: 0x55, hash_algorithm: HashAlgorithm::Sha256,
}
}
}
impl CidBuilder {
#[inline]
pub fn new() -> Self {
Self::default()
}
pub fn v0() -> Self {
Self {
version: cid::Version::V0,
codec: 0x70, hash_algorithm: HashAlgorithm::Sha256,
}
}
pub fn version(mut self, version: cid::Version) -> Self {
self.version = version;
self
}
pub fn codec(mut self, codec: u64) -> Self {
self.codec = codec;
self
}
pub fn hash_algorithm(mut self, algorithm: HashAlgorithm) -> Self {
self.hash_algorithm = algorithm;
self
}
pub fn build(&self, data: &[u8]) -> Result<Cid> {
let hash = self.hash_algorithm.code().digest(data);
if self.version == cid::Version::V0 {
if self.hash_algorithm != HashAlgorithm::Sha256 {
return Err(Error::InvalidInput(
"CIDv0 requires SHA2-256 hash algorithm".to_string(),
));
}
return Cid::new_v0(hash)
.map_err(|e| Error::Cid(format!("Failed to create CIDv0: {}", e)));
}
Cid::new(self.version, self.codec, hash)
.map_err(|e| Error::Cid(format!("Failed to create CID: {}", e)))
}
pub fn build_dag_cbor(&self, data: &[u8]) -> Result<Cid> {
let mut builder = self.clone();
builder.codec = 0x71; builder.build(data)
}
pub fn build_raw(&self, data: &[u8]) -> Result<Cid> {
let mut builder = self.clone();
builder.codec = 0x55; builder.build(data)
}
pub fn build_v0(&self, data: &[u8]) -> Result<Cid> {
let hash = Code::Sha2_256.digest(data);
Cid::new_v0(hash).map_err(|e| Error::Cid(format!("Failed to create CIDv0: {}", e)))
}
}
pub trait CidExt {
fn to_string_with_base(&self, base: MultibaseEncoding) -> String;
fn to_v1(&self) -> Result<Cid>;
fn to_v0(&self) -> Result<Cid>;
fn can_be_v0(&self) -> bool;
fn is_v0(&self) -> bool;
fn is_v1(&self) -> bool;
fn codec_code(&self) -> u64;
fn hash_algorithm_name(&self) -> &'static str;
fn hash_algorithm_code(&self) -> u64;
}
impl CidExt for Cid {
fn to_string_with_base(&self, base: MultibaseEncoding) -> String {
let bytes = self.to_bytes();
multibase::encode(base.base(), bytes)
}
fn to_v1(&self) -> Result<Cid> {
if self.version() == cid::Version::V1 {
return Ok(*self);
}
Ok(Cid::new_v1(codec::DAG_PB, *self.hash()))
}
fn to_v0(&self) -> Result<Cid> {
if self.version() == cid::Version::V0 {
return Ok(*self);
}
if self.hash().code() != 0x12 {
return Err(Error::InvalidInput(
"CIDv0 requires SHA2-256 hash algorithm".to_string(),
));
}
if self.codec() != codec::DAG_PB {
return Err(Error::InvalidInput(
"CIDv0 requires DAG-PB codec (0x70)".to_string(),
));
}
Cid::new_v0(*self.hash()).map_err(|e| Error::Cid(format!("Failed to create CIDv0: {}", e)))
}
fn can_be_v0(&self) -> bool {
self.hash().code() == 0x12 && self.codec() == codec::DAG_PB
}
fn is_v0(&self) -> bool {
self.version() == cid::Version::V0
}
fn is_v1(&self) -> bool {
self.version() == cid::Version::V1
}
fn codec_code(&self) -> u64 {
self.codec()
}
fn hash_algorithm_name(&self) -> &'static str {
match self.hash().code() {
0x12 => "sha2-256",
0x14 => "sha3-256", 0x16 => "sha3-256", 0x1b => "keccak-256",
0x1e => "blake2b-256",
_ => "unknown",
}
}
fn hash_algorithm_code(&self) -> u64 {
self.hash().code()
}
}
pub fn parse_cid(s: &str) -> Result<Cid> {
s.parse()
.map_err(|e| Error::Cid(format!("Failed to parse CID: {}", e)))
}
pub fn parse_cid_with_base(s: &str) -> Result<(Cid, MultibaseEncoding)> {
let first_char = s
.chars()
.next()
.ok_or_else(|| Error::Cid("Empty CID string".to_string()))?;
if s.starts_with("Qm") {
let cid: Cid = s
.parse()
.map_err(|e| Error::Cid(format!("Failed to parse CIDv0: {}", e)))?;
return Ok((cid, MultibaseEncoding::Base58Btc));
}
let base = MultibaseEncoding::from_prefix(first_char)
.ok_or_else(|| Error::Cid(format!("Unknown multibase prefix: {}", first_char)))?;
let cid: Cid = s
.parse()
.map_err(|e| Error::Cid(format!("Failed to parse CID: {}", e)))?;
Ok((cid, base))
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct SerializableCid(#[serde(with = "cid_serde")] pub Cid);
impl SerializableCid {
pub fn new(cid: Cid) -> Self {
Self(cid)
}
pub fn inner(&self) -> &Cid {
&self.0
}
pub fn to_string_with_base(&self, base: MultibaseEncoding) -> String {
self.0.to_string_with_base(base)
}
}
impl Copy for SerializableCid {}
impl From<Cid> for SerializableCid {
fn from(cid: Cid) -> Self {
Self(cid)
}
}
impl From<SerializableCid> for Cid {
fn from(cid: SerializableCid) -> Self {
cid.0
}
}
impl std::fmt::Display for SerializableCid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
mod cid_serde {
use super::*;
use serde::{Deserializer, Serializer};
pub fn serialize<S>(cid: &Cid, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&cid.to_string())
}
pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result<Cid, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
pub mod codec {
pub const RAW: u64 = 0x55;
pub const DAG_CBOR: u64 = 0x71;
pub const DAG_JSON: u64 = 0x0129;
pub const DAG_PB: u64 = 0x70;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multibase_encoding() {
let cid = CidBuilder::new().build(b"hello world").unwrap();
let base32 = cid.to_string_with_base(MultibaseEncoding::Base32Lower);
let base58 = cid.to_string_with_base(MultibaseEncoding::Base58Btc);
let base64 = cid.to_string_with_base(MultibaseEncoding::Base64);
assert!(base32.starts_with('b'));
assert!(base58.starts_with('z'));
assert!(base64.starts_with('m'));
let parsed32: Cid = base32.parse().unwrap();
let parsed58: Cid = base58.parse().unwrap();
let parsed64: Cid = base64.parse().unwrap();
assert_eq!(cid, parsed32);
assert_eq!(cid, parsed58);
assert_eq!(cid, parsed64);
}
#[test]
fn test_parse_cid_with_base() {
let cid = CidBuilder::new().build(b"test data").unwrap();
let base32_str = cid.to_string_with_base(MultibaseEncoding::Base32Lower);
let (parsed_cid, detected_base) = parse_cid_with_base(&base32_str).unwrap();
assert_eq!(cid, parsed_cid);
assert_eq!(detected_base, MultibaseEncoding::Base32Lower);
}
#[test]
fn test_cid_ext_methods() {
let cid = CidBuilder::new().build(b"test").unwrap();
assert!(cid.is_v1());
assert!(!cid.is_v0());
assert_eq!(cid.codec_code(), codec::RAW);
assert_eq!(cid.hash_algorithm_name(), "sha2-256");
}
#[test]
fn test_serializable_cid() {
let cid = CidBuilder::new().build(b"test").unwrap();
let serializable = SerializableCid::new(cid);
let json = serde_json::to_string(&serializable).unwrap();
let deserialized: SerializableCid = serde_json::from_str(&json).unwrap();
assert_eq!(serializable, deserialized);
}
#[test]
fn test_cidv0_creation() {
let cid_v0 = CidBuilder::v0().build(b"hello world").unwrap();
let cid_str = cid_v0.to_string();
assert!(cid_str.starts_with("Qm"), "CIDv0 should start with 'Qm'");
assert!(cid_v0.is_v0());
assert!(!cid_v0.is_v1());
}
#[test]
fn test_cidv0_build_v0_method() {
let cid_v0 = CidBuilder::new().build_v0(b"test data").unwrap();
assert!(cid_v0.is_v0());
assert!(cid_v0.to_string().starts_with("Qm"));
}
#[test]
fn test_cidv0_to_v1_conversion() {
let cid_v0 = CidBuilder::v0().build(b"test").unwrap();
let cid_v1 = cid_v0.to_v1().unwrap();
assert!(cid_v1.is_v1());
assert!(!cid_v1.is_v0());
assert_eq!(cid_v0.hash(), cid_v1.hash());
assert_eq!(cid_v1.codec_code(), codec::DAG_PB);
}
#[test]
fn test_cidv1_to_v0_conversion() {
let cid_v1 = CidBuilder::new()
.codec(codec::DAG_PB)
.build(b"test")
.unwrap();
let cid_v0 = cid_v1.to_v0().unwrap();
assert!(cid_v0.is_v0());
assert!(cid_v0.to_string().starts_with("Qm"));
}
#[test]
fn test_cidv1_to_v0_fails_wrong_codec() {
let cid_v1 = CidBuilder::new().build(b"test").unwrap();
let result = cid_v1.to_v0();
assert!(result.is_err());
}
#[test]
fn test_can_be_v0() {
let cid_compatible = CidBuilder::new()
.codec(codec::DAG_PB)
.build(b"test")
.unwrap();
assert!(cid_compatible.can_be_v0());
let cid_incompatible = CidBuilder::new().build(b"test").unwrap();
assert!(!cid_incompatible.can_be_v0());
let cid_sha3 = CidBuilder::new()
.codec(codec::DAG_PB)
.hash_algorithm(HashAlgorithm::Sha3_256)
.build(b"test")
.unwrap();
assert!(!cid_sha3.can_be_v0());
}
#[test]
fn test_parse_cidv0_string() {
let original = CidBuilder::v0().build(b"hello ipfs").unwrap();
let cid_str = original.to_string();
let parsed = parse_cid(&cid_str).unwrap();
assert_eq!(original, parsed);
let (parsed2, base) = parse_cid_with_base(&cid_str).unwrap();
assert_eq!(original, parsed2);
assert_eq!(base, MultibaseEncoding::Base58Btc);
}
#[test]
fn test_cidv0_roundtrip() {
let data = b"test content for roundtrip";
let cid_v0 = CidBuilder::v0().build(data).unwrap();
let cid_v1 = cid_v0.to_v1().unwrap();
let cid_v0_again = cid_v1.to_v0().unwrap();
assert_eq!(cid_v0, cid_v0_again);
}
}