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
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 pub raw: String,
173 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
310fn 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 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
352fn 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}