1use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct FieldPath {
16 pub base_var: String,
18
19 pub indices: Vec<usize>,
22}
23
24impl FieldPath {
25 pub fn new(base_var: String, indices: Vec<usize>) -> Self {
27 FieldPath { base_var, indices }
28 }
29
30 pub fn whole_var(base_var: String) -> Self {
32 FieldPath {
33 base_var,
34 indices: Vec::new(),
35 }
36 }
37
38 pub fn single_field(base_var: String, index: usize) -> Self {
40 FieldPath {
41 base_var,
42 indices: vec![index],
43 }
44 }
45
46 pub fn to_string(&self) -> String {
49 if self.indices.is_empty() {
50 self.base_var.clone()
51 } else {
52 let indices_str = self
53 .indices
54 .iter()
55 .map(|i| i.to_string())
56 .collect::<Vec<_>>()
57 .join(".");
58 format!("{}.{}", self.base_var, indices_str)
59 }
60 }
61
62 pub fn is_prefix_of(&self, other: &FieldPath) -> bool {
68 if self.base_var != other.base_var {
69 return false;
70 }
71
72 if self.indices.len() > other.indices.len() {
73 return false;
74 }
75
76 self.indices
77 .iter()
78 .zip(other.indices.iter())
79 .all(|(a, b)| a == b)
80 }
81
82 pub fn is_whole_var(&self) -> bool {
84 self.indices.is_empty()
85 }
86
87 pub fn parent(&self) -> Option<FieldPath> {
92 if self.indices.is_empty() {
93 None
94 } else {
95 let mut parent_indices = self.indices.clone();
96 parent_indices.pop();
97 Some(FieldPath {
98 base_var: self.base_var.clone(),
99 indices: parent_indices,
100 })
101 }
102 }
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub enum FieldTaint {
108 Clean,
110
111 Tainted {
113 source_type: String,
114 source_location: String,
115 },
116
117 Sanitized { sanitizer: String },
119
120 Unknown,
122}
123
124impl FieldTaint {
125 pub fn is_tainted(&self) -> bool {
127 matches!(self, FieldTaint::Tainted { .. })
128 }
129
130 pub fn is_clean(&self) -> bool {
132 matches!(self, FieldTaint::Clean)
133 }
134
135 pub fn is_sanitized(&self) -> bool {
137 matches!(self, FieldTaint::Sanitized { .. })
138 }
139}
140
141#[derive(Debug, Clone)]
146pub struct FieldTaintMap {
147 fields: HashMap<FieldPath, FieldTaint>,
149}
150
151impl FieldTaintMap {
152 pub fn new() -> Self {
154 FieldTaintMap {
155 fields: HashMap::new(),
156 }
157 }
158
159 pub fn set_field_taint(&mut self, path: FieldPath, taint: FieldTaint) {
161 self.fields.insert(path, taint);
162 }
163
164 pub fn get_field_taint(&self, path: &FieldPath) -> FieldTaint {
169 if let Some(taint) = self.fields.get(path) {
171 return taint.clone();
172 }
173
174 let mut current = path.clone();
176 while let Some(parent) = current.parent() {
177 if let Some(taint) = self.fields.get(&parent) {
178 return taint.clone();
179 }
180 current = parent;
181 }
182
183 FieldTaint::Unknown
185 }
186
187 pub fn set_var_taint(&mut self, base_var: &str, taint: FieldTaint) {
192 let base_path = FieldPath::whole_var(base_var.to_string());
194 self.fields.insert(base_path, taint.clone());
195
196 let fields_to_update: Vec<FieldPath> = self
198 .fields
199 .keys()
200 .filter(|path| path.base_var == base_var && !path.is_whole_var())
201 .cloned()
202 .collect();
203
204 for field_path in fields_to_update {
205 self.fields.insert(field_path, taint.clone());
206 }
207 }
208
209 pub fn get_fields_for_var(&self, base_var: &str) -> Vec<(FieldPath, FieldTaint)> {
211 self.fields
212 .iter()
213 .filter(|(path, _)| path.base_var == base_var)
214 .map(|(path, taint)| (path.clone(), taint.clone()))
215 .collect()
216 }
217
218 pub fn has_tainted_field(&self, base_var: &str) -> bool {
220 self.fields
221 .iter()
222 .any(|(path, taint)| path.base_var == base_var && taint.is_tainted())
223 }
224
225 pub fn merge(&mut self, other: &FieldTaintMap) {
231 for (path, other_taint) in &other.fields {
232 let current_taint = self.get_field_taint(path);
233
234 let merged_taint = match (¤t_taint, other_taint) {
236 (FieldTaint::Tainted { .. }, _) => current_taint,
237 (_, FieldTaint::Tainted { .. }) => other_taint.clone(),
238 (FieldTaint::Sanitized { .. }, _) => current_taint,
239 (_, FieldTaint::Sanitized { .. }) => other_taint.clone(),
240 (FieldTaint::Clean, _) => current_taint,
241 _ => other_taint.clone(),
242 };
243
244 self.fields.insert(path.clone(), merged_taint);
245 }
246 }
247
248 pub fn clear(&mut self) {
250 self.fields.clear();
251 }
252
253 pub fn len(&self) -> usize {
255 self.fields.len()
256 }
257
258 pub fn is_empty(&self) -> bool {
260 self.fields.is_empty()
261 }
262}
263
264impl Default for FieldTaintMap {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270pub mod parser {
272 use super::*;
273
274 pub fn parse_field_access(expr: &str) -> Option<FieldPath> {
284 let expr = expr.trim();
285
286 if !expr.contains('.') {
288 return None;
289 }
290
291 if expr.contains('(') {
293 if let Some(path) = parse_nested_field_access(expr) {
295 return Some(path);
296 }
297
298 if let Some(path) = parse_downcast_field_access(expr) {
300 return Some(path);
301 }
302
303 if let Some(path) = parse_simple_field_access(expr) {
305 return Some(path);
306 }
307 }
308
309 parse_dot_notation(expr)
311 }
312
313 fn parse_dot_notation(expr: &str) -> Option<FieldPath> {
315 let expr = expr.trim();
316
317 if !expr.starts_with('_') {
321 return None;
322 }
323
324 let parts: Vec<&str> = expr.split('.').collect();
325 if parts.len() < 2 {
326 return None;
327 }
328
329 let base_var = parts[0].trim().to_string();
330
331 if !base_var.starts_with('_') {
333 return None;
334 }
335 let digits_only = base_var[1..].chars().all(|c| c.is_ascii_digit());
336 if !digits_only || base_var.len() < 2 {
337 return None;
338 }
339
340 let mut indices = Vec::new();
342 for part in &parts[1..] {
343 if let Ok(index) = part.trim().parse::<usize>() {
344 indices.push(index);
345 } else {
346 return None;
348 }
349 }
350
351 if indices.is_empty() {
352 None
353 } else {
354 Some(FieldPath::new(base_var, indices))
355 }
356 }
357
358 fn parse_downcast_field_access(expr: &str) -> Option<FieldPath> {
360 let expr = expr.trim();
361
362 if !expr.starts_with("((") {
366 return None;
367 }
368
369 let as_pos = expr.find(" as ")?;
371
372 if as_pos <= 2 {
375 return None;
376 }
377 let base_var_raw = expr[2..as_pos].trim();
378
379 let mut clean_base = base_var_raw;
381 while clean_base.starts_with('(') && clean_base.ends_with(')') {
382 clean_base = &clean_base[1..clean_base.len() - 1].trim();
383 }
384
385 let base_var = if clean_base.starts_with('*') {
386 clean_base[1..].trim().to_string()
387 } else {
388 clean_base.to_string()
389 };
390
391 if !base_var.starts_with('_') {
392 return None;
393 }
394
395 let downcast_end = expr[as_pos..].find(").")?;
398 let absolute_downcast_end = as_pos + downcast_end;
399
400 let remaining = &expr[absolute_downcast_end + 1..];
403
404 if !remaining.starts_with('.') {
406 return None;
407 }
408
409 let colon_pos = remaining.find(':')?;
411
412 let index_str = remaining[1..colon_pos].trim();
415
416 if let Ok(index) = index_str.parse::<usize>() {
417 return Some(FieldPath::new(base_var, vec![index]));
421 }
422
423 None
424 }
425
426 fn parse_simple_field_access(expr: &str) -> Option<FieldPath> {
428 let expr = expr.trim();
429
430 if !expr.starts_with('(') {
434 return None;
435 }
436
437 let colon_pos = expr.find(':')?;
439
440 let field_part = &expr[1..colon_pos].trim();
442
443 let parts: Vec<&str> = field_part.split('.').collect();
445
446 if parts.len() < 2 {
447 return None;
448 }
449
450 let base_var = parts[0].trim().to_string();
451
452 let mut indices = Vec::new();
454 for part in &parts[1..] {
455 if let Ok(index) = part.trim().parse::<usize>() {
456 indices.push(index);
457 } else {
458 return None;
460 }
461 }
462
463 if indices.is_empty() {
464 None
465 } else {
466 Some(FieldPath::new(base_var, indices))
467 }
468 }
469
470 fn parse_nested_field_access(expr: &str) -> Option<FieldPath> {
472 let expr = expr.trim();
473
474 if !expr.starts_with("((") {
478 return None;
479 }
480
481 let mut depth = 0;
483 let mut inner_end = 0;
484
485 for (i, ch) in expr.char_indices() {
486 match ch {
487 '(' => depth += 1,
488 ')' => {
489 depth -= 1;
490 if depth == 1 {
491 inner_end = i;
493 break;
494 }
495 }
496 _ => {}
497 }
498 }
499
500 if inner_end == 0 {
501 return None;
502 }
503
504 let inner_expr = &expr[1..inner_end + 1]; let mut base_path = parse_simple_field_access(inner_expr)?;
507
508 let remaining = &expr[inner_end + 1..];
510
511 if !remaining.starts_with('.') {
513 return None;
514 }
515
516 let colon_pos = remaining.find(':')?;
518 let index_str = &remaining[1..colon_pos].trim();
519
520 if let Ok(index) = index_str.parse::<usize>() {
521 base_path.indices.push(index);
522 Some(base_path)
523 } else {
524 None
525 }
526 }
527
528 pub fn contains_field_access(expr: &str) -> bool {
530 let has_mir_style = expr.contains(':')
533 && expr.contains('.')
534 && (expr.contains("(_") || expr.contains("(*_"));
535 if has_mir_style {
536 return true;
537 }
538
539 if expr.starts_with('_') && expr.contains('.') {
542 let parts: Vec<&str> = expr.split('.').collect();
543 if parts.len() >= 2 {
544 return parts[1..].iter().any(|p| p.trim().parse::<usize>().is_ok());
546 }
547 }
548
549 false
550 }
551
552 pub fn extract_base_var(expr: &str) -> Option<String> {
559 let expr = expr.trim();
560
561 if let Some(path) = parse_field_access(expr) {
563 return Some(path.base_var);
564 }
565
566 if expr.starts_with('_') {
568 let var: String = expr
570 .chars()
571 .take_while(|c| *c == '_' || c.is_ascii_digit())
572 .collect();
573 if !var.is_empty() {
574 return Some(var);
575 }
576 }
577
578 for prefix in &["&mut ", "move ", "copy ", "&"] {
581 if expr.starts_with(prefix) {
582 let rest = &expr[prefix.len()..];
583 return extract_base_var(rest);
584 }
585 }
586
587 None
588 }
589
590 pub fn extract_all_field_paths(expr: &str) -> Vec<FieldPath> {
594 let mut paths = Vec::new();
595 let expr = expr.trim();
596
597 let mut search_pos = 0;
599
600 while let Some(paren_start) = expr[search_pos..].find("(_") {
601 let actual_pos = search_pos + paren_start;
602
603 let remaining = &expr[actual_pos..];
605
606 if let Some(colon_pos) = remaining.find(':') {
608 if let Some(close_paren) = remaining[colon_pos..].find(')') {
610 let field_expr = &remaining[..colon_pos + close_paren + 1];
611
612 if let Some(path) = parse_field_access(field_expr) {
613 paths.push(path);
614 }
615 }
616 }
617
618 search_pos = actual_pos + 1;
619 }
620
621 paths
622 }
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
630 fn test_field_path_creation() {
631 let path = FieldPath::new("_1".to_string(), vec![0, 1, 2]);
632 assert_eq!(path.base_var, "_1");
633 assert_eq!(path.indices, vec![0, 1, 2]);
634 }
635
636 #[test]
637 fn test_field_path_to_string() {
638 let path1 = FieldPath::whole_var("_1".to_string());
639 assert_eq!(path1.to_string(), "_1");
640
641 let path2 = FieldPath::single_field("_1".to_string(), 0);
642 assert_eq!(path2.to_string(), "_1.0");
643
644 let path3 = FieldPath::new("_1".to_string(), vec![1, 2]);
645 assert_eq!(path3.to_string(), "_1.1.2");
646 }
647
648 #[test]
649 fn test_field_path_is_prefix() {
650 let path1 = FieldPath::whole_var("_1".to_string());
651 let path2 = FieldPath::single_field("_1".to_string(), 0);
652 let path3 = FieldPath::new("_1".to_string(), vec![0, 1]);
653 let path4 = FieldPath::new("_1".to_string(), vec![1, 2]);
654
655 assert!(path1.is_prefix_of(&path2));
656 assert!(path1.is_prefix_of(&path3));
657 assert!(path2.is_prefix_of(&path3));
658 assert!(!path2.is_prefix_of(&path4));
659 assert!(!path3.is_prefix_of(&path2));
660 }
661
662 #[test]
663 fn test_field_path_parent() {
664 let path1 = FieldPath::new("_1".to_string(), vec![1, 2]);
665 let parent1 = path1.parent().unwrap();
666 assert_eq!(parent1.to_string(), "_1.1");
667
668 let parent2 = parent1.parent().unwrap();
669 assert_eq!(parent2.to_string(), "_1");
670
671 assert!(parent2.parent().is_none());
672 }
673
674 #[test]
675 fn test_field_taint_map_basic() {
676 let mut map = FieldTaintMap::new();
677
678 let path = FieldPath::single_field("_1".to_string(), 0);
679 map.set_field_taint(
680 path.clone(),
681 FieldTaint::Tainted {
682 source_type: "environment".to_string(),
683 source_location: "env::args".to_string(),
684 },
685 );
686
687 let taint = map.get_field_taint(&path);
688 assert!(taint.is_tainted());
689 }
690
691 #[test]
692 fn test_field_taint_map_inheritance() {
693 let mut map = FieldTaintMap::new();
694
695 let parent = FieldPath::single_field("_1".to_string(), 1);
697 map.set_field_taint(
698 parent,
699 FieldTaint::Tainted {
700 source_type: "environment".to_string(),
701 source_location: "test".to_string(),
702 },
703 );
704
705 let child = FieldPath::new("_1".to_string(), vec![1, 2]);
707 let taint = map.get_field_taint(&child);
708 assert!(taint.is_tainted());
709 }
710
711 #[test]
712 fn test_set_var_taint() {
713 let mut map = FieldTaintMap::new();
714
715 map.set_field_taint(
717 FieldPath::single_field("_1".to_string(), 0),
718 FieldTaint::Clean,
719 );
720 map.set_field_taint(
721 FieldPath::single_field("_1".to_string(), 1),
722 FieldTaint::Clean,
723 );
724
725 map.set_var_taint(
727 "_1",
728 FieldTaint::Tainted {
729 source_type: "test".to_string(),
730 source_location: "test".to_string(),
731 },
732 );
733
734 assert!(map
736 .get_field_taint(&FieldPath::single_field("_1".to_string(), 0))
737 .is_tainted());
738 assert!(map
739 .get_field_taint(&FieldPath::single_field("_1".to_string(), 1))
740 .is_tainted());
741 }
742
743 #[test]
744 fn test_merge() {
745 let mut map1 = FieldTaintMap::new();
746 map1.set_field_taint(
747 FieldPath::single_field("_1".to_string(), 0),
748 FieldTaint::Tainted {
749 source_type: "test".to_string(),
750 source_location: "test".to_string(),
751 },
752 );
753
754 let mut map2 = FieldTaintMap::new();
755 map2.set_field_taint(
756 FieldPath::single_field("_1".to_string(), 1),
757 FieldTaint::Tainted {
758 source_type: "test2".to_string(),
759 source_location: "test2".to_string(),
760 },
761 );
762
763 map1.merge(&map2);
764
765 assert!(map1
766 .get_field_taint(&FieldPath::single_field("_1".to_string(), 0))
767 .is_tainted());
768 assert!(map1
769 .get_field_taint(&FieldPath::single_field("_1".to_string(), 1))
770 .is_tainted());
771 }
772
773 #[test]
776 fn test_parse_simple_field_access() {
777 use parser::parse_field_access;
778
779 let path = parse_field_access("(_1.0: std::string::String)").unwrap();
780 assert_eq!(path.base_var, "_1");
781 assert_eq!(path.indices, vec![0]);
782
783 let path = parse_field_access("(_1.1: std::string::String)").unwrap();
784 assert_eq!(path.base_var, "_1");
785 assert_eq!(path.indices, vec![1]);
786
787 let path = parse_field_access("(_3.2: u32)").unwrap();
788 assert_eq!(path.base_var, "_3");
789 assert_eq!(path.indices, vec![2]);
790 }
791
792 #[test]
793 fn test_parse_nested_field_access() {
794 use parser::parse_field_access;
795
796 let path = parse_field_access("((_1.1: Credentials).0: std::string::String)").unwrap();
797 assert_eq!(path.base_var, "_1");
798 assert_eq!(path.indices, vec![1, 0]);
799
800 let path = parse_field_access("((_1.1: Credentials).2: std::string::String)").unwrap();
801 assert_eq!(path.base_var, "_1");
802 assert_eq!(path.indices, vec![1, 2]);
803 }
804
805 #[test]
806 fn test_parse_field_access_invalid() {
807 use parser::parse_field_access;
808
809 assert!(parse_field_access("_1").is_none());
811 assert!(parse_field_access("move _2").is_none());
812 assert!(parse_field_access("copy _3").is_none());
813
814 assert!(parse_field_access("(_1: Type)").is_none());
816 }
817
818 #[test]
819 fn test_contains_field_access() {
820 use parser::contains_field_access;
821
822 assert!(contains_field_access("(_1.0: String)"));
823 assert!(contains_field_access("((_1.1: Type).2: Type2)"));
824 assert!(!contains_field_access("_1"));
825 assert!(!contains_field_access("move _2"));
826 }
827
828 #[test]
829 fn test_extract_base_var() {
830 use parser::extract_base_var;
831
832 assert_eq!(extract_base_var("(_1.0: Type)").unwrap(), "_1");
833 assert_eq!(extract_base_var("_2").unwrap(), "_2");
834 assert_eq!(extract_base_var("move _3").unwrap(), "_3");
835 assert_eq!(extract_base_var("copy _4").unwrap(), "_4");
836 assert_eq!(extract_base_var("&_5").unwrap(), "_5");
837 assert_eq!(extract_base_var("&mut _6").unwrap(), "_6");
838 }
839
840 #[test]
841 fn test_extract_all_field_paths() {
842 use parser::extract_all_field_paths;
843
844 let paths = extract_all_field_paths("(_1.0: String) = move (_2.1: String)");
845 assert_eq!(paths.len(), 2);
846 assert_eq!(paths[0].to_string(), "_1.0");
847 assert_eq!(paths[1].to_string(), "_2.1");
848
849 let paths = extract_all_field_paths("_3 = Command::arg(copy _4, copy (_5.2: String))");
850 assert_eq!(paths.len(), 1);
851 assert_eq!(paths[0].to_string(), "_5.2");
852 }
853}