1use alloc::format;
2use alloc::string::String;
3use alloc::vec::Vec;
4use core::fmt;
5
6#[cfg(feature = "std")]
7use regex::Regex;
8
9#[derive(Debug, Clone, Default)]
30pub struct PathFilter {
31 #[cfg(feature = "std")]
33 regex_patterns: Vec<Regex>,
34
35 exact_paths: Vec<String>,
37
38 predicates: Vec<fn(&str, &str) -> bool>,
41
42 match_all: bool,
44}
45
46impl PathFilter {
47 pub fn new() -> Self {
49 Self::default()
50 }
51
52 pub fn all() -> Self {
54 Self {
55 match_all: true,
56 ..Default::default()
57 }
58 }
59
60 pub fn none() -> Self {
62 Self::default()
63 }
64
65 #[cfg(feature = "std")]
71 pub fn with_regex<S: AsRef<str>>(mut self, pattern: S) -> Self {
72 let regex = Regex::new(pattern.as_ref()).expect("Invalid regex pattern");
73 self.regex_patterns.push(regex);
74 self
75 }
76
77 #[cfg(feature = "std")]
83 pub fn with_regexes<I, S>(mut self, patterns: I) -> Self
84 where
85 I: IntoIterator<Item = S>,
86 S: AsRef<str>,
87 {
88 for pattern in patterns {
89 let regex = Regex::new(pattern.as_ref()).expect("Invalid regex pattern");
90 self.regex_patterns.push(regex);
91 }
92 self
93 }
94
95 pub fn with_full_path<S: Into<String>>(mut self, path: S) -> Self {
97 self.exact_paths.push(path.into());
98 self
99 }
100
101 pub fn with_full_paths<I, S>(mut self, paths: I) -> Self
103 where
104 I: IntoIterator<Item = S>,
105 S: Into<String>,
106 {
107 self.exact_paths.extend(paths.into_iter().map(|p| p.into()));
108 self
109 }
110
111 pub fn with_predicate(mut self, predicate: fn(&str, &str) -> bool) -> Self {
113 self.predicates.push(predicate);
114 self
115 }
116
117 pub fn with_predicates<I>(mut self, predicates: I) -> Self
119 where
120 I: IntoIterator<Item = fn(&str, &str) -> bool>,
121 {
122 self.predicates.extend(predicates);
123 self
124 }
125
126 pub fn match_all(mut self) -> Self {
128 self.match_all = true;
129 self
130 }
131
132 pub fn matches(&self, path: &str) -> bool {
134 self.matches_with_container_path_str(path, "")
135 }
136
137 pub fn matches_with_container(&self, path: &str, container_type: &str) -> bool {
139 self.matches_with_container_path_str(path, container_type)
141 }
142
143 pub fn matches_with_container_path(&self, path: &[String], container_stack: &[String]) -> bool {
145 let path_str = path.join(".");
146 let container_path = container_stack.join(".");
147 self.matches_with_container_path_str(&path_str, &container_path)
148 }
149
150 pub fn matches_with_container_path_str(&self, path: &str, container_path: &str) -> bool {
152 if self.match_all {
154 return true;
155 }
156
157 if self.is_empty() {
159 return false;
160 }
161
162 if self.exact_paths.iter().any(|p| p == path) {
164 return true;
165 }
166
167 #[cfg(feature = "std")]
169 {
170 for regex in &self.regex_patterns {
171 if regex.is_match(path) {
172 return true;
173 }
174 }
175 }
176
177 if self
179 .predicates
180 .iter()
181 .any(|pred| pred(path, container_path))
182 {
183 return true;
184 }
185
186 false
187 }
188
189 pub fn is_empty(&self) -> bool {
191 if self.match_all {
192 return false;
193 }
194
195 #[cfg(feature = "std")]
196 let regex_empty = self.regex_patterns.is_empty();
197 #[cfg(not(feature = "std"))]
198 let regex_empty = true;
199
200 self.exact_paths.is_empty() && self.predicates.is_empty() && regex_empty
201 }
202
203 pub fn criteria_count(&self) -> usize {
205 if self.match_all {
206 return 1;
207 }
208
209 #[allow(unused_mut)]
210 let mut count = self.exact_paths.len() + self.predicates.len();
211
212 #[cfg(feature = "std")]
213 {
214 count += self.regex_patterns.len();
215 }
216
217 count
218 }
219
220 #[cfg(feature = "std")]
222 pub fn clear_regex(&mut self) -> &mut Self {
223 self.regex_patterns.clear();
224 self
225 }
226
227 pub fn clear_paths(&mut self) -> &mut Self {
229 self.exact_paths.clear();
230 self
231 }
232
233 pub fn clear_predicates(&mut self) -> &mut Self {
235 self.predicates.clear();
236 self
237 }
238
239 pub fn clear(&mut self) -> &mut Self {
241 #[cfg(feature = "std")]
242 self.clear_regex();
243
244 self.clear_paths().clear_predicates();
245 self.match_all = false;
246 self
247 }
248
249 #[cfg(feature = "std")]
255 pub fn from_regex_patterns<I, S>(patterns: I) -> Self
256 where
257 I: IntoIterator<Item = S>,
258 S: AsRef<str>,
259 {
260 Self::new().with_regexes(patterns)
261 }
262
263 pub fn from_paths<I, S>(paths: I) -> Self
265 where
266 I: IntoIterator<Item = S>,
267 S: Into<String>,
268 {
269 Self::new().with_full_paths(paths)
270 }
271
272 pub fn from_predicate(predicate: fn(&str, &str) -> bool) -> Self {
274 Self::new().with_predicate(predicate)
275 }
276
277 pub fn or(mut self, other: Self) -> Self {
279 if self.match_all || other.match_all {
280 return Self::all();
281 }
282
283 #[cfg(feature = "std")]
284 {
285 self.regex_patterns.extend(other.regex_patterns);
286 }
287
288 self.exact_paths.extend(other.exact_paths);
289 self.predicates.extend(other.predicates);
290
291 self
292 }
293}
294
295impl fmt::Display for PathFilter {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 if self.match_all {
298 return write!(f, "PathFilter::all()");
299 }
300
301 if self.is_empty() {
302 return write!(f, "PathFilter::none()");
303 }
304
305 write!(f, "PathFilter[")?;
306
307 let mut parts = Vec::new();
308
309 #[cfg(feature = "std")]
310 if !self.regex_patterns.is_empty() {
311 parts.push(format!("regex: {:?}", self.regex_patterns));
312 }
313
314 if !self.exact_paths.is_empty() {
315 parts.push(format!("paths: {:?}", self.exact_paths));
316 }
317
318 if !self.predicates.is_empty() {
319 parts.push(format!("predicates: {}", self.predicates.len()));
320 }
321
322 write!(f, "{}]", parts.join(", "))
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 #[test]
331 fn empty_filter() {
332 let filter = PathFilter::new();
333 assert!(filter.is_empty());
334 assert!(!filter.matches("encoder.weight"));
335 assert!(!filter.matches("decoder.bias"));
336 }
337
338 #[test]
339 fn match_all() {
340 let filter = PathFilter::all();
341 assert!(!filter.is_empty());
342 assert!(filter.matches("encoder.weight"));
343 assert!(filter.matches("decoder.bias"));
344 assert!(filter.matches("anything"));
345 }
346
347 #[test]
348 fn exact_paths() {
349 let filter = PathFilter::new()
350 .with_full_path("encoder.weight")
351 .with_full_path("decoder.bias");
352
353 assert!(filter.matches("encoder.weight"));
354 assert!(filter.matches("decoder.bias"));
355 assert!(!filter.matches("encoder.bias"));
356 assert!(!filter.matches("decoder.weight"));
357 }
358
359 #[test]
360 #[cfg(feature = "std")]
361 fn regex_patterns() {
362 let filter = PathFilter::new()
363 .with_regex(r"^encoder\..*")
364 .with_regex(r".*\.weight$");
365
366 assert!(filter.matches("encoder.layer1.bias"));
367 assert!(filter.matches("decoder.weight"));
368 assert!(filter.matches("encoder.weight"));
369 assert!(!filter.matches("decoder.bias"));
370 }
371
372 #[test]
373 #[cfg(feature = "std")]
374 #[should_panic(expected = "Invalid regex pattern")]
375 fn invalid_regex_panics() {
376 let _ = PathFilter::new().with_regex("[");
377 }
378
379 #[test]
380 #[cfg(feature = "std")]
381 #[should_panic(expected = "Invalid regex pattern")]
382 fn invalid_regex_in_collection_panics() {
383 let _ = PathFilter::new().with_regexes([r"^valid$", "["]);
384 }
385
386 #[test]
387 fn predicates() {
388 fn contains_norm(path: &str, _container_path: &str) -> bool {
389 path.contains("norm")
390 }
391
392 fn is_short(path: &str, _container_path: &str) -> bool {
393 path.len() < 10
394 }
395
396 let filter = PathFilter::new()
397 .with_predicate(contains_norm)
398 .with_predicate(is_short);
399
400 assert!(filter.matches("norm.weight"));
401 assert!(filter.matches("layer.norm.bias"));
402 assert!(filter.matches("bias"));
403 assert!(!filter.matches("encoder.decoder.weight.long.name"));
404 }
405
406 #[test]
407 fn combined_filters() {
408 let filter = PathFilter::new()
409 .with_full_path("special.tensor")
410 .with_predicate(|path, _container_path| path.contains("attention"));
411
412 #[cfg(feature = "std")]
413 let filter = filter.with_regex(r"^encoder\..*");
414
415 assert!(filter.matches("special.tensor"));
416 assert!(filter.matches("self_attention.query"));
417
418 #[cfg(feature = "std")]
419 assert!(filter.matches("encoder.anything"));
420
421 assert!(!filter.matches("decoder.weight"));
422 }
423
424 #[test]
425 fn or_combination() {
426 let encoder_filter = PathFilter::new().with_full_path("encoder.weight");
427 let decoder_filter = PathFilter::new().with_full_path("decoder.bias");
428
429 let combined = encoder_filter.or(decoder_filter);
430
431 assert!(combined.matches("encoder.weight"));
432 assert!(combined.matches("decoder.bias"));
433 assert!(!combined.matches("model.head.weight"));
434 }
435
436 #[test]
437 #[cfg(feature = "std")]
438 fn common_patterns() {
439 let encoder = PathFilter::new().with_regex(r"^encoder\..*");
441 assert!(encoder.matches("encoder.weight"));
442 assert!(!encoder.matches("decoder.weight"));
443
444 let weights = PathFilter::new().with_regex(r".*\.weight$");
446 assert!(weights.matches("encoder.weight"));
447 assert!(weights.matches("decoder.weight"));
448 assert!(!weights.matches("encoder.bias"));
449
450 let layers = PathFilter::new()
452 .with_regex(r"(^|.*\.)layers\.0\.")
453 .with_regex(r"(^|.*\.)layers\.2\.")
454 .with_regex(r"(^|.*\.)layers\.4\.");
455 assert!(layers.matches("model.layers.0.weight"));
456 assert!(layers.matches("layers.2.bias"));
457 assert!(!layers.matches("layers.1.weight"));
458 }
459
460 #[test]
461 fn criteria_count() {
462 let filter = PathFilter::new()
463 .with_full_path("path1")
464 .with_full_path("path2")
465 .with_predicate(|_, _| true);
466
467 #[cfg(feature = "std")]
468 let filter = filter.with_regex(".*");
469
470 #[cfg(feature = "std")]
471 assert_eq!(filter.criteria_count(), 4);
472
473 #[cfg(not(feature = "std"))]
474 assert_eq!(filter.criteria_count(), 3);
475 }
476
477 #[test]
478 fn clear_operations() {
479 let mut filter = PathFilter::new().with_full_path("test");
480
481 filter.clear_paths();
482 assert!(!filter.matches("test"));
483
484 filter.clear();
485 assert!(filter.is_empty());
486 }
487
488 #[test]
489 fn container_predicates() {
490 let linear_weights = PathFilter::new().with_predicate(|path, container_path| {
492 container_path.split('.').next_back() == Some("Linear") && path.ends_with(".weight")
493 });
494
495 assert!(linear_weights.matches_with_container("layer1.weight", "Linear"));
496 assert!(!linear_weights.matches_with_container("layer1.weight", "Conv2d"));
497 assert!(!linear_weights.matches_with_container("layer1.bias", "Linear"));
498
499 let conv_only = PathFilter::new().with_predicate(|_path, container_path| {
501 let last = container_path.split('.').next_back();
502 last == Some("Conv2d") || last == Some("ConvTranspose2d")
503 });
504
505 assert!(conv_only.matches_with_container("encoder.weight", "Conv2d"));
506 assert!(conv_only.matches_with_container("decoder.weight", "ConvTranspose2d"));
507 assert!(!conv_only.matches_with_container("fc.weight", "Linear"));
508
509 let combined = PathFilter::new()
511 .with_predicate(|path, _container_path| path.starts_with("encoder."))
512 .with_predicate(|_path, container_path| {
513 container_path.split('.').next_back() == Some("BatchNorm2d")
514 });
515
516 assert!(combined.matches_with_container("encoder.layer1", "Linear"));
518 assert!(combined.matches_with_container("decoder.bn", "BatchNorm2d"));
519 assert!(!combined.matches_with_container("decoder.layer", "Linear"));
520 }
521
522 #[test]
523 fn container_predicate_with_regex() {
524 #[cfg(feature = "std")]
526 {
527 let filter = PathFilter::new()
528 .with_regex(r"^encoder\..*")
529 .with_predicate(|path, container_path| {
530 container_path.split('.').next_back() == Some("Linear")
531 && path.contains(".bias")
532 });
533
534 assert!(filter.matches_with_container("encoder.layer1.weight", "Conv2d"));
536 assert!(filter.matches_with_container("decoder.fc.bias", "Linear"));
538 assert!(!filter.matches_with_container("decoder.conv.weight", "Conv2d"));
540 }
541 }
542
543 #[test]
544 fn container_stack_predicates() {
545 let nested_filter = PathFilter::new().with_predicate(|_path, container_path| {
547 let parts: Vec<&str> = container_path.split('.').collect();
549 parts.len() >= 3
550 && parts[0] == "Model"
551 && parts[1] == "TransformerBlock"
552 && parts[2] == "Linear"
553 });
554
555 assert!(nested_filter.matches_with_container_path_str(
556 "encoder.weight",
557 "Model.TransformerBlock.Linear.Param"
558 ));
559 assert!(
560 !nested_filter
561 .matches_with_container_path_str("decoder.weight", "Model.Decoder.Linear.Param")
562 );
563 assert!(!nested_filter.matches_with_container_path_str(
564 "encoder.weight",
565 "Model.TransformerBlock.Conv2d.Param"
566 ));
567
568 let depth_filter = PathFilter::new().with_predicate(|_path, container_path| {
570 let parts: Vec<&str> = container_path.split('.').collect();
571 parts.len() == 4 && parts.get(2) == Some(&"Linear")
572 });
573
574 assert!(depth_filter.matches_with_container_path_str(
575 "model.layer.weight",
576 "Model.TransformerBlock.Linear.Param"
577 ));
578 assert!(
579 !depth_filter
580 .matches_with_container_path_str("model.weight", "Model.TransformerBlock.Conv2d")
581 ); let any_linear = PathFilter::new()
585 .with_predicate(|_path, container_path| container_path.contains("Linear"));
586
587 assert!(
588 any_linear.matches_with_container_path_str(
589 "some.path",
590 "Model.TransformerBlock.Linear.Param"
591 )
592 );
593 assert!(
594 any_linear.matches_with_container_path_str("other.path", "Model.Decoder.Linear.Param")
595 );
596 assert!(
597 !any_linear.matches_with_container_path_str(
598 "conv.path",
599 "Model.TransformerBlock.Conv2d.Param"
600 )
601 );
602 }
603
604 #[test]
605 fn container_path_dot_notation() {
606 let dot_filter = PathFilter::new().with_predicate(|_path, container_path| {
608 container_path.starts_with("Model.TransformerBlock")
609 });
610
611 assert!(
613 dot_filter.matches_with_container_path_str("weight", "Model.TransformerBlock.Linear")
614 );
615 assert!(!dot_filter.matches_with_container_path_str("weight", "Model.Decoder.Linear"));
616
617 let pattern_filter = PathFilter::new().with_predicate(|_path, container_path| {
619 container_path.contains("Block.Linear") || container_path.contains("Block.Conv")
621 });
622
623 assert!(
624 pattern_filter
625 .matches_with_container_path_str("weight", "Model.TransformerBlock.Linear")
626 );
627 assert!(pattern_filter.matches_with_container_path_str("weight", "Model.ResBlock.Conv2d"));
628 assert!(!pattern_filter.matches_with_container_path_str("weight", "Model.Linear.Param"));
629
630 let combined = PathFilter::new().with_predicate(|path, container_path| {
632 path.ends_with(".weight")
634 && container_path.contains("Block")
635 && container_path.split('.').next_back() == Some("Linear")
636 });
637
638 assert!(
639 combined
640 .matches_with_container_path_str("layer.weight", "Model.TransformerBlock.Linear")
641 );
642 assert!(
643 !combined
644 .matches_with_container_path_str("layer.bias", "Model.TransformerBlock.Linear")
645 );
646 assert!(!combined.matches_with_container_path_str("layer.weight", "Model.Decoder.Linear"));
647 }
648}