rs_dirent_filter_cel/
lib.rs

1use cel::Context;
2use cel::Value;
3use cel::objects::{Key, Map};
4use cel::{ExecutionError, FunctionContext, ResolveResult};
5
6use std::collections::HashMap;
7use std::os::unix::fs::{FileTypeExt, MetadataExt}; // Import FileTypeExt
8#[allow(unused_imports)] // PathBuf is only used in tests
9use std::path::{Path, PathBuf}; // PathBuf is used in tests
10use std::sync::Arc; // This is crucial for nlink()
11
12fn cel_parse_size(ftx: &FunctionContext, s: Arc<String>) -> ResolveResult {
13    match parse_size::parse_size(&*s) {
14        Ok(size) => Value::Int(size as i64).into(),
15        Err(e) => ExecutionError::function_error(&ftx.name, e).into(),
16    }
17}
18
19pub fn parser2ctx<'a>(ctx: &mut Context<'a>, name: &str) {
20    ctx.add_function(name, cel_parse_size);
21}
22
23pub struct DirentInfo {
24    pub name: String,
25    pub is_file: bool,
26    pub is_dir: bool,
27    pub is_symlink: bool,
28    pub is_block_device: bool,
29    pub is_char_device: bool,
30    pub is_hidden: bool,
31    pub is_readonly: bool,
32    pub is_socket: bool,
33    pub is_fifo: bool,
34    pub len: u64,
35    pub nlink: u64,
36    pub mode: u32,
37    pub uid: u32,
38    pub gid: u32,
39    pub mtime: i64,
40    pub atime: i64,
41    pub ctime: i64,
42}
43
44impl From<&Path> for DirentInfo {
45    fn from(path: &Path) -> Self {
46        let name = path
47            .file_name()
48            .and_then(|os_str| os_str.to_str())
49            .unwrap_or("")
50            .to_string();
51
52        let metadata = path.symlink_metadata().ok(); // Use symlink_metadata to get info about the entry itself
53
54        DirentInfo {
55            name: name.clone(),
56            is_file: metadata.as_ref().is_some_and(|m| m.is_file()),
57            is_dir: metadata.as_ref().is_some_and(|m| m.is_dir()),
58            is_symlink: metadata.as_ref().is_some_and(|m| m.is_symlink()),
59            is_block_device: metadata.as_ref().is_some_and(|m| {
60                #[cfg(unix)]
61                {
62                    m.file_type().is_block_device()
63                }
64                #[cfg(not(unix))]
65                {
66                    false
67                }
68            }),
69            is_char_device: metadata.as_ref().is_some_and(|m| {
70                #[cfg(unix)]
71                {
72                    m.file_type().is_char_device()
73                }
74                #[cfg(not(unix))]
75                {
76                    false
77                }
78            }),
79            is_hidden: name.starts_with('.'),
80            is_readonly: metadata
81                .as_ref()
82                .is_some_and(|m| m.permissions().readonly()),
83            is_socket: metadata.as_ref().is_some_and(|m| {
84                #[cfg(unix)]
85                {
86                    m.file_type().is_socket()
87                }
88                #[cfg(not(unix))]
89                {
90                    false
91                }
92            }),
93            is_fifo: metadata.as_ref().is_some_and(|m| {
94                #[cfg(unix)]
95                {
96                    m.file_type().is_fifo()
97                }
98                #[cfg(not(unix))]
99                {
100                    false
101                }
102            }),
103            len: metadata.as_ref().map_or(0, |m| m.len()),
104            nlink: metadata.as_ref().map_or(0, |m| {
105                #[cfg(unix)]
106                {
107                    m.nlink()
108                }
109                #[cfg(not(unix))]
110                {
111                    1
112                }
113            }),
114            mode: metadata.as_ref().map_or(0, |m| {
115                #[cfg(unix)]
116                {
117                    m.mode()
118                }
119                #[cfg(not(unix))]
120                {
121                    0
122                }
123            }),
124            uid: metadata.as_ref().map_or(0, |m| {
125                #[cfg(unix)]
126                {
127                    m.uid()
128                }
129                #[cfg(not(unix))]
130                {
131                    0
132                }
133            }),
134            gid: metadata.as_ref().map_or(0, |m| {
135                #[cfg(unix)]
136                {
137                    m.gid()
138                }
139                #[cfg(not(unix))]
140                {
141                    0
142                }
143            }),
144            mtime: metadata.as_ref().map_or(0, |m| {
145                m.modified()
146                    .map(|st| {
147                        st.duration_since(std::time::UNIX_EPOCH)
148                            .unwrap_or_default()
149                            .as_secs() as i64
150                    })
151                    .unwrap_or(0)
152            }),
153            atime: metadata.as_ref().map_or(0, |m| {
154                m.accessed()
155                    .map(|st| {
156                        st.duration_since(std::time::UNIX_EPOCH)
157                            .unwrap_or_default()
158                            .as_secs() as i64
159                    })
160                    .unwrap_or(0)
161            }),
162            ctime: metadata.as_ref().map_or(0, |m| {
163                #[cfg(unix)]
164                {
165                    m.ctime()
166                }
167                #[cfg(not(unix))]
168                {
169                    0
170                }
171            }),
172        }
173    }
174}
175
176impl From<DirentInfo> for Value {
177    fn from(dirent_info: DirentInfo) -> Self {
178        let mut map = Map {
179            map: Arc::new(HashMap::new()),
180        };
181
182        if let Some(internal_map) = Arc::get_mut(&mut map.map) {
183            internal_map.insert(
184                Key::String(Arc::new("name".to_string())),
185                Value::String(dirent_info.name.into()),
186            );
187            internal_map.insert(
188                Key::String(Arc::new("is_file".to_string())),
189                Value::Bool(dirent_info.is_file),
190            );
191            internal_map.insert(
192                Key::String(Arc::new("is_dir".to_string())),
193                Value::Bool(dirent_info.is_dir),
194            );
195            internal_map.insert(
196                Key::String(Arc::new("is_symlink".to_string())),
197                Value::Bool(dirent_info.is_symlink),
198            );
199            internal_map.insert(
200                Key::String(Arc::new("is_block_device".to_string())),
201                Value::Bool(dirent_info.is_block_device),
202            );
203            internal_map.insert(
204                Key::String(Arc::new("is_char_device".to_string())),
205                Value::Bool(dirent_info.is_char_device),
206            );
207            internal_map.insert(
208                Key::String(Arc::new("is_hidden".to_string())),
209                Value::Bool(dirent_info.is_hidden),
210            );
211            internal_map.insert(
212                Key::String(Arc::new("is_readonly".to_string())),
213                Value::Bool(dirent_info.is_readonly),
214            );
215            internal_map.insert(
216                Key::String(Arc::new("is_socket".to_string())),
217                Value::Bool(dirent_info.is_socket),
218            );
219            internal_map.insert(
220                Key::String(Arc::new("is_fifo".to_string())),
221                Value::Bool(dirent_info.is_fifo),
222            );
223            internal_map.insert(
224                Key::String(Arc::new("len".to_string())),
225                Value::Int(dirent_info.len as i64),
226            );
227            internal_map.insert(
228                Key::String(Arc::new("nlink".to_string())),
229                Value::Int(dirent_info.nlink as i64),
230            );
231            internal_map.insert(
232                Key::String(Arc::new("mode".to_string())),
233                Value::Int(dirent_info.mode as i64),
234            );
235            internal_map.insert(
236                Key::String(Arc::new("uid".to_string())),
237                Value::Int(dirent_info.uid as i64),
238            );
239            internal_map.insert(
240                Key::String(Arc::new("gid".to_string())),
241                Value::Int(dirent_info.gid as i64),
242            );
243            internal_map.insert(
244                Key::String(Arc::new("mtime".to_string())),
245                Value::Int(dirent_info.mtime),
246            );
247            internal_map.insert(
248                Key::String(Arc::new("atime".to_string())),
249                Value::Int(dirent_info.atime),
250            );
251            internal_map.insert(
252                Key::String(Arc::new("ctime".to_string())),
253                Value::Int(dirent_info.ctime),
254            );
255        }
256
257        Value::Map(map)
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use std::fs;
265    use std::io::Write;
266    use std::path::PathBuf;
267    use tempfile::tempdir;
268
269    #[test]
270    fn test_dirent_info_from_path_file() {
271        let dir = tempdir().unwrap();
272        let file_path = dir.path().join("test_file.txt");
273        fs::File::create(&file_path)
274            .unwrap()
275            .write_all(b"hello")
276            .unwrap();
277
278        let metadata = fs::symlink_metadata(&file_path).unwrap();
279        let expected_len = metadata.len();
280
281        let dirent_info = DirentInfo::from(file_path.as_path());
282
283        assert_eq!(dirent_info.name, "test_file.txt");
284        assert!(dirent_info.is_file);
285        assert!(!dirent_info.is_dir);
286        assert!(!dirent_info.is_symlink);
287        assert_eq!(dirent_info.len, expected_len);
288    }
289
290    #[test]
291    fn test_dirent_info_from_path_dir() {
292        let dir = tempdir().unwrap();
293        let subdir_path = dir.path().join("test_dir");
294        fs::create_dir(&subdir_path).unwrap();
295
296        let dirent_info = DirentInfo::from(subdir_path.as_path());
297
298        assert_eq!(dirent_info.name, "test_dir");
299        assert!(!dirent_info.is_file);
300        assert!(dirent_info.is_dir);
301        assert!(!dirent_info.is_symlink);
302    }
303
304    #[test]
305    fn test_dirent_info_from_path_symlink() {
306        let dir = tempdir().unwrap();
307        let target_file = dir.path().join("target.txt");
308        fs::File::create(&target_file).unwrap();
309        let symlink_path = dir.path().join("link_to_target.txt");
310        #[cfg(unix)] // Symlinks are primarily a Unix-like feature
311        std::os::unix::fs::symlink(&target_file, &symlink_path).unwrap();
312
313        // This test block for symlinks is also OS-specific due to `symlink` function.
314        #[cfg(unix)]
315        {
316            let dirent_info = DirentInfo::from(symlink_path.as_path());
317            assert_eq!(dirent_info.name, "link_to_target.txt");
318            assert!(!dirent_info.is_file); // symlink is not a file itself
319            assert!(!dirent_info.is_dir); // symlink is not a dir itself
320            assert!(dirent_info.is_symlink);
321        }
322    }
323
324    #[test]
325    fn test_dirent_info_non_existent_path() {
326        let path = PathBuf::from("non_existent_file.txt");
327        let dirent_info = DirentInfo::from(path.as_path());
328
329        assert_eq!(dirent_info.name, "non_existent_file.txt");
330        assert!(!dirent_info.is_file);
331        assert!(!dirent_info.is_dir);
332        assert!(!dirent_info.is_symlink);
333        assert_eq!(dirent_info.len, 0);
334        assert_eq!(dirent_info.nlink, 0);
335        assert_eq!(dirent_info.mtime, 0);
336    }
337
338    #[test]
339    fn test_dirent_info_to_cel_value() {
340        let dirent_info = DirentInfo {
341            name: "example.txt".to_string(),
342            is_file: true,
343            is_dir: false,
344            is_symlink: false,
345            is_block_device: false,
346            is_char_device: false,
347            is_hidden: false,
348            is_readonly: true,
349            is_socket: false,
350            is_fifo: false,
351            len: 123,
352            nlink: 1,
353            mode: 0o644,
354            uid: 1000,
355            gid: 1000,
356            mtime: 1678886400, // March 15, 2023 12:00:00 AM UTC
357            atime: 1678886400,
358            ctime: 1678886400,
359        };
360
361        let cel_value: Value = dirent_info.into();
362
363        if let Value::Map(map) = cel_value {
364            let internal_map = map.map.as_ref();
365
366            assert_eq!(
367                internal_map.get(&Key::String(Arc::new("name".to_string()))),
368                Some(&Value::String("example.txt".to_string().into()))
369            );
370            assert_eq!(
371                internal_map.get(&Key::String(Arc::new("is_file".to_string()))),
372                Some(&Value::Bool(true))
373            );
374            assert_eq!(
375                internal_map.get(&Key::String(Arc::new("is_dir".to_string()))),
376                Some(&Value::Bool(false))
377            );
378            assert_eq!(
379                internal_map.get(&Key::String(Arc::new("is_symlink".to_string()))),
380                Some(&Value::Bool(false))
381            );
382            assert_eq!(
383                internal_map.get(&Key::String(Arc::new("is_block_device".to_string()))),
384                Some(&Value::Bool(false))
385            );
386            assert_eq!(
387                internal_map.get(&Key::String(Arc::new("is_char_device".to_string()))),
388                Some(&Value::Bool(false))
389            );
390            assert_eq!(
391                internal_map.get(&Key::String(Arc::new("is_hidden".to_string()))),
392                Some(&Value::Bool(false))
393            );
394            assert_eq!(
395                internal_map.get(&Key::String(Arc::new("is_readonly".to_string()))),
396                Some(&Value::Bool(true))
397            );
398            assert_eq!(
399                internal_map.get(&Key::String(Arc::new("is_socket".to_string()))),
400                Some(&Value::Bool(false))
401            );
402            assert_eq!(
403                internal_map.get(&Key::String(Arc::new("is_fifo".to_string()))),
404                Some(&Value::Bool(false))
405            );
406            assert_eq!(
407                internal_map.get(&Key::String(Arc::new("len".to_string()))),
408                Some(&Value::Int(123))
409            );
410            assert_eq!(
411                internal_map.get(&Key::String(Arc::new("nlink".to_string()))),
412                Some(&Value::Int(1))
413            );
414            assert_eq!(
415                internal_map.get(&Key::String(Arc::new("mode".to_string()))),
416                Some(&Value::Int(0o644))
417            );
418            assert_eq!(
419                internal_map.get(&Key::String(Arc::new("uid".to_string()))),
420                Some(&Value::Int(1000))
421            );
422            assert_eq!(
423                internal_map.get(&Key::String(Arc::new("gid".to_string()))),
424                Some(&Value::Int(1000))
425            );
426            assert_eq!(
427                internal_map.get(&Key::String(Arc::new("mtime".to_string()))),
428                Some(&Value::Int(1678886400))
429            );
430            assert_eq!(
431                internal_map.get(&Key::String(Arc::new("atime".to_string()))),
432                Some(&Value::Int(1678886400))
433            );
434            assert_eq!(
435                internal_map.get(&Key::String(Arc::new("ctime".to_string()))),
436                Some(&Value::Int(1678886400))
437            );
438        } else {
439            panic!("Expected a CEL Map Value");
440        }
441    }
442}