Skip to content

Commit 30e9dc0

Browse files
cgwaltersjeckersb
authored andcommitted
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 30e9dc0

File tree

2 files changed

+133
-41
lines changed

2 files changed

+133
-41
lines changed

crates/lib/src/install.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,12 +1663,10 @@ 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()
16741672
.filter(|arg| arg.key.starts_with(crate::kernel::INITRD_ARG_PREFIX));

crates/lib/src/kernel.rs

Lines changed: 131 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,99 @@ 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>(&'a [u8]);
72+
73+
impl<'a> std::ops::Deref for ParameterKey<'a> {
74+
type Target = [u8];
75+
76+
fn deref(&self) -> &'a Self::Target {
77+
self.0
78+
}
79+
}
80+
81+
impl<'a> From<&'a [u8]> for ParameterKey<'a> {
82+
fn from(value: &'a [u8]) -> Self {
83+
Self(value)
84+
}
85+
}
86+
87+
/// A single kernel command line parameter.
88+
#[derive(Debug, PartialEq, Eq)]
3889
pub(crate) struct Parameter<'a> {
39-
pub key: &'a [u8],
90+
/// The parameter key as raw bytes
91+
pub key: ParameterKey<'a>,
92+
/// The parameter value as raw bytes, if present
4093
pub value: Option<&'a [u8]>,
4194
}
4295

4396
impl<'a> Parameter<'a> {
97+
/// Create a new parameter with the provided key and value.
98+
#[cfg(test)]
99+
pub fn new_kv<'k: 'a, 'v: 'a>(key: &'k [u8], value: &'v [u8]) -> Self {
100+
Self {
101+
key: ParameterKey(key),
102+
value: Some(value),
103+
}
104+
}
105+
106+
/// Create a new parameter with the provided key.
107+
#[cfg(test)]
108+
pub fn new_key(key: &'a [u8]) -> Self {
109+
Self {
110+
key: ParameterKey(key),
111+
value: None,
112+
}
113+
}
114+
115+
/// Returns the key as a lossy UTF-8 string.
116+
///
117+
/// Invalid UTF-8 sequences are replaced with the Unicode replacement character.
44118
pub fn key_lossy(&self) -> String {
45-
String::from_utf8_lossy(self.key).to_string()
119+
String::from_utf8_lossy(&self.key).to_string()
46120
}
47121

122+
/// Returns the value as a lossy UTF-8 string.
123+
///
124+
/// Invalid UTF-8 sequences are replaced with the Unicode replacement character.
125+
/// Returns an empty string if no value is present.
48126
pub fn value_lossy(&self) -> String {
49127
String::from_utf8_lossy(self.value.unwrap_or(&[])).to_string()
50128
}
51129
}
52130

