watchexec_cli/filterer/proglib/
file.rs

1use 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}