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