use std::ops::Deref;
use crate::{Action, bytes};
use anyhow::Result;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Cmdline<'a>(bytes::Cmdline<'a>);
pub type CmdlineOwned = Cmdline<'static>;
impl<'a, T: AsRef<str> + ?Sized> From<&'a T> for Cmdline<'a> {
fn from(input: &'a T) -> Self {
Self(bytes::Cmdline::from(input.as_ref().as_bytes()))
}
}
impl From<String> for CmdlineOwned {
fn from(input: String) -> Self {
Self(bytes::Cmdline::from(input.into_bytes()))
}
}
#[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::Item> {
self.0.next().map(Parameter::from_bytes)
}
}
#[derive(Debug)]
pub struct CmdlineIterStr<'a>(bytes::CmdlineIterBytes<'a>);
impl<'a> Iterator for CmdlineIterStr<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let bytes = self.0.next()?;
Some(str::from_utf8(bytes).expect("Parameter bytes come from valid UTF-8 cmdline"))
}
}
impl<'a> Cmdline<'a> {
pub fn new() -> CmdlineOwned {
Cmdline::default()
}
pub fn from_proc() -> Result<Self> {
let cmdline = std::fs::read("/proc/cmdline")?;
str::from_utf8(&cmdline)?;
Ok(Self(bytes::Cmdline::from(cmdline)))
}
pub fn iter(&'a self) -> CmdlineIter<'a> {
CmdlineIter(self.0.iter())
}
pub fn iter_str(&self) -> CmdlineIterStr<'_> {
CmdlineIterStr(self.0.iter_bytes())
}
pub fn find<T: AsRef<str> + ?Sized>(&'a self, key: &T) -> Option<Parameter<'a>> {
let key = ParameterKey::from(key.as_ref());
self.iter().find(|p| p.key() == key)
}
pub fn find_all_starting_with<T: AsRef<str> + ?Sized>(
&'a self,
prefix: &'a T,
) -> impl Iterator<Item = Parameter<'a>> + 'a {
self.iter()
.filter(move |p| p.key().starts_with(prefix.as_ref()))
}
pub fn value_of<T: AsRef<str> + ?Sized>(&'a self, key: &T) -> Option<&'a str> {
self.0.value_of(key.as_ref().as_bytes()).map(|v| {
str::from_utf8(v).expect("We only construct the underlying bytes from valid UTF-8")
})
}
pub fn require_value_of<T: AsRef<str> + ?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}'"))
}
pub fn add(&mut self, param: &Parameter) -> Action {
self.0.add(¶m.0)
}
pub fn add_or_modify(&mut self, param: &Parameter) -> Action {
self.0.add_or_modify(¶m.0)
}
pub fn remove(&mut self, key: &ParameterKey) -> bool {
self.0.remove(&key.0)
}
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 {
str::from_utf8(&self.0).expect("We only construct the underlying bytes from valid UTF-8")
}
}
impl<'a, T> AsRef<T> for Cmdline<'a>
where
T: ?Sized,
<Cmdline<'a> as Deref>::Target: AsRef<T>,
{
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<Parameter<'other>> for Cmdline<'a> {
fn extend<T: IntoIterator<Item = Parameter<'other>>>(&mut self, iter: T) {
for param in iter {
self.add(¶m);
}
}
}
#[derive(Clone, Debug, Eq)]
pub struct ParameterKey<'a>(bytes::ParameterKey<'a>);
impl Deref for ParameterKey<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
str::from_utf8(&self.0).expect("We only construct the underlying bytes from valid UTF-8")
}
}
impl<'a, T> AsRef<T> for ParameterKey<'a>
where
T: ?Sized,
<ParameterKey<'a> as Deref>::Target: AsRef<T>,
{
fn as_ref(&self) -> &T {
self.deref().as_ref()
}
}
impl<'a> ParameterKey<'a> {
fn from_bytes(input: bytes::ParameterKey<'a>) -> Self {
Self(input)
}
}
impl<'a, T: AsRef<str> + ?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<'_> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
#[derive(Clone, Debug, Eq)]
pub struct Parameter<'a>(bytes::Parameter<'a>);
impl<'a> Parameter<'a> {
pub fn parse<T: AsRef<str> + ?Sized>(input: &'a T) -> Option<Self> {
bytes::Parameter::parse(input.as_ref().as_bytes()).map(Self)
}
fn from_bytes(bytes: bytes::Parameter<'a>) -> Self {
Self(bytes)
}
pub fn key(&'a self) -> ParameterKey<'a> {
ParameterKey::from_bytes(self.0.key())
}
pub fn value(&'a self) -> Option<&'a str> {
self.0.value().map(|p| {
str::from_utf8(p).expect("We only construct the underlying bytes from valid UTF-8")
})
}
}
impl<'a> TryFrom<bytes::Parameter<'a>> for Parameter<'a> {
type Error = anyhow::Error;
fn try_from(bytes: bytes::Parameter<'a>) -> Result<Self, Self::Error> {
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 {
str::from_utf8(&self.0).expect("We only construct the underlying bytes from valid UTF-8")
}
}
impl<'a, T> AsRef<T> for Parameter<'a>
where
T: ?Sized,
<Parameter<'a> as Deref>::Target: AsRef<T>,
{
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::*;
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);
let p = Parameter::parse("foo=bar baz").unwrap();
assert_eq!(p.key(), "foo".into());
assert_eq!(p.value(), Some("bar"));
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() {
assert_eq!(param("foo").to_string(), "foo");
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() {
let p = Parameter::parse("foo bar=baz").unwrap();
assert_eq!(p.key(), "foo".into());
assert_eq!(p.value(), None);
}
#[test]
fn test_parameter_pathological() {
let p = param("\"foo\"=bar");
assert_eq!(p.key(), ParameterKey::from("foo\""));
assert_eq!(p.value(), Some("bar"));
assert_ne!(p, param("foo=bar"));
let p = param("\"foo=\"bar");
assert_eq!(p.key(), ParameterKey::from("foo"));
assert_eq!(p.value(), Some("bar"));
assert_eq!(p, param("foo=bar"));
let p = param("foo=\"internal\"quotes\"are\"ok\"");
assert_eq!(p.value(), Some("internal\"quotes\"are\"ok"));
}
#[test]
fn test_parameter_equality() {
let foo = param("foo");
let bar = param("foobar");
assert_ne!(foo, bar);
assert_ne!(bar, foo);
let dashes = param("a-delimited-param");
let underscores = param("a_delimited_param");
assert_eq!(dashes, underscores);
let dashes = param("a-delimited-param=same_values");
let underscores = param("a_delimited_param=same_values");
assert_eq!(dashes, underscores);
let dashes = param("a-delimited-param=different_values");
let underscores = param("a_delimited_param=DiFfErEnT_valUEZ");
assert_ne!(dashes, underscores);
let switch = param("same_key");
let keyvalue = param("same_key=but_with_a_value");
assert_ne!(switch, keyvalue);
}
#[test]
fn test_parameter_tryfrom() {
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);
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()));
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"
);
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() {
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);
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());
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);
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();
assert!(kargs.iter().count() > 0);
}
#[test]
fn test_kargs_find_dash_hyphen() {
let kargs = Cmdline::from("a-b=1 a_b=2");
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");
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");
assert_eq!(kargs.value_of("foo"), Some("bar"));
assert_eq!(kargs.value_of("baz"), Some("qux"));
assert_eq!(kargs.value_of("switch"), None);
assert_eq!(kargs.value_of("missing"), None);
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");
assert_eq!(kargs.require_value_of("foo").unwrap(), "bar");
assert_eq!(kargs.require_value_of("baz").unwrap(), "qux");
let err = kargs.require_value_of("switch").unwrap_err();
assert!(
err.to_string()
.contains("Failed to find kernel argument 'switch'")
);
let err = kargs.require_value_of("missing").unwrap_err();
assert!(
err.to_string()
.contains("Failed to find kernel argument 'missing'")
);
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");
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);
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);
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");
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);
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);
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");
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);
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");
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);
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);
drop(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(), 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() {
assert_eq!(
Cmdline::from("foo bar-with-delim=\"with spaces\""),
Cmdline::from("\"bar_with_delim=with spaces\" foo")
);
assert_ne!(Cmdline::from("foo"), Cmdline::from("foo foo"));
assert_ne!(Cmdline::from("foo foo"), Cmdline::from("foo"));
assert_ne!(Cmdline::from("a a b"), Cmdline::from("a b b"));
}
}