watchexec_cli/filterer/proglib/
file.rs1use std::{
2 fs::{metadata, File, FileType, Metadata},
3 io::{BufReader, Read},
4 iter::once,
5 time::{SystemTime, UNIX_EPOCH},
6};
7
8use jaq_core::{Error, Native};
9use jaq_json::Val;
10use jaq_std::{v, Filter};
11use serde_json::{json, Value};
12use tracing::{debug, error};
13
14use super::macros::return_err;
15
16pub fn funs() -> [Filter<Native<jaq_json::Val>>; 3] {
17 [
18 (
19 "file_read",
20 v(0),
21 Native::new({
22 move |_, (mut ctx, val)| {
23 let path = match &val {
24 Val::Str(v) => v.to_string(),
25 _ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
26 };
27
28 let Val::Int(bytes) = ctx.pop_var() else {
29 return_err!(Err(Error::str("expected integer")));
30 };
31
32 let bytes = match u64::try_from(bytes) {
33 Ok(b) => b,
34 Err(err) => return_err!(Err(Error::str(format!(
35 "expected positive integer; {err}"
36 )))),
37 };
38
39 Box::new(once(Ok(match File::open(&path) {
40 Ok(file) => {
41 let buf_reader = BufReader::new(file);
42 let mut limited = buf_reader.take(bytes);
43 let mut buffer = String::with_capacity(bytes as _);
44 match limited.read_to_string(&mut buffer) {
45 Ok(read) => {
46 debug!("jaq: read {read} bytes from {path:?}");
47 Val::Str(buffer.into())
48 }
49 Err(err) => {
50 error!("jaq: failed to read from {path:?}: {err:?}");
51 Val::Null
52 }
53 }
54 }
55 Err(err) => {
56 error!("jaq: failed to open file {path:?}: {err:?}");
57 Val::Null
58 }
59 })))
60 }
61 }),
62 ),
63 (
64 "file_meta",
65 v(0),
66 Native::new({
67 move |_, (_, val)| {
68 let path = match &val {
69 Val::Str(v) => v.to_string(),
70 _ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
71 };
72
73 Box::new(once(Ok(match metadata(&path) {
74 Ok(meta) => Val::from(json_meta(meta)),
75 Err(err) => {
76 error!("jaq: failed to open {path:?}: {err:?}");
77 Val::Null
78 }
79 })))
80 }
81 }),
82 ),
83 (
84 "file_size",
85 v(0),
86 Native::new({
87 move |_, (_, val)| {
88 let path = match &val {
89 Val::Str(v) => v.to_string(),
90 _ => return_err!(Err(Error::str("expected string (path) but got {val:?}"))),
91 };
92
93 Box::new(once(Ok(match metadata(&path) {
94 Ok(meta) => Val::Int(meta.len() as _),
95 Err(err) => {
96 error!("jaq: failed to open {path:?}: {err:?}");
97 Val::Null
98 }
99 })))
100 }
101 }),
102 ),
103 ]
104}
105
106fn json_meta(meta: Metadata) -> Value {
107 let perms = meta.permissions();
108 #[cfg_attr(not(unix), allow(unused_mut))]
109 let mut val = json!({
110 "type": filetype_str(meta.file_type()),
111 "size": meta.len(),
112 "modified": fs_time(meta.modified()),
113 "accessed": fs_time(meta.accessed()),
114 "created": fs_time(meta.created()),
115 "dir": meta.is_dir(),
116 "file": meta.is_file(),
117 "symlink": meta.is_symlink(),
118 "readonly": perms.readonly(),
119 });
120
121 #[cfg(unix)]
122 {
123 use std::os::unix::fs::PermissionsExt;
124 let map = val.as_object_mut().unwrap();
125 map.insert(
126 "mode".to_string(),
127 Value::String(format!("{:o}", perms.mode())),
128 );
129 map.insert("mode_byte".to_string(), Value::from(perms.mode()));
130 map.insert(
131 "executable".to_string(),
132 Value::Bool(perms.mode() & 0o111 != 0),
133 );
134 }
135
136 val
137}
138
139fn filetype_str(filetype: FileType) -> &'static str {
140 #[cfg(unix)]
141 {
142 use std::os::unix::fs::FileTypeExt;
143 if filetype.is_char_device() {
144 return "char";
145 } else if filetype.is_block_device() {
146 return "block";
147 } else if filetype.is_fifo() {
148 return "fifo";
149 } else if filetype.is_socket() {
150 return "socket";
151 }
152 }
153
154 #[cfg(windows)]
155 {
156 use std::os::windows::fs::FileTypeExt;
157 if filetype.is_symlink_dir() {
158 return "symdir";
159 } else if filetype.is_symlink_file() {
160 return "symfile";
161 }
162 }
163
164 if filetype.is_dir() {
165 "dir"
166 } else if filetype.is_file() {
167 "file"
168 } else if filetype.is_symlink() {
169 "symlink"
170 } else {
171 "unknown"
172 }
173}
174
175fn fs_time(time: std::io::Result<SystemTime>) -> Option<u64> {
176 time.ok()
177 .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
178 .map(|dur| dur.as_secs())
179}