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 {
27      file,
28      contains,
29      negate,
30    } => {
31      let result = std::fs::read_to_string(project_root.join(file))
32        .map(|s| s.contains(contains.as_str()))
33        .unwrap_or(false);
34      if *negate { !result } else { result }
35    }
36
37    DetectRule::JsonContains {
38      file,
39      key_path,
40      value,
41      negate,
42    } => {
43      let result = std::fs::read_to_string(project_root.join(file))
44        .ok()
45        .and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
46        .map(|v| traverse_json(&v, key_path, value.as_deref()))
47        .unwrap_or(false);
48      if *negate { !result } else { result }
49    }
50
51    DetectRule::TomlContains {
52      file,
53      key_path,
54      value,
55      negate,
56    } => {
57      let result = std::fs::read_to_string(project_root.join(file))
58        .ok()
59        .and_then(|s| toml::from_str::<toml::Value>(&s).ok())
60        .map(|v| traverse_toml(&v, key_path, value.as_deref()))
61        .unwrap_or(false);
62      if *negate { !result } else { result }
63    }
64
65    DetectRule::YamlContains {
66      file,
67      key_path,
68      value,
69      negate,
70    } => {
71      let result = std::fs::read_to_string(project_root.join(file))
72        .ok()
73        .and_then(|s| serde_yaml::from_str::<serde_yaml::Value>(&s).ok())
74        .map(|v| traverse_yaml(&v, key_path, value.as_deref()))
75        .unwrap_or(false);
76      if *negate { !result } else { result }
77    }
78  }
79}
80
81fn traverse_json(mut v: &serde_json::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      serde_json::Value::String(s) => s == expected,
92      // Allow: intentionally converts numeric/boolean JSON values to their
93      // string representation so manifests can match e.g. `value: "true"`.
94      #[allow(clippy::cmp_owned)]
95      other => other.to_string() == expected,
96    },
97  }
98}
99
100fn traverse_toml(mut v: &toml::Value, key_path: &str, expected: Option<&str>) -> bool {
101  for key in key_path.split('.') {
102    match v.get(key) {
103      Some(next) => v = next,
104      None => return false,
105    }
106  }
107  match expected {
108    None => true,
109    Some(expected) => match v {
110      toml::Value::String(s) => s == expected,
111      // Allow: intentionally converts numeric/boolean TOML values to string.
112      #[allow(clippy::cmp_owned)]
113      other => other.to_string() == expected,
114    },
115  }
116}
117
118fn traverse_yaml(mut v: &serde_yaml::Value, key_path: &str, expected: Option<&str>) -> bool {
119  for key in key_path.split('.') {
120    match v.get(key) {
121      Some(next) => v = next,
122      None => return false,
123    }
124  }
125  match expected {
126    None => true,
127    Some(expected) => match v {
128      serde_yaml::Value::String(s) => s == expected,
129      other => format!("{other:?}") == expected,
130    },
131  }
132}