1use crate::{PackageId, PackageSource};
13use indexmap::IndexMap;
14use serde::{Deserialize, Serialize};
15use std::fmt;
16
17#[derive(Clone, Copy, PartialEq, Eq, Hash)]
19pub struct ContentHash(pub [u8; 32]);
20
21impl ContentHash {
22 #[must_use]
24 pub fn of(bytes: &[u8]) -> Self {
25 Self(*blake3::hash(bytes).as_bytes())
26 }
27
28 #[must_use]
30 pub const fn genesis() -> Self {
31 Self([0u8; 32])
32 }
33
34 #[must_use]
36 pub fn hex(&self) -> String {
37 let mut s = String::with_capacity(64);
38 for byte in &self.0 {
39 s.push_str(&format!("{byte:02x}"));
40 }
41 s
42 }
43
44 #[must_use]
47 pub fn from_hex(s: &str) -> Option<Self> {
48 if s.len() != 64 {
49 return None;
50 }
51 let mut out = [0u8; 32];
52 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
53 let hex_str = std::str::from_utf8(chunk).ok()?;
54 out[i] = u8::from_str_radix(hex_str, 16).ok()?;
55 }
56 Some(Self(out))
57 }
58
59 #[must_use]
64 pub fn from_hex_padded(s: &str) -> Option<Self> {
65 if s.len() == 64 {
66 Self::from_hex(s)
67 } else {
68 Some(Self::of(s.as_bytes()))
69 }
70 }
71}
72
73impl fmt::Debug for ContentHash {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(f, "ContentHash({}…)", &self.hex()[..16])
76 }
77}
78
79impl fmt::Display for ContentHash {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 f.write_str(&self.hex())
82 }
83}
84
85impl Serialize for ContentHash {
86 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
87 ser.serialize_str(&self.hex())
88 }
89}
90
91impl<'de> Deserialize<'de> for ContentHash {
92 fn deserialize<D: serde::Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
93 let s = String::deserialize(de)?;
94 if s.len() != 64 {
95 return Err(serde::de::Error::custom(format!(
96 "ContentHash expected 64 hex chars, got {}",
97 s.len()
98 )));
99 }
100 let mut out = [0u8; 32];
101 for (i, chunk) in s.as_bytes().chunks(2).enumerate() {
102 let hex_str = std::str::from_utf8(chunk).map_err(serde::de::Error::custom)?;
103 out[i] = u8::from_str_radix(hex_str, 16).map_err(serde::de::Error::custom)?;
104 }
105 Ok(Self(out))
106 }
107}
108
109#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
112pub struct ResolvedPackage {
113 pub id: PackageId,
114 pub source: PackageSource,
115 pub integrity: Option<String>,
117 #[serde(default)]
120 pub resolved_dependencies: Vec<PackageId>,
121 #[serde(default, skip_serializing_if = "Option::is_none")]
133 pub links: Option<String>,
134}
135
136#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
138pub struct Lockfile {
139 pub resolved: IndexMap<String, ResolvedPackage>,
140 pub content_addressed_hash: ContentHash,
143}
144
145impl Lockfile {
146 #[must_use]
149 pub fn empty() -> Self {
150 Self {
151 resolved: IndexMap::new(),
152 content_addressed_hash: ContentHash::genesis(),
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::{Registry, Version};
161
162 #[test]
163 fn content_hash_round_trips_through_serde() {
164 let h = ContentHash::of(b"some bytes");
165 let j = serde_json::to_string(&h).unwrap();
166 let parsed: ContentHash = serde_json::from_str(&j).unwrap();
167 assert_eq!(h, parsed);
168 }
169
170 #[test]
171 fn content_hash_genesis_is_all_zero() {
172 assert_eq!(ContentHash::genesis().0, [0u8; 32]);
173 }
174
175 #[test]
176 fn content_hash_hex_is_64_chars() {
177 let h = ContentHash::of(b"x");
178 assert_eq!(h.hex().len(), 64);
179 }
180
181 #[test]
182 fn lockfile_round_trips_through_serde() {
183 let mut resolved = IndexMap::new();
184 resolved.insert(
185 "serde".to_string(),
186 ResolvedPackage {
187 id: PackageId {
188 name: "serde".into(),
189 version: Version::new(1, 0, 228),
190 registry: Registry::CratesIo,
191 },
192 source: PackageSource::Registry {
193 registry: Registry::CratesIo,
194 registry_name: "serde".into(),
195 integrity_hash: Some("sha256:abc".into()),
196 },
197 integrity: Some("sha256:abc".into()),
198 resolved_dependencies: vec![],
199 links: None,
200 },
201 );
202 let l = Lockfile {
203 resolved,
204 content_addressed_hash: ContentHash::of(b"snapshot"),
205 };
206 let j = serde_json::to_string(&l).unwrap();
207 let parsed: Lockfile = serde_json::from_str(&j).unwrap();
208 assert_eq!(l, parsed);
209 }
210}