Skip to main content

nix_compat/derivation/
output.rs

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/// References the derivation output.
11#[derive(Clone, Debug, Default, Eq, PartialEq)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize))]
13pub struct Output {
14    /// Store path of build result.
15    pub path: Option<StorePath>,
16
17    #[cfg_attr(feature = "serde", serde(flatten))]
18    pub output_hash: Option<OutputHash>,
19}
20
21/// Represents the information about the hash of a single-output FOD.
22/// We store it in a [OutputHashMode] and [NixHash].
23/// The serde model uses a different format, as we want to emit the same JSON:
24/// There we use `hashAlgo` and `hash`:
25///  - `hashAlgo`: optional `r:` prefix (for recursive),
26///    followed by hash algo identifier (`sha1`, `sha256`, `sha512`, `md5`)
27///  - `hash`: hexlower-encoded digest
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct OutputHash {
30    pub mode: OutputHashMode,
31    pub hash: NixHash,
32}
33
34/// Whether the FOD describes the hash of the raw contents (only possible if it's a single file),
35/// or a digest over the NAR representation of the contents.
36#[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    /// Construct from a string containing the algo (with an optional `r:` prefix), and a digest.
71    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    /// Returns the OutputHashMode prefix str and the algo, concatenated.
90    /// This is used in the ATerm representation.
91    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            // deserialize Option<OutputHash>. we don't do this in a `impl Deserialize for OutputHash`,
150            // as this is flattened and we don't want to silently swallow errors.
151            output_hash: match (fields.get("hash"), fields.get("hashAlgo")) {
152                // If hash is not provided, do nothing.
153                (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/// Errors that can occur during the validation of a specific
188// [crate::derivation::Output] of a [crate::derivation::Derivation].
189#[derive(Debug, thiserror::Error, PartialEq)]
190pub enum ParseOutputHashModeError {
191    #[error("Invalid hash mode: {0}")]
192    InvalidHashMode(String),
193}
194
195/// Errors that can occur during the validation of a specific
196// [crate::derivation::Output] of a [crate::derivation::Derivation].
197#[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/// This ensures that a potentially valid input addressed
214/// output is deserialized as a non-fixed output.
215#[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/// This ensures that a potentially valid fixed output
228/// output deserializes fine as a fixed output.
229#[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/// This ensures that parsing an input with the invalid hash encoding
244/// will result in a parsing failure.
245#[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/// This ensures that parsing an input with the wrong hash algo
260/// will result in a parsing failure.
261#[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/// This ensures that parsing an input with the missing hash algo but present hash will result in a
276/// parsing failure.
277#[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/// This ensures that parsing an input with the missing hash but present hash algo will result in a
291/// parsing failure.
292#[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}