Skip to content

Commit 3e1fbc5

Browse files
committed
kernel: Add find API w/correct hyphen-dash equality, add docs
We had use cases which were doing `iter().find(|v| v.key ==` which would NOT do the `-_` insensitive comparison. Add a newtype `ParameterKey` and move the comparison there. This makes the API slightly more awkward to use when inspecting as one needs `.key.0` but eh. Signed-off-by: Colin Walters <[email protected]>
1 parent 92409e9 commit 3e1fbc5

File tree

2 files changed

+126
-42
lines changed

2 files changed

+126
-42
lines changed

crates/lib/src/install.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,15 +1663,13 @@ struct RootMountInfo {
16631663
/// Discover how to mount the root filesystem, using existing kernel arguments and information
16641664
/// about the root mount.
16651665
fn find_root_args_to_inherit(cmdline: &Cmdline, root_info: &Filesystem) -> Result<RootMountInfo> {
1666-
let root = cmdline.iter().find(|p| p.key == b"root");
1666+
let root = cmdline.find("root");
16671667
// If we have a root= karg, then use that
16681668
let (mount_spec, kargs) = if let Some(root) = root {
1669-
let rootflags = cmdline
1670-
.iter()
1671-
.find(|arg| arg.key == crate::kernel::ROOTFLAGS);
1669+
let rootflags = cmdline.find(crate::kernel::ROOTFLAGS);
16721670
let inherit_kargs = cmdline
16731671
.iter()
1674-
.filter(|arg| arg.key.starts_with(crate::kernel::INITRD_ARG_PREFIX));
1672+
.filter(|arg| arg.key.0.starts_with(crate::kernel::INITRD_ARG_PREFIX));
16751673
(
16761674
root.value_lossy(),
16771675
rootflags

crates/lib/src/kernel.rs

Lines changed: 123 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
//! Kernel command line parsing utilities.
2+
//!
3+
//! This module provides functionality for parsing and working with kernel command line
4+
//! arguments, supporting both key-only switches and key-value pairs with proper quote handling.
5+
16
use std::borrow::Cow;
27

38
use anyhow::Result;
@@ -7,19 +12,35 @@ pub(crate) const INITRD_ARG_PREFIX: &[u8] = b"rd.";
712
/// The kernel argument for configuring the rootfs flags.
813
pub(crate) const ROOTFLAGS: &[u8] = b"rootflags";
914

15+
/// A parsed kernel command line.
16+
///
17+
/// Wraps the raw command line bytes and provides methods for parsing and iterating
18+
/// over individual parameters. Uses copy-on-write semantics to avoid unnecessary
19+
/// allocations when working with borrowed data.
1020
pub(crate) struct Cmdline<'a>(Cow<'a, [u8]>);
1121

1222
impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for Cmdline<'a> {
23+
/// Creates a new `Cmdline` from any type that can be referenced as bytes.
24+
///
25+
/// Uses borrowed data when possible to avoid unnecessary allocations.
1326
fn from(input: &'a T) -> Self {
1427
Self(Cow::Borrowed(input.as_ref()))
1528
}
1629
}
1730

1831
impl<'a> Cmdline<'a> {
32+
/// Reads the kernel command line from `/proc/cmdline`.
33+
///
34+
/// Returns an error if the file cannot be read or if there are I/O issues.
1935
pub fn from_proc() -> Result<Self> {
2036
Ok(Self(Cow::Owned(std::fs::read("/proc/cmdline")?)))
2137
}
2238

39+
/// Returns an iterator over all parameters in the command line.
40+
///
41+
/// Properly handles quoted values containing whitespace and splits on
42+
/// unquoted whitespace characters. Parameters are parsed as either
43+
/// key-only switches or key=value pairs.
2344
pub fn iter(&'a self) -> impl Iterator<Item = Parameter<'a>> {
2445
let mut in_quotes = false;
2546

@@ -32,36 +53,91 @@ impl<'a> Cmdline<'a> {
3253
})
3354
.map(Parameter::from)
3455
}
56+
57+
/// Locate a kernel argument with the given key name.
58+
///
59+
/// Returns the first parameter matching the given key, or `None` if not found.
60+
/// Key comparison treats dashes and underscores as equivalent.
61+
pub fn find(&'a self, key: impl AsRef<[u8]>) -> Option<Parameter<'a>> {
62+
let key = ParameterKey(key.as_ref());
63+
self.iter().find(|p| p.key == key)
64+
}
3565
}
3666

67+
/// A single kernel command line parameter key
68+
///
69+
/// Handles quoted values and treats dashes and underscores in keys as equivalent.
3770
#[derive(Debug, Eq)]
71+
pub(crate) struct ParameterKey<'a>(pub &'a [u8]);
72+
73+
impl<'a> From<&'a [u8]> for ParameterKey<'a> {
74+
fn from(value: &'a [u8]) -> Self {
75+
Self(value)
76+
}
77+
}
78+
79+
/// A single kernel command line parameter.
80+
#[derive(Debug, PartialEq, Eq)]
3881
pub(crate) struct Parameter<'a> {
39-
pub key: &'a [u8],
82+
/// The parameter key as raw bytes
83+
pub key: ParameterKey<'a>,
84+
/// The parameter value as raw bytes, if present
4085
pub value: Option<&'a [u8]>,
4186
}
4287

