Skip to main content

oxide_cli/addons/
detect.rs

1use std::path::Path;
2
3use crate::addons::manifest::{DetectBlock, DetectRule, MatchMode};
4
5/// Returns the ID of the first matching DetectBlock, or None (→ use universal variant).
6pub fn detect_variant(detect: &[DetectBlock], project_root: &Path) -> Option<String> {
7  for block in detect {
8    let matches = match block.match_mode {
9      MatchMode::All => block.rules.iter().all(|r| eval_rule(r, project_root)),
10      MatchMode::Any => block.rules.iter().any(|r| eval_rule(r, project_root)),
11    };
12    if matches {
13      return Some(block.id.clone());
14    }
15  }
16  None
17}
18
19fn eval_rule(rule: &DetectRule, project_root: &Path) -> bool {
20  match rule {
21    DetectRule::FileExists { file, negate } => {
22      let result = project_root.join(file).exists();
23      if *negate { !result } else { result }
24    }
25
26    DetectRule::FileContains { file, contains, negate } => {
27      let result = std::fs::read_to_string(project_root.join(file))
28        .map(|s| s.contains(contains.as_str()))
29        .unwrap_or(false);
30      if *negate { !result } else { result }
31    }
32
33    DetectRule::JsonContains { file, key_path, value, negate } => {
34      let result = std::fs::read_to_string(project_root.join(file))
35        .ok()
36        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
37        .map(|v| traverse_json(&v, key_path, value.as_deref()))
38        .unwrap_or(false);
39      if *negate { !result } else { result }
40    }
41
42    DetectRule::TomlContains { file, key_path, value, negate } => {
43      let result = std::fs::read_to_string(project_root.join(file))
44        .ok()
45        .and_then(|s| toml::from_str::<toml::Value>(&s).ok())
46        .map(|v| traverse_toml(&v, key_path, value.as_deref()))
47        .unwrap_or(false);
48      if *negate { !result } else { result }
49    }
50
51    DetectRule::YamlContains { file, key_path, value, negate } => {
52      let result = std::fs::read_to_string(project_root.join(file))
53        .ok()
54        .and_then(|s| serde_yaml::from_str::<serde_yaml::Value>(&s).ok())
55        .map(|v| traverse_yaml(&v, key_path, value.as_deref()))
56        .unwrap_or(false);
57      if *negate { !result } else { result }
58    }
59  }
60}
61
62fn traverse_json(mut v: &serde_json::Value, key_path: &str, expected: Option<&str>) -> bool {
63  for key in key_path.split('.') {
64    match v.get(key) {
65      Some(next) => v = next,
66      None => return false,
67    }
68  }
69  match expected {
70    None => true,
71    Some(expected) => match v {
72      serde_json::Value::String(s) => s == expected,
73      // Allow: intentionally converts numeric/boolean JSON values to their
74      // string representation so manifests can match e.g. `value: "true"`.
75      #[allow(clippy::cmp_owned)]
76      other => other.to_string() == expected,
77    },
78  }
79}
80
81fn traverse_toml(mut v: &toml::Value, key_path: &str, expected: Option<&str>) -> bool {
82  for key in key_path.split('.') {
83    match v.get(key) {
84      Some(next) => v = next,
85      None => return false,
86    }
87  }
88  match expected {
89    None => true,
90    Some(expected) => match v {
91      toml::Value::String(s) => s == expected,
92      // Allow: intentionally converts numeric/boolean TOML values to string.
93      #[allow(clippy::cmp_owned)]
94      other => other.to_string() == expected,
95    },
96  }
97}
98
99fn traverse_yaml(mut v: &serde_yaml::Value, key_path: &str, expected: Option<&str>) -> bool {
100  for key in key_path.split('.') {
101    match v.get(key) {
102      Some(next) => v = next,
103      None => return false,
104    }
105  }
106  match expected {
107    None => true,
108    Some(expected) => match v {
109      serde_yaml::Value::String(s) => s == expected,
110      other => format!("{other:?}") == expected,
111    },
112  }
113}
114