use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum EventType {
Create = 0x00000001,
Modify = 0x00000002,
Delete = 0x00000004,
Rename = 0x00000008,
Attrib = 0x00000010,
Open = 0x00000020,
Close = 0x00000040,
DirCreate = 0x00000080,
DirDelete = 0x00000100,
Sync = 0x00000200,
Truncate = 0x00000400,
Link = 0x00000800,
Symlink = 0x00001000,
Xattr = 0x00002000,
Clone = 0x00004000,
}
impl EventType {
pub fn mask(&self) -> u32 {
*self as u32
}
pub fn as_str(&self) -> &'static str {
match self {
EventType::Create => "CREATE",
EventType::Modify => "MODIFY",
EventType::Delete => "DELETE",
EventType::Rename => "RENAME",
EventType::Attrib => "ATTRIB",
EventType::Open => "OPEN",
EventType::Close => "CLOSE",
EventType::DirCreate => "DIR_CREATE",
EventType::DirDelete => "DIR_DELETE",
EventType::Sync => "SYNC",
EventType::Truncate => "TRUNCATE",
EventType::Link => "LINK",
EventType::Symlink => "SYMLINK",
EventType::Xattr => "XATTR",
EventType::Clone => "CLONE",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"CREATE" => Some(EventType::Create),
"MODIFY" => Some(EventType::Modify),
"DELETE" => Some(EventType::Delete),
"RENAME" => Some(EventType::Rename),
"ATTRIB" => Some(EventType::Attrib),
"OPEN" => Some(EventType::Open),
"CLOSE" => Some(EventType::Close),
"DIR_CREATE" => Some(EventType::DirCreate),
"DIR_DELETE" => Some(EventType::DirDelete),
"SYNC" => Some(EventType::Sync),
"TRUNCATE" => Some(EventType::Truncate),
"LINK" => Some(EventType::Link),
"SYMLINK" => Some(EventType::Symlink),
"XATTR" => Some(EventType::Xattr),
"CLONE" => Some(EventType::Clone),
_ => None,
}
}
pub fn all() -> &'static [EventType] {
&[
EventType::Create,
EventType::Modify,
EventType::Delete,
EventType::Rename,
EventType::Attrib,
EventType::Open,
EventType::Close,
EventType::DirCreate,
EventType::DirDelete,
EventType::Sync,
EventType::Truncate,
EventType::Link,
EventType::Symlink,
EventType::Xattr,
EventType::Clone,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EventMask(pub u32);
impl EventMask {
pub const NONE: EventMask = EventMask(0);
pub const ALL: EventMask = EventMask(0xFFFFFFFF);
pub const FILE_CHANGES: EventMask = EventMask(
EventType::Create as u32
| EventType::Modify as u32
| EventType::Delete as u32
| EventType::Rename as u32,
);
pub const DIR_CHANGES: EventMask =
EventMask(EventType::DirCreate as u32 | EventType::DirDelete as u32);
pub fn new(mask: u32) -> Self {
EventMask(mask)
}
pub fn from_events(events: &[EventType]) -> Self {
let mut mask = 0u32;
for event in events {
mask |= event.mask();
}
EventMask(mask)
}
pub fn contains(&self, event: EventType) -> bool {
(self.0 & event.mask()) != 0
}
pub fn add(&mut self, event: EventType) {
self.0 |= event.mask();
}
pub fn remove(&mut self, event: EventType) {
self.0 &= !event.mask();
}
pub fn raw(&self) -> u32 {
self.0
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn events(&self) -> Vec<EventType> {
EventType::all()
.iter()
.filter(|e| self.contains(**e))
.copied()
.collect()
}
}
impl core::ops::BitOr for EventMask {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
EventMask(self.0 | rhs.0)
}
}
impl core::ops::BitAnd for EventMask {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
EventMask(self.0 & rhs.0)
}
}
#[derive(Debug, Clone)]
pub struct FsEvent {
pub event_type: EventType,
pub dataset: String,
pub path: String,
pub object_id: u64,
pub old_path: Option<String>,
pub timestamp: u64,
pub txg: u64,
pub size: Option<u64>,
pub pid: Option<u32>,
}
impl FsEvent {
pub fn new(event_type: EventType, dataset: &str, path: &str) -> Self {
Self {
event_type,
dataset: dataset.into(),
path: path.into(),
object_id: 0,
old_path: None,
timestamp: 0,
txg: 0,
size: None,
pid: None,
}
}
pub fn with_object_id(mut self, id: u64) -> Self {
self.object_id = id;
self
}
pub fn with_old_path(mut self, path: &str) -> Self {
self.old_path = Some(path.into());
self
}
pub fn with_timestamp(mut self, ts: u64) -> Self {
self.timestamp = ts;
self
}
pub fn with_txg(mut self, txg: u64) -> Self {
self.txg = txg;
self
}
pub fn with_size(mut self, size: u64) -> Self {
self.size = Some(size);
self
}
pub fn with_pid(mut self, pid: u32) -> Self {
self.pid = Some(pid);
self
}
pub fn is_directory(&self) -> bool {
matches!(self.event_type, EventType::DirCreate | EventType::DirDelete)
}
pub fn is_rename(&self) -> bool {
self.event_type == EventType::Rename
}
pub fn filename(&self) -> &str {
self.path.rsplit('/').next().unwrap_or(&self.path)
}
pub fn parent(&self) -> &str {
if let Some(idx) = self.path.rfind('/') {
if idx == 0 { "/" } else { &self.path[..idx] }
} else {
"/"
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct WatchDescriptor(pub u64);
impl WatchDescriptor {
pub fn new(id: u64) -> Self {
WatchDescriptor(id)
}
pub fn id(&self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, Default)]
pub struct WatchOptions {
pub recursive: bool,
pub oneshot: bool,
pub no_follow: bool,
pub exclude_self: bool,
pub debounce_ms: u64,
}
impl WatchOptions {
pub fn recursive() -> Self {
Self {
recursive: true,
..Default::default()
}
}
pub fn oneshot() -> Self {
Self {
oneshot: true,
..Default::default()
}
}
pub fn with_debounce(mut self, ms: u64) -> Self {
self.debounce_ms = ms;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NotifyError {
WatchNotFound(u64),
PathNotFound(String),
TooManyWatches,
InvalidPath(String),
Internal(String),
QueueOverflow,
NoEvents,
Timeout,
}
impl NotifyError {
pub fn message(&self) -> String {
match self {
NotifyError::WatchNotFound(id) => alloc::format!("Watch {} not found", id),
NotifyError::PathNotFound(path) => alloc::format!("Path not found: {}", path),
NotifyError::TooManyWatches => "Too many watches".into(),
NotifyError::InvalidPath(path) => alloc::format!("Invalid path: {}", path),
NotifyError::Internal(msg) => alloc::format!("Internal error: {}", msg),
NotifyError::QueueOverflow => "Event queue overflow".into(),
NotifyError::NoEvents => "No events available".into(),
NotifyError::Timeout => "Timeout waiting for events".into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_event_type_mask() {
assert_eq!(EventType::Create.mask(), 0x00000001);
assert_eq!(EventType::Modify.mask(), 0x00000002);
assert_eq!(EventType::Delete.mask(), 0x00000004);
}
#[test]
fn test_event_type_round_trip() {
for event in EventType::all() {
let s = event.as_str();
let parsed = EventType::from_str(s).unwrap();
assert_eq!(*event, parsed);
}
}
#[test]
fn test_event_mask_from_events() {
let mask = EventMask::from_events(&[EventType::Create, EventType::Delete]);
assert!(mask.contains(EventType::Create));
assert!(mask.contains(EventType::Delete));
assert!(!mask.contains(EventType::Modify));
}
#[test]
fn test_event_mask_add_remove() {
let mut mask = EventMask::NONE;
assert!(mask.is_empty());
mask.add(EventType::Create);
assert!(mask.contains(EventType::Create));
mask.add(EventType::Delete);
assert!(mask.contains(EventType::Delete));
mask.remove(EventType::Create);
assert!(!mask.contains(EventType::Create));
assert!(mask.contains(EventType::Delete));
}
#[test]
fn test_event_mask_bitwise() {
let mask1 = EventMask::from_events(&[EventType::Create]);
let mask2 = EventMask::from_events(&[EventType::Delete]);
let combined = mask1 | mask2;
assert!(combined.contains(EventType::Create));
assert!(combined.contains(EventType::Delete));
let intersect = combined & mask1;
assert!(intersect.contains(EventType::Create));
assert!(!intersect.contains(EventType::Delete));
}
#[test]
fn test_fs_event_builder() {
let event = FsEvent::new(EventType::Create, "tank/data", "/path/to/file.txt")
.with_object_id(12345)
.with_timestamp(1000000)
.with_txg(100)
.with_size(1024)
.with_pid(42);
assert_eq!(event.event_type, EventType::Create);
assert_eq!(event.dataset, "tank/data");
assert_eq!(event.path, "/path/to/file.txt");
assert_eq!(event.object_id, 12345);
assert_eq!(event.timestamp, 1000000);
assert_eq!(event.txg, 100);
assert_eq!(event.size, Some(1024));
assert_eq!(event.pid, Some(42));
}
#[test]
fn test_fs_event_rename() {
let event = FsEvent::new(EventType::Rename, "tank/data", "/new/path.txt")
.with_old_path("/old/path.txt");
assert!(event.is_rename());
assert_eq!(event.old_path, Some("/old/path.txt".into()));
}
#[test]
fn test_fs_event_filename_parent() {
let event = FsEvent::new(EventType::Create, "tank", "/path/to/file.txt");
assert_eq!(event.filename(), "file.txt");
assert_eq!(event.parent(), "/path/to");
let event2 = FsEvent::new(EventType::Create, "tank", "/file.txt");
assert_eq!(event2.filename(), "file.txt");
assert_eq!(event2.parent(), "/");
}
#[test]
fn test_watch_descriptor() {
let wd = WatchDescriptor::new(42);
assert_eq!(wd.id(), 42);
}
#[test]
fn test_watch_options_default() {
let opts = WatchOptions::default();
assert!(!opts.recursive);
assert!(!opts.oneshot);
assert_eq!(opts.debounce_ms, 0);
}
#[test]
fn test_watch_options_recursive() {
let opts = WatchOptions::recursive();
assert!(opts.recursive);
}
#[test]
fn test_notify_error_message() {
let err = NotifyError::WatchNotFound(42);
assert!(err.message().contains("42"));
let err = NotifyError::PathNotFound("/missing".into());
assert!(err.message().contains("/missing"));
}
}