4388
impl<'a> Parameter<'a> {
89+
/// Create a new parameter with the provided key and value.
90+
#[cfg(test)]
91+
pub fn new_kv<'k: 'a, 'v: 'a>(key: &'k [u8], value: &'v [u8]) -> Self {
92+
Self {
93+
key: ParameterKey(key),
94+
value: Some(value),
95+
}
96+
}
97+
98+
/// Create a new parameter with the provided key.
99+
#[cfg(test)]
100+
pub fn new_key(key: &'a [u8]) -> Self {
101+
Self {
102+
key: ParameterKey(key),
103+
value: None,
104+
}
105+
}
106+
107+
/// Returns the key as a lossy UTF-8 string.
108+
///
109+
/// Invalid UTF-8 sequences are replaced with the Unicode replacement character.
44110
pub fn key_lossy(&self) -> String {
45-
String::from_utf8_lossy(self.key).to_string()
111+
String::from_utf8_lossy(self.key.0).to_string()
46112
}
47113

114+
/// Returns the value as a lossy UTF-8 string.
115+
///
116+
/// Invalid UTF-8 sequences are replaced with the Unicode replacement character.
117+
/// Returns an empty string if no value is present.
48118
pub fn value_lossy(&self) -> String {
49119
String::from_utf8_lossy(self.value.unwrap_or(&[])).to_string()
50120
}
51121
}
52122

