1use crate::error::{Error, Result};
2use serde::{Serialize, Serializer};
3use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
8#[non_exhaustive]
9pub enum HashAlgo {
10 Sha256,
11}
12
13impl HashAlgo {
14 pub const fn as_str(self) -> &'static str {
15 match self {
16 HashAlgo::Sha256 => "sha256",
17 }
18 }
19}
20
21impl fmt::Display for HashAlgo {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 f.write_str(self.as_str())
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub struct ContentHash {
31 algo: HashAlgo,
32 bytes: Vec<u8>,
33}
34
35impl ContentHash {
36 pub fn new(algo: HashAlgo, bytes: Vec<u8>) -> Self {
37 Self { algo, bytes }
38 }
39
40 pub fn sha256(digest: [u8; 32]) -> Self {
42 Self {
43 algo: HashAlgo::Sha256,
44 bytes: digest.to_vec(),
45 }
46 }
47
48 pub fn from_sha256_hex(hex: &str) -> Result<Self> {
51 if hex.len() != 64 {
52 return Err(Error::Malformed {
53 what: format!("sha256 hex must be 64 chars, got {}", hex.len()),
54 });
55 }
56 let mut digest = [0u8; 32];
57 for (i, pair) in hex.as_bytes().chunks_exact(2).enumerate() {
58 let s = std::str::from_utf8(pair).map_err(|_| Error::Malformed {
59 what: "sha256 hex is not valid UTF-8".to_owned(),
60 })?;
61 digest[i] = u8::from_str_radix(s, 16).map_err(|_| Error::Malformed {
62 what: format!("invalid hex byte `{s}`"),
63 })?;
64 }
65 Ok(Self::sha256(digest))
66 }
67
68 pub fn algo(&self) -> HashAlgo {
69 self.algo
70 }
71
72 pub fn bytes(&self) -> &[u8] {
73 &self.bytes
74 }
75
76 pub fn to_hex(&self) -> String {
78 use fmt::Write as _;
79 let mut s = String::with_capacity(self.bytes.len() * 2);
80 for b in &self.bytes {
81 let _ = write!(s, "{b:02x}");
82 }
83 s
84 }
85}
86
87impl fmt::Display for ContentHash {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 write!(f, "{}:{}", self.algo, self.to_hex())
90 }
91}
92
93impl Serialize for ContentHash {
94 fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
96 serializer.serialize_str(&self.to_string())
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
103#[non_exhaustive]
104pub enum EcosystemId {
105 Cargo,
106}
107
108impl EcosystemId {
109 pub const fn as_str(self) -> &'static str {
110 match self {
111 EcosystemId::Cargo => "cargo",
112 }
113 }
114}
115
116impl fmt::Display for EcosystemId {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 f.write_str(self.as_str())
119 }
120}
121
122macro_rules! string_newtype {
123 ($(#[$meta:meta])* $name:ident) => {
124 $(#[$meta])*
125 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
126 pub struct $name(String);
127
128 impl $name {
129 pub fn new(value: impl Into<String>) -> Self {
130 Self(value.into())
131 }
132 pub fn as_str(&self) -> &str {
133 &self.0
134 }
135 }
136
137 impl fmt::Display for $name {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 f.write_str(&self.0)
140 }
141 }
142
143 impl From<String> for $name {
144 fn from(value: String) -> Self {
145 Self(value)
146 }
147 }
148
149 impl From<&str> for $name {
150 fn from(value: &str) -> Self {
151 Self(value.to_owned())
152 }
153 }
154 };
155}
156
157string_newtype!(
158 SourceId
160);
161string_newtype!(
162 RepoId
164);
165string_newtype!(
166 DepName
168);
169string_newtype!(
170 Version
172);
173
174#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
176pub struct DepRef {
177 pub ecosystem: EcosystemId,
178 pub name: DepName,
179 pub version: Version,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
185pub struct Pin {
186 pub dep: DepRef,
187 pub expected: ContentHash,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
192pub struct ArtifactRef {
193 pub ecosystem: EcosystemId,
194 pub hash: ContentHash,
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn content_hash_renders_with_algo_prefix() {
203 let h = ContentHash::sha256([0xab; 32]);
204 assert_eq!(h.algo(), HashAlgo::Sha256);
205 assert_eq!(h.to_string(), format!("sha256:{}", "ab".repeat(32)));
206 }
207
208 #[test]
209 fn string_newtypes_roundtrip() {
210 let name = DepName::from("serde");
211 assert_eq!(name.as_str(), "serde");
212 assert_eq!(name, DepName::new("serde"));
213 }
214
215 #[test]
216 fn from_sha256_hex_roundtrips_and_validates() {
217 let hex = "ab".repeat(32);
218 let h = ContentHash::from_sha256_hex(&hex).unwrap();
219 assert_eq!(h.to_hex(), hex);
220 assert_eq!(h, ContentHash::sha256([0xab; 32]));
221
222 assert!(ContentHash::from_sha256_hex("tooshort").is_err());
223 assert!(ContentHash::from_sha256_hex(&"zz".repeat(32)).is_err());
224 }
225}