nix_compat/derivation/
parser.rs

1//! This module constructs a [Derivation] by parsing its [ATerm][]
2//! serialization.
3//!
4//! [ATerm]: http://program-transformation.org/Tools/ATermFormat.html
5
6use nom::bytes::streaming::tag;
7use nom::character::streaming::char as nomchar;
8use nom::combinator::{all_consuming, consumed, map_res};
9use nom::multi::{separated_list0, separated_list1};
10use nom::sequence::{delimited, preceded, separated_pair, terminated};
11use nom::Parser;
12use std::collections::{btree_map, BTreeMap, BTreeSet};
13use thiserror;
14
15use crate::derivation::parse_error::{into_nomerror, ErrorKind, NomError, NomResult};
16use crate::derivation::{write, CAHash, Derivation, Output};
17use crate::store_path::{self, StorePath};
18use crate::{aterm, nixhash, nixhash::NixHash};
19
20#[derive(Debug, thiserror::Error)]
21pub enum Error<I> {
22    #[error("parsing error: {0}")]
23    Parser(#[from] NomError<I>),
24    #[error("premature EOF")]
25    Incomplete,
26    #[error("validation error: {0}")]
27    Validation(super::DerivationError),
28}
29
30/// Convenience conversion of borring Error to an owned counterpart.
31impl From<Error<&[u8]>> for Error<Vec<u8>> {
32    fn from(value: Error<&[u8]>) -> Self {
33        match value {
34            Error::Parser(nom_error) => Error::Parser(NomError {
35                input: nom_error.input.to_vec(),
36                code: nom_error.code,
37            }),
38            Error::Incomplete => Error::Incomplete,
39            Error::Validation(e) => Error::Validation(e),
40        }
41    }
42}
43
44pub(crate) fn parse(i: &[u8]) -> Result<Derivation, Error<&[u8]>> {
45    match all_consuming(parse_derivation).parse(i) {
46        Ok((rest, derivation)) => {
47            // this shouldn't happen, as all_consuming shouldn't return.
48            debug_assert!(rest.is_empty());
49
50            // invoke validate
51            derivation.validate(true).map_err(Error::Validation)?;
52
53            Ok(derivation)
54        }
55        Err(nom::Err::Incomplete(_)) => Err(Error::Incomplete),
56        Err(nom::Err::Error(e) | nom::Err::Failure(e)) => Err(e.into()),
57    }
58}
59
60/// This parses a derivation in streaming fashion.
61/// If the parse is successful, it returns the leftover bytes which were not used for the parsing.
62/// If the parse is unsuccessful, either it returns incomplete or an error with the input as
63/// leftover.
64#[allow(dead_code)]
65pub fn parse_streaming(i: &[u8]) -> (Result<Derivation, Error<&[u8]>>, &[u8]) {
66    match consumed(parse_derivation).parse(i) {
67        Ok((_, (rest, derivation))) => {
68            // invoke validate
69            if let Err(e) = derivation.validate(true).map_err(Error::Validation) {
70                return (Err(e), i);
71            }
72
73            (Ok(derivation), rest)
74        }
75        Err(nom::Err::Incomplete(_)) => (Err(Error::Incomplete), i),
76        Err(nom::Err::Error(e) | nom::Err::Failure(e)) => (Err(e.into()), i),
77    }
78}
79
80/// Consume a string containing the algo, and optionally a `r:`
81/// prefix, and a digest (bytes), return a [CAHash::Nar] or [CAHash::Flat].
82// TODO: This maybe should belong in ca_hash.rs
83fn from_algo_and_mode_and_digest<B: AsRef<[u8]>>(
84    algo_and_mode: &str,
85    digest: B,
86) -> Result<CAHash, nixhash::Error> {
87    Ok(match algo_and_mode.strip_prefix("r:") {
88        Some(algo) => nixhash::CAHash::Nar(NixHash::from_algo_and_digest(
89            algo.try_into()?,
90            digest.as_ref(),
91        )?),
92        None => nixhash::CAHash::Flat(NixHash::from_algo_and_digest(
93            algo_and_mode.try_into()?,
94            digest.as_ref(),
95        )?),
96    })
97}
98
99/// Parse one output in ATerm. This is 4 string fields inside parans:
100/// output name, output path, algo (and mode), digest.
101/// Returns the output name and [Output] struct.
102fn parse_output(i: &[u8]) -> NomResult<&[u8], (String, Output)> {
103    delimited(
104        nomchar('('),
105        map_res(
106            |i| {
107                (
108                    terminated(aterm::parse_string_field, nomchar(',')),
109                    terminated(aterm::parse_string_field, nomchar(',')),
110                    terminated(aterm::parse_string_field, nomchar(',')),
111                    aterm::parse_bytes_field,
112                )
113                    .parse(i)
114                    .map_err(into_nomerror)
115            },
116            |(output_name, output_path, algo_and_mode, encoded_digest)| {
117                // convert these 4 fields into an [Output].
118                let ca_hash_res = {
119                    if algo_and_mode.is_empty() && encoded_digest.is_empty() {
120                        None
121                    } else {
122                        match data_encoding::HEXLOWER.decode(&encoded_digest) {
123                            Ok(digest) => {
124                                Some(from_algo_and_mode_and_digest(&algo_and_mode, digest))
125                            }
126                            Err(e) => Some(Err(nixhash::Error::InvalidBase64Encoding(e))),
127                        }
128                    }
129                }
130                .transpose();
131
132                match ca_hash_res {
133                    Ok(hash_with_mode) => Ok((
134                        output_name,
135                        Output {
136                            // TODO: Check if allowing empty paths here actually makes sense
137                            //       or we should make this code stricter.
138                            path: if output_path.is_empty() {
139                                None
140                            } else {
141                                Some(string_to_store_path(i, &output_path)?)
142                            },
143                            ca_hash: hash_with_mode,
144                        },
145                    )),
146                    Err(e) => Err(nom::Err::Failure(NomError {
147                        input: i,
148                        code: ErrorKind::NixHashError(e),
149                    })),
150                }
151            },
152        ),
153        nomchar(')'),
154    )
155    .parse(i)
156}
157
158/// Parse multiple outputs in ATerm. This is a list of things acccepted by
159/// parse_output, and takes care of turning the (String, Output) returned from
160/// it to a BTreeMap.
161/// We don't use parse_kv here, as it's dealing with 2-tuples, and these are
162/// 4-tuples.
163fn parse_outputs(i: &[u8]) -> NomResult<&[u8], BTreeMap<String, Output>> {
164    let res = delimited(
165        nomchar('['),
166        separated_list1(tag(","), parse_output),
167        nomchar(']'),
168    )
169    .parse(i);
170
171    match res {
172        Ok((rst, outputs_lst)) => {
173            let mut outputs = BTreeMap::default();
174            for (output_name, output) in outputs_lst.into_iter() {
175                if outputs.contains_key(&output_name) {
176                    return Err(nom::Err::Failure(NomError {
177                        input: i,
178                        code: ErrorKind::DuplicateMapKey(output_name.to_string()),
179                    }));
180                }
181                outputs.insert(output_name, output);
182            }
183            Ok((rst, outputs))
184        }
185        // pass regular parse errors along
186        Err(e) => Err(e),
187    }
188}
189
190fn parse_input_derivations(
191    i: &[u8],
192) -> NomResult<&[u8], BTreeMap<StorePath<String>, BTreeSet<String>>> {
193    let (i, input_derivations_list) = parse_kv(aterm::parse_string_list)(i)?;
194
195    // This is a HashMap of drv paths to a list of output names.
196    let mut input_derivations: BTreeMap<StorePath<String>, BTreeSet<_>> = BTreeMap::new();
197
198    for (input_derivation, output_names) in input_derivations_list {
199        let mut new_output_names = BTreeSet::new();
200        for output_name in output_names.into_iter() {
201            if new_output_names.contains(&output_name) {
202                return Err(nom::Err::Failure(NomError {
203                    input: i,
204                    code: ErrorKind::DuplicateInputDerivationOutputName(
205                        input_derivation.to_string(),
206                        output_name.to_string(),
207                    ),
208                }));
209            }
210            new_output_names.insert(output_name);
211        }
212
213        let input_derivation = string_to_store_path(i, input_derivation.as_str())?;
214
215        input_derivations.insert(input_derivation, new_output_names);
216    }
217
218    Ok((i, input_derivations))
219}
220
221fn parse_input_sources(i: &[u8]) -> NomResult<&[u8], BTreeSet<StorePath<String>>> {
222    let (i, input_sources_lst) = aterm::parse_string_list(i).map_err(into_nomerror)?;
223
224    let mut input_sources: BTreeSet<_> = BTreeSet::new();
225    for input_source in input_sources_lst.into_iter() {
226        let input_source = string_to_store_path(i, input_source.as_str())?;
227        if input_sources.contains(&input_source) {
228            return Err(nom::Err::Failure(NomError {
229                input: i,
230                code: ErrorKind::DuplicateInputSource(input_source.to_owned()),
231            }));
232        } else {
233            input_sources.insert(input_source);
234        }
235    }
236
237    Ok((i, input_sources))
238}
239
240fn string_to_store_path<'a, 'i, S>(
241    i: &'i [u8],
242    path_str: &'a str,
243) -> Result<StorePath<S>, nom::Err<NomError<&'i [u8]>>>
244where
245    S: std::clone::Clone + AsRef<str> + std::convert::From<&'a str>,
246{
247    let path =
248        StorePath::from_absolute_path(path_str.as_bytes()).map_err(|e: store_path::Error| {
249            nom::Err::Failure(NomError {
250                input: i,
251                code: e.into(),
252            })
253        })?;
254
255    #[cfg(debug_assertions)]
256    assert_eq!(path_str, path.to_absolute_path());
257
258    Ok(path)
259}
260
261pub fn parse_derivation(i: &[u8]) -> NomResult<&[u8], Derivation> {
262    use nom::Parser;
263    preceded(
264        tag(write::DERIVATION_PREFIX),
265        delimited(
266            // inside parens
267            nomchar('('),
268            // tuple requires all errors to be of the same type, so we need to be a
269            // bit verbose here wrapping generic IResult into [NomATermResult].
270            (
271                // parse outputs
272                terminated(parse_outputs, nomchar(',')),
273                // // parse input derivations
274                terminated(parse_input_derivations, nomchar(',')),
275                // // parse input sources
276                terminated(parse_input_sources, nomchar(',')),
277                // // parse system
278                |i| {
279                    terminated(aterm::parse_string_field, nomchar(','))
280                        .parse(i)
281                        .map_err(into_nomerror)
282                },
283                // // parse builder
284                |i| {
285                    terminated(aterm::parse_string_field, nomchar(','))
286                        .parse(i)
287                        .map_err(into_nomerror)
288                },
289                // // parse arguments
290                |i| {
291                    terminated(aterm::parse_string_list, nomchar(','))
292                        .parse(i)
293                        .map_err(into_nomerror)
294                },
295                // parse environment
296                parse_kv(aterm::parse_bytes_field),
297            ),
298            nomchar(')'),
299        )
300        .map(
301            |(
302                outputs,
303                input_derivations,
304                input_sources,
305                system,
306                builder,
307                arguments,
308                environment,
309            )| {
310                Derivation {
311                    arguments,
312                    builder,
313                    environment,
314                    input_derivations,
315                    input_sources,
316                    outputs,
317                    system,
318                }
319            },
320        ),
321    )
322    .parse(i)
323}
324
325/// Parse a list of key/value pairs into a BTreeMap.
326/// The parser for the values can be passed in.
327/// In terms of ATerm, this is just a 2-tuple,
328/// but we have the additional restriction that the first element needs to be
329/// unique across all tuples.
330pub(crate) fn parse_kv<'a, V, VF>(
331    vf: VF,
332) -> impl FnMut(&'a [u8]) -> NomResult<&'a [u8], BTreeMap<String, V>> + 'static
333where
334    VF: FnMut(&'a [u8]) -> nom::IResult<&'a [u8], V, nom::error::Error<&'a [u8]>> + Clone + 'static,
335{
336    move |i|
337    // inside brackets
338    delimited(
339        nomchar('['),
340        |ii| {
341            let res = separated_list0(
342                nomchar(','),
343                // inside parens
344                delimited(
345                    nomchar('('),
346                    separated_pair(
347                        aterm::parse_string_field,
348                        nomchar(','),
349                        vf.clone(),
350                    ),
351                    nomchar(')'),
352                ),
353            ).parse(ii).map_err(into_nomerror);
354
355            match res {
356                Ok((rest, pairs)) => {
357                    let mut kvs: BTreeMap<String, V> = BTreeMap::new();
358                    for (k, v) in pairs.into_iter() {
359                        // collect the 2-tuple to a BTreeMap,
360                        // and fail if the key was already seen before.
361                        match kvs.entry(k) {
362                            btree_map::Entry::Vacant(e) => { e.insert(v); },
363                            btree_map::Entry::Occupied(e) => {
364                                return Err(nom::Err::Failure(NomError {
365                                    input: i,
366                                    code: ErrorKind::DuplicateMapKey(e.key().clone()),
367                                }));
368                            }
369                        }
370                    }
371                    Ok((rest, kvs))
372                }
373                Err(e) => Err(e),
374            }
375        },
376        nomchar(']'),
377    ).parse(i)
378}
379
380#[cfg(test)]
381mod tests {
382    use crate::store_path::StorePathRef;
383    use std::collections::{BTreeMap, BTreeSet};
384    use std::sync::LazyLock;
385
386    use crate::{
387        derivation::{
388            parse_error::ErrorKind, parser::from_algo_and_mode_and_digest, CAHash, NixHash, Output,
389        },
390        store_path::StorePath,
391    };
392    use bstr::{BString, ByteSlice};
393    use hex_literal::hex;
394    use rstest::rstest;
395
396    const DIGEST_SHA256: [u8; 32] =
397        hex!("a5ce9c155ed09397614646c9717fc7cd94b1023d7b76b618d409e4fefd6e9d39");
398
399    static NIXHASH_SHA256: NixHash = NixHash::Sha256(DIGEST_SHA256);
400    static EXP_MULTI_OUTPUTS: LazyLock<BTreeMap<String, Output>> = LazyLock::new(|| {
401        let mut b = BTreeMap::new();
402        b.insert(
403            "lib".to_string(),
404            Output {
405                path: Some(
406                    StorePath::from_bytes(b"2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib")
407                        .unwrap(),
408                ),
409                ca_hash: None,
410            },
411        );
412        b.insert(
413            "out".to_string(),
414            Output {
415                path: Some(
416                    StorePath::from_bytes(
417                        b"55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out".as_bytes(),
418                    )
419                    .unwrap(),
420                ),
421                ca_hash: None,
422            },
423        );
424        b
425    });
426
427    static EXP_AB_MAP: LazyLock<BTreeMap<String, BString>> = LazyLock::new(|| {
428        let mut b = BTreeMap::new();
429        b.insert("a".to_string(), b"1".into());
430        b.insert("b".to_string(), b"2".into());
431        b
432    });
433
434    static EXP_INPUT_DERIVATIONS_SIMPLE: LazyLock<BTreeMap<StorePath<String>, BTreeSet<String>>> =
435        LazyLock::new(|| {
436            let mut b = BTreeMap::new();
437            b.insert(
438                StorePath::from_bytes(b"8bjm87p310sb7r2r0sg4xrynlvg86j8k-hello-2.12.1.tar.gz.drv")
439                    .unwrap(),
440                {
441                    let mut output_names = BTreeSet::new();
442                    output_names.insert("out".to_string());
443                    output_names
444                },
445            );
446            b.insert(
447                StorePath::from_bytes(b"p3jc8aw45dza6h52v81j7lk69khckmcj-bash-5.2-p15.drv")
448                    .unwrap(),
449                {
450                    let mut output_names = BTreeSet::new();
451                    output_names.insert("out".to_string());
452                    output_names.insert("lib".to_string());
453                    output_names
454                },
455            );
456            b
457        });
458
459    static EXP_INPUT_DERIVATIONS_SIMPLE_ATERM: LazyLock<String> = LazyLock::new(|| {
460        format!(
461            "[(\"{0}\",[\"out\"]),(\"{1}\",[\"out\",\"lib\"])]",
462            "/nix/store/8bjm87p310sb7r2r0sg4xrynlvg86j8k-hello-2.12.1.tar.gz.drv",
463            "/nix/store/p3jc8aw45dza6h52v81j7lk69khckmcj-bash-5.2-p15.drv"
464        )
465    });
466
467    static EXP_INPUT_SOURCES_SIMPLE: LazyLock<BTreeSet<String>> = LazyLock::new(|| {
468        let mut b = BTreeSet::new();
469        b.insert("/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out".to_string());
470        b.insert("/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib".to_string());
471        b
472    });
473
474    /// Ensure parsing KVs works
475    #[rstest]
476    #[case::empty(b"[]", &BTreeMap::new(), b"")]
477    #[case::simple(b"[(\"a\",\"1\"),(\"b\",\"2\")]", &EXP_AB_MAP, b"")]
478    fn parse_kv(
479        #[case] input: &'static [u8],
480        #[case] expected: &BTreeMap<String, BString>,
481        #[case] exp_rest: &[u8],
482    ) {
483        let (rest, parsed) =
484            super::parse_kv(crate::aterm::parse_bytes_field)(input).expect("must parse");
485        assert_eq!(exp_rest, rest, "expected remainder");
486        assert_eq!(*expected, parsed);
487    }
488
489    #[rstest]
490    #[case::incomplete_empty(b"[")]
491    #[case::incomplete_simple(b"[(\"a\",\"1\")")]
492    #[case::incomplete_complicated_escape(b"[(\"a")]
493    #[case::incomplete_complicated_sep(b"[(\"a\",")]
494    #[case::incomplete_complicated_multi_escape(b"[(\"a\",\"")]
495    #[case::incomplete_complicated_multi_outer_sep(b"[(\"a\",\"b\"),")]
496    fn parse_kv_incomplete(#[case] input: &'static [u8]) {
497        assert!(matches!(
498            super::parse_kv(crate::aterm::parse_bytes_field)(input),
499            Err(nom::Err::Incomplete(_))
500        ));
501    }
502
503    /// Ensures the kv parser complains about duplicate map keys
504    #[test]
505    fn parse_kv_fail_dup_keys() {
506        let input: &'static [u8] = b"[(\"a\",\"1\"),(\"a\",\"2\")]";
507        let e = super::parse_kv(crate::aterm::parse_bytes_field)(input).expect_err("must fail");
508
509        match e {
510            nom::Err::Failure(e) => {
511                assert_eq!(ErrorKind::DuplicateMapKey("a".to_string()), e.code);
512            }
513            _ => panic!("unexpected error"),
514        }
515    }
516
517    /// Ensure parsing input derivations works.
518    #[rstest]
519    #[case::empty(b"[]", &BTreeMap::new())]
520    #[case::simple(EXP_INPUT_DERIVATIONS_SIMPLE_ATERM.as_bytes(), &EXP_INPUT_DERIVATIONS_SIMPLE)]
521    fn parse_input_derivations(
522        #[case] input: &'static [u8],
523        #[case] expected: &BTreeMap<StorePath<String>, BTreeSet<String>>,
524    ) {
525        let (rest, parsed) = super::parse_input_derivations(input).expect("must parse");
526
527        assert_eq!(expected, &parsed, "parsed mismatch");
528        assert!(rest.is_empty(), "rest must be empty");
529    }
530
531    /// Ensures the input derivation parser complains about duplicate output names
532    #[test]
533    fn parse_input_derivations_fail_dup_output_names() {
534        let input_str = format!(
535            "[(\"{0}\",[\"out\"]),(\"{1}\",[\"out\",\"out\"])]",
536            "/nix/store/8bjm87p310sb7r2r0sg4xrynlvg86j8k-hello-2.12.1.tar.gz.drv",
537            "/nix/store/p3jc8aw45dza6h52v81j7lk69khckmcj-bash-5.2-p15.drv"
538        );
539        let e = super::parse_input_derivations(input_str.as_bytes()).expect_err("must fail");
540
541        match e {
542            nom::Err::Failure(e) => {
543                assert_eq!(
544                    ErrorKind::DuplicateInputDerivationOutputName(
545                        "/nix/store/p3jc8aw45dza6h52v81j7lk69khckmcj-bash-5.2-p15.drv".to_string(),
546                        "out".to_string()
547                    ),
548                    e.code
549                );
550            }
551            _ => panic!("unexpected error"),
552        }
553    }
554
555    /// Ensure parsing input sources works
556    #[rstest]
557    #[case::empty(b"[]", &BTreeSet::new())]
558    #[case::simple(b"[\"/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out\",\"/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib\"]", &EXP_INPUT_SOURCES_SIMPLE)]
559    fn parse_input_sources(#[case] input: &'static [u8], #[case] expected: &BTreeSet<String>) {
560        let (rest, parsed) = super::parse_input_sources(input).expect("must parse");
561
562        assert_eq!(
563            expected,
564            &parsed
565                .iter()
566                .map(StorePath::to_absolute_path)
567                .collect::<BTreeSet<_>>(),
568            "parsed mismatch"
569        );
570        assert!(rest.is_empty(), "rest must be empty");
571    }
572
573    /// Ensures the input sources parser complains about duplicate input sources
574    #[test]
575    fn parse_input_sources_fail_dup_keys() {
576        let input: &'static [u8] = b"[\"/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-foo\",\"/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-foo\"]";
577        let e = super::parse_input_sources(input).expect_err("must fail");
578
579        match e {
580            nom::Err::Failure(e) => {
581                assert_eq!(
582                    ErrorKind::DuplicateInputSource(
583                        StorePathRef::from_absolute_path(
584                            "/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-foo".as_bytes()
585                        )
586                        .unwrap()
587                        .to_owned()
588                    ),
589                    e.code
590                );
591            }
592            _ => panic!("unexpected error"),
593        }
594    }
595
596    #[rstest]
597    #[case::simple(
598        br#"("out","/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo","","")"#,
599        ("out".to_string(), Output {
600            path: Some(
601                StorePathRef::from_absolute_path("/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo".as_bytes()).unwrap().to_owned()),
602            ca_hash: None
603        })
604    )]
605    #[case::fod(
606        br#"("out","/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar","r:sha256","08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba")"#,
607        ("out".to_string(), Output {
608            path: Some(
609                StorePathRef::from_absolute_path(
610                "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar".as_bytes()).unwrap().to_owned()),
611            ca_hash: Some(from_algo_and_mode_and_digest("r:sha256",
612                   data_encoding::HEXLOWER.decode(b"08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba").unwrap()            ).unwrap()),
613        })
614     )]
615    fn parse_output(#[case] input: &[u8], #[case] expected: (String, Output)) {
616        let (rest, parsed) = super::parse_output(input).expect("must parse");
617        assert!(rest.is_empty());
618        assert_eq!(expected, parsed);
619    }
620
621    #[rstest]
622    #[case::multi_out(
623        br#"[("lib","/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib","",""),("out","/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out","","")]"#,
624        &EXP_MULTI_OUTPUTS
625    )]
626    fn parse_outputs(#[case] input: &[u8], #[case] expected: &BTreeMap<String, Output>) {
627        let (rest, parsed) = super::parse_outputs(input).expect("must parse");
628        assert!(rest.is_empty());
629        assert_eq!(*expected, parsed);
630    }
631
632    #[rstest]
633    #[case::sha256_flat("sha256", &DIGEST_SHA256, CAHash::Flat(NIXHASH_SHA256.clone()))]
634    #[case::sha256_recursive("r:sha256", &DIGEST_SHA256, CAHash::Nar(NIXHASH_SHA256.clone()))]
635    fn test_from_algo_and_mode_and_digest(
636        #[case] algo_and_mode: &str,
637        #[case] digest: &[u8],
638        #[case] expected: CAHash,
639    ) {
640        assert_eq!(
641            expected,
642            from_algo_and_mode_and_digest(algo_and_mode, digest).unwrap()
643        );
644    }
645
646    #[test]
647    fn from_algo_and_mode_and_digest_failure() {
648        assert!(from_algo_and_mode_and_digest("r:sha256", []).is_err());
649        assert!(from_algo_and_mode_and_digest("ha256", DIGEST_SHA256).is_err());
650    }
651}