⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/fs/
inotify.rs

1//! inotify -- File Event Monitoring
2//!
3//! Provides inotify-style file event monitoring for the VFS layer.
4//! Watches can be placed on files and directories to receive notifications
5//! when filesystem events occur (create, delete, modify, move, etc.).
6//!
7//! Key features:
8//! - Watch descriptor management (`inotify_init`, `inotify_add_watch`,
9//!   `inotify_rm_watch`)
10//! - Bounded per-instance event queues with configurable max (default 4096)
11//! - Event coalescing: identical consecutive events are deduplicated
12//! - Recursive watch support via `IN_RECURSIVE` flag (custom extension)
13//! - Thread-safe via atomic operations and spin::RwLock
14
15#![allow(dead_code)]
16
17use alloc::{
18    collections::{BTreeMap, VecDeque},
19    string::String,
20    vec::Vec,
21};
22use core::sync::atomic::{AtomicI32, AtomicU64, Ordering};
23
24#[cfg(not(target_arch = "aarch64"))]
25use spin::RwLock;
26
27#[cfg(target_arch = "aarch64")]
28use super::bare_lock::RwLock;
29use crate::error::KernelError;
30
31// ---------------------------------------------------------------------------
32// Event type constants (Linux-compatible values)
33// ---------------------------------------------------------------------------
34
35/// File was accessed (read).
36pub const IN_ACCESS: u32 = 0x0000_0001;
37
38/// File was modified (write).
39pub const IN_MODIFY: u32 = 0x0000_0002;
40
41/// File attributes changed (chmod, chown, etc.).
42pub const IN_ATTRIB: u32 = 0x0000_0004;
43
44/// File opened for writing was closed.
45pub const IN_CLOSE_WRITE: u32 = 0x0000_0008;
46
47/// File not opened for writing was closed.
48pub const IN_CLOSE_NOWRITE: u32 = 0x0000_0010;
49
50/// File was opened.
51pub const IN_OPEN: u32 = 0x0000_0020;
52
53/// File/directory moved out of watched directory.
54pub const IN_MOVED_FROM: u32 = 0x0000_0040;
55
56/// File/directory moved into watched directory.
57pub const IN_MOVED_TO: u32 = 0x0000_0080;
58
59/// File/directory created in watched directory.
60pub const IN_CREATE: u32 = 0x0000_0100;
61
62/// File/directory deleted from watched directory.
63pub const IN_DELETE: u32 = 0x0000_0200;
64
65/// Watched file/directory was itself deleted.
66pub const IN_DELETE_SELF: u32 = 0x0000_0400;
67
68/// Watched file/directory was itself moved.
69pub const IN_MOVE_SELF: u32 = 0x0000_0800;
70
71/// Combination of all standard event types.
72pub const IN_ALL_EVENTS: u32 = IN_ACCESS
73    | IN_MODIFY
74    | IN_ATTRIB
75    | IN_CLOSE_WRITE
76    | IN_CLOSE_NOWRITE
77    | IN_OPEN
78    | IN_MOVED_FROM
79    | IN_MOVED_TO
80    | IN_CREATE
81    | IN_DELETE
82    | IN_DELETE_SELF
83    | IN_MOVE_SELF;
84
85/// Watch subdirectories recursively (VeridianOS extension, not in Linux
86/// inotify).
87pub const IN_RECURSIVE: u32 = 0x0100_0000;
88
89/// Event occurred against a directory (set in returned events, not in watch
90/// mask).
91pub const IN_ISDIR: u32 = 0x4000_0000;
92
93// ---------------------------------------------------------------------------
94// Default configuration
95// ---------------------------------------------------------------------------
96
97/// Default maximum number of events per inotify instance.
98const DEFAULT_MAX_EVENTS: usize = 4096;
99
100/// Default maximum number of watches per inotify instance.
101const DEFAULT_MAX_WATCHES: usize = 8192;
102
103// ---------------------------------------------------------------------------
104// Global state
105// ---------------------------------------------------------------------------
106
107/// Global counter for generating unique instance IDs.
108static NEXT_INSTANCE_ID: AtomicU64 = AtomicU64::new(1);
109
110/// Global counter for generating unique watch descriptors within an instance.
111static NEXT_WD: AtomicI32 = AtomicI32::new(1);
112
113/// Global cookie counter for pairing MOVED_FROM/MOVED_TO events.
114static NEXT_COOKIE: AtomicU64 = AtomicU64::new(1);
115
116/// Global registry of all inotify instances, keyed by instance ID.
117static INOTIFY_INSTANCES: RwLock<BTreeMap<u64, InotifyInstance>> = RwLock::new(BTreeMap::new());
118
119/// Reverse mapping: inode -> list of (instance_id, wd) watching that inode.
120/// Used by `notify_event()` to efficiently find watches for a given inode.
121static INODE_WATCHES: RwLock<BTreeMap<u64, Vec<(u64, i32)>>> = RwLock::new(BTreeMap::new());
122
123// ---------------------------------------------------------------------------
124// Data structures
125// ---------------------------------------------------------------------------
126
127/// An inotify event delivered to userspace.
128#[derive(Debug, Clone)]
129pub struct InotifyEvent {
130    /// Watch descriptor that triggered this event.
131    pub wd: i32,
132    /// Bitmask of event types (IN_CREATE, IN_MODIFY, etc.).
133    pub mask: u32,
134    /// Cookie for pairing MOVED_FROM / MOVED_TO events (0 if not a move).
135    pub cookie: u32,
136    /// Optional filename associated with the event (for directory watches).
137    pub name: Option<String>,
138}
139
140impl InotifyEvent {
141    /// Check if two events are identical for coalescing purposes.
142    /// Two events are coalesceable if they have the same wd, mask, cookie,
143    /// and name.
144    fn is_coalesceable_with(&self, other: &InotifyEvent) -> bool {
145        self.wd == other.wd
146            && self.mask == other.mask
147            && self.cookie == other.cookie
148            && self.name == other.name
149    }
150}
151
152/// A watch descriptor tracking a single watched path/inode.
153#[derive(Debug, Clone)]
154pub struct WatchDescriptor {
155    /// Unique watch descriptor ID (returned to userspace).
156    pub wd: i32,
157    /// Inode number of the watched file/directory.
158    pub inode: u64,
159    /// Bitmask of event types to watch for.
160    pub mask: u32,
161    /// Whether to recursively watch subdirectories.
162    pub recursive: bool,
163    /// Original path being watched (for debugging/display).
164    pub path: String,
165}
166
167/// An inotify instance, representing one open inotify file descriptor.
168pub struct InotifyInstance {
169    /// Unique instance ID.
170    id: u64,
171    /// Active watches, keyed by watch descriptor.
172    watches: BTreeMap<i32, WatchDescriptor>,
173    /// Pending events queue (bounded).
174    events: VecDeque<InotifyEvent>,
175    /// Maximum number of events in the queue before oldest are dropped.
176    max_events: usize,
177    /// Maximum number of watches allowed.
178    max_watches: usize,
179}
180
181impl InotifyInstance {
182    /// Create a new inotify instance with default limits.
183    fn new(id: u64) -> Self {
184        Self {
185            id,
186            watches: BTreeMap::new(),
187            events: VecDeque::new(),
188            max_events: DEFAULT_MAX_EVENTS,
189            max_watches: DEFAULT_MAX_WATCHES,
190        }
191    }
192
193    /// Create a new inotify instance with custom limits.
194    fn with_limits(id: u64, max_events: usize, max_watches: usize) -> Self {
195        Self {
196            id,
197            watches: BTreeMap::new(),
198            events: VecDeque::new(),
199            max_events,
200            max_watches,
201        }
202    }
203
204    /// Push an event into this instance's queue.
205    /// If the queue is full, the oldest event is discarded.
206    /// Performs event coalescing: if the new event is identical to the
207    /// most recently queued event, it is silently dropped.
208    fn push_event(&mut self, event: InotifyEvent) {
209        // Coalesce: skip if identical to the last queued event
210        if let Some(last) = self.events.back() {
211            if last.is_coalesceable_with(&event) {
212                return;
213            }
214        }
215
216        // Enforce queue limit by dropping oldest
217        while self.events.len() >= self.max_events {
218            self.events.pop_front();
219        }
220
221        self.events.push_back(event);
222    }
223
224    /// Read and drain up to `max_count` events from the queue.
225    /// Returns an empty Vec if no events are pending.
226    fn read_events(&mut self, max_count: usize) -> Vec<InotifyEvent> {
227        let count = max_count.min(self.events.len());
228        let mut result = Vec::with_capacity(count);
229        for _ in 0..count {
230            if let Some(event) = self.events.pop_front() {
231                result.push(event);
232            }
233        }
234        result
235    }
236
237    /// Return the number of pending events.
238    fn pending_count(&self) -> usize {
239        self.events.len()
240    }
241
242    /// Check whether any events are pending.
243    fn has_events(&self) -> bool {
244        !self.events.is_empty()
245    }
246}
247
248// ---------------------------------------------------------------------------
249// Public API
250// ---------------------------------------------------------------------------
251
252/// Initialize a new inotify instance.
253///
254/// Returns the instance ID (analogous to the fd returned by inotify_init(2)).
255pub fn inotify_init() -> Result<u64, KernelError> {
256    let id = NEXT_INSTANCE_ID.fetch_add(1, Ordering::Relaxed);
257    let instance = InotifyInstance::new(id);
258
259    let mut instances = INOTIFY_INSTANCES.write();
260    instances.insert(id, instance);
261
262    Ok(id)
263}
264
265/// Initialize a new inotify instance with custom limits.
266///
267/// Returns the instance ID.
268pub fn inotify_init_with_limits(max_events: usize, max_watches: usize) -> Result<u64, KernelError> {
269    if max_events == 0 {
270        return Err(KernelError::InvalidArgument {
271            name: "max_events",
272            value: "must be > 0",
273        });
274    }
275    if max_watches == 0 {
276        return Err(KernelError::InvalidArgument {
277            name: "max_watches",
278            value: "must be > 0",
279        });
280    }
281
282    let id = NEXT_INSTANCE_ID.fetch_add(1, Ordering::Relaxed);
283    let instance = InotifyInstance::with_limits(id, max_events, max_watches);
284
285    let mut instances = INOTIFY_INSTANCES.write();
286    instances.insert(id, instance);
287
288    Ok(id)
289}
290
291/// Add a watch to an inotify instance.
292///
293/// Watches the inode at the given path for the specified event types.
294/// If the inode is already watched by this instance, the watch mask is updated.
295///
296/// # Arguments
297/// * `instance_id` - The inotify instance (from `inotify_init`)
298/// * `inode` - Inode number to watch
299/// * `path` - Path being watched (stored for debugging)
300/// * `mask` - Bitmask of event types to watch (IN_CREATE, IN_MODIFY, etc.)
301///
302/// Returns the watch descriptor (wd).
303pub fn inotify_add_watch(
304    instance_id: u64,
305    inode: u64,
306    path: &str,
307    mask: u32,
308) -> Result<i32, KernelError> {
309    let event_mask = mask & IN_ALL_EVENTS;
310    if event_mask == 0 {
311        return Err(KernelError::InvalidArgument {
312            name: "mask",
313            value: "no valid event types specified",
314        });
315    }
316
317    let recursive = mask & IN_RECURSIVE != 0;
318
319    let mut instances = INOTIFY_INSTANCES.write();
320    let instance = instances
321        .get_mut(&instance_id)
322        .ok_or(KernelError::NotFound {
323            resource: "inotify instance",
324            id: instance_id,
325        })?;
326
327    // Check if this inode is already watched by this instance
328    for watch in instance.watches.values_mut() {
329        if watch.inode == inode {
330            // Update existing watch mask
331            watch.mask = event_mask;
332            watch.recursive = recursive;
333            return Ok(watch.wd);
334        }
335    }
336
337    // Check watch limit
338    if instance.watches.len() >= instance.max_watches {
339        return Err(KernelError::ResourceExhausted {
340            resource: "inotify watches",
341        });
342    }
343
344    // Create new watch
345    let wd = NEXT_WD.fetch_add(1, Ordering::Relaxed);
346    let watch = WatchDescriptor {
347        wd,
348        inode,
349        mask: event_mask,
350        recursive,
351        path: String::from(path),
352    };
353
354    instance.watches.insert(wd, watch);
355
356    // Update reverse mapping
357    let mut inode_watches = INODE_WATCHES.write();
358    inode_watches
359        .entry(inode)
360        .or_default()
361        .push((instance_id, wd));
362
363    Ok(wd)
364}
365
366/// Remove a watch from an inotify instance.
367///
368/// # Arguments
369/// * `instance_id` - The inotify instance
370/// * `wd` - The watch descriptor to remove (from `inotify_add_watch`)
371pub fn inotify_rm_watch(instance_id: u64, wd: i32) -> Result<(), KernelError> {
372    let mut instances = INOTIFY_INSTANCES.write();
373    let instance = instances
374        .get_mut(&instance_id)
375        .ok_or(KernelError::NotFound {
376            resource: "inotify instance",
377            id: instance_id,
378        })?;
379
380    let watch = instance.watches.remove(&wd).ok_or(KernelError::NotFound {
381        resource: "inotify watch",
382        id: wd as u64,
383    })?;
384
385    // Remove from reverse mapping
386    let mut inode_watches = INODE_WATCHES.write();
387    if let Some(watchers) = inode_watches.get_mut(&watch.inode) {
388        watchers.retain(|&(iid, w)| !(iid == instance_id && w == wd));
389        if watchers.is_empty() {
390            inode_watches.remove(&watch.inode);
391        }
392    }
393
394    Ok(())
395}
396
397/// Destroy an inotify instance, removing all its watches.
398///
399/// # Arguments
400/// * `instance_id` - The inotify instance to destroy
401pub fn inotify_close(instance_id: u64) -> Result<(), KernelError> {
402    let mut instances = INOTIFY_INSTANCES.write();
403    let instance = instances
404        .remove(&instance_id)
405        .ok_or(KernelError::NotFound {
406            resource: "inotify instance",
407            id: instance_id,
408        })?;
409
410    // Clean up all reverse mappings for this instance's watches
411    let mut inode_watches = INODE_WATCHES.write();
412    for watch in instance.watches.values() {
413        if let Some(watchers) = inode_watches.get_mut(&watch.inode) {
414            watchers.retain(|&(iid, _)| iid != instance_id);
415            if watchers.is_empty() {
416                inode_watches.remove(&watch.inode);
417            }
418        }
419    }
420
421    Ok(())
422}
423
424/// Read pending events from an inotify instance.
425///
426/// Returns up to `max_count` events, removing them from the queue.
427/// Returns an empty Vec if no events are pending.
428pub fn inotify_read(instance_id: u64, max_count: usize) -> Result<Vec<InotifyEvent>, KernelError> {
429    let mut instances = INOTIFY_INSTANCES.write();
430    let instance = instances
431        .get_mut(&instance_id)
432        .ok_or(KernelError::NotFound {
433            resource: "inotify instance",
434            id: instance_id,
435        })?;
436
437    Ok(instance.read_events(max_count))
438}
439
440/// Check how many events are pending for an inotify instance.
441pub fn inotify_pending(instance_id: u64) -> Result<usize, KernelError> {
442    let instances = INOTIFY_INSTANCES.read();
443    let instance = instances.get(&instance_id).ok_or(KernelError::NotFound {
444        resource: "inotify instance",
445        id: instance_id,
446    })?;
447
448    Ok(instance.pending_count())
449}
450
451/// Generate a new cookie for pairing MOVED_FROM / MOVED_TO events.
452pub fn generate_move_cookie() -> u32 {
453    // Truncate to u32; wrapping is fine for cookies
454    NEXT_COOKIE.fetch_add(1, Ordering::Relaxed) as u32
455}
456
457// ---------------------------------------------------------------------------
458// VFS integration: event notification
459// ---------------------------------------------------------------------------
460
461/// Notify all watchers of a filesystem event on the given inode.
462///
463/// This is the primary integration point: VFS operations (create, delete,
464/// write, rename, etc.) call this function to push events to any inotify
465/// instances watching the affected inode.
466///
467/// # Arguments
468/// * `inode` - The inode where the event occurred
469/// * `mask` - Event type bitmask (IN_CREATE, IN_MODIFY, etc.)
470/// * `cookie` - Cookie for pairing move events (0 for non-move events)
471/// * `name` - Optional filename (for events in a watched directory)
472pub fn notify_event(inode: u64, mask: u32, cookie: u32, name: Option<&str>) {
473    // Clone watchers inside a scope so the read lock is released before
474    // acquiring the write lock on INOTIFY_INSTANCES.
475    let watchers = {
476        let inode_watches = INODE_WATCHES.read();
477        match inode_watches.get(&inode) {
478            Some(w) => w.clone(),
479            None => return, // No watches on this inode
480        }
481    };
482
483    let mut instances = INOTIFY_INSTANCES.write();
484
485    for (instance_id, wd) in &watchers {
486        if let Some(instance) = instances.get_mut(instance_id) {
487            // Check if this watch cares about this event type
488            if let Some(watch) = instance.watches.get(wd) {
489                if watch.mask & mask != 0 {
490                    let event = InotifyEvent {
491                        wd: *wd,
492                        mask,
493                        cookie,
494                        name: name.map(String::from),
495                    };
496                    instance.push_event(event);
497                }
498            }
499        }
500    }
501}
502
503/// Convenience wrapper: notify a directory watch about an event on a child.
504///
505/// Sets the IN_ISDIR flag if `is_dir` is true.
506pub fn notify_child_event(
507    parent_inode: u64,
508    mask: u32,
509    cookie: u32,
510    child_name: &str,
511    is_dir: bool,
512) {
513    let effective_mask = if is_dir { mask | IN_ISDIR } else { mask };
514    notify_event(parent_inode, effective_mask, cookie, Some(child_name));
515}
516
517// ---------------------------------------------------------------------------
518// Statistics / introspection
519// ---------------------------------------------------------------------------
520
521/// Statistics about the inotify subsystem.
522#[derive(Debug, Clone, Copy)]
523pub struct InotifyStats {
524    /// Total number of active inotify instances.
525    pub instance_count: usize,
526    /// Total number of active watches across all instances.
527    pub total_watches: usize,
528    /// Total number of pending events across all instances.
529    pub total_pending_events: usize,
530}
531
532/// Get current inotify subsystem statistics.
533pub fn get_stats() -> InotifyStats {
534    let instances = INOTIFY_INSTANCES.read();
535    let mut total_watches = 0;
536    let mut total_pending = 0;
537
538    for instance in instances.values() {
539        total_watches += instance.watches.len();
540        total_pending += instance.events.len();
541    }
542
543    InotifyStats {
544        instance_count: instances.len(),
545        total_watches,
546        total_pending_events: total_pending,
547    }
548}
549
550// ---------------------------------------------------------------------------
551// Tests
552// ---------------------------------------------------------------------------
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[test]
559    fn test_inotify_init() {
560        let id = inotify_init().unwrap();
561        assert!(id > 0);
562
563        // Clean up
564        inotify_close(id).unwrap();
565    }
566
567    #[test]
568    fn test_inotify_init_with_limits() {
569        let id = inotify_init_with_limits(100, 50).unwrap();
570        assert!(id > 0);
571        inotify_close(id).unwrap();
572    }
573
574    #[test]
575    fn test_inotify_init_zero_events_rejected() {
576        let result = inotify_init_with_limits(0, 50);
577        assert!(result.is_err());
578    }
579
580    #[test]
581    fn test_inotify_init_zero_watches_rejected() {
582        let result = inotify_init_with_limits(100, 0);
583        assert!(result.is_err());
584    }
585
586    #[test]
587    fn test_add_and_remove_watch() {
588        let id = inotify_init().unwrap();
589
590        let wd = inotify_add_watch(id, 42, "/tmp/test", IN_MODIFY | IN_CREATE).unwrap();
591        assert!(wd > 0);
592
593        inotify_rm_watch(id, wd).unwrap();
594        inotify_close(id).unwrap();
595    }
596
597    #[test]
598    fn test_add_watch_invalid_mask() {
599        let id = inotify_init().unwrap();
600
601        // Mask with no valid event bits
602        let result = inotify_add_watch(id, 42, "/tmp/test", 0);
603        assert!(result.is_err());
604
605        // Only IN_RECURSIVE set, no event types
606        let result = inotify_add_watch(id, 42, "/tmp/test", IN_RECURSIVE);
607        assert!(result.is_err());
608
609        inotify_close(id).unwrap();
610    }
611
612    #[test]
613    fn test_add_watch_updates_existing() {
614        let id = inotify_init().unwrap();
615
616        let wd1 = inotify_add_watch(id, 42, "/tmp/test", IN_MODIFY).unwrap();
617        let wd2 = inotify_add_watch(id, 42, "/tmp/test", IN_CREATE).unwrap();
618
619        // Same inode should reuse the same wd
620        assert_eq!(wd1, wd2);
621
622        inotify_close(id).unwrap();
623    }
624
625    #[test]
626    fn test_rm_watch_invalid_instance() {
627        let result = inotify_rm_watch(999_999, 1);
628        assert!(result.is_err());
629    }
630
631    #[test]
632    fn test_rm_watch_invalid_wd() {
633        let id = inotify_init().unwrap();
634        let result = inotify_rm_watch(id, 999);
635        assert!(result.is_err());
636        inotify_close(id).unwrap();
637    }
638
639    #[test]
640    fn test_close_invalid_instance() {
641        let result = inotify_close(999_999);
642        assert!(result.is_err());
643    }
644
645    #[test]
646    fn test_notify_event_delivery() {
647        let id = inotify_init().unwrap();
648        let inode = 100;
649        let wd = inotify_add_watch(id, inode, "/tmp/watched", IN_MODIFY | IN_CREATE).unwrap();
650
651        // Fire a MODIFY event
652        notify_event(inode, IN_MODIFY, 0, None);
653
654        let events = inotify_read(id, 10).unwrap();
655        assert_eq!(events.len(), 1);
656        assert_eq!(events[0].wd, wd);
657        assert_eq!(events[0].mask, IN_MODIFY);
658        assert_eq!(events[0].cookie, 0);
659        assert!(events[0].name.is_none());
660
661        inotify_close(id).unwrap();
662    }
663
664    #[test]
665    fn test_notify_event_with_name() {
666        let id = inotify_init().unwrap();
667        let inode = 101;
668        let wd = inotify_add_watch(id, inode, "/tmp/dir", IN_CREATE).unwrap();
669
670        notify_child_event(inode, IN_CREATE, 0, "newfile.txt", false);
671
672        let events = inotify_read(id, 10).unwrap();
673        assert_eq!(events.len(), 1);
674        assert_eq!(events[0].wd, wd);
675        assert_eq!(events[0].mask, IN_CREATE);
676        assert_eq!(events[0].name.as_deref(), Some("newfile.txt"));
677
678        inotify_close(id).unwrap();
679    }
680
681    #[test]
682    fn test_notify_isdir_flag() {
683        let id = inotify_init().unwrap();
684        let inode = 102;
685        inotify_add_watch(id, inode, "/tmp/dir", IN_CREATE).unwrap();
686
687        notify_child_event(inode, IN_CREATE, 0, "subdir", true);
688
689        let events = inotify_read(id, 10).unwrap();
690        assert_eq!(events.len(), 1);
691        assert_eq!(events[0].mask, IN_CREATE | IN_ISDIR);
692
693        inotify_close(id).unwrap();
694    }
695
696    #[test]
697    fn test_event_filtering_by_mask() {
698        let id = inotify_init().unwrap();
699        let inode = 103;
700        // Only watch for MODIFY, not CREATE
701        inotify_add_watch(id, inode, "/tmp/filtered", IN_MODIFY).unwrap();
702
703        // Fire a CREATE event -- should be filtered out
704        notify_event(inode, IN_CREATE, 0, Some("ignored.txt"));
705
706        // Fire a MODIFY event -- should be delivered
707        notify_event(inode, IN_MODIFY, 0, None);
708
709        let events = inotify_read(id, 10).unwrap();
710        assert_eq!(events.len(), 1);
711        assert_eq!(events[0].mask, IN_MODIFY);
712
713        inotify_close(id).unwrap();
714    }
715
716    #[test]
717    fn test_event_coalescing() {
718        let id = inotify_init().unwrap();
719        let inode = 104;
720        inotify_add_watch(id, inode, "/tmp/coalesce", IN_MODIFY).unwrap();
721
722        // Fire three identical MODIFY events
723        notify_event(inode, IN_MODIFY, 0, None);
724        notify_event(inode, IN_MODIFY, 0, None);
725        notify_event(inode, IN_MODIFY, 0, None);
726
727        // Should be coalesced to 1
728        let events = inotify_read(id, 10).unwrap();
729        assert_eq!(events.len(), 1);
730
731        inotify_close(id).unwrap();
732    }
733
734    #[test]
735    fn test_no_coalescing_different_events() {
736        let id = inotify_init().unwrap();
737        let inode = 105;
738        inotify_add_watch(id, inode, "/tmp/nocoalesce", IN_ALL_EVENTS).unwrap();
739
740        notify_event(inode, IN_MODIFY, 0, None);
741        notify_event(inode, IN_CREATE, 0, Some("a.txt"));
742        notify_event(inode, IN_MODIFY, 0, None);
743
744        // All 3 should remain (MODIFY, CREATE, MODIFY are not consecutive-identical)
745        let events = inotify_read(id, 10).unwrap();
746        assert_eq!(events.len(), 3);
747
748        inotify_close(id).unwrap();
749    }
750
751    #[test]
752    fn test_event_queue_overflow() {
753        let id = inotify_init_with_limits(5, 100).unwrap();
754        let inode = 106;
755        inotify_add_watch(id, inode, "/tmp/overflow", IN_MODIFY).unwrap();
756
757        // Fire 10 events with different names to avoid coalescing
758        for i in 0..10 {
759            let name = if i % 2 == 0 { Some("a") } else { Some("b") };
760            notify_event(inode, IN_MODIFY, 0, name);
761        }
762
763        // Queue limit is 5, so only the last 5 should remain
764        let events = inotify_read(id, 20).unwrap();
765        assert_eq!(events.len(), 5);
766
767        inotify_close(id).unwrap();
768    }
769
770    #[test]
771    fn test_read_events_empty() {
772        let id = inotify_init().unwrap();
773
774        let events = inotify_read(id, 10).unwrap();
775        assert!(events.is_empty());
776
777        inotify_close(id).unwrap();
778    }
779
780    #[test]
781    fn test_read_events_partial() {
782        let id = inotify_init().unwrap();
783        let inode = 107;
784        inotify_add_watch(id, inode, "/tmp/partial", IN_ALL_EVENTS).unwrap();
785
786        notify_event(inode, IN_CREATE, 0, Some("a.txt"));
787        notify_event(inode, IN_MODIFY, 0, Some("a.txt"));
788        notify_event(inode, IN_DELETE, 0, Some("a.txt"));
789
790        // Read only 2 of 3
791        let events = inotify_read(id, 2).unwrap();
792        assert_eq!(events.len(), 2);
793        assert_eq!(events[0].mask, IN_CREATE);
794        assert_eq!(events[1].mask, IN_MODIFY);
795
796        // Remaining 1
797        let events = inotify_read(id, 10).unwrap();
798        assert_eq!(events.len(), 1);
799        assert_eq!(events[0].mask, IN_DELETE);
800
801        inotify_close(id).unwrap();
802    }
803
804    #[test]
805    fn test_inotify_pending() {
806        let id = inotify_init().unwrap();
807        let inode = 108;
808        inotify_add_watch(id, inode, "/tmp/pending", IN_MODIFY).unwrap();
809
810        assert_eq!(inotify_pending(id).unwrap(), 0);
811
812        notify_event(inode, IN_MODIFY, 0, Some("x"));
813        assert_eq!(inotify_pending(id).unwrap(), 1);
814
815        notify_event(inode, IN_MODIFY, 0, Some("y"));
816        assert_eq!(inotify_pending(id).unwrap(), 2);
817
818        inotify_close(id).unwrap();
819    }
820
821    #[test]
822    fn test_notify_unwatched_inode() {
823        // Should not panic or error; just a no-op
824        notify_event(999_999, IN_MODIFY, 0, None);
825    }
826
827    #[test]
828    fn test_move_cookie() {
829        let id = inotify_init().unwrap();
830        let src_inode = 200;
831        let dst_inode = 201;
832        inotify_add_watch(id, src_inode, "/tmp/src", IN_MOVED_FROM).unwrap();
833        inotify_add_watch(id, dst_inode, "/tmp/dst", IN_MOVED_TO).unwrap();
834
835        let cookie = generate_move_cookie();
836        notify_event(src_inode, IN_MOVED_FROM, cookie, Some("file.txt"));
837        notify_event(dst_inode, IN_MOVED_TO, cookie, Some("file.txt"));
838
839        let events = inotify_read(id, 10).unwrap();
840        assert_eq!(events.len(), 2);
841        assert_eq!(events[0].mask, IN_MOVED_FROM);
842        assert_eq!(events[1].mask, IN_MOVED_TO);
843        // Same cookie pairs the events
844        assert_eq!(events[0].cookie, events[1].cookie);
845        assert!(events[0].cookie > 0);
846
847        inotify_close(id).unwrap();
848    }
849
850    #[test]
851    fn test_recursive_watch_flag() {
852        let id = inotify_init().unwrap();
853
854        let wd = inotify_add_watch(id, 300, "/tmp/recursive", IN_CREATE | IN_RECURSIVE).unwrap();
855
856        // Verify the watch was created with recursive flag
857        {
858            let instances = INOTIFY_INSTANCES.read();
859            let instance = instances.get(&id).unwrap();
860            let watch = instance.watches.get(&wd).unwrap();
861            assert!(watch.recursive);
862        }
863
864        inotify_close(id).unwrap();
865    }
866
867    #[test]
868    fn test_multiple_instances_same_inode() {
869        let id1 = inotify_init().unwrap();
870        let id2 = inotify_init().unwrap();
871        let inode = 400;
872
873        inotify_add_watch(id1, inode, "/tmp/multi", IN_MODIFY).unwrap();
874        inotify_add_watch(id2, inode, "/tmp/multi", IN_CREATE).unwrap();
875
876        // MODIFY event should reach id1 but not id2
877        notify_event(inode, IN_MODIFY, 0, None);
878        assert_eq!(inotify_pending(id1).unwrap(), 1);
879        assert_eq!(inotify_pending(id2).unwrap(), 0);
880
881        // CREATE event should reach id2 but not id1
882        notify_event(inode, IN_CREATE, 0, Some("new.txt"));
883        assert_eq!(inotify_pending(id1).unwrap(), 1); // still 1, no new
884        assert_eq!(inotify_pending(id2).unwrap(), 1);
885
886        inotify_close(id1).unwrap();
887        inotify_close(id2).unwrap();
888    }
889
890    #[test]
891    fn test_close_cleans_up_inode_watches() {
892        let id = inotify_init().unwrap();
893        let inode = 500;
894        inotify_add_watch(id, inode, "/tmp/cleanup", IN_MODIFY).unwrap();
895
896        inotify_close(id).unwrap();
897
898        // After close, events on that inode should not deliver anywhere
899        // (should not panic)
900        notify_event(inode, IN_MODIFY, 0, None);
901    }
902
903    #[test]
904    fn test_rm_watch_cleans_up_inode_mapping() {
905        let id = inotify_init().unwrap();
906        let inode = 501;
907        let wd = inotify_add_watch(id, inode, "/tmp/rmclean", IN_MODIFY).unwrap();
908
909        inotify_rm_watch(id, wd).unwrap();
910
911        // Event should not be delivered after watch removal
912        notify_event(inode, IN_MODIFY, 0, None);
913        assert_eq!(inotify_pending(id).unwrap(), 0);
914
915        inotify_close(id).unwrap();
916    }
917
918    #[test]
919    fn test_watch_limit_enforcement() {
920        let id = inotify_init_with_limits(1000, 3).unwrap();
921
922        inotify_add_watch(id, 601, "/a", IN_MODIFY).unwrap();
923        inotify_add_watch(id, 602, "/b", IN_MODIFY).unwrap();
924        inotify_add_watch(id, 603, "/c", IN_MODIFY).unwrap();
925
926        // 4th watch should fail
927        let result = inotify_add_watch(id, 604, "/d", IN_MODIFY);
928        assert!(result.is_err());
929
930        inotify_close(id).unwrap();
931    }
932
933    #[test]
934    fn test_get_stats() {
935        let id = inotify_init().unwrap();
936        let inode = 700;
937        inotify_add_watch(id, inode, "/tmp/stats", IN_MODIFY).unwrap();
938        notify_event(inode, IN_MODIFY, 0, None);
939
940        let stats = get_stats();
941        assert!(stats.instance_count >= 1);
942        assert!(stats.total_watches >= 1);
943        assert!(stats.total_pending_events >= 1);
944
945        inotify_close(id).unwrap();
946    }
947
948    #[test]
949    fn test_event_coalesceable() {
950        let e1 = InotifyEvent {
951            wd: 1,
952            mask: IN_MODIFY,
953            cookie: 0,
954            name: None,
955        };
956        let e2 = InotifyEvent {
957            wd: 1,
958            mask: IN_MODIFY,
959            cookie: 0,
960            name: None,
961        };
962        let e3 = InotifyEvent {
963            wd: 1,
964            mask: IN_CREATE,
965            cookie: 0,
966            name: None,
967        };
968        let e4 = InotifyEvent {
969            wd: 2,
970            mask: IN_MODIFY,
971            cookie: 0,
972            name: None,
973        };
974        let e5 = InotifyEvent {
975            wd: 1,
976            mask: IN_MODIFY,
977            cookie: 0,
978            name: Some(String::from("file.txt")),
979        };
980
981        assert!(e1.is_coalesceable_with(&e2));
982        assert!(!e1.is_coalesceable_with(&e3)); // different mask
983        assert!(!e1.is_coalesceable_with(&e4)); // different wd
984        assert!(!e1.is_coalesceable_with(&e5)); // different name
985    }
986}