Skip to main content

dysk_cli/
col_expr.rs

1use {
2    crate::col::*,
3    lfs_core::*,
4    std::{
5        fmt,
6        str::FromStr,
7    },
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ColOperator {
12    Lower,
13    LowerOrEqual,
14    Like,
15    Equal,
16    NotEqual,
17    GreaterOrEqual,
18    Greater,
19}
20
21impl ColOperator {
22    pub fn eval<T: PartialOrd + PartialEq>(
23        self,
24        a: T,
25        b: T,
26    ) -> bool {
27        match self {
28            Self::Lower => a < b,
29            Self::LowerOrEqual => a <= b,
30            Self::Equal | Self::Like => a == b,
31            Self::NotEqual => a != b,
32            Self::GreaterOrEqual => a >= b,
33            Self::Greater => a > b,
34        }
35    }
36    pub fn eval_option<T: PartialOrd + PartialEq>(
37        self,
38        a: Option<T>,
39        b: T,
40    ) -> bool {
41        match a {
42            Some(a) => self.eval(a, b),
43            None => false,
44        }
45    }
46    pub fn eval_str(
47        self,
48        a: &str,
49        b: &str,
50    ) -> bool {
51        match self {
52            Self::Like => a.to_lowercase().contains(&b.to_lowercase()),
53            _ => self.eval(a, b),
54        }
55    }
56    pub fn eval_option_str(
57        self,
58        a: Option<&str>,
59        b: &str,
60    ) -> bool {
61        match (a, self) {
62            (Some(a), Self::Like) => a.to_lowercase().contains(&b.to_lowercase()),
63            _ => self.eval_option(a, b),
64        }
65    }
66}
67
68/// A leaf in the filter expression tree, an expression which
69/// may return true or false for any filesystem
70#[derive(Debug, Clone, PartialEq)]
71pub struct ColExpr {
72    col: Col,
73    operator: ColOperator,
74    value: String,
75}
76
77impl ColExpr {
78    #[cfg(test)]
79    pub fn new<S: Into<String>>(
80        col: Col,
81        operator: ColOperator,
82        value: S,
83    ) -> Self {
84        Self {
85            col,
86            operator,
87            value: value.into(),
88        }
89    }
90    pub fn eval(
91        &self,
92        mount: &Mount,
93    ) -> Result<bool, EvalExprError> {
94        Ok(match self.col {
95            Col::Id => self.operator.eval_option(
96                mount.info.id,
97                self.value
98                    .parse::<MountId>()
99                    .map_err(|_| EvalExprError::NotAnId(self.value.to_string()))?,
100            ),
101            Col::Dev => self.operator.eval(
102                mount.info.dev,
103                self.value
104                    .parse::<DeviceId>()
105                    .map_err(|_| EvalExprError::NotADeviceId(self.value.to_string()))?,
106            ),
107            Col::Filesystem => self.operator.eval_str(&mount.info.fs, &self.value),
108            Col::Label => self
109                .operator
110                .eval_option_str(mount.fs_label.as_deref(), &self.value),
111            Col::Type => self.operator.eval_str(&mount.info.fs_type, &self.value),
112            Col::Remote => self
113                .operator
114                .eval(mount.is_remote(), parse_bool(&self.value)?),
115            Col::Disk => self
116                .operator
117                .eval_option_str(mount.disk.as_ref().map(|d| d.disk_type()), &self.value),
118            Col::Used => self.operator.eval_option(
119                mount.stats().as_ref().map(|s| s.used()),
120                parse_integer(&self.value)?,
121            ),
122            Col::Use | Col::UsePercent => self.operator.eval_option(
123                mount.stats().as_ref().map(|s| s.use_share()),
124                parse_float(&self.value)?,
125            ),
126            Col::Free | Col::FreePercent => self.operator.eval_option(
127                mount.stats().as_ref().map(|s| s.available()),
128                parse_integer(&self.value)?,
129            ),
130            Col::Size => self.operator.eval_option(
131                mount.stats().as_ref().map(|s| s.size()),
132                parse_integer(&self.value)?,
133            ),
134            Col::InodesUsed => self.operator.eval_option(
135                mount.inodes().as_ref().map(|i| i.used()),
136                parse_integer(&self.value)?,
137            ),
138            Col::InodesUse | Col::InodesUsePercent => self.operator.eval_option(
139                mount.inodes().as_ref().map(|i| i.use_share()),
140                parse_float(&self.value)?,
141            ),
142            Col::InodesFree => self.operator.eval_option(
143                mount.inodes().as_ref().map(|i| i.favail),
144                parse_integer(&self.value)?,
145            ),
146            Col::InodesCount => self.operator.eval_option(
147                mount.inodes().as_ref().map(|i| i.files),
148                parse_integer(&self.value)?,
149            ),
150            Col::MountPoint => self
151                .operator
152                .eval_str(&mount.info.mount_point.to_string_lossy(), &self.value),
153            Col::Uuid => self
154                .operator
155                .eval_option_str(mount.uuid.as_deref(), &self.value),
156            Col::PartUuid => self
157                .operator
158                .eval_option_str(mount.part_uuid.as_deref(), &self.value),
159            Col::MountOptions => self
160                .operator
161                .eval_str(&mount.info.options_string(), &self.value),
162            Col::CompressLevel => self
163                .operator
164                .eval_option_str(mount.info.option_value("compress"), &self.value),
165        })
166    }
167}
168
169#[derive(Debug)]
170pub struct ParseExprError {
171    /// the string which couldn't be parsed
172    pub raw: String,
173    /// why
174    pub message: String,
175}
176impl ParseExprError {
177    pub fn new<R: Into<String>, M: Into<String>>(
178        raw: R,
179        message: M,
180    ) -> Self {
181        Self {
182            raw: raw.into(),
183            message: message.into(),
184        }
185    }
186}
187impl fmt::Display for ParseExprError {
188    fn fmt(
189        &self,
190        f: &mut fmt::Formatter<'_>,
191    ) -> fmt::Result {
192        write!(
193            f,
194            "{:?} can't be parsed as an expression: {}",
195            self.raw, self.message
196        )
197    }
198}
199impl std::error::Error for ParseExprError {}
200
201impl FromStr for ColExpr {
202    type Err = ParseExprError;
203    fn from_str(input: &str) -> Result<Self, ParseExprError> {
204        let mut chars_indices = input.char_indices();
205        let mut op_idx = 0;
206        for (idx, c) in &mut chars_indices {
207            if c == '<' || c == '>' || c == '=' {
208                op_idx = idx;
209                break;
210            }
211        }
212        if op_idx == 0 {
213            return Err(ParseExprError::new(
214                input,
215                "Invalid expression; expected <column><operator><value>",
216            ));
217        }
218        let mut val_idx = op_idx + 1;
219        for (idx, c) in &mut chars_indices {
220            if c != '<' && c != '>' && c != '=' {
221                val_idx = idx;
222                break;
223            }
224        }
225        if val_idx == input.len() {
226            return Err(ParseExprError::new(input, "no value"));
227        }
228        let col = &input[..op_idx];
229        let col = col
230            .parse()
231            .map_err(|e: ParseColError| ParseExprError::new(input, e.to_string()))?;
232        let operator = match &input[op_idx..val_idx] {
233            "<" => ColOperator::Lower,
234            "<=" => ColOperator::LowerOrEqual,
235            "=" => ColOperator::Like,
236            "==" => ColOperator::Equal,
237            "<>" => ColOperator::NotEqual,
238            ">=" => ColOperator::GreaterOrEqual,
239            ">" => ColOperator::Greater,
240            op => {
241                return Err(ParseExprError::new(
242                    input,
243                    format!("unknown operator: {:?}", op),
244                ));
245            }
246        };
247        let value = &input[val_idx..];
248        let value = value.into();
249        Ok(Self {
250            col,
251            operator,
252            value,
253        })
254    }
255}
256
257#[test]
258fn test_col_filter_parsing() {
259    assert_eq!(
260        "remote=false".parse::<ColExpr>().unwrap(),
261        ColExpr::new(Col::Remote, ColOperator::Like, "false"),
262    );
263    assert_eq!(
264        "size<32G".parse::<ColExpr>().unwrap(),
265        ColExpr::new(Col::Size, ColOperator::Lower, "32G"),
266    );
267}
268
269#[derive(Debug, PartialEq)]
270#[allow(clippy::enum_variant_names)]
271pub enum EvalExprError {
272    NotANumber(String),
273    NotAnId(String),
274    NotADeviceId(String),
275    NotABool(String),
276}
277impl EvalExprError {}
278impl fmt::Display for EvalExprError {
279    fn fmt(
280        &self,
281        f: &mut fmt::Formatter<'_>,
282    ) -> fmt::Result {
283        match self {
284            Self::NotANumber(s) => {
285                write!(f, "{:?} can't be evaluated as a number", &s)
286            }
287            Self::NotAnId(s) => {
288                write!(f, "{:?} can't be evaluated as an id", &s)
289            }
290            Self::NotADeviceId(s) => {
291                write!(f, "{:?} can't be evaluated as a device id", &s)
292            }
293            Self::NotABool(s) => {
294                write!(f, "{:?} can't be evaluated as a boolean", &s)
295            }
296        }
297    }
298}
299impl std::error::Error for EvalExprError {}
300
301fn parse_bool(input: &str) -> Result<bool, EvalExprError> {
302    let s = input.to_lowercase();
303    match s.as_ref() {
304        "x" | "t" | "true" | "1" | "y" | "yes" => Ok(true),
305        "f" | "false" | "0" | "n" | "no" => Ok(false),
306        _ => Err(EvalExprError::NotABool(input.to_string())),
307    }
308}
309
310/// Parse numbers like "1234", "32G", "4kB", "54Gib", "1.2M"
311fn parse_integer(input: &str) -> Result<u64, EvalExprError> {
312    let s = input.to_lowercase();
313    let s = s.trim_end_matches('b');
314    let (s, binary) = match s.strip_suffix('i') {
315        Some(s) => (s, true),
316        None => (s, false),
317    };
318    let cut = s.find(|c: char| !(c.is_ascii_digit() || c == '.'));
319    let (digits, factor): (&str, u64) = match cut {
320        Some(idx) => (
321            &s[..idx],
322            match (&s[idx..], binary) {
323                ("k", false) => 1000,
324                ("k", true) => 1024,
325                ("m", false) => 1000 * 1000,
326                ("m", true) => 1024 * 1024,
327                ("g", false) => 1000 * 1000 * 1000,
328                ("g", true) => 1024 * 1024 * 1024,
329                ("t", false) => 1000 * 1000 * 1000 * 1000,
330                ("t", true) => 1024 * 1024 * 1024 * 1024,
331                _ => {
332                    // it's not a number
333                    return Err(EvalExprError::NotANumber(input.to_string()));
334                }
335            },
336        ),
337        None => (s, 1),
338    };
339    match digits.parse::<f64>() {
340        Ok(n) => Ok((n * factor as f64).ceil() as u64),
341        _ => Err(EvalExprError::NotANumber(input.to_string())),
342    }
343}
344
345#[test]
346fn test_parse_integer() {
347    assert_eq!(parse_integer("33"), Ok(33));
348    assert_eq!(parse_integer("55G"), Ok(55_000_000_000));
349    assert_eq!(parse_integer("1.23kiB"), Ok(1260));
350}
351
352/// parse numbers like "0.25", "50%"
353fn parse_float(input: &str) -> Result<f64, EvalExprError> {
354    let s = input.to_lowercase();
355    let (s, percent) = match s.strip_suffix('%') {
356        Some(s) => (s, true),
357        None => (s.as_str(), false),
358    };
359    let mut n = s
360        .parse::<f64>()
361        .map_err(|_| EvalExprError::NotANumber(input.to_string()))?;
362    if percent {
363        n /= 100.0;
364    }
365    Ok(n)
366}
367
368#[test]
369fn test_parse_float() {
370    assert_eq!(parse_float("50%").unwrap().to_string(), "0.5".to_string());
371}