1 // Copyright 2021 The ChromiumOS Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 pub mod device; 6 7 use std::fmt::Debug; 8 9 use argh::FromArgValue; 10 use serde::Deserialize; 11 use serde_keyvalue::ErrorKind; 12 use serde_keyvalue::KeyValueDeserializer; 13 14 pub use self::device::*; 15 16 /// Extends any device configuration with a mandatory extra "vhost" parameter to specify the socket 17 /// or PCI device to use in order to communicate with a vhost client. 18 /// 19 /// The `vhost` argument must come first, followed by all the arguments required by `device`. 20 #[derive(Debug, Deserialize)] 21 #[serde(deny_unknown_fields)] 22 // TODO(b/262345003): This requires a custom `Deserialize` implementation to support configuration 23 // files properly. Right now the pseudo-flattening is done by the `FromArgValue` implementation, 24 // which is only used with command-line arguments. A good `Deserialize` implementation would allow 25 // the same behavior with any deserializer, but requires some serde-fu that is above my current 26 // skills. 27 pub struct VhostUserParams<T: Debug> { 28 pub vhost: String, 29 pub device: T, 30 } 31 32 impl<T> FromArgValue for VhostUserParams<T> 33 where 34 T: Debug + for<'de> Deserialize<'de>, 35 { from_arg_value(value: &str) -> std::result::Result<Self, String>36 fn from_arg_value(value: &str) -> std::result::Result<Self, String> { 37 // `from_arg_value` returns a `String` as error, but our deserializer API defines its own 38 // error type. Perform parsing from a closure so we can easily map returned errors. 39 let builder = move || { 40 let mut deserializer = KeyValueDeserializer::from(value); 41 42 // Parse the "vhost" parameter 43 let id = deserializer.parse_identifier()?; 44 if id != "vhost" { 45 return Err(deserializer 46 .error_here(ErrorKind::SerdeError("expected \"vhost\" parameter".into()))); 47 } 48 if deserializer.next_char() != Some('=') { 49 return Err(deserializer.error_here(ErrorKind::ExpectedEqual)); 50 } 51 let vhost = deserializer.parse_string()?; 52 match deserializer.next_char() { 53 Some(',') | None => (), 54 _ => return Err(deserializer.error_here(ErrorKind::ExpectedComma)), 55 } 56 57 // Parse the device-specific parameters and finish 58 let device = T::deserialize(&mut deserializer)?; 59 deserializer.finish()?; 60 61 Ok(Self { 62 vhost: vhost.into(), 63 device, 64 }) 65 }; 66 67 builder().map_err(|e| e.to_string()) 68 } 69 } 70 71 #[cfg(test)] 72 mod tests { 73 use std::path::PathBuf; 74 75 use argh::FromArgValue; 76 use serde::Deserialize; 77 use serde_keyvalue::*; 78 79 use super::VhostUserParams; 80 81 #[derive(Debug, Deserialize, PartialEq, Eq)] 82 #[serde(deny_unknown_fields, rename_all = "kebab-case")] 83 struct DummyDevice { 84 path: PathBuf, 85 #[serde(default)] 86 boom_range: u32, 87 } 88 from_arg_value(s: &str) -> Result<VhostUserParams<DummyDevice>, String>89 fn from_arg_value(s: &str) -> Result<VhostUserParams<DummyDevice>, String> { 90 VhostUserParams::<DummyDevice>::from_arg_value(s) 91 } 92 93 #[test] vhost_user_params()94 fn vhost_user_params() { 95 let device = from_arg_value("vhost=vhost_sock,path=/path/to/dummy,boom-range=42").unwrap(); 96 assert_eq!(device.vhost.as_str(), "vhost_sock"); 97 assert_eq!( 98 device.device, 99 DummyDevice { 100 path: "/path/to/dummy".into(), 101 boom_range: 42, 102 } 103 ); 104 105 // Default parameter of device not specified. 106 let device = from_arg_value("vhost=vhost_sock,path=/path/to/dummy").unwrap(); 107 assert_eq!(device.vhost.as_str(), "vhost_sock"); 108 assert_eq!( 109 device.device, 110 DummyDevice { 111 path: "/path/to/dummy".into(), 112 boom_range: Default::default(), 113 } 114 ); 115 116 // Invalid parameter is rejected. 117 assert_eq!( 118 from_arg_value("vhost=vhost_sock,path=/path/to/dummy,boom-range=42,invalid-param=10") 119 .unwrap_err(), 120 "unknown field `invalid-param`, expected `path` or `boom-range`".to_string(), 121 ); 122 123 // Device path can be parsed even if specified as a number. 124 // This ensures that we don't flatten the `device` member, which would result in 125 // `deserialize_any` being called and the type of `path` to be mistaken for an integer. 126 let device = from_arg_value("vhost=vhost_sock,path=10").unwrap(); 127 assert_eq!(device.vhost.as_str(), "vhost_sock"); 128 assert_eq!( 129 device.device, 130 DummyDevice { 131 path: "10".into(), 132 boom_range: Default::default(), 133 } 134 ); 135 136 // Misplaced `vhost` parameter is rejected 137 let _ = from_arg_value("path=/path/to/dummy,vhost=vhost_sock,boom-range=42").unwrap_err(); 138 } 139 } 140