Nushell plugin for interacting with D-Bus
at main 470 lines 15 kB view raw
1#[derive(Debug, Clone, PartialEq, Eq)] 2pub struct Pattern { 3 separator: Option<char>, 4 tokens: Vec<PatternToken>, 5} 6 7#[derive(Debug, Clone, PartialEq, Eq)] 8enum PatternToken { 9 Exact(String), 10 OneWildcard, 11 ManyWildcard, 12 AnyChar, 13} 14 15impl Pattern { 16 pub fn new(pattern: &str, separator: Option<char>) -> Pattern { 17 let mut tokens = vec![]; 18 for ch in pattern.chars() { 19 match ch { 20 '*' => { 21 if tokens.last() == Some(&PatternToken::OneWildcard) { 22 *tokens.last_mut().unwrap() = PatternToken::ManyWildcard; 23 } else { 24 tokens.push(PatternToken::OneWildcard); 25 } 26 } 27 '?' => tokens.push(PatternToken::AnyChar), 28 _ => match tokens.last_mut() { 29 Some(PatternToken::Exact(s)) => s.push(ch), 30 _ => tokens.push(PatternToken::Exact(ch.into())), 31 }, 32 } 33 } 34 Pattern { separator, tokens } 35 } 36 37 pub fn is_match(&self, string: &str) -> bool { 38 #[derive(Debug)] 39 enum MatchState { 40 Precise, 41 ScanAhead { stop_at_separator: bool }, 42 } 43 let mut state = MatchState::Precise; 44 let mut tokens = &self.tokens[..]; 45 let mut search_str = string; 46 while !tokens.is_empty() { 47 match tokens.first().unwrap() { 48 PatternToken::Exact(s) => { 49 if search_str.starts_with(s) { 50 // Exact match passed. Consume the token and string and continue 51 tokens = &tokens[1..]; 52 search_str = &search_str[s.len()..]; 53 state = MatchState::Precise; 54 } else { 55 match state { 56 MatchState::Precise => { 57 // Can't possibly match 58 return false; 59 } 60 MatchState::ScanAhead { stop_at_separator } => { 61 if search_str.is_empty() { 62 // End of input, can't match 63 return false; 64 } 65 if stop_at_separator 66 && self 67 .separator 68 .is_some_and(|sep| search_str.starts_with(sep)) 69 { 70 // Found the separator. Consume a char and revert to precise 71 // mode 72 search_str = &search_str[1..]; 73 state = MatchState::Precise; 74 } else { 75 // Skip the non-matching char and continue 76 search_str = &search_str[1..]; 77 } 78 } 79 } 80 } 81 } 82 PatternToken::OneWildcard => { 83 // Set the mode to ScanAhead, stopping at separator 84 state = MatchState::ScanAhead { 85 stop_at_separator: true, 86 }; 87 tokens = &tokens[1..]; 88 } 89 PatternToken::ManyWildcard => { 90 // Set the mode to ScanAhead, ignoring separator 91 state = MatchState::ScanAhead { 92 stop_at_separator: false, 93 }; 94 tokens = &tokens[1..]; 95 } 96 PatternToken::AnyChar => { 97 if !search_str.is_empty() { 98 // Take a char from the search str and continue 99 search_str = &search_str[1..]; 100 tokens = &tokens[1..]; 101 } else { 102 // End of input 103 return false; 104 } 105 } 106 } 107 } 108 #[cfg(test)] 109 { 110 println!( 111 "end, state={:?}, search_str={:?}, tokens={:?}", 112 state, search_str, tokens 113 ); 114 } 115 if !search_str.is_empty() { 116 // If the search str is not empty at the end 117 match state { 118 // We didn't end with a wildcard, so this is a fail 119 MatchState::Precise => false, 120 // This could be a match as long as the separator isn't contained in the remainder 121 MatchState::ScanAhead { 122 stop_at_separator: true, 123 } => { 124 if let Some(separator) = self.separator { 125 !search_str.contains(separator) 126 } else { 127 // No separator specified, so this is a success 128 true 129 } 130 } 131 // Always a success, no matter what remains 132 MatchState::ScanAhead { 133 stop_at_separator: false, 134 } => true, 135 } 136 } else { 137 // The match has succeeded - there is nothing more to match 138 true 139 } 140 } 141} 142 143#[test] 144fn test_pattern_new() { 145 assert_eq!( 146 Pattern::new("", Some('/')), 147 Pattern { 148 separator: Some('/'), 149 tokens: vec![] 150 } 151 ); 152 assert_eq!( 153 Pattern::new("", None), 154 Pattern { 155 separator: None, 156 tokens: vec![] 157 } 158 ); 159 assert_eq!( 160 Pattern::new("org.freedesktop.DBus", Some('.')), 161 Pattern { 162 separator: Some('.'), 163 tokens: vec![PatternToken::Exact("org.freedesktop.DBus".into()),] 164 } 165 ); 166 assert_eq!( 167 Pattern::new("*", Some('.')), 168 Pattern { 169 separator: Some('.'), 170 tokens: vec![PatternToken::OneWildcard,] 171 } 172 ); 173 assert_eq!( 174 Pattern::new("**", Some('.')), 175 Pattern { 176 separator: Some('.'), 177 tokens: vec![PatternToken::ManyWildcard,] 178 } 179 ); 180 assert_eq!( 181 Pattern::new("?", Some('.')), 182 Pattern { 183 separator: Some('.'), 184 tokens: vec![PatternToken::AnyChar,] 185 } 186 ); 187 assert_eq!( 188 Pattern::new("org.freedesktop.*", Some('.')), 189 Pattern { 190 separator: Some('.'), 191 tokens: vec![ 192 PatternToken::Exact("org.freedesktop.".into()), 193 PatternToken::OneWildcard, 194 ] 195 } 196 ); 197 assert_eq!( 198 Pattern::new("org.freedesktop.**", Some('.')), 199 Pattern { 200 separator: Some('.'), 201 tokens: vec![ 202 PatternToken::Exact("org.freedesktop.".into()), 203 PatternToken::ManyWildcard, 204 ] 205 } 206 ); 207 assert_eq!( 208 Pattern::new("org.*.DBus", Some('.')), 209 Pattern { 210 separator: Some('.'), 211 tokens: vec![ 212 PatternToken::Exact("org.".into()), 213 PatternToken::OneWildcard, 214 PatternToken::Exact(".DBus".into()), 215 ] 216 } 217 ); 218 assert_eq!( 219 Pattern::new("org.**.DBus", Some('.')), 220 Pattern { 221 separator: Some('.'), 222 tokens: vec![ 223 PatternToken::Exact("org.".into()), 224 PatternToken::ManyWildcard, 225 PatternToken::Exact(".DBus".into()), 226 ] 227 } 228 ); 229 assert_eq!( 230 Pattern::new("org.**.?Bus", Some('.')), 231 Pattern { 232 separator: Some('.'), 233 tokens: vec![ 234 PatternToken::Exact("org.".into()), 235 PatternToken::ManyWildcard, 236 PatternToken::Exact(".".into()), 237 PatternToken::AnyChar, 238 PatternToken::Exact("Bus".into()), 239 ] 240 } 241 ); 242 assert_eq!( 243 Pattern::new("org.free*top", Some('.')), 244 Pattern { 245 separator: Some('.'), 246 tokens: vec![ 247 PatternToken::Exact("org.free".into()), 248 PatternToken::OneWildcard, 249 PatternToken::Exact("top".into()), 250 ] 251 } 252 ); 253 assert_eq!( 254 Pattern::new("org.free**top", Some('.')), 255 Pattern { 256 separator: Some('.'), 257 tokens: vec![ 258 PatternToken::Exact("org.free".into()), 259 PatternToken::ManyWildcard, 260 PatternToken::Exact("top".into()), 261 ] 262 } 263 ); 264 assert_eq!( 265 Pattern::new("org.**top", Some('.')), 266 Pattern { 267 separator: Some('.'), 268 tokens: vec![ 269 PatternToken::Exact("org.".into()), 270 PatternToken::ManyWildcard, 271 PatternToken::Exact("top".into()), 272 ] 273 } 274 ); 275 assert_eq!( 276 Pattern::new("**top", Some('.')), 277 Pattern { 278 separator: Some('.'), 279 tokens: vec![ 280 PatternToken::ManyWildcard, 281 PatternToken::Exact("top".into()), 282 ] 283 } 284 ); 285 assert_eq!( 286 Pattern::new("org.free**", Some('.')), 287 Pattern { 288 separator: Some('.'), 289 tokens: vec![ 290 PatternToken::Exact("org.free".into()), 291 PatternToken::ManyWildcard, 292 ] 293 } 294 ); 295} 296 297#[test] 298fn test_pattern_is_match_empty() { 299 let pat = Pattern { 300 separator: Some('.'), 301 tokens: vec![], 302 }; 303 assert!(pat.is_match("")); 304 assert!(!pat.is_match("anystring")); 305 assert!(!pat.is_match("anystring.anyotherstring")); 306} 307 308#[test] 309fn test_pattern_is_match_exact() { 310 let pat = Pattern { 311 separator: Some('.'), 312 tokens: vec![PatternToken::Exact("specific".into())], 313 }; 314 assert!(pat.is_match("specific")); 315 assert!(!pat.is_match("")); 316 assert!(!pat.is_match("specifi")); 317 assert!(!pat.is_match("specifica")); 318} 319 320#[test] 321fn test_pattern_is_match_one_wildcard() { 322 let pat = Pattern { 323 separator: Some('.'), 324 tokens: vec![ 325 PatternToken::Exact("foo.".into()), 326 PatternToken::OneWildcard, 327 PatternToken::Exact(".baz".into()), 328 ], 329 }; 330 assert!(pat.is_match("foo.bar.baz")); 331 assert!(pat.is_match("foo.grok.baz")); 332 assert!(pat.is_match("foo..baz")); 333 assert!(!pat.is_match("foo.ono.notmatch.baz")); 334 assert!(!pat.is_match("")); 335 assert!(!pat.is_match("specifi")); 336 assert!(!pat.is_match("specifica.baz")); 337 assert!(!pat.is_match("foo.specifica")); 338} 339 340#[test] 341fn test_pattern_is_match_one_wildcard_at_end() { 342 let pat = Pattern { 343 separator: Some('.'), 344 tokens: vec![ 345 PatternToken::Exact("foo.".into()), 346 PatternToken::OneWildcard, 347 ], 348 }; 349 assert!(pat.is_match("foo.bar")); 350 assert!(pat.is_match("foo.grok")); 351 assert!(pat.is_match("foo.")); 352 assert!(!pat.is_match("foo.ono.notmatch.baz")); 353 assert!(!pat.is_match("")); 354 assert!(!pat.is_match("specifi")); 355 assert!(!pat.is_match("specifica.baz")); 356} 357 358#[test] 359fn test_pattern_is_match_one_wildcard_at_start() { 360 let pat = Pattern { 361 separator: Some('.'), 362 tokens: vec![ 363 PatternToken::OneWildcard, 364 PatternToken::Exact(".bar".into()), 365 ], 366 }; 367 assert!(pat.is_match("foo.bar")); 368 assert!(pat.is_match("grok.bar")); 369 assert!(pat.is_match(".bar")); 370 assert!(!pat.is_match("foo.ono.notmatch.bar")); 371 assert!(!pat.is_match("")); 372 assert!(!pat.is_match("specifi")); 373 assert!(!pat.is_match("specifica.baz")); 374} 375 376#[test] 377fn test_pattern_is_match_one_wildcard_no_separator() { 378 let pat = Pattern { 379 separator: None, 380 tokens: vec![ 381 PatternToken::Exact("foo.".into()), 382 PatternToken::OneWildcard, 383 PatternToken::Exact(".baz".into()), 384 ], 385 }; 386 assert!(pat.is_match("foo.bar.baz")); 387 assert!(pat.is_match("foo.grok.baz")); 388 assert!(pat.is_match("foo..baz")); 389 assert!(pat.is_match("foo.this.shouldmatch.baz")); 390 assert!(pat.is_match("foo.this.should.match.baz")); 391 assert!(!pat.is_match("")); 392 assert!(!pat.is_match("specifi")); 393 assert!(!pat.is_match("specifica.baz")); 394 assert!(!pat.is_match("foo.specifica")); 395} 396 397#[test] 398fn test_pattern_is_match_many_wildcard() { 399 let pat = Pattern { 400 separator: Some('.'), 401 tokens: vec![ 402 PatternToken::Exact("foo.".into()), 403 PatternToken::ManyWildcard, 404 PatternToken::Exact(".baz".into()), 405 ], 406 }; 407 assert!(pat.is_match("foo.bar.baz")); 408 assert!(pat.is_match("foo.grok.baz")); 409 assert!(pat.is_match("foo..baz")); 410 assert!(pat.is_match("foo.this.shouldmatch.baz")); 411 assert!(pat.is_match("foo.this.should.match.baz")); 412 assert!(!pat.is_match("")); 413 assert!(!pat.is_match("specifi")); 414 assert!(!pat.is_match("specifica.baz")); 415 assert!(!pat.is_match("foo.specifica")); 416} 417 418#[test] 419fn test_pattern_is_match_many_wildcard_at_end() { 420 let pat = Pattern { 421 separator: Some('.'), 422 tokens: vec![ 423 PatternToken::Exact("foo.".into()), 424 PatternToken::ManyWildcard, 425 ], 426 }; 427 assert!(pat.is_match("foo.bar")); 428 assert!(pat.is_match("foo.grok")); 429 assert!(pat.is_match("foo.")); 430 assert!(pat.is_match("foo.this.should.match")); 431 assert!(!pat.is_match("")); 432 assert!(!pat.is_match("specifi")); 433 assert!(!pat.is_match("specifica.baz")); 434} 435 436#[test] 437fn test_pattern_is_match_many_wildcard_at_start() { 438 let pat = Pattern { 439 separator: Some('.'), 440 tokens: vec![ 441 PatternToken::ManyWildcard, 442 PatternToken::Exact(".bar".into()), 443 ], 444 }; 445 assert!(pat.is_match("foo.bar")); 446 assert!(pat.is_match("grok.bar")); 447 assert!(pat.is_match("should.match.bar")); 448 assert!(pat.is_match(".bar")); 449 assert!(!pat.is_match("")); 450 assert!(!pat.is_match("specifi")); 451 assert!(!pat.is_match("specifica.baz")); 452} 453 454#[test] 455fn test_pattern_is_match_any_char() { 456 let pat = Pattern { 457 separator: Some('.'), 458 tokens: vec![ 459 PatternToken::Exact("fo".into()), 460 PatternToken::AnyChar, 461 PatternToken::Exact(".baz".into()), 462 ], 463 }; 464 assert!(pat.is_match("foo.baz")); 465 assert!(pat.is_match("foe.baz")); 466 assert!(pat.is_match("foi.baz")); 467 assert!(!pat.is_match("")); 468 assert!(!pat.is_match("fooo.baz")); 469 assert!(!pat.is_match("fo.baz")); 470}