dysk_cli/
col_expr.rs

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