53123
impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for Parameter<'a> {
124+
/// Parses a parameter from raw bytes.
125+
///
126+
/// Splits on the first `=` character to separate key and value.
127+
/// Strips only the outermost pair of double quotes from values.
128+
/// If no `=` is found, treats the entire input as a key-only parameter.
54129
fn from(input: &'a T) -> Self {
55130
let input = input.as_ref();
56131
let equals = input.iter().position(|b| *b == b'=');
57132

58133
match equals {
59134
None => Self {
60-
key: input,
135+
key: ParameterKey(input),
61136
value: None,
62137
},
63138
Some(i) => {
64139
let (key, mut value) = input.split_at(i);
140+
let key = ParameterKey(key);
65141

66142
// skip `=`, we know it's the first byte because we
67143
// found it above
@@ -83,7 +159,11 @@ impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for Parameter<'a> {
83159
}
84160
}
85161

86-
impl PartialEq for Parameter<'_> {
162+
impl PartialEq for ParameterKey<'_> {
163+
/// Compares two parameter keys for equality.
164+
///
165+
/// Keys are compared with dashes and underscores treated as equivalent.
166+
/// This comparison is case-sensitive.
87167
fn eq(&self, other: &Self) -> bool {
88168
let dedashed = |&c: &u8| {
89169
if c == b'-' {
@@ -97,21 +177,18 @@ impl PartialEq for Parameter<'_> {
97177
//
98178
// For example, "foo" == "foobar" since the zipped iterator
99179
// only compares the first three chars.
100-
let our_iter = self.key.iter().map(dedashed);
101-
let other_iter = other.key.iter().map(dedashed);
102-
if !our_iter.eq(other_iter) {
103-
return false;
104-
}
105-
106-
match (self.value, other.value) {
107-
(Some(ours), Some(other)) => ours == other,
108-
(None, None) => true,
109-
_ => false,
110-
}
180+
let our_iter = self.0.iter().map(dedashed);
181+
let other_iter = other.0.iter().map(dedashed);
182+
our_iter.eq(other_iter)
111183
}
112184
}
113185

114186
impl std::fmt::Display for Parameter<'_> {
187+
/// Formats the parameter for display.
188+
///
189+
/// Key-only parameters are displayed as just the key.
190+
/// Key-value parameters are displayed as `key=value`.
191+
/// Values containing whitespace are automatically quoted.
115192
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
116193
let key = self.key_lossy();
117194

@@ -136,11 +213,11 @@ mod tests {
136213
#[test]
137214
fn test_parameter_simple() {
138215
let switch = Parameter::from("foo");
139-
assert_eq!(switch.key, b"foo");
216+
assert_eq!(switch.key.0, b"foo");
140217
assert_eq!(switch.value, None);
141218

142219
let kv = Parameter::from("bar=baz");
143-
assert_eq!(kv.key, b"bar");
220+
assert_eq!(kv.key.0, b"bar");
144221
assert_eq!(kv.value, Some(b"baz".as_slice()));
145222
}
146223

@@ -156,7 +233,7 @@ mod tests {
156233

157234
// quotes don't get removed from keys
158235
let p = Parameter::from("\"\"\"");
159-
assert_eq!(p.key, b"\"\"\"");
236+
assert_eq!(p.key.0, b"\"\"\"");
160237

161238
// quotes only get stripped from the absolute ends of values
162239
let p = Parameter::from("foo=\"internal \" quotes \" are ok\"");
@@ -212,31 +289,19 @@ mod tests {
212289
let kargs = Cmdline::from(b"foo=bar,bar2 baz=fuz wiz".as_slice());
213290
let mut iter = kargs.iter();
214291

215-
assert_eq!(
216-
iter.next(),
217-
Some(Parameter {
218-
key: b"foo",
219-
value: Some(b"bar,bar2".as_slice())
220-
})
221-
);
292+
assert_eq!(iter.next(), Some(Parameter::new_kv(b"foo", b"bar,bar2")));
222293

223294
assert_eq!(
224295
iter.next(),
225-
Some(Parameter {
226-
key: b"baz",
227-
value: Some(b"fuz".as_slice())
228-
})
229-
);
230-
231-
assert_eq!(
232-
iter.next(),
233-
Some(Parameter {
234-
key: b"wiz",
235-
value: None,
236-
})
296+
Some(Parameter::new_kv(b"baz", b"fuz".as_slice()))
237297
);
238298

299+
assert_eq!(iter.next(), Some(Parameter::new_key(b"wiz")));
239300
assert_eq!(iter.next(), None);
301+
302+
// Test the find API
303+
assert_eq!(kargs.find("foo").unwrap().value.unwrap(), b"bar,bar2");
304+
assert!(kargs.find("nothing").is_none());
240305
}
241306

242307
#[test]
@@ -248,4 +313,25 @@ mod tests {
248313
// tests are running
249314
assert!(kargs.iter().count() > 0);
250315
}
316+
317+
#[test]
318+
fn test_kargs_find_dash_hyphen() {
319+
let kargs = Cmdline::from(b"a-b=1 a_b=2".as_slice());
320+
// find should find the first one, which is a-b=1
321+
let p = kargs.find("a_b").unwrap();
322+
assert_eq!(p.key.0, b"a-b");
323+
assert_eq!(p.value.unwrap(), b"1");
324+
let p = kargs.find("a-b").unwrap();
325+
assert_eq!(p.key.0, b"a-b");
326+
assert_eq!(p.value.unwrap(), b"1");
327+
328+
let kargs = Cmdline::from(b"a_b=2 a-b=1".as_slice());
329+
// find should find the first one, which is a_b=2
330+
let p = kargs.find("a_b").unwrap();
331+
assert_eq!(p.key.0, b"a_b");
332+
assert_eq!(p.value.unwrap(), b"2");
333+
let p = kargs.find("a-b").unwrap();
334+
assert_eq!(p.key.0, b"a_b");
335+
assert_eq!(p.value.unwrap(), b"2");
336+
}
251337
}

0 commit comments

Comments
 (0)