linux-cmdline 0.1.2

Parses and manipulates Linux kernel cmdline strings
Documentation
// cmdline.rs
//
// Copyright 2019 Alberto Ruiz <aruiz@gnome.org>
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// SPDX-License-Identifier: MPL-2.0

#![cfg_attr(not(feature = "std"), no_std)]
#![cfg(not(feature = "std"))]
extern crate alloc;

#[cfg(not(feature = "std"))]
use alloc::string::String;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;

/// Represents a set of values for a cmdline parameter
pub type CmdlineValue = Vec<Option<String>>;
/// Represents a parameter and all its values
pub type CmdlineParam = (String, CmdlineValue);
/// Represents a full or partial cmdline parameter set
pub type Cmdline = Vec<CmdlineParam>;

// Basic dictionary like methods for Cmdline
trait VecOfParamsAsDict {
    fn get<'a>(&'a mut self, key: &String) -> Option<&'a mut CmdlineValue>;
    fn take_from_key(&mut self, key: &str) -> Option<CmdlineValue>;
    fn entry_or_insert<'a>(&'a mut self, key: String, insert: CmdlineValue)
        -> &'a mut CmdlineValue;
}

impl VecOfParamsAsDict for Cmdline {
    fn get<'a>(&'a mut self, key: &String) -> Option<&'a mut CmdlineValue> {
        for item in self {
            if &item.0 == key {
                return Some(&mut item.1);
            }
        }
        None
    }

    fn take_from_key(&mut self, key: &str) -> Option<CmdlineValue> {
        match { self.iter().position(|x| &x.0 == key) } {
            Some(pos) => Some(self.remove(pos).1),
            None => None,
        }
    }

    fn entry_or_insert<'a>(
        &'a mut self,
        key: String,
        insert: CmdlineValue,
    ) -> &'a mut CmdlineValue {
        let pos = { self.iter().position(|(k, _)| k == &key) };

        match pos {
            Some(index) => &mut self[index].1,
            None => {
                self.push((key, insert));
                let len = { self.len() };
                &mut self[len - 1].1
            }
        }
    }
}

/// Basic Cmdline content operations
pub trait CmdlineContent {
    /// Parses a UTF8 &str and returns a ```Cmdline``` object
    ///
    /// # Arguments
    ///
    /// - ```buffer: &str```: the cmdline string
    ///
    /// # Returns
    ///
    /// A ```Cmdline``` object that represents the contents of the input string
    ///
    /// # Errors
    ///
    /// Returns a (usize, &'static str) error pointing at the line position of the error as well as an error message

    fn parse(buffer: &str) -> Result<Cmdline, (usize, &'static str)>;
    /// Renders a Cmdline object into a valid cmdline string
    ///
    /// # Returns
    ///
    /// A String that represents the ```Cmdline``` object
    ///
    /// # Errors
    ///
    /// Returns error if a parameter has bogus values
    fn render(&self) -> Result<String, &'static str>;

    /// Adds a parameter to a ```Cmdline```
    ///
    /// # Arguments
    /// - ```key: String```: The parameter name
    /// - ```value: Option<String>```: An optional value
    fn add_param(&mut self, key: String, value: Option<String>);
}

impl CmdlineContent for Cmdline {
    fn parse(buffer: &str) -> Result<Cmdline, (usize, &'static str)> {
        #[derive(Debug)]
        enum Scope {
            InValueQuoted,
            InValueUnquoted,
            InKey,
            InEqual,
            InSpace,
        }

        let mut key = String::new();
        let mut value = String::new();

        let mut result = Cmdline::new();
        let mut scope = Scope::InSpace;

        let mut i: usize = 0;
        for c in buffer.chars() {
            match c {
                ' ' => match scope {
                    Scope::InValueQuoted => {
                        value.push(c);
                    }
                    Scope::InValueUnquoted => {
                        result.add_param(key.drain(..).collect(), Some(value.drain(..).collect()));
                        scope = Scope::InSpace;
                    }
                    Scope::InSpace => {}
                    Scope::InEqual => {
                        return Err((i, "empty parameter value"));
                    }
                    Scope::InKey => {
                        result.add_param(key.drain(..).collect(), None);
                    }
                },
                '"' => match scope {
                    Scope::InValueQuoted => {
                        scope = Scope::InValueUnquoted;
                    }
                    Scope::InEqual => {
                        scope = Scope::InValueQuoted;
                    }
                    Scope::InKey => {
                        return Err((i, "quote in parameter name"));
                    }
                    Scope::InValueUnquoted => {
                        scope = Scope::InValueQuoted;
                    }
                    Scope::InSpace => {
                        return Err((i, "quote after unquoted space"));
                    }
                },
                '=' => match scope {
                    Scope::InKey => {
                        scope = Scope::InEqual;
                    }
                    Scope::InValueQuoted | Scope::InValueUnquoted => {
                        value.push(c);
                    }
                    Scope::InEqual => {
                        scope = Scope::InValueUnquoted;
                        value.push(c)
                    }
                    Scope::InSpace => {
                        return Err((i, "equals after space"));
                    }
                },
                _ => match scope {
                    Scope::InKey => {
                        key.push(c);
                    }
                    Scope::InValueQuoted => {
                        value.push(c);
                    }
                    Scope::InValueUnquoted => {
                        value.push(c);
                    }
                    Scope::InSpace => {
                        scope = Scope::InKey;
                        key.push(c);
                    }
                    Scope::InEqual => {
                        scope = Scope::InValueUnquoted;
                        value.push(c);
                    }
                },
            };
            i += 1;
        }

        match scope {
            Scope::InKey => {
                result.add_param(key.drain(..).collect(), None);
            }
            Scope::InValueQuoted => {
                return Err((i, "unclosed quote in parameter value"));
            }
            Scope::InValueUnquoted => {
                result.add_param(key.drain(..).collect(), Some(value.drain(..).collect()))
            }
            Scope::InEqual => {
                return Err((i, "empty parameter value"));
            }
            Scope::InSpace => {}
        }

        Ok(result)
    }

