initial commit
This commit is contained in:
9
fanotify/Cargo.toml
Normal file
9
fanotify/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "fanotify"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
thiserror = "2"
|
||||
bitflags = "2"
|
5
fanotify/README
Normal file
5
fanotify/README
Normal file
@ -0,0 +1,5 @@
|
||||
rust style fanotify wrapper
|
||||
|
||||
------------------------------------------
|
||||
|
||||
this crate wraps around calls and consts of fanotify(7) from libc crate.
|
116
fanotify/src/consts.rs
Normal file
116
fanotify/src/consts.rs
Normal file
@ -0,0 +1,116 @@
|
||||
pub use libc::{
|
||||
FAN_ACCESS, FAN_ACCESS_PERM, FAN_ALLOW, FAN_ATTRIB, FAN_AUDIT, FAN_CLASS_CONTENT,
|
||||
FAN_CLASS_NOTIF, FAN_CLASS_PRE_CONTENT, FAN_CLOEXEC, FAN_CLOSE, FAN_CLOSE_NOWRITE,
|
||||
FAN_CLOSE_WRITE, FAN_CREATE, FAN_DELETE, FAN_DELETE_SELF, FAN_DENY, FAN_ENABLE_AUDIT,
|
||||
FAN_EPIDFD, FAN_EVENT_INFO_TYPE_DFID, FAN_EVENT_INFO_TYPE_DFID_NAME, FAN_EVENT_INFO_TYPE_ERROR,
|
||||
FAN_EVENT_INFO_TYPE_FID, FAN_EVENT_INFO_TYPE_NEW_DFID_NAME, FAN_EVENT_INFO_TYPE_OLD_DFID_NAME,
|
||||
FAN_EVENT_INFO_TYPE_PIDFD, FAN_EVENT_ON_CHILD, FAN_FS_ERROR, FAN_INFO, FAN_MARK_ADD,
|
||||
FAN_MARK_DONT_FOLLOW, FAN_MARK_EVICTABLE, FAN_MARK_FILESYSTEM, FAN_MARK_FLUSH, FAN_MARK_IGNORE,
|
||||
FAN_MARK_IGNORE_SURV, FAN_MARK_IGNORED_MASK, FAN_MARK_IGNORED_SURV_MODIFY, FAN_MARK_INODE,
|
||||
FAN_MARK_MOUNT, FAN_MARK_ONLYDIR, FAN_MARK_REMOVE, FAN_MODIFY, FAN_MOVE, FAN_MOVE_SELF,
|
||||
FAN_MOVED_FROM, FAN_MOVED_TO, FAN_NOFD, FAN_NONBLOCK, FAN_NOPIDFD, FAN_ONDIR, FAN_OPEN,
|
||||
FAN_OPEN_EXEC, FAN_OPEN_EXEC_PERM, FAN_OPEN_PERM, FAN_Q_OVERFLOW, FAN_RENAME,
|
||||
FAN_REPORT_DFID_NAME, FAN_REPORT_DFID_NAME_TARGET, FAN_REPORT_DIR_FID, FAN_REPORT_FID,
|
||||
FAN_REPORT_NAME, FAN_REPORT_PIDFD, FAN_REPORT_TARGET_FID, FAN_REPORT_TID,
|
||||
FAN_RESPONSE_INFO_AUDIT_RULE, FAN_RESPONSE_INFO_NONE, FAN_UNLIMITED_MARKS, FAN_UNLIMITED_QUEUE,
|
||||
FANOTIFY_METADATA_VERSION,
|
||||
};
|
||||
|
||||
pub use libc::{
|
||||
O_APPEND, O_CLOEXEC, O_DSYNC, O_LARGEFILE, O_NOATIME, O_NONBLOCK, O_RDONLY, O_RDWR, O_SYNC,
|
||||
O_WRONLY,
|
||||
};
|
||||
|
||||
// NOTE: the definitions is handwritten in 2025-04-03, on Debian trixie(testing) 6.12.20-1 x86_64
|
||||
// it may need updating for future kernel updates, and pls update this comment for maintainence
|
||||
fa_bitflags! {
|
||||
pub struct InitFlags: u32 {
|
||||
/*
|
||||
one of three classes:
|
||||
- notification-only: get notified when file or directory is accessed
|
||||
- content-access: get notified and also check permission when content is ready. usually used by security softwares.
|
||||
- pre-content-access: get notified and also check permission BEFORE content is ready. usually used by storage managers.
|
||||
*/
|
||||
FAN_CLASS_NOTIF;
|
||||
FAN_CLASS_CONTENT;
|
||||
FAN_CLASS_PRE_CONTENT;
|
||||
|
||||
// additional flags
|
||||
FAN_CLOEXEC;
|
||||
FAN_NONBLOCK;
|
||||
FAN_UNLIMITED_QUEUE;
|
||||
FAN_UNLIMITED_MARKS;
|
||||
FAN_ENABLE_AUDIT; // Linux 4.15
|
||||
|
||||
FAN_REPORT_TID; // Linux 4.20
|
||||
FAN_REPORT_FID; // Linux 5.1
|
||||
FAN_REPORT_DIR_FID; // Linux 5.9
|
||||
FAN_REPORT_NAME; // Linux 5.9
|
||||
FAN_REPORT_DFID_NAME; // Linux 5.9, FAN_REPORT_DIR_FID|FAN_REPORT_NAME
|
||||
FAN_REPORT_TARGET_FID; // Linux 5.17 / 5.15.154 / 5.10.220
|
||||
FAN_REPORT_DFID_NAME_TARGET; // Linux 5.17 / 5.15.154 / 5.10.220, FAN_REPORT_DFID_NAME|FAN_REPORT_FID|FAN_REPORT_TARGET_FID
|
||||
FAN_REPORT_PIDFD; // Linux 5.15 / 5.10.220
|
||||
}
|
||||
|
||||
pub struct EventFFlags: ~u32 {
|
||||
O_RDONLY;
|
||||
O_WRONLY;
|
||||
O_RDWR;
|
||||
|
||||
O_LARGEFILE; // file size limit 2G+
|
||||
O_CLOEXEC; // Linux 3.18
|
||||
|
||||
O_APPEND;
|
||||
O_DSYNC;
|
||||
O_NOATIME;
|
||||
O_NONBLOCK;
|
||||
O_SYNC;
|
||||
}
|
||||
|
||||
pub struct MarkFlags: u32 {
|
||||
// operation, choose exactly one
|
||||
FAN_MARK_ADD;
|
||||
FAN_MARK_REMOVE;
|
||||
FAN_MARK_FLUSH;
|
||||
|
||||
// additional flags
|
||||
FAN_MARK_DONT_FOLLOW;
|
||||
FAN_MARK_ONLYDIR;
|
||||
FAN_MARK_MOUNT;
|
||||
FAN_MARK_FILESYSTEM; // Linux 4.20
|
||||
FAN_MARK_IGNORED_MASK;
|
||||
FAN_MARK_IGNORE; // Linux 6.0 / 5.15.154 / 5.10.220
|
||||
FAN_MARK_IGNORED_SURV_MODIFY;
|
||||
FAN_MARK_IGNORE_SURV; // Linux 6.0 / 5.15.154 / 5.10.220, FAN_MARK_IGNORE|FAN_MARK_IGNORED_SURV_MODIFY
|
||||
FAN_MARK_EVICTABLE; // Linux 5.19 / 5.15.154 / 5.10.220
|
||||
}
|
||||
|
||||
pub struct MaskFlags: u64 {
|
||||
FAN_ACCESS;
|
||||
FAN_MODIFY;
|
||||
FAN_CLOSE_WRITE;
|
||||
FAN_CLOSE_NOWRITE;
|
||||
FAN_CLOSE; // FAN_CLOSE_WRITE|FAN_CLOSE_NOWRITE
|
||||
FAN_OPEN;
|
||||
FAN_OPEN_EXEC; // Linux 5.0
|
||||
FAN_ATTRIB; // Linux 5.1
|
||||
FAN_CREATE; // Linux 5.1
|
||||
FAN_DELETE; // Linux 5.1
|
||||
FAN_DELETE_SELF; // Linux 5.1
|
||||
FAN_FS_ERROR; // Linux 5.16 / 5.15.154 / 5.10.220
|
||||
FAN_MOVED_FROM; // Linux 5.1
|
||||
FAN_MOVED_TO; // Linux 5.1
|
||||
FAN_MOVE; // Linux 5.1, FAN_MOVED_FROM|FAN_MOVED_TO
|
||||
FAN_RENAME; // Linux 5.17 / 5.15.154 / 5.10.220
|
||||
FAN_MOVE_SELF; // Linux 5.1
|
||||
|
||||
// Permissions, need FAN_CLASS_CONTENT or FAN_CLASS_PRE_CONTENT on init
|
||||
FAN_OPEN_PERM;
|
||||
FAN_ACCESS_PERM;
|
||||
FAN_OPEN_EXEC_PERM; // Linux 5.0
|
||||
|
||||
// Flags
|
||||
FAN_ONDIR; // enable events on directories
|
||||
FAN_EVENT_ON_CHILD; // enable events on direct
|
||||
}
|
||||
}
|
72
fanotify/src/error.rs
Normal file
72
fanotify/src/error.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
fmt::{Debug, Display},
|
||||
};
|
||||
|
||||
pub struct Errno {
|
||||
raw_errno: i32,
|
||||
}
|
||||
|
||||
impl Errno {
|
||||
pub fn new(errno: i32) -> Self {
|
||||
Self { raw_errno: errno }
|
||||
}
|
||||
pub fn errno() -> Self {
|
||||
Self::new(unsafe { *libc::__errno_location() } )
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Into<i32>> From<E> for Errno {
|
||||
fn from(value: E) -> Self {
|
||||
Self {
|
||||
raw_errno: value.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Errno {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let str = unsafe {
|
||||
let cstr = libc::strerror(self.raw_errno);
|
||||
let len = libc::strlen(cstr);
|
||||
|
||||
let mut buffer = Vec::with_capacity(len);
|
||||
buffer.set_len(len);
|
||||
|
||||
libc::strncpy(buffer.as_mut_ptr() as *mut i8, cstr, len);
|
||||
|
||||
String::from_utf8(buffer)
|
||||
};
|
||||
let result = if let Ok(str) = str {
|
||||
f.write_str(&str)
|
||||
} else {
|
||||
f.write_str(&format!("Unknown error {}", self.raw_errno))
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Errno {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Errno")
|
||||
.field("errno", &self.raw_errno)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for Errno {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Errno;
|
||||
|
||||
#[test]
|
||||
fn test_errno() {
|
||||
assert_eq!(
|
||||
Errno::new(libc::ENOENT).to_string(),
|
||||
"No such file or directory"
|
||||
);
|
||||
assert_eq!(Errno::new(0).to_string(), "Success");
|
||||
}
|
||||
}
|
188
fanotify/src/fanotify.rs
Normal file
188
fanotify/src/fanotify.rs
Normal file
@ -0,0 +1,188 @@
|
||||
use std::{
|
||||
ffi::CString,
|
||||
mem::MaybeUninit,
|
||||
os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd},
|
||||
ptr::null,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
consts::{EventFFlags, InitFlags, MarkFlags, MaskFlags},
|
||||
error::Errno,
|
||||
};
|
||||
|
||||
pub struct Fanotify {
|
||||
fd: OwnedFd,
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("cannot convert path into c string: {0}")]
|
||||
PathError(#[from] std::ffi::NulError),
|
||||
|
||||
#[error("fanotify returned errno: {0}")]
|
||||
FanotifyError(#[from] Errno),
|
||||
}
|
||||
|
||||
impl Fanotify {
|
||||
pub fn init(init_flags: InitFlags, event_fd_flags: EventFFlags) -> Result<Self, Error> {
|
||||
Self::try_init(init_flags, event_fd_flags)
|
||||
}
|
||||
pub fn try_init(init_flags: InitFlags, event_fd_flags: EventFFlags) -> Result<Self, Error> {
|
||||
let fd = unsafe {
|
||||
let ret = libc::fanotify_init(init_flags.bits(), event_fd_flags.bits());
|
||||
if ret == -1 {
|
||||
return Err(Error::FanotifyError(Errno::errno()));
|
||||
}
|
||||
OwnedFd::from_raw_fd(ret)
|
||||
};
|
||||
Ok(Self { fd })
|
||||
}
|
||||
|
||||
pub fn mark<P: Into<String>>(
|
||||
&self,
|
||||
operation: MarkFlags,
|
||||
mask: MaskFlags,
|
||||
dirfd: Option<BorrowedFd>,
|
||||
path: Option<P>,
|
||||
) -> Result<(), Error> {
|
||||
let dirfd = match dirfd {
|
||||
Some(fd) => fd.as_raw_fd(),
|
||||
None => libc::AT_FDCWD,
|
||||
};
|
||||
let result = unsafe {
|
||||
// hold it here to prevent drop. don't merge two matches
|
||||
if let Some(path) = path {
|
||||
let path: String = path.into();
|
||||
let cstr = CString::new(path)?;
|
||||
libc::fanotify_mark(
|
||||
self.fd.as_raw_fd(),
|
||||
operation.bits(),
|
||||
mask.bits(),
|
||||
dirfd,
|
||||
cstr.as_ptr(),
|
||||
)
|
||||
} else {
|
||||
libc::fanotify_mark(
|
||||
self.fd.as_raw_fd(),
|
||||
operation.bits(),
|
||||
mask.bits(),
|
||||
dirfd,
|
||||
null(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
if result != 0 {
|
||||
return Err(Error::FanotifyError(Errno::errno()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_events(&self) -> Result<Vec<Event>, Error> {
|
||||
const BUFFER_SIZE: usize = 4096;
|
||||
const EVENT_SIZE: usize = size_of::<libc::fanotify_event_metadata>();
|
||||
let mut buffer = [0u8; BUFFER_SIZE];
|
||||
let mut result = Vec::new();
|
||||
unsafe {
|
||||
let nread = libc::read(self.fd.as_raw_fd(), buffer.as_mut_ptr().cast(), BUFFER_SIZE);
|
||||
if nread < 0 {
|
||||
return Err(Error::FanotifyError(Errno::new(nread as i32)));
|
||||
}
|
||||
let nread = nread as usize;
|
||||
let mut offset = 0;
|
||||
while offset + EVENT_SIZE <= nread {
|
||||
let mut event: MaybeUninit<libc::fanotify_event_metadata> = MaybeUninit::uninit();
|
||||
std::ptr::copy(
|
||||
buffer.as_ptr().add(offset),
|
||||
event.as_mut_ptr().cast(),
|
||||
EVENT_SIZE,
|
||||
);
|
||||
|
||||
result.push(Event(event.assume_init()));
|
||||
|
||||
offset += EVENT_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn write_response(&self, response: Response) -> Result<(), Errno> {
|
||||
let n = unsafe {
|
||||
libc::write(
|
||||
self.fd.as_raw_fd(),
|
||||
(&response.inner as *const libc::fanotify_response).cast(),
|
||||
size_of::<libc::fanotify_response>(),
|
||||
)
|
||||
};
|
||||
if n == -1 {
|
||||
return Err(Errno::errno());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Event(pub libc::fanotify_event_metadata);
|
||||
|
||||
impl Event {
|
||||
// compatible to nix::sys::fanotify::FanotifyEvent
|
||||
pub fn metadata_version(&self) -> u8 {
|
||||
self.0.vers
|
||||
}
|
||||
pub fn check_metadata_version(&self) -> bool {
|
||||
self.0.vers == libc::FANOTIFY_METADATA_VERSION
|
||||
}
|
||||
pub fn fd(&self) -> Option<BorrowedFd> {
|
||||
if self.0.fd == libc::FAN_NOFD {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { BorrowedFd::borrow_raw(self.0.fd) })
|
||||
}
|
||||
}
|
||||
pub fn pid(&self) -> i32 {
|
||||
self.0.pid
|
||||
}
|
||||
pub fn mask(&self) -> MaskFlags {
|
||||
MaskFlags::from_bits_truncate(self.0.mask)
|
||||
}
|
||||
|
||||
// sometimes we don't want to close the fd immediately, so we forget about it, store it somewhere, and drop it later
|
||||
// it is safe to just call this method without store it in variable, it will be dropped immediately due to the nature of rust
|
||||
pub fn forget_fd(&mut self) -> OwnedFd {
|
||||
let fd = self.0.fd;
|
||||
self.0.fd = libc::FAN_NOFD;
|
||||
|
||||
unsafe { OwnedFd::from_raw_fd(fd) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Event {
|
||||
fn drop(&mut self) {
|
||||
if self.0.fd == libc::FAN_NOFD {
|
||||
return;
|
||||
}
|
||||
|
||||
let e = unsafe { libc::close(self.0.fd) };
|
||||
if !std::thread::panicking() && e == libc::EBADF {
|
||||
panic!("Closing an invalid file descriptor!");
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
inner: libc::fanotify_response,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub const FAN_ALLOW: u32 = libc::FAN_ALLOW;
|
||||
pub const FAN_DENY: u32 = libc::FAN_DENY;
|
||||
pub fn new(fd: BorrowedFd, response: u32) -> Self {
|
||||
Self {
|
||||
inner: libc::fanotify_response {
|
||||
fd: fd.as_raw_fd(),
|
||||
response,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
10
fanotify/src/lib.rs
Normal file
10
fanotify/src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
|
||||
pub mod error;
|
||||
pub mod fanotify;
|
||||
pub mod consts;
|
||||
|
||||
pub use bitflags;
|
||||
|
||||
pub use fanotify::{Fanotify, Error, Response, Response as FanotifyResponse, Event};
|
75
fanotify/src/macros.rs
Normal file
75
fanotify/src/macros.rs
Normal file
@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copied from nix crate and modified to allow multiple patterns in a single block.
|
||||
|
||||
This simplifies flag groups definition.
|
||||
*/
|
||||
macro_rules! fa_bitflags {
|
||||
// modified: accept a list of pub struct, force cast to T
|
||||
(
|
||||
// first
|
||||
$(#[$outer:meta])*
|
||||
pub struct $BitFlags:ident: ~$T:ty {
|
||||
$(
|
||||
$(#[$inner:ident $($args:tt)*])*
|
||||
$Flag:ident;
|
||||
)+
|
||||
}
|
||||
|
||||
// modified part: match rest
|
||||
$($t:tt)*
|
||||
) => {
|
||||
::bitflags::bitflags! {
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(transparent)]
|
||||
$(#[$outer])*
|
||||
pub struct $BitFlags: $T {
|
||||
$(
|
||||
$(#[$inner $($args)*])*
|
||||
// always cast to $T
|
||||
const $Flag = libc::$Flag as $T;
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
// modified part: recursively handle rest structs
|
||||
fa_bitflags! {
|
||||
$($t)*
|
||||
}
|
||||
};
|
||||
|
||||
// from nix: input: accept a list of pub struct
|
||||
(
|
||||
// first
|
||||
$(#[$outer:meta])*
|
||||
pub struct $BitFlags:ident: $T:ty {
|
||||
$(
|
||||
$(#[$inner:ident $($args:tt)*])*
|
||||
$Flag:ident $(as $cast:ty)*;
|
||||
)+
|
||||
}
|
||||
|
||||
// modified part: match rest
|
||||
$($t:tt)*
|
||||
) => {
|
||||
// generate bitflags struct
|
||||
::bitflags::bitflags! {
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
#[repr(transparent)]
|
||||
$(#[$outer])*
|
||||
pub struct $BitFlags: $T {
|
||||
$(
|
||||
$(#[$inner $($args)*])*
|
||||
const $Flag = libc::$Flag $(as $cast)*;
|
||||
)+
|
||||
}
|
||||
}
|
||||
|
||||
// modified part: recursively handle rest structs
|
||||
fa_bitflags! {
|
||||
$($t)*
|
||||
}
|
||||
};
|
||||
|
||||
// modified part: empty block don't produce anything.
|
||||
() => {}
|
||||
}
|
Reference in New Issue
Block a user