1use anyhow::{bail, Result};
2use log::debug;
3use thiserror::Error;
4
5use std::collections::BTreeMap;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug)]
9#[cfg_attr(test, derive(Default))]
10pub struct Script {
11 pub shadow_dir: PathBuf,
12 pub ignores: Vec<String>,
13 pub effectors: Effectors,
14 pub paths: PathContentMap,
15}
16
17pub type Effectors = BTreeMap<String, Vec<String>>;
18pub type PathContentMap = BTreeMap<String, String>;
19
20impl Script {
21 pub fn parse_ncl_file(ncl_path: &Path) -> Result<Self> {
22 let mut toml = parse_ncl::from_file(&ncl_path)?;
23 let ncl_parent = if let Some(p) = ncl_path.parent() {
24 p.to_owned()
25 } else {
26 PathBuf::from(".")
27 };
28 Self::parse_toml(&mut toml, &ncl_parent)
29 }
30
31 fn parse_toml(toml: &mut toml::Table, base_dir: &Path) -> Result<Self> {
32 let raw_shadow_dir = if let Some(dir) = toml.remove("shadow_dir") {
37 let toml::Value::String(dir) = dir else {
38 bail!("Expected 'shadow_dir' to be text, got: {dir:?}");
39 };
40 dir
41 } else {
42 ".".to_string()
43 };
44 let shadow_dir = base_dir.join(&raw_shadow_dir);
46 debug!("SHAD: {shadow_dir:?} (from {raw_shadow_dir:?})");
47
48 let mut ignores = Vec::<String>::new();
49 if let Some(raw_ignores) = toml.remove("ignores") {
50 let toml::Value::Array(raw_ignores) = raw_ignores else {
51 bail!("Expected 'ignores' to be array, got: {raw_ignores:?}");
52 };
53 for (i, v) in raw_ignores.into_iter().enumerate() {
54 let toml::Value::String(s) = v else {
55 bail!("Unexpected type of ignores[{i}], want String, got: {v:?}");
56 };
57 ignores.push(s);
58 }
59 }
60
61 let Some(raw_effectors) = toml.remove("effectors") else {
64 bail!("Missing 'effectors' in stdin");
65 };
66 let toml::Value::Table(raw_effectors) = raw_effectors else {
67 bail!("Expected 'effectors' to be table, got: {raw_effectors:?}");
68 };
69
70 let Some(raw_tree) = toml.remove("tree") else {
73 bail!("Missing 'tree' in stdin");
74 };
75 let toml::Value::Table(raw_tree) = raw_tree else {
76 bail!("Expected 'tree' to be table, got: {raw_tree:?}");
77 };
78
79 let mut effectors = Effectors::new();
82 for (k, v) in raw_effectors {
83 let toml::Value::String(s) = v else {
84 bail!("Unexpected type of effector {k:?}, want String, got: {v:?}");
85 };
86 effectors.insert(k, s.split_whitespace().map(str::to_string).collect());
87 }
88 debug!("HANDL: {effectors:?}");
89
90 let mut paths = PathContentMap::new();
92 let mut todo = vec![(String::new(), raw_tree)];
93 loop {
94 let Some((parent, subtree)) = todo.pop() else {
95 break;
96 };
97 for (key, value) in subtree {
98 let path = parent.clone() + &key;
99 match value {
100 toml::Value::String(s) => {
101 paths.insert(path, s);
102 }
103 toml::Value::Table(t) => {
104 todo.push((path + "/", t));
105 }
106 _ => {
107 bail!("Unexpected type of value at {path:?} in tree: {value}");
108 }
109 }
110 }
111 }
112 Ok(Script {
118 shadow_dir: shadow_dir.into(),
119 ignores,
120 effectors,
121 paths,
122 })
123 }
124
125 pub fn validate(&self) -> ValidationResult {
126 use ValidationError::*;
127 fn path_error_of(p: &String) -> Option<ValidationError> {
130 if p.ends_with("/") {
131 return Some(TrailingSlashInPath(p.clone()));
132 } else if p.contains("//") {
133 return Some(DoubleSlashInPath(p.clone()));
134 } else if p.contains("/../") {
135 return Some(DoubleDotInPath(p.clone()));
136 }
137 None
138 }
139 if let Some(err) = self.paths.keys().flat_map(path_error_of).next() {
140 return Err(err);
141 }
142 Ok(())
143 }
144
145 pub fn ignores_path(&self, path: &str) -> bool {
146 let first_segment_of_path = path.split('/').next().unwrap();
147 self.ignores.iter().any(|ign| ign == first_segment_of_path)
148 }
149}
150
151#[derive(Error, Debug)]
152#[cfg_attr(test, derive(PartialEq))]
153pub enum ValidationError {
154 #[error("path `{0}` contains double slash `//`")]
155 DoubleSlashInPath(String),
156 #[error("path `{0}` ends with a slash `/`")]
157 TrailingSlashInPath(String),
158 #[error("path `{0}` contains double dot `/../`")]
159 DoubleDotInPath(String),
160}
161
162type ValidationResult = std::result::Result<(), ValidationError>;
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn s(string: &str) -> String {
169 string.to_string()
170 }
171
172 #[test]
173 fn validate_problems_in_paths() {
174 fn vsp<'a>(paths: impl IntoIterator<Item = &'a str>) -> ValidationResult {
176 Script {
177 paths: paths
178 .into_iter()
179 .map(|s| (s.to_string(), "".to_string()))
180 .collect(),
181 ..<_>::default()
182 }
183 .validate()
184 }
185
186 use ValidationError::*;
187 assert_eq!(vsp(["a/../b"]), Err(DoubleDotInPath(s("a/../b"))));
188 assert_eq!(vsp(["foo//bar"]), Err(DoubleSlashInPath(s("foo//bar"))));
189 assert_eq!(
190 vsp(["foo/bar//baz"]),
191 Err(DoubleSlashInPath(s("foo/bar//baz")))
192 );
193 assert_eq!(
194 vsp(["ok_a/ok_b", "foo/bar//baz"]),
195 Err(DoubleSlashInPath(s("foo/bar//baz")))
196 );
197 assert_eq!(
198 vsp(["ok_a/ok_b", "foo/bar/"]),
199 Err(TrailingSlashInPath(s("foo/bar/")))
200 );
201 assert_eq!(
202 vsp(["ok_a/ok_b", "foo/"]),
203 Err(TrailingSlashInPath(s("foo/")))
204 );
205 }
206}