use crate::{
RuntimeError,
modules::{fd::Fd, retried::retried},
};
use std::{
ffi::CString,
mem,
ops::{BitOr, BitOrAssign},
};
pub(super) const EVERY_NOTE: u32 = libc::NOTE_WRITE
| libc::NOTE_EXTEND
| libc::NOTE_ATTRIB
| libc::NOTE_LINK
| libc::NOTE_RENAME
| libc::NOTE_DELETE;
const CREATED_NOTE: u32 = 1 << 31;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Change(u32);
impl Change {
pub const WRITTEN: Self = Self(libc::NOTE_WRITE);
pub const EXTENDED: Self = Self(libc::NOTE_EXTEND);
pub const ATTRIBUTES: Self = Self(libc::NOTE_ATTRIB);
pub const LINKED: Self = Self(libc::NOTE_LINK);
pub const RENAMED: Self = Self(libc::NOTE_RENAME);
pub const REMOVED: Self = Self(libc::NOTE_DELETE);
pub const ANY: Self = Self(EVERY_NOTE);
pub const CREATED: Self = Self(CREATED_NOTE);
pub(super) const fn new(notes: u32) -> Self {
Self(notes)
}
pub(super) const fn notes(self) -> u32 {
self.0 & EVERY_NOTE
}
const fn is_nothing(self) -> bool {
self.0 == 0
}
pub fn written(self) -> bool {
self.holds(Self::WRITTEN)
}
pub fn extended(self) -> bool {
self.holds(Self::EXTENDED)
}
pub fn attributes(self) -> bool {
self.holds(Self::ATTRIBUTES)
}
pub fn linked(self) -> bool {
self.holds(Self::LINKED)
}
pub fn renamed(self) -> bool {
self.holds(Self::RENAMED)
}
pub fn removed(self) -> bool {
self.holds(Self::REMOVED)
}
pub fn created(self) -> bool {
self.holds(Self::CREATED)
}
fn holds(self, part: Self) -> bool {
self.0 & part.0 == part.0
}
}
impl BitOr for Change {
type Output = Self;
fn bitor(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
impl BitOrAssign for Change {
fn bitor_assign(&mut self, other: Self) {
self.0 |= other.0;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Snapshot {
ino: u64,
size: u64,
mtime: (i64, i64),
mode: u32,
owner: (u32, u32),
nlink: u64,
at_path: Option<u64>,
}
impl Snapshot {
pub(super) fn take(fd: &Fd, path: &CString) -> Result<Self, RuntimeError> {
let mut raw: libc::stat = unsafe { mem::zeroed() };
retried(|| unsafe { libc::fstat(fd.raw(), &mut raw) })?;
Ok(Self {
ino: raw.st_ino,
size: raw.st_size.max(0) as u64,
mtime: (raw.st_mtime, raw.st_mtime_nsec),
mode: raw.st_mode as u32,
owner: (raw.st_uid, raw.st_gid),
nlink: raw.st_nlink as u64,
at_path: at_path(path),
})
}
pub(super) fn against(&self, now: &Self, wanted: u32) -> Option<Change> {
let mut change = Change::new(0);
if now.nlink == 0 {
if self.nlink != 0 {
change |= Change::REMOVED;
}
} else {
if now.at_path != self.at_path {
change |= Change::RENAMED;
}
if now.nlink != self.nlink {
change |= Change::LINKED;
}
if now.size > self.size {
change |= Change::EXTENDED;
}
if now.mtime != self.mtime {
change |= Change::WRITTEN;
}
if now.mode != self.mode || now.owner != self.owner {
change |= Change::ATTRIBUTES;
}
}
let wanted = Change::new(change.notes() & wanted);
match wanted.is_nothing() {
true => None,
false => Some(wanted),
}
}
pub(super) fn gone(&self) -> bool {
self.nlink == 0
}
}
fn at_path(path: &CString) -> Option<u64> {
let mut raw: libc::stat = unsafe { mem::zeroed() };
retried(|| unsafe { libc::stat(path.as_ptr(), &mut raw) })
.ok()
.map(|_| raw.st_ino)
}
#[cfg(test)]
mod tests {
use super::*;
fn settled() -> Snapshot {
Snapshot {
ino: 7,
size: 100,
mtime: (1000, 0),
mode: 0o100_644,
owner: (501, 20),
nlink: 1,
at_path: Some(7),
}
}
#[test]
fn a_snapshot_against_itself_is_no_change() {
let seen = settled();
assert_eq!(seen.against(&seen, EVERY_NOTE), None);
}
#[test]
fn a_write_past_the_end_is_written_and_extended() {
let seen = settled();
let mut now = seen;
now.size = 200;
now.mtime = (1001, 0);
let change = seen.against(&now, EVERY_NOTE).expect("a write must report");
assert!(change.written(), "the write time moved");
assert!(change.extended(), "the file got longer");
assert!(!change.attributes(), "a write is not an attribute change");
}
#[test]
fn a_write_in_place_does_not_extend() {
let seen = settled();
let mut now = seen;
now.mtime = (1001, 0);
let change = seen.against(&now, EVERY_NOTE).expect("a write must report");
assert!(change.written(), "the write time moved");
assert!(!change.extended(), "the file is the same length");
}
#[test]
fn a_new_mode_or_owner_is_an_attribute_change() {
let seen = settled();
let mut chmodded = seen;
chmodded.mode = 0o100_600;
let mut chowned = seen;
chowned.owner = (0, 0);
for now in [chmodded, chowned] {
let change = seen
.against(&now, EVERY_NOTE)
.expect("an attribute must report");
assert!(change.attributes(), "an attribute moved");
assert!(!change.written(), "the write time stayed where it was");
}
}
#[test]
fn an_unlinked_file_reads_as_removed() {
let seen = settled();
let mut now = seen;
now.nlink = 0;
now.at_path = None;
now.mtime = (1001, 0);
let change = seen
.against(&now, EVERY_NOTE)
.expect("a removal must report");
assert_eq!(change, Change::REMOVED, "a removal is the whole answer");
}
#[test]
fn a_path_that_lost_the_file_reads_as_renamed() {
let seen = settled();
let mut moved = seen;
moved.at_path = None;
let mut replaced = seen;
replaced.at_path = Some(9);
for now in [moved, replaced] {
let change = seen
.against(&now, EVERY_NOTE)
.expect("a rename must report");
assert!(change.renamed(), "the path stopped leading to the file");
assert!(!change.removed(), "the file itself is still there");
}
}
#[test]
fn a_removal_is_not_reported_twice() {
let seen = settled();
let mut gone = seen;
gone.nlink = 0;
gone.at_path = None;
assert_eq!(seen.against(&gone, EVERY_NOTE), Some(Change::REMOVED));
assert_eq!(
gone.against(&gone, EVERY_NOTE),
None,
"still gone is not news"
);
}
#[test]
fn a_rename_is_not_reported_twice() {
let seen = settled();
let mut moved = seen;
moved.at_path = None;
let change = seen
.against(&moved, EVERY_NOTE)
.expect("a rename must report");
assert!(change.renamed(), "the path stopped leading to the file");
assert_eq!(
moved.against(&moved, EVERY_NOTE),
None,
"still away is not news"
);
}
#[test]
fn a_new_link_reads_as_linked() {
let seen = settled();
let mut now = seen;
now.nlink = 2;
let change = seen.against(&now, EVERY_NOTE).expect("a link must report");
assert!(change.linked(), "the link count moved");
}
#[test]
fn a_narrowed_watch_ignores_what_it_did_not_ask_for() {
let seen = settled();
let mut now = seen;
now.size = 200;
now.mtime = (1001, 0);
assert_eq!(seen.against(&now, Change::REMOVED.notes()), None);
now.nlink = 0;
assert_eq!(
seen.against(&now, Change::REMOVED.notes()),
Some(Change::REMOVED),
);
}
#[test]
fn naming_two_parts_holds_both() {
let picked = Change::REMOVED | Change::RENAMED;
assert!(picked.removed(), "a removal was named");
assert!(picked.renamed(), "a rename was named");
assert!(!picked.written(), "a write was not");
assert_eq!(Change::ANY.notes(), EVERY_NOTE);
}
}