1 // Copyright 2017 The Chromium OS Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 //! Helper for creating valid kernel command line strings.
6 
7 use std::fmt::{self, Display};
8 use std::result;
9 
10 /// The error type for command line building operations.
11 #[derive(PartialEq, Debug)]
12 pub enum Error {
13     /// Operation would have resulted in a non-printable ASCII character.
14     InvalidAscii,
15     /// Key/Value Operation would have had a space in it.
16     HasSpace,
17     /// Key/Value Operation would have had an equals sign in it.
18     HasEquals,
19     /// Operation would have made the command line too large.
20     TooLarge,
21 }
22 
23 impl Display for Error {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result24     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
25         use self::Error::*;
26 
27         let description = match self {
28             InvalidAscii => "string contains non-printable ASCII character",
29             HasSpace => "string contains a space",
30             HasEquals => "string contains an equals sign",
31             TooLarge => "inserting string would make command line too long",
32         };
33 
34         write!(f, "{}", description)
35     }
36 }
37 
38 /// Specialized Result type for command line operations.
39 pub type Result<T> = result::Result<T, Error>;
40 
valid_char(c: char) -> bool41 fn valid_char(c: char) -> bool {
42     matches!(c, ' '..='~')
43 }
44 
valid_str(s: &str) -> Result<()>45 fn valid_str(s: &str) -> Result<()> {
46     if s.chars().all(valid_char) {
47         Ok(())
48     } else {
49         Err(Error::InvalidAscii)
50     }
51 }
52 
valid_element(s: &str) -> Result<()>53 fn valid_element(s: &str) -> Result<()> {
54     if !s.chars().all(valid_char) {
55         Err(Error::InvalidAscii)
56     } else if s.contains(' ') {
57         Err(Error::HasSpace)
58     } else if s.contains('=') {
59         Err(Error::HasEquals)
60     } else {
61         Ok(())
62     }
63 }
64 
65 /// A builder for a kernel command line string that validates the string as its being built. A
66 /// `CString` can be constructed from this directly using `CString::new`.
67 pub struct Cmdline {
68     line: String,
69     capacity: usize,
70 }
71 
72 impl Cmdline {
73     /// Constructs an empty Cmdline with the given capacity, which includes the nul terminator.
74     /// Capacity must be greater than 0.
new(capacity: usize) -> Cmdline75     pub fn new(capacity: usize) -> Cmdline {
76         assert_ne!(capacity, 0);
77         Cmdline {
78             line: String::new(),
79             capacity,
80         }
81     }
82 
has_capacity(&self, more: usize) -> Result<()>83     fn has_capacity(&self, more: usize) -> Result<()> {
84         let needs_space = if self.line.is_empty() { 0 } else { 1 };
85         if self.line.len() + more + needs_space < self.capacity {
86             Ok(())
87         } else {
88             Err(Error::TooLarge)
89         }
90     }
91 
start_push(&mut self)92     fn start_push(&mut self) {
93         if !self.line.is_empty() {
94             self.line.push(' ');
95         }
96     }
97 
end_push(&mut self)98     fn end_push(&mut self) {
99         // This assert is always true because of the `has_capacity` check that each insert method
100         // uses.
101         assert!(self.line.len() < self.capacity);
102     }
103 
104     /// Validates and inserts a key value pair into this command line
insert<T: AsRef<str>>(&mut self, key: T, val: T) -> Result<()>105     pub fn insert<T: AsRef<str>>(&mut self, key: T, val: T) -> Result<()> {
106         let k = key.as_ref();
107         let v = val.as_ref();
108 
109         valid_element(k)?;
110         valid_element(v)?;
111         self.has_capacity(k.len() + v.len() + 1)?;
112 
113         self.start_push();
114         self.line.push_str(k);
115         self.line.push('=');
116         self.line.push_str(v);
117         self.end_push();
118 
119         Ok(())
120     }
121 
122     /// Validates and inserts a string to the end of the current command line
insert_str<T: AsRef<str>>(&mut self, slug: T) -> Result<()>123     pub fn insert_str<T: AsRef<str>>(&mut self, slug: T) -> Result<()> {
124         let s = slug.as_ref();
125         valid_str(s)?;
126 
127         self.has_capacity(s.len())?;
128 
129         self.start_push();
130         self.line.push_str(s);
131         self.end_push();
132 
133         Ok(())
134     }
135 
136     /// Returns the cmdline in progress without nul termination
as_str(&self) -> &str137     pub fn as_str(&self) -> &str {
138         self.line.as_str()
139     }
140 }
141 
142 impl Into<Vec<u8>> for Cmdline {
into(self) -> Vec<u8>143     fn into(self) -> Vec<u8> {
144         self.line.into_bytes()
145     }
146 }
147 
148 #[cfg(test)]
149 mod tests {
150     use super::*;
151     use std::ffi::CString;
152 
153     #[test]
insert_hello_world()154     fn insert_hello_world() {
155         let mut cl = Cmdline::new(100);
156         assert_eq!(cl.as_str(), "");
157         assert!(cl.insert("hello", "world").is_ok());
158         assert_eq!(cl.as_str(), "hello=world");
159 
160         let s = CString::new(cl).expect("failed to create CString from Cmdline");
161         assert_eq!(s, CString::new("hello=world").unwrap());
162     }
163 
164     #[test]
insert_multi()165     fn insert_multi() {
166         let mut cl = Cmdline::new(100);
167         assert!(cl.insert("hello", "world").is_ok());
168         assert!(cl.insert("foo", "bar").is_ok());
169         assert_eq!(cl.as_str(), "hello=world foo=bar");
170     }
171 
172     #[test]
insert_space()173     fn insert_space() {
174         let mut cl = Cmdline::new(100);
175         assert_eq!(cl.insert("a ", "b"), Err(Error::HasSpace));
176         assert_eq!(cl.insert("a", "b "), Err(Error::HasSpace));
177         assert_eq!(cl.insert("a ", "b "), Err(Error::HasSpace));
178         assert_eq!(cl.insert(" a", "b"), Err(Error::HasSpace));
179         assert_eq!(cl.as_str(), "");
180     }
181 
182     #[test]
insert_equals()183     fn insert_equals() {
184         let mut cl = Cmdline::new(100);
185         assert_eq!(cl.insert("a=", "b"), Err(Error::HasEquals));
186         assert_eq!(cl.insert("a", "b="), Err(Error::HasEquals));
187         assert_eq!(cl.insert("a=", "b "), Err(Error::HasEquals));
188         assert_eq!(cl.insert("=a", "b"), Err(Error::HasEquals));
189         assert_eq!(cl.insert("a", "=b"), Err(Error::HasEquals));
190         assert_eq!(cl.as_str(), "");
191     }
192 
193     #[test]
insert_emoji()194     fn insert_emoji() {
195         let mut cl = Cmdline::new(100);
196         assert_eq!(cl.insert("heart", "��"), Err(Error::InvalidAscii));
197         assert_eq!(cl.insert("��", "love"), Err(Error::InvalidAscii));
198         assert_eq!(cl.as_str(), "");
199     }
200 
201     #[test]
insert_string()202     fn insert_string() {
203         let mut cl = Cmdline::new(13);
204         assert_eq!(cl.as_str(), "");
205         assert!(cl.insert_str("noapic").is_ok());
206         assert_eq!(cl.as_str(), "noapic");
207         assert!(cl.insert_str("nopci").is_ok());
208         assert_eq!(cl.as_str(), "noapic nopci");
209     }
210 
211     #[test]
insert_too_large()212     fn insert_too_large() {
213         let mut cl = Cmdline::new(4);
214         assert_eq!(cl.insert("hello", "world"), Err(Error::TooLarge));
215         assert_eq!(cl.insert("a", "world"), Err(Error::TooLarge));
216         assert_eq!(cl.insert("hello", "b"), Err(Error::TooLarge));
217         assert!(cl.insert("a", "b").is_ok());
218         assert_eq!(cl.insert("a", "b"), Err(Error::TooLarge));
219         assert_eq!(cl.insert_str("a"), Err(Error::TooLarge));
220         assert_eq!(cl.as_str(), "a=b");
221 
222         let mut cl = Cmdline::new(10);
223         assert!(cl.insert("ab", "ba").is_ok()); // adds 5 length
224         assert_eq!(cl.insert("c", "da"), Err(Error::TooLarge)); // adds 5 (including space) length
225         assert!(cl.insert("c", "d").is_ok()); // adds 4 (including space) length
226     }
227 }
228