| 1 | use std::error::Error as stdError; |
| 2 | use std::fmt::{Debug, Formatter, Display}; |
| 3 | use crate::message::MatchRule; |
| 4 | use crate::{MessageType, Path}; |
| 5 | use std::convert::TryFrom; |
| 6 | use crate::strings::{Interface, BusName, Member}; |
| 7 | |
| 8 | // Our grammar: |
| 9 | // rules: rule (, rule)* |
| 10 | // rule: sender | type | interface | member | path | path_namespace | destination | arg | arg_path |
| 11 | // bool : 'true' | 'false' |
| 12 | // type: "type" "=" message_type |
| 13 | // message_type: "'signal'" | "'method_call'" | "'method_return'" | "'error'" |
| 14 | // sender: "sender" "=" string |
| 15 | // interface: "interface" "=" string |
| 16 | // member: "member" "=" string |
| 17 | // path: "path" "=" string |
| 18 | // path_namespace: "path_namespace" "=" string |
| 19 | // destination: "destination" "=" string |
| 20 | // arg: "arg" 0-63 "=" string |
| 21 | // arg_path: "arg" 0-63 "path" "=" string |
| 22 | // eavesdrop: "eavesdrop" "=" bool |
| 23 | |
| 24 | |
| 25 | #[derive (Clone, Debug)] |
| 26 | /// Error type that covers errors that might happen during parsing. |
| 27 | pub enum Error { |
| 28 | /// The type specified in the match rule is unknown |
| 29 | UnknownType, |
| 30 | /// The key is wrong / unsupported |
| 31 | UnknownKey, |
| 32 | /// Boolean could not be parsed |
| 33 | BadBoolean, |
| 34 | /// Error that occured while converting a string to a DBus format |
| 35 | BadConversion(String), |
| 36 | } |
| 37 | |
| 38 | impl Display for Error { |
| 39 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { |
| 40 | write!(f, "Error while parsing MatchRule: " )?; |
| 41 | match self { |
| 42 | Error::UnknownType => { |
| 43 | write!(f, "Unsupported message type" ) |
| 44 | } |
| 45 | Error::UnknownKey => { |
| 46 | write!(f, "Unknown key used" ) |
| 47 | } |
| 48 | Error::BadBoolean => { |
| 49 | write!(f, "Got bad boolean value" ) |
| 50 | } |
| 51 | Error::BadConversion(err: &String) => { |
| 52 | write!(f, "Error while converting: {}" , err) |
| 53 | } |
| 54 | } |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | impl stdError for Error {} |
| 59 | |
| 60 | /// Key-Value-pair |
| 61 | pub type TokenRule<'a> = (&'a str, &'a str); |
| 62 | /// Fixed size buffer for match rule tokens |
| 63 | pub type TokenBuffer<'a> = Vec<TokenRule<'a>>; |
| 64 | |
| 65 | #[derive (Clone, Debug)] |
| 66 | /// Tokenizes a match rule into key-value-pairs |
| 67 | struct Tokenizer<'a> { |
| 68 | text: &'a str, |
| 69 | } |
| 70 | |
| 71 | impl<'a> Tokenizer<'a> { |
| 72 | /// Builds a new tokenizer for the input &str |
| 73 | pub fn new(text: &'a str) -> Self { |
| 74 | Self { |
| 75 | text, |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /// Parse the key part of the key-value-pair. This is rather easy as all keys are rather |
| 80 | /// easily defined and may only contain `[a-z0-9]` so we can simply split at `'='`. |
| 81 | fn key(&self) -> (&'a str, &'a str) { |
| 82 | let index = self.text.find('=' ).unwrap_or_else(|| self.text.len()); |
| 83 | |
| 84 | ((&self.text[..index]).trim(), &self.text[index + 1..]) |
| 85 | } |
| 86 | |
| 87 | /// Parses values as generic strings. |
| 88 | /// This does not do any validation (yet) with regards to supported characters. |
| 89 | fn value(&self) -> (&'a str, &'a str) { |
| 90 | let mut i = 0; |
| 91 | let mut quoted = false; |
| 92 | let mut escape = false; |
| 93 | |
| 94 | for c in self.text.chars() { |
| 95 | match c { |
| 96 | ' \'' if !escape => { |
| 97 | quoted = !quoted; |
| 98 | } |
| 99 | ',' if !quoted => { |
| 100 | break; |
| 101 | } |
| 102 | ' \\' if !quoted => { |
| 103 | escape = true; |
| 104 | i += 1; |
| 105 | continue; |
| 106 | } |
| 107 | _ => {} |
| 108 | } |
| 109 | escape = false; |
| 110 | |
| 111 | i += 1; |
| 112 | } |
| 113 | |
| 114 | // Skip comma if there is still space in the buffer |
| 115 | let j = if self.text.len() == i { i } else { i + 1 }; |
| 116 | ((&self.text[..i]).trim(), &self.text[j..]) |
| 117 | } |
| 118 | |
| 119 | /// Tokenizes a string into key-value-pairs |
| 120 | pub fn tokenize(&mut self) -> Result<TokenBuffer<'a>, Error> { |
| 121 | let mut rules = TokenBuffer::new(); |
| 122 | |
| 123 | while !self.text.is_empty() { |
| 124 | let (key, rest) = self.key(); |
| 125 | self.text = rest; |
| 126 | let (value, rest) = self.value(); |
| 127 | self.text = rest; |
| 128 | rules.push((key, value)) |
| 129 | } |
| 130 | Ok(rules) |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | #[derive (Clone, Debug)] |
| 135 | /// Helper struct for parsing MatchRule's |
| 136 | pub struct Parser<'a> { |
| 137 | tokens: TokenBuffer<'a>, |
| 138 | } |
| 139 | |
| 140 | impl<'a> Parser<'a> { |
| 141 | /// Builds a new parser after tokenizing the input string `text`. |
| 142 | pub fn new(text: &'a str) -> Result<Self, Error> { |
| 143 | Ok(Self { |
| 144 | tokens: Tokenizer::new(text).tokenize()? |
| 145 | }) |
| 146 | } |
| 147 | |
| 148 | /// Cleans a string from the string syntax allowed by DBus. This includes concatenating |
| 149 | /// things like `''\'''` to `'`. DBus strings sort of work like in a POSIX shell and |
| 150 | /// concatenation is implied. There is only one escape sequence and that is in an unquoted |
| 151 | /// substring a backslash may escape an ASCII apostrophe (U+0027). |
| 152 | /// This method is the only one within the parser that allocates and in theory could be |
| 153 | /// rewritten to taking a `&mut str` instead of returning a `String` since all |
| 154 | /// strings are |output| <= |buf.len()|. |
| 155 | fn clean_string(&self, buf: &str) -> String { |
| 156 | let mut quoted = false; |
| 157 | let mut escape = false; |
| 158 | let mut outbuf = String::with_capacity(buf.len()); |
| 159 | |
| 160 | for c in buf.chars() { |
| 161 | match c { |
| 162 | ' \'' if !escape => { |
| 163 | quoted = !quoted; |
| 164 | } |
| 165 | ' \\' if !quoted => { |
| 166 | escape = true; |
| 167 | continue; |
| 168 | } |
| 169 | c if c.is_whitespace() && !quoted => { |
| 170 | continue; |
| 171 | } |
| 172 | c => { |
| 173 | outbuf.push(c); |
| 174 | } |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | outbuf |
| 179 | } |
| 180 | |
| 181 | /// Parses key-value-pair tokens into a MatchRule |
| 182 | pub fn parse(&self) -> Result<MatchRule<'a>, Error> { |
| 183 | let mut match_rule = MatchRule::new(); |
| 184 | |
| 185 | for &(key, raw_value) in &self.tokens { |
| 186 | let value = self.clean_string(raw_value); |
| 187 | match key { |
| 188 | "type" => { |
| 189 | match_rule = match_rule.with_type(MessageType::try_from(value.as_str()).map_err(|_| Error::UnknownType)?); |
| 190 | Ok(()) |
| 191 | } |
| 192 | "interface" => { |
| 193 | match_rule.interface = Some(Interface::new(value).map_err(Error::BadConversion)?); |
| 194 | Ok(()) |
| 195 | } |
| 196 | "sender" => { |
| 197 | match_rule.sender = Some(BusName::new(value).map_err(Error::BadConversion)?); |
| 198 | Ok(()) |
| 199 | } |
| 200 | "member" => { |
| 201 | match_rule.member = Some(Member::new(value).map_err(Error::BadConversion)?); |
| 202 | Ok(()) |
| 203 | } |
| 204 | "path" => { |
| 205 | match_rule.path = Some(Path::new(value).map_err(Error::BadConversion)?); |
| 206 | Ok(()) |
| 207 | } |
| 208 | "path_namespace" => { |
| 209 | match_rule.path = Some(Path::new(value).map_err(Error::BadConversion)?); |
| 210 | match_rule.path_is_namespace = true; |
| 211 | Ok(()) |
| 212 | } |
| 213 | "eavesdrop" => { |
| 214 | match raw_value { |
| 215 | "'true'" | "true" => { |
| 216 | match_rule = match_rule.with_eavesdrop(); |
| 217 | Ok(()) |
| 218 | } |
| 219 | "'false'" | "false" => { |
| 220 | Ok(()) |
| 221 | } |
| 222 | _ => { |
| 223 | Err(Error::BadBoolean) |
| 224 | } |
| 225 | } |
| 226 | } |
| 227 | _ => { |
| 228 | // Args and Destination are not supported yet. |
| 229 | Err(Error::UnknownKey) |
| 230 | } |
| 231 | }?; |
| 232 | } |
| 233 | |
| 234 | Ok(match_rule) |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | #[cfg (test)] |
| 239 | mod tests { |
| 240 | use crate::message::parser::Error; |
| 241 | use crate::message::MatchRule; |
| 242 | |
| 243 | #[test ] |
| 244 | fn test_tokenizer() -> Result<(), Error> { |
| 245 | let mr = MatchRule::parse(r"interface='org.freedesktop.Notifications',member='Notify'" )?; |
| 246 | assert_eq!(mr.match_str(), "interface='org.freedesktop.Notifications',member='Notify'" ); |
| 247 | let mr = MatchRule::parse(r"interface='org.mpris.MediaPlayer2.Player' , path= /org/mpris/MediaPlayer2,member='Notify', eavesdrop ='true'" )?; |
| 248 | assert_eq!(mr.match_str(), "path='/org/mpris/MediaPlayer2',interface='org.mpris.MediaPlayer2.Player',member='Notify',eavesdrop='true'" ); |
| 249 | Ok(()) |
| 250 | } |
| 251 | |
| 252 | #[test ] |
| 253 | fn test_malformed() { |
| 254 | assert!(MatchRule::parse(r"interface='org.freedesktop.Notifications',member=" ).is_err()); |
| 255 | } |
| 256 | |
| 257 | #[test ] |
| 258 | fn test_spurious_comma() { |
| 259 | assert!(MatchRule::parse(r"interface='org.freedesktop.Notifications'," ).is_ok()); |
| 260 | } |
| 261 | } |