diff --git a/fish-rust/src/wutil/fileid.rs b/fish-rust/src/wutil/fileid.rs new file mode 100644 index 000000000..943a89f38 --- /dev/null +++ b/fish-rust/src/wutil/fileid.rs @@ -0,0 +1,85 @@ +use crate::wutil::{wstat, wstr}; +use std::cmp::Ordering; +use std::fs::{File, Metadata}; +use std::os::fd::RawFd; + +use std::os::fd::{FromRawFd, IntoRawFd}; +#[cfg(target_os = "linux")] +use std::os::linux::fs::MetadataExt; +#[cfg(target_os = "macos")] +use std::os::macos::fs::MetadataExt; +#[cfg(not(any(target_os = "macos", target_os = "linux")))] +use std::os::unix::fs::MetadataExt; + +/// Struct for representing a file's inode. We use this to detect and avoid symlink loops, among +/// other things. While an inode / dev pair is sufficient to distinguish co-existing files, Linux +/// seems to aggressively re-use inodes, so it cannot determine if a file has been deleted (ABA +/// problem). Therefore we include richer information. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct FileId { + pub device: u64, + pub inode: u64, + pub size: u64, + pub change_seconds: i64, + pub change_nanoseconds: i64, + pub mod_seconds: i64, + pub mod_nanoseconds: i64, +} + +impl FileId { + pub fn from_stat(buf: Metadata) -> Self { + // These "into()" calls are because the various fields have different types + // on different platforms. + #[allow(clippy::useless_conversion)] + FileId { + device: buf.st_dev(), + inode: buf.st_ino(), + size: buf.st_size(), + change_seconds: buf.st_ctime().into(), + change_nanoseconds: buf.st_ctime_nsec().into(), + mod_seconds: buf.st_mtime().into(), + mod_nanoseconds: buf.st_mtime_nsec().into(), + } + } + + pub fn older_than(&self, rhs: &FileId) -> bool { + match (self.change_seconds, self.change_nanoseconds) + .cmp(&(rhs.change_seconds, rhs.change_nanoseconds)) + { + Ordering::Less => true, + Ordering::Equal | Ordering::Greater => false, + } + } +} + +pub const INVALID_FILE_ID: FileId = FileId { + device: u64::MAX, + inode: u64::MAX, + size: u64::MAX, + change_seconds: i64::MIN, + change_nanoseconds: -1, + mod_seconds: i64::MIN, + mod_nanoseconds: -1, +}; + +/// Get a FileID corresponding to a raw file descriptor, or INVALID_FILE_ID if it fails. +pub fn file_id_for_fd(fd: RawFd) -> FileId { + // Safety: we just want fstat(). Rust makes this stupidly hard. + // The only way to get fstat from an fd is to use a File as an intermediary, + // but File assumes ownership; so we have to use into_raw_fd() to release it. + let file = unsafe { File::from_raw_fd(fd) }; + let res = file + .metadata() + .map(FileId::from_stat) + .unwrap_or(INVALID_FILE_ID); + let fd2 = file.into_raw_fd(); + assert_eq!(fd, fd2); + res +} + +/// Get a FileID corresponding to a path, or INVALID_FILE_ID if it fails. +pub fn file_id_for_path(path: &wstr) -> FileId { + wstat(path) + .map(FileId::from_stat) + .unwrap_or(INVALID_FILE_ID) +} diff --git a/fish-rust/src/wutil/mod.rs b/fish-rust/src/wutil/mod.rs index 63e77b341..c8ac0938c 100644 --- a/fish-rust/src/wutil/mod.rs +++ b/fish-rust/src/wutil/mod.rs @@ -1,5 +1,6 @@ pub mod encoding; pub mod errors; +pub mod fileid; pub mod gettext; pub mod printf; pub mod wcstod;