1use std::ffi::CString;
12use std::fmt;
13use std::result;
14
15use vm_memory::{Address, GuestAddress, GuestUsize};
16
17const INIT_ARGS_SEPARATOR: &str = " -- ";
18
19#[derive(Debug, PartialEq, Eq)]
21pub enum Error {
22 NullTerminator,
24 NoBootArgsInserted,
26 InvalidCapacity,
28 InvalidAscii,
30 HasSpace,
32 HasEquals,
34 MissingVal(String),
36 MmioSize,
38 TooLarge,
40}
41
42impl fmt::Display for Error {
43 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44 match *self {
45 Error::NullTerminator => {
46 write!(f, "Null terminator detected in the command line structure.")
47 }
48 Error::NoBootArgsInserted => write!(f, "Cmdline cannot contain only init args."),
49 Error::InvalidCapacity => write!(f, "Invalid cmdline capacity provided."),
50 Error::InvalidAscii => write!(f, "String contains a non-printable ASCII character."),
51 Error::HasSpace => write!(f, "String contains a space."),
52 Error::HasEquals => write!(f, "String contains an equals sign."),
53 Error::MissingVal(ref k) => write!(f, "Missing value for key {}.", k),
54 Error::MmioSize => write!(
55 f,
56 "0-sized virtio MMIO device passed to the kernel command line builder."
57 ),
58 Error::TooLarge => write!(f, "Inserting string would make command line too long."),
59 }
60 }
61}
62
63impl std::error::Error for Error {}
64
65pub type Result<T> = result::Result<T, Error>;
69
70fn valid_char(c: char) -> bool {
71 matches!(c, ' '..='~')
72}
73
74fn valid_str(s: &str) -> Result<()> {
75 if s.chars().all(valid_char) {
76 Ok(())
77 } else {
78 Err(Error::InvalidAscii)
79 }
80}
81
82fn valid_element(s: &str) -> Result<()> {
83 if !s.chars().all(valid_char) {
84 Err(Error::InvalidAscii)
85 } else if s.contains(' ') {
86 Err(Error::HasSpace)
87 } else if s.contains('=') {
88 Err(Error::HasEquals)
89 } else {
90 Ok(())
91 }
92}
93
94#[derive(Clone, Debug)]
106pub struct Cmdline {
107 boot_args: String,
108 init_args: String,
109 capacity: usize,
110}
111
112impl Cmdline {
113 pub fn new(capacity: usize) -> Result<Cmdline> {
127 if capacity == 0 {
128 return Err(Error::InvalidCapacity);
129 }
130
131 Ok(Cmdline {
132 boot_args: String::new(),
133 init_args: String::new(),
134 capacity,
135 })
136 }
137
138 pub fn insert<T: AsRef<str>>(&mut self, key: T, val: T) -> Result<()> {
155 let k = key.as_ref();
156 let v = val.as_ref();
157
158 valid_element(k)?;
159 valid_element(v)?;
160
161 let kv_str = format!("{}={}", k, v);
162
163 self.insert_str(kv_str)
164 }
165
166 pub fn insert_multiple<T: AsRef<str>>(&mut self, key: T, vals: &[T]) -> Result<()> {
187 let k = key.as_ref();
188
189 valid_element(k)?;
190 if vals.is_empty() {
191 return Err(Error::MissingVal(k.to_string()));
192 }
193
194 let kv_str = format!(
195 "{}={}",
196 k,
197 vals.iter()
198 .map(|v| -> Result<&str> {
199 valid_element(v.as_ref())?;
200 Ok(v.as_ref())
201 })
202 .collect::<Result<Vec<&str>>>()?
203 .join(",")
204 );
205
206 self.insert_str(kv_str)
207 }
208
209 pub fn insert_str<T: AsRef<str>>(&mut self, slug: T) -> Result<()> {
226 let s = slug.as_ref().trim();
229 valid_str(s)?;
230
231 let mut cmdline_size = self.get_null_terminated_representation_size();
234
235 if !self.boot_args.is_empty() {
237 cmdline_size = cmdline_size.checked_add(1).ok_or(Error::TooLarge)?;
238 }
239
240 cmdline_size = cmdline_size.checked_add(s.len()).ok_or(Error::TooLarge)?;
242
243 if cmdline_size > self.capacity {
244 return Err(Error::TooLarge);
245 }
246
247 if !self.boot_args.is_empty() {
249 self.boot_args.push(' ');
250 }
251
252 self.boot_args.push_str(s);
253
254 Ok(())
255 }
256
257 pub fn insert_init_args<T: AsRef<str>>(&mut self, slug: T) -> Result<()> {
278 let s = slug.as_ref().trim();
281 valid_str(s)?;
282
283 let mut cmdline_size = self.get_null_terminated_representation_size();
286
287 cmdline_size = cmdline_size
289 .checked_add(if self.init_args.is_empty() {
290 INIT_ARGS_SEPARATOR.len()
291 } else {
292 1
293 })
294 .ok_or(Error::TooLarge)?;
295
296 cmdline_size = cmdline_size.checked_add(s.len()).ok_or(Error::TooLarge)?;
298
299 if cmdline_size > self.capacity {
300 return Err(Error::TooLarge);
301 }
302
303 if !self.init_args.is_empty() {
305 self.init_args.push(' ');
306 }
307
308 self.init_args.push_str(s);
309
310 Ok(())
311 }
312
313 fn get_null_terminated_representation_size(&self) -> usize {
314 let mut cmdline_size = self.boot_args.len() + 1; if !self.init_args.is_empty() {
319 cmdline_size += INIT_ARGS_SEPARATOR.len() + self.init_args.len();
320 }
321
322 cmdline_size
323 }
324
325 pub fn as_cstring(&self) -> Result<CString> {
345 if self.boot_args.is_empty() && self.init_args.is_empty() {
346 CString::new("".to_string()).map_err(|_| Error::NullTerminator)
347 } else if self.boot_args.is_empty() {
348 Err(Error::NoBootArgsInserted)
349 } else if self.init_args.is_empty() {
350 CString::new(self.boot_args.to_string()).map_err(|_| Error::NullTerminator)
351 } else {
352 CString::new(format!(
353 "{}{}{}",
354 self.boot_args, INIT_ARGS_SEPARATOR, self.init_args
355 ))
356 .map_err(|_| Error::NullTerminator)
357 }
358 }
359
360 pub fn add_virtio_mmio_device(
392 &mut self,
393 size: GuestUsize,
394 baseaddr: GuestAddress,
395 irq: u32,
396 id: Option<u32>,
397 ) -> Result<()> {
398 if size == 0 {
399 return Err(Error::MmioSize);
400 }
401
402 let mut device_str = format!(
403 "virtio_mmio.device={}@0x{:x?}:{}",
404 Self::guestusize_to_str(size),
405 baseaddr.raw_value(),
406 irq
407 );
408 if let Some(id) = id {
409 device_str.push_str(format!(":{}", id).as_str());
410 }
411 self.insert_str(&device_str)
412 }
413
414 fn guestusize_to_str(size: GuestUsize) -> String {
416 const KB_MULT: u64 = 1 << 10;
417 const MB_MULT: u64 = KB_MULT << 10;
418 const GB_MULT: u64 = MB_MULT << 10;
419
420 if size % GB_MULT == 0 {
421 return format!("{}G", size / GB_MULT);
422 }
423 if size % MB_MULT == 0 {
424 return format!("{}M", size / MB_MULT);
425 }
426 if size % KB_MULT == 0 {
427 return format!("{}K", size / KB_MULT);
428 }
429 size.to_string()
430 }
431
432 fn check_outside_double_quotes(slug: &str) -> bool {
433 slug.matches('\"').count() % 2 == 0
434 }
435
436 pub fn try_from(cmdline_raw: &str, capacity: usize) -> Result<Cmdline> {
464 if capacity == 0 {
469 return Err(Error::InvalidCapacity);
470 }
471
472 let (mut boot_args, mut init_args) = match cmdline_raw
477 .match_indices(INIT_ARGS_SEPARATOR)
478 .find(|&separator_occurrence| {
479 Self::check_outside_double_quotes(&cmdline_raw[..(separator_occurrence.0)])
480 }) {
481 None => (cmdline_raw, ""),
482 Some((delimiter_index, _)) => (
483 &cmdline_raw[..delimiter_index],
484 &cmdline_raw[(delimiter_index + INIT_ARGS_SEPARATOR.len())..],
489 ),
490 };
491
492 boot_args = boot_args.trim();
493 init_args = init_args.trim();
494
495 let mut cmdline_size = boot_args.len().checked_add(1).ok_or(Error::TooLarge)?;
498
499 if !init_args.is_empty() {
500 cmdline_size = cmdline_size
501 .checked_add(INIT_ARGS_SEPARATOR.len())
502 .ok_or(Error::TooLarge)?;
503
504 cmdline_size = cmdline_size
505 .checked_add(init_args.len())
506 .ok_or(Error::TooLarge)?;
507 }
508
509 if cmdline_size > capacity {
510 return Err(Error::InvalidCapacity);
511 }
512
513 Ok(Cmdline {
514 boot_args: boot_args.to_string(),
515 init_args: init_args.to_string(),
516 capacity,
517 })
518 }
519}
520
521impl TryFrom<Cmdline> for Vec<u8> {
522 type Error = Error;
523
524 fn try_from(cmdline: Cmdline) -> result::Result<Self, Self::Error> {
525 cmdline
526 .as_cstring()
527 .map(|cmdline_cstring| cmdline_cstring.into_bytes_with_nul())
528 }
529}
530
531impl PartialEq for Cmdline {
532 fn eq(&self, other: &Self) -> bool {
533 self.as_cstring() == other.as_cstring()
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540 use std::ffi::CString;
541
542 const CMDLINE_MAX_SIZE: usize = 4096;
543
544 #[test]
545 fn test_insert_hello_world() {
546 let mut cl = Cmdline::new(100).unwrap();
547 assert_eq!(cl.as_cstring().unwrap().as_bytes_with_nul(), b"\0");
548 assert!(cl.insert("hello", "world").is_ok());
549 assert_eq!(
550 cl.as_cstring().unwrap().as_bytes_with_nul(),
551 b"hello=world\0"
552 );
553 }
554
555 #[test]
556 fn test_insert_multi() {
557 let mut cl = Cmdline::new(100).unwrap();
558 assert!(cl.insert("hello", "world").is_ok());
559 assert!(cl.insert("foo", "bar").is_ok());
560 assert_eq!(
561 cl.as_cstring().unwrap().as_bytes_with_nul(),
562 b"hello=world foo=bar\0"
563 );
564 }
565
566 #[test]
567 fn test_insert_space() {
568 let mut cl = Cmdline::new(100).unwrap();
569 assert_eq!(cl.insert("a ", "b"), Err(Error::HasSpace));
570 assert_eq!(cl.insert("a", "b "), Err(Error::HasSpace));
571 assert_eq!(cl.insert("a ", "b "), Err(Error::HasSpace));
572 assert_eq!(cl.insert(" a", "b"), Err(Error::HasSpace));
573 assert_eq!(cl.as_cstring().unwrap().as_bytes_with_nul(), b"\0");
574 }
575
576 #[test]
577 fn test_insert_equals() {
578 let mut cl = Cmdline::new(100).unwrap();
579 assert_eq!(cl.insert("a=", "b"), Err(Error::HasEquals));
580 assert_eq!(cl.insert("a", "b="), Err(Error::HasEquals));
581 assert_eq!(cl.insert("a=", "b "), Err(Error::HasEquals));
582 assert_eq!(cl.insert("=a", "b"), Err(Error::HasEquals));
583 assert_eq!(cl.insert("a", "=b"), Err(Error::HasEquals));
584 assert_eq!(cl.as_cstring().unwrap().as_bytes_with_nul(), b"\0");
585 }
586
587 #[test]
588 fn test_insert_emoji() {
589 let mut cl = Cmdline::new(100).unwrap();
590 assert_eq!(cl.insert("heart", "💖"), Err(Error::InvalidAscii));
591 assert_eq!(cl.insert("💖", "love"), Err(Error::InvalidAscii));
592 assert_eq!(cl.insert_str("heart=💖"), Err(Error::InvalidAscii));
593 assert_eq!(
594 cl.insert_multiple("💖", &["heart", "love"]),
595 Err(Error::InvalidAscii)
596 );
597 assert_eq!(
598 cl.insert_multiple("heart", &["💖", "love"]),
599 Err(Error::InvalidAscii)
600 );
601 assert_eq!(cl.as_cstring().unwrap().as_bytes_with_nul(), b"\0");
602 }
603
604 #[test]
605 fn test_insert_string() {
606 let mut cl = Cmdline::new(13).unwrap();
607 assert_eq!(cl.as_cstring().unwrap().as_bytes_with_nul(), b"\0");
608 assert!(cl.insert_str("noapic").is_ok());
609 assert_eq!(cl.as_cstring().unwrap().as_bytes_with_nul(), b"noapic\0");
610 assert!(cl.insert_str("nopci").is_ok());
611 assert_eq!(
612 cl.as_cstring().unwrap().as_bytes_with_nul(),
613 b"noapic nopci\0"
614 );
615 }
616
617 #[test]
618 fn test_insert_too_large() {
619 let mut cl = Cmdline::new(4).unwrap();
620 assert_eq!(cl.insert("hello", "world"), Err(Error::TooLarge));
621 assert_eq!(cl.insert("a", "world"), Err(Error::TooLarge));
622 assert_eq!(cl.insert("hello", "b"), Err(Error::TooLarge));
623 assert!(cl.insert("a", "b").is_ok());
624 assert_eq!(cl.insert("a", "b"), Err(Error::TooLarge));
625 assert_eq!(cl.insert_str("a"), Err(Error::TooLarge));
626 assert_eq!(cl.as_cstring().unwrap().as_bytes_with_nul(), b"a=b\0");
627
628 let mut cl = Cmdline::new(10).unwrap();
629 assert!(cl.insert("ab", "ba").is_ok()); assert_eq!(cl.insert("c", "da"), Err(Error::TooLarge)); assert!(cl.insert("c", "d").is_ok()); let mut cl = Cmdline::new(11).unwrap();
634 assert!(cl.insert("ab", "ba").is_ok()); assert_eq!(cl.insert_init_args("da"), Err(Error::TooLarge)); assert!(cl.insert_init_args("d").is_ok()); let mut cl = Cmdline::new(20).unwrap();
639 assert!(cl.insert("ab", "ba").is_ok()); assert!(cl.insert_init_args("da").is_ok()); assert_eq!(cl.insert_init_args("abcdabcd"), Err(Error::TooLarge)); assert!(cl.insert_init_args("abcdabc").is_ok()); }
644
645 #[test]
646 fn test_add_virtio_mmio_device() {
647 let mut cl = Cmdline::new(5).unwrap();
648 assert_eq!(
649 cl.add_virtio_mmio_device(0, GuestAddress(0), 0, None),
650 Err(Error::MmioSize)
651 );
652 assert_eq!(
653 cl.add_virtio_mmio_device(1, GuestAddress(0), 0, None),
654 Err(Error::TooLarge)
655 );
656
657 let mut cl = Cmdline::new(150).unwrap();
658 assert!(cl
659 .add_virtio_mmio_device(1, GuestAddress(0), 1, None)
660 .is_ok());
661 let mut expected_str = "virtio_mmio.device=1@0x0:1".to_string();
662 assert_eq!(
663 cl.as_cstring().unwrap(),
664 CString::new(expected_str.as_bytes()).unwrap()
665 );
666
667 assert!(cl
668 .add_virtio_mmio_device(2 << 10, GuestAddress(0x100), 2, None)
669 .is_ok());
670 expected_str.push_str(" virtio_mmio.device=2K@0x100:2");
671 assert_eq!(
672 cl.as_cstring().unwrap(),
673 CString::new(expected_str.as_bytes()).unwrap()
674 );
675
676 assert!(cl
677 .add_virtio_mmio_device(3 << 20, GuestAddress(0x1000), 3, None)
678 .is_ok());
679 expected_str.push_str(" virtio_mmio.device=3M@0x1000:3");
680 assert_eq!(
681 cl.as_cstring().unwrap(),
682 CString::new(expected_str.as_bytes()).unwrap()
683 );
684
685 assert!(cl
686 .add_virtio_mmio_device(4 << 30, GuestAddress(0x0001_0000), 4, Some(42))
687 .is_ok());
688 expected_str.push_str(" virtio_mmio.device=4G@0x10000:4:42");
689 assert_eq!(
690 cl.as_cstring().unwrap(),
691 CString::new(expected_str.as_bytes()).unwrap()
692 );
693 }
694
695 #[test]
696 fn test_insert_kv() {
697 let mut cl = Cmdline::new(10).unwrap();
698
699 let no_vals: Vec<&str> = vec![];
700 assert_eq!(cl.insert_multiple("foo=", &no_vals), Err(Error::HasEquals));
701 assert_eq!(
702 cl.insert_multiple("foo", &no_vals),
703 Err(Error::MissingVal("foo".to_string()))
704 );
705 assert_eq!(cl.insert_multiple("foo", &["bar "]), Err(Error::HasSpace));
706 assert_eq!(
707 cl.insert_multiple("foo", &["bar", "baz"]),
708 Err(Error::TooLarge)
709 );
710
711 let mut cl = Cmdline::new(100).unwrap();
712 assert!(cl.insert_multiple("foo", &["bar"]).is_ok());
713 assert_eq!(cl.as_cstring().unwrap().as_bytes_with_nul(), b"foo=bar\0");
714
715 let mut cl = Cmdline::new(100).unwrap();
716 assert!(cl.insert_multiple("foo", &["bar", "baz"]).is_ok());
717 assert_eq!(
718 cl.as_cstring().unwrap().as_bytes_with_nul(),
719 b"foo=bar,baz\0"
720 );
721 }
722
723 #[test]
724 fn test_try_from_cmdline_for_vec() {
725 let cl = Cmdline::new(CMDLINE_MAX_SIZE).unwrap();
726 assert_eq!(Vec::try_from(cl).unwrap(), vec![b'\0']);
727
728 let cl = Cmdline::try_from("foo", CMDLINE_MAX_SIZE).unwrap();
729 assert_eq!(Vec::try_from(cl).unwrap(), vec![b'f', b'o', b'o', b'\0']);
730
731 let mut cl = Cmdline::new(CMDLINE_MAX_SIZE).unwrap();
732 cl.insert_init_args("foo--bar").unwrap();
733 assert_eq!(Vec::try_from(cl), Err(Error::NoBootArgsInserted));
734 }
735
736 #[test]
737 fn test_partial_eq() {
738 let mut c1 = Cmdline::new(20).unwrap();
739 let mut c2 = Cmdline::new(30).unwrap();
740
741 c1.insert_str("hello world!").unwrap();
742 c2.insert_str("hello").unwrap();
743 assert_ne!(c1, c2);
744
745 c2.insert_str("world!").unwrap();
747 assert_eq!(c1, c2);
748
749 let mut cl1 = Cmdline::new(CMDLINE_MAX_SIZE).unwrap();
750 let mut cl2 = Cmdline::new(CMDLINE_MAX_SIZE).unwrap();
751
752 assert_eq!(cl1, cl2);
753 assert!(cl1
754 .add_virtio_mmio_device(1, GuestAddress(0), 1, None)
755 .is_ok());
756 assert_ne!(cl1, cl2);
757 assert!(cl2
758 .add_virtio_mmio_device(1, GuestAddress(0), 1, None)
759 .is_ok());
760 assert_eq!(cl1, cl2);
761 }
762
763 #[test]
764 fn test_try_from() {
765 assert_eq!(
766 Cmdline::try_from("foo -- bar", 0),
767 Err(Error::InvalidCapacity)
768 );
769 assert_eq!(
770 Cmdline::try_from("foo -- bar", 10),
771 Err(Error::InvalidCapacity)
772 );
773 assert!(Cmdline::try_from("foo -- bar", 11).is_ok());
774
775 let cl = Cmdline::try_from("hello=world foo=bar", CMDLINE_MAX_SIZE).unwrap();
776
777 assert_eq!(cl.boot_args, "hello=world foo=bar");
778 assert_eq!(cl.init_args, "");
779
780 let cl = Cmdline::try_from("hello=world -- foo=bar", CMDLINE_MAX_SIZE).unwrap();
781
782 assert_eq!(cl.boot_args, "hello=world");
783 assert_eq!(cl.init_args, "foo=bar");
784
785 let cl =
786 Cmdline::try_from("hello=world --foo=bar -- arg1 --arg2", CMDLINE_MAX_SIZE).unwrap();
787
788 assert_eq!(cl.boot_args, "hello=world --foo=bar");
789 assert_eq!(cl.init_args, "arg1 --arg2");
790
791 let cl = Cmdline::try_from("arg1-- arg2 --arg3", CMDLINE_MAX_SIZE).unwrap();
792
793 assert_eq!(cl.boot_args, "arg1-- arg2 --arg3");
794 assert_eq!(cl.init_args, "");
795
796 let cl = Cmdline::try_from("--arg1-- -- arg2 -- --arg3", CMDLINE_MAX_SIZE).unwrap();
797
798 assert_eq!(cl.boot_args, "--arg1--");
799 assert_eq!(cl.init_args, "arg2 -- --arg3");
800
801 let cl = Cmdline::try_from("a=\"b -- c\" d -- e ", CMDLINE_MAX_SIZE).unwrap();
802
803 assert_eq!(cl.boot_args, "a=\"b -- c\" d");
804 assert_eq!(cl.init_args, "e");
805
806 let cl = Cmdline::try_from("foo--bar=baz a=\"b -- c\"", CMDLINE_MAX_SIZE).unwrap();
807
808 assert_eq!(cl.boot_args, "foo--bar=baz a=\"b -- c\"");
809 assert_eq!(cl.init_args, "");
810
811 let cl = Cmdline::try_from("--foo --bar", CMDLINE_MAX_SIZE).unwrap();
812
813 assert_eq!(cl.boot_args, "--foo --bar");
814 assert_eq!(cl.init_args, "");
815
816 let cl = Cmdline::try_from("foo=\"bar--baz\" foo", CMDLINE_MAX_SIZE).unwrap();
817
818 assert_eq!(cl.boot_args, "foo=\"bar--baz\" foo");
819 assert_eq!(cl.init_args, "");
820 }
821
822 #[test]
823 fn test_error_try_from() {
824 assert_eq!(Cmdline::try_from("", 0), Err(Error::InvalidCapacity));
825
826 assert_eq!(
827 Cmdline::try_from(
828 String::from_utf8(vec![b'X'; CMDLINE_MAX_SIZE])
829 .unwrap()
830 .as_str(),
831 CMDLINE_MAX_SIZE - 1
832 ),
833 Err(Error::InvalidCapacity)
834 );
835
836 let cl = Cmdline::try_from(
837 "console=ttyS0 nomodules -- /etc/password --param",
838 CMDLINE_MAX_SIZE,
839 )
840 .unwrap();
841 assert_eq!(
842 cl.as_cstring().unwrap().as_bytes_with_nul(),
843 b"console=ttyS0 nomodules -- /etc/password --param\0"
844 );
845 }
846
847 #[test]
848 fn test_as_cstring() {
849 let mut cl = Cmdline::new(CMDLINE_MAX_SIZE).unwrap();
850
851 assert_eq!(cl.as_cstring().unwrap().into_bytes_with_nul(), b"\0");
852 assert!(cl.insert_init_args("/etc/password").is_ok());
853 assert_eq!(cl.as_cstring(), Err(Error::NoBootArgsInserted));
854 assert_eq!(cl.boot_args, "");
855 assert_eq!(cl.init_args, "/etc/password");
856 assert!(cl.insert("console", "ttyS0").is_ok());
857 assert_eq!(
858 cl.as_cstring().unwrap().into_bytes_with_nul(),
859 b"console=ttyS0 -- /etc/password\0"
860 );
861 assert!(cl.insert_str("nomodules").is_ok());
862 assert_eq!(
863 cl.as_cstring().unwrap().into_bytes_with_nul(),
864 b"console=ttyS0 nomodules -- /etc/password\0"
865 );
866 assert!(cl.insert_init_args("--param").is_ok());
867 assert_eq!(
868 cl.as_cstring().unwrap().into_bytes_with_nul(),
869 b"console=ttyS0 nomodules -- /etc/password --param\0"
870 );
871 }
872}