air_sys_syscall/fs/
inotify.rs1use super::{errno_from_negative_syscall_ret, fd_to_u64, nr, read, syscall1, syscall2, syscall3};
19use air_sys_types::Errno;
20use air_sys_types::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
21use air_sys_types::fs::{InotifyEventMask, InotifyEvents, InotifyFlags, WatchDescriptor};
22use core::ffi::CStr;
23
24pub const INOTIFY_RECOMMENDED_BUFFER_SIZE: usize = 4096;
28
29#[derive(Debug)]
31pub struct Inotify {
32 fd: OwnedFd,
33}
34
35pub fn inotify_init(flags: InotifyFlags) -> Result<Inotify, Errno> {
45 let raw_flags = (flags | InotifyFlags::CLOEXEC).bits();
47 let ret = unsafe { syscall1(nr::INOTIFY_INIT1, u64::from(raw_flags.cast_unsigned())) };
50 if ret < 0 {
51 return Err(errno_from_negative_syscall_ret(ret));
52 }
53 let fd = i32::try_from(ret).map_err(|_| Errno::EINVAL)?;
54 Ok(Inotify {
56 fd: unsafe { OwnedFd::from_raw_fd(fd) },
57 })
58}
59
60impl Inotify {
61 #[must_use]
63 pub fn as_fd(&self) -> BorrowedFd<'_> {
64 self.fd.as_fd()
65 }
66
67 #[must_use]
69 pub fn into_fd(self) -> OwnedFd {
70 self.fd
71 }
72
73 pub fn add_watch(&self, path: &CStr, mask: InotifyEventMask) -> Result<WatchDescriptor, Errno> {
85 let ret = unsafe {
89 syscall3(
90 nr::INOTIFY_ADD_WATCH,
91 fd_to_u64(self.fd.as_raw_fd()),
92 path.as_ptr() as u64,
93 u64::from(mask.bits()),
94 )
95 };
96 if ret < 0 {
97 return Err(errno_from_negative_syscall_ret(ret));
98 }
99 let wd = i32::try_from(ret).map_err(|_| Errno::EINVAL)?;
100 Ok(WatchDescriptor::from_raw(wd))
101 }
102
103 pub fn remove_watch(&self, wd: WatchDescriptor) -> Result<(), Errno> {
109 let ret = unsafe {
112 syscall2(
113 nr::INOTIFY_RM_WATCH,
114 fd_to_u64(self.fd.as_raw_fd()),
115 u64::from(wd.as_raw().cast_unsigned()),
116 )
117 };
118 if ret < 0 {
119 return Err(errno_from_negative_syscall_ret(ret));
120 }
121 Ok(())
122 }
123
124 pub fn read_events<'b>(&self, buffer: &'b mut [u8]) -> Result<InotifyEvents<'b>, Errno> {
136 let count = read(self.as_fd(), buffer)?;
137 let filled: &'b [u8] = buffer;
139 Ok(InotifyEvents::parse(filled.get(..count).unwrap_or(filled)))
140 }
141}
142
143#[cfg(test)]
149mod tests {
150 use super::{INOTIFY_RECOMMENDED_BUFFER_SIZE, Inotify, inotify_init};
151 use air_sys_types::Errno;
152 use air_sys_types::fs::{InotifyEventMask, InotifyFlags};
153 use std::ffi::CString;
154 use std::io::Write as _;
155
156 fn temp_dir(tag: &str) -> std::path::PathBuf {
158 let dir = std::env::temp_dir().join(format!("air-inotify-{tag}-{}", std::process::id()));
159 let _ = std::fs::remove_dir_all(&dir);
160 std::fs::create_dir_all(&dir).expect("création répertoire temp");
161 dir
162 }
163
164 fn cpath(p: &std::path::Path) -> CString {
165 CString::new(p.as_os_str().as_encoded_bytes()).expect("chemin sans NUL")
166 }
167
168 fn drain(inotify: &Inotify) -> Vec<(InotifyEventMask, Option<Vec<u8>>, u32)> {
171 let mut out = Vec::new();
172 let mut buffer = vec![0u8; INOTIFY_RECOMMENDED_BUFFER_SIZE];
173 loop {
177 match inotify.read_events(&mut buffer) {
178 Ok(events) => {
179 for ev in events {
180 out.push((ev.mask, ev.name.map(<[u8]>::to_vec), ev.cookie));
181 }
182 }
183 Err(e) => {
184 assert_eq!(e, Errno::EAGAIN, "seul EAGAIN attendu (NONBLOCK)");
185 break;
186 }
187 }
188 }
189 out
190 }
191
192 #[test]
193 #[cfg_attr(miri, ignore = "syscalls inotify non supportés par Miri")]
194 fn watch_create_modify_move_delete_round_trip() {
195 let dir = temp_dir("rt");
196 let inotify = inotify_init(InotifyFlags::NONBLOCK).expect("inotify_init");
198 let wd = inotify
199 .add_watch(
200 &cpath(&dir),
201 InotifyEventMask::CREATE
202 | InotifyEventMask::MODIFY
203 | InotifyEventMask::MOVED_FROM
204 | InotifyEventMask::MOVED_TO
205 | InotifyEventMask::DELETE,
206 )
207 .expect("add_watch");
208
209 let path_a = dir.join("a");
211 {
212 let mut f = std::fs::File::create(&path_a).expect("create a");
213 f.write_all(b"hello").expect("write a");
214 }
215 let path_b = dir.join("b");
217 std::fs::rename(&path_a, &path_b).expect("rename a→b");
218 std::fs::remove_file(&path_b).expect("remove b");
220
221 let events = drain(&inotify);
222 assert!(
224 events
225 .iter()
226 .any(|(m, n, _)| m.contains(InotifyEventMask::CREATE)
227 && n.as_deref() == Some(&b"a"[..])),
228 "CREATE a manquant : {events:?}"
229 );
230 assert!(
232 events
233 .iter()
234 .any(|(m, n, _)| m.contains(InotifyEventMask::MODIFY)
235 && n.as_deref() == Some(&b"a"[..])),
236 "MODIFY a manquant"
237 );
238 let from = events
240 .iter()
241 .find(|(m, n, _)| {
242 m.contains(InotifyEventMask::MOVED_FROM) && n.as_deref() == Some(&b"a"[..])
243 })
244 .expect("MOVED_FROM a");
245 let to = events
246 .iter()
247 .find(|(m, n, _)| {
248 m.contains(InotifyEventMask::MOVED_TO) && n.as_deref() == Some(&b"b"[..])
249 })
250 .expect("MOVED_TO b");
251 assert_ne!(from.2, 0, "cookie non nul");
252 assert_eq!(from.2, to.2, "MOVED_FROM/MOVED_TO corrélés par cookie");
253 assert!(
255 events
256 .iter()
257 .any(|(m, n, _)| m.contains(InotifyEventMask::DELETE)
258 && n.as_deref() == Some(&b"b"[..])),
259 "DELETE b manquant"
260 );
261
262 inotify.remove_watch(wd).expect("remove_watch");
264 let after = drain(&inotify);
265 assert!(
266 after
267 .iter()
268 .any(|(m, _, _)| m.contains(InotifyEventMask::IGNORED)),
269 "IGNORED après remove_watch"
270 );
271
272 let _ = std::fs::remove_dir_all(&dir);
273 }
274
275 #[test]
276 #[cfg_attr(miri, ignore = "syscalls inotify non supportés par Miri")]
277 fn nonblock_read_with_no_events_is_eagain() {
278 let dir = temp_dir("eagain");
279 let inotify = inotify_init(InotifyFlags::NONBLOCK).expect("inotify_init");
280 inotify
281 .add_watch(&cpath(&dir), InotifyEventMask::CREATE)
282 .expect("add_watch");
283 let mut buffer = [0u8; 64];
284 assert!(matches!(
286 inotify.read_events(&mut buffer),
287 Err(Errno::EAGAIN)
288 ));
289 let _ = std::fs::remove_dir_all(&dir);
290 }
291
292 #[test]
293 #[cfg_attr(miri, ignore = "syscalls inotify non supportés par Miri")]
294 fn add_watch_on_missing_path_errors() {
295 let inotify = inotify_init(InotifyFlags::empty()).expect("inotify_init");
296 let missing = cpath(std::path::Path::new("/nonexistent/air-inotify/xyz"));
297 assert!(matches!(
298 inotify.add_watch(&missing, InotifyEventMask::CREATE),
299 Err(Errno::ENOENT)
300 ));
301 }
302
303 #[test]
304 #[cfg_attr(miri, ignore = "syscalls inotify non supportés par Miri")]
305 fn into_fd_and_as_fd_expose_the_descriptor() {
306 use air_sys_types::fd::AsRawFd as _;
307 let inotify = inotify_init(InotifyFlags::empty()).expect("inotify_init");
308 assert!(inotify.as_fd().as_raw_fd() >= 0);
309 let owned = inotify.into_fd();
310 assert!(owned.as_raw_fd() >= 0);
311 let other = inotify_init(InotifyFlags::empty()).expect("inotify_init 2");
313 assert!(format!("{other:?}").contains("Inotify"));
314 }
315
316 #[test]
317 #[cfg_attr(miri, ignore = "syscalls inotify non supportés par Miri")]
318 fn remove_invalid_watch_errors() {
319 let inotify = inotify_init(InotifyFlags::empty()).expect("inotify_init");
320 let bogus = air_sys_types::fs::WatchDescriptor::from_raw(424_242);
321 assert!(matches!(inotify.remove_watch(bogus), Err(Errno::EINVAL)));
322 }
323}