1use super::reports::Mirror;
4use crate::codec::Format;
5use crate::model::{ExceptionItem, ExceptionList, Rule};
6use crate::normalize;
7use elasticctl_core::{Error, ErrorKind, Result};
8use serde_json::Value;
9use std::path::{Path, PathBuf};
10
11pub fn read_mirror(dir: &Path) -> Result<Mirror> {
19 let mut mirror = Mirror {
20 rules: Vec::new(),
21 lists: Vec::new(),
22 items: Vec::new(),
23 };
24
25 let rules_path = super::rules_dir(dir);
26 if let Some(paths) = mirror_root_files(&rules_path)? {
27 for path in paths {
28 let body = std::fs::read_to_string(&path).map_err(|e| {
29 Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display()))
30 })?;
31 let (mut rules, lists, items) = decode_rule_file(&body, Format::from_path(&path))?;
32 mirror.rules.append(&mut rules);
33 for mut list in lists {
34 let list_items = split_items(&mut list)?;
35 mirror.lists.push(list);
36 mirror.items.extend(list_items);
37 }
38 for item in &items {
39 validate_top_level_item(item)?;
40 }
41 mirror.items.extend(items);
42 }
43 }
44
45 let lists_path = super::exceptions_dir(dir);
46 if let Some(paths) = mirror_root_files(&lists_path)? {
47 for path in paths {
48 let body = std::fs::read_to_string(&path).map_err(|e| {
49 Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display()))
50 })?;
51 let mut list = decode_list_file(&body, Format::from_path(&path))?;
52 let items = split_items(&mut list)?;
53 mirror.lists.push(list);
54 mirror.items.extend(items);
55 }
56 }
57
58 normalize::sort_rules(&mut mirror.rules);
59 normalize::sort_lists(&mut mirror.lists);
60 normalize::sort_items(&mut mirror.items);
61 Ok(mirror)
62}
63
64fn validate_top_level_item(item: &ExceptionItem) -> Result<()> {
65 let item_id = item.item_id()?;
66 if item_id.is_empty() {
67 return Err(Error::new(
68 ErrorKind::Error,
69 "exception item field item_id must be a non-empty string",
70 ));
71 }
72 let list_id = item.list_id()?;
73 if list_id.is_empty() {
74 return Err(Error::new(
75 ErrorKind::Error,
76 "exception item field list_id must be a non-empty string",
77 ));
78 }
79 match item.as_map().get("namespace_type") {
80 None => Ok(()),
81 Some(Value::String(value)) if !value.is_empty() => Ok(()),
82 Some(_) => Err(Error::new(
83 ErrorKind::Error,
84 "exception item field namespace_type must be a non-empty string",
85 )),
86 }
87}
88
89pub fn read_local(dir: &Path) -> Result<Vec<Rule>> {
91 Ok(read_mirror(dir)?.rules)
92}
93
94fn mirror_root_files(dir: &Path) -> Result<Option<Vec<PathBuf>>> {
99 match std::fs::symlink_metadata(dir) {
100 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
101 Err(e) => Err(Error::new(
102 ErrorKind::Error,
103 format!("reading {}: {e}", dir.display()),
104 )),
105 Ok(metadata) => {
106 if metadata.file_type().is_symlink() || !metadata.is_dir() {
107 return Err(Error::new(
108 ErrorKind::Error,
109 format!("mirror directory {} is not a real directory", dir.display()),
110 ));
111 }
112 Ok(Some(mirror_files(dir)?))
113 }
114 }
115}
116
117fn mirror_files(dir: &Path) -> Result<Vec<PathBuf>> {
119 let mut out = Vec::new();
120 for entry in std::fs::read_dir(dir)
121 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", dir.display())))?
122 {
123 if let Some(path) = mirror_entry_path(dir, entry)? {
124 out.push(path);
125 }
126 }
127 out.sort();
128 Ok(out)
129}
130
131fn mirror_entry_path(
135 dir: &Path,
136 entry: std::io::Result<std::fs::DirEntry>,
137) -> Result<Option<PathBuf>> {
138 let entry = entry
139 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", dir.display())))?;
140 let path = entry.path();
141 if !super::is_rule_file(&path) {
142 return Ok(None);
143 }
144 let file_type = entry
147 .file_type()
148 .map_err(|e| Error::new(ErrorKind::Error, format!("reading {}: {e}", path.display())))?;
149 if !file_type.is_file() {
150 return Err(Error::new(
151 ErrorKind::Error,
152 format!("mirror entry {} is not a regular file", path.display()),
153 ));
154 }
155 Ok(Some(path))
156}
157
158fn decode_rule_file(
166 body: &str,
167 format: Format,
168) -> Result<(Vec<Rule>, Vec<ExceptionList>, Vec<ExceptionItem>)> {
169 match format {
170 Format::Ndjson => {
171 let bundle = crate::codec::decode_bundle(body)?;
172 Ok((bundle.rules, bundle.lists, bundle.items))
173 }
174 Format::Yaml => {
175 let values: Vec<Value> = serde_yaml_ng::from_str(body)
176 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing YAML: {e}")))?;
177 let mut rules = Vec::new();
178 let mut lists = Vec::new();
179 let mut items = Vec::new();
180 for value in values {
181 if value.get("rule_id").is_some() {
185 rules.push(Rule::from_value(value)?);
186 } else if value.get("item_id").is_some() {
187 items.push(ExceptionItem::from_value(value)?);
188 } else if value.get("list_id").is_some() {
189 lists.push(ExceptionList::from_value(value)?);
190 } else {
191 return Err(Error::new(
192 ErrorKind::Error,
193 "a mirror file entry has neither rule_id, item_id, nor list_id",
194 ));
195 }
196 }
197 Ok((rules, lists, items))
198 }
199 }
200}
201
202pub(crate) fn encode_rule_file(
205 rule: &Rule,
206 inline_lists: &[ExceptionList],
207 format: Format,
208) -> Result<String> {
209 let mut objects = Vec::with_capacity(1 + inline_lists.len());
210 objects.push(rule.clone().into_value());
211 for list in inline_lists {
212 objects.push(list.clone().into_value());
213 }
214 match format {
215 Format::Yaml => serde_yaml_ng::to_string(&objects)
216 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding YAML: {e}"))),
217 Format::Ndjson => {
218 let mut out = String::new();
219 for object in &objects {
220 out.push_str(
221 &serde_json::to_string(object).map_err(|e| {
222 Error::new(ErrorKind::Error, format!("encoding NDJSON: {e}"))
223 })?,
224 );
225 out.push('\n');
226 }
227 Ok(out)
228 }
229 }
230}
231
232pub(crate) fn encode_list_file(list: &ExceptionList, format: Format) -> Result<String> {
235 match format {
236 Format::Yaml => serde_yaml_ng::to_string(list.as_map())
237 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding YAML: {e}"))),
238 Format::Ndjson => Ok(format!(
239 "{}\n",
240 serde_json::to_string(list.as_map())
241 .map_err(|e| Error::new(ErrorKind::Error, format!("encoding NDJSON: {e}")))?
242 )),
243 }
244}
245
246fn decode_list_file(body: &str, format: Format) -> Result<ExceptionList> {
247 let value = match format {
248 Format::Yaml => serde_yaml_ng::from_str(body)
249 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing exception list: {e}")))?,
250 Format::Ndjson => {
251 let mut lines = body.lines().filter(|line| !line.trim().is_empty());
252 let line = lines
253 .next()
254 .ok_or_else(|| Error::new(ErrorKind::Error, "empty exception list file"))?;
255 if lines.next().is_some() {
256 return Err(Error::new(
257 ErrorKind::Error,
258 "an exception mirror file must contain exactly one nonblank NDJSON object",
259 ));
260 }
261 serde_json::from_str(line.trim())
262 .map_err(|e| Error::new(ErrorKind::Error, format!("parsing exception list: {e}")))?
263 }
264 };
265 ExceptionList::from_value(value)
266}
267
268fn split_items(list: &mut ExceptionList) -> Result<Vec<ExceptionItem>> {
271 let items = match list.as_map_mut().remove("items") {
272 None => return Ok(Vec::new()),
273 Some(Value::Array(items)) => items,
274 Some(_) => {
275 return Err(Error::new(
276 ErrorKind::Error,
277 "exception list field items must be an array",
278 ));
279 }
280 };
281 let list_id = list.list_id()?.to_string();
282 let namespace = list.namespace_type().to_string();
283 items
284 .into_iter()
285 .map(|value| {
286 let mut item = ExceptionItem::from_value(value)?;
287 item.as_map_mut()
292 .insert("list_id".into(), Value::String(list_id.clone()));
293 item.as_map_mut()
294 .insert("namespace_type".into(), Value::String(namespace.clone()));
295 Ok(item)
296 })
297 .collect()
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn mirror_entry_path_reports_a_directory_read_error() {
306 let dir = Path::new("/mirror");
307 let err = mirror_entry_path(
308 dir,
309 Err(std::io::Error::new(
310 std::io::ErrorKind::PermissionDenied,
311 "denied",
312 )),
313 )
314 .unwrap_err();
315 assert_eq!(err.kind, ErrorKind::Error);
316 assert!(err.message.contains("/mirror"), "{}", err.message);
317 }
318}