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.operator.eval_str(&mount.info.options_string(), &self.value),
160            Col::CompressLevel => self
161                .operator
162                .eval_option_str(mount.info.option_value("compress"), &self.value),
163        })
164    }
165}
166
167#[derive(Debug)]
168pub struct ParseExprError {
169    /// the string which couldn't be parsed
170    pub raw: String,
171    /// why
172    pub message: String,
173}
174impl ParseExprError {
175    pub fn new<R: Into<String>, M: Into<String>>(
176        raw: R,
177        message: M,
178    ) -> Self {
179        Self {
180            raw: raw.into(),
181            message: message.into(),
182        }
183    }
184}
185impl fmt::Display for ParseExprError {
186    fn fmt(
187        &self,
188        f: &mut fmt::Formatter<'_>,
189    ) -> fmt::Result {
190        write!(
191            f,
192            "{:?} can't be parsed as an expression: {}",
193            self.raw, self.message
194        )
195    }
196}
197impl std::error::Error for ParseExprError {}
198
199impl FromStr for ColExpr {
200    type Err = ParseExprError;
201    fn from_str(input: &str) -> Result<Self, ParseExprError> {
202        let mut chars_indices = input.char_indices();
203        let mut op_idx = 0;
204        for (idx, c) in &mut chars_indices {
205            if c == '<' || c == '>' || c == '=' {
206                op_idx = idx;
207                break;
208            }
209        }
210        if op_idx == 0 {
211            return Err(ParseExprError::new(
212                input,
213                "Invalid expression; expected <column><operator><value>",
214            ));
215        }
216        let mut val_idx = op_idx + 1;
217        for (idx, c) in &mut chars_indices {
218            if c != '<' && c != '>' && c != '=' {
219                val_idx = idx;
220                break;
221            }
222        }
223        if val_idx == input.len() {
224            return Err(ParseExprError::new(input, "no value"));
225        }
226        let col = &input[..op_idx];
227        let col = col
228            .parse()
229            .map_err(|e: ParseColError| ParseExprError::new(input, e.to_string()))?;
230        let operator = match &input[op_idx..val_idx] {
231            "<" => ColOperator::Lower,
232            "<=" => ColOperator::LowerOrEqual,
233            "=" => ColOperator::Like,
234            "==" => ColOperator::Equal,
235            "<>" => ColOperator::NotEqual,
236            ">=" => ColOperator::GreaterOrEqual,
237            ">" => ColOperator::Greater,
238            op => {
239                return Err(ParseExprError::new(
240                    input,
241                    format!("unknown operator: {:?}", op),
242                ));
243            }
244        };
245        let value = &input[val_idx..];
246        let value = value.into();
247        Ok(Self {
248            col,
249            operator,
250            value,
251        })
252    }
253}
254
255#[test]
256fn test_col_filter_parsing() {
257    assert_eq!(
258        "remote=false".parse::<ColExpr>().unwrap(),
259        ColExpr::new(Col::Remote, ColOperator::Like, "false"),
260    );
261    assert_eq!(
262        "size<32G".parse::<ColExpr>().unwrap(),
263        ColExpr::new(Col::Size, ColOperator::Lower, "32G"),
264    );
265}
266
267#[derive(Debug, PartialEq)]
268#[allow(clippy::enum_variant_names)]
269pub enum EvalExprError {
270    NotANumber(String),
271    NotAnId(String),
272    NotADeviceId(String),
273    NotABool(String),
274}
275impl EvalExprError {}
276impl fmt::Display for EvalExprError {
277    fn fmt(
278        &self,
279        f: &mut fmt::Formatter<'_>,
280    ) -> fmt::Result {
281        match self {
282            Self::NotANumber(s) => {
283                write!(f, "{:?} can't be evaluated as a number", &s)
284            }
285            Self::NotAnId(s) => {
286                write!(f, "{:?} can't be evaluated as an id", &s)
287            }
288            Self::NotADeviceId(s) => {
289                write!(f, "{:?} can't be evaluated as a device id", &s)
290            }
291            Self::NotABool(s) => {
292                write!(f, "{:?} can't be evaluated as a boolean", &s)
293            }
294        }
295    }
296}
297impl std::error::Error for EvalExprError {}
298
299fn parse_bool(input: &str) -> Result<bool, EvalExprError> {
300    let s = input.to_lowercase();
301    match s.as_ref() {
302        "x" | "t" | "true" | "1" | "y" | "yes" => Ok(true),
303        "f" | "false" | "0" | "n" | "no" => Ok(false),
304        _ => Err(EvalExprError::NotABool(input.to_string())),
305    }
306}
307
308/// Parse numbers like "1234", "32G", "4kB", "54Gib", "1.2M"
309fn parse_integer(input: &str) -> Result<u64, EvalExprError> {
310    let s = input.to_lowercase();
311    let s = s.trim_end_matches('b');
312    let (s, binary) = match s.strip_suffix('i') {
313        Some(s) => (s, true),
314        None => (s, false),
315    };
316    let cut = s.find(|c: char| !(c.is_ascii_digit() || c == '.'));
317    let (digits, factor): (&str, u64) = match cut {
318        Some(idx) => (
319            &s[..idx],
320            match (&s[idx..], binary) {
321                ("k", false) => 1000,
322                ("k", true) => 1024,
323                ("m", false) => 1000 * 1000,
324                ("m", true) => 1024 * 1024,
325                ("g", false) => 1000 * 1000 * 1000,
326                ("g", true) => 1024 * 1024 * 1024,
327                ("t", false) => 1000 * 1000 * 1000 * 1000,
328                ("t", true) => 1024 * 1024 * 1024 * 1024,
329                _ => {
330                    // it's not a number
331                    return Err(EvalExprError::NotANumber(input.to_string()));
332                }
333            },
334        ),
335        None => (s, 1),
336    };
337    match digits.parse::<f64>() {
338        Ok(n) => Ok((n * factor as f64).ceil() as u64),
339        _ => Err(EvalExprError::NotANumber(input.to_string())),
340    }
341}
342
343#[test]
344fn test_parse_integer() {
345    assert_eq!(parse_integer("33"), Ok(33));
346    assert_eq!(parse_integer("55G"), Ok(55_000_000_000));
347    assert_eq!(parse_integer("1.23kiB"), Ok(1260));
348}
349
350/// parse numbers like "0.25", "50%"
351fn parse_float(input: &str) -> Result<f64, EvalExprError> {
352    let s = input.to_lowercase();
353    let (s, percent) = match s.strip_suffix('%') {
354        Some(s) => (s, true),
355        None => (s.as_str(), false),
356    };
357    let mut n = s
358        .parse::<f64>()
359        .map_err(|_| EvalExprError::NotANumber(input.to_string()))?;
360    if percent {
361        n /= 100.0;
362    }
363    Ok(n)
364}
365
366#[test]
367fn test_parse_float() {
368    assert_eq!(parse_float("50%").unwrap().to_string(), "0.5".to_string());
369}