Skip to main content

fanotify_fid/
lib.rs

1//! # fanotify-fid
2//!
3//! Linux fanotify **FID (File Identifier) mode** event parser and file handle utilities.
4//!
5//! This crate fills the gap left by [`fanotify-rs`](https://crates.io/crates/fanotify-rs),
6//! which only supports non-FID (legacy) event reading.  If you pass
7//! `FAN_REPORT_FID` / `FAN_REPORT_DIR_FID` / `FAN_REPORT_NAME` to
8//! `fanotify_init`, you **must** use this crate (or equivalent code) to
9//! correctly parse the variable-length events.
10//!
11//! ## Requirements
12//!
13//! - Linux kernel **≥ 5.1** (FID mode), **≥ 5.15** (`FAN_REPORT_TARGET_FID`)
14//! - **`CAP_SYS_ADMIN`** capability (run as root)
15//! - Minimum Rust version: **1.75** (edition 2024)
16//!
17//! ## Error handling
18//!
19//! All operations return [`Result<T, FanotifyError>`].  Each error variant
20//! includes the raw errno and a **man-page-level description** explaining
21//! the cause, common pitfalls, and how to fix it.
22//!
23//! ## Quick start
24//!
25//! ```rust,no_run
26//! use fanotify_fid::prelude::*;
27//! use std::os::fd::OwnedFd;
28//!
29//! # fn open_mount(_: &str) -> OwnedFd { panic!() }
30//!
31//! // 1. Create fanotify group in FID mode
32//! let fan = Fanotify::new()
33//!     .nonblock()
34//!     .report_fid()
35//!     .report_dir_fid()
36//!     .report_name()
37//!     .init()
38//!     .unwrap();
39//!
40//! // 2. Add marks (whole filesystem)
41//! fan.mark(FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
42//!          FAN_CREATE | FAN_DELETE | FAN_MODIFY,
43//!          "/").unwrap();
44//!
45//! // 3. Open mount fds for handle resolution
46//! let mount_fds = vec![open_mount("/")];
47//!
48//! // 4. Read events
49//! let mut buf = Vec::with_capacity(65536);
50//! let events = fan.read_events(&mount_fds, &mut buf, None).unwrap();
51//!
52//! for ev in &events {
53//!     println!("{:?} {:?}", ev.event_names(), ev.path);
54//! }
55//! ```
56
57pub mod consts;
58pub mod handle;
59pub mod parse;
60pub mod read;
61pub mod types;
62
63use std::borrow::Cow;
64use std::ffi::OsStr;
65use std::fmt;
66use std::io;
67use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
68use std::os::unix::ffi::OsStrExt;
69
70use crate::types::HandleCache;
71
72/// Convenience re-exports for the most common types and constants.
73pub mod prelude {
74    pub use crate::consts::*;
75    pub use crate::handle::{name_to_handle_at, open_by_handle_at, resolve_file_handle};
76    pub use crate::parse::parse_fid_events;
77    pub use crate::read::{read_fid_events, read_legacy, read_legacy_do, write_response, legacy_buffer_events, set_legacy_buffer_events};
78    pub use crate::types::{FidEvent, HandleCache, HandleKey, LegacyEvent, FanotifyResponse};
79    pub use crate::{fanotify_init, fanotify_mark, open_mount, Fanotify, FanotifyBuilder, FanotifyError};
80}
81
82// ── Error type ──
83
84/// Error type for all fanotify operations.
85///
86/// Carries semantics: you can match on the variant to know which operation
87/// failed, and get the raw OS error code and a human-readable description.
88///
89/// Each variant's `Display` implementation includes a multi-paragraph
90/// man-page-level explanation of the error cause, common pitfalls, and
91/// troubleshooting steps.
92///
93/// This type is `Send + Sync`.
94#[derive(Debug)]
95pub enum FanotifyError {
96    /// `fanotify_init` failed.
97    Init(i32),
98    /// `fanotify_mark` failed.
99    Mark(i32),
100    /// `read` on the fanotify fd failed.
101    Read(i32),
102    /// File handle resolution failed (via `open_by_handle_at` or
103    /// `name_to_handle_at`).
104    Handle(i32),
105    /// Generic I/O error from internal operations (path resolution, etc.).
106    Io(io::Error),
107}
108
109impl fmt::Display for FanotifyError {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        match self {
112            Self::Init(code) => write!(f, "fanotify_init failed (errno={}): {}", code, errno_desc_init(*code)),
113            Self::Mark(code) => write!(f, "fanotify_mark failed (errno={}): {}", code, errno_desc_mark(*code)),
114            Self::Read(code) => write!(f, "fanotify_read failed (errno={}): {}", code, errno_desc_read(*code)),
115            Self::Handle(code) => write!(f, "file_handle operation failed (errno={}): {}", code, errno_desc_handle(*code)),
116            Self::Io(e) => write!(f, "I/O error: {}", e),
117        }
118    }
119}
120
121impl std::error::Error for FanotifyError {}
122
123impl From<io::Error> for FanotifyError {
124    fn from(e: io::Error) -> Self {
125        Self::Io(e)
126    }
127}
128
129fn errno_desc_init(code: i32) -> Cow<'static, str> {
130    match code {
131        libc::EINVAL => Cow::Borrowed(concat!(
132            "An invalid value was passed in flags or event_f_flags.\n",
133            "  Common mistakes:\n",
134            "  - Using FAN_REPORT_NAME without FAN_REPORT_DIR_FID\n",
135            "  - Combining FAN_REPORT_FID with legacy-only flags\n",
136            "  - Setting reserved or unsupported bits in event_f_flags\n",
137            "  Check man fanotify_init(2) for all allowable bits."
138        )),
139        libc::EMFILE => Cow::Borrowed(concat!(
140            "Too many fanotify groups for this user.\n",
141            "  The per-user limit is 128 groups.  Each init() call creates\n",
142            "  a new notification group.  Check if previous groups are still\n",
143            "  open (forgeting to close an OwnedFd can leak groups)."
144        )),
145        libc::ENOMEM => Cow::Borrowed(concat!(
146            "Out of memory.\n",
147            "  The kernel could not allocate memory for the notification\n",
148            "  group's internal data structures.  Try reducing the event\n",
149            "  queue size or closing other fanotify groups."
150        )),
151        libc::ENOSYS => Cow::Borrowed(concat!(
152            "This kernel does not support fanotify.\n",
153            "  The fanotify API is available only if the kernel was\n",
154            "  configured with CONFIG_FANOTIFY.  Most distro kernels\n",
155            "  include this by default.  Custom or container-optimized\n",
156            "  kernels may omit it.  Check /proc/config.gz or\n",
157            "  /boot/config-$(uname -r) for CONFIG_FANOTIFY=y."
158        )),
159        libc::EPERM => Cow::Borrowed(concat!(
160            "Need CAP_SYS_ADMIN capability.\n",
161            "  Creating a fanotify group requires elevated privileges.\n",
162            "  Run as root, or add the capability via:\n",
163            "    sudo setcap cap_sys_admin+ep /path/to/binary\n",
164            "  Or run the process under a user namespace with\n",
165            "  CAP_SYS_ADMIN mapped."
166        )),
167        _ => Cow::Owned(format!("Unknown error (errno={}).  See fanotify_init(2) for details.", code)),
168    }
169}
170
171fn errno_desc_mark(code: i32) -> Cow<'static, str> {
172    match code {
173        libc::EBADF => Cow::Borrowed(concat!(
174            "Invalid file descriptor.\n",
175            "  Either the fanotify fd is invalid, or pathname is relative\n",
176            "  but dirfd is neither AT_FDCWD nor a valid fd.\n",
177            "  Check that fanotify_init succeeded and the fd hasn't been\n",
178            "  closed or moved into another process."
179        )),
180        libc::EINVAL => Cow::Borrowed(concat!(
181            "Invalid flags or mask, or wrong notification class.\n",
182            "  Common causes:\n",
183            "  - The fanotify group was created with FAN_CLASS_NOTIF but\n",
184            "    mask contains permission events (FAN_OPEN_PERM or\n",
185            "    FAN_ACCESS_PERM).  Permission events require\n",
186            "    FAN_CLASS_CONTENT or FAN_CLASS_PRE_CONTENT.\n",
187            "  - An invalid combination of mark flags was passed.\n",
188            "  - In FID mode, some mask flags are incompatible."
189        )),
190        libc::ENODEV => Cow::Borrowed(concat!(
191            "Filesystem does not support fsid.\n",
192            "  The filesystem indicated by pathname is not associated with\n",
193            "  a filesystem that supports fsid (e.g., tmpfs).  This error\n",
194            "  can occur only with a fanotify group that identifies objects\n",
195            "  by file handles (FID mode)."
196        )),
197        libc::ENOENT => Cow::Borrowed(concat!(
198            "Path does not exist.\n",
199            "  The filesystem object indicated by dirfd and pathname does\n",
200            "  not exist.  This also occurs when trying to remove a mark\n",
201            "  from an object which is not marked.\n",
202            "  Tip: use FAN_MARK_DONT_FOLLOW if pathname is a dangling\n",
203            "  symlink, or check that the path exists before marking."
204        )),
205        libc::ENOMEM => Cow::Borrowed(concat!(
206            "Out of memory.\n",
207            "  The kernel could not allocate memory to store the mark.\n",
208            "  Try reducing the number of marks or closing other groups."
209        )),
210        libc::ENOSPC => Cow::Borrowed(concat!(
211            "Too many marks (exceeded 8192 limit).\n",
212            "  The default mark limit is 8192 per group.  Either:\n",
213            "  - Use FAN_MARK_FILESYSTEM instead of marking individual\n",
214            "    paths to reduce mark count.\n",
215            "  - Pass FAN_UNLIMITED_MARKS to init() if you have\n",
216            "    CAP_SYS_ADMIN and genuinely need more marks.\n",
217            "  - Remove unused marks with FAN_MARK_REMOVE."
218        )),
219        libc::ENOSYS => Cow::Borrowed(concat!(
220            "This kernel does not implement fanotify_mark.\n",
221            "  CONFIG_FANOTIFY is likely missing from the kernel config."
222        )),
223        libc::ENOTDIR => Cow::Borrowed(concat!(
224            "FAN_MARK_ONLYDIR specified but path is not a directory.\n",
225            "  Remove FAN_MARK_ONLYDIR if you intended to mark a regular\n",
226            "  file, or point the path to a directory."
227        )),
228        libc::EOPNOTSUPP => Cow::Borrowed(concat!(
229            "Filesystem does not support file handles.\n",
230            "  The object is on a filesystem that does not support the\n",
231            "  encoding of file handles (e.g., some FUSE filesystems,\n",
232            "  network filesystems without export support).  This error\n",
233            "  can occur only with a fanotify group in FID mode."
234        )),
235        libc::EXDEV => Cow::Borrowed(concat!(
236            "Filesystem subvolume uses a different fsid.\n",
237            "  The object resides within a filesystem subvolume (e.g.,\n",
238            "  btrfs subvolume) which uses a different fsid than its root\n",
239            "  superblock.  Try marking the subvolume's mount point,\n",
240            "  or use FAN_MARK_FILESYSTEM on the subvolume directly."
241        )),
242        _ => Cow::Owned(format!("Unknown error (errno={}).  See fanotify_mark(2) for details.", code)),
243    }
244}
245
246fn errno_desc_read(code: i32) -> Cow<'static, str> {
247    match code {
248        libc::EAGAIN => Cow::Borrowed(concat!(
249            "No events available (non-blocking fd).\n",
250            "  The fanotify fd was created with FAN_NONBLOCK and no events\n",
251            "  are currently pending.  This is not an error — retry later\n",
252            "  using epoll/poll/select to wait for readability, or switch\n",
253            "  to blocking mode (remove FAN_NONBLOCK)."
254        )),
255        libc::EBADF => Cow::Borrowed(concat!(
256            "Invalid file descriptor.\n",
257            "  The fanotify fd is not a valid open file descriptor or\n",
258            "  was not opened for reading.  Check that fanotify_init()\n",
259            "  succeeded and the fd hasn't been closed."
260        )),
261        libc::EINTR => Cow::Borrowed(concat!(
262            "Interrupted by signal.\n",
263            "  The read call was interrupted by a signal before any data\n",
264            "  was read.  Retry the read (EINTR is transient)."
265        )),
266        libc::ENOMEM => Cow::Borrowed(concat!(
267            "Out of memory.\n",
268            "  Cannot allocate memory for the read buffer.  Try reducing\n",
269            "  the buffer size or closing other memory-intensive\n",
270            "  applications."
271        )),
272        _ => Cow::Owned(format!("Unknown error (errno={}).  See fanotify_read(2) for details.", code)),
273    }
274}
275
276fn errno_desc_handle(code: i32) -> Cow<'static, str> {
277    match code {
278        libc::EBADF => Cow::Borrowed(concat!(
279            "Invalid mount file descriptor.\n",
280            "  The mount_fd passed to open_by_handle_at is not a valid\n",
281            "  open file descriptor.  Make sure open_mount() succeeded\n",
282            "  and the fd hasn't been closed.  The mount_fd must belong\n",
283            "  to a mount point on the same filesystem as the handle."
284        )),
285        libc::ENOENT => Cow::Borrowed(concat!(
286            "File or directory does not exist.\n",
287            "  The file identified by the handle has been deleted.  In\n",
288            "  fanotify FID mode this is expected when events are\n",
289            "  delivered concurrently with deletions.  Use a persistent\n",
290            "  HandleCache to recover paths in later read cycles.\n",
291            "  See parse::resolve_with_cache for details."
292        )),
293        libc::EINVAL => Cow::Borrowed(concat!(
294            "Invalid handle or flags.\n",
295            "  The file handle data is malformed or the flags passed to\n",
296            "  open_by_handle_at are invalid.  This may indicate a kernel\n",
297            "  bug or corrupted handle data."
298        )),
299        libc::EOVERFLOW => Cow::Borrowed(concat!(
300            "Handle buffer too small.\n",
301            "  The initial buffer passed to name_to_handle_at was too\n",
302            "  small.  This is handled automatically by retrying with\n",
303            "  the correct size, but if you see this error it means the\n",
304            "  retry also failed.  Try using a larger initial buffer."
305        )),
306        libc::EOPNOTSUPP => Cow::Borrowed(concat!(
307            "Filesystem does not support file handles.\n",
308            "  The filesystem does not support name_to_handle_at or\n",
309            "  open_by_handle_at.  Common examples:\n",
310            "  - tmpfs (only supports handles for directories)\n",
311            "  - Some FUSE filesystems\n",
312            "  - Network filesystems without export support\n",
313            "  Try using open_mount() on a different path backed by a\n",
314            "  filesystem that supports handles (e.g., ext4, xfs, btrfs)."
315        )),
316        _ => Cow::Owned(format!("Unknown error (errno={}).  See name_to_handle_at(2) for details.", code)),
317    }
318}
319
320// Convenience alias.
321/// Alias for `Result<T, FanotifyError>`.
322pub type Result<T> = std::result::Result<T, FanotifyError>;
323
324// ── High-level Fanotify wrapper ──
325
326/// An RAII fanotify file descriptor with safe `mark` and `read_events`
327/// methods.
328///
329/// The underlying `OwnedFd` is automatically closed on `Drop`.
330///
331/// `Fanotify` is `Send + Sync` (delegates to `OwnedFd` which is also
332/// `Send + Sync`).  You may share it across threads safely.
333///
334/// Use [`FanotifyBuilder`] (via [`Fanotify::new`]) for ergonomic construction:
335///
336/// ```rust,no_run
337/// use fanotify_fid::prelude::*;
338///
339/// let fan = Fanotify::new()
340///     .report_fid()
341///     .report_dir_fid()
342///     .report_name()
343///     .init()
344///     .unwrap();
345/// ```
346#[derive(Debug)]
347pub struct Fanotify {
348    fd: OwnedFd,
349}
350
351impl Fanotify {
352    /// Create a [`FanotifyBuilder`] with default settings
353    /// (`FAN_CLASS_NOTIF | FAN_CLOEXEC`).
354    ///
355    /// Call `.init()` on the builder to create the fanotify group.
356    #[allow(clippy::new_ret_no_self)]
357    pub fn new() -> FanotifyBuilder {
358        FanotifyBuilder {
359            flags: consts::FAN_CLASS_NOTIF | consts::FAN_CLOEXEC,
360            event_f_flags: 0,
361        }
362    }
363
364    /// Add or remove a mark on the given path.
365    ///
366    /// # Example
367    ///
368    /// ```rust,no_run
369    /// use fanotify_fid::prelude::*;
370    ///
371    /// let fan = Fanotify::new().report_fid().init().unwrap();
372    /// fan.mark(FAN_MARK_ADD | FAN_MARK_FILESYSTEM,
373    ///          FAN_CREATE | FAN_DELETE, "/").unwrap();
374    /// ```
375    pub fn mark<P: AsRef<OsStr> + ?Sized>(
376        &self,
377        flags: u32,
378        mask: u64,
379        path: &P,
380    ) -> std::result::Result<(), FanotifyError> {
381        fanotify_mark(&self.fd, flags, mask, consts::AT_FDCWD, path)
382    }
383
384    /// Read and parse FID-format events from the fanotify file descriptor.
385    ///
386    /// Convenience wrapper around [`read_fid_events`] that takes `&self`.
387    ///
388    /// See [`read_fid_events`] for full documentation.
389    pub fn read_events(
390        &self,
391        mount_fds: &[OwnedFd],
392        buf: &mut Vec<u8>,
393        cache: Option<&mut HandleCache>,
394    ) -> std::result::Result<Vec<crate::types::FidEvent>, FanotifyError> {
395        crate::read::read_fid_events(&self.fd, mount_fds, buf, cache)
396    }
397
398    /// Read legacy (non-FID) events.
399    ///
400    /// The fanotify fd must NOT have been initialized with `FAN_REPORT_FID`.
401    pub fn read_legacy(&self) -> Result<Vec<crate::types::LegacyEvent>> {
402        crate::read::read_legacy(&self.fd)
403    }
404
405    /// Read legacy events with a callback.
406    ///
407    /// Convenience wrapper around [`read_legacy_do`](crate::read::read_legacy_do).
408    pub fn read_legacy_do<F>(&self, callback: F) -> Result<()>
409    where
410        F: FnMut(&crate::types::LegacyEvent),
411    {
412        crate::read::read_legacy_do(&self.fd, callback)
413    }
414
415    /// Write a permission response.
416    ///
417    /// Convenience wrapper around [`write_response`](crate::read::write_response).
418    pub fn send_response(&self, response: &crate::types::FanotifyResponse) -> Result<()> {
419        crate::read::write_response(&self.fd, response)
420    }
421
422    /// Get the legacy buffer size (event count).
423    pub fn legacy_buffer_events() -> usize {
424        crate::read::legacy_buffer_events()
425    }
426
427    /// Set the legacy buffer size (event count).
428    pub fn set_legacy_buffer_events(n: usize) {
429        crate::read::set_legacy_buffer_events(n)
430    }
431
432    /// Add a mark on a mount point (monitor all files under it).
433    pub fn mark_mount<P: AsRef<OsStr> + ?Sized>(
434        &self,
435        mask: u64,
436        path: &P,
437    ) -> Result<()> {
438        fanotify_mark(
439            &self.fd,
440            crate::consts::FAN_MARK_ADD | crate::consts::FAN_MARK_MOUNT,
441            mask,
442            crate::consts::AT_FDCWD,
443            path,
444        )
445    }
446
447    /// Get a reference to the underlying `OwnedFd`.
448    pub fn as_fd(&self) -> &OwnedFd {
449        &self.fd
450    }
451
452    /// Consume the wrapper and return the raw file descriptor.
453    ///
454    /// The fd will be closed when `OwnedFd` is dropped.
455    pub fn into_inner(self) -> OwnedFd {
456        self.fd
457    }
458}
459
460impl AsFd for Fanotify {
461    fn as_fd(&self) -> BorrowedFd<'_> {
462        self.fd.as_fd()
463    }
464}
465
466// ── Builder ──
467
468/// Builder for [`Fanotify`].
469///
470/// Created via [`Fanotify::new()`].
471#[derive(Debug, Clone)]
472pub struct FanotifyBuilder {
473    flags: u32,
474    event_f_flags: u32,
475}
476
477impl FanotifyBuilder {
478    /// Enable close-on-exec (always on by default).
479    pub fn cloexec(mut self) -> Self {
480        self.flags |= consts::FAN_CLOEXEC;
481        self
482    }
483
484    /// Make the fanotify fd non-blocking.
485    pub fn nonblock(mut self) -> Self {
486        self.flags |= consts::FAN_NONBLOCK;
487        self
488    }
489
490    /// Set notification class to `FAN_CLASS_NOTIF` (default).
491    pub fn class_notif(mut self) -> Self {
492        self.flags = (self.flags & !0x0C) | consts::FAN_CLASS_NOTIF;
493        self
494    }
495
496    /// Set notification class to `FAN_CLASS_CONTENT` (for permission events).
497    pub fn class_content(mut self) -> Self {
498        self.flags = (self.flags & !0x0C) | 0x0000_0004;
499        self
500    }
501
502    /// Set notification class to `FAN_CLASS_PRE_CONTENT`.
503    pub fn class_pre_content(mut self) -> Self {
504        self.flags = (self.flags & !0x0C) | 0x0000_0008;
505        self
506    }
507
508    /// Report file identifiers (file handles) instead of file descriptors.
509    pub fn report_fid(mut self) -> Self {
510        self.flags |= consts::FAN_REPORT_FID;
511        self
512    }
513
514    /// Report parent directory identifiers.
515    pub fn report_dir_fid(mut self) -> Self {
516        self.flags |= consts::FAN_REPORT_DIR_FID;
517        self
518    }
519
520    /// Report entry names in parent directory events.
521    pub fn report_name(mut self) -> Self {
522        self.flags |= consts::FAN_REPORT_NAME;
523        self
524    }
525
526    /// Report thread ID instead of process ID.
527    pub fn report_tid(mut self) -> Self {
528        self.flags |= 0x0000_0100;
529        self
530    }
531
532    /// Remove event queue size limit (needs `CAP_SYS_ADMIN`).
533    pub fn unlimited_queue(mut self) -> Self {
534        self.flags |= 0x0000_0010;
535        self
536    }
537
538    /// Remove mark count limit (needs `CAP_SYS_ADMIN`).
539    pub fn unlimited_marks(mut self) -> Self {
540        self.flags |= 0x0000_0020;
541        self
542    }
543
544    /// Set event_f_flags (flags for opened event fds).
545    ///
546    /// In FID mode, the fanotify fd doesn't produce event fds, so this
547    /// is typically 0.
548    pub fn event_flags(mut self, flags: u32) -> Self {
549        self.event_f_flags = flags;
550        self
551    }
552
553    /// Enable audit logging for permission events.
554    pub fn enable_audit(mut self) -> Self {
555        self.flags |= crate::consts::FAN_ENABLE_AUDIT;
556        self
557    }
558
559    /// Report pidfd for event->pid.
560    pub fn report_pidfd(mut self) -> Self {
561        self.flags |= crate::consts::FAN_REPORT_PIDFD;
562        self
563    }
564
565    /// Report dirent target id (requires Linux ≥ 5.15).
566    ///
567    /// Requires both `FAN_REPORT_DFID_NAME` and `FAN_REPORT_FID`.
568    pub fn report_target_fid(mut self) -> Self {
569        self.flags |= crate::consts::FAN_REPORT_TARGET_FID;
570        self
571    }
572
573    /// Add arbitrary raw flags.
574    pub fn raw_flags(mut self, flags: u32) -> Self {
575        self.flags |= flags;
576        self
577    }
578
579    /// Create the fanotify group.  Returns a [`Fanotify`] handle on success.
580    ///
581    /// See [`fanotify_init`] for error details.
582    pub fn init(self) -> std::result::Result<Fanotify, FanotifyError> {
583        let fd = fanotify_init(self.flags, self.event_f_flags)?;
584        Ok(Fanotify { fd })
585    }
586}
587
588impl Default for FanotifyBuilder {
589    fn default() -> Self {
590        FanotifyBuilder {
591            flags: consts::FAN_CLASS_NOTIF | consts::FAN_CLOEXEC,
592            event_f_flags: 0,
593        }
594    }
595}
596
597// ── Low-level wrappers ──
598
599/// Thin safe wrapper around `fanotify_init` (raw syscall).
600///
601/// Returns an `OwnedFd` that will be automatically closed on drop.
602///
603/// Requires Linux **≥ 5.1** for FID mode flags (`FAN_REPORT_FID`,
604/// `FAN_REPORT_DIR_FID`, `FAN_REPORT_NAME`).  Some flags like
605/// `FAN_REPORT_TARGET_FID` require newer kernels (≥ 5.15).
606///
607/// Provided for convenience when you prefer free functions over the
608/// [`Fanotify`] struct.
609pub fn fanotify_init(flags: u32, event_f_flags: u32) -> std::result::Result<OwnedFd, FanotifyError> {
610    // SAFETY: `fanotify_init` is a pure kernel syscall with no memory-safety
611    // requirements beyond passing correctly-typed integer flags.  The kernel
612    // validates all flag combinations and returns EINVAL on error.
613    let fd = unsafe { libc::fanotify_init(flags as libc::c_uint, event_f_flags as libc::c_uint) };
614    if fd < 0 {
615        return Err(FanotifyError::Init(io::Error::last_os_error().raw_os_error().unwrap_or(0)));
616    }
617    // SAFETY: `fd` was just returned by a successful `fanotify_init` call and
618    // is therefore a valid, owned file descriptor.  `OwnedFd::from_raw_fd`
619    // takes ownership; it will be closed on drop.
620    Ok(unsafe { <OwnedFd as FromRawFd>::from_raw_fd(fd) })
621}
622
623/// Thin safe wrapper around `fanotify_mark` (raw syscall).
624///
625/// `path` can be a `&Path`, `&str`, or anything `AsRef<OsStr>`.
626pub fn fanotify_mark<P: AsRef<OsStr> + ?Sized>(
627    fanotify_fd: &OwnedFd,
628    flags: u32,
629    mask: u64,
630    dirfd: i32,
631    path: &P,
632) -> std::result::Result<(), FanotifyError> {
633    let mut raw = path.as_ref().as_bytes().to_vec();
634    raw.push(0); // null-terminate
635
636    // SAFETY: `fanotify_mark` is a pure kernel syscall.  `path` has been
637    // null-terminated and `fanotify_fd` is a valid `OwnedFd`.  The kernel
638    // validates all arguments internally.
639    let ret = unsafe {
640        libc::fanotify_mark(
641            fanotify_fd.as_raw_fd(),
642            flags as libc::c_uint,
643            mask,
644            dirfd,
645            raw.as_ptr() as *const libc::c_char,
646        )
647    };
648    if ret < 0 {
649        return Err(FanotifyError::Mark(io::Error::last_os_error().raw_os_error().unwrap_or(0)));
650    }
651    Ok(())
652}
653
654/// Open a path with `O_PATH` to obtain a mount fd for handle resolution.
655///
656/// The returned `OwnedFd` is opened with `O_PATH | O_CLOEXEC`, and can be
657/// used with [`resolve_file_handle`] and [`read_fid_events`].
658///
659/// This is equivalent to `open(path, O_PATH | O_CLOEXEC)`.
660///
661/// # Errors
662///
663/// Returns `FanotifyError::Io` if the path cannot be opened (permissions,
664/// does not exist, etc.).
665pub fn open_mount<P: AsRef<OsStr> + ?Sized>(path: &P) -> std::result::Result<OwnedFd, FanotifyError> {
666    use std::os::unix::fs::OpenOptionsExt;
667    let file = std::fs::OpenOptions::new()
668        .custom_flags(libc::O_PATH | libc::O_CLOEXEC)
669        .read(true)
670        .open(path.as_ref())
671        .map_err(FanotifyError::Io)?;
672    let fd = file.into();
673    Ok(fd)
674}
675
676// ── Comprehensive tests ──
677
678#[cfg(test)]
679mod integration_tests {
680    use super::*;
681    use std::path::PathBuf;
682    use crate::types::{FidEvent, LegacyEvent, FanotifyResponse, HandleCache};
683
684    // ── Constants tests ──
685
686    #[test]
687    fn test_new_event_constants_exist() {
688        // Verify all naughtyfy-sourced constants are accessible
689        let _ = consts::FAN_OPEN_PERM;
690        let _ = consts::FAN_ACCESS_PERM;
691        let _ = consts::FAN_OPEN_EXEC_PERM;
692        let _ = consts::FAN_RENAME;
693        let _ = consts::FAN_FS_ERROR;
694        let _ = consts::FAN_REPORT_TID;
695        let _ = consts::FAN_REPORT_PIDFD;
696        let _ = consts::FAN_REPORT_TARGET_FID;
697        let _ = consts::FAN_UNLIMITED_QUEUE;
698        let _ = consts::FAN_UNLIMITED_MARKS;
699        let _ = consts::FAN_ENABLE_AUDIT;
700        let _ = consts::FAN_CLASS_CONTENT;
701        let _ = consts::FAN_CLASS_PRE_CONTENT;
702        let _ = consts::FAN_REPORT_DFID_NAME;
703        let _ = consts::FAN_REPORT_DFID_NAME_TARGET;
704        let _ = consts::FAN_MARK_DONT_FOLLOW;
705        let _ = consts::FAN_MARK_ONLYDIR;
706        let _ = consts::FAN_MARK_MOUNT;
707        let _ = consts::FAN_MARK_IGNORED_MASK;
708        let _ = consts::FAN_MARK_IGNORED_SURV_MODIFY;
709        let _ = consts::FAN_MARK_EVICTABLE;
710        let _ = consts::FAN_MARK_IGNORE;
711        let _ = consts::FAN_MARK_IGNORE_SURV;
712        let _ = consts::FAN_ALLOW;
713        let _ = consts::FAN_DENY;
714        let _ = consts::FAN_AUDIT;
715        let _ = consts::O_RDONLY;
716        let _ = consts::O_WRONLY;
717        let _ = consts::O_RDWR;
718        let _ = consts::O_APPEND;
719        let _ = consts::O_CLOEXEC;
720    }
721
722    #[test]
723    fn test_deprecated_constants_still_compile() {
724        #[allow(deprecated)]
725        {
726            let _ = consts::FAN_ALL_CLASS_BITS;
727            let _ = consts::FAN_ALL_INIT_FLAGS;
728            let _ = consts::FAN_ALL_MARK_FLAGS;
729            let _ = consts::FAN_ALL_EVENTS;
730            let _ = consts::FAN_ALL_PERM_EVENTS;
731            let _ = consts::FAN_ALL_OUTGOING_EVENTS;
732        }
733    }
734
735    #[test]
736    fn test_mask_to_event_names_includes_new() {
737        let names = consts::mask_to_event_names(
738            consts::FAN_OPEN_PERM | consts::FAN_RENAME | consts::FAN_FS_ERROR
739        );
740        assert!(names.contains(&"OPEN_PERM"));
741        assert!(names.contains(&"RENAME"));
742        assert!(names.contains(&"FS_ERROR"));
743    }
744
745    #[test]
746    fn test_composed_event_masks() {
747        let close = consts::FAN_CLOSE;
748        assert_eq!(close, consts::FAN_CLOSE_WRITE | consts::FAN_CLOSE_NOWRITE);
749
750        let mv = consts::FAN_MOVE;
751        assert_eq!(mv, consts::FAN_MOVED_FROM | consts::FAN_MOVED_TO);
752
753        let dfid_name = consts::FAN_REPORT_DFID_NAME;
754        assert_eq!(dfid_name, consts::FAN_REPORT_DIR_FID | consts::FAN_REPORT_NAME);
755    }
756
757    // ── Builder tests ──
758
759    #[test]
760    fn test_builder_default_flags() {
761        let builder = FanotifyBuilder::default();
762        // Default should be NOTIF (0) + CLOEXEC
763        // NOTIF=0 means the class bits (0x0C) are clear
764        assert!(builder.flags & 0x0C == 0, "class bits should be NOTIF");
765        assert!(builder.flags & consts::FAN_CLOEXEC != 0, "CLOEXEC should be set by default");
766        assert!(builder.flags & consts::FAN_CLOEXEC != 0);
767    }
768
769    #[test]
770    fn test_builder_chain_all_flags() {
771        let builder = FanotifyBuilder::default()
772            .cloexec()
773            .nonblock()
774            .class_content()
775            .report_fid()
776            .report_dir_fid()
777            .report_name()
778            .report_tid()
779            .report_pidfd()
780            .report_target_fid()
781            .unlimited_queue()
782            .unlimited_marks()
783            .enable_audit()
784            .event_flags(consts::O_CLOEXEC)
785            .raw_flags(0x1000);
786        // Builder should have accumulated flags
787        assert!(builder.flags & consts::FAN_NONBLOCK != 0);
788        assert!(builder.flags & consts::FAN_REPORT_FID != 0);
789        assert!(builder.flags & consts::FAN_REPORT_TID != 0);
790        assert!(builder.flags & consts::FAN_UNLIMITED_QUEUE != 0);
791        assert!(builder.flags & 0x1000 != 0);
792        assert_eq!(builder.event_f_flags, consts::O_CLOEXEC);
793    }
794
795    #[test]
796    fn test_builder_class_modes_are_exclusive() {
797        // Setting class_pre_content should clear class_content bits
798        let b = FanotifyBuilder::default().class_content();
799        assert!(b.flags & 0x0C == consts::FAN_CLASS_CONTENT || (b.flags & 0x0C) == 0x04);
800
801        let b = b.class_pre_content();
802        // 0x08 should be set, 0x04 should not
803        assert_eq!(b.flags & 0x0C, consts::FAN_CLASS_PRE_CONTENT);
804    }
805
806    // ── Error tests ──
807
808    #[test]
809    fn test_error_display_init() {
810        let e = FanotifyError::Init(libc::EPERM);
811        let msg = e.to_string();
812        assert!(msg.contains("fanotify_init"));
813        assert!(msg.contains("CAP_SYS_ADMIN"));
814    }
815
816    #[test]
817    fn test_error_display_mark() {
818        let e = FanotifyError::Mark(libc::ENOENT);
819        let msg = e.to_string();
820        assert!(msg.contains("fanotify_mark"));
821        assert!(msg.contains("does not exist"));
822    }
823
824    #[test]
825    fn test_error_display_read() {
826        let e = FanotifyError::Read(libc::EAGAIN);
827        let msg = e.to_string();
828        assert!(msg.contains("fanotify_read"));
829        assert!(msg.contains("non-blocking"));
830    }
831
832    #[test]
833    fn test_error_display_handle() {
834        let e = FanotifyError::Handle(libc::EOPNOTSUPP);
835        let msg = e.to_string();
836        assert!(msg.contains("file_handle"));
837        assert!(msg.contains("does not support file handles"));
838    }
839
840    #[test]
841    fn test_error_into_from_io() {
842        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
843        let e: FanotifyError = io_err.into();
844        match e {
845            FanotifyError::Io(_) => {}
846            _ => panic!("expected Io variant"),
847        }
848    }
849
850    #[test]
851    fn test_error_impl_error_trait() {
852        fn check_error(_: &dyn std::error::Error) {}
853        let e = FanotifyError::Init(libc::EINVAL);
854        check_error(&e); // must compile
855    }
856
857    // ── Type tests ──
858
859    #[test]
860    fn test_fid_event_methods() {
861        let ev = FidEvent {
862            mask: consts::FAN_CREATE | consts::FAN_MODIFY,
863            pid: 42,
864            path: PathBuf::from("/tmp/foo"),
865            dfid_name_handle: None,
866            dfid_name_filename: None,
867            self_handle: None,
868        };
869        assert!(!ev.is_overflow());
870        let names = ev.event_names();
871        assert_eq!(names, vec!["MODIFY", "CREATE"]);
872    }
873
874    #[test]
875    fn test_fid_event_overflow() {
876        let ev = FidEvent {
877            mask: consts::FAN_Q_OVERFLOW,
878            pid: 0,
879            path: PathBuf::new(),
880            dfid_name_handle: None,
881            dfid_name_filename: None,
882            self_handle: None,
883        };
884        assert!(ev.is_overflow());
885    }
886
887    #[test]
888    fn test_legacy_event_auto_close_fd() {
889        // LegacyEvent with fd=-1 should not crash on drop
890        let ev = LegacyEvent {
891            mask: 0,
892            fd: -1,
893            pid: 0,
894            path: PathBuf::new(),
895        };
896        drop(ev);
897    }
898
899    #[test]
900    fn test_fanotify_response_struct() {
901        let resp = FanotifyResponse {
902            fd: 5,
903            response: consts::FAN_ALLOW,
904        };
905        assert_eq!(resp.fd, 5);
906        assert_eq!(resp.response, 0x01);
907    }
908
909    #[test]
910    fn test_handle_cache_type() {
911        use std::collections::HashMap;
912        let _cache: HandleCache = HashMap::new();
913    }
914
915    // ── open_mount test (path resolution without privileges) ──
916
917    #[test]
918    fn test_open_mount_fails_on_nonexistent() {
919        let result = open_mount("/nonexistent_path_12345");
920        assert!(result.is_err());
921    }
922
923    #[test]
924    fn test_open_mount_succeeds_on_dev() {
925        // /dev is always a valid directory even without special permissions
926        let result = open_mount("/dev");
927        assert!(result.is_ok());
928    }
929
930    // ── Fanotify struct tests ──
931
932    #[test]
933    fn test_fanotify_impl_as_fd() {
934        use std::os::fd::AsFd;
935        // We can't create a real Fanotify without CAP_SYS_ADMIN,
936        // but we can verify the trait impl compiles.
937        fn _takes_as_fd(_: &impl AsFd) {}
938        // If this compiles, the impl is correct.
939    }
940
941    // ── Pre-commit sanity tests ──
942
943    /// Make sure all public functions compile with expected signatures.
944    /// This is a compile-time check.
945    #[test]
946    fn test_public_api_function_signatures() {
947        // These just need to compile — verification that signatures are correct
948        fn _check_free_fns() {
949            let _ = fanotify_init(0, 0);
950            let _ = open_mount("/");
951            let _ = handle::name_to_handle_at(std::path::Path::new("/"));
952        }
953
954        // Check all prelude exports resolve
955        fn _check_prelude() {
956            let _ = crate::prelude::Fanotify::new();
957            let _ = crate::prelude::FanotifyBuilder::default();
958            let _ = crate::prelude::FidEvent {
959                mask: 0, pid: 0, path: PathBuf::new(),
960                dfid_name_handle: None, dfid_name_filename: None, self_handle: None,
961            };
962            let _ = crate::prelude::LegacyEvent {
963                mask: 0, fd: -1, pid: 0, path: PathBuf::new(),
964            };
965            let _ = crate::prelude::FanotifyResponse { fd: -1, response: 0 };
966        }
967
968        _check_free_fns();
969        _check_prelude();
970    }
971}