Skip to main content

coreshift_core/
inotify.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Raw inotify helpers.
6//!
7//! This module provides low-level interaction with the Linux `inotify` subsystem.
8//! It handles the initialization of watches, reading of raw events, and
9//! decoding of the packed event stream.
10//!
11//! Higher-level modules should use these primitives to monitor configuration
12//! files, log directories, or process markers.
13//! The API stays close to kernel behavior and does only minimal decoding.
14
15use crate::CoreError;
16use crate::error::syscall_ret;
17use crate::reactor::Fd;
18
19/// A decoded inotify event header.
20///
21/// This structure represents an `inotify_event` including its optional name.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct InotifyEvent {
24    /// Watch descriptor that generated this event.
25    pub wd: i32,
26    /// Event mask (e.g., [`MODIFY_MASK`]).
27    pub mask: u32,
28    /// Optional raw name bytes associated with the event.
29    ///
30    /// Inotify names are arbitrary bytes except for the NUL terminator. Callers
31    /// decide whether and how to interpret them as text.
32    pub name: Option<Vec<u8>>,
33}
34
35/// File was modified (`IN_MODIFY`).
36pub const MODIFY_MASK: u32 = libc::IN_MODIFY;
37/// Mask for monitoring package file state changes (`IN_MODIFY | IN_DELETE_SELF | IN_MOVE_SELF`).
38pub const PACKAGE_FILE_MASK: u32 = libc::IN_MODIFY | libc::IN_DELETE_SELF | libc::IN_MOVE_SELF;
39/// Mask for monitoring parent directories that may create, replace, or remove a watched file.
40/// Maps to `IN_CREATE | IN_MOVED_TO | IN_CLOSE_WRITE | IN_MODIFY | IN_DELETE_SELF | IN_MOVE_SELF`.
41pub const PARENT_WATCH_MASK: u32 = libc::IN_CREATE
42    | libc::IN_MOVED_TO
43    | libc::IN_CLOSE_WRITE
44    | libc::IN_MODIFY
45    | libc::IN_DELETE_SELF
46    | libc::IN_MOVE_SELF;
47/// Inotify event queue overflowed (`IN_Q_OVERFLOW`).
48pub const QUEUE_OVERFLOW_MASK: u32 = libc::IN_Q_OVERFLOW;
49/// Watch was removed (`IN_IGNORED`).
50pub const IGNORED_MASK: u32 = libc::IN_IGNORED;
51/// Filesystem containing watched object was unmounted (`IN_UNMOUNT`).
52pub const UNMOUNT_MASK: u32 = libc::IN_UNMOUNT;
53/// Watched file/directory was deleted (`IN_DELETE_SELF`).
54pub const DELETE_SELF_MASK: u32 = libc::IN_DELETE_SELF;
55/// Watched file/directory was moved (`IN_MOVE_SELF`).
56pub const MOVE_SELF_MASK: u32 = libc::IN_MOVE_SELF;
57
58/// Create a non-blocking close-on-exec inotify file descriptor.
59///
60/// The descriptor is created with `IN_CLOEXEC` and `IN_NONBLOCK` set.
61///
62/// ### Fork Safety
63/// The descriptor is `O_CLOEXEC` and will be closed in the child after `exec`.
64///
65/// ### Errors
66/// - `EMFILE`: Process limit on open file descriptors hit.
67/// - `ENFILE`: System-wide limit on open files hit.
68/// - `ENOMEM`: Insufficient kernel memory.
69pub fn init() -> Result<Fd, CoreError> {
70    let fd = unsafe { libc::inotify_init1(libc::IN_CLOEXEC | libc::IN_NONBLOCK) };
71    syscall_ret(fd, "inotify_init1")?;
72    Fd::new(fd, "inotify_init1")
73}
74
75/// Add a watch to an existing inotify instance.
76///
77/// # Arguments
78/// * `fd` - The inotify file descriptor.
79/// * `path` - Path to the file or directory to watch.
80/// * `mask` - Events to monitor (e.g., [`MODIFY_MASK`]).
81///
82/// ### Errors
83/// - `EACCES`: Read access to the path is denied.
84/// - `EBADF`: The provided file descriptor is invalid.
85/// - `EINVAL`: The mask contains invalid bits or the path is invalid.
86/// - `ENOENT`: A component of the path does not exist.
87/// - `ENOSPC`: The user limit on the total number of inotify watches was reached.
88/// - `ENOMEM`: Insufficient kernel memory.
89pub fn add_watch(fd: &Fd, path: &str, mask: u32) -> Result<i32, CoreError> {
90    let path = std::ffi::CString::new(path)
91        .map_err(|_| CoreError::sys(libc::EINVAL, "inotify path contains nul"))?;
92    let wd = unsafe { libc::inotify_add_watch(fd.raw(), path.as_ptr(), mask) };
93    if wd < 0 {
94        return Err(CoreError::sys(
95            std::io::Error::last_os_error().raw_os_error().unwrap_or(0),
96            "inotify_add_watch",
97        ));
98    }
99    Ok(wd)
100}
101
102/// Remove an existing watch descriptor from an inotify instance.
103///
104/// ### Errors
105/// - `EBADF`: The provided file descriptor is invalid.
106/// - `EINVAL`: The watch descriptor `wd` is invalid for this inotify instance.
107pub fn remove_watch(fd: &Fd, wd: i32) -> Result<(), CoreError> {
108    #[cfg(target_os = "android")]
109    let raw_wd = u32::try_from(wd).map_err(|_| CoreError::sys(libc::EINVAL, "inotify_rm_watch"))?;
110
111    #[cfg(not(target_os = "android"))]
112    let raw_wd = wd;
113
114    let ret = unsafe { libc::inotify_rm_watch(fd.raw(), raw_wd) };
115    syscall_ret(ret, "inotify_rm_watch")
116}
117
118/// Read all available inotify events from the descriptor.
119///
120/// This function drains the inotify file descriptor until no more events
121/// are available (`EAGAIN`). It is safe to use with edge-triggered reactors.
122///
123/// ### Edge Cases
124/// - **Zero-length read**: If the descriptor is non-blocking and no data is
125///   ready, this returns `Ok(Vec::new())` (via `EAGAIN` mapping).
126/// - **Partial read**: This function ensures that only complete event
127///   structures are decoded from the buffer.
128///
129/// ### Errors
130/// - `EBADF`: The provided file descriptor is invalid.
131/// - `EFAULT`: The internal buffer points outside the process's address space.
132/// - `EINVAL`: Internal buffer is too small for even one event (should not happen).
133/// - `EIO`: Low-level I/O error.
134pub fn read_events(fd: &Fd) -> Result<Vec<InotifyEvent>, CoreError> {
135    let mut all_events = Vec::new();
136    let mut buf = vec![0u8; 4096];
137
138    loop {
139        match fd.read_slice(&mut buf) {
140            Ok(Some(0)) => break,
141            Ok(Some(n)) => {
142                all_events.extend(decode_events(&buf[..n])?);
143            }
144            Ok(None) => break, // EAGAIN
145            Err(e) => return Err(e),
146        }
147    }
148
149    Ok(all_events)
150}
151
152/// Decode packed inotify events from a raw byte buffer.
153///
154/// This handles multi-event buffers and handles unaligned reads safely.
155/// Malformed or truncated events result in an error.
156pub fn decode_events(buf: &[u8]) -> Result<Vec<InotifyEvent>, CoreError> {
157    let mut events = Vec::new();
158    let mut offset = 0;
159    let base = std::mem::size_of::<libc::inotify_event>();
160
161    while offset + base <= buf.len() {
162        // SAFETY: We have at least 'base' bytes. We use read_unaligned to
163        // handle potential alignment issues in the raw buffer.
164        let event: libc::inotify_event = unsafe {
165            std::ptr::read_unaligned(buf.as_ptr().add(offset) as *const libc::inotify_event)
166        };
167
168        let Some(size) = base.checked_add(event.len as usize) else {
169            return Err(CoreError::sys(libc::EINVAL, "decode_inotify_event"));
170        };
171        if offset + size > buf.len() {
172            return Err(CoreError::sys(libc::EINVAL, "decode_inotify_event"));
173        }
174
175        let name = if event.len > 0 {
176            let name_buf = &buf[offset + base..offset + base + event.len as usize];
177            // Name is null-terminated, but may have multiple trailing nulls for padding.
178            name_buf.split(|&b| b == 0).next().map(|s| s.to_vec())
179        } else {
180            None
181        };
182
183        events.push(InotifyEvent {
184            wd: event.wd,
185            mask: event.mask,
186            name,
187        });
188        offset += size;
189    }
190
191    if offset != buf.len() {
192        return Err(CoreError::sys(libc::EINVAL, "decode_inotify_event"));
193    }
194
195    Ok(events)
196}