Skip to main content

uqa_storage/native_file/
path_watch.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! A one-shot invalidation gate for a canonical local filesystem pathname.
8
9use std::{io, path::Path};
10
11#[cfg(target_os = "linux")]
12#[path = "path_watch/linux.rs"]
13mod platform;
14#[cfg(target_os = "macos")]
15#[path = "path_watch/macos.rs"]
16mod platform;
17
18/// Watches the containing directory, every ancestor's own identity, and mount
19/// changes. Construct before validating the pathname. After any event or error,
20/// validate again with a new watch; this watch stays invalidated. Unsupported
21/// filesystems and platforms require ordinary pathname validation on every call.
22pub struct PathChangeWatch {
23    #[cfg(any(target_os = "linux", target_os = "macos"))]
24    watch: platform::Watch,
25    process: u32,
26    changed: bool,
27}
28
29impl PathChangeWatch {
30    pub fn new(path: &Path) -> io::Result<Self> {
31        #[cfg(any(target_os = "linux", target_os = "macos"))]
32        {
33            if path.components().any(|component| {
34                matches!(
35                    component,
36                    std::path::Component::CurDir | std::path::Component::ParentDir
37                )
38            }) {
39                return Err(unsupported());
40            }
41            let parent = path
42                .parent()
43                .filter(|_| path.is_absolute())
44                .ok_or_else(unsupported)?;
45            let watch = platform::Watch::new(parent)?;
46            // A replaced leaf may be a symlink to the original inode outside
47            // this directory tree. Its target can change without a watched
48            // directory event. Check after subscribing so a concurrent leaf
49            // replacement still invalidates the watch.
50            if path.symlink_metadata()?.file_type().is_symlink() {
51                return Err(unsupported());
52            }
53            Ok(Self {
54                watch,
55                process: std::process::id(),
56                changed: false,
57            })
58        }
59        #[cfg(not(any(target_os = "linux", target_os = "macos")))]
60        {
61            let _ = path;
62            Err(unsupported())
63        }
64    }
65
66    /// Whether the pathname must be revalidated. This never waits for an event.
67    pub fn changed(&mut self) -> io::Result<bool> {
68        if self.changed || self.process != std::process::id() {
69            return Ok(true);
70        }
71        // A failed poll cannot authorize any subsequent reuse of this watch.
72        self.changed = true;
73        #[cfg(any(target_os = "linux", target_os = "macos"))]
74        {
75            self.changed = self.watch.changed()?;
76        }
77        Ok(self.changed)
78    }
79}
80
81fn unsupported() -> io::Error {
82    io::Error::new(
83        io::ErrorKind::Unsupported,
84        "pathname requires direct identity checks",
85    )
86}
87
88#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
89mod tests;