Skip to main content

neco_cid/
lib.rs

1//! Minimal CIDv1 and multibase core.
2
3use core::fmt;
4
5use neco_sha2::Sha256;
6
7const CID_VERSION_V1: u64 = 1;
8const SHA2_256_CODE: u64 = 0x12;
9const SHA2_256_DIGEST_LEN: usize = 32;
10const BASE32_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct Cid {
14    version: u64,
15    codec: Codec,
16    hash_code: u64,
17    digest: [u8; SHA2_256_DIGEST_LEN],
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[repr(u64)]
22pub enum Codec {
23    DagCbor = 0x71,
24    Raw = 0x55,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Base {
29    Base32Lower,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum CidError {
34    InvalidVersion(u64),
35    UnsupportedCodec(u64),
36    UnsupportedHashCode(u64),
37    InvalidDigestLength,
38    InvalidMultibase,
39    UnexpectedEnd,
40    VarintOverflow,
41}
42
43impl fmt::Display for CidError {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::InvalidVersion(version) => write!(f, "invalid CID version: {version}"),
47            Self::UnsupportedCodec(codec) => write!(f, "unsupported codec: {codec}"),
48            Self::UnsupportedHashCode(code) => write!(f, "unsupported hash code: {code}"),
49            Self::InvalidDigestLength => f.write_str("invalid digest length"),
50            Self::InvalidMultibase => f.write_str("invalid multibase"),
51            Self::UnexpectedEnd => f.write_str("unexpected end of input"),
52            Self::VarintOverflow => f.write_str("varint exceeds 64-bit range"),
53        }
54    }
55}
56
57impl std::error::Error for CidError {}
58
59impl Cid {
60    pub fn compute(codec: Codec, data: &[u8]) -> Self {
61        let digest = Sha256::digest(data);
62        let mut digest_bytes = [0u8; SHA2_256_DIGEST_LEN];
63        digest_bytes.copy_from_slice(&digest);
64
65        Self {
66            version: CID_VERSION_V1,
67            codec,
68            hash_code: SHA2_256_CODE,
69            digest: digest_bytes,
70        }
71    }
72
73    pub fn from_bytes(input: &[u8]) -> Result<(Self, usize), CidError> {
74        let (version, mut offset) = decode_varint(input)?;
75        if version != CID_VERSION_V1 {
76            return Err(CidError::InvalidVersion(version));
77        }
78
79        let (codec_raw, consumed) = decode_varint(&input[offset..])?;
80        offset += consumed;
81        let codec = decode_codec(codec_raw)?;
82
83        let (hash_code, consumed) = decode_varint(&input[offset..])?;
84        offset += consumed;
85        if hash_code != SHA2_256_CODE {
86            return Err(CidError::UnsupportedHashCode(hash_code));
87        }
88
89        let (digest_len, consumed) = decode_varint(&input[offset..])?;
90        offset += consumed;
91        if digest_len != SHA2_256_DIGEST_LEN as u64 {
92            return Err(CidError::InvalidDigestLength);
93        }
94
95        let end = offset + SHA2_256_DIGEST_LEN;
96        if input.len() < end {
97            return Err(CidError::UnexpectedEnd);
98        }
99
100        let mut digest = [0u8; SHA2_256_DIGEST_LEN];
101        digest.copy_from_slice(&input[offset..end]);
102
103        Ok((
104            Self {
105                version,
106                codec,
107                hash_code,
108                digest,
109            },
110            end,
111        ))
112    }
113
114    pub fn to_bytes(&self) -> Vec<u8> {
115        let mut out = Vec::with_capacity(4 + SHA2_256_DIGEST_LEN);
116        encode_varint_into(self.version, &mut out);
117        encode_varint_into(self.codec as u64, &mut out);
118        encode_varint_into(self.hash_code, &mut out);
119        encode_varint_into(SHA2_256_DIGEST_LEN as u64, &mut out);
120        out.extend_from_slice(&self.digest);
121        out
122    }
123
124    pub fn to_multibase(&self, base: Base) -> String {
125        match base {
126            Base::Base32Lower => {
127                let binary = self.to_bytes();
128                let mut encoded = String::with_capacity(1 + (binary.len() * 8).div_ceil(5));
129                encoded.push('b');
130                encoded.push_str(&base32lower_encode(&binary));
131                encoded
132            }
133        }
134    }
135
136    pub fn from_multibase(input: &str) -> Result<Self, CidError> {
137        let payload = input.strip_prefix('b').ok_or(CidError::InvalidMultibase)?;
138        let bytes = base32lower_decode(payload)?;
139        let (cid, consumed) = Self::from_bytes(&bytes).map_err(|error| match error {
140            CidError::UnexpectedEnd => CidError::InvalidMultibase,
141            other => other,
142        })?;
143        if consumed != bytes.len() {
144            return Err(CidError::InvalidMultibase);
145        }
146        Ok(cid)
147    }
148
149    pub fn codec(&self) -> Codec {
150        self.codec
151    }
152
153    pub fn digest(&self) -> &[u8; SHA2_256_DIGEST_LEN] {
154        &self.digest
155    }
156}
157
158fn decode_codec(value: u64) -> Result<Codec, CidError> {
159    match value {
160        0x71 => Ok(Codec::DagCbor),
161        0x55 => Ok(Codec::Raw),
162        other => Err(CidError::UnsupportedCodec(other)),
163    }
164}
165
166fn encode_varint_into(mut value: u64, out: &mut Vec<u8>) {
167    loop {
168        let lower = (value & 0x7f) as u8;
169        value >>= 7;
170        if value == 0 {
171            out.push(lower);
172            return;
173        }
174        out.push(lower | 0x80);
175    }
176}
177
178fn decode_varint(input: &[u8]) -> Result<(u64, usize), CidError> {
179    let mut value = 0u64;
180    let mut shift = 0u32;
181
182    for (index, &byte) in input.iter().enumerate() {
183        let chunk = u64::from(byte & 0x7f);
184        value |= chunk << shift;
185        if byte & 0x80 == 0 {
186            return Ok((value, index + 1));
187        }
188        shift += 7;
189        if shift >= 64 {
190            return Err(CidError::VarintOverflow);
191        }
192    }
193
194    Err(CidError::UnexpectedEnd)
195}
196
197pub fn base32lower_encode(input: &[u8]) -> String {
198    if input.is_empty() {
199        return String::new();
200    }
201
202    let mut output = String::with_capacity((input.len() * 8).div_ceil(5));
203    let mut buffer = 0u16;
204    let mut bits = 0u8;
205
206    for &byte in input {
207        buffer = (buffer << 8) | u16::from(byte);
208        bits += 8;
209        while bits >= 5 {
210            bits -= 5;
211            let index = ((buffer >> bits) & 0x1f) as usize;
212            output.push(BASE32_ALPHABET[index] as char);
213        }
214    }
215
216    if bits > 0 {
217        let index = ((buffer << (5 - bits)) & 0x1f) as usize;
218        output.push(BASE32_ALPHABET[index] as char);
219    }
220
221    output
222}
223
224fn base32lower_decode(input: &str) -> Result<Vec<u8>, CidError> {
225    if input.is_empty() {
226        return Err(CidError::InvalidMultibase);
227    }
228
229    let mut output = Vec::with_capacity((input.len() * 5) / 8);
230    let mut buffer = 0u32;
231    let mut bits = 0u8;
232
233    for byte in input.bytes() {
234        let value = decode_base32_char(byte)?;
235        buffer = (buffer << 5) | u32::from(value);
236        bits += 5;
237        while bits >= 8 {
238            bits -= 8;
239            output.push(((buffer >> bits) & 0xff) as u8);
240        }
241    }
242
243    if bits > 0 {
244        let mask = (1u32 << bits) - 1;
245        if buffer & mask != 0 {
246            return Err(CidError::InvalidMultibase);
247        }
248    }
249
250    Ok(output)
251}
252
253fn decode_base32_char(byte: u8) -> Result<u8, CidError> {
254    match byte {
255        b'a'..=b'z' => Ok(byte - b'a'),
256        b'2'..=b'7' => Ok(byte - b'2' + 26),
257        _ => Err(CidError::InvalidMultibase),
258    }
259}