1use std::str::FromStr;
2
3use crate::nixhash;
4use crate::nixhash::CAHash;
5use crate::nixhash::HashAlgo;
6use crate::nixhash::NixHash;
7use crate::store_path::ParseStorePathError;
8use crate::store_path::StorePath;
9
10#[derive(Clone, Debug, Default, Eq, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize))]
13pub struct Output {
14 pub path: Option<StorePath>,
16
17 #[cfg_attr(feature = "serde", serde(flatten))]
18 pub output_hash: Option<OutputHash>,
19}
20
21#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct OutputHash {
30 pub mode: OutputHashMode,
31 pub hash: NixHash,
32}
33
34#[cfg_attr(
37 feature = "serde",
38 derive(serde::Serialize, serde::Deserialize),
39 serde(rename_all = "lowercase")
40)]
41#[derive(Clone, Debug, Default, Eq, PartialEq)]
42pub enum OutputHashMode {
43 #[default]
44 Flat,
45 Recursive,
46}
47
48impl OutputHashMode {
49 pub const fn as_mode_prefix(&self) -> &'static str {
50 match self {
51 OutputHashMode::Flat => "",
52 OutputHashMode::Recursive => "r:",
53 }
54 }
55}
56
57impl FromStr for OutputHashMode {
58 type Err = ParseOutputHashModeError;
59
60 fn from_str(s: &str) -> Result<Self, Self::Err> {
61 match s {
62 "" | "flat" => Ok(Self::Flat),
63 "recursive" => Ok(Self::Recursive),
64 _ => Err(ParseOutputHashModeError::InvalidHashMode(s.to_owned())),
65 }
66 }
67}
68
69impl OutputHash {
70 pub fn from_mode_algo_and_digest(
72 mode_and_algo: &str,
73 digest: impl AsRef<[u8]>,
74 ) -> Result<Self, nixhash::Error> {
75 let (hash_mode, algo_str) = if let Some(algo_str) = mode_and_algo.strip_prefix("r:") {
76 (OutputHashMode::Recursive, algo_str)
77 } else {
78 (OutputHashMode::Flat, mode_and_algo)
79 };
80
81 let algo = algo_str.parse()?;
82
83 Ok(OutputHash {
84 mode: hash_mode,
85 hash: NixHash::from_algo_and_digest(algo, digest.as_ref())?,
86 })
87 }
88
89 pub const fn as_mode_and_algo_str(&self) -> &'static str {
92 match self.mode {
93 OutputHashMode::Flat => self.hash.algo().as_str(),
94 OutputHashMode::Recursive => match self.hash.algo() {
95 HashAlgo::Md5 => "r:md5",
96 HashAlgo::Sha1 => "r:sha1",
97 HashAlgo::Sha256 => "r:sha256",
98 HashAlgo::Sha512 => "r:sha512",
99 },
100 }
101 }
102}
103
104#[cfg(feature = "serde")]
105impl serde::Serialize for OutputHash {
106 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
107 where
108 S: serde::Serializer,
109 {
110 use serde::ser::SerializeMap;
111 let mut map = serializer.serialize_map(Some(3))?;
112
113 map.serialize_entry(
114 "hash",
115 &data_encoding::HEXLOWER.encode(self.hash.digest_as_bytes()),
116 )?;
117
118 map.serialize_entry("hashAlgo", self.as_mode_and_algo_str())?;
119
120 map.end()
121 }
122}
123
124#[cfg(feature = "serde")]
125impl<'de> serde::Deserialize<'de> for Output {
126 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127 where
128 D: serde::Deserializer<'de>,
129 {
130 use serde_json::Map;
131 let fields = Map::deserialize(deserializer)?;
132 let path: &str = fields
133 .get("path")
134 .ok_or(serde::de::Error::missing_field(
135 "`path` is missing but required for outputs",
136 ))?
137 .as_str()
138 .ok_or(serde::de::Error::invalid_type(
139 serde::de::Unexpected::Other("certainly not a string"),
140 &"a string",
141 ))?;
142
143 let path = StorePath::from_absolute_path(path.as_bytes()).map_err(|_| {
144 serde::de::Error::invalid_value(serde::de::Unexpected::Str(path), &"StorePath")
145 })?;
146
147 Ok(Self {
148 path: Some(path),
149 output_hash: match (fields.get("hash"), fields.get("hashAlgo")) {
152 (None, None) => None,
154 (Some(hash_f), Some(mode_and_algo)) => {
155 let hash_str = hash_f.as_str().ok_or(serde::de::Error::invalid_type(
156 serde::de::Unexpected::Other("certainly not a string"),
157 &"a string",
158 ))?;
159 let mode_and_algo =
160 mode_and_algo
161 .as_str()
162 .ok_or(serde::de::Error::invalid_type(
163 serde::de::Unexpected::Other("certainly not a string"),
164 &"a mode:algo string",
165 ))?;
166
167 let digest = data_encoding::HEXLOWER
168 .decode(hash_str.as_bytes())
169 .map_err(serde::de::Error::custom)?;
170
171 let output_hash = OutputHash::from_mode_algo_and_digest(mode_and_algo, digest)
172 .map_err(serde::de::Error::custom)?;
173
174 Some(output_hash)
175 }
176 _ => {
177 return Err(serde::de::Error::invalid_value(
178 serde::de::Unexpected::Other("Exactly one of `hash` and `hashAlgo`"),
179 &"none or both fields",
180 ));
181 }
182 },
183 })
184 }
185}
186
187#[derive(Debug, thiserror::Error, PartialEq)]
190pub enum ParseOutputHashModeError {
191 #[error("Invalid hash mode: {0}")]
192 InvalidHashMode(String),
193}
194
195#[derive(Debug, thiserror::Error, PartialEq)]
198pub enum ParseOutputError {
199 #[error("Invalid output path {0}: {1}")]
200 InvalidOutputPath(String, ParseStorePathError),
201 #[error("Missing output path")]
202 MissingOutputPath,
203 #[error("Invalid CAHash: {:?}", .0)]
204 InvalidCAHash(CAHash),
205}
206
207impl Output {
208 pub fn is_fixed(&self) -> bool {
209 self.output_hash.is_some()
210 }
211}
212
213#[cfg(feature = "serde")]
216#[test]
217fn deserialize_valid_input_addressed_output() {
218 let json_bytes = r#"
219 {
220 "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432"
221 }"#;
222 let output: Output = serde_json::from_str(json_bytes).expect("must parse");
223
224 assert!(!output.is_fixed());
225}
226
227#[cfg(feature = "serde")]
230#[test]
231fn deserialize_valid_fixed_output() {
232 let json_bytes = r#"
233 {
234 "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
235 "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
236 "hashAlgo": "r:sha256"
237 }"#;
238 let output: Output = serde_json::from_str(json_bytes).expect("must parse");
239
240 assert!(output.is_fixed());
241}
242
243#[cfg(feature = "serde")]
246#[test]
247fn deserialize_with_error_invalid_hash_encoding_fixed_output() {
248 let json_bytes = r#"
249 {
250 "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
251 "hash": "IAMNOTVALIDNIXBASE32",
252 "hashAlgo": "r:sha256"
253 }"#;
254 let output: Result<Output, _> = serde_json::from_str(json_bytes);
255
256 assert!(output.is_err());
257}
258
259#[cfg(feature = "serde")]
262#[test]
263fn deserialize_with_error_invalid_hash_algo_fixed_output() {
264 let json_bytes = r#"
265 {
266 "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
267 "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
268 "hashAlgo": "r:sha1024"
269 }"#;
270 let output: Result<Output, _> = serde_json::from_str(json_bytes);
271
272 assert!(output.is_err());
273}
274
275#[cfg(feature = "serde")]
278#[test]
279fn deserialize_with_error_missing_hash_algo_fixed_output() {
280 let json_bytes = r#"
281 {
282 "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
283 "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
284 }"#;
285 let output: Result<Output, _> = serde_json::from_str(json_bytes);
286
287 assert!(output.is_err());
288}
289
290#[cfg(feature = "serde")]
293#[test]
294fn deserialize_with_error_missing_hash_fixed_output() {
295 let json_bytes = r#"
296 {
297 "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
298 "hashAlgo": "r:sha1024"
299 }"#;
300 let output: Result<Output, _> = serde_json::from_str(json_bytes);
301
302 assert!(output.is_err());
303}
304
305#[cfg(feature = "serde")]
306#[test]
307fn serialize_deserialize() {
308 let json_bytes = r#"
309 {
310 "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432"
311 }"#;
312 let output: Output = serde_json::from_str(json_bytes).expect("must parse");
313
314 let s = serde_json::to_string(&output).expect("Serialize");
315 let output2: Output = serde_json::from_str(&s).expect("must parse again");
316
317 assert_eq!(output, output2);
318}
319
320#[cfg(feature = "serde")]
321#[test]
322fn serialize_deserialize_fixed() {
323 let json_bytes = r#"
324 {
325 "path": "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432",
326 "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
327 "hashAlgo": "r:sha256"
328 }"#;
329 let output: Output = serde_json::from_str(json_bytes).expect("must parse");
330
331 let s = serde_json::to_string_pretty(&output).expect("Serialize");
332 let output2: Output = serde_json::from_str(&s).expect("must parse again");
333
334 assert_eq!(output, output2);
335}
336
337#[cfg(test)]
338mod tests {
339 use crate::nixhash::NixHash;
340
341 use super::{OutputHash, OutputHashMode};
342 use hex_literal::hex;
343 use rstest::rstest;
344
345 const DIGEST_SHA256: [u8; 32] =
346 hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
347 const NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
348
349 #[rstest]
350 #[case::sha256_flat("sha256", &DIGEST_SHA256, OutputHash { mode: OutputHashMode::Flat, hash: NIXHASH_SHA256.clone()})]
351 #[case::sha256_recursive("r:sha256", &DIGEST_SHA256, OutputHash { mode: OutputHashMode::Recursive, hash: NIXHASH_SHA256.clone()})]
352 fn test_from_algo_and_mode_and_digest(
353 #[case] algo_and_mode: &str,
354 #[case] digest: &[u8],
355 #[case] expected: OutputHash,
356 ) {
357 assert_eq!(
358 expected,
359 OutputHash::from_mode_algo_and_digest(algo_and_mode, digest).expect("to parse")
360 );
361 }
362
363 #[test]
364 fn from_algo_and_mode_and_digest_failure() {
365 assert!(OutputHash::from_mode_algo_and_digest("r:sha256", []).is_err());
366 assert!(OutputHash::from_mode_algo_and_digest("ha256", DIGEST_SHA256).is_err());
367 }
368}