From c3127484f4078a957f709cfd66808ca4281f62e1 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Wed, 8 Jul 2026 15:42:08 -0400 Subject: [PATCH] Replace local bootc-kernel-cmdline crate with linux-kernel-cmdline The kernel command line parsing crate has been split out into a separate repository and published to crates.io as linux-kernel-cmdline. Replace the local copy with the published version 0.1.1. The two bootc-specific constants (INITRD_ARG_PREFIX and ROOTFLAGS) that were defined in the local crate's lib.rs are not part of the published crate. These already had local definitions in bootc_kargs.rs, so update install.rs to use those instead. Signed-off-by: John Eckersberg --- Cargo.lock | 26 +- Cargo.toml | 1 + crates/initramfs/Cargo.toml | 2 +- crates/initramfs/src/lib.rs | 2 +- crates/kernel_cmdline/Cargo.toml | 19 - crates/kernel_cmdline/src/bytes.rs | 1129 ----------------- crates/kernel_cmdline/src/lib.rs | 32 - crates/kernel_cmdline/src/utf8.rs | 953 -------------- crates/lib/Cargo.toml | 2 +- crates/lib/src/bootc_composefs/boot.rs | 2 +- crates/lib/src/bootc_composefs/soft_reboot.rs | 2 +- crates/lib/src/bootc_composefs/state.rs | 2 +- crates/lib/src/bootc_composefs/status.rs | 2 +- crates/lib/src/bootc_composefs/utils.rs | 2 +- crates/lib/src/bootc_kargs.rs | 2 +- crates/lib/src/deploy.rs | 2 +- crates/lib/src/install.rs | 7 +- crates/lib/src/install/baseline.rs | 2 +- crates/lib/src/kernel.rs | 2 +- crates/lib/src/lib.rs | 2 +- crates/lib/src/loader_entries.rs | 2 +- crates/lib/src/parsers/bls_config.rs | 2 +- crates/lib/src/ukify.rs | 2 +- crates/tests-integration/Cargo.toml | 2 +- 24 files changed, 35 insertions(+), 2166 deletions(-) delete mode 100644 crates/kernel_cmdline/Cargo.toml delete mode 100644 crates/kernel_cmdline/src/bytes.rs delete mode 100644 crates/kernel_cmdline/src/lib.rs delete mode 100644 crates/kernel_cmdline/src/utf8.rs diff --git a/Cargo.lock b/Cargo.lock index 1741e7d90..5060b8bc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,12 +272,12 @@ name = "bootc-initramfs-setup" version = "0.1.0" dependencies = [ "anyhow", - "bootc-kernel-cmdline", "cap-std-ext 5.1.2", "clap", "composefs-ctl", "fn-error-context", "libc", + "linux-kernel-cmdline", "rustix", "serde", "toml 1.1.2+spec-1.1.0", @@ -343,16 +343,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "bootc-kernel-cmdline" -version = "0.0.0" -dependencies = [ - "anyhow", - "serde", - "similar-asserts", - "static_assertions", -] - [[package]] name = "bootc-lib" version = "1.16.3" @@ -364,7 +354,6 @@ dependencies = [ "bootc-internal-blockdev", "bootc-internal-mount", "bootc-internal-utils", - "bootc-kernel-cmdline", "bootc-sysusers", "bootc-tmpfiles", "camino", @@ -390,6 +379,7 @@ dependencies = [ "liboverdrop", "libsystemd", "linkme", + "linux-kernel-cmdline", "nom", "ocidir", "openssl", @@ -2512,6 +2502,16 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "linux-kernel-cmdline" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d18193117ee846e8400539a7e565ce7382bcf5913dff47395c1094bd1d918d04" +dependencies = [ + "anyhow", + "serde", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4008,7 +4008,6 @@ version = "0.1.0" dependencies = [ "anyhow", "bcvk-qemu", - "bootc-kernel-cmdline", "camino", "cap-std-ext 5.1.2", "clap", @@ -4017,6 +4016,7 @@ dependencies = [ "indicatif 0.18.4", "indoc", "libtest-mimic", + "linux-kernel-cmdline", "oci-spec 0.10.0", "rand 0.10.1", "rexpect", diff --git a/Cargo.toml b/Cargo.toml index 2f0dbc70e..439588072 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,6 +75,7 @@ tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-journald = "0.3.1" uzers = "0.12" +linux-kernel-cmdline = { version = "0.1.1", features = ["serde"] } xshell = "0.2.6" # See https://github.com/coreos/cargo-vendor-filterer diff --git a/crates/initramfs/Cargo.toml b/crates/initramfs/Cargo.toml index b0e237507..a801b4363 100644 --- a/crates/initramfs/Cargo.toml +++ b/crates/initramfs/Cargo.toml @@ -16,7 +16,7 @@ composefs-ctl.workspace = true toml.workspace = true tracing.workspace = true fn-error-context.workspace = true -bootc-kernel-cmdline = { path = "../kernel_cmdline", version = "0.0.0" } +linux-kernel-cmdline = { workspace = true } [lints] workspace = true diff --git a/crates/initramfs/src/lib.rs b/crates/initramfs/src/lib.rs index c734f547c..079256529 100644 --- a/crates/initramfs/src/lib.rs +++ b/crates/initramfs/src/lib.rs @@ -34,7 +34,7 @@ use composefs_ctl::composefs_boot; use fn_error_context::context; -use bootc_kernel_cmdline::utf8::Cmdline; +use linux_kernel_cmdline::utf8::Cmdline; // mount_setattr syscall support const MOUNT_ATTR_RDONLY: u64 = 0x00000001; diff --git a/crates/kernel_cmdline/Cargo.toml b/crates/kernel_cmdline/Cargo.toml deleted file mode 100644 index 329327b28..000000000 --- a/crates/kernel_cmdline/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "bootc-kernel-cmdline" -description = "Kernel command line parsing utilities for bootc" -version = "0.0.0" -edition = "2024" -license = "MIT OR Apache-2.0" -repository = "https://github.com/bootc-dev/bootc" - -[dependencies] -# Workspace dependencies -anyhow = { workspace = true } -serde = { workspace = true, features = ["derive"] } - -[dev-dependencies] -similar-asserts = { workspace = true } -static_assertions = { workspace = true } - -[lints] -workspace = true diff --git a/crates/kernel_cmdline/src/bytes.rs b/crates/kernel_cmdline/src/bytes.rs deleted file mode 100644 index 807b72d1c..000000000 --- a/crates/kernel_cmdline/src/bytes.rs +++ /dev/null @@ -1,1129 +0,0 @@ -//! Byte-based kernel command line parsing utilities. -//! -//! This module provides functionality for parsing and working with kernel command line -//! arguments, supporting both key-only switches and key-value pairs with proper quote handling. - -use std::borrow::Cow; -use std::cmp::Ordering; -use std::ops::Deref; - -use crate::{Action, utf8}; - -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -/// A parsed kernel command line. -/// -/// Wraps the raw command line bytes and provides methods for parsing and iterating -/// over individual parameters. Uses copy-on-write semantics to avoid unnecessary -/// allocations when working with borrowed data. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct Cmdline<'a>(Cow<'a, [u8]>); - -/// An owned Cmdline. Alias for `Cmdline<'static>`. -pub type CmdlineOwned = Cmdline<'static>; - -impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for Cmdline<'a> { - /// Creates a new `Cmdline` from any type that can be referenced as bytes. - /// - /// Uses borrowed data when possible to avoid unnecessary allocations. - fn from(input: &'a T) -> Self { - Self(Cow::Borrowed(input.as_ref())) - } -} - -impl Deref for Cmdline<'_> { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a, T> AsRef for Cmdline<'a> -where - T: ?Sized, - as Deref>::Target: AsRef, -{ - fn as_ref(&self) -> &T { - self.deref().as_ref() - } -} - -impl From> for CmdlineOwned { - /// Creates a new `Cmdline` from an owned `Vec`. - fn from(input: Vec) -> Self { - Self(Cow::Owned(input)) - } -} - -/// An iterator over kernel command line parameters. -/// -/// This is created by the `iter` method on `Cmdline`. -#[derive(Debug)] -pub struct CmdlineIter<'a>(CmdlineIterBytes<'a>); - -impl<'a> Iterator for CmdlineIter<'a> { - type Item = Parameter<'a>; - - fn next(&mut self) -> Option { - self.0.next().and_then(Parameter::parse_internal) - } -} - -/// An iterator over kernel command line parameters as byte slices. -/// -/// This is created by the `iter_bytes` method on `Cmdline`. -#[derive(Debug)] -pub struct CmdlineIterBytes<'a>(&'a [u8]); - -impl<'a> Iterator for CmdlineIterBytes<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - let input = self.0.trim_ascii_start(); - - if input.is_empty() { - self.0 = input; - return None; - } - - let mut in_quotes = false; - let end = input.iter().position(move |c| { - if *c == b'"' { - in_quotes = !in_quotes; - } - !in_quotes && c.is_ascii_whitespace() - }); - - let end = end.unwrap_or(input.len()); - let (param, rest) = input.split_at(end); - self.0 = rest; - - Some(param) - } -} - -impl<'a> Cmdline<'a> { - /// Creates a new empty owned `Cmdline`. - /// - /// This is equivalent to `Cmdline::default()` but makes ownership explicit. - pub fn new() -> CmdlineOwned { - Cmdline::default() - } - - /// Reads the kernel command line from `/proc/cmdline`. - /// - /// Returns an error if the file cannot be read or if there are I/O issues. - pub fn from_proc() -> Result { - Ok(Self(Cow::Owned(std::fs::read("/proc/cmdline")?))) - } - - /// Returns an iterator over all parameters in the command line. - /// - /// Properly handles quoted values containing whitespace and splits on - /// unquoted whitespace characters. Parameters are parsed as either - /// key-only switches or key=value pairs. - pub fn iter(&'a self) -> CmdlineIter<'a> { - CmdlineIter(self.iter_bytes()) - } - - /// Returns an iterator over all parameters in the command line as byte slices. - /// - /// This is similar to `iter()` but yields `&[u8]` directly instead of `Parameter`, - /// which can be more convenient when you just need the raw byte representation. - pub fn iter_bytes(&self) -> CmdlineIterBytes<'_> { - CmdlineIterBytes(&self.0) - } - - /// Returns an iterator over all parameters in the command line - /// which are valid UTF-8. - pub fn iter_utf8(&'a self) -> impl Iterator> { - self.iter() - .filter_map(|p| utf8::Parameter::try_from(p).ok()) - } - - /// Locate a kernel argument with the given key name. - /// - /// Returns the first parameter matching the given key, or `None` if not found. - /// Key comparison treats dashes and underscores as equivalent. - pub fn find + ?Sized>(&'a self, key: &T) -> Option> { - let key = ParameterKey(key.as_ref()); - self.iter().find(|p| p.key == key) - } - - /// Locate a kernel argument with the given key name. - /// - /// Returns an error if a parameter with the given key name is - /// found, but the value is not valid UTF-8. - /// - /// Otherwise, returns the first parameter matching the given key, - /// or `None` if not found. Key comparison treats dashes and - /// underscores as equivalent. - pub fn find_utf8 + ?Sized>( - &'a self, - key: &T, - ) -> Result>> { - let bytes = match self.find(key.as_ref()) { - Some(p) => p, - None => return Ok(None), - }; - - Ok(Some(utf8::Parameter::try_from(bytes)?)) - } - - /// Find all kernel arguments starting with the given prefix. - /// - /// This is a variant of [`Self::find`]. - pub fn find_all_starting_with + ?Sized>( - &'a self, - prefix: &'a T, - ) -> impl Iterator> + 'a { - self.iter() - .filter(move |p| p.key.0.starts_with(prefix.as_ref())) - } - - /// Locate the value of the kernel argument with the given key name. - /// - /// Returns the first value matching the given key, or `None` if not found. - /// Key comparison treats dashes and underscores as equivalent. - pub fn value_of + ?Sized>(&'a self, key: &T) -> Option<&'a [u8]> { - self.find(&key).and_then(|p| p.value) - } - - /// Find the value of the kernel argument with the provided name, which must be present. - /// - /// Otherwise the same as [`Self::value_of`]. - pub fn require_value_of + ?Sized>(&'a self, key: &T) -> Result<&'a [u8]> { - let key = key.as_ref(); - self.value_of(key).ok_or_else(|| { - let key = String::from_utf8_lossy(key); - anyhow::anyhow!("Failed to find kernel argument '{key}'") - }) - } - - /// Add a parameter to the command line if it doesn't already exist - /// - /// Returns `Action::Added` if the parameter did not already exist - /// and was added. - /// - /// Returns `Action::Existed` if the exact parameter (same key and value) - /// already exists. No modification was made. - /// - /// Unlike `add_or_modify`, this method will not modify existing - /// parameters. If a parameter with the same key exists but has a - /// different value, the new parameter is still added, allowing - /// duplicate keys (e.g., multiple `console=` parameters). - pub fn add(&mut self, param: &Parameter) -> Action { - // Check if the exact parameter already exists - for p in self.iter() { - if p == *param { - // Exact match found, don't add duplicate - return Action::Existed; - } - } - - // The exact parameter was not found, so we append it. - let self_mut = self.0.to_mut(); - if self_mut - .last() - .filter(|v| !v.is_ascii_whitespace()) - .is_some() - { - self_mut.push(b' '); - } - self_mut.extend_from_slice(param.parameter); - Action::Added - } - - /// Add or modify a parameter to the command line - /// - /// Returns `Action::Added` if the parameter did not exist before - /// and was added. - /// - /// Returns `Action::Modified` if the parameter existed before, - /// but contained a different value. The value was updated to the - /// newly-requested value. - /// - /// Returns `Action::Existed` if the parameter existed before, and - /// contained the same value as the newly-requested value. No - /// modification was made. - pub fn add_or_modify(&mut self, param: &Parameter) -> Action { - let mut new_params = Vec::new(); - let mut modified = false; - let mut seen_key = false; - - for p in self.iter() { - if p.key == param.key { - if !seen_key { - // This is the first time we've seen this key. - // We will replace it with the new parameter. - if p != *param { - modified = true; - } - new_params.push(param.parameter); - } else { - // This is a subsequent parameter with the same key. - // We will remove it, which constitutes a modification. - modified = true; - } - seen_key = true; - } else { - new_params.push(p.parameter); - } - } - - if !seen_key { - // The parameter was not found, so we append it. - let self_mut = self.0.to_mut(); - if self_mut - .last() - .filter(|v| !v.is_ascii_whitespace()) - .is_some() - { - self_mut.push(b' '); - } - self_mut.extend_from_slice(param.parameter); - return Action::Added; - } - if modified { - self.0 = Cow::Owned(new_params.join(b" ".as_slice())); - Action::Modified - } else { - // The parameter already existed with the same content, and there were no duplicates. - Action::Existed - } - } - - /// Remove parameter(s) with the given key from the command line - /// - /// Returns `true` if parameter(s) were removed. - pub fn remove(&mut self, key: &ParameterKey) -> bool { - let mut removed = false; - let mut new_params = Vec::new(); - - for p in self.iter() { - if p.key == *key { - removed = true; - } else { - new_params.push(p.parameter); - } - } - - if removed { - self.0 = Cow::Owned(new_params.join(b" ".as_slice())); - } - - removed - } - - /// Remove all parameters that exactly match the given parameter - /// from the command line - /// - /// Returns `true` if parameter(s) were removed. - pub fn remove_exact(&mut self, param: &Parameter) -> bool { - let mut removed = false; - let mut new_params = Vec::new(); - - for p in self.iter() { - if p == *param { - removed = true; - } else { - new_params.push(p.parameter); - } - } - - if removed { - self.0 = Cow::Owned(new_params.join(b" ".as_slice())); - } - - removed - } - - #[cfg(test)] - pub(crate) fn is_owned(&self) -> bool { - matches!(self.0, Cow::Owned(_)) - } - - #[cfg(test)] - pub(crate) fn is_borrowed(&self) -> bool { - matches!(self.0, Cow::Borrowed(_)) - } -} - -impl<'a> IntoIterator for &'a Cmdline<'a> { - type Item = Parameter<'a>; - type IntoIter = CmdlineIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl<'a, 'other> Extend> for Cmdline<'a> { - fn extend>>(&mut self, iter: T) { - // Note this is O(N*M), but in practice this doesn't matter - // because kernel cmdlines are typically quite small (limited - // to at most 4k depending on arch). Using a hash-based - // structure to reduce this to O(N)+C would likely raise the C - // portion so much as to erase any benefit from removing the - // combinatorial complexity. Plus CPUs are good at - // caching/pipelining through contiguous memory. - for param in iter { - self.add(¶m); - } - } -} - -impl PartialEq for Cmdline<'_> { - fn eq(&self, other: &Self) -> bool { - let mut our_params = self.iter().collect::>(); - our_params.sort(); - let mut their_params = other.iter().collect::>(); - their_params.sort(); - - our_params == their_params - } -} - -impl Eq for Cmdline<'_> {} - -/// A single kernel command line parameter key -/// -/// Handles quoted values and treats dashes and underscores in keys as equivalent. -#[derive(Clone, Debug)] -pub struct ParameterKey<'a>(pub(crate) &'a [u8]); - -impl Deref for ParameterKey<'_> { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - self.0 - } -} - -impl<'a, T> AsRef for ParameterKey<'a> -where - T: ?Sized, - as Deref>::Target: AsRef, -{ - fn as_ref(&self) -> &T { - self.deref().as_ref() - } -} - -impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for ParameterKey<'a> { - fn from(s: &'a T) -> Self { - Self(s.as_ref()) - } -} - -impl ParameterKey<'_> { - /// Returns an iterator over the canonicalized bytes of the - /// parameter, with dashes turned into underscores. - fn iter(&self) -> impl Iterator + use<'_> { - self.0 - .iter() - .map(|&c: &u8| if c == b'-' { b'_' } else { c }) - } -} - -impl PartialEq for ParameterKey<'_> { - /// Compares two parameter keys for equality. - /// - /// Keys are compared with dashes and underscores treated as equivalent. - /// This comparison is case-sensitive. - fn eq(&self, other: &Self) -> bool { - self.iter().eq(other.iter()) - } -} - -impl Eq for ParameterKey<'_> {} - -impl Ord for ParameterKey<'_> { - fn cmp(&self, other: &Self) -> Ordering { - self.iter().cmp(other.iter()) - } -} - -impl PartialOrd for ParameterKey<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// A single kernel command line parameter. -#[derive(Clone, Debug)] -pub struct Parameter<'a> { - /// The full original value - parameter: &'a [u8], - /// The parameter key as raw bytes - key: ParameterKey<'a>, - /// The parameter value as raw bytes, if present - value: Option<&'a [u8]>, -} - -impl<'a> Parameter<'a> { - /// Attempt to parse a single command line parameter from a slice - /// of bytes. - /// - /// Returns `Some(Parameter)`, or `None` if a Parameter could not - /// be constructed from the input. This occurs when the input is - /// either empty or contains only whitespace. - /// - /// If the input contains multiple parameters, only the first one - /// is parsed and the rest is discarded. - pub fn parse + ?Sized>(input: &'a T) -> Option { - CmdlineIterBytes(input.as_ref()) - .next() - .and_then(Self::parse_internal) - } - - /// Parse a parameter from a byte slice that contains exactly one parameter. - /// - /// This is an internal method that assumes the input has already been - /// split into a single parameter (e.g., by CmdlineIterBytes). - fn parse_internal(input: &'a [u8]) -> Option { - // *Only* the first and last double quotes are stripped - let dequoted_input = input.strip_prefix(b"\"").unwrap_or(input); - let dequoted_input = dequoted_input.strip_suffix(b"\"").unwrap_or(dequoted_input); - - let equals = dequoted_input.iter().position(|b| *b == b'='); - - match equals { - None => Some(Self { - parameter: input, - key: ParameterKey(dequoted_input), - value: None, - }), - Some(i) => { - let (key, mut value) = dequoted_input.split_at(i); - let key = ParameterKey(key); - - // skip `=`, we know it's the first byte because we - // found it above - value = &value[1..]; - - // If there is a quote after the equals, skip it. If - // there was a closing quote at the end of the value, - // we would have already removed it in - // `dequoted_input` above - value = value.strip_prefix(b"\"").unwrap_or(value); - - Some(Self { - parameter: input, - key, - value: Some(value), - }) - } - } - } - - /// Returns the key part of the parameter - pub fn key(&self) -> ParameterKey<'a> { - self.key.clone() - } - - /// Returns the optional value part of the parameter - pub fn value(&self) -> Option<&'a [u8]> { - self.value - } -} - -impl PartialEq for Parameter<'_> { - fn eq(&self, other: &Self) -> bool { - // Note we don't compare parameter because we want hyphen-dash insensitivity for the key - self.key == other.key && self.value == other.value - } -} - -impl Eq for Parameter<'_> {} - -impl Ord for Parameter<'_> { - fn cmp(&self, other: &Self) -> Ordering { - self.key.cmp(&other.key).then(self.value.cmp(&other.value)) - } -} - -impl PartialOrd for Parameter<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Deref for Parameter<'_> { - type Target = [u8]; - - fn deref(&self) -> &Self::Target { - self.parameter - } -} - -impl<'a, T> AsRef for Parameter<'a> -where - T: ?Sized, - as Deref>::Target: AsRef, -{ - fn as_ref(&self) -> &T { - self.deref().as_ref() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // convenience methods for tests - fn param(s: &str) -> Parameter<'_> { - Parameter::parse(s.as_bytes()).unwrap() - } - - fn param_utf8(s: &str) -> utf8::Parameter<'_> { - utf8::Parameter::parse(s).unwrap() - } - - #[test] - fn test_parameter_parse() { - let p = Parameter::parse(b"foo").unwrap(); - assert_eq!(p.key.0, b"foo"); - assert_eq!(p.value, None); - - // should parse only the first parameter and discard the rest of the input - let p = Parameter::parse(b"foo=bar baz").unwrap(); - assert_eq!(p.key.0, b"foo"); - assert_eq!(p.value, Some(b"bar".as_slice())); - - // should return None on empty or whitespace inputs - assert!(Parameter::parse(b"").is_none()); - assert!(Parameter::parse(b" ").is_none()); - } - - #[test] - fn test_parameter_simple() { - let switch = param("foo"); - assert_eq!(switch.key.0, b"foo"); - assert_eq!(switch.value, None); - - let kv = param("bar=baz"); - assert_eq!(kv.key.0, b"bar"); - assert_eq!(kv.value, Some(b"baz".as_slice())); - } - - #[test] - fn test_parameter_quoted() { - let p = param("foo=\"quoted value\""); - assert_eq!(p.value, Some(b"quoted value".as_slice())); - - let p = param("foo=\"unclosed quotes"); - assert_eq!(p.value, Some(b"unclosed quotes".as_slice())); - - let p = param("foo=trailing_quotes\""); - assert_eq!(p.value, Some(b"trailing_quotes".as_slice())); - - let outside_quoted = param("\"foo=quoted value\""); - let value_quoted = param("foo=\"quoted value\""); - assert_eq!(outside_quoted, value_quoted); - } - - #[test] - fn test_parameter_extra_whitespace() { - let p = param(" foo=bar "); - assert_eq!(p.key.0, b"foo"); - assert_eq!(p.value, Some(b"bar".as_slice())); - } - - #[test] - fn test_parameter_internal_key_whitespace() { - // parse should only consume the first parameter - let p = Parameter::parse("foo bar=baz".as_bytes()).unwrap(); - assert_eq!(p.key.0, b"foo"); - assert_eq!(p.value, None); - } - - #[test] - fn test_parameter_pathological() { - // valid things that certified insane people would do - - // you can quote just the key part in a key-value param, but - // the end quote is actually part of the key as far as the - // kernel is concerned... - let p = param("\"foo\"=bar"); - assert_eq!(p.key.0, b"foo\""); - assert_eq!(p.value, Some(b"bar".as_slice())); - // and it is definitely not equal to an unquoted foo ... - assert_ne!(p, param("foo=bar")); - - // ... but if you close the quote immediately after the - // equals sign, it does get removed. - let p = param("\"foo=\"bar"); - assert_eq!(p.key.0, b"foo"); - assert_eq!(p.value, Some(b"bar".as_slice())); - // ... so of course this makes sense ... - assert_eq!(p, param("foo=bar")); - - // quotes only get stripped from the absolute ends of values - let p = param("foo=\"internal\"quotes\"are\"ok\""); - assert_eq!(p.value, Some(b"internal\"quotes\"are\"ok".as_slice())); - - // non-UTF8 things are in fact valid - let non_utf8_byte = b"\xff"; - #[allow(invalid_from_utf8)] - let failed_conversion = str::from_utf8(non_utf8_byte); - assert!(failed_conversion.is_err()); - let mut p = b"foo=".to_vec(); - p.push(non_utf8_byte[0]); - let p = Parameter::parse(&p).unwrap(); - assert_eq!(p.value, Some(non_utf8_byte.as_slice())); - } - - #[test] - fn test_parameter_equality() { - // substrings are not equal - let foo = param("foo"); - let bar = param("foobar"); - assert_ne!(foo, bar); - assert_ne!(bar, foo); - - // dashes and underscores are treated equally - let dashes = param("a-delimited-param"); - let underscores = param("a_delimited_param"); - assert_eq!(dashes, underscores); - - // same key, same values is equal - let dashes = param("a-delimited-param=same_values"); - let underscores = param("a_delimited_param=same_values"); - assert_eq!(dashes, underscores); - - // same key, different values is not equal - let dashes = param("a-delimited-param=different_values"); - let underscores = param("a_delimited_param=DiFfErEnT_valUEZ"); - assert_ne!(dashes, underscores); - - // mixed variants are never equal - let switch = param("same_key"); - let keyvalue = param("same_key=but_with_a_value"); - assert_ne!(switch, keyvalue); - } - - #[test] - fn test_kargs_simple() { - // example taken lovingly from: - // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/params.c?id=89748acdf226fd1a8775ff6fa2703f8412b286c8#n160 - let kargs = Cmdline::from(b"foo=bar,bar2 baz=fuz wiz".as_slice()); - let mut iter = kargs.iter(); - - assert_eq!(iter.next(), Some(param("foo=bar,bar2"))); - assert_eq!(iter.next(), Some(param("baz=fuz"))); - assert_eq!(iter.next(), Some(param("wiz"))); - assert_eq!(iter.next(), None); - - // Test the find API - assert_eq!(kargs.find("foo").unwrap().value.unwrap(), b"bar,bar2"); - assert!(kargs.find("nothing").is_none()); - } - - #[test] - fn test_cmdline_default() { - let kargs: Cmdline = Default::default(); - assert_eq!(kargs.iter().next(), None); - } - - #[test] - fn test_cmdline_new() { - let kargs = Cmdline::new(); - assert_eq!(kargs.iter().next(), None); - assert!(kargs.is_owned()); - - // Verify we can store it in an owned ('static) context - let _static_kargs: CmdlineOwned = Cmdline::new(); - } - - #[test] - fn test_kargs_iter_utf8() { - let kargs = Cmdline::from(b"foo=bar,bar2 \xff baz=fuz bad=oh\xffno wiz"); - let mut iter = kargs.iter_utf8(); - - assert_eq!(iter.next(), Some(param_utf8("foo=bar,bar2"))); - assert_eq!(iter.next(), Some(param_utf8("baz=fuz"))); - assert_eq!(iter.next(), Some(param_utf8("wiz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_kargs_find_utf8() { - let kargs = Cmdline::from(b"foo=bar,bar2 \xff baz=fuz bad=oh\xffno wiz"); - - // found it - assert_eq!( - kargs.find_utf8("foo").unwrap().unwrap().value().unwrap(), - "bar,bar2" - ); - - // didn't find it - assert!(kargs.find_utf8("nothing").unwrap().is_none()); - - // found it but key is invalid - let p = kargs.find_utf8("bad"); - assert_eq!( - p.unwrap_err().to_string(), - "Parameter value is not valid UTF-8" - ); - } - - #[test] - fn test_kargs_from_proc() { - let kargs = Cmdline::from_proc().unwrap(); - - // Not really a good way to test this other than assume - // there's at least one argument in /proc/cmdline wherever the - // tests are running - assert!(kargs.iter().count() > 0); - } - - #[test] - fn test_kargs_find_dash_hyphen() { - let kargs = Cmdline::from(b"a-b=1 a_b=2".as_slice()); - // find should find the first one, which is a-b=1 - let p = kargs.find("a_b").unwrap(); - assert_eq!(p.key.0, b"a-b"); - assert_eq!(p.value.unwrap(), b"1"); - let p = kargs.find("a-b").unwrap(); - assert_eq!(p.key.0, b"a-b"); - assert_eq!(p.value.unwrap(), b"1"); - - let kargs = Cmdline::from(b"a_b=2 a-b=1".as_slice()); - // find should find the first one, which is a_b=2 - let p = kargs.find("a_b").unwrap(); - assert_eq!(p.key.0, b"a_b"); - assert_eq!(p.value.unwrap(), b"2"); - let p = kargs.find("a-b").unwrap(); - assert_eq!(p.key.0, b"a_b"); - assert_eq!(p.value.unwrap(), b"2"); - } - - #[test] - fn test_kargs_extra_whitespace() { - let kargs = Cmdline::from(b" foo=bar baz=fuz wiz ".as_slice()); - let mut iter = kargs.iter(); - - assert_eq!(iter.next(), Some(param("foo=bar"))); - assert_eq!(iter.next(), Some(param("baz=fuz"))); - assert_eq!(iter.next(), Some(param("wiz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_value_of() { - let kargs = Cmdline::from(b"foo=bar baz=qux switch".as_slice()); - - // Test existing key with value - assert_eq!(kargs.value_of("foo"), Some(b"bar".as_slice())); - assert_eq!(kargs.value_of("baz"), Some(b"qux".as_slice())); - - // Test key without value - assert_eq!(kargs.value_of("switch"), None); - - // Test non-existent key - assert_eq!(kargs.value_of("missing"), None); - - // Test dash/underscore equivalence - let kargs = Cmdline::from(b"dash-key=value1 under_key=value2".as_slice()); - assert_eq!(kargs.value_of("dash_key"), Some(b"value1".as_slice())); - assert_eq!(kargs.value_of("under-key"), Some(b"value2".as_slice())); - } - - #[test] - fn test_require_value_of() { - let kargs = Cmdline::from(b"foo=bar baz=qux switch".as_slice()); - - // Test existing key with value - assert_eq!(kargs.require_value_of("foo").unwrap(), b"bar"); - assert_eq!(kargs.require_value_of("baz").unwrap(), b"qux"); - - // Test key without value should fail - let err = kargs.require_value_of("switch").unwrap_err(); - assert!( - err.to_string() - .contains("Failed to find kernel argument 'switch'") - ); - - // Test non-existent key should fail - let err = kargs.require_value_of("missing").unwrap_err(); - assert!( - err.to_string() - .contains("Failed to find kernel argument 'missing'") - ); - - // Test dash/underscore equivalence - let kargs = Cmdline::from(b"dash-key=value1 under_key=value2".as_slice()); - assert_eq!(kargs.require_value_of("dash_key").unwrap(), b"value1"); - assert_eq!(kargs.require_value_of("under-key").unwrap(), b"value2"); - } - - #[test] - fn test_find_all() { - let kargs = - Cmdline::from(b"foo=bar rd.foo=a rd.bar=b rd.baz rd.qux=c notrd.val=d".as_slice()); - let mut rd_args: Vec<_> = kargs.find_all_starting_with(b"rd.".as_slice()).collect(); - rd_args.sort_by(|a, b| a.key.0.cmp(b.key.0)); - assert_eq!(rd_args.len(), 4); - assert_eq!(rd_args[0], param("rd.bar=b")); - assert_eq!(rd_args[1], param("rd.baz")); - assert_eq!(rd_args[2], param("rd.foo=a")); - assert_eq!(rd_args[3], param("rd.qux=c")); - } - - #[test] - fn test_add() { - let mut kargs = Cmdline::from(b"console=tty0 console=ttyS1"); - - // add new parameter with duplicate key but different value - assert!(matches!(kargs.add(¶m("console=ttyS2")), Action::Added)); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("console=tty0"))); - assert_eq!(iter.next(), Some(param("console=ttyS1"))); - assert_eq!(iter.next(), Some(param("console=ttyS2"))); - assert_eq!(iter.next(), None); - - // try to add exact duplicate - should return Existed - assert!(matches!( - kargs.add(¶m("console=ttyS1")), - Action::Existed - )); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("console=tty0"))); - assert_eq!(iter.next(), Some(param("console=ttyS1"))); - assert_eq!(iter.next(), Some(param("console=ttyS2"))); - assert_eq!(iter.next(), None); - - // add completely new parameter - assert!(matches!(kargs.add(¶m("quiet")), Action::Added)); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("console=tty0"))); - assert_eq!(iter.next(), Some(param("console=ttyS1"))); - assert_eq!(iter.next(), Some(param("console=ttyS2"))); - assert_eq!(iter.next(), Some(param("quiet"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_add_empty_cmdline() { - let mut kargs = Cmdline::from(b""); - assert!(matches!(kargs.add(¶m("foo")), Action::Added)); - assert_eq!(kargs.0, b"foo".as_slice()); - } - - #[test] - fn test_add_or_modify() { - let mut kargs = Cmdline::from(b"foo=bar"); - - // add new - assert!(matches!(kargs.add_or_modify(¶m("baz")), Action::Added)); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=bar"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - - // modify existing - assert!(matches!( - kargs.add_or_modify(¶m("foo=fuz")), - Action::Modified - )); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=fuz"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - - // already exists with same value returns false and doesn't - // modify anything - assert!(matches!( - kargs.add_or_modify(¶m("foo=fuz")), - Action::Existed - )); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=fuz"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_add_or_modify_empty_cmdline() { - let mut kargs = Cmdline::from(b""); - assert!(matches!(kargs.add_or_modify(¶m("foo")), Action::Added)); - assert_eq!(kargs.0, b"foo".as_slice()); - } - - #[test] - fn test_add_or_modify_duplicate_parameters() { - let mut kargs = Cmdline::from(b"a=1 a=2"); - assert!(matches!( - kargs.add_or_modify(¶m("a=3")), - Action::Modified - )); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("a=3"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_remove() { - let mut kargs = Cmdline::from(b"foo bar baz"); - - // remove existing - assert!(kargs.remove(&"bar".into())); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - - // doesn't exist? returns false and doesn't modify anything - assert!(!kargs.remove(&"missing".into())); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_remove_duplicates() { - let mut kargs = Cmdline::from(b"a=1 b=2 a=3"); - assert!(kargs.remove(&"a".into())); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("b=2"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_remove_exact() { - let mut kargs = Cmdline::from(b"foo foo=bar foo=baz"); - - // remove existing - assert!(kargs.remove_exact(¶m("foo=bar"))); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo"))); - assert_eq!(iter.next(), Some(param("foo=baz"))); - assert_eq!(iter.next(), None); - - // doesn't exist? returns false and doesn't modify anything - assert!(!kargs.remove_exact(¶m("foo=wuz"))); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo"))); - assert_eq!(iter.next(), Some(param("foo=baz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_extend() { - let mut kargs = Cmdline::from(b"foo=bar baz"); - let other = Cmdline::from(b"qux=quux foo=updated"); - - kargs.extend(&other); - - // Sanity check that the lifetimes of the two Cmdlines are not - // tied to each other. - drop(other); - - // Should have preserved the original foo, added qux, baz - // unchanged, and added the second (duplicate key) foo - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=bar"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), Some(param("qux=quux"))); - assert_eq!(iter.next(), Some(param("foo=updated"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_extend_empty() { - let mut kargs = Cmdline::from(b""); - let other = Cmdline::from(b"foo=bar baz"); - - kargs.extend(&other); - - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=bar"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_into_iterator() { - let kargs = Cmdline::from(b"foo=bar baz=qux wiz"); - let params: Vec<_> = (&kargs).into_iter().collect(); - - assert_eq!(params.len(), 3); - assert_eq!(params[0], param("foo=bar")); - assert_eq!(params[1], param("baz=qux")); - assert_eq!(params[2], param("wiz")); - } - - #[test] - fn test_iter_bytes_simple() { - let kargs = Cmdline::from(b"foo bar baz"); - let params: Vec<_> = kargs.iter_bytes().collect(); - - assert_eq!(params.len(), 3); - assert_eq!(params[0], b"foo"); - assert_eq!(params[1], b"bar"); - assert_eq!(params[2], b"baz"); - } - - #[test] - fn test_iter_bytes_with_values() { - let kargs = Cmdline::from(b"foo=bar baz=qux wiz"); - let params: Vec<_> = kargs.iter_bytes().collect(); - - assert_eq!(params.len(), 3); - assert_eq!(params[0], b"foo=bar"); - assert_eq!(params[1], b"baz=qux"); - assert_eq!(params[2], b"wiz"); - } - - #[test] - fn test_iter_bytes_with_quotes() { - let kargs = Cmdline::from(b"foo=\"bar baz\" qux"); - let params: Vec<_> = kargs.iter_bytes().collect(); - - assert_eq!(params.len(), 2); - assert_eq!(params[0], b"foo=\"bar baz\""); - assert_eq!(params[1], b"qux"); - } - - #[test] - fn test_iter_bytes_extra_whitespace() { - let kargs = Cmdline::from(b" foo bar "); - let params: Vec<_> = kargs.iter_bytes().collect(); - - assert_eq!(params.len(), 2); - assert_eq!(params[0], b"foo"); - assert_eq!(params[1], b"bar"); - } - - #[test] - fn test_iter_bytes_empty() { - let kargs = Cmdline::from(b""); - let params: Vec<_> = kargs.iter_bytes().collect(); - - assert_eq!(params.len(), 0); - } - - #[test] - fn test_cmdline_eq() { - // Ordering, quoting, and the whole dash-underscore - // equivalence thing shouldn't affect whether these are - // semantically equal - assert_eq!( - Cmdline::from("foo bar-with-delim=\"with spaces\""), - Cmdline::from("\"bar_with_delim=with spaces\" foo") - ); - - // Uneven lengths are not equal even if the parameters are. Or - // to put it another way, duplicate parameters break equality. - // Check with both orderings. - assert_ne!(Cmdline::from("foo"), Cmdline::from("foo foo")); - assert_ne!(Cmdline::from("foo foo"), Cmdline::from("foo")); - - // Equal lengths but differing duplicates are also not equal - assert_ne!(Cmdline::from("a a b"), Cmdline::from("a b b")); - } -} diff --git a/crates/kernel_cmdline/src/lib.rs b/crates/kernel_cmdline/src/lib.rs deleted file mode 100644 index 5f2d85c29..000000000 --- a/crates/kernel_cmdline/src/lib.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Kernel command line parsing utilities. -//! -//! This module provides functionality for parsing and working with kernel command line -//! arguments, supporting both key-only switches and key-value pairs with proper quote handling. -//! -//! The kernel command line is not required to be UTF-8. The `bytes` -//! module works on arbitrary byte data and attempts to parse the -//! command line in the same manner as the kernel itself. -//! -//! The `utf8` module performs the same functionality, but requires -//! all data to be valid UTF-8. - -pub mod bytes; -pub mod utf8; - -/// This is used by dracut. -pub const INITRD_ARG_PREFIX: &str = "rd."; -/// The kernel argument for configuring the rootfs flags. -pub const ROOTFLAGS: &str = "rootflags"; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -/// Possible outcomes for `add_or_modify` operations. -pub enum Action { - /// The parameter did not exist before and was added - Added, - /// The parameter existed before, but contained a different value. - /// The value was updated to the newly-requested value. - Modified, - /// The parameter existed before, and contained the same value as - /// the newly-requested value. No modification was made. - Existed, -} diff --git a/crates/kernel_cmdline/src/utf8.rs b/crates/kernel_cmdline/src/utf8.rs deleted file mode 100644 index 850a4fcd4..000000000 --- a/crates/kernel_cmdline/src/utf8.rs +++ /dev/null @@ -1,953 +0,0 @@ -//! UTF-8-based kernel command line parsing utilities. -//! -//! This module provides functionality for parsing and working with kernel command line -//! arguments, supporting both key-only switches and key-value pairs with proper quote handling. - -use std::ops::Deref; - -use crate::{Action, bytes}; - -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -/// A parsed UTF-8 kernel command line. -/// -/// Wraps the raw command line bytes and provides methods for parsing and iterating -/// over individual parameters. Uses copy-on-write semantics to avoid unnecessary -/// allocations when working with borrowed data. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct Cmdline<'a>(bytes::Cmdline<'a>); - -/// An owned `Cmdline`. Alias for `Cmdline<'static>`. -pub type CmdlineOwned = Cmdline<'static>; - -impl<'a, T: AsRef + ?Sized> From<&'a T> for Cmdline<'a> { - /// Creates a new `Cmdline` from any type that can be referenced as `str`. - /// - /// Uses borrowed data when possible to avoid unnecessary allocations. - fn from(input: &'a T) -> Self { - Self(bytes::Cmdline::from(input.as_ref().as_bytes())) - } -} - -impl From for CmdlineOwned { - /// Creates a new `Cmdline` from a `String`. - /// - /// Takes ownership of input and maintains it for internal owned data. - fn from(input: String) -> Self { - Self(bytes::Cmdline::from(input.into_bytes())) - } -} - -/// An iterator over UTF-8 kernel command line parameters. -/// -/// This is created by the `iter` method on `CmdlineUTF8`. -#[derive(Debug)] -pub struct CmdlineIter<'a>(bytes::CmdlineIter<'a>); - -impl<'a> Iterator for CmdlineIter<'a> { - type Item = Parameter<'a>; - - fn next(&mut self) -> Option { - self.0.next().map(Parameter::from_bytes) - } -} - -/// An iterator over UTF-8 kernel command line parameters as string slices. -/// -/// This is created by the `iter_str` method on `Cmdline`. -#[derive(Debug)] -pub struct CmdlineIterStr<'a>(bytes::CmdlineIterBytes<'a>); - -impl<'a> Iterator for CmdlineIterStr<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - // Get the next byte slice from the underlying iterator - let bytes = self.0.next()?; - - // Convert to UTF-8 string slice - // SAFETY: We know this is valid UTF-8 since the Cmdline was constructed from valid UTF-8 - Some(str::from_utf8(bytes).expect("Parameter bytes come from valid UTF-8 cmdline")) - } -} - -impl<'a> Cmdline<'a> { - /// Creates a new empty owned `Cmdline`. - /// - /// This is equivalent to `Cmdline::default()` but makes ownership explicit. - pub fn new() -> CmdlineOwned { - Cmdline::default() - } - - /// Reads the kernel command line from `/proc/cmdline`. - /// - /// Returns an error if: - /// - The file cannot be read - /// - There are I/O issues - /// - The cmdline from proc is not valid UTF-8 - pub fn from_proc() -> Result { - let cmdline = std::fs::read("/proc/cmdline")?; - - // SAFETY: validate the value from proc is valid UTF-8. We - // don't need to save this, but checking now will ensure we - // can safely convert from the underlying bytes back to UTF-8 - // later. - str::from_utf8(&cmdline)?; - - Ok(Self(bytes::Cmdline::from(cmdline))) - } - - /// Returns an iterator over all parameters in the command line. - /// - /// Properly handles quoted values containing whitespace and splits on - /// unquoted whitespace characters. Parameters are parsed as either - /// key-only switches or key=value pairs. - pub fn iter(&'a self) -> CmdlineIter<'a> { - CmdlineIter(self.0.iter()) - } - - /// Returns an iterator over all parameters in the command line as string slices. - /// - /// This is similar to `iter()` but yields `&str` directly instead of `Parameter`, - /// which can be more convenient when you just need the string representation. - pub fn iter_str(&self) -> CmdlineIterStr<'_> { - CmdlineIterStr(self.0.iter_bytes()) - } - - /// Locate a kernel argument with the given key name. - /// - /// Returns the first parameter matching the given key, or `None` if not found. - /// Key comparison treats dashes and underscores as equivalent. - pub fn find + ?Sized>(&'a self, key: &T) -> Option> { - let key = ParameterKey::from(key.as_ref()); - self.iter().find(|p| p.key() == key) - } - - /// Find all kernel arguments starting with the given UTF-8 prefix. - /// - /// This is a variant of [`Self::find`]. - pub fn find_all_starting_with + ?Sized>( - &'a self, - prefix: &'a T, - ) -> impl Iterator> + 'a { - self.iter() - .filter(move |p| p.key().starts_with(prefix.as_ref())) - } - - /// Locate the value of the kernel argument with the given key name. - /// - /// Returns the first value matching the given key, or `None` if not found. - /// Key comparison treats dashes and underscores as equivalent. - pub fn value_of + ?Sized>(&'a self, key: &T) -> Option<&'a str> { - self.0.value_of(key.as_ref().as_bytes()).map(|v| { - // SAFETY: We know this is valid UTF-8 since we only - // construct the underlying `bytes` from valid UTF-8 - str::from_utf8(v).expect("We only construct the underlying bytes from valid UTF-8") - }) - } - - /// Find the value of the kernel argument with the provided name, which must be present. - /// - /// Otherwise the same as [`Self::value_of`]. - pub fn require_value_of + ?Sized>(&'a self, key: &T) -> Result<&'a str> { - let key = key.as_ref(); - self.value_of(key) - .ok_or_else(|| anyhow::anyhow!("Failed to find kernel argument '{key}'")) - } - - /// Add a parameter to the command line if it doesn't already exist - /// - /// Returns `Action::Added` if the parameter did not already exist - /// and was added. - /// - /// Returns `Action::Existed` if the exact parameter (same key and value) - /// already exists. No modification was made. - /// - /// Unlike `add_or_modify`, this method will not modify existing - /// parameters. If a parameter with the same key exists but has a - /// different value, the new parameter is still added, allowing - /// duplicate keys (e.g., multiple `console=` parameters). - pub fn add(&mut self, param: &Parameter) -> Action { - self.0.add(¶m.0) - } - - /// Add or modify a parameter to the command line - /// - /// Returns `Action::Added` if the parameter did not exist before - /// and was added. - /// - /// Returns `Action::Modified` if the parameter existed before, - /// but contained a different value. The value was updated to the - /// newly-requested value. - /// - /// Returns `Action::Existed` if the parameter existed before, and - /// contained the same value as the newly-requested value. No - /// modification was made. - pub fn add_or_modify(&mut self, param: &Parameter) -> Action { - self.0.add_or_modify(¶m.0) - } - - /// Remove parameter(s) with the given key from the command line - /// - /// Returns `true` if parameter(s) were removed. - pub fn remove(&mut self, key: &ParameterKey) -> bool { - self.0.remove(&key.0) - } - - /// Remove all parameters that exactly match the given parameter - /// from the command line - /// - /// Returns `true` if parameter(s) were removed. - pub fn remove_exact(&mut self, param: &Parameter) -> bool { - self.0.remove_exact(¶m.0) - } - - #[cfg(test)] - pub(crate) fn is_owned(&self) -> bool { - self.0.is_owned() - } - - #[cfg(test)] - pub(crate) fn is_borrowed(&self) -> bool { - self.0.is_borrowed() - } -} - -impl Deref for Cmdline<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - // SAFETY: We know this is valid UTF-8 since we only - // construct the underlying `bytes` from valid UTF-8 - str::from_utf8(&self.0).expect("We only construct the underlying bytes from valid UTF-8") - } -} - -impl<'a, T> AsRef for Cmdline<'a> -where - T: ?Sized, - as Deref>::Target: AsRef, -{ - fn as_ref(&self) -> &T { - self.deref().as_ref() - } -} - -impl<'a> std::fmt::Display for Cmdline<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.write_str(self) - } -} - -impl<'a> IntoIterator for &'a Cmdline<'a> { - type Item = Parameter<'a>; - type IntoIter = CmdlineIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl<'a, 'other> Extend> for Cmdline<'a> { - // Note this is O(N*M), but in practice this doesn't matter - // because kernel cmdlines are typically quite small (limited - // to at most 4k depending on arch). Using a hash-based - // structure to reduce this to O(N)+C would likely raise the C - // portion so much as to erase any benefit from removing the - // combinatorial complexity. Plus CPUs are good at - // caching/pipelining through contiguous memory. - fn extend>>(&mut self, iter: T) { - for param in iter { - self.add(¶m); - } - } -} - -/// A single kernel command line parameter key -/// -/// Handles quoted values and treats dashes and underscores in keys as equivalent. -#[derive(Clone, Debug, Eq)] -pub struct ParameterKey<'a>(bytes::ParameterKey<'a>); - -impl Deref for ParameterKey<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - // SAFETY: We know this is valid UTF-8 since we only - // construct the underlying `bytes` from valid UTF-8 - str::from_utf8(&self.0).expect("We only construct the underlying bytes from valid UTF-8") - } -} - -impl<'a, T> AsRef for ParameterKey<'a> -where - T: ?Sized, - as Deref>::Target: AsRef, -{ - fn as_ref(&self) -> &T { - self.deref().as_ref() - } -} - -impl<'a> ParameterKey<'a> { - /// Construct a utf8::ParameterKey from a bytes::ParameterKey - /// - /// This is non-public and should only be used when the underlying - /// bytes are known to be valid UTF-8. - fn from_bytes(input: bytes::ParameterKey<'a>) -> Self { - Self(input) - } -} - -impl<'a, T: AsRef + ?Sized> From<&'a T> for ParameterKey<'a> { - fn from(input: &'a T) -> Self { - Self(bytes::ParameterKey(input.as_ref().as_bytes())) - } -} - -impl<'a> std::fmt::Display for ParameterKey<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.write_str(self) - } -} - -impl PartialEq for ParameterKey<'_> { - /// Compares two parameter keys for equality. - /// - /// Keys are compared with dashes and underscores treated as equivalent. - /// This comparison is case-sensitive. - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -/// A single kernel command line parameter. -#[derive(Clone, Debug, Eq)] -pub struct Parameter<'a>(bytes::Parameter<'a>); - -impl<'a> Parameter<'a> { - /// Attempt to parse a single command line parameter from a UTF-8 - /// string. - /// - /// Returns `Some(Parameter)`, or `None` if a Parameter could not - /// be constructed from the input. This occurs when the input is - /// either empty or contains only whitespace. - pub fn parse + ?Sized>(input: &'a T) -> Option { - bytes::Parameter::parse(input.as_ref().as_bytes()).map(Self) - } - - /// Construct a utf8::Parameter from a bytes::Parameter - /// - /// This is non-public and should only be used when the underlying - /// bytes are known to be valid UTF-8. - fn from_bytes(bytes: bytes::Parameter<'a>) -> Self { - Self(bytes) - } - - /// Returns the key part of the parameter - pub fn key(&'a self) -> ParameterKey<'a> { - ParameterKey::from_bytes(self.0.key()) - } - - /// Returns the optional value part of the parameter - pub fn value(&'a self) -> Option<&'a str> { - self.0.value().map(|p| { - // SAFETY: We know this is valid UTF-8 since we only - // construct the underlying `bytes` from valid UTF-8 - str::from_utf8(p).expect("We only construct the underlying bytes from valid UTF-8") - }) - } -} - -impl<'a> TryFrom> for Parameter<'a> { - type Error = anyhow::Error; - - fn try_from(bytes: bytes::Parameter<'a>) -> Result { - if str::from_utf8(bytes.key().deref()).is_err() { - anyhow::bail!("Parameter key is not valid UTF-8"); - } - - if let Some(value) = bytes.value() { - if str::from_utf8(value).is_err() { - anyhow::bail!("Parameter value is not valid UTF-8"); - } - } - - Ok(Self(bytes)) - } -} - -impl<'a> std::fmt::Display for Parameter<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - f.write_str(self) - } -} - -impl Deref for Parameter<'_> { - type Target = str; - - fn deref(&self) -> &Self::Target { - // SAFETY: We know this is valid UTF-8 since we only - // construct the underlying `bytes` from valid UTF-8 - str::from_utf8(&self.0).expect("We only construct the underlying bytes from valid UTF-8") - } -} - -impl<'a, T> AsRef for Parameter<'a> -where - T: ?Sized, - as Deref>::Target: AsRef, -{ - fn as_ref(&self) -> &T { - self.deref().as_ref() - } -} - -impl<'a> PartialEq for Parameter<'a> { - fn eq(&self, other: &Self) -> bool { - self.0 == other.0 - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // convenience method for tests - fn param(s: &str) -> Parameter<'_> { - Parameter::parse(s).unwrap() - } - - #[test] - fn test_parameter_parse() { - let p = Parameter::parse("foo").unwrap(); - assert_eq!(p.key(), "foo".into()); - assert_eq!(p.value(), None); - - // should parse only the first parameter and discard the rest of the input - let p = Parameter::parse("foo=bar baz").unwrap(); - assert_eq!(p.key(), "foo".into()); - assert_eq!(p.value(), Some("bar")); - - // should return None on empty or whitespace inputs - assert!(Parameter::parse("").is_none()); - assert!(Parameter::parse(" ").is_none()); - } - - #[test] - fn test_parameter_simple() { - let switch = param("foo"); - assert_eq!(switch.key(), "foo".into()); - assert_eq!(switch.value(), None); - - let kv = param("bar=baz"); - assert_eq!(kv.key(), "bar".into()); - assert_eq!(kv.value(), Some("baz")); - } - - #[test] - fn test_parameter_quoted() { - let p = param("foo=\"quoted value\""); - assert_eq!(p.value(), Some("quoted value")); - - let p = param("foo=\"unclosed quotes"); - assert_eq!(p.value(), Some("unclosed quotes")); - - let p = param("foo=trailing_quotes\""); - assert_eq!(p.value(), Some("trailing_quotes")); - - let outside_quoted = param("\"foo=quoted value\""); - let value_quoted = param("foo=\"quoted value\""); - assert_eq!(outside_quoted, value_quoted); - } - - #[test] - fn test_parameter_display() { - // Basically this should always return the original data - // without modification. - - // unquoted stays unquoted - assert_eq!(param("foo").to_string(), "foo"); - - // quoted stays quoted - assert_eq!(param("\"foo\"").to_string(), "\"foo\""); - } - - #[test] - fn test_parameter_extra_whitespace() { - let p = param(" foo=bar "); - assert_eq!(p.key(), "foo".into()); - assert_eq!(p.value(), Some("bar")); - } - - #[test] - fn test_parameter_internal_key_whitespace() { - // parse should only consume the first parameter - let p = Parameter::parse("foo bar=baz").unwrap(); - assert_eq!(p.key(), "foo".into()); - assert_eq!(p.value(), None); - } - - #[test] - fn test_parameter_pathological() { - // valid things that certified insane people would do - - // you can quote just the key part in a key-value param, but - // the end quote is actually part of the key as far as the - // kernel is concerned... - let p = param("\"foo\"=bar"); - assert_eq!(p.key(), ParameterKey::from("foo\"")); - assert_eq!(p.value(), Some("bar")); - // and it is definitely not equal to an unquoted foo ... - assert_ne!(p, param("foo=bar")); - - // ... but if you close the quote immediately after the - // equals sign, it does get removed. - let p = param("\"foo=\"bar"); - assert_eq!(p.key(), ParameterKey::from("foo")); - assert_eq!(p.value(), Some("bar")); - // ... so of course this makes sense ... - assert_eq!(p, param("foo=bar")); - - // quotes only get stripped from the absolute ends of values - let p = param("foo=\"internal\"quotes\"are\"ok\""); - assert_eq!(p.value(), Some("internal\"quotes\"are\"ok")); - } - - #[test] - fn test_parameter_equality() { - // substrings are not equal - let foo = param("foo"); - let bar = param("foobar"); - assert_ne!(foo, bar); - assert_ne!(bar, foo); - - // dashes and underscores are treated equally - let dashes = param("a-delimited-param"); - let underscores = param("a_delimited_param"); - assert_eq!(dashes, underscores); - - // same key, same values is equal - let dashes = param("a-delimited-param=same_values"); - let underscores = param("a_delimited_param=same_values"); - assert_eq!(dashes, underscores); - - // same key, different values is not equal - let dashes = param("a-delimited-param=different_values"); - let underscores = param("a_delimited_param=DiFfErEnT_valUEZ"); - assert_ne!(dashes, underscores); - - // mixed variants are never equal - let switch = param("same_key"); - let keyvalue = param("same_key=but_with_a_value"); - assert_ne!(switch, keyvalue); - } - - #[test] - fn test_parameter_tryfrom() { - // ok switch - let p = bytes::Parameter::parse(b"foo").unwrap(); - let utf = Parameter::try_from(p).unwrap(); - assert_eq!(utf.key(), "foo".into()); - assert_eq!(utf.value(), None); - - // ok key/value - let p = bytes::Parameter::parse(b"foo=bar").unwrap(); - let utf = Parameter::try_from(p).unwrap(); - assert_eq!(utf.key(), "foo".into()); - assert_eq!(utf.value(), Some("bar".into())); - - // bad switch - let p = bytes::Parameter::parse(b"f\xffoo").unwrap(); - let e = Parameter::try_from(p); - assert_eq!( - e.unwrap_err().to_string(), - "Parameter key is not valid UTF-8" - ); - - // bad key/value - let p = bytes::Parameter::parse(b"foo=b\xffar").unwrap(); - let e = Parameter::try_from(p); - assert_eq!( - e.unwrap_err().to_string(), - "Parameter value is not valid UTF-8" - ); - } - - #[test] - fn test_kargs_simple() { - // example taken lovingly from: - // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/kernel/params.c?id=89748acdf226fd1a8775ff6fa2703f8412b286c8#n160 - let kargs = Cmdline::from("foo=bar,bar2 baz=fuz wiz"); - assert!(kargs.is_borrowed()); - let mut iter = kargs.iter(); - - assert_eq!(iter.next(), Some(param("foo=bar,bar2"))); - assert_eq!(iter.next(), Some(param("baz=fuz"))); - assert_eq!(iter.next(), Some(param("wiz"))); - assert_eq!(iter.next(), None); - - // Test the find API - assert_eq!(kargs.find("foo").unwrap().value().unwrap(), "bar,bar2"); - assert!(kargs.find("nothing").is_none()); - } - - #[test] - fn test_cmdline_default() { - let kargs: Cmdline = Default::default(); - assert_eq!(kargs.iter().next(), None); - } - - #[test] - fn test_cmdline_new() { - let kargs = Cmdline::new(); - assert_eq!(kargs.iter().next(), None); - assert!(kargs.is_owned()); - - // Verify we can store it in an owned ('static) context - let _static_kargs: CmdlineOwned = Cmdline::new(); - } - - #[test] - fn test_kargs_simple_from_string() { - let kargs = Cmdline::from("foo=bar,bar2 baz=fuz wiz".to_string()); - assert!(kargs.is_owned()); - let mut iter = kargs.iter(); - - assert_eq!(iter.next(), Some(param("foo=bar,bar2"))); - assert_eq!(iter.next(), Some(param("baz=fuz"))); - assert_eq!(iter.next(), Some(param("wiz"))); - assert_eq!(iter.next(), None); - - // Test the find API - assert_eq!(kargs.find("foo").unwrap().value().unwrap(), "bar,bar2"); - assert!(kargs.find("nothing").is_none()); - } - - #[test] - fn test_kargs_from_proc() { - let kargs = Cmdline::from_proc().unwrap(); - - // Not really a good way to test this other than assume - // there's at least one argument in /proc/cmdline wherever the - // tests are running - assert!(kargs.iter().count() > 0); - } - - #[test] - fn test_kargs_find_dash_hyphen() { - let kargs = Cmdline::from("a-b=1 a_b=2"); - // find should find the first one, which is a-b=1 - let p = kargs.find("a_b").unwrap(); - assert_eq!(p.key(), "a-b".into()); - assert_eq!(p.value().unwrap(), "1"); - let p = kargs.find("a-b").unwrap(); - assert_eq!(p.key(), "a-b".into()); - assert_eq!(p.value().unwrap(), "1"); - - let kargs = Cmdline::from("a_b=2 a-b=1"); - // find should find the first one, which is a_b=2 - let p = kargs.find("a_b").unwrap(); - assert_eq!(p.key(), "a_b".into()); - assert_eq!(p.value().unwrap(), "2"); - let p = kargs.find("a-b").unwrap(); - assert_eq!(p.key(), "a_b".into()); - assert_eq!(p.value().unwrap(), "2"); - } - - #[test] - fn test_kargs_extra_whitespace() { - let kargs = Cmdline::from(" foo=bar baz=fuz wiz "); - let mut iter = kargs.iter(); - - assert_eq!(iter.next(), Some(param("foo=bar"))); - assert_eq!(iter.next(), Some(param("baz=fuz"))); - assert_eq!(iter.next(), Some(param("wiz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_value_of() { - let kargs = Cmdline::from("foo=bar baz=qux switch"); - - // Test existing key with value - assert_eq!(kargs.value_of("foo"), Some("bar")); - assert_eq!(kargs.value_of("baz"), Some("qux")); - - // Test key without value - assert_eq!(kargs.value_of("switch"), None); - - // Test non-existent key - assert_eq!(kargs.value_of("missing"), None); - - // Test dash/underscore equivalence - let kargs = Cmdline::from("dash-key=value1 under_key=value2"); - assert_eq!(kargs.value_of("dash_key"), Some("value1")); - assert_eq!(kargs.value_of("under-key"), Some("value2")); - } - - #[test] - fn test_require_value_of() { - let kargs = Cmdline::from("foo=bar baz=qux switch"); - - // Test existing key with value - assert_eq!(kargs.require_value_of("foo").unwrap(), "bar"); - assert_eq!(kargs.require_value_of("baz").unwrap(), "qux"); - - // Test key without value should fail - let err = kargs.require_value_of("switch").unwrap_err(); - assert!( - err.to_string() - .contains("Failed to find kernel argument 'switch'") - ); - - // Test non-existent key should fail - let err = kargs.require_value_of("missing").unwrap_err(); - assert!( - err.to_string() - .contains("Failed to find kernel argument 'missing'") - ); - - // Test dash/underscore equivalence - let kargs = Cmdline::from("dash-key=value1 under_key=value2"); - assert_eq!(kargs.require_value_of("dash_key").unwrap(), "value1"); - assert_eq!(kargs.require_value_of("under-key").unwrap(), "value2"); - } - - #[test] - fn test_find_str() { - let kargs = Cmdline::from("foo=bar baz=qux switch rd.break"); - let p = kargs.find("foo").unwrap(); - assert_eq!(p, param("foo=bar")); - let p = kargs.find("rd.break").unwrap(); - assert_eq!(p, param("rd.break")); - assert!(kargs.find("missing").is_none()); - } - - #[test] - fn test_find_all_str() { - let kargs = Cmdline::from("foo=bar rd.foo=a rd.bar=b rd.baz rd.qux=c notrd.val=d"); - let mut rd_args: Vec<_> = kargs.find_all_starting_with("rd.").collect(); - rd_args.sort_by(|a, b| a.key().cmp(&b.key())); - assert_eq!(rd_args.len(), 4); - assert_eq!(rd_args[0], param("rd.bar=b")); - assert_eq!(rd_args[1], param("rd.baz")); - assert_eq!(rd_args[2], param("rd.foo=a")); - assert_eq!(rd_args[3], param("rd.qux=c")); - } - - #[test] - fn test_param_key_eq() { - let k1 = ParameterKey::from("a-b"); - let k2 = ParameterKey::from("a_b"); - assert_eq!(k1, k2); - let k1 = ParameterKey::from("a-b"); - let k2 = ParameterKey::from("a-c"); - assert_ne!(k1, k2); - } - - #[test] - fn test_add() { - let mut kargs = Cmdline::from("console=tty0 console=ttyS1"); - - // add new parameter with duplicate key but different value - assert!(matches!(kargs.add(¶m("console=ttyS2")), Action::Added)); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("console=tty0"))); - assert_eq!(iter.next(), Some(param("console=ttyS1"))); - assert_eq!(iter.next(), Some(param("console=ttyS2"))); - assert_eq!(iter.next(), None); - - // try to add exact duplicate - should return Existed - assert!(matches!( - kargs.add(¶m("console=ttyS1")), - Action::Existed - )); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("console=tty0"))); - assert_eq!(iter.next(), Some(param("console=ttyS1"))); - assert_eq!(iter.next(), Some(param("console=ttyS2"))); - assert_eq!(iter.next(), None); - - // add completely new parameter - assert!(matches!(kargs.add(¶m("quiet")), Action::Added)); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("console=tty0"))); - assert_eq!(iter.next(), Some(param("console=ttyS1"))); - assert_eq!(iter.next(), Some(param("console=ttyS2"))); - assert_eq!(iter.next(), Some(param("quiet"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_add_empty_cmdline() { - let mut kargs = Cmdline::from(""); - assert!(matches!(kargs.add(¶m("foo")), Action::Added)); - assert_eq!(&*kargs, "foo"); - } - - #[test] - fn test_add_or_modify() { - let mut kargs = Cmdline::from("foo=bar"); - - // add new - assert!(matches!(kargs.add_or_modify(¶m("baz")), Action::Added)); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=bar"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - - // modify existing - assert!(matches!( - kargs.add_or_modify(¶m("foo=fuz")), - Action::Modified - )); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=fuz"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - - // already exists with same value returns false and doesn't - // modify anything - assert!(matches!( - kargs.add_or_modify(¶m("foo=fuz")), - Action::Existed - )); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=fuz"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_add_or_modify_empty_cmdline() { - let mut kargs = Cmdline::from(""); - assert!(matches!(kargs.add_or_modify(¶m("foo")), Action::Added)); - assert_eq!(&*kargs, "foo"); - } - - #[test] - fn test_add_or_modify_duplicate_parameters() { - let mut kargs = Cmdline::from("a=1 a=2"); - assert!(matches!( - kargs.add_or_modify(¶m("a=3")), - Action::Modified - )); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("a=3"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_remove() { - let mut kargs = Cmdline::from("foo bar baz"); - - // remove existing - assert!(kargs.remove(&"bar".into())); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - - // doesn't exist? returns false and doesn't modify anything - assert!(!kargs.remove(&"missing".into())); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_remove_duplicates() { - let mut kargs = Cmdline::from("a=1 b=2 a=3"); - assert!(kargs.remove(&"a".into())); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("b=2"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_remove_exact() { - let mut kargs = Cmdline::from("foo foo=bar foo=baz"); - - // remove existing - assert!(kargs.remove_exact(¶m("foo=bar"))); - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo"))); - assert_eq!(iter.next(), Some(param("foo=baz"))); - assert_eq!(iter.next(), None); - - // doesn't exist? returns false and doesn't modify anything - assert!(!kargs.remove_exact(¶m("foo=wuz"))); - iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo"))); - assert_eq!(iter.next(), Some(param("foo=baz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_extend() { - let mut kargs = Cmdline::from("foo=bar baz"); - let other = Cmdline::from("qux=quux foo=updated"); - - kargs.extend(&other); - - // Sanity check that the lifetimes of the two Cmdlines are not - // tied to each other. - drop(other); - - // Should have preserved the original foo, added qux, baz - // unchanged, and added the second (duplicate key) foo - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=bar"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), Some(param("qux=quux"))); - assert_eq!(iter.next(), Some(param("foo=updated"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_extend_empty() { - let mut kargs = Cmdline::from(""); - let other = Cmdline::from("foo=bar baz"); - - kargs.extend(&other); - - let mut iter = kargs.iter(); - assert_eq!(iter.next(), Some(param("foo=bar"))); - assert_eq!(iter.next(), Some(param("baz"))); - assert_eq!(iter.next(), None); - } - - #[test] - fn test_into_iterator() { - let kargs = Cmdline::from("foo=bar baz=qux wiz"); - let params: Vec<_> = (&kargs).into_iter().collect(); - - assert_eq!(params.len(), 3); - assert_eq!(params[0], param("foo=bar")); - assert_eq!(params[1], param("baz=qux")); - assert_eq!(params[2], param("wiz")); - } - - #[test] - fn test_cmdline_eq() { - // Ordering, quoting, and the whole dash-underscore - // equivalence thing shouldn't affect whether these are - // semantically equal - assert_eq!( - Cmdline::from("foo bar-with-delim=\"with spaces\""), - Cmdline::from("\"bar_with_delim=with spaces\" foo") - ); - - // Uneven lengths are not equal even if the parameters are. Or - // to put it another way, duplicate parameters break equality. - // Check with both orderings. - assert_ne!(Cmdline::from("foo"), Cmdline::from("foo foo")); - assert_ne!(Cmdline::from("foo foo"), Cmdline::from("foo")); - - // Equal lengths but differing duplicates are also not equal - assert_ne!(Cmdline::from("a a b"), Cmdline::from("a b b")); - } -} diff --git a/crates/lib/Cargo.toml b/crates/lib/Cargo.toml index 49d74f701..d56ef1655 100644 --- a/crates/lib/Cargo.toml +++ b/crates/lib/Cargo.toml @@ -15,7 +15,7 @@ include = ["/src", "LICENSE-APACHE", "LICENSE-MIT"] [dependencies] # Internal crates bootc-blockdev = { package = "bootc-internal-blockdev", path = "../blockdev", version = "1.16.3" } -bootc-kernel-cmdline = { path = "../kernel_cmdline", version = "0.0.0" } +linux-kernel-cmdline = { workspace = true } bootc-mount = { package = "bootc-internal-mount", path = "../mount", version = "1.16.3" } bootc-sysusers = { path = "../sysusers" } bootc-tmpfiles = { path = "../tmpfiles" } diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 5437fe44b..dd4079c7a 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -67,7 +67,6 @@ use std::path::Path; use std::sync::Arc; use anyhow::{Context, Result, anyhow, bail}; -use bootc_kernel_cmdline::utf8::{Cmdline, Parameter}; use bootc_mount::tempmount::TempMount; use camino::{Utf8Path, Utf8PathBuf}; use cap_std_ext::{ @@ -89,6 +88,7 @@ use composefs_ctl::composefs; use composefs_ctl::composefs_boot; use composefs_ctl::composefs_oci; use fn_error_context::context; +use linux_kernel_cmdline::utf8::{Cmdline, Parameter}; use rustix::{mount::MountFlags, path::Arg}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; diff --git a/crates/lib/src/bootc_composefs/soft_reboot.rs b/crates/lib/src/bootc_composefs/soft_reboot.rs index 74e35d204..1d8ecfc22 100644 --- a/crates/lib/src/bootc_composefs/soft_reboot.rs +++ b/crates/lib/src/bootc_composefs/soft_reboot.rs @@ -8,13 +8,13 @@ use crate::{ }; use anyhow::{Context, Result}; use bootc_initramfs_setup::setup_root; -use bootc_kernel_cmdline::utf8::Cmdline; use bootc_mount::{PID1, bind_mount_from_pidns}; use camino::Utf8Path; use cap_std_ext::cap_std::ambient_authority; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; +use linux_kernel_cmdline::utf8::Cmdline; use ostree_ext::systemd_has_soft_reboot; use rustix::mount::{UnmountFlags, unmount}; use std::{fs::create_dir_all, os::unix::process::CommandExt, path::PathBuf, process::Command}; diff --git a/crates/lib/src/bootc_composefs/state.rs b/crates/lib/src/bootc_composefs/state.rs index 019662559..e43447a2f 100644 --- a/crates/lib/src/bootc_composefs/state.rs +++ b/crates/lib/src/bootc_composefs/state.rs @@ -5,7 +5,6 @@ use std::{fs::create_dir_all, process::Command}; use anyhow::{Context, Result}; use bootc_initramfs_setup::{mount_at_wrapper, overlay_transient}; -use bootc_kernel_cmdline::utf8::Cmdline; use bootc_mount::tempmount::TempMount; use bootc_utils::CommandRunExt; use camino::Utf8PathBuf; @@ -16,6 +15,7 @@ use cap_std_ext::dirext::CapStdExtDirExt; use composefs::fsverity::{FsVerityHashValue, Sha512HashValue}; use composefs_ctl::composefs; use fn_error_context::context; +use linux_kernel_cmdline::utf8::Cmdline; use ostree_ext::container::deploy::ORIGIN_CONTAINER; use rustix::{ diff --git a/crates/lib/src/bootc_composefs/status.rs b/crates/lib/src/bootc_composefs/status.rs index 62e906628..e37790373 100644 --- a/crates/lib/src/bootc_composefs/status.rs +++ b/crates/lib/src/bootc_composefs/status.rs @@ -1,12 +1,12 @@ use std::{io::Read, sync::OnceLock}; use anyhow::{Context, Result}; -use bootc_kernel_cmdline::utf8::Cmdline; use bootc_mount::inspect_filesystem; use composefs_ctl::composefs::fsverity::Sha512HashValue; use composefs_ctl::composefs_oci; use composefs_oci::OciImage; use fn_error_context::context; +use linux_kernel_cmdline::utf8::Cmdline; use openssl::sha::Sha256; use serde::{Deserialize, Serialize}; diff --git a/crates/lib/src/bootc_composefs/utils.rs b/crates/lib/src/bootc_composefs/utils.rs index 653722071..f7aab3eea 100644 --- a/crates/lib/src/bootc_composefs/utils.rs +++ b/crates/lib/src/bootc_composefs/utils.rs @@ -6,9 +6,9 @@ use crate::{ store::Storage, }; use anyhow::Result; -use bootc_kernel_cmdline::utf8::Cmdline; use composefs_ctl::composefs_boot; use fn_error_context::context; +use linux_kernel_cmdline::utf8::Cmdline; fn get_uki(storage: &Storage, deployment_verity: &str) -> Result { let uki_dir = storage.require_esp()?.fd.open_dir(BOOTC_UKI_DIR)?; diff --git a/crates/lib/src/bootc_kargs.rs b/crates/lib/src/bootc_kargs.rs index 50f0c09b5..6d1883576 100644 --- a/crates/lib/src/bootc_kargs.rs +++ b/crates/lib/src/bootc_kargs.rs @@ -1,11 +1,11 @@ //! This module handles the bootc-owned kernel argument lists in `/usr/lib/bootc/kargs.d`. use anyhow::{Context, Result}; -use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use camino::Utf8Path; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::cap_std::fs_utf8::Dir as DirUtf8; use cap_std_ext::dirext::CapStdExtDirExt; use cap_std_ext::dirext::CapStdExtDirExtUtf8; +use linux_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use ostree::gio; use ostree_ext::ostree; use ostree_ext::ostree::Deployment; diff --git a/crates/lib/src/deploy.rs b/crates/lib/src/deploy.rs index c15f6cb76..739f54d81 100644 --- a/crates/lib/src/deploy.rs +++ b/crates/lib/src/deploy.rs @@ -50,12 +50,12 @@ use std::os::fd::AsFd; use std::process::Command; use anyhow::{Context, Result, anyhow}; -use bootc_kernel_cmdline::utf8::CmdlineOwned; use bootc_utils::skopeo_bin; use cap_std::fs::{Dir, MetadataExt}; use cap_std_ext::cap_std; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; +use linux_kernel_cmdline::utf8::CmdlineOwned; use ostree::{gio, glib}; use ostree_container::OstreeImageReference; use ostree_ext::container as ostree_container; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index fea62d28a..78974fef5 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -160,7 +160,6 @@ use std::time::Duration; use aleph::InstallAleph; use anyhow::{Context, Result, anyhow, ensure}; -use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use bootc_utils::CommandRunExt; use camino::Utf8Path; use camino::Utf8PathBuf; @@ -174,6 +173,7 @@ use cap_std_ext::cmdext::CapStdExtCommandExt; use cap_std_ext::prelude::CapStdExtDirExt; use clap::ValueEnum; use fn_error_context::context; +use linux_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use ostree::gio; use ostree_ext::ostree; use ostree_ext::ostree_prepareroot::{ComposefsState, Tristate}; @@ -192,6 +192,7 @@ use crate::bootc_composefs::{ boot::setup_composefs_boot, repo::initialize_composefs_repository, status::get_container_manifest_and_config, }; +use crate::bootc_kargs::{INITRD_ARG_PREFIX, ROOTFLAGS_KEY}; use crate::boundimage::{BoundImage, ResolvedBoundImage}; use crate::containerenv::ContainerExecutionInfo; use crate::deploy::{MergeState, PreparedPullResult, prepare_for_pull, pull_from_prepared}; @@ -202,9 +203,9 @@ use crate::spec::{Bootloader, ImageReference}; use crate::store::Storage; use crate::task::Task; use crate::utils::sigpolicy_from_opt; -use bootc_kernel_cmdline::{INITRD_ARG_PREFIX, ROOTFLAGS, bytes, utf8}; use bootc_mount::Filesystem; use composefs_ctl::composefs::repository::RepositoryConfig; +use linux_kernel_cmdline::{bytes, utf8}; /// The toplevel boot directory pub(crate) const BOOT: &str = "boot"; @@ -2389,7 +2390,7 @@ fn find_root_args_to_inherit( .find_utf8("root")? .and_then(|p| p.value().map(|p| p.to_string())); let (mount_spec, kargs) = if let Some(root) = root { - let rootflags = cmdline.find(ROOTFLAGS); + let rootflags = cmdline.find(ROOTFLAGS_KEY); let inherit_kargs = cmdline.find_all_starting_with(INITRD_ARG_PREFIX); ( root, diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index 25e1b1a65..0db968b20 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -30,9 +30,9 @@ use super::RootSetup; use super::State; use super::config::Filesystem; use crate::task::Task; -use bootc_kernel_cmdline::utf8::Cmdline; #[cfg(feature = "install-to-disk")] use bootc_mount::is_mounted_in_pid1_mountns; +use linux_kernel_cmdline::utf8::Cmdline; /// Check whether DPS auto-discovery is enabled. When `true`, /// `root=UUID=` is omitted and `systemd-gpt-auto-generator` discovers diff --git a/crates/lib/src/kernel.rs b/crates/lib/src/kernel.rs index 83ae22bcd..c7b4c34ba 100644 --- a/crates/lib/src/kernel.rs +++ b/crates/lib/src/kernel.rs @@ -7,11 +7,11 @@ use std::path::Path; use anyhow::{Context, Result}; -use bootc_kernel_cmdline::utf8::Cmdline; use camino::Utf8PathBuf; use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::dirext::CapStdExtDirExt; use composefs_ctl::composefs_boot; +use linux_kernel_cmdline::utf8::Cmdline; use serde::Serialize; use crate::bootc_composefs::boot::EFI_LINUX; diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index e24458797..d9eccc0c8 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -60,7 +60,7 @@ //! //! - [`ostree-ext`](../ostree_ext/index.html) - OCI/ostree bridging //! - [`bootc-internal-mount`](../bootc_mount/index.html) - Mount utilities -//! - [`bootc-kernel-cmdline`](../bootc_kernel_cmdline/index.html) - Cmdline parsing +//! - [`linux-kernel-cmdline`](../linux_kernel_cmdline/index.html) - Cmdline parsing //! - [`etc-merge`](../etc_merge/index.html) - `/etc` three-way merge mod bootc_composefs; diff --git a/crates/lib/src/loader_entries.rs b/crates/lib/src/loader_entries.rs index 0db9f8756..3f193da39 100644 --- a/crates/lib/src/loader_entries.rs +++ b/crates/lib/src/loader_entries.rs @@ -10,8 +10,8 @@ //! See use anyhow::{Context, Result, ensure}; -use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use fn_error_context::context; +use linux_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use ostree::{gio, glib}; use ostree_ext::ostree; use std::collections::BTreeMap; diff --git a/crates/lib/src/parsers/bls_config.rs b/crates/lib/src/parsers/bls_config.rs index 1de39e4be..c796ffdab 100644 --- a/crates/lib/src/parsers/bls_config.rs +++ b/crates/lib/src/parsers/bls_config.rs @@ -3,11 +3,11 @@ //! This module parses the config files for the spec. use anyhow::{Result, anyhow}; -use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use camino::Utf8PathBuf; use composefs_boot::bootloader::EFI_EXT; use composefs_ctl::composefs_boot; use core::fmt; +use linux_kernel_cmdline::utf8::{Cmdline, CmdlineOwned}; use std::collections::HashMap; use std::fmt::Display; use uapi_version::Version; diff --git a/crates/lib/src/ukify.rs b/crates/lib/src/ukify.rs index 523836217..cd434a396 100644 --- a/crates/lib/src/ukify.rs +++ b/crates/lib/src/ukify.rs @@ -7,11 +7,11 @@ use std::ffi::OsString; use std::process::Command; use anyhow::{Context, Result}; -use bootc_kernel_cmdline::utf8::Cmdline; use bootc_utils::CommandRunExt; use camino::Utf8Path; use cap_std_ext::cap_std::fs::Dir; use fn_error_context::context; +use linux_kernel_cmdline::utf8::Cmdline; use crate::bootc_composefs::digest::compute_composefs_digest; use crate::bootc_composefs::status::ComposefsCmdline; diff --git a/crates/tests-integration/Cargo.toml b/crates/tests-integration/Cargo.toml index d812ac5d8..36f7abfe9 100644 --- a/crates/tests-integration/Cargo.toml +++ b/crates/tests-integration/Cargo.toml @@ -23,7 +23,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tempfile = { workspace = true } xshell = { workspace = true } -bootc-kernel-cmdline = { path = "../kernel_cmdline", version = "0.0.0" } +linux-kernel-cmdline = { workspace = true } # Crate-specific dependencies # bcvk-qemu: QEMU/virtiofsd management from the bcvk project.