53131
impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for Parameter<'a> {
132+
/// Parses a parameter from raw bytes.
133+
///
134+
/// Splits on the first `=` character to separate key and value.
135+
/// Strips only the outermost pair of double quotes from values.
136+
/// If no `=` is found, treats the entire input as a key-only parameter.
54137
fn from(input: &'a T) -> Self {
55138
let input = input.as_ref();
56139
let equals = input.iter().position(|b| *b == b'=');
57140

58141
match equals {
59142
None => Self {
60-
key: input,
143+
key: ParameterKey(input),
61144
value: None,
62145
},
63146
Some(i) => {
64147
let (key, mut value) = input.split_at(i);
148+
let key = ParameterKey(key);
65149

66150
// skip `=`, we know it's the first byte because we
67151
// found it above
@@ -83,7 +167,11 @@ impl<'a, T: AsRef<[u8]> + ?Sized> From<&'a T> for Parameter<'a> {
83167
}
84168
}
85169

86-
impl PartialEq for Parameter<'_> {
170+
impl PartialEq for ParameterKey<'_> {
171+
/// Compares two parameter keys for equality.
172+
///
173+
/// Keys are compared with dashes and underscores treated as equivalent.
174+
/// This comparison is case-sensitive.
87175
fn eq(&self, other: &Self) -> bool {
88176
let dedashed = |&c: &u8| {
89177
if c == b'-' {
@@ -97,21 +185,18 @@ impl PartialEq for Parameter<'_> {
97185
//
98186
// For example, "foo" == "foobar" since the zipped iterator
99187
// 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-
}
188+
let our_iter = self.0.iter().map(dedashed);
189+
let other_iter = other.0.iter().map(dedashed);
190+
our_iter.eq(other_iter)
111191
}
112192
}
113193

114194
impl std::fmt::Display for Parameter<'_> {
195+
/// Formats the parameter for display.
196+
///
197+
/// Key-only parameters are displayed as just the key.
198+
/// Key-value parameters are displayed as `key=value`.
199+
/// Values containing whitespace are automatically quoted.
115200
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
116201
let key = self.key_lossy();
117202

@@ -136,11 +221,11 @@ mod tests {
136221
#[test]
137222
fn test_parameter_simple() {
138223
let switch = Parameter::from("foo");
139-
assert_eq!(switch.key, b"foo");
224+
assert_eq!(switch.key.0, b"foo");
140225
assert_eq!(switch.value, None);
141226

142227
let kv = Parameter::from("bar=baz");
143-
assert_eq!(kv.key, b"bar");
228+
assert_eq!(kv.key.0, b"bar");
144229
assert_eq!(kv.value, Some(b"baz".as_slice()));
145230
}
146231

@@ -156,7 +241,7 @@ mod tests {
156241

157242
// quotes don't get removed from keys
158243
let p = Parameter::from("\"\"\"");
159-
assert_eq!(p.key, b"\"\"\"");
244+
assert_eq!(p.key.0, b"\"\"\"");
160245

161246
// quotes only get stripped from the absolute ends of values
162247
let p = Parameter::from("foo=\"internal \" quotes \" are ok\"");
@@ -212,31 +297,19 @@ mod tests {
212297
let kargs = Cmdline::from(b"foo=bar,bar2 baz=fuz wiz".as_slice());
213298
let mut iter = kargs.iter();
214299

215-
assert_eq!(
216-
iter.next(),
217-
Some(Parameter {
218-
key: b"foo",
219-
value: Some(b"bar,bar2".as_slice())
220-
})
221-
);
222-
223-
assert_eq!(
224-
iter.next(),
225-
Some(Parameter {
226-
key: b"baz",
227-
value: Some(b"fuz".as_slice())
228-
})
229-
);
300+
assert_eq!(iter.next(), Some(Parameter::new_kv(b"foo", b"bar,bar2")));
230301

231302
assert_eq!(
232303
iter.next(),
233-
Some(Parameter {
234-
key: b"wiz",
235-
value: None,
236-
})
304+
Some(Parameter::new_kv(b"baz", b"fuz".as_slice()))
237305
);
238306

307+
assert_eq!(iter.next(), Some(Parameter::new_key(b"wiz")));
239308
assert_eq!(iter.next(), None);
309+
310+
// Test the find API
311+
assert_eq!(kargs.find("foo").unwrap().value.unwrap(), b"bar,bar2");
312+
assert!(kargs.find("nothing").is_none());
240313
}
241314

242315
#[test]
@@ -248,4 +321,25 @@ mod tests {
248321
// tests are running
249322
assert!(kargs.iter().count() > 0);
250323
}
324+
325+
#[test]
326+
fn test_kargs_find_dash_hyphen() {
327+
let kargs = Cmdline::from(b"a-b=1 a_b=2".as_slice());
328+
// find should find the first one, which is a-b=1
329+
let p = kargs.find("a_b").unwrap();
330+
assert_eq!(p.key.0, b"a-b");
331+
assert_eq!(p.value.unwrap(), b"1");
332+
let p = kargs.find("a-b").unwrap();
333+
assert_eq!(p.key.0, b"a-b");
334+
assert_eq!(p.value.unwrap(), b"1");
335+
336+
let kargs = Cmdline::from(b"a_b=2 a-b=1".as_slice());
337+
// find should find the first one, which is a_b=2
338+
let p = kargs.find("a_b").unwrap();
339+
assert_eq!(p.key.0, b"a_b");
340+
assert_eq!(p.value.unwrap(), b"2");
341+
let p = kargs.find("a-b").unwrap();
342+
assert_eq!(p.key.0, b"a_b");
343+
assert_eq!(p.value.unwrap(), b"2");
344+
}
251345
}

0 commit comments

Comments
 (0)