    fn add_param(&mut self, key: String, value: Option<String>) {
        //FIXME: Prevent malformed
        let vec = self.entry_or_insert(key, Vec::new());
        vec.push(value);
    }

    fn render(&self) -> Result<String, &'static str> {
        let mut render = String::new();
        for (param, values) in self {
            for value in values {
                match value {
                    Some(value) => {
                        render.push_str(&param);
                        render.push('=');
                        if value.contains('"') {
                            return Err("cannot escape quote character");
                        }
                        if value.contains(' ') {
                            render.push('"');
                            render.push_str(&value);
                            render.push('"');
                        } else {
                            render.push_str(&value);
                        }
                    }
                    _ => {
                        render.push_str(&param);
                    }
                }
                render.push(' ');
            }
        }
        render.pop();

        Ok(render)
    }
}

#[cfg(test)]
mod cmdline_tests {
    use super::*;
    #[cfg(not(feature = "std"))]
    use alloc::vec;

    #[test]
    fn cmdline_parse_test() {
        let test = "a=test";
        assert_eq!(
            Cmdline::parse(test),
            Ok(vec![(String::from("a"), vec![Some(String::from("test"))])])
        );

        let test = "a=te\"s\"t";
        assert_eq!(
            Cmdline::parse(test),
            Ok(vec![(String::from("a"), vec![Some(String::from("test"))])])
        );

        let test = "a b c";
        assert_eq!(
            Cmdline::parse(test),
            Ok(vec![
                (String::from("a"), vec![None]),
                (String::from("b"), vec![None]),
                (String::from("c"), vec![None])
            ])
        );

        let test = "a=test a a=test2 c a=test3";
        assert_eq!(
            Cmdline::parse(test),
            Ok(vec![
                (
                    String::from("a"),
                    vec![
                        Some(String::from("test")),
                        None,
                        Some(String::from("test2")),
                        Some(String::from("test3"))
                    ]
                ),
                (String::from("c"), vec![None])
            ])
        );

        let test = "a=3 =asd";
        assert!(Cmdline::parse(test).is_err());

        let test = "a=3 b= ";
        assert!(Cmdline::parse(test).is_err());

        let test = "a=3 b=";
        assert!(Cmdline::parse(test).is_err());

        let test = "\"quoted param\"=should_error";
        assert!(Cmdline::parse(test).is_err());

        let test = "quot\"ed param\"=should_error";
        assert!(Cmdline::parse(test).is_err());

        let test = "arg1 \"quoted param\"=should_error";
        assert!(Cmdline::parse(test).is_err());

        let test = "param=\"unclosed quote";
        assert!(Cmdline::parse(test).is_err());
    }
}