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#[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 pub raw: String,
171 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
308fn 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 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
350fn 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}