diff --git a/src/backend/libc/fs/inotify.rs b/src/backend/libc/fs/inotify.rs index 3be68c7cf..36911689a 100644 --- a/src/backend/libc/fs/inotify.rs +++ b/src/backend/libc/fs/inotify.rs @@ -76,3 +76,49 @@ bitflags! { const _ = !0; } } + +bitflags! { + /// `IN*` for use with [`InotifyReader`]. + /// + /// [`InotifyReader`]: crate::fs::inotify::InotifyReader + #[repr(transparent)] + #[derive(Default, Copy, Clone, Eq, PartialEq, Hash, Debug)] + pub struct ReadFlags: u32 { + /// `IN_ACCESS` + const ACCESS = c::IN_ACCESS; + /// `IN_ATTRIB` + const ATTRIB = c::IN_ATTRIB; + /// `IN_CLOSE_NOWRITE` + const CLOSE_NOWRITE = c::IN_CLOSE_NOWRITE; + /// `IN_CLOSE_WRITE` + const CLOSE_WRITE = c::IN_CLOSE_WRITE; + /// `IN_CREATE` + const CREATE = c::IN_CREATE; + /// `IN_DELETE` + const DELETE = c::IN_DELETE; + /// `IN_DELETE_SELF` + const DELETE_SELF = c::IN_DELETE_SELF; + /// `IN_MODIFY` + const MODIFY = c::IN_MODIFY; + /// `IN_MOVE_SELF` + const MOVE_SELF = c::IN_MOVE_SELF; + /// `IN_MOVED_FROM` + const MOVED_FROM = c::IN_MOVED_FROM; + /// `IN_MOVED_TO` + const MOVED_TO = c::IN_MOVED_TO; + /// `IN_OPEN` + const OPEN = c::IN_OPEN; + + /// `IN_IGNORED` + const IGNORED = c::IN_IGNORED; + /// `IN_ISDIR` + const ISDIR = c::IN_ISDIR; + /// `IN_Q_OVERFLOW` + const QUEUE_OVERFLOW = c::IN_Q_OVERFLOW; + /// `IN_UNMOUNT` + const UNMOUNT = c::IN_UNMOUNT; + + /// + const _ = !0; + } +} diff --git a/src/backend/linux_raw/fs/inotify.rs b/src/backend/linux_raw/fs/inotify.rs index 1876e8349..d517151e7 100644 --- a/src/backend/linux_raw/fs/inotify.rs +++ b/src/backend/linux_raw/fs/inotify.rs @@ -76,3 +76,49 @@ bitflags! { const _ = !0; } } + +bitflags! { + /// `IN*` for use with [`InotifyReader`]. + /// + /// [`InotifyReader`]: crate::fs::inotify::InotifyReader + #[repr(transparent)] + #[derive(Default, Copy, Clone, Eq, PartialEq, Hash, Debug)] + pub struct ReadFlags: c::c_uint { + /// `IN_ACCESS` + const ACCESS = linux_raw_sys::general::IN_ACCESS; + /// `IN_ATTRIB` + const ATTRIB = linux_raw_sys::general::IN_ATTRIB; + /// `IN_CLOSE_NOWRITE` + const CLOSE_NOWRITE = linux_raw_sys::general::IN_CLOSE_NOWRITE; + /// `IN_CLOSE_WRITE` + const CLOSE_WRITE = linux_raw_sys::general::IN_CLOSE_WRITE; + /// `IN_CREATE` + const CREATE = linux_raw_sys::general::IN_CREATE; + /// `IN_DELETE` + const DELETE = linux_raw_sys::general::IN_DELETE; + /// `IN_DELETE_SELF` + const DELETE_SELF = linux_raw_sys::general::IN_DELETE_SELF; + /// `IN_MODIFY` + const MODIFY = linux_raw_sys::general::IN_MODIFY; + /// `IN_MOVE_SELF` + const MOVE_SELF = linux_raw_sys::general::IN_MOVE_SELF; + /// `IN_MOVED_FROM` + const MOVED_FROM = linux_raw_sys::general::IN_MOVED_FROM; + /// `IN_MOVED_TO` + const MOVED_TO = linux_raw_sys::general::IN_MOVED_TO; + /// `IN_OPEN` + const OPEN = linux_raw_sys::general::IN_OPEN; + + /// `IN_IGNORED` + const IGNORED = linux_raw_sys::general::IN_IGNORED; + /// `IN_ISDIR` + const ISDIR = linux_raw_sys::general::IN_ISDIR; + /// `IN_Q_OVERFLOW` + const QUEUE_OVERFLOW = linux_raw_sys::general::IN_Q_OVERFLOW; + /// `IN_UNMOUNT` + const UNMOUNT = linux_raw_sys::general::IN_UNMOUNT; + + /// + const _ = !0; + } +} diff --git a/src/fs/inotify.rs b/src/fs/inotify.rs index addcfda57..21b85aca0 100644 --- a/src/fs/inotify.rs +++ b/src/fs/inotify.rs @@ -1,9 +1,13 @@ //! inotify support for working with inotifies -pub use crate::backend::fs::inotify::{CreateFlags, WatchFlags}; +pub use crate::backend::fs::inotify::{CreateFlags, ReadFlags, WatchFlags}; use crate::backend::fs::syscalls; use crate::fd::{AsFd, OwnedFd}; +use crate::ffi::CStr; use crate::io; +use crate::io::{read_uninit, Errno}; +use core::mem::{align_of, size_of, MaybeUninit}; +use linux_raw_sys::general::inotify_event; /// `inotify_init1(flags)`—Creates a new inotify object. /// @@ -41,3 +45,118 @@ pub fn inotify_add_watch( pub fn inotify_remove_watch(inot: impl AsFd, wd: i32) -> io::Result<()> { syscalls::inotify_rm_watch(inot.as_fd(), wd) } + +/// An inotify event iterator implemented with the read syscall. +/// +/// See the [`RawDir`] API for more details and usage examples as this API is +/// based on it. +/// +/// [`RawDir`]: crate::fs::raw_dir::RawDir +pub struct InotifyReader<'buf, Fd: AsFd> { + fd: Fd, + buf: &'buf mut [MaybeUninit], + initialized: usize, + offset: usize, +} + +impl<'buf, Fd: AsFd> InotifyReader<'buf, Fd> { + /// Create a new iterator from the given file descriptor and buffer. + pub fn new(fd: Fd, buf: &'buf mut [MaybeUninit]) -> Self { + Self { + fd, + buf: { + let offset = buf.as_ptr().align_offset(align_of::()); + if offset < buf.len() { + &mut buf[offset..] + } else { + &mut [] + } + }, + initialized: 0, + offset: 0, + } + } +} + +/// An inotify event. +#[derive(Debug)] +pub struct InotifyEvent<'a> { + wd: i32, + events: ReadFlags, + cookie: u32, + file_name: Option<&'a CStr>, +} + +impl<'a> InotifyEvent<'a> { + /// Returns the watch for which this event occurs. + #[inline] + pub fn wd(&self) -> i32 { + self.wd + } + + /// Returns a description of the events. + #[inline] + #[doc(alias = "mask")] + pub fn events(&self) -> ReadFlags { + self.events + } + + /// Returns the unique cookie associating related events. + #[inline] + pub fn cookie(&self) -> u32 { + self.cookie + } + + /// Returns the file name of this event, if any. + #[inline] + pub fn file_name(&self) -> Option<&CStr> { + self.file_name + } +} + +impl<'buf, Fd: AsFd> InotifyReader<'buf, Fd> { + /// Read the next inotify event. + #[allow(unsafe_code)] + pub fn next(&mut self) -> io::Result { + if self.is_buffer_empty() { + match read_uninit(self.fd.as_fd(), self.buf).map(|(init, _)| init.len()) { + Ok(0) => return Err(Errno::INVAL), + Ok(bytes_read) => { + self.initialized = bytes_read; + self.offset = 0; + } + Err(e) => return Err(e), + } + } + + let ptr = self.buf[self.offset..].as_ptr(); + // SAFETY: + // - This data is initialized by the check above. + // - Assumption: the kernel will not give us partial structs. + // - Assumption: the kernel uses proper alignment between structs. + // - The starting pointer is aligned (performed in RawDir::new) + let event = unsafe { &*ptr.cast::() }; + + self.offset += size_of::() + usize::try_from(event.len).unwrap(); + + Ok(InotifyEvent { + wd: event.wd, + events: ReadFlags::from_bits_retain(event.mask), + cookie: event.cookie, + file_name: if event.len > 0 { + // SAFETY: The kernel guarantees a NUL-terminated string. + Some(unsafe { CStr::from_ptr(event.name.as_ptr().cast()) }) + } else { + None + }, + }) + } + + /// Returns true if the internal buffer is empty and will be refilled when + /// calling [`next`]. This is useful to avoid further blocking reads. + /// + /// [`next`]: Self::next + pub fn is_buffer_empty(&self) -> bool { + self.offset >= self.initialized + } +} diff --git a/tests/fs/inotify.rs b/tests/fs/inotify.rs new file mode 100644 index 000000000..0e5f7c1ee --- /dev/null +++ b/tests/fs/inotify.rs @@ -0,0 +1,109 @@ +use rustix::fs::inotify::{ + inotify_add_watch, inotify_init, CreateFlags, InotifyReader, WatchFlags, +}; +use rustix::io::Errno; +use std::fmt::Write; +use std::fs::{create_dir_all, remove_file, rename, File}; +use std::mem::MaybeUninit; + +#[test] +fn test_inotify_iter() { + let inotify = inotify_init(CreateFlags::NONBLOCK).unwrap(); + create_dir_all("/tmp/.rustix-inotify-test").unwrap(); + inotify_add_watch( + &inotify, + "/tmp/.rustix-inotify-test", + WatchFlags::ALL_EVENTS, + ) + .unwrap(); + + File::create("/tmp/.rustix-inotify-test/foo").unwrap(); + rename( + "/tmp/.rustix-inotify-test/foo", + "/tmp/.rustix-inotify-test/bar", + ) + .unwrap(); + remove_file("/tmp/.rustix-inotify-test/bar").unwrap(); + + let mut output = String::new(); + let mut cookie = 0; + + let mut buf = [MaybeUninit::uninit(); 512]; + let mut iter = InotifyReader::new(inotify, &mut buf); + loop { + let e = match iter.next() { + Err(Errno::WOULDBLOCK) => break, + r => r.unwrap(), + }; + + writeln!(output, "{e:#?}").unwrap(); + if e.cookie() != 0 { + cookie = e.cookie(); + } + } + + let expected = format!( + r#"InotifyEvent {{ + wd: 1, + events: ReadFlags( + CREATE, + ), + cookie: 0, + file_name: Some( + "foo", + ), +}} +InotifyEvent {{ + wd: 1, + events: ReadFlags( + OPEN, + ), + cookie: 0, + file_name: Some( + "foo", + ), +}} +InotifyEvent {{ + wd: 1, + events: ReadFlags( + CLOSE_WRITE, + ), + cookie: 0, + file_name: Some( + "foo", + ), +}} +InotifyEvent {{ + wd: 1, + events: ReadFlags( + MOVED_FROM, + ), + cookie: {cookie}, + file_name: Some( + "foo", + ), +}} +InotifyEvent {{ + wd: 1, + events: ReadFlags( + MOVED_TO, + ), + cookie: {cookie}, + file_name: Some( + "bar", + ), +}} +InotifyEvent {{ + wd: 1, + events: ReadFlags( + DELETE, + ), + cookie: 0, + file_name: Some( + "bar", + ), +}} +"# + ); + assert_eq!(expected, output); +} diff --git a/tests/fs/main.rs b/tests/fs/main.rs index 4ab608fc7..db074fedb 100644 --- a/tests/fs/main.rs +++ b/tests/fs/main.rs @@ -20,6 +20,8 @@ mod file; #[cfg(not(target_os = "wasi"))] mod flock; mod futimens; +#[cfg(linux_kernel)] +mod inotify; mod invalid_offset; #[cfg(not(target_os = "redox"))] mod ioctl;