1#[cfg(feature = "rhai")]
2use crate::error::RhaiErrorKinds;
3
4#[cfg(feature = "rhai")]
5use std::error::Error;
6use std::{
7 fmt::{Debug, Display},
8 str::FromStr,
9};
10
11#[cfg(feature = "jq")]
12use anyhow::{anyhow, bail};
13use bytesize::ByteSize;
14use derive_more::derive::Display;
15use log::warn;
16use rustic_core::{repofile::SnapshotFile, StringList};
17
18use cached::proc_macro::cached;
19use chrono::{DateTime, Local, NaiveTime};
20use conflate::Merge;
21
22#[cfg(feature = "jq")]
23use jaq_core::{
24 load::{Arena, File, Loader},
25 Compiler, Ctx, Filter, Native, RcIter,
26};
27#[cfg(feature = "jq")]
28use jaq_json::Val;
29#[cfg(feature = "rhai")]
30use rhai::{serde::to_dynamic, Dynamic, Engine, FnPtr, AST};
31use serde::{Deserialize, Serialize};
32#[cfg(feature = "jq")]
33use serde_json::Value;
34use serde_with::{serde_as, DisplayFromStr};
35
36#[cfg(feature = "rhai")]
40#[derive(Clone, Debug)]
41pub(crate) struct SnapshotFn(FnPtr, AST);
42
43#[cfg(feature = "rhai")]
44impl FromStr for SnapshotFn {
45 type Err = RhaiErrorKinds;
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 let engine = Engine::new();
48 let ast = engine.compile(s)?;
49 let func = engine.eval_ast::<FnPtr>(&ast)?;
50 Ok(Self(func, ast))
51 }
52}
53
54#[cfg(feature = "rhai")]
55impl SnapshotFn {
56 fn call<T: Clone + Send + Sync + 'static>(
64 &self,
65 sn: &SnapshotFile,
66 ) -> Result<T, Box<dyn Error>> {
67 let engine = Engine::new();
68 let sn: Dynamic = to_dynamic(sn)?;
69 Ok(self.0.call::<T>(&engine, &self.1, (sn,))?)
70 }
71}
72
73#[cfg(feature = "rhai")]
74#[cached(key = "String", convert = r#"{ s.to_string() }"#, size = 1)]
75fn string_to_fn(s: &str) -> Option<SnapshotFn> {
76 match SnapshotFn::from_str(s) {
77 Ok(filter_fn) => Some(filter_fn),
78 Err(err) => {
79 warn!("Error evaluating filter-fn {s}: {err}",);
80 None
81 }
82 }
83}
84
85#[cfg(feature = "jq")]
86#[derive(Clone)]
87pub(crate) struct SnapshotJq(Filter<Native<Val>>);
88
89#[cfg(feature = "jq")]
90impl FromStr for SnapshotJq {
91 type Err = anyhow::Error;
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 let programm = File { code: s, path: () };
94 let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs()));
95 let arena = Arena::default();
96 let modules = loader
97 .load(&arena, programm)
98 .map_err(|errs| anyhow!("errors loading modules in jq: {errs:?}"))?;
99 let filter = Compiler::<_, Native<_>>::default()
100 .with_funs(jaq_std::funs().chain(jaq_json::funs()))
101 .compile(modules)
102 .map_err(|errs| anyhow!("errors during compiling filters in jq: {errs:?}"))?;
103
104 Ok(Self(filter))
105 }
106}
107
108#[cfg(feature = "jq")]
109impl SnapshotJq {
110 fn call(&self, snap: &SnapshotFile) -> Result<bool, anyhow::Error> {
111 let input = serde_json::to_value(snap)?;
112
113 let inputs = RcIter::new(core::iter::empty());
114 let res = self.0.run((Ctx::new([], &inputs), Val::from(input))).next();
115
116 match res {
117 Some(Ok(val)) => {
118 let val: Value = val.into();
119 match val.as_bool() {
120 Some(true) => Ok(true),
121 Some(false) => Ok(false),
122 None => bail!("expression does not return bool"),
123 }
124 }
125 _ => bail!("expression does not return bool"),
126 }
127 }
128}
129
130#[cfg(feature = "jq")]
131#[cached(key = "String", convert = r#"{ s.to_string() }"#, size = 1)]
132fn string_to_jq(s: &str) -> Option<SnapshotJq> {
133 match SnapshotJq::from_str(s) {
134 Ok(filter_jq) => Some(filter_jq),
135 Err(err) => {
136 warn!("Error evaluating filter-fn {s}: {err}",);
137 None
138 }
139 }
140}
141
142#[serde_as]
143#[derive(Clone, Default, Debug, Serialize, Deserialize, Merge, clap::Parser)]
144#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
145pub struct SnapshotFilter {
146 #[clap(long = "filter-host", global = true, value_name = "HOSTNAME")]
148 #[merge(strategy=conflate::vec::overwrite_empty)]
149 filter_hosts: Vec<String>,
150
151 #[clap(long = "filter-label", global = true, value_name = "LABEL")]
153 #[merge(strategy=conflate::vec::overwrite_empty)]
154 filter_labels: Vec<String>,
155
156 #[clap(long, global = true, value_name = "PATH[,PATH,..]")]
158 #[serde_as(as = "Vec<DisplayFromStr>")]
159 #[merge(strategy=conflate::vec::overwrite_empty)]
160 filter_paths: Vec<StringList>,
161
162 #[clap(long, global = true, value_name = "PATH[,PATH,..]")]
164 #[serde_as(as = "Vec<DisplayFromStr>")]
165 #[merge(strategy=conflate::vec::overwrite_empty)]
166 filter_paths_exact: Vec<StringList>,
167
168 #[clap(long, global = true, value_name = "TAG[,TAG,..]")]
170 #[serde_as(as = "Vec<DisplayFromStr>")]
171 #[merge(strategy=conflate::vec::overwrite_empty)]
172 filter_tags: Vec<StringList>,
173
174 #[clap(long, global = true, value_name = "TAG[,TAG,..]")]
176 #[serde_as(as = "Vec<DisplayFromStr>")]
177 #[merge(strategy=conflate::vec::overwrite_empty)]
178 filter_tags_exact: Vec<StringList>,
179
180 #[serde_as(as = "Option<DisplayFromStr>")]
182 #[clap(long, global = true, value_name = "DATE(TIME)")]
183 #[merge(strategy=conflate::option::overwrite_none)]
184 filter_after: Option<AfterDate>,
185
186 #[serde_as(as = "Option<DisplayFromStr>")]
188 #[clap(long, global = true, value_name = "DATE(TIME)")]
189 #[merge(strategy=conflate::option::overwrite_none)]
190 filter_before: Option<BeforeDate>,
191
192 #[serde_as(as = "Option<DisplayFromStr>")]
194 #[clap(long, global = true, value_name = "SIZE")]
195 #[merge(strategy=conflate::option::overwrite_none)]
196 filter_size: Option<SizeRange>,
197
198 #[serde_as(as = "Option<DisplayFromStr>")]
200 #[clap(long, global = true, value_name = "SIZE")]
201 #[merge(strategy=conflate::option::overwrite_none)]
202 filter_size_added: Option<SizeRange>,
203
204 #[cfg(feature = "rhai")]
206 #[clap(long, global = true, value_name = "FUNC")]
207 #[serde_as(as = "Option<DisplayFromStr>")]
208 #[merge(strategy=conflate::option::overwrite_none)]
209 filter_fn: Option<String>,
210
211 #[cfg(feature = "jq")]
213 #[clap(long, global = true, value_name = "JQ")]
214 #[serde_as(as = "Option<DisplayFromStr>")]
215 #[merge(strategy=conflate::option::overwrite_none)]
216 filter_jq: Option<String>,
217}
218
219impl SnapshotFilter {
220 #[must_use]
230 pub fn matches(&self, snapshot: &SnapshotFile) -> bool {
231 #[cfg(feature = "rhai")]
232 if let Some(filter_fn) = &self.filter_fn {
233 if let Some(func) = string_to_fn(filter_fn) {
234 match func.call::<bool>(snapshot) {
235 Ok(result) => {
236 if !result {
237 return false;
238 }
239 }
240 Err(err) => {
241 warn!(
242 "Error evaluating filter-fn for snapshot {}: {err}",
243 snapshot.id
244 );
245 return false;
246 }
247 }
248 }
249 }
250 #[cfg(feature = "jq")]
251 if let Some(filter_jq) = &self.filter_jq {
252 if let Some(jq) = string_to_jq(filter_jq) {
253 match jq.call(snapshot) {
254 Ok(result) => {
255 if !result {
256 return false;
257 }
258 }
259 Err(err) => {
260 warn!(
261 "Error evaluating filter-jq for snapshot {}: {err}",
262 snapshot.id
263 );
264 return false;
265 }
266 }
267 }
268 }
269
270 if matches!(&self.filter_after, Some(after) if !after.matches(snapshot.time))
272 || matches!(&self.filter_before, Some(before) if !before.matches(snapshot.time))
273 || matches!((&self.filter_size,&snapshot.summary), (Some(size),Some(summary)) if !size.matches(summary.total_bytes_processed))
274 || matches!((&self.filter_size_added,&snapshot.summary), (Some(size),Some(summary)) if !size.matches(summary.data_added))
275 {
276 return false;
277 }
278
279 snapshot.paths.matches(&self.filter_paths)
283 && snapshot.tags.matches(&self.filter_tags)
284 && (self.filter_paths_exact.is_empty()
288 || self.filter_paths_exact.contains(&snapshot.paths))
289 && (self.filter_tags_exact.is_empty()
290 || self.filter_tags_exact.contains(&snapshot.tags))
291 && (self.filter_hosts.is_empty() || self.filter_hosts.contains(&snapshot.hostname))
292 && (self.filter_labels.is_empty() || self.filter_labels.contains(&snapshot.label))
293 }
294}
295
296#[derive(Debug, Clone, Display)]
297struct AfterDate(DateTime<Local>);
298
299impl AfterDate {
300 fn matches(&self, datetime: DateTime<Local>) -> bool {
301 self.0 < datetime
302 }
303}
304
305impl FromStr for AfterDate {
306 type Err = anyhow::Error;
307 fn from_str(s: &str) -> Result<Self, Self::Err> {
308 let before_midnight = NaiveTime::from_hms_nano_opt(23, 59, 59, 999_999_999).unwrap();
309 let datetime = dateparser::parse_with(s, &Local, before_midnight)?;
310 Ok(Self(datetime.into()))
311 }
312}
313
314#[derive(Debug, Clone, Display)]
315struct BeforeDate(DateTime<Local>);
316
317impl BeforeDate {
318 fn matches(&self, datetime: DateTime<Local>) -> bool {
319 datetime < self.0
320 }
321}
322
323impl FromStr for BeforeDate {
324 type Err = anyhow::Error;
325 fn from_str(s: &str) -> Result<Self, Self::Err> {
326 let midnight = NaiveTime::from_hms_opt(0, 0, 0).unwrap();
327 let datetime = dateparser::parse_with(s, &Local, midnight)?;
328 Ok(Self(datetime.into()))
329 }
330}
331
332#[derive(Debug, Clone)]
333struct SizeRange {
334 from: Option<ByteSize>,
335 to: Option<ByteSize>,
336}
337
338impl SizeRange {
339 fn matches(&self, size: u64) -> bool {
340 !matches!(self.from, Some(from) if size < from.0)
343 && !matches!(self.to, Some(to) if size > to.0)
345 }
346}
347
348fn parse_size(s: &str) -> Result<Option<ByteSize>, String> {
349 let s = s.trim();
350 if s.is_empty() {
351 return Ok(None);
352 }
353 Ok(Some(s.parse()?))
354}
355
356impl FromStr for SizeRange {
357 type Err = String;
358 fn from_str(s: &str) -> Result<Self, Self::Err> {
359 let (from, to) = match s.split_once("..") {
360 Some((s1, s2)) => (parse_size(s1)?, parse_size(s2)?),
361 None => (parse_size(s)?, None),
362 };
363 Ok(Self { from, to })
364 }
365}
366
367impl Display for SizeRange {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 if let Some(from) = self.from {
370 f.write_str(&from.to_string_as(true))?;
371 }
372 f.write_str("..")?;
373 if let Some(to) = self.to {
374 f.write_str(&to.to_string_as(true))?;
375 }
376
377 Ok(())
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384 use rstest::rstest;
385
386 #[rstest]
387 #[case("..", None, None)]
388 #[case("10", Some(10), None)]
389 #[case("..10k", None, Some(10_000))]
390 #[case("1MB..", Some(1_000_000), None)]
391 #[case("1 MB .. 1 GiB", Some(1_000_000), Some(1_073_741_824))]
392 #[case("10 .. 20 ", Some(10), Some(20))]
393 #[case(" 2G ", Some(2_000_000_000), None)]
394 fn size_range_from_str(
395 #[case] input: SizeRange,
396 #[case] from: Option<u64>,
397 #[case] to: Option<u64>,
398 ) {
399 assert_eq!(input.from.map(|v| v.0), from);
400 assert_eq!(input.to.map(|v| v.0), to);
401 }
402}