Build and behave correctly on every release target
Split platform into a portable core (mod.rs) plus unix.rs and windows.rs. - Linux: statx through syscall(2) so static musl links (fstatat fallback on ENOSYS); virtual-fs detection compares f_type as u32. - macOS: Finder trash (~/.Trash or /Volumes/X/.Trashes/<uid>, "name N" collisions, EXDEV falls through to the next location) and `open -R` reveal; never headless. - FreeBSD/NetBSD: fstatat with birth time; trash and reveal unsupported, permanent delete still available on headless systems. - Windows: std read_dir and metadata, allocated size unknown, no inode identity, Recycle Bin via SHFileOperationW (FOF_ALLOWUNDO, all architectures), Explorer /select reveal, no permanent delete, '\\' separator with Windows glob semantics, abs paths via GetFullPathNameW, config in %AppData%. - Local time via a portable LocalTime helper (localtime_r tm_gmtoff on Unix, SystemTimeToTzSpecificLocalTime on Windows) replacing libc tm and mktime in the TUI, snapshot and filter code. - errno, stat field names and mode_t width handled per OS; Unix-only tests gated; clippy clean on all targets. Adds windows-sys =0.61.2 (Windows only; Win32_Foundation, Win32_System_Time, Win32_UI_Shell), already in the lockfile via crossterm.
This commit is contained in:
Generated
+1
@@ -194,6 +194,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ serde_json = "1"
|
|||||||
unicode-segmentation = "1"
|
unicode-segmentation = "1"
|
||||||
unicode-width = "0.2"
|
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]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
+6
-2
@@ -44,14 +44,18 @@ impl Default for Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The config file location.
|
/// The config file location: `<dir>/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<PathBuf> {
|
pub fn path() -> Option<PathBuf> {
|
||||||
let env = |k: &str| {
|
let env = |k: &str| {
|
||||||
std::env::var_os(k)
|
std::env::var_os(k)
|
||||||
.filter(|v| !v.is_empty())
|
.filter(|v| !v.is_empty())
|
||||||
.map(PathBuf::from)
|
.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")
|
env("HOME")?.join("Library/Application Support")
|
||||||
} else {
|
} else {
|
||||||
env("XDG_CONFIG_HOME").or_else(|| env("HOME").map(|h| h.join(".config")))?
|
env("XDG_CONFIG_HOME").or_else(|| env("HOME").map(|h| h.join(".config")))?
|
||||||
|
|||||||
+5
-5
@@ -14,7 +14,6 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{self, Read};
|
use std::io::{self, Read};
|
||||||
use std::os::unix::fs::FileExt;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
|
||||||
use std::sync::{Mutex, RwLock};
|
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 h = Sha256::new();
|
||||||
let mut buf = vec![0u8; SAMPLE_SIZE as usize];
|
let mut buf = vec![0u8; SAMPLE_SIZE as usize];
|
||||||
for off in [0, c.size / 2 - SAMPLE_SIZE / 2, c.size - SAMPLE_SIZE] {
|
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(()) => {}
|
Ok(()) => {}
|
||||||
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {} // shrank; stage 3 rejects it
|
Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {} // shrank; stage 3 rejects it
|
||||||
Err(_) => return None,
|
Err(_) => return None,
|
||||||
@@ -263,14 +262,12 @@ fn full_hash(f: &Finder, c: &Cand) -> Option<[u8; 32]> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::scan::{self, tests::TempDir};
|
use crate::scan::{self, tests::TempDir};
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
fn scan(root: &Path, workers: usize) -> Arc<RwLock<Tree>> {
|
fn scan(root: &Path, workers: usize) -> Arc<RwLock<Tree>> {
|
||||||
let s = scan::start(
|
let s = scan::start(
|
||||||
root.as_os_str().as_bytes(),
|
root.as_os_str().as_encoded_bytes(),
|
||||||
scan::Options {
|
scan::Options {
|
||||||
workers,
|
workers,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -323,8 +320,11 @@ mod tests {
|
|||||||
|
|
||||||
/// A candidate replaced by a FIFO (or a symlink to one) after the scan
|
/// 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).
|
/// must be skipped, not block the search forever in open(2).
|
||||||
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn find_skips_swapped_fifo() {
|
fn find_skips_swapped_fifo() {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use std::time::Duration;
|
||||||
let root = TempDir::new("dupfifo");
|
let root = TempDir::new("dupfifo");
|
||||||
let other = TempDir::new("dupfifo2");
|
let other = TempDir::new("dupfifo2");
|
||||||
for n in ["a", "b", "c"] {
|
for n in ["a", "b", "c"] {
|
||||||
|
|||||||
+2
-2
@@ -98,8 +98,8 @@ impl<'a> Ctx<'a> {
|
|||||||
.filter(|p| !p.is_empty())
|
.filter(|p| !p.is_empty())
|
||||||
{
|
{
|
||||||
self.path.push_str(pp);
|
self.path.push_str(pp);
|
||||||
if !pp.ends_with('/') {
|
if !pp.ends_with(platform::SEP as char) {
|
||||||
self.path.push('/');
|
self.path.push(platform::SEP as char);
|
||||||
}
|
}
|
||||||
push_lower(&mut self.path, &self.node.name);
|
push_lower(&mut self.path, &self.node.name);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+2
-19
@@ -1,6 +1,7 @@
|
|||||||
use super::eval::{Expr, NumOp};
|
use super::eval::{Expr, NumOp};
|
||||||
use super::lexer::{lex, TokKind, Token};
|
use super::lexer::{lex, TokKind, Token};
|
||||||
use crate::inventory::{self as inv, Flags};
|
use crate::inventory::{self as inv, Flags};
|
||||||
|
use crate::platform::days_from_civil;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
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<i64> {
|
fn local_nanos(y: i64, mo: i64, d: i64, h: i64, mi: i64) -> Option<i64> {
|
||||||
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
|
Some(crate::platform::local_to_unix(y, mo, d, h, mi) * 1_000_000_000)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ pub use category::*;
|
|||||||
pub use query::*;
|
pub use query::*;
|
||||||
pub use stats::*;
|
pub use stats::*;
|
||||||
|
|
||||||
|
use crate::platform;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
/// Addresses a node within one Tree. The root is always 0.
|
/// 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());
|
let mut out = Vec::with_capacity(parts.iter().map(|p| p.len() + 1).sum());
|
||||||
for p in parts.iter().rev() {
|
for p in parts.iter().rev() {
|
||||||
if !out.is_empty() && out.last() != Some(&b'/') {
|
if !out.is_empty() && !out.last().is_some_and(|&b| platform::is_sep(b)) {
|
||||||
out.push(b'/');
|
out.push(platform::SEP);
|
||||||
}
|
}
|
||||||
out.extend_from_slice(p);
|
out.extend_from_slice(p);
|
||||||
}
|
}
|
||||||
@@ -320,7 +319,7 @@ impl Tree {
|
|||||||
|
|
||||||
/// Reconstructs the full path of id.
|
/// Reconstructs the full path of id.
|
||||||
pub fn path(&self, id: NodeId) -> PathBuf {
|
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.
|
/// The full path as (lossy) UTF-8, unsanitised.
|
||||||
|
|||||||
@@ -42,16 +42,18 @@ impl ErrKind {
|
|||||||
|
|
||||||
/// Maps an OS error to an ErrKind.
|
/// Maps an OS error to an ErrKind.
|
||||||
pub fn classify(err: &io::Error) -> ErrKind {
|
pub fn classify(err: &io::Error) -> ErrKind {
|
||||||
|
#[cfg(unix)]
|
||||||
match err.raw_os_error() {
|
match err.raw_os_error() {
|
||||||
Some(libc::EACCES) | Some(libc::EPERM) => ErrKind::Permission,
|
Some(libc::EACCES | libc::EPERM) => return ErrKind::Permission,
|
||||||
Some(libc::ENOENT) => ErrKind::Vanished,
|
Some(libc::ENOENT) => return ErrKind::Vanished,
|
||||||
Some(libc::ELOOP) => ErrKind::Loop,
|
Some(libc::ELOOP) => return ErrKind::Loop,
|
||||||
Some(libc::EIO) => ErrKind::Io,
|
Some(libc::EIO) => return ErrKind::Io,
|
||||||
_ => match err.kind() {
|
_ => {}
|
||||||
|
}
|
||||||
|
match err.kind() {
|
||||||
io::ErrorKind::PermissionDenied => ErrKind::Permission,
|
io::ErrorKind::PermissionDenied => ErrKind::Permission,
|
||||||
io::ErrorKind::NotFound => ErrKind::Vanished,
|
io::ErrorKind::NotFound => ErrKind::Vanished,
|
||||||
_ => ErrKind::Other,
|
_ => ErrKind::Other,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+338
-668
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,797 @@
|
|||||||
|
//! Unix: directory reads and stat calls relative to the directory fd, owner
|
||||||
|
//! names, local time, and the trash/delete/reveal operations.
|
||||||
|
|
||||||
|
use super::{base_name, check_abs, mode, other, Expect, Meta};
|
||||||
|
use std::ffi::{CStr, CString, OsStr};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Converts a Unix st_mode to the portable layout in `mode`.
|
||||||
|
#[allow(clippy::unnecessary_cast)] // mode_t is u16 on some systems
|
||||||
|
pub fn mode_from_unix(m: u32) -> u32 {
|
||||||
|
let mut out = m & 0o777;
|
||||||
|
let fmt = m & libc::S_IFMT as u32;
|
||||||
|
out |= if fmt == libc::S_IFDIR as u32 {
|
||||||
|
mode::DIR
|
||||||
|
} else if fmt == libc::S_IFLNK as u32 {
|
||||||
|
mode::SYMLINK
|
||||||
|
} else if fmt == libc::S_IFIFO as u32 {
|
||||||
|
mode::NAMED_PIPE
|
||||||
|
} else if fmt == libc::S_IFSOCK as u32 {
|
||||||
|
mode::SOCKET
|
||||||
|
} else if fmt == libc::S_IFCHR as u32 {
|
||||||
|
mode::DEVICE | mode::CHAR_DEVICE
|
||||||
|
} else if fmt == libc::S_IFBLK as u32 {
|
||||||
|
mode::DEVICE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
if m & libc::S_ISUID as u32 != 0 {
|
||||||
|
out |= mode::SETUID;
|
||||||
|
}
|
||||||
|
if m & libc::S_ISGID as u32 != 0 {
|
||||||
|
out |= mode::SETGID;
|
||||||
|
}
|
||||||
|
if m & libc::S_ISVTX as u32 != 0 {
|
||||||
|
out |= mode::STICKY;
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cstr(b: &[u8]) -> io::Result<CString> {
|
||||||
|
CString::new(b).map_err(|_| io::Error::from_raw_os_error(libc::EINVAL))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cvt(r: libc::c_int) -> io::Result<libc::c_int> {
|
||||||
|
if r < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_dir(path: &[u8]) -> io::Result<OwnedFd> {
|
||||||
|
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: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||||
|
|
||||||
|
/// statx (one call, includes birth time), falling back to fstatat on kernels
|
||||||
|
/// before 4.11. Called through syscall(2) because not every libc (musl)
|
||||||
|
/// provides a wrapper.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn stat_at(dirfd: RawFd, name: &CStr, flags: libc::c_int) -> io::Result<Meta> {
|
||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
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::syscall(
|
||||||
|
libc::SYS_statx,
|
||||||
|
dirfd,
|
||||||
|
name.as_ptr(),
|
||||||
|
flags | libc::AT_STATX_DONT_SYNC,
|
||||||
|
mask,
|
||||||
|
&mut st as *mut libc::statx,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
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<Meta> {
|
||||||
|
fstat_at(dirfd, name, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fstat_at(dirfd: RawFd, name: &CStr, flags: libc::c_int) -> io::Result<Meta> {
|
||||||
|
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 {
|
||||||
|
#[cfg(target_os = "netbsd")]
|
||||||
|
let mtime_nsec = st.st_mtimensec;
|
||||||
|
#[cfg(not(target_os = "netbsd"))]
|
||||||
|
let mtime_nsec = st.st_mtime_nsec;
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
|
||||||
|
let btime = st.st_birthtime as i64 * 1_000_000_000 + st.st_birthtime_nsec as i64;
|
||||||
|
#[cfg(target_os = "netbsd")]
|
||||||
|
let btime = st.st_birthtime as i64 * 1_000_000_000 + st.st_birthtimensec as i64;
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "freebsd", target_os = "netbsd")))]
|
||||||
|
let btime = 0;
|
||||||
|
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 + mtime_nsec as i64,
|
||||||
|
btime,
|
||||||
|
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) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn errno_location() -> *mut libc::c_int {
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "android"))]
|
||||||
|
return unsafe { libc::__errno_location() };
|
||||||
|
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "freebsd"))]
|
||||||
|
return unsafe { libc::__error() };
|
||||||
|
#[cfg(any(target_os = "netbsd", target_os = "openbsd"))]
|
||||||
|
return unsafe { libc::__errno() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<E: From<io::Error>>(
|
||||||
|
path: &[u8],
|
||||||
|
chunk: usize,
|
||||||
|
mut f: impl FnMut(Vec<Meta>) -> 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 {
|
||||||
|
// readdir signals an error only through errno.
|
||||||
|
unsafe { *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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn stat_path(path: &[u8], follow: bool) -> io::Result<Meta> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn lookup_os_name(group: bool, id: u32) -> Option<String> {
|
||||||
|
let mut buf = vec![0u8; 16384];
|
||||||
|
unsafe {
|
||||||
|
if group {
|
||||||
|
let mut gr: libc::group = std::mem::zeroed();
|
||||||
|
let mut res = std::ptr::null_mut();
|
||||||
|
libc::getgrgid_r(id, &mut gr, buf.as_mut_ptr().cast(), buf.len(), &mut res);
|
||||||
|
(!res.is_null()).then(|| CStr::from_ptr(gr.gr_name).to_string_lossy().into_owned())
|
||||||
|
} else {
|
||||||
|
let mut pw: libc::passwd = std::mem::zeroed();
|
||||||
|
let mut res = std::ptr::null_mut();
|
||||||
|
libc::getpwuid_r(id, &mut pw, buf.as_mut_ptr().cast(), buf.len(), &mut res);
|
||||||
|
(!res.is_null()).then(|| CStr::from_ptr(pw.pw_name).to_string_lossy().into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The system time zone's offset from UTC (seconds east) at unix time secs.
|
||||||
|
#[allow(deprecated)] // libc::time_t on 32-bit musl
|
||||||
|
#[allow(clippy::unnecessary_cast)]
|
||||||
|
pub(super) fn local_offset(secs: i64) -> i32 {
|
||||||
|
let t = secs as libc::time_t;
|
||||||
|
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
|
||||||
|
if unsafe { libc::localtime_r(&t, &mut tm) }.is_null() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
tm.tm_gmtoff as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host name, or "" if unknown.
|
||||||
|
pub fn hostname() -> String {
|
||||||
|
let mut b = [0u8; 256];
|
||||||
|
if unsafe { libc::gethostname(b.as_mut_ptr() as *mut libc::c_char, b.len()) } != 0 {
|
||||||
|
return String::new();
|
||||||
|
}
|
||||||
|
let n = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
||||||
|
String::from_utf8_lossy(&b[..n]).into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linux pseudo-filesystems whose contents are not disk usage.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
const VIRTUAL_FS: [i64; 19] = [
|
||||||
|
0x9fa0, // proc
|
||||||
|
0x62656572, // sysfs
|
||||||
|
0x1cd1, // devpts
|
||||||
|
0x27e0eb, // cgroup
|
||||||
|
0x63677270, // cgroup2
|
||||||
|
0x64626720, // debugfs
|
||||||
|
0x74726163, // tracefs
|
||||||
|
0x73636673, // securityfs
|
||||||
|
0x6165676c, // pstore
|
||||||
|
0xcafe4a11, // bpf
|
||||||
|
0x62656570, // configfs
|
||||||
|
0x65735543, // fusectl
|
||||||
|
0x19800202, // mqueue
|
||||||
|
0xde5e81e4, // efivarfs
|
||||||
|
0x42494e4d, // binfmt_misc
|
||||||
|
0xf97cff8c, // selinuxfs
|
||||||
|
0x958458f6, // hugetlbfs
|
||||||
|
0x6e736673, // nsfs
|
||||||
|
0x50495045, // pipefs
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Whether path is the root of a pseudo filesystem (proc, sysfs, cgroup, …)
|
||||||
|
/// that should not be scanned as disk usage. Only Linux has these.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[allow(clippy::unnecessary_cast)]
|
||||||
|
pub fn is_virtual_fs(path: &[u8]) -> bool {
|
||||||
|
let Ok(c) = cstr(path) else { return false };
|
||||||
|
let mut st: libc::statfs = unsafe { std::mem::zeroed() };
|
||||||
|
if unsafe { libc::statfs(c.as_ptr(), &mut st) } != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// f_type is 32-bit on some architectures: compare as unsigned 32-bit.
|
||||||
|
VIRTUAL_FS.contains(&(st.f_type as u32 as i64))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether path is the root of a pseudo filesystem. Only Linux has these.
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
pub fn is_virtual_fs(_: &[u8]) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a file whose content is about to be read. It does not follow a
|
||||||
|
/// final symlink, and O_NONBLOCK keeps a FIFO swapped in since the scan from
|
||||||
|
/// blocking open(2) forever; callers still check the file type.
|
||||||
|
pub fn open_content(path: &Path) -> io::Result<File> {
|
||||||
|
std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK)
|
||||||
|
.open(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new file readable and writable only by the owner.
|
||||||
|
pub fn create_private(path: &Path) -> io::Result<File> {
|
||||||
|
std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.mode(0o600)
|
||||||
|
.open(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHANGED: &str = "item changed since the scan (owner or type differs); rescan first";
|
||||||
|
|
||||||
|
fn split_parent(path: &Path) -> io::Result<(&Path, &OsStr)> {
|
||||||
|
match (path.parent(), path.file_name()) {
|
||||||
|
(Some(d), Some(b)) => Ok((d, b)),
|
||||||
|
_ => Err(other("refusing to operate on a filesystem root")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_expect(dirfd: RawFd, base: &CStr, want: Expect) -> io::Result<libc::stat> {
|
||||||
|
let mut st: libc::stat = unsafe { std::mem::zeroed() };
|
||||||
|
cvt(unsafe { libc::fstatat(dirfd, base.as_ptr(), &mut st, libc::AT_SYMLINK_NOFOLLOW) })?;
|
||||||
|
#[allow(clippy::unnecessary_cast)]
|
||||||
|
if st.st_uid != want.uid
|
||||||
|
|| mode_from_unix(st.st_mode as u32) & mode::TYPE != want.mode & mode::TYPE
|
||||||
|
{
|
||||||
|
return Err(other(CHANGED));
|
||||||
|
}
|
||||||
|
Ok(st)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Permanently removes path (recursively for a directory) if it still
|
||||||
|
/// matches want. The check and the removal both go through descriptors
|
||||||
|
/// opened from the parent directory without following symlinks, so a
|
||||||
|
/// symlink swapped in since the scan cannot redirect the removal outside it.
|
||||||
|
pub fn delete(path: &Path, want: Expect) -> io::Result<()> {
|
||||||
|
check_abs(path)?;
|
||||||
|
let (dir, base) = split_parent(path)?;
|
||||||
|
let dfd = open_dir(dir.as_os_str().as_bytes())?;
|
||||||
|
let cbase = cstr(base.as_bytes())?;
|
||||||
|
let st = check_expect(dfd.as_raw_fd(), &cbase, want)?;
|
||||||
|
remove_at(
|
||||||
|
dfd.as_raw_fd(),
|
||||||
|
&cbase,
|
||||||
|
st.st_mode & libc::S_IFMT == libc::S_IFDIR,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_at(dirfd: RawFd, name: &CStr, is_dir: bool) -> io::Result<()> {
|
||||||
|
if !is_dir {
|
||||||
|
return cvt(unsafe { libc::unlinkat(dirfd, name.as_ptr(), 0) }).map(|_| ());
|
||||||
|
}
|
||||||
|
let fd = cvt(unsafe {
|
||||||
|
libc::openat(
|
||||||
|
dirfd,
|
||||||
|
name.as_ptr(),
|
||||||
|
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let sub = unsafe { OwnedFd::from_raw_fd(fd) };
|
||||||
|
// Collect names first: removing while iterating a DIR stream is unspecified.
|
||||||
|
let mut names = Vec::new();
|
||||||
|
let dup = cvt(unsafe { libc::dup(sub.as_raw_fd()) })?;
|
||||||
|
let dir = unsafe { libc::fdopendir(dup) };
|
||||||
|
if dir.is_null() {
|
||||||
|
unsafe { libc::close(dup) };
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
let ds = DirStream { dir };
|
||||||
|
loop {
|
||||||
|
let ent = unsafe { libc::readdir(ds.dir) };
|
||||||
|
if ent.is_null() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let n = unsafe { CStr::from_ptr((*ent).d_name.as_ptr()) };
|
||||||
|
if n.to_bytes() != b"." && n.to_bytes() != b".." {
|
||||||
|
let t = unsafe { (*ent).d_type };
|
||||||
|
let is_dir = match t {
|
||||||
|
libc::DT_UNKNOWN => fstat_at(sub.as_raw_fd(), n, libc::AT_SYMLINK_NOFOLLOW)
|
||||||
|
.is_ok_and(|m| mode::is_dir(m.mode)),
|
||||||
|
t => t == libc::DT_DIR,
|
||||||
|
};
|
||||||
|
names.push((n.to_owned(), is_dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(ds);
|
||||||
|
for (n, d) in names {
|
||||||
|
remove_at(sub.as_raw_fd(), &n, d)?;
|
||||||
|
}
|
||||||
|
cvt(unsafe { libc::unlinkat(dirfd, name.as_ptr(), libc::AT_REMOVEDIR) }).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves path to dst if it still matches want. The parent directory is
|
||||||
|
/// opened once and both the check and the rename are relative to that
|
||||||
|
/// descriptor, so a parent swapped for a symlink since the scan (or between
|
||||||
|
/// check and rename) cannot redirect the move to another user's file.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
fn rename_checked(path: &Path, dst: &Path, want: Expect) -> io::Result<()> {
|
||||||
|
let (dir, base) = split_parent(path)?;
|
||||||
|
let dfd = open_dir(dir.as_os_str().as_bytes())?;
|
||||||
|
let cbase = cstr(base.as_bytes())?;
|
||||||
|
check_expect(dfd.as_raw_fd(), &cbase, want)?;
|
||||||
|
let cdst = cstr(dst.as_os_str().as_bytes())?;
|
||||||
|
cvt(unsafe {
|
||||||
|
libc::renameat(
|
||||||
|
dfd.as_raw_fd(),
|
||||||
|
cbase.as_ptr(),
|
||||||
|
libc::AT_FDCWD,
|
||||||
|
cdst.as_ptr(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates dir (mode 0700) if missing; an existing entry must be a real
|
||||||
|
/// directory (not a symlink) owned by us. With strict (trash on a shared
|
||||||
|
/// filesystem) it must also be inaccessible to others, as the freedesktop.org
|
||||||
|
/// spec requires; home trash directories made by desktop environments are
|
||||||
|
/// sometimes 0755, which is harmless there.
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||||
|
fn ensure_private_dir(dir: &Path, strict: bool) -> io::Result<()> {
|
||||||
|
use std::os::unix::fs::{DirBuilderExt, MetadataExt};
|
||||||
|
if let Err(e) = std::fs::DirBuilder::new().mode(0o700).create(dir) {
|
||||||
|
if e.kind() != io::ErrorKind::AlreadyExists {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let md = std::fs::symlink_metadata(dir)?;
|
||||||
|
if !md.is_dir() {
|
||||||
|
return Err(other("not a directory (symlink?)"));
|
||||||
|
}
|
||||||
|
if md.uid() != unsafe { libc::getuid() } {
|
||||||
|
return Err(other("owned by another user"));
|
||||||
|
}
|
||||||
|
if strict && md.mode() & 0o077 != 0 {
|
||||||
|
return Err(other("accessible to other users"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether there is no graphical session (no X or Wayland display), so there
|
||||||
|
/// is no file manager to open and no desktop trash anyone will empty.
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub fn headless() -> bool {
|
||||||
|
std::env::var_os("DISPLAY").is_none_or(|v| v.is_empty())
|
||||||
|
&& std::env::var_os("WAYLAND_DISPLAY").is_none_or(|v| v.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub use linux::*;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux {
|
||||||
|
use super::*;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// The user-facing name of the trash.
|
||||||
|
pub const TRASH_NAME: &str = "trash";
|
||||||
|
|
||||||
|
/// Opens the item's directory in the desktop file manager.
|
||||||
|
pub fn reveal(path: &Path) -> io::Result<()> {
|
||||||
|
check_abs(path)?;
|
||||||
|
let md = std::fs::symlink_metadata(path)?;
|
||||||
|
let target = if md.is_dir() {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
path.parent().unwrap_or(path)
|
||||||
|
};
|
||||||
|
crate::platform::start_detached("xdg-open", &[target.as_os_str()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The top directory of the filesystem containing path.
|
||||||
|
fn mount_top(path: &Path, dev: u64) -> PathBuf {
|
||||||
|
let mut cur = path.parent().unwrap_or(path).to_path_buf();
|
||||||
|
loop {
|
||||||
|
let Some(parent) = cur.parent() else {
|
||||||
|
return cur;
|
||||||
|
};
|
||||||
|
match std::fs::metadata(parent) {
|
||||||
|
Ok(m) if m.dev() == dev => cur = parent.to_path_buf(),
|
||||||
|
_ => return cur,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn percent_encode_path(p: &[u8]) -> String {
|
||||||
|
let mut s = String::with_capacity(p.len());
|
||||||
|
for &b in p {
|
||||||
|
if b.is_ascii_alphanumeric() || b"-._~/!$&'()*+,;=:@".contains(&b) {
|
||||||
|
s.push(b as char);
|
||||||
|
} else {
|
||||||
|
s.push_str(&format!("%{b:02X}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_timestamp() -> String {
|
||||||
|
let now = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let t = crate::platform::local_time(now.as_secs() as i64);
|
||||||
|
format!(
|
||||||
|
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
|
||||||
|
t.year, t.month, t.day, t.hour, t.min, t.sec
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves path to the freedesktop.org trash: the home trash when on the
|
||||||
|
/// same filesystem, otherwise $topdir/.Trash-$uid on the item's
|
||||||
|
/// filesystem. It never copies across filesystems and never deletes.
|
||||||
|
pub fn trash(path: &Path, want: Expect) -> io::Result<()> {
|
||||||
|
check_abs(path)?;
|
||||||
|
let dev = std::fs::symlink_metadata(path)?.dev();
|
||||||
|
let data_home = match std::env::var_os("XDG_DATA_HOME").filter(|v| !v.is_empty()) {
|
||||||
|
Some(d) => PathBuf::from(d),
|
||||||
|
None => {
|
||||||
|
let home = std::env::var_os("HOME")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.ok_or_else(|| other("$HOME is not defined"))?;
|
||||||
|
PathBuf::from(home).join(".local/share")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let home = data_home.join("Trash");
|
||||||
|
let (trash, info_path): (PathBuf, Vec<u8>) = match std::fs::metadata(&data_home) {
|
||||||
|
Ok(m) if m.dev() == dev => (home.clone(), path.as_os_str().as_bytes().to_vec()),
|
||||||
|
_ => {
|
||||||
|
let top = mount_top(path, dev);
|
||||||
|
let rel = path.strip_prefix(&top).map_err(|e| other(e.to_string()))?;
|
||||||
|
(
|
||||||
|
top.join(format!(".Trash-{}", unsafe { libc::getuid() })),
|
||||||
|
rel.as_os_str().as_bytes().to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let shared = trash != home;
|
||||||
|
if !shared {
|
||||||
|
use std::os::unix::fs::DirBuilderExt;
|
||||||
|
std::fs::DirBuilder::new()
|
||||||
|
.recursive(true)
|
||||||
|
.mode(0o700)
|
||||||
|
.create(&trash)?;
|
||||||
|
} else if let Err(e) = ensure_private_dir(&trash, true) {
|
||||||
|
// On a shared filesystem another user could plant a symlink or a
|
||||||
|
// world-writable directory here; refuse rather than follow it.
|
||||||
|
return Err(other(format!(
|
||||||
|
"trash directory {} is unsafe: {e}",
|
||||||
|
trash.display()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let (files, info) = (trash.join("files"), trash.join("info"));
|
||||||
|
for d in [&files, &info] {
|
||||||
|
ensure_private_dir(d, shared)
|
||||||
|
.map_err(|e| other(format!("trash directory {} is unsafe: {e}", d.display())))?;
|
||||||
|
}
|
||||||
|
let base = path.file_name().ok_or_else(|| other("no file name"))?;
|
||||||
|
for i in 1..10000 {
|
||||||
|
let mut name = base.to_os_string();
|
||||||
|
if i > 1 {
|
||||||
|
name.push(format!(".{i}"));
|
||||||
|
}
|
||||||
|
let dst = files.join(&name);
|
||||||
|
if std::fs::symlink_metadata(&dst).is_ok() {
|
||||||
|
continue; // never overwrite an earlier trashed item (even without .trashinfo)
|
||||||
|
}
|
||||||
|
let mut info_name = name.clone();
|
||||||
|
info_name.push(".trashinfo");
|
||||||
|
let ip = info.join(info_name);
|
||||||
|
let mut f = match create_private(&ip) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
let w = write!(
|
||||||
|
f,
|
||||||
|
"[Trash Info]\nPath={}\nDeletionDate={}\n",
|
||||||
|
percent_encode_path(&info_path),
|
||||||
|
local_timestamp()
|
||||||
|
)
|
||||||
|
.and_then(|_| f.sync_all());
|
||||||
|
drop(f);
|
||||||
|
if let Err(e) = w.and_then(|_| rename_checked(path, &dst, want)) {
|
||||||
|
let _ = std::fs::remove_file(&ip);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
Err(other("trash: too many items with the same name"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub use macos::*;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// The user-facing name of the trash.
|
||||||
|
pub const TRASH_NAME: &str = "Trash";
|
||||||
|
|
||||||
|
/// Selects the item in Finder.
|
||||||
|
pub fn reveal(path: &Path) -> io::Result<()> {
|
||||||
|
check_abs(path)?;
|
||||||
|
crate::platform::start_detached("open", &[OsStr::new("-R"), path.as_os_str()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Always false: macOS has Finder and the Trash without an X display.
|
||||||
|
pub fn headless() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "/Volumes/<name>" for a path on a mounted volume, else None.
|
||||||
|
fn volume_of(path: &Path) -> Option<PathBuf> {
|
||||||
|
let rest = path.strip_prefix("/Volumes/").ok()?;
|
||||||
|
rest.components()
|
||||||
|
.next()
|
||||||
|
.map(|c| Path::new("/Volumes").join(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves path into ~/.Trash (same volume) or the volume's .Trashes/<uid>
|
||||||
|
/// directory. It never copies and never deletes.
|
||||||
|
pub fn trash(path: &Path, want: Expect) -> io::Result<()> {
|
||||||
|
check_abs(path)?;
|
||||||
|
let home = std::env::var_os("HOME")
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.ok_or_else(|| other("$HOME is not defined"))?;
|
||||||
|
let mut dirs = vec![PathBuf::from(home).join(".Trash")];
|
||||||
|
if let Some(vol) = volume_of(path) {
|
||||||
|
dirs.push(
|
||||||
|
vol.join(".Trashes")
|
||||||
|
.join(unsafe { libc::getuid() }.to_string()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let base = path.file_name().ok_or_else(|| other("no file name"))?;
|
||||||
|
for (vi, dir) in dirs.iter().enumerate() {
|
||||||
|
if dir
|
||||||
|
.parent()
|
||||||
|
.is_some_and(|p| std::fs::create_dir_all(p).is_err())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A volume trash is on a shared filesystem: another user could
|
||||||
|
// plant a symlink or an open directory there, so insist on a
|
||||||
|
// private one.
|
||||||
|
if ensure_private_dir(dir, vi > 0).is_err() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for i in 1..10000 {
|
||||||
|
let mut name = base.to_os_string();
|
||||||
|
if i > 1 {
|
||||||
|
name.push(format!(" {i}"));
|
||||||
|
}
|
||||||
|
let dst = dir.join(&name);
|
||||||
|
if std::fs::symlink_metadata(&dst).is_ok() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match rename_checked(path, &dst, want) {
|
||||||
|
Err(e) if e.raw_os_error() == Some(libc::EXDEV) => break, // try the next trash location
|
||||||
|
r => return r,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(other("trash: item is on a volume without a usable trash"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
pub use bsd::*;
|
||||||
|
|
||||||
|
/// FreeBSD, NetBSD and other Unix systems: no desktop trash or file
|
||||||
|
/// manager integration. Permanent delete is still available.
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
|
||||||
|
mod bsd {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// The user-facing name of the trash.
|
||||||
|
pub const TRASH_NAME: &str = "trash";
|
||||||
|
|
||||||
|
/// Unsupported on this platform.
|
||||||
|
pub fn reveal(_: &Path) -> io::Result<()> {
|
||||||
|
Err(other("not supported on this platform"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unsupported on this platform.
|
||||||
|
pub fn trash(_: &Path, _: Expect) -> io::Result<()> {
|
||||||
|
Err(other("trash not supported on this platform"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn uid() -> u32 {
|
||||||
|
unsafe { libc::getuid() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tmp(tag: &str) -> std::path::PathBuf {
|
||||||
|
let d = std::env::temp_dir().join(format!("mapsize-{tag}-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
|
std::fs::create_dir_all(&d).unwrap();
|
||||||
|
d
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delete_refuses_changed_type() {
|
||||||
|
let d = tmp("del");
|
||||||
|
std::fs::create_dir_all(d.join("sub/inner")).unwrap();
|
||||||
|
std::fs::write(d.join("sub/inner/f"), b"x").unwrap();
|
||||||
|
// A symlink inside must be removed, not followed.
|
||||||
|
std::fs::write(d.join("keep"), b"k").unwrap();
|
||||||
|
std::os::unix::fs::symlink(d.join("keep"), d.join("sub/link")).unwrap();
|
||||||
|
assert!(delete(
|
||||||
|
&d.join("sub"),
|
||||||
|
Expect {
|
||||||
|
uid: uid(),
|
||||||
|
mode: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
delete(
|
||||||
|
&d.join("sub"),
|
||||||
|
Expect {
|
||||||
|
uid: uid(),
|
||||||
|
mode: mode::DIR,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(!d.join("sub").exists());
|
||||||
|
assert!(d.join("keep").exists());
|
||||||
|
assert!(delete(Path::new("relative/path"), Expect { uid: 0, mode: 0 }).is_err());
|
||||||
|
std::fs::remove_dir_all(&d).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::unnecessary_cast)]
|
||||||
|
fn mode_bits() {
|
||||||
|
assert_eq!(
|
||||||
|
mode_from_unix(libc::S_IFDIR as u32 | 0o755),
|
||||||
|
mode::DIR | 0o755
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mode_from_unix(libc::S_IFCHR as u32 | 0o600),
|
||||||
|
mode::DEVICE | mode::CHAR_DEVICE | 0o600
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
mode_from_unix(libc::S_IFREG as u32 | libc::S_ISUID as u32 | 0o4755),
|
||||||
|
mode::SETUID | 0o755
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn ensure_private_dir_rejects_symlink_and_open_dir() {
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
let d = tmp("priv");
|
||||||
|
std::fs::create_dir(d.join("attacker")).unwrap();
|
||||||
|
std::os::unix::fs::symlink(d.join("attacker"), d.join(".Trash-1000")).unwrap();
|
||||||
|
assert!(ensure_private_dir(&d.join(".Trash-1000"), false).is_err());
|
||||||
|
std::fs::create_dir(d.join("open")).unwrap();
|
||||||
|
std::fs::set_permissions(d.join("open"), std::fs::Permissions::from_mode(0o777)).unwrap();
|
||||||
|
assert!(ensure_private_dir(&d.join("open"), true).is_err());
|
||||||
|
ensure_private_dir(&d.join("fresh"), true).unwrap();
|
||||||
|
std::fs::remove_dir_all(&d).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
//! Windows: std directory listing and metadata, Recycle Bin and Explorer.
|
||||||
|
//!
|
||||||
|
//! Allocated size is reported as unknown (it would cost an extra handle open
|
||||||
|
//! per file), and there is no inode identity, owner or link count: symlinks
|
||||||
|
//! are never followed and hard links are not deduplicated.
|
||||||
|
|
||||||
|
use super::{base_name, check_abs, mode, os_bytes, other, path_from_bytes, Expect, Meta};
|
||||||
|
use std::fs::{File, Metadata};
|
||||||
|
use std::io;
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
fn nanos(t: io::Result<SystemTime>) -> i64 {
|
||||||
|
match t {
|
||||||
|
Ok(t) => match t.duration_since(UNIX_EPOCH) {
|
||||||
|
Ok(d) => d.as_nanos() as i64,
|
||||||
|
Err(e) => -(e.duration().as_nanos() as i64),
|
||||||
|
},
|
||||||
|
Err(_) => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Permissions follow the read-only attribute (0444 or 0666, plus 0111 for
|
||||||
|
/// directories). Symlinks and junctions both report as symlinks.
|
||||||
|
fn mode_of(md: &Metadata) -> u32 {
|
||||||
|
let ft = md.file_type();
|
||||||
|
let mut m = if md.permissions().readonly() {
|
||||||
|
0o444
|
||||||
|
} else {
|
||||||
|
0o666
|
||||||
|
};
|
||||||
|
if ft.is_symlink() {
|
||||||
|
m |= mode::SYMLINK;
|
||||||
|
} else if ft.is_dir() {
|
||||||
|
m |= mode::DIR | 0o111;
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_metadata(md: &Metadata) -> Meta {
|
||||||
|
let size = md.len() as i64;
|
||||||
|
Meta {
|
||||||
|
mode: mode_of(md),
|
||||||
|
size,
|
||||||
|
alloc: size,
|
||||||
|
mtime: nanos(md.modified()),
|
||||||
|
btime: nanos(md.created()),
|
||||||
|
nlink: 1,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Streams the entries of a directory in chunks of up to `chunk`. Entry
|
||||||
|
/// metadata comes from the directory listing itself and does not follow
|
||||||
|
/// symlinks. Returning an error from f stops the read and returns it.
|
||||||
|
pub fn read_dir<E: From<io::Error>>(
|
||||||
|
path: &[u8],
|
||||||
|
chunk: usize,
|
||||||
|
mut f: impl FnMut(Vec<Meta>) -> Result<(), E>,
|
||||||
|
) -> Result<(), E> {
|
||||||
|
let mut out = Vec::with_capacity(chunk);
|
||||||
|
for de in std::fs::read_dir(path_from_bytes(path))? {
|
||||||
|
let de = de?;
|
||||||
|
let mut m = de
|
||||||
|
.metadata()
|
||||||
|
.map(|md| from_metadata(&md))
|
||||||
|
.unwrap_or_else(|e| Meta {
|
||||||
|
err: Some(e),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
m.name = os_bytes(&de.file_name()).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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn stat_path(path: &[u8], follow: bool) -> io::Result<Meta> {
|
||||||
|
let p = path_from_bytes(path);
|
||||||
|
let md = if follow {
|
||||||
|
std::fs::metadata(&p)
|
||||||
|
} else {
|
||||||
|
std::fs::symlink_metadata(&p)
|
||||||
|
}?;
|
||||||
|
let mut m = from_metadata(&md);
|
||||||
|
m.name = base_name(path).to_vec();
|
||||||
|
Ok(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Windows has no numeric uid/gid; names fall back to the number.
|
||||||
|
pub(super) fn lookup_os_name(_: bool, _: u32) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The system time zone's offset from UTC (seconds east) at unix time secs.
|
||||||
|
pub(super) fn local_offset(secs: i64) -> i32 {
|
||||||
|
use windows_sys::Win32::Foundation::SYSTEMTIME;
|
||||||
|
use windows_sys::Win32::System::Time::SystemTimeToTzSpecificLocalTime;
|
||||||
|
let (y, mo, d) = super::civil_from_days(secs.div_euclid(86400));
|
||||||
|
if !(1601..=30827).contains(&y) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let rem = secs.rem_euclid(86400);
|
||||||
|
let utc = SYSTEMTIME {
|
||||||
|
wYear: y as u16,
|
||||||
|
wMonth: mo as u16,
|
||||||
|
wDayOfWeek: 0,
|
||||||
|
wDay: d as u16,
|
||||||
|
wHour: (rem / 3600) as u16,
|
||||||
|
wMinute: (rem / 60 % 60) as u16,
|
||||||
|
wSecond: (rem % 60) as u16,
|
||||||
|
wMilliseconds: 0,
|
||||||
|
};
|
||||||
|
let mut local: SYSTEMTIME = unsafe { std::mem::zeroed() };
|
||||||
|
if unsafe { SystemTimeToTzSpecificLocalTime(std::ptr::null(), &utc, &mut local) } == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let l = super::days_from_civil(local.wYear as i64, local.wMonth as i64, local.wDay as i64)
|
||||||
|
* 86400
|
||||||
|
+ local.wHour as i64 * 3600
|
||||||
|
+ local.wMinute as i64 * 60
|
||||||
|
+ local.wSecond as i64;
|
||||||
|
(l - secs) as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host name, or "" if unknown.
|
||||||
|
pub fn hostname() -> String {
|
||||||
|
std::env::var("COMPUTERNAME").unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether path is the root of a pseudo filesystem. Windows has none.
|
||||||
|
pub fn is_virtual_fs(_: &[u8]) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a file whose content is about to be read; callers check the type.
|
||||||
|
pub fn open_content(path: &Path) -> io::Result<File> {
|
||||||
|
File::open(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new file (it inherits the directory's access control list).
|
||||||
|
pub fn create_private(path: &Path) -> io::Result<File> {
|
||||||
|
std::fs::OpenOptions::new()
|
||||||
|
.write(true)
|
||||||
|
.create_new(true)
|
||||||
|
.open(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The user-facing name of the trash.
|
||||||
|
pub const TRASH_NAME: &str = "Recycle Bin";
|
||||||
|
|
||||||
|
/// Always false: Windows always has Explorer and the Recycle Bin.
|
||||||
|
pub fn headless() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selects the item in Explorer.
|
||||||
|
pub fn reveal(path: &Path) -> io::Result<()> {
|
||||||
|
check_abs(path)?;
|
||||||
|
super::start_detached("explorer.exe", &["/select,".as_ref(), path.as_os_str()])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves path to the Recycle Bin (SHFileOperationW with FOF_ALLOWUNDO). It
|
||||||
|
/// never deletes permanently. want is not checked: the scan records no owner
|
||||||
|
/// or inode identity on Windows.
|
||||||
|
pub fn trash(path: &Path, _: Expect) -> io::Result<()> {
|
||||||
|
use windows_sys::Win32::UI::Shell::{
|
||||||
|
SHFileOperationW, FOF_ALLOWUNDO, FOF_NO_UI, FO_DELETE, SHFILEOPSTRUCTW,
|
||||||
|
};
|
||||||
|
check_abs(path)?;
|
||||||
|
// A list of paths, each NUL-terminated, ending with an extra NUL.
|
||||||
|
let mut from: Vec<u16> = path.as_os_str().encode_wide().collect();
|
||||||
|
if from.contains(&0) {
|
||||||
|
return Err(other("path contains NUL"));
|
||||||
|
}
|
||||||
|
from.extend([0, 0]);
|
||||||
|
let mut op: SHFILEOPSTRUCTW = unsafe { std::mem::zeroed() };
|
||||||
|
op.wFunc = FO_DELETE;
|
||||||
|
op.pFrom = from.as_ptr();
|
||||||
|
op.fFlags = (FOF_ALLOWUNDO | FOF_NO_UI) as u16;
|
||||||
|
let r = unsafe { SHFileOperationW(&mut op) };
|
||||||
|
if r != 0 {
|
||||||
|
return Err(other(format!("SHFileOperation failed: code {r}")));
|
||||||
|
}
|
||||||
|
if op.fAnyOperationsAborted != 0 {
|
||||||
|
return Err(other("operation aborted"));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Permanent delete is not offered on Windows; items go to the Recycle Bin.
|
||||||
|
pub fn delete(_: &Path, _: Expect) -> io::Result<()> {
|
||||||
|
Err(other("permanent delete not supported on Windows"))
|
||||||
|
}
|
||||||
+10
-5
@@ -484,7 +484,8 @@ impl Controller {
|
|||||||
self.excludes.iter().any(|(is_path, p)| {
|
self.excludes.iter().any(|(is_path, p)| {
|
||||||
if *is_path {
|
if *is_path {
|
||||||
path == p.as_slice()
|
path == p.as_slice()
|
||||||
|| path.starts_with(p) && path.get(p.len()) == Some(&b'/')
|
|| path.starts_with(p)
|
||||||
|
&& path.get(p.len()).is_some_and(|&b| platform::is_sep(b))
|
||||||
|| platform::glob_match(p, path)
|
|| platform::glob_match(p, path)
|
||||||
} else {
|
} else {
|
||||||
platform::glob_match(p, name)
|
platform::glob_match(p, name)
|
||||||
@@ -662,7 +663,6 @@ fn err_msg(e: &io::Error) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub struct TempDir(pub PathBuf);
|
pub struct TempDir(pub PathBuf);
|
||||||
@@ -716,7 +716,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_scan(root: &Path, o: Options) -> Tree {
|
pub fn run_scan(root: &Path, o: Options) -> Tree {
|
||||||
let s = start(root.as_os_str().as_bytes(), o, Cancel::new()).unwrap();
|
let s = start(root.as_os_str().as_encoded_bytes(), o, Cancel::new()).unwrap();
|
||||||
s.done()
|
s.done()
|
||||||
.recv_timeout(std::time::Duration::from_secs(60))
|
.recv_timeout(std::time::Duration::from_secs(60))
|
||||||
.expect_err("scan did not finish (deadlock?)");
|
.expect_err("scan did not finish (deadlock?)");
|
||||||
@@ -797,7 +797,7 @@ pub(crate) mod tests {
|
|||||||
mk_tree(&root.0, 8, 3, 2);
|
mk_tree(&root.0, 8, 3, 2);
|
||||||
let c = Cancel::new();
|
let c = Cancel::new();
|
||||||
let s = start(
|
let s = start(
|
||||||
root.0.as_os_str().as_bytes(),
|
root.0.as_os_str().as_encoded_bytes(),
|
||||||
Options {
|
Options {
|
||||||
workers: 2,
|
workers: 2,
|
||||||
job_buffer: 1,
|
job_buffer: 1,
|
||||||
@@ -816,6 +816,7 @@ pub(crate) mod tests {
|
|||||||
assert!(t.stats.cancelled && t.stats.incomplete());
|
assert!(t.stats.cancelled && t.stats.incomplete());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn symlink_loops() {
|
fn symlink_loops() {
|
||||||
let root = TempDir::new("loop");
|
let root = TempDir::new("loop");
|
||||||
@@ -845,6 +846,7 @@ pub(crate) mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn hardlinks_counted_once() {
|
fn hardlinks_counted_once() {
|
||||||
let root = TempDir::new("hl");
|
let root = TempDir::new("hl");
|
||||||
@@ -863,6 +865,7 @@ pub(crate) mod tests {
|
|||||||
assert!(t.node(0).tot_size < 200000);
|
assert!(t.node(0).tot_size < 200000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn permission_errors_surface() {
|
fn permission_errors_surface() {
|
||||||
if unsafe { libc::geteuid() } == 0 {
|
if unsafe { libc::geteuid() } == 0 {
|
||||||
@@ -873,7 +876,7 @@ pub(crate) mod tests {
|
|||||||
let locked = root.0.join("locked");
|
let locked = root.0.join("locked");
|
||||||
std::fs::create_dir(&locked).unwrap();
|
std::fs::create_dir(&locked).unwrap();
|
||||||
std::fs::write(locked.join("f"), b"x").unwrap();
|
std::fs::write(locked.join("f"), b"x").unwrap();
|
||||||
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0)).unwrap();
|
std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
|
||||||
let t = run_scan(
|
let t = run_scan(
|
||||||
&root.0,
|
&root.0,
|
||||||
Options {
|
Options {
|
||||||
@@ -907,8 +910,10 @@ pub(crate) mod tests {
|
|||||||
assert_eq!((t.stats.excluded, t.node(0).files), (2, 1));
|
assert_eq!((t.stats.excluded, t.node(0).files), (2, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn hostile_names_preserved() {
|
fn hostile_names_preserved() {
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
let root = TempDir::new("names");
|
let root = TempDir::new("names");
|
||||||
let names: [&[u8]; 6] = [
|
let names: [&[u8]; 6] = [
|
||||||
"日本語.txt".as_bytes(),
|
"日本語.txt".as_bytes(),
|
||||||
|
|||||||
+7
-55
@@ -34,13 +34,13 @@ use crate::brand;
|
|||||||
use crate::inventory::{
|
use crate::inventory::{
|
||||||
Category, ErrKind, ErrorRecord, Kind, NodeId, Tree, MAX_ERROR_RECORDS, NUM_CATEGORIES,
|
Category, ErrKind, ErrorRecord, Kind, NodeId, Tree, MAX_ERROR_RECORDS, NUM_CATEGORIES,
|
||||||
};
|
};
|
||||||
|
use crate::platform::{civil_from_days, days_from_civil};
|
||||||
use crossbeam_channel::Receiver;
|
use crossbeam_channel::Receiver;
|
||||||
use flate2::{bufread::GzDecoder, write::GzEncoder, Compression};
|
use flate2::{bufread::GzDecoder, write::GzEncoder, Compression};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::fs::{self, File, OpenOptions};
|
use std::fs::{self, File};
|
||||||
use std::io::{self, BufReader, BufWriter, Read, Write};
|
use std::io::{self, BufReader, BufWriter, Read, Write};
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ pub fn save(out: &mut dyn Write, t: &Tree) -> io::Result<()> {
|
|||||||
one_file_system: st.one_file_system,
|
one_file_system: st.one_file_system,
|
||||||
follow: st.follow.clone(),
|
follow: st.follow.clone(),
|
||||||
workers: st.workers as i64,
|
workers: st.workers as i64,
|
||||||
host: hostname(),
|
host: crate::platform::hostname(),
|
||||||
generator: format!("{} {}", brand::NAME, brand::VERSION),
|
generator: format!("{} {}", brand::NAME, brand::VERSION),
|
||||||
};
|
};
|
||||||
let mb = serde_json::to_vec(&m)?;
|
let mb = serde_json::to_vec(&m)?;
|
||||||
@@ -245,12 +245,7 @@ fn create_temp(dir: &Path) -> io::Result<(PathBuf, File)> {
|
|||||||
".mapsize-{}.tmp",
|
".mapsize-{}.tmp",
|
||||||
seed.wrapping_add(i.wrapping_mul(7919))
|
seed.wrapping_add(i.wrapping_mul(7919))
|
||||||
));
|
));
|
||||||
match OpenOptions::new()
|
match crate::platform::create_private(&p) {
|
||||||
.write(true)
|
|
||||||
.create_new(true)
|
|
||||||
.mode(0o600)
|
|
||||||
.open(&p)
|
|
||||||
{
|
|
||||||
Ok(f) => return Ok((p, f)),
|
Ok(f) => return Ok((p, f)),
|
||||||
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
|
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
@@ -262,15 +257,6 @@ fn create_temp(dir: &Path) -> io::Result<(PathBuf, File)> {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hostname() -> String {
|
|
||||||
let mut b = [0u8; 256];
|
|
||||||
if unsafe { libc::gethostname(b.as_mut_ptr() as *mut libc::c_char, b.len()) } != 0 {
|
|
||||||
return String::new();
|
|
||||||
}
|
|
||||||
let n = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
|
||||||
String::from_utf8_lossy(&b[..n]).into_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The decompressed body, fed in chunks by the decompressing thread.
|
/// The decompressed body, fed in chunks by the decompressing thread.
|
||||||
struct Body {
|
struct Body {
|
||||||
rx: Receiver<Result<Vec<u8>, String>>,
|
rx: Receiver<Result<Vec<u8>, String>>,
|
||||||
@@ -674,7 +660,7 @@ impl GoTime {
|
|||||||
|
|
||||||
/// The same instant in the local time zone (as Go's time.Now()).
|
/// The same instant in the local time zone (as Go's time.Now()).
|
||||||
fn local(self) -> GoTime {
|
fn local(self) -> GoTime {
|
||||||
let off = localtime(self.secs).map_or(0, |tm| tm.tm_gmtoff as i32);
|
let off = crate::platform::local_time(self.secs).off;
|
||||||
GoTime { off, ..self }
|
GoTime { off, ..self }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -785,26 +771,14 @@ impl GoTime {
|
|||||||
|
|
||||||
/// "2006-01-02 15:04" in local time.
|
/// "2006-01-02 15:04" in local time.
|
||||||
fn local_minutes(self) -> String {
|
fn local_minutes(self) -> String {
|
||||||
let Some(tm) = localtime(self.secs) else {
|
let t = crate::platform::local_time(self.secs);
|
||||||
return self.format();
|
|
||||||
};
|
|
||||||
format!(
|
format!(
|
||||||
"{:04}-{:02}-{:02} {:02}:{:02}",
|
"{:04}-{:02}-{:02} {:02}:{:02}",
|
||||||
tm.tm_year as i64 + 1900,
|
t.year, t.month, t.day, t.hour, t.min
|
||||||
tm.tm_mon + 1,
|
|
||||||
tm.tm_mday,
|
|
||||||
tm.tm_hour,
|
|
||||||
tm.tm_min
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn localtime(secs: i64) -> Option<libc::tm> {
|
|
||||||
let t = secs as libc::time_t;
|
|
||||||
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
|
|
||||||
(!unsafe { libc::localtime_r(&t, &mut tm) }.is_null()).then_some(tm)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Serialize for GoTime {
|
impl Serialize for GoTime {
|
||||||
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
||||||
s.serialize_str(&self.format())
|
s.serialize_str(&self.format())
|
||||||
@@ -822,28 +796,6 @@ impl<'de> Deserialize<'de> for GoTime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Civil-from-days (Howard Hinnant).
|
|
||||||
fn civil_from_days(days: i64) -> (i64, i64, i64) {
|
|
||||||
let z = days + 719468;
|
|
||||||
let era = z.div_euclid(146097);
|
|
||||||
let doe = z - era * 146097;
|
|
||||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
||||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
||||||
let mp = (5 * doy + 2) / 153;
|
|
||||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
|
||||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
||||||
(yoe + era * 400 + (m <= 2) as i64, m, d)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + 9) % 12) + 2) / 5 + d - 1;
|
|
||||||
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
||||||
era * 146097 + doe - 719468
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+1
-1
@@ -240,7 +240,7 @@ pub fn count(n: i64) -> String {
|
|||||||
out.push('-');
|
out.push('-');
|
||||||
}
|
}
|
||||||
for (i, c) in s.chars().enumerate() {
|
for (i, c) in s.chars().enumerate() {
|
||||||
if i > 0 && (s.len() - i) % 3 == 0 {
|
if i > 0 && (s.len() - i).is_multiple_of(3) {
|
||||||
out.push(',');
|
out.push(',');
|
||||||
}
|
}
|
||||||
out.push(c);
|
out.push(c);
|
||||||
|
|||||||
+2
-4
@@ -10,8 +10,6 @@ use crate::inventory::{NodeId, SizeMode, Tree, NO_NODE};
|
|||||||
use crate::platform;
|
use crate::platform;
|
||||||
use crate::snapshot;
|
use crate::snapshot;
|
||||||
use crate::textutil;
|
use crate::textutil;
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
@@ -232,9 +230,9 @@ impl Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_snapshot_name(root: &[u8]) -> String {
|
pub(crate) fn default_snapshot_name(root: &[u8]) -> String {
|
||||||
let p = Path::new(OsStr::from_bytes(root));
|
let p = crate::platform::path_from_bytes(root);
|
||||||
let base = match p.file_name() {
|
let base = match p.file_name() {
|
||||||
Some(b) => String::from_utf8_lossy(b.as_bytes()).into_owned(),
|
Some(b) => b.to_string_lossy().into_owned(),
|
||||||
None => "root".into(),
|
None => "root".into(),
|
||||||
};
|
};
|
||||||
let wd = std::env::current_dir().unwrap_or_default();
|
let wd = std::env::current_dir().unwrap_or_default();
|
||||||
|
|||||||
+5
-22
@@ -55,22 +55,14 @@ pub(crate) fn san(b: &[u8]) -> String {
|
|||||||
textutil::sanitize_bytes(b)
|
textutil::sanitize_bytes(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_tm(unix_nanos: i64) -> libc::tm {
|
fn local_tm(unix_nanos: i64) -> crate::platform::LocalTime {
|
||||||
let secs = unix_nanos.div_euclid(1_000_000_000) as libc::time_t;
|
crate::platform::local_time(unix_nanos.div_euclid(1_000_000_000))
|
||||||
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
|
|
||||||
unsafe { libc::localtime_r(&secs, &mut tm) };
|
|
||||||
tm
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a timestamp as a local date ("2006-01-02").
|
/// Formats a timestamp as a local date ("2006-01-02").
|
||||||
pub(crate) fn fmt_date(unix_nanos: i64) -> String {
|
pub(crate) fn fmt_date(unix_nanos: i64) -> String {
|
||||||
let t = local_tm(unix_nanos);
|
let t = local_tm(unix_nanos);
|
||||||
format!(
|
format!("{:04}-{:02}-{:02}", t.year, t.month, t.day)
|
||||||
"{:04}-{:02}-{:02}",
|
|
||||||
t.tm_year + 1900,
|
|
||||||
t.tm_mon + 1,
|
|
||||||
t.tm_mday
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats a timestamp as local date and time ("2006-01-02 15:04:05").
|
/// Formats a timestamp as local date and time ("2006-01-02 15:04:05").
|
||||||
@@ -78,12 +70,7 @@ pub(crate) fn fmt_time(unix_nanos: i64) -> String {
|
|||||||
let t = local_tm(unix_nanos);
|
let t = local_tm(unix_nanos);
|
||||||
format!(
|
format!(
|
||||||
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
|
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
|
||||||
t.tm_year + 1900,
|
t.year, t.month, t.day, t.hour, t.min, t.sec
|
||||||
t.tm_mon + 1,
|
|
||||||
t.tm_mday,
|
|
||||||
t.tm_hour,
|
|
||||||
t.tm_min,
|
|
||||||
t.tm_sec
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,10 +82,6 @@ pub(crate) fn fmt_stamp_now() -> String {
|
|||||||
let t = local_tm(now.as_nanos() as i64);
|
let t = local_tm(now.as_nanos() as i64);
|
||||||
format!(
|
format!(
|
||||||
"{:04}{:02}{:02}-{:02}{:02}",
|
"{:04}{:02}{:02}-{:02}{:02}",
|
||||||
t.tm_year + 1900,
|
t.year, t.month, t.day, t.hour, t.min
|
||||||
t.tm_mon + 1,
|
|
||||||
t.tm_mday,
|
|
||||||
t.tm_hour,
|
|
||||||
t.tm_min
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-5
@@ -21,6 +21,7 @@ use crate::config::Settings;
|
|||||||
use crate::duplicate;
|
use crate::duplicate;
|
||||||
use crate::filter::FilterResult;
|
use crate::filter::FilterResult;
|
||||||
use crate::inventory::{NodeId, SizeMode, Tree};
|
use crate::inventory::{NodeId, SizeMode, Tree};
|
||||||
|
use crate::platform;
|
||||||
use crate::scan::Scanner;
|
use crate::scan::Scanner;
|
||||||
use crate::snapshot;
|
use crate::snapshot;
|
||||||
use crate::textutil;
|
use crate::textutil;
|
||||||
@@ -612,17 +613,20 @@ impl Model {
|
|||||||
let root = t.path_bytes(0);
|
let root = t.path_bytes(0);
|
||||||
let rel: &[u8] = if pz == root {
|
let rel: &[u8] = if pz == root {
|
||||||
b""
|
b""
|
||||||
} else if let Some(r) = pz
|
} else if let Some(r) = pz.strip_prefix(root.as_slice()).filter(|r| {
|
||||||
.strip_prefix(root.as_slice())
|
root.last().is_some_and(|&b| platform::is_sep(b))
|
||||||
.filter(|r| root.ends_with(b"/") || r.starts_with(b"/"))
|
|| r.first().is_some_and(|&b| platform::is_sep(b))
|
||||||
{
|
}) {
|
||||||
r
|
r
|
||||||
} else {
|
} else {
|
||||||
self.pending_zoom = None;
|
self.pending_zoom = None;
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let mut id = 0;
|
let mut id = 0;
|
||||||
for part in rel.split(|&b| b == b'/').filter(|p| !p.is_empty()) {
|
for part in rel
|
||||||
|
.split(|&b| platform::is_sep(b))
|
||||||
|
.filter(|p| !p.is_empty())
|
||||||
|
{
|
||||||
match t.children(id).find(|(_, n)| &*n.name == part) {
|
match t.children(id).find(|(_, n)| &*n.name == part) {
|
||||||
Some((c, _)) => id = c,
|
Some((c, _)) => id = c,
|
||||||
None => return, // not discovered yet
|
None => return, // not discovered yet
|
||||||
|
|||||||
+3
-3
@@ -272,9 +272,9 @@ impl Model {
|
|||||||
// Drop leading segments until the rest fits; keep at least the last.
|
// Drop leading segments until the rest fits; keep at least the last.
|
||||||
let total = |from: usize| {
|
let total = |from: usize| {
|
||||||
let mut w = 0;
|
let mut w = 0;
|
||||||
for i in from..segs.len() {
|
for (k, (_, s)) in segs[from..].iter().enumerate() {
|
||||||
w += tw(&segs[i].1);
|
w += tw(s);
|
||||||
if i > from {
|
if k > 0 {
|
||||||
w += tw(sep);
|
w += tw(sep);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ const RESTORE: &str =
|
|||||||
fn write_raw(s: &str) {
|
fn write_raw(s: &str) {
|
||||||
let mut b = s.as_bytes();
|
let mut b = s.as_bytes();
|
||||||
while !b.is_empty() {
|
while !b.is_empty() {
|
||||||
let n = unsafe { libc::write(1, b.as_ptr().cast(), b.len()) };
|
let n = unsafe { libc::write(1, b.as_ptr().cast(), b.len().min(1 << 30) as _) };
|
||||||
if n <= 0 {
|
if n <= 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -11,7 +11,6 @@ use crate::inventory::{category_for, ext, Kind, NodeId, Tree, FLAG_SCANNED};
|
|||||||
use crate::scan;
|
use crate::scan;
|
||||||
use crate::textutil;
|
use crate::textutil;
|
||||||
use crate::treemap::Block;
|
use crate::treemap::Block;
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
@@ -630,7 +629,7 @@ fn live_scan_rendering() {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let path = root.0.as_os_str().as_bytes().to_vec();
|
let path = root.0.as_os_str().as_encoded_bytes().to_vec();
|
||||||
let start: StartFn = Arc::new(move |c: Cancel| {
|
let start: StartFn = Arc::new(move |c: Cancel| {
|
||||||
let s = scan::start(
|
let s = scan::start(
|
||||||
&path,
|
&path,
|
||||||
|
|||||||
Reference in New Issue
Block a user