diff --git a/Cargo.lock b/Cargo.lock
index 4150d0d..9205b91 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -194,6 +194,7 @@ dependencies = [
"sha2",
"unicode-segmentation",
"unicode-width",
+ "windows-sys 0.61.2",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 044ac2c..8db12b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,9 @@ serde_json = "1"
unicode-segmentation = "1"
unicode-width = "0.2"
+[target.'cfg(windows)'.dependencies]
+windows-sys = { version = "=0.61.2", features = ["Win32_Foundation", "Win32_System_Time", "Win32_UI_Shell"] }
+
[profile.release]
lto = true
codegen-units = 1
diff --git a/src/config.rs b/src/config.rs
index 3dcfe7c..5715cb3 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -44,14 +44,18 @@ impl Default for Settings {
}
}
-/// The config file location.
+/// The config file location: `
/mapsize/config.json` where dir is
+/// %AppData% on Windows, ~/Library/Application Support on macOS, and
+/// $XDG_CONFIG_HOME or ~/.config elsewhere.
pub fn path() -> Option {
let env = |k: &str| {
std::env::var_os(k)
.filter(|v| !v.is_empty())
.map(PathBuf::from)
};
- let dir = if cfg!(target_os = "macos") {
+ let dir = if cfg!(windows) {
+ env("AppData")?
+ } else if cfg!(target_os = "macos") {
env("HOME")?.join("Library/Application Support")
} else {
env("XDG_CONFIG_HOME").or_else(|| env("HOME").map(|h| h.join(".config")))?
diff --git a/src/duplicate.rs b/src/duplicate.rs
index 1fd640d..fec328b 100644
--- a/src/duplicate.rs
+++ b/src/duplicate.rs
@@ -14,7 +14,6 @@ use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fs::File;
use std::io::{self, Read};
-use std::os::unix::fs::FileExt;
use std::path::PathBuf;
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
use std::sync::{Mutex, RwLock};
@@ -229,7 +228,7 @@ fn sample_hash(_: &Finder, c: &Cand) -> Option<[u8; 32]> {
let mut h = Sha256::new();
let mut buf = vec![0u8; SAMPLE_SIZE as usize];
for off in [0, c.size / 2 - SAMPLE_SIZE / 2, c.size - SAMPLE_SIZE] {
- match fh.read_exact_at(&mut buf, off as u64) {
+ match platform::read_exact_at(&fh, &mut buf, off as u64) {
Ok(()) => {}
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {} // shrank; stage 3 rejects it
Err(_) => return None,
@@ -263,14 +262,12 @@ fn full_hash(f: &Finder, c: &Cand) -> Option<[u8; 32]> {
mod tests {
use super::*;
use crate::scan::{self, tests::TempDir};
- use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use std::sync::Arc;
- use std::time::Duration;
fn scan(root: &Path, workers: usize) -> Arc> {
let s = scan::start(
- root.as_os_str().as_bytes(),
+ root.as_os_str().as_encoded_bytes(),
scan::Options {
workers,
..Default::default()
@@ -323,8 +320,11 @@ mod tests {
/// A candidate replaced by a FIFO (or a symlink to one) after the scan
/// must be skipped, not block the search forever in open(2).
+ #[cfg(unix)]
#[test]
fn find_skips_swapped_fifo() {
+ use std::os::unix::ffi::OsStrExt;
+ use std::time::Duration;
let root = TempDir::new("dupfifo");
let other = TempDir::new("dupfifo2");
for n in ["a", "b", "c"] {
diff --git a/src/filter/eval.rs b/src/filter/eval.rs
index 24ed7e1..7142c81 100644
--- a/src/filter/eval.rs
+++ b/src/filter/eval.rs
@@ -98,8 +98,8 @@ impl<'a> Ctx<'a> {
.filter(|p| !p.is_empty())
{
self.path.push_str(pp);
- if !pp.ends_with('/') {
- self.path.push('/');
+ if !pp.ends_with(platform::SEP as char) {
+ self.path.push(platform::SEP as char);
}
push_lower(&mut self.path, &self.node.name);
return;
diff --git a/src/filter/parser.rs b/src/filter/parser.rs
index 7633c07..4de7ccb 100644
--- a/src/filter/parser.rs
+++ b/src/filter/parser.rs
@@ -1,6 +1,7 @@
use super::eval::{Expr, NumOp};
use super::lexer::{lex, TokKind, Token};
use crate::inventory::{self as inv, Flags};
+use crate::platform::days_from_civil;
use regex::Regex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
@@ -561,24 +562,6 @@ fn days_in(y: i64, m: i64) -> i64 {
}
}
-/// Days since 1970-01-01 of a proleptic Gregorian date.
-fn days_from_civil(y: i64, m: i64, d: i64) -> i64 {
- let y = if m <= 2 { y - 1 } else { y };
- let era = y.div_euclid(400);
- let yoe = y - era * 400;
- let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + d - 1;
- let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
- era * 146097 + doe - 719468
-}
-
fn local_nanos(y: i64, mo: i64, d: i64, h: i64, mi: i64) -> Option {
- let mut tm: libc::tm = unsafe { std::mem::zeroed() };
- tm.tm_year = (y - 1900) as i32;
- tm.tm_mon = (mo - 1) as i32;
- tm.tm_mday = d as i32;
- tm.tm_hour = h as i32;
- tm.tm_min = mi as i32;
- tm.tm_isdst = -1;
- let t = unsafe { libc::mktime(&mut tm) };
- (t != -1).then(|| t as i64 * 1_000_000_000)
+ Some(crate::platform::local_to_unix(y, mo, d, h, mi) * 1_000_000_000)
}
diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs
index 315a6c6..cda018b 100644
--- a/src/inventory/mod.rs
+++ b/src/inventory/mod.rs
@@ -13,9 +13,8 @@ pub use category::*;
pub use query::*;
pub use stats::*;
+use crate::platform;
use std::collections::HashMap;
-use std::ffi::OsStr;
-use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
/// Addresses a node within one Tree. The root is always 0.
@@ -310,8 +309,8 @@ impl Tree {
}
let mut out = Vec::with_capacity(parts.iter().map(|p| p.len() + 1).sum());
for p in parts.iter().rev() {
- if !out.is_empty() && out.last() != Some(&b'/') {
- out.push(b'/');
+ if !out.is_empty() && !out.last().is_some_and(|&b| platform::is_sep(b)) {
+ out.push(platform::SEP);
}
out.extend_from_slice(p);
}
@@ -320,7 +319,7 @@ impl Tree {
/// Reconstructs the full path of id.
pub fn path(&self, id: NodeId) -> PathBuf {
- PathBuf::from(OsStr::from_bytes(&self.path_bytes(id)))
+ platform::path_from_bytes(&self.path_bytes(id))
}
/// The full path as (lossy) UTF-8, unsanitised.
diff --git a/src/inventory/stats.rs b/src/inventory/stats.rs
index 2c9369c..2685296 100644
--- a/src/inventory/stats.rs
+++ b/src/inventory/stats.rs
@@ -42,16 +42,18 @@ impl ErrKind {
/// Maps an OS error to an ErrKind.
pub fn classify(err: &io::Error) -> ErrKind {
+ #[cfg(unix)]
match err.raw_os_error() {
- Some(libc::EACCES) | Some(libc::EPERM) => ErrKind::Permission,
- Some(libc::ENOENT) => ErrKind::Vanished,
- Some(libc::ELOOP) => ErrKind::Loop,
- Some(libc::EIO) => ErrKind::Io,
- _ => match err.kind() {
- io::ErrorKind::PermissionDenied => ErrKind::Permission,
- io::ErrorKind::NotFound => ErrKind::Vanished,
- _ => ErrKind::Other,
- },
+ Some(libc::EACCES | libc::EPERM) => return ErrKind::Permission,
+ Some(libc::ENOENT) => return ErrKind::Vanished,
+ Some(libc::ELOOP) => return ErrKind::Loop,
+ Some(libc::EIO) => return ErrKind::Io,
+ _ => {}
+ }
+ match err.kind() {
+ io::ErrorKind::PermissionDenied => ErrKind::Permission,
+ io::ErrorKind::NotFound => ErrKind::Vanished,
+ _ => ErrKind::Other,
}
}
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index bb1a180..7106c18 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -1,19 +1,57 @@
-//! OS-specific filesystem metadata and file operations (Unix; Linux first).
-//! Nothing else in the program calls libc for filesystem work.
+//! OS-specific filesystem metadata and file operations behind a small common
+//! API. Nothing else in the program calls the OS directly for filesystem
+//! work; the per-OS halves live in `unix.rs` and `windows.rs`.
+//!
+//! Per OS:
+//! * Linux: statx relative to the directory fd (birth time), virtual
+//! filesystem detection, freedesktop.org trash, xdg-open.
+//! * macOS: fstatat relative to the directory fd, Finder trash, `open -R`.
+//! * FreeBSD, NetBSD: fstatat relative to the directory fd; no trash or
+//! reveal (permanent delete only, offered on headless systems).
+//! * Windows: std directory listing and metadata. Allocated size is unknown
+//! and there is no inode identity, so symlinks are never followed and hard
+//! links are not deduplicated. Recycle Bin and Explorer reveal; no
+//! permanent delete.
use std::collections::HashMap;
-use std::ffi::{CStr, CString, OsStr};
+use std::ffi::OsStr;
use std::fs::File;
-use std::io::{self, Write};
-use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
-use std::os::unix::ffi::OsStrExt;
-use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
+use std::io;
use std::path::{Path, PathBuf};
-use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Mutex;
-/// File mode bits in Go's io/fs.FileMode layout. Kept so snapshots stay
-/// byte-compatible with the Go implementation.
+#[cfg(unix)]
+mod unix;
+#[cfg(unix)]
+pub use unix::*;
+#[cfg(windows)]
+mod windows;
+#[cfg(windows)]
+pub use windows::*;
+
+/// File mode bits as stored in snapshots, identical on every OS so that
+/// snapshots are portable.
+///
+/// The low 9 bits are the rwxrwxrwx permissions. Bits 31 down to 19 are
+/// flags, rendered by [`mode::string`] with one letter each:
+///
+/// | bit | letter | meaning |
+/// |-----|--------|---------|
+/// | 31 | d | directory |
+/// | 30 | a | append-only |
+/// | 29 | l | exclusive use |
+/// | 28 | T | temporary |
+/// | 27 | L | symbolic link |
+/// | 26 | D | device |
+/// | 25 | p | named pipe (FIFO) |
+/// | 24 | S | Unix domain socket |
+/// | 23 | u | setuid |
+/// | 22 | g | setgid |
+/// | 21 | c | character device (with D) |
+/// | 20 | t | sticky |
+/// | 19 | ? | irregular (none of the above types) |
+///
+/// A regular file has none of the type bits (d L D p S c ?) set.
pub mod mode {
pub const DIR: u32 = 1 << 31;
pub const APPEND: u32 = 1 << 30;
@@ -41,7 +79,8 @@ pub mod mode {
m & SYMLINK != 0
}
- /// Formats like Go's FileMode.String, e.g. "drwxr-xr-x".
+ /// Formats the mode as the set flag letters (in bit order, or "-" if
+ /// none) followed by rwxrwxrwx, e.g. "drwxr-xr-x", "Lrwxrwxrwx".
pub fn string(m: u32) -> String {
const STR: &[u8] = b"dalTLDpSugct?";
let mut out = String::with_capacity(12);
@@ -60,35 +99,11 @@ pub mod mode {
}
}
-/// Converts a Unix st_mode to the Go FileMode layout.
-pub fn mode_from_unix(m: u32) -> u32 {
- let mut out = m & 0o777;
- out |= match m & libc::S_IFMT {
- libc::S_IFDIR => mode::DIR,
- libc::S_IFLNK => mode::SYMLINK,
- libc::S_IFIFO => mode::NAMED_PIPE,
- libc::S_IFSOCK => mode::SOCKET,
- libc::S_IFCHR => mode::DEVICE | mode::CHAR_DEVICE,
- libc::S_IFBLK => mode::DEVICE,
- _ => 0,
- };
- if m & libc::S_ISUID != 0 {
- out |= mode::SETUID;
- }
- if m & libc::S_ISGID != 0 {
- out |= mode::SETGID;
- }
- if m & libc::S_ISVTX != 0 {
- out |= mode::STICKY;
- }
- out
-}
-
/// Metadata of one directory entry.
#[derive(Debug, Default)]
pub struct Meta {
pub name: Vec,
- pub mode: u32, // Go FileMode layout
+ pub mode: u32, // see `mode`
pub size: i64,
pub alloc: i64,
pub alloc_known: bool,
@@ -99,175 +114,10 @@ pub struct Meta {
pub nlink: u32,
pub dev: u64,
pub ino: u64,
- pub has_ino: bool,
+ pub has_ino: bool, // dev/ino are meaningful
pub err: Option, // stat failed; only name is valid
}
-fn cstr(b: &[u8]) -> io::Result {
- CString::new(b).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))
-}
-
-fn cvt(r: libc::c_int) -> io::Result {
- if r < 0 {
- Err(io::Error::last_os_error())
- } else {
- Ok(r)
- }
-}
-
-fn open_dir(path: &[u8]) -> io::Result {
- let c = cstr(path)?;
- let fd = cvt(unsafe {
- libc::open(
- c.as_ptr(),
- libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
- )
- })?;
- Ok(unsafe { OwnedFd::from_raw_fd(fd) })
-}
-
-#[cfg(target_os = "linux")]
-static NO_STATX: AtomicBool = AtomicBool::new(false);
-
-#[cfg(target_os = "linux")]
-fn stat_at(dirfd: RawFd, name: &CStr, flags: libc::c_int) -> io::Result {
- if !NO_STATX.load(Ordering::Relaxed) {
- let mut st: libc::statx = unsafe { std::mem::zeroed() };
- let mask = libc::STATX_BASIC_STATS | libc::STATX_BTIME;
- let r = unsafe {
- libc::statx(
- dirfd,
- name.as_ptr(),
- flags | libc::AT_STATX_DONT_SYNC,
- mask,
- &mut st,
- )
- };
- if r == 0 {
- let mut m = Meta {
- mode: mode_from_unix(st.stx_mode as u32),
- size: st.stx_size as i64,
- alloc: st.stx_blocks as i64 * 512,
- alloc_known: true,
- mtime: st.stx_mtime.tv_sec * 1_000_000_000 + st.stx_mtime.tv_nsec as i64,
- uid: st.stx_uid,
- gid: st.stx_gid,
- nlink: st.stx_nlink,
- dev: libc::makedev(st.stx_dev_major, st.stx_dev_minor),
- ino: st.stx_ino,
- has_ino: true,
- ..Default::default()
- };
- if st.stx_mask & libc::STATX_BTIME != 0 {
- m.btime = st.stx_btime.tv_sec * 1_000_000_000 + st.stx_btime.tv_nsec as i64;
- }
- return Ok(m);
- }
- let err = io::Error::last_os_error();
- if err.raw_os_error() != Some(libc::ENOSYS) {
- return Err(err);
- }
- NO_STATX.store(true, Ordering::Relaxed);
- }
- fstat_at(dirfd, name, flags)
-}
-
-#[cfg(not(target_os = "linux"))]
-fn stat_at(dirfd: RawFd, name: &CStr, flags: libc::c_int) -> io::Result {
- fstat_at(dirfd, name, flags)
-}
-
-fn fstat_at(dirfd: RawFd, name: &CStr, flags: libc::c_int) -> io::Result {
- let mut st: libc::stat = unsafe { std::mem::zeroed() };
- cvt(unsafe { libc::fstatat(dirfd, name.as_ptr(), &mut st, flags) })?;
- Ok(from_stat(&st))
-}
-
-#[allow(clippy::unnecessary_cast)]
-fn from_stat(st: &libc::stat) -> Meta {
- Meta {
- mode: mode_from_unix(st.st_mode as u32),
- size: st.st_size as i64,
- alloc: st.st_blocks as i64 * 512,
- alloc_known: true,
- mtime: st.st_mtime as i64 * 1_000_000_000 + st.st_mtime_nsec as i64,
- uid: st.st_uid,
- gid: st.st_gid,
- nlink: st.st_nlink as u32,
- dev: st.st_dev as u64,
- ino: st.st_ino as u64,
- has_ino: true,
- ..Default::default()
- }
-}
-
-/// An open directory stream (readdir over the fd we also stat relative to).
-struct DirStream {
- dir: *mut libc::DIR,
-}
-
-impl Drop for DirStream {
- fn drop(&mut self) {
- unsafe { libc::closedir(self.dir) };
- }
-}
-
-/// Streams the entries of a directory in chunks of up to `chunk`, stat-ing
-/// each entry relative to the directory fd (no repeated path resolution).
-/// Returning an error from f stops the read and returns it.
-pub fn read_dir>(
- path: &[u8],
- chunk: usize,
- mut f: impl FnMut(Vec) -> Result<(), E>,
-) -> Result<(), E> {
- let fd = open_dir(path)?;
- let raw = fd.as_raw_fd();
- let dir = unsafe { libc::fdopendir(raw) };
- if dir.is_null() {
- return Err(io::Error::last_os_error().into());
- }
- std::mem::forget(fd); // owned by the DIR stream now
- let ds = DirStream { dir };
- let mut out = Vec::with_capacity(chunk);
- loop {
- unsafe { *libc::__errno_location() = 0 };
- let ent = unsafe { libc::readdir(ds.dir) };
- if ent.is_null() {
- let e = io::Error::last_os_error();
- if e.raw_os_error().unwrap_or(0) != 0 {
- return Err(e.into());
- }
- break;
- }
- let name = unsafe { CStr::from_ptr((*ent).d_name.as_ptr()) };
- let nb = name.to_bytes();
- if nb == b"." || nb == b".." {
- continue;
- }
- let mut m = stat_at(raw, name, libc::AT_SYMLINK_NOFOLLOW).unwrap_or_else(|e| Meta {
- err: Some(e),
- ..Default::default()
- });
- m.name = nb.to_vec();
- out.push(m);
- if out.len() >= chunk {
- f(std::mem::replace(&mut out, Vec::with_capacity(chunk)))?;
- }
- }
- if !out.is_empty() {
- f(out)?;
- }
- Ok(())
-}
-
-fn stat_path(path: &[u8], follow: bool) -> io::Result {
- let c = cstr(path)?;
- let flags = if follow { 0 } else { libc::AT_SYMLINK_NOFOLLOW };
- let mut m = stat_at(libc::AT_FDCWD, &c, flags)?;
- m.name = base_name(path).to_vec();
- Ok(m)
-}
-
/// Metadata for path without following a final symlink.
pub fn lstat(path: &[u8]) -> io::Result {
stat_path(path, false)
@@ -278,108 +128,6 @@ pub fn stat(path: &[u8]) -> io::Result {
stat_path(path, true)
}
-/// Joins a directory and an entry name without cleaning.
-pub fn join_path(dir: &[u8], name: &[u8]) -> Vec {
- let mut p = Vec::with_capacity(dir.len() + name.len() + 1);
- p.extend_from_slice(dir);
- if dir.last() != Some(&b'/') {
- p.push(b'/');
- }
- p.extend_from_slice(name);
- p
-}
-
-fn base_name(path: &[u8]) -> &[u8] {
- let mut end = path.len();
- while end > 1 && path[end - 1] == b'/' {
- end -= 1;
- }
- let p = &path[..end];
- match p.iter().rposition(|&b| b == b'/') {
- Some(i) if p.len() > 1 => &p[i + 1..],
- _ => p,
- }
-}
-
-static NAMES: Mutex