nix_compat/nixhash/
ca_hash.rs

1use crate::nixbase32;
2use crate::nixhash::NixHash;
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Unexpected, ser::SerializeMap};
5#[cfg(feature = "serde")]
6use serde_json::{Map, Value};
7use std::borrow::Cow;
8
9/// A Nix CAHash describes a content-addressed hash of a path.
10///
11/// The way Nix prints it as a string is a bit confusing, but there's essentially
12/// three modes, `Flat`, `Nar` and `Text`.
13/// `Flat` and `Nar` support all 4 algos that [NixHash] supports
14/// (sha1, md5, sha256, sha512), `Text` only supports sha256.
15#[derive(Clone, Debug, Eq, PartialEq)]
16pub enum CAHash {
17    Flat(NixHash),  // "fixed flat"
18    Nar(NixHash),   // "fixed recursive"
19    Text([u8; 32]), // "text", only supports sha256
20}
21
22/// Representation for the supported hash modes.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum HashMode {
25    Flat,
26    Nar,
27    Text,
28}
29
30impl CAHash {
31    pub fn hash(&self) -> Cow<NixHash> {
32        match *self {
33            CAHash::Flat(ref digest) => Cow::Borrowed(digest),
34            CAHash::Nar(ref digest) => Cow::Borrowed(digest),
35            CAHash::Text(digest) => Cow::Owned(NixHash::Sha256(digest)),
36        }
37    }
38
39    pub fn mode(&self) -> HashMode {
40        match self {
41            CAHash::Flat(_) => HashMode::Flat,
42            CAHash::Nar(_) => HashMode::Nar,
43            CAHash::Text(_) => HashMode::Text,
44        }
45    }
46
47    /// Constructs a [CAHash] from the textual representation,
48    /// which is one of the three:
49    /// - `text:sha256:$nixbase32sha256digest`
50    /// - `fixed:r:$algo:$nixbase32digest`
51    /// - `fixed:$algo:$nixbase32digest`
52    ///
53    /// These formats are used in NARInfo, for example.
54    pub fn from_nix_hex_str(s: &str) -> Option<Self> {
55        let (tag, s) = s.split_once(':')?;
56
57        match tag {
58            "text" => {
59                let digest = s.strip_prefix("sha256:")?;
60                let digest = nixbase32::decode_fixed(digest).ok()?;
61                Some(CAHash::Text(digest))
62            }
63            "fixed" => {
64                if let Some(s) = s.strip_prefix("r:") {
65                    NixHash::from_nix_nixbase32(s).map(CAHash::Nar)
66                } else {
67                    NixHash::from_nix_nixbase32(s).map(CAHash::Flat)
68                }
69            }
70            _ => None,
71        }
72    }
73
74    /// Formats a [CAHash] in the Nix default hash format, which is the format
75    /// that's used in NARInfos for example.
76    pub fn to_nix_nixbase32_string(&self) -> String {
77        let (algo, hash) = match self {
78            CAHash::Flat(h) => match h {
79                NixHash::Md5(h) => ("fixed:md5", &h[..]),
80                NixHash::Sha1(h) => ("fixed:sha1", &h[..]),
81                NixHash::Sha256(h) => ("fixed:sha256", &h[..]),
82                NixHash::Sha512(h) => ("fixed:sha512", &h[..]),
83            },
84            CAHash::Nar(h) => match h {
85                NixHash::Md5(h) => ("fixed:r:md5", &h[..]),
86                NixHash::Sha1(h) => ("fixed:r:sha1", &h[..]),
87                NixHash::Sha256(h) => ("fixed:r:sha256", &h[..]),
88                NixHash::Sha512(h) => ("fixed:r:sha512", &h[..]),
89            },
90            CAHash::Text(h) => ("text:sha256", &h[..]),
91        };
92
93        format!("{}:{}", algo, nixbase32::encode(hash))
94    }
95
96    /// This takes a serde_json::Map and turns it into this structure. This is necessary to do such
97    /// shenigans because we have external consumers, like the Derivation parser, who would like to
98    /// know whether we have a invalid or a missing NixHashWithMode structure in another structure,
99    /// e.g. Output.
100    /// This means we have this combinatorial situation:
101    ///
102    /// - no hash, no hashAlgo: no [CAHash] so we return Ok(None).
103    /// - present hash, missing hashAlgo: invalid, we will return missing_field
104    /// - missing hash, present hashAlgo: same
105    /// - present hash, present hashAlgo: either we return ourselves or a type/value validation
106    ///   error.
107    ///
108    /// This function is for internal consumption regarding those needs until we have a better
109    /// solution. Now this is said, let's explain how this works.
110    ///
111    /// We want to map the serde data model into a [CAHash].
112    ///
113    /// The serde data model has a `hash` field (containing a digest in nixbase32),
114    /// and a `hashAlgo` field, containing the stringified hash algo.
115    /// In case the hash is recursive, hashAlgo also has a `r:` prefix.
116    ///
117    /// This is to match how `nix show-derivation` command shows them in JSON
118    /// representation.
119    #[cfg(feature = "serde")]
120    pub(crate) fn from_map<'de, D>(map: &Map<String, Value>) -> Result<Option<Self>, D::Error>
121    where
122        D: Deserializer<'de>,
123    {
124        use super::algos::SUPPORTED_ALGOS;
125        use super::decode_digest;
126        use crate::nixhash::HashAlgo;
127
128        // If we don't have hash neither hashAlgo, let's just return None.
129        if !map.contains_key("hash") && !map.contains_key("hashAlgo") {
130            return Ok(None);
131        }
132
133        let hash_algo_v = map.get("hashAlgo").ok_or_else(|| {
134            serde::de::Error::missing_field(
135                "couldn't extract `hashAlgo` key, but `hash` key present",
136            )
137        })?;
138        let hash_algo = hash_algo_v.as_str().ok_or_else(|| {
139            serde::de::Error::invalid_type(Unexpected::Other(&hash_algo_v.to_string()), &"a string")
140        })?;
141        let (mode_is_nar, hash_algo) = if let Some(s) = hash_algo.strip_prefix("r:") {
142            (true, s)
143        } else {
144            (false, hash_algo)
145        };
146        let hash_algo = HashAlgo::try_from(hash_algo).map_err(|e| {
147            serde::de::Error::invalid_value(
148                Unexpected::Other(&e.to_string()),
149                &format!("one of {}", SUPPORTED_ALGOS.join(",")).as_str(),
150            )
151        })?;
152
153        let hash_v = map.get("hash").ok_or_else(|| {
154            serde::de::Error::missing_field(
155                "couldn't extract `hash` key but `hashAlgo` key present",
156            )
157        })?;
158        let hash = hash_v.as_str().ok_or_else(|| {
159            serde::de::Error::invalid_type(Unexpected::Other(&hash_v.to_string()), &"a string")
160        })?;
161        let hash = decode_digest(hash.as_bytes(), hash_algo)
162            .map_err(|e| serde::de::Error::custom(e.to_string()))?;
163        if mode_is_nar {
164            Ok(Some(Self::Nar(hash)))
165        } else {
166            Ok(Some(Self::Flat(hash)))
167        }
168    }
169}
170
171#[cfg(feature = "serde")]
172impl Serialize for CAHash {
173    /// map a CAHash into the serde data model.
174    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
175    where
176        S: Serializer,
177    {
178        let mut map = serializer.serialize_map(Some(2))?;
179        match self {
180            CAHash::Flat(h) => {
181                map.serialize_entry("hash", &nixbase32::encode(h.digest_as_bytes()))?;
182                map.serialize_entry("hashAlgo", &h.algo())?;
183            }
184            CAHash::Nar(h) => {
185                map.serialize_entry("hash", &nixbase32::encode(h.digest_as_bytes()))?;
186                map.serialize_entry("hashAlgo", &format!("r:{}", &h.algo()))?;
187            }
188            // It is not legal for derivations to use this (which is where
189            // we're currently exercising [Serialize] mostly,
190            // but it's still good to be able to serialize other CA hashes too.
191            CAHash::Text(h) => {
192                map.serialize_entry("hash", &nixbase32::encode(h.as_ref()))?;
193                map.serialize_entry("hashAlgo", "text")?;
194            }
195        };
196        map.end()
197    }
198}
199
200#[cfg(feature = "serde")]
201impl<'de> Deserialize<'de> for CAHash {
202    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
203    where
204        D: Deserializer<'de>,
205    {
206        let value = Self::from_map::<D>(&Map::deserialize(deserializer)?)?;
207
208        match value {
209            None => Err(serde::de::Error::custom("couldn't parse as map")),
210            Some(v) => Ok(v),
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    #[cfg(feature = "serde")]
218    use hex_literal::hex;
219
220    #[cfg(feature = "serde")]
221    use crate::{
222        derivation::CAHash,
223        nixhash::{HashAlgo, NixHash},
224    };
225
226    #[cfg(feature = "serde")]
227    #[test]
228    fn serialize_flat() {
229        let json_bytes = r#"{
230  "hash": "1fnf2m46ya7r7afkcb8ba2j0sc4a85m749sh9jz64g4hx6z3r088",
231  "hashAlgo": "sha256"
232}"#;
233        let hash = CAHash::Flat(
234            NixHash::from_algo_and_digest(
235                HashAlgo::Sha256,
236                &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),
237            )
238            .unwrap(),
239        );
240        let serialized = serde_json::to_string_pretty(&hash).unwrap();
241        assert_eq!(serialized, json_bytes);
242    }
243
244    #[cfg(feature = "serde")]
245    #[test]
246    fn serialize_nar() {
247        let json_bytes = r#"{
248  "hash": "1fnf2m46ya7r7afkcb8ba2j0sc4a85m749sh9jz64g4hx6z3r088",
249  "hashAlgo": "r:sha256"
250}"#;
251        let hash = CAHash::Nar(
252            NixHash::from_algo_and_digest(
253                HashAlgo::Sha256,
254                &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),
255            )
256            .unwrap(),
257        );
258        let serialized = serde_json::to_string_pretty(&hash).unwrap();
259        assert_eq!(serialized, json_bytes);
260    }
261
262    #[cfg(feature = "serde")]
263    #[test]
264    fn deserialize_flat() {
265        let json_bytes = r#"
266        {
267            "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
268            "hashAlgo": "sha256"
269        }"#;
270        let hash: CAHash = serde_json::from_str(json_bytes).expect("must parse");
271
272        assert_eq!(
273            hash,
274            CAHash::Flat(
275                NixHash::from_algo_and_digest(
276                    HashAlgo::Sha256,
277                    &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba")
278                )
279                .unwrap()
280            )
281        );
282    }
283
284    #[cfg(feature = "serde")]
285    #[test]
286    fn deserialize_hex() {
287        let json_bytes = r#"
288        {
289            "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
290            "hashAlgo": "r:sha256"
291        }"#;
292        let hash: CAHash = serde_json::from_str(json_bytes).expect("must parse");
293
294        assert_eq!(
295            hash,
296            CAHash::Nar(
297                NixHash::from_algo_and_digest(
298                    HashAlgo::Sha256,
299                    &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba")
300                )
301                .unwrap()
302            )
303        );
304    }
305
306    #[cfg(feature = "serde")]
307    #[test]
308    fn deserialize_nixbase32() {
309        let json_bytes = r#"
310        {
311            "hash": "1fnf2m46ya7r7afkcb8ba2j0sc4a85m749sh9jz64g4hx6z3r088",
312            "hashAlgo": "r:sha256"
313        }"#;
314        let hash: CAHash = serde_json::from_str(json_bytes).expect("must parse");
315
316        assert_eq!(
317            hash,
318            CAHash::Nar(
319                NixHash::from_algo_and_digest(
320                    HashAlgo::Sha256,
321                    &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),
322                )
323                .unwrap()
324            )
325        );
326    }
327
328    #[cfg(feature = "serde")]
329    #[test]
330    fn deserialize_base64() {
331        let json_bytes = r#"
332        {
333            "hash": "CIE8vumQPGK+TFAncmpBijANpFALLTadOvkob0gVzro=",
334            "hashAlgo": "r:sha256"
335        }"#;
336        let hash: CAHash = serde_json::from_str(json_bytes).expect("must parse");
337
338        assert_eq!(
339            hash,
340            CAHash::Nar(
341                NixHash::from_algo_and_digest(
342                    HashAlgo::Sha256,
343                    &hex!("08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),
344                )
345                .unwrap()
346            )
347        );
348    }
349
350    #[cfg(feature = "serde")]
351    #[test]
352    fn serialize_deserialize_nar() {
353        let json_bytes = r#"
354        {
355            "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
356            "hashAlgo": "r:sha256"
357        }"#;
358        let hash: CAHash = serde_json::from_str(json_bytes).expect("must parse");
359
360        let serialized = serde_json::to_string(&hash).expect("Serialize");
361        let hash2: CAHash = serde_json::from_str(&serialized).expect("must parse again");
362
363        assert_eq!(hash, hash2);
364    }
365
366    #[cfg(feature = "serde")]
367    #[test]
368    fn serialize_deserialize_flat() {
369        let json_bytes = r#"
370        {
371            "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba",
372            "hashAlgo": "sha256"
373        }"#;
374        let hash: CAHash = serde_json::from_str(json_bytes).expect("must parse");
375
376        let serialized = serde_json::to_string(&hash).expect("Serialize");
377        let hash2: CAHash = serde_json::from_str(&serialized).expect("must parse again");
378
379        assert_eq!(hash, hash2);
380    }
381}