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 | } |