oxide_cli/addons/
detect.rs1use std::path::Path;
2
3use crate::addons::manifest::{DetectBlock, DetectRule, MatchMode};
4
5pub 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(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(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}