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