Skip to main content

composefs_boot/
cmdline.rs

1//! Kernel command line parsing and manipulation.
2//!
3//! This module provides utilities for parsing and generating kernel command line arguments,
4//! with specific support for composefs parameters. It handles the kernel's simple quoting
5//! mechanism and provides functions to extract and create composefs= arguments with optional
6//! insecure mode indicators.
7
8use anyhow::{Context, Result};
9use composefs::fsverity::{Algorithm, FsVerityHashValue};
10
11/// Legacy kernel argument for V2 EROFS: `composefs=<hex_digest>`.
12///
13/// Shorthand for `composefs.digest=v2-<hash>-12:<hex>`.  Used in existing
14/// sealed UKIs.  The initramfs checks for [`KARG_COMPOSEFS_DIGEST`] first,
15/// then falls back to this.
16pub const KARG_V2: &str = "composefs";
17
18/// Self-describing kernel argument: `composefs.digest=<version>-<hash>-<lg>:<hex>`.
19///
20/// The value encodes the EROFS format version, hash algorithm, and block size,
21/// e.g. `composefs.digest=v1-sha256-12:<hex>` or `composefs.digest=v2-sha512-12:<hex>`.
22/// Both `v1` and `v2` are accepted; `composefs=<hex>` is a legacy alias for
23/// the `v2` form.
24///
25/// Multiple entries may appear on the cmdline with different format/algorithm
26/// combinations; the initramfs tries each in order, mounting the first image
27/// that exists in the repository.
28pub const KARG_COMPOSEFS_DIGEST: &str = "composefs.digest";
29
30/// A composefs kernel argument identifying which EROFS image to mount at boot.
31///
32/// Two variants exist to distinguish EROFS format versions:
33/// - [`ComposefsCmdline::V2`]: V2 EROFS — either `composefs=<digest>` (legacy shorthand)
34///   or `composefs.digest=v2-<hash>-<lg>:<digest>` (explicit form)
35/// - [`ComposefsCmdline::V1`]: V1 EROFS — `composefs.digest=v1-<hash>-<lg>:<digest>`
36///
37/// The initramfs checks for `composefs.digest=` first (accepting both `v1` and `v2`
38/// descriptors), then falls back to the legacy `composefs=` shorthand.
39/// Multiple `composefs.digest=` entries may appear on the cmdline (different
40/// format/algorithm combinations); the initramfs tries each in order, mounting
41/// the first image that exists.
42///
43/// NOTE: The equivalent parsing logic in bootc's `crates/initramfs/src/lib.rs` must be
44/// kept in sync with this file manually, since bootc does not yet depend on composefs-boot
45/// directly.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum ComposefsCmdline<ObjectID: FsVerityHashValue> {
48    /// V2 EROFS image: embedded as `composefs=<hex-digest>` in the UKI cmdline.
49    ///
50    /// The `insecure` flag, when `true`, means the digest is prefixed with `?`
51    /// (e.g. `composefs=?<hex>`), making fs-verity verification optional.
52    V2 {
53        /// The fs-verity hash of the EROFS image.
54        digest: ObjectID,
55        /// If `true`, a `?` prefix is added to the digest, making fs-verity
56        /// verification optional at boot.
57        insecure: bool,
58    },
59    /// V1 EROFS image: embedded as `composefs.digest=v1-<hash>-<lg>:<hex-digest>` in the UKI cmdline.
60    ///
61    /// The value encodes the algorithm, e.g. `composefs.digest=v1-sha256-12:<hex>`
62    /// or `composefs.digest=v1-sha512-12:<hex>`.
63    ///
64    /// The `insecure` flag, when `true`, means the value is prefixed with `?`
65    /// (e.g. `composefs.digest=?v1-sha512-12:<hex>`), making fs-verity verification optional.
66    V1 {
67        /// The fs-verity hash of the EROFS image.
68        digest: ObjectID,
69        /// If `true`, a `?` prefix is added before the format descriptor in the value,
70        /// making fs-verity verification optional at boot.
71        insecure: bool,
72    },
73}
74
75impl<ObjectID: FsVerityHashValue> ComposefsCmdline<ObjectID> {
76    /// Returns a reference to the hex digest, regardless of variant.
77    ///
78    /// Useful for looking up the image in `composefs/images/<digest>`.
79    pub fn digest(&self) -> &ObjectID {
80        match self {
81            ComposefsCmdline::V2 { digest, .. } | ComposefsCmdline::V1 { digest, .. } => digest,
82        }
83    }
84
85    /// Validates that this UKI cmdline's digest matches one of the acceptable
86    /// boot image digests.
87    ///
88    /// With dual V1+V2 EROFS, a single composefs image is stored as two boot
89    /// EROFS serializations with distinct digests; a UKI is sealed carrying
90    /// exactly one of them. This accepts the UKI if its digest matches ANY of
91    /// `acceptable`. Returns the matched (UKI's own) digest on success.
92    pub fn validate_digest<'a>(
93        &self,
94        acceptable: impl IntoIterator<Item = &'a ObjectID>,
95    ) -> Result<&ObjectID>
96    where
97        ObjectID: 'a,
98    {
99        let acceptable: Vec<&ObjectID> = acceptable.into_iter().collect();
100        let uki_digest = self.digest();
101        if acceptable.contains(&uki_digest) {
102            return Ok(uki_digest);
103        }
104        let expected = acceptable
105            .iter()
106            .map(|id| format!("{id:?}"))
107            .collect::<Vec<_>>()
108            .join(", ");
109        anyhow::bail!(
110            "The UKI has the wrong composefs digest (is '{uki_digest:?}', should be one of [{expected}])"
111        )
112    }
113
114    /// Returns whether this karg is in insecure mode (fs-verity verification skipped).
115    pub fn is_insecure(&self) -> bool {
116        match self {
117            ComposefsCmdline::V1 { insecure, .. } | ComposefsCmdline::V2 { insecure, .. } => {
118                *insecure
119            }
120        }
121    }
122
123    /// Constructs a V2 cmdline value (`composefs=<hex>`).
124    pub fn new_v2(digest: ObjectID, insecure: bool) -> Self {
125        ComposefsCmdline::V2 { digest, insecure }
126    }
127
128    /// Constructs a V1 cmdline value (`composefs.digest=v1-<hash>-<lg>:<hex>`).
129    pub fn new_v1(digest: ObjectID, insecure: bool) -> Self {
130        ComposefsCmdline::V1 { digest, insecure }
131    }
132
133    /// Parses a [`ComposefsCmdline`] from a kernel command line string.
134    ///
135    /// Scans for `composefs.digest=` tokens first (→ [`ComposefsCmdline::V1`]).  Multiple
136    /// such tokens may appear on the cmdline (different algorithms); the first one whose
137    /// format descriptor matches the `ObjectID` algorithm is returned.  Then falls back to
138    /// `composefs=` (→ [`ComposefsCmdline::V2`]).  Returns `None` if no matching token is
139    /// present.
140    ///
141    /// # Errors
142    ///
143    /// Returns an error if a matching karg is found but the hex digest cannot be parsed
144    /// for the given `ObjectID` type.
145    pub fn from_cmdline(cmdline: &str) -> Result<Option<Self>> {
146        let expected_hex_len = size_of::<ObjectID>() * 2;
147
148        // V1: composefs.digest=v1-<hash>-<lg>:<hex>
149        // Optional '?' insecure marker directly after '=': composefs.digest=?v1-sha256-12:<hex>
150        // There may be multiple composefs.digest= tokens with different algorithms; find the
151        // first one whose format descriptor matches this ObjectID type.
152        let v1_key_prefix = format!("{KARG_COMPOSEFS_DIGEST}=");
153        for token in split_cmdline(cmdline) {
154            let Some(val) = token.strip_prefix(&v1_key_prefix) else {
155                continue;
156            };
157            let (val_no_q, insecure) = if let Some(s) = val.strip_prefix('?') {
158                (s, true)
159            } else {
160                (val, false)
161            };
162            let (desc, hex) = parse_digest_value(val_no_q)
163                .with_context(|| format!("parsing {KARG_COMPOSEFS_DIGEST}= value: {val}"))?;
164            if !desc.algorithm.is_compatible::<ObjectID>() {
165                // Different algorithm (e.g. sha512 when we're sha256) — skip.
166                continue;
167            }
168            let digest = ObjectID::from_hex(hex).with_context(|| {
169                format!(
170                    "parsing {KARG_COMPOSEFS_DIGEST}= hash: got {} hex chars, expected {} for {}",
171                    hex.len(),
172                    expected_hex_len,
173                    ObjectID::ALGORITHM,
174                )
175            })?;
176            return Ok(Some(match desc.version {
177                1 => ComposefsCmdline::V1 { digest, insecure },
178                _ => ComposefsCmdline::V2 { digest, insecure },
179            }));
180        }
181
182        // V2: composefs=<hex>  (optional '?' prefix for insecure mode)
183        if let Some(val) = get_cmdline_value(cmdline, &format!("{KARG_V2}=")) {
184            let (hex, insecure) = if let Some(stripped) = val.strip_prefix('?') {
185                (stripped, true)
186            } else {
187                (val, false)
188            };
189            let digest = ObjectID::from_hex(hex).with_context(|| {
190                format!(
191                    "parsing {KARG_V2}= hash: got {} hex chars, expected {} for {}",
192                    hex.len(),
193                    expected_hex_len,
194                    ObjectID::ALGORITHM,
195                )
196            })?;
197            return Ok(Some(ComposefsCmdline::V2 { digest, insecure }));
198        }
199
200        Ok(None)
201    }
202
203    /// Renders this value as a kernel command line fragment.
204    ///
205    /// - [`ComposefsCmdline::V1`] (secure)   → `"composefs.digest=v1-<hash>-<lg>:<hex>"`
206    /// - [`ComposefsCmdline::V1`] (insecure) → `"composefs.digest=?v1-<hash>-<lg>:<hex>"`
207    /// - [`ComposefsCmdline::V2`] (secure)   → `"composefs=<hex>"`
208    /// - [`ComposefsCmdline::V2`] (insecure) → `"composefs=?<hex>"`
209    pub fn to_cmdline_arg(&self) -> String {
210        let verity_suffix = ObjectID::ALGORITHM.verity_suffix();
211        match self {
212            ComposefsCmdline::V1 {
213                digest,
214                insecure: false,
215            } => format!(
216                "{KARG_COMPOSEFS_DIGEST}=v1-{verity_suffix}:{}",
217                digest.to_hex()
218            ),
219            ComposefsCmdline::V1 {
220                digest,
221                insecure: true,
222            } => format!(
223                "{KARG_COMPOSEFS_DIGEST}=?v1-{verity_suffix}:{}",
224                digest.to_hex()
225            ),
226            ComposefsCmdline::V2 {
227                digest,
228                insecure: false,
229            } => {
230                format!("{KARG_V2}={}", digest.to_hex())
231            }
232            ComposefsCmdline::V2 {
233                digest,
234                insecure: true,
235            } => {
236                format!("{KARG_V2}=?{}", digest.to_hex())
237            }
238        }
239    }
240}
241
242/// Perform kernel command line splitting.
243///
244/// The way this works in the kernel is to split on whitespace with an extremely simple quoting
245/// mechanism: whitespace inside of double quotes is literal, but there is no escaping mechanism.
246/// That means that having a literal double quote in the cmdline is effectively impossible.
247pub fn split_cmdline(cmdline: &str) -> impl Iterator<Item = &str> {
248    let mut in_quotes = false;
249
250    cmdline.split(move |c: char| {
251        if c == '"' {
252            in_quotes = !in_quotes;
253        }
254        !in_quotes && c.is_ascii_whitespace()
255    })
256}
257
258/// Gets the value of an entry from the kernel cmdline.
259///
260/// The prefix should be something like "composefs=".
261///
262/// This iterates the entries in the provided cmdline string searching for an entry that starts
263/// with the provided prefix.  This will successfully handle quoting of other items in the cmdline,
264/// but the value of the searched entry is returned verbatim (ie: not dequoted).
265pub fn get_cmdline_value<'a>(cmdline: &'a str, prefix: &str) -> Option<&'a str> {
266    split_cmdline(cmdline).find_map(|item| item.strip_prefix(prefix))
267}
268
269/// Parsed format descriptor from a `composefs.digest=` value.
270#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub struct DigestDescriptor {
272    /// The EROFS format version (`v1` or `v2`).
273    pub version: u32,
274    /// The fs-verity algorithm (hash + blocksize).
275    pub algorithm: Algorithm,
276}
277
278/// Parse a `composefs.digest=` value like `v1-sha256-12:<hex>` or `v2-sha512-12:<hex>`.
279///
280/// Returns `(descriptor, hex_digest)`.  Errors on malformed or unsupported
281/// format descriptors (unknown version, bad hash name, unsupported blocksize).
282pub fn parse_digest_value(s: &str) -> Result<(DigestDescriptor, &str)> {
283    // Split "v1-sha256-12:<hex>" into descriptor "v1-sha256-12" and hex.
284    let (descriptor, hex) = s
285        .split_once(':')
286        .with_context(|| format!("expected '<version>-<hash>-<blocksize>:<hex>', got: {s}"))?;
287
288    // Split "v1-sha256-12" → version "v1", remainder "sha256-12".
289    let (version_str, hash_and_bs) = descriptor
290        .split_once('-')
291        .with_context(|| format!("expected 'v<N>-<hash>-<blocksize>', got: {descriptor}"))?;
292
293    let version = match version_str {
294        "v1" => 1,
295        "v2" => 2,
296        _ => anyhow::bail!("unsupported format version '{version_str}'"),
297    };
298
299    // Reuse Algorithm's parser by prepending the expected "fsverity-" prefix.
300    let algorithm: Algorithm = format!("fsverity-{hash_and_bs}")
301        .parse()
302        .with_context(|| format!("parsing algorithm from '{hash_and_bs}'"))?;
303
304    Ok((DigestDescriptor { version, algorithm }, hex))
305}
306
307/// Creates a composefs kernel command line argument string.
308///
309/// # Arguments
310///
311/// * `id` - The composefs object ID as a hex string
312/// * `insecure` - If true, prepends '?' to make fs-verity verification optional
313/// * `version` - Which EROFS format version karg to emit
314/// * `algorithm` - The fs-verity algorithm (used to build the V1 value prefix)
315///
316/// # Returns
317///
318/// A string like `"composefs.digest=v1-sha512-12:abc123"` (V1) or `"composefs=abc123"` (V2),
319/// with optional `?` insecure marker for V1 (`composefs.digest=?v1-sha512-12:abc123`).
320pub fn make_cmdline_composefs(
321    id: &str,
322    insecure: bool,
323    version: composefs::erofs::format::FormatVersion,
324    algorithm: composefs::fsverity::Algorithm,
325) -> String {
326    use composefs::erofs::format::FormatVersion;
327    match version {
328        // V0 and V1 both use the C-compatible compact-inode layout; same karg key.
329        FormatVersion::V0 | FormatVersion::V1 => {
330            let fmt_desc = format!("v1-{}", algorithm.verity_suffix());
331            if insecure {
332                format!("{KARG_COMPOSEFS_DIGEST}=?{fmt_desc}:{id}")
333            } else {
334                format!("{KARG_COMPOSEFS_DIGEST}={fmt_desc}:{id}")
335            }
336        }
337        FormatVersion::V2 => {
338            if insecure {
339                format!("{KARG_V2}=?{id}")
340            } else {
341                format!("{KARG_V2}={id}")
342            }
343        }
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use composefs::fsverity::{Algorithm, Sha256HashValue, Sha512HashValue};
350
351    use super::*;
352
353    const SHA256_HEX: &str = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
354    const SHA512_HEX: &str = "6f06b5e82420abec546d6e6d3ddd612c50cfa9b707c129345b7ec16f456b92fe\
355        35df68999b042e1a6a70dfe75f2fed8cf9f67afd0bf08d2374678d75e2f65a02";
356
357    #[test]
358    fn test_composefs_cmdline_v2_round_trip() {
359        let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
360        let karg = ComposefsCmdline::new_v2(digest.clone(), false);
361        assert_eq!(karg.to_cmdline_arg(), format!("composefs={SHA256_HEX}"));
362
363        let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
364            .unwrap()
365            .unwrap();
366        assert_eq!(
367            parsed,
368            ComposefsCmdline::V2 {
369                digest,
370                insecure: false
371            }
372        );
373    }
374
375    #[test]
376    fn test_composefs_cmdline_v2_insecure_round_trip() {
377        let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
378        let karg = ComposefsCmdline::new_v2(digest.clone(), true);
379        assert_eq!(karg.to_cmdline_arg(), format!("composefs=?{SHA256_HEX}"));
380
381        let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
382            .unwrap()
383            .unwrap();
384        assert_eq!(
385            parsed,
386            ComposefsCmdline::V2 {
387                digest,
388                insecure: true
389            }
390        );
391    }
392
393    #[test]
394    fn test_composefs_cmdline_v1_round_trip_sha256() {
395        let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
396        let karg = ComposefsCmdline::new_v1(digest.clone(), false);
397        assert_eq!(
398            karg.to_cmdline_arg(),
399            format!("composefs.digest=v1-sha256-12:{SHA256_HEX}")
400        );
401
402        let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
403            .unwrap()
404            .unwrap();
405        assert_eq!(
406            parsed,
407            ComposefsCmdline::V1 {
408                digest,
409                insecure: false
410            }
411        );
412    }
413
414    #[test]
415    fn test_composefs_cmdline_v1_round_trip_sha512() {
416        let digest = Sha512HashValue::from_hex(SHA512_HEX).unwrap();
417        let karg = ComposefsCmdline::new_v1(digest.clone(), false);
418        assert_eq!(
419            karg.to_cmdline_arg(),
420            format!("composefs.digest=v1-sha512-12:{SHA512_HEX}")
421        );
422
423        let parsed = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&karg.to_cmdline_arg())
424            .unwrap()
425            .unwrap();
426        assert_eq!(
427            parsed,
428            ComposefsCmdline::V1 {
429                digest,
430                insecure: false
431            }
432        );
433    }
434
435    #[test]
436    fn test_composefs_cmdline_v1_insecure_round_trip() {
437        let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
438        let karg = ComposefsCmdline::new_v1(digest.clone(), true);
439        assert_eq!(
440            karg.to_cmdline_arg(),
441            format!("composefs.digest=?v1-sha256-12:{SHA256_HEX}")
442        );
443
444        let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
445            .unwrap()
446            .unwrap();
447        assert_eq!(
448            parsed,
449            ComposefsCmdline::V1 {
450                digest,
451                insecure: true
452            }
453        );
454        assert!(parsed.is_insecure());
455    }
456
457    #[test]
458    fn test_composefs_cmdline_v1_takes_priority_over_v2() {
459        // When both kargs are present, V1 (composefs.digest=) should win.
460        let hex_v1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
461        let hex_v2 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
462        let cmdline = format!("composefs={hex_v2} composefs.digest=v1-sha256-12:{hex_v1}");
463
464        let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
465            .unwrap()
466            .unwrap();
467        assert!(
468            matches!(&parsed, ComposefsCmdline::V1 { digest, .. } if digest.to_hex() == hex_v1),
469            "expected V1 variant with hex_v1, got {parsed:?}"
470        );
471    }
472
473    #[test]
474    fn test_composefs_cmdline_v1_cross_type_rejection() {
475        // A sha512 V1 karg should NOT be parsed by the sha256 variant (returns None).
476        let cmdline = format!("composefs.digest=v1-sha512-12:{SHA512_HEX}");
477        let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline).unwrap();
478        assert!(
479            result.is_none(),
480            "sha256 parser should not match sha512 karg, got {result:?}"
481        );
482
483        // And vice versa: sha256 V1 karg should not be parsed by the sha512 variant.
484        let cmdline256 = format!("composefs.digest=v1-sha256-12:{SHA256_HEX}");
485        let result512 = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline256).unwrap();
486        assert!(
487            result512.is_none(),
488            "sha512 parser should not match sha256 karg, got {result512:?}"
489        );
490    }
491
492    #[test]
493    fn test_composefs_cmdline_absent_returns_none() {
494        assert!(
495            ComposefsCmdline::<Sha256HashValue>::from_cmdline("quiet splash rw")
496                .unwrap()
497                .is_none()
498        );
499        assert!(
500            ComposefsCmdline::<Sha256HashValue>::from_cmdline("")
501                .unwrap()
502                .is_none()
503        );
504    }
505
506    #[test]
507    fn test_composefs_cmdline_invalid_hex_errors() {
508        // Valid key present but digest is garbage.
509        let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(
510            "composefs.digest=v1-sha256-12:notahex",
511        )
512        .unwrap_err();
513        assert!(err.to_string().contains("composefs.digest="));
514
515        let err =
516            ComposefsCmdline::<Sha256HashValue>::from_cmdline("composefs=notahex").unwrap_err();
517        assert!(err.to_string().contains("composefs="));
518    }
519
520    #[test]
521    fn test_composefs_cmdline_unsupported_blocksize_errors() {
522        // Right hash, wrong blocksize → error (not silently skipped)
523        let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&format!(
524            "composefs.digest=v1-sha256-8:{SHA256_HEX}"
525        ))
526        .unwrap_err();
527        // The root cause is AlgorithmParseError::UnsupportedBlockSize, wrapped by anyhow context.
528        let chain = format!("{err:#}");
529        assert!(
530            chain.contains("unsupported"),
531            "expected 'unsupported' in error chain, got: {chain}"
532        );
533
534        // Right hash (sha512), wrong blocksize
535        let err = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&format!(
536            "composefs.digest=v1-sha512-99:{SHA512_HEX}"
537        ))
538        .unwrap_err();
539        let chain = format!("{err:#}");
540        assert!(
541            chain.contains("unsupported"),
542            "expected 'unsupported' in error chain, got: {chain}"
543        );
544
545        // Unknown version → error
546        let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&format!(
547            "composefs.digest=v3-sha256-12:{SHA256_HEX}"
548        ))
549        .unwrap_err();
550        let chain = format!("{err:#}");
551        assert!(
552            chain.contains("unsupported format version"),
553            "expected version error, got: {chain}"
554        );
555    }
556
557    #[test]
558    fn test_composefs_digest_v2_parsed_as_v2() {
559        // composefs.digest=v2-sha256-12:<hex> should parse as V2, same as composefs=<hex>
560        let cmdline = format!("composefs.digest=v2-sha256-12:{SHA256_HEX}");
561        let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
562            .unwrap()
563            .unwrap();
564        assert!(
565            matches!(parsed, ComposefsCmdline::V2 { .. }),
566            "expected V2 variant, got {parsed:?}"
567        );
568        assert_eq!(parsed.digest().to_hex(), SHA256_HEX);
569        assert!(!parsed.is_insecure());
570
571        // Insecure variant
572        let cmdline = format!("composefs.digest=?v2-sha512-12:{SHA512_HEX}");
573        let parsed = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
574            .unwrap()
575            .unwrap();
576        assert!(matches!(
577            parsed,
578            ComposefsCmdline::V2 { insecure: true, .. }
579        ));
580    }
581
582    #[test]
583    fn test_digest_accessor() {
584        let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
585        let v1 = ComposefsCmdline::new_v1(digest.clone(), false);
586        let v2 = ComposefsCmdline::new_v2(digest.clone(), false);
587        assert_eq!(v1.digest(), &digest);
588        assert_eq!(v2.digest(), &digest);
589    }
590
591    #[test]
592    fn test_from_cmdline_v1() {
593        let cmdline = format!("root=UUID=abc composefs.digest=v1-sha256-12:{SHA256_HEX} rw");
594        let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
595            .unwrap()
596            .unwrap();
597        assert!(matches!(result, ComposefsCmdline::V1 { .. }));
598        assert_eq!(result.digest().to_hex(), SHA256_HEX);
599        assert!(!result.is_insecure());
600    }
601
602    #[test]
603    fn test_from_cmdline_v2_fallback() {
604        let cmdline = format!("root=UUID=abc composefs={SHA256_HEX} rw");
605        let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
606            .unwrap()
607            .unwrap();
608        assert!(matches!(result, ComposefsCmdline::V2 { .. }));
609        assert_eq!(result.digest().to_hex(), SHA256_HEX);
610        assert!(!result.is_insecure());
611    }
612
613    #[test]
614    fn test_from_cmdline_missing_returns_none() {
615        let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline("root=UUID=abc rw").unwrap();
616        assert!(result.is_none());
617    }
618
619    #[test]
620    fn test_from_cmdline_insecure_prefix() {
621        let cmdline = format!("composefs=?{SHA256_HEX}");
622        let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
623            .unwrap()
624            .unwrap();
625        assert!(result.is_insecure());
626        assert_eq!(result.digest().to_hex(), SHA256_HEX);
627    }
628
629    #[test]
630    fn test_make_cmdline_composefs_v1() {
631        use composefs::erofs::format::FormatVersion;
632        let result =
633            make_cmdline_composefs(SHA256_HEX, false, FormatVersion::V1, Algorithm::SHA256);
634        assert_eq!(
635            result,
636            format!("composefs.digest=v1-sha256-12:{SHA256_HEX}")
637        );
638    }
639
640    #[test]
641    fn test_make_cmdline_composefs_v1_sha512() {
642        use composefs::erofs::format::FormatVersion;
643        let result =
644            make_cmdline_composefs(SHA512_HEX, false, FormatVersion::V1, Algorithm::SHA512);
645        assert_eq!(
646            result,
647            format!("composefs.digest=v1-sha512-12:{SHA512_HEX}")
648        );
649    }
650
651    #[test]
652    fn test_validate_digest() {
653        let v1_digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
654        let other = Sha256HashValue::from_hex(
655            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
656        )
657        .unwrap();
658        let karg = ComposefsCmdline::new_v2(v1_digest.clone(), false);
659
660        // Digest present in a multi-element acceptable set → Ok with the match.
661        let acceptable = [&other, &v1_digest];
662        let matched = karg.validate_digest(acceptable.iter().copied()).unwrap();
663        assert_eq!(matched, &v1_digest);
664
665        // Digest absent → Err mentioning "should be one of".
666        let err = karg.validate_digest(std::iter::once(&other)).unwrap_err();
667        assert!(
668            err.to_string().contains("should be one of"),
669            "unexpected error message: {err}"
670        );
671    }
672
673    #[test]
674    fn test_make_cmdline_composefs_v2_insecure() {
675        use composefs::erofs::format::FormatVersion;
676        let result = make_cmdline_composefs(SHA256_HEX, true, FormatVersion::V2, Algorithm::SHA256);
677        assert_eq!(result, format!("composefs=?{SHA256_HEX}"));
678    }
679}