-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathcid.rs
More file actions
132 lines (115 loc) · 4.17 KB
/
Copy pathcid.rs
File metadata and controls
132 lines (115 loc) · 4.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
//! Content Identifier (CID) computation for gitlawb.
//!
//! Git SHA-256 object hashes map **deterministically** to IPFS CIDs:
//!
//! CID = CIDv1(codec=raw, mh=multihash(sha2-256, git_object_bytes))
//!
//! This means any git client using `--object-format=sha256` can verify
//! objects fetched from IPFS without modification. The CID is derived
//! from the raw git object bytes, not the SHA-256 hash string.
use cid::CidGeneric;
use multihash_codetable::{Code, MultihashDigest};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fmt;
use crate::{Error, Result};
/// IPFS multicodec for raw binary data.
const RAW: u64 = 0x55;
/// A CIDv1 identifier for a git object.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Cid(String);
impl Cid {
/// Compute a CID from raw git object bytes.
///
/// This is the canonical mapping: git objects pushed to IPFS always
/// produce this CID, so the content is self-verifying.
pub fn from_git_object_bytes(bytes: &[u8]) -> Self {
let mh = Code::Sha2_256.digest(bytes);
// CIDv1 with raw codec
let c = CidGeneric::<64>::new_v1(RAW, mh);
Self(c.to_string())
}
/// Compute a CID from an existing SHA-256 hex hash (e.g. from `git rev-parse`).
///
/// NOTE: This requires the original object bytes to recompute the multihash
/// correctly. If you only have the hex hash and not the bytes, use
/// `from_sha256_hex_trusted` — but note that is not self-verifying.
pub fn from_sha256_bytes(sha256_bytes: &[u8; 32]) -> Self {
// Construct multihash from raw bytes (0x12 = sha2-256, 0x20 = 32 bytes length)
let mut mh_bytes = vec![0x12u8, 0x20];
mh_bytes.extend_from_slice(sha256_bytes);
let mh = multihash::Multihash::<64>::from_bytes(&mh_bytes)
.expect("valid multihash construction from sha256 bytes");
let c = CidGeneric::<64>::new_v1(RAW, mh);
Self(c.to_string())
}
/// Parse a CID from a string.
#[allow(clippy::should_implement_trait)]
pub fn from_str(s: &str) -> Result<Self> {
s.parse::<CidGeneric<64>>()
.map(|_| Self(s.to_string()))
.map_err(|e| Error::InvalidCid(e.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Cid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
/// Compute a SHA-256 hash of arbitrary bytes and return as hex string.
/// Used for git object hashing (git uses SHA-256 in --object-format=sha256 mode).
pub fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
/// Compute a SHA-256 hash and return as raw bytes.
pub fn sha256_bytes(bytes: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(bytes);
hasher.finalize().into()
}
/// Parse a 64-character hex SHA-256 string into raw bytes.
pub fn sha256_hex_to_bytes(hex_str: &str) -> Result<[u8; 32]> {
let bytes = hex::decode(hex_str).map_err(|e| Error::InvalidCid(format!("invalid hex: {e}")))?;
bytes
.try_into()
.map_err(|_| Error::InvalidCid("sha256 hash must be 32 bytes (64 hex chars)".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cid_is_deterministic() {
let data = b"hello gitlawb";
let c1 = Cid::from_git_object_bytes(data);
let c2 = Cid::from_git_object_bytes(data);
assert_eq!(c1, c2);
}
#[test]
fn cid_starts_with_b() {
// CIDv1 base32 strings start with 'b'
let data = b"blob 13\0hello gitlawb";
let c = Cid::from_git_object_bytes(data);
assert!(
c.to_string().starts_with('b'),
"CIDv1 should be base32 (starts with 'b')"
);
}
#[test]
fn sha256_hex_len() {
let h = sha256_hex(b"test");
assert_eq!(h.len(), 64);
}
#[test]
fn sha256_round_trip() {
let data = b"git object content";
let hex = sha256_hex(data);
let bytes = sha256_hex_to_bytes(&hex).unwrap();
assert_eq!(sha256_bytes(data), bytes);
}
}