use super::*;
#[cfg(unix)]
fn format_attributes(metadata: &fs::Metadata, is_dir: bool, is_symlink: bool) -> String {
use std::os::unix::fs::PermissionsExt;
let kind = if is_symlink {
'l'
} else if is_dir {
'd'
} else {
'-'
};
let mut text = String::with_capacity(10);
text.push(kind);
text.push_str(&mode_string(metadata.permissions().mode()));
text
}
pub fn mode_string(mode: u32) -> String {
let triplet = |shift: u32, special: u32, on: char| {
let bits = mode >> shift;
let execute = bits & 0o1 != 0;
[
if bits & 0o4 != 0 { 'r' } else { '-' },
if bits & 0o2 != 0 { 'w' } else { '-' },
match (mode & special != 0, execute) {
(true, true) => on,
(true, false) => on.to_ascii_uppercase(),
(false, true) => 'x',
(false, false) => '-',
},
]
};
let mut text = String::with_capacity(9);
text.extend(triplet(6, 0o4000, 's'));
text.extend(triplet(3, 0o2000, 's'));
text.extend(triplet(0, 0o1000, 't'));
text
}
#[cfg(windows)]
fn format_attributes(metadata: &fs::Metadata, _is_dir: bool, is_symlink: bool) -> String {
use std::os::windows::fs::MetadataExt;
const READONLY: u32 = 0x0000_0001;
const HIDDEN: u32 = 0x0000_0002;
const SYSTEM: u32 = 0x0000_0004;
const DIRECTORY: u32 = 0x0000_0010;
const ARCHIVE: u32 = 0x0000_0020;
let flags = metadata.file_attributes();
let flag = |bit: u32, on: char| if flags & bit != 0 { on } else { '-' };
let kind = if is_symlink { 'l' } else { flag(DIRECTORY, 'd') };
[kind, flag(ARCHIVE, 'a'), flag(READONLY, 'r'), flag(HIDDEN, 'h'), flag(SYSTEM, 's')].iter().collect()
}
#[cfg(not(any(unix, windows)))]
fn format_attributes(_metadata: &fs::Metadata, _is_dir: bool, _is_symlink: bool) -> String {
String::new()
}
#[cfg(windows)]
pub fn is_hidden(_name: &str, metadata: Option<&fs::Metadata>) -> bool {
use std::os::windows::fs::MetadataExt;
const HIDDEN: u32 = 0x0000_0002;
metadata.is_some_and(|metadata| metadata.file_attributes() & HIDDEN != 0)
}
#[cfg(not(windows))]
pub fn is_hidden(name: &str, _metadata: Option<&fs::Metadata>) -> bool {
name.starts_with('.')
}
fn parent_row() -> Item {
Item {
name_os: OsString::from(".."),
name_full: "..".to_string(),
name: "..".to_string(),
extension: String::new(),
is_dir: true,
is_symlink: false,
size: String::new(),
size_bytes: 0,
modified_at: None,
attributes: String::new(),
}
}
fn row(name_os: OsString, is_dir: bool, is_symlink: bool, size_bytes: u64, modified_at: Option<std::time::SystemTime>, attributes: String) -> Item {
let name_full = name_os.to_string_lossy().into_owned();
let as_path = Path::new(&name_os);
let name = if is_dir { name_full.clone() } else { as_path.file_stem().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default() };
let extension = if is_dir { String::new() } else { as_path.extension().map(|e| e.to_string_lossy().into_owned()).unwrap_or_default() };
let size_bytes = if is_dir { 0 } else { size_bytes };
let size = if is_dir { "<DIR>".to_string() } else { format_size(size_bytes) };
Item {
name_os,
name_full,
name,
extension,
is_dir,
is_symlink,
size,
size_bytes,
modified_at,
attributes,
}
}
fn flat_row(name_os: OsString, is_symlink: bool, size_bytes: u64, modified_at: Option<std::time::SystemTime>, attributes: String) -> Item {
let mut item = row(name_os, false, is_symlink, size_bytes, modified_at, attributes);
if let Some(dir) = Path::new(&item.name_os).parent().filter(|dir| !dir.as_os_str().is_empty()) {
item.name = format!("{}{}{}", dir.to_string_lossy(), std::path::MAIN_SEPARATOR, item.name);
}
item
}
pub fn load_flat_rows(path: &Path, options: &Options, cancel: &AtomicBool) -> Result<(Vec<Item>, bool), Error> {
let mut children = Vec::new();
if path.parent().is_some() {
children.push(parent_row());
}
let mut truncated = false;
let mut stack: Vec<(PathBuf, PathBuf, Option<u64>)> = vec![(path.to_path_buf(), PathBuf::new(), None)];
let mut top = true;
'walk: while let Some((dir, relative, parent_device)) = stack.pop() {
let device = device_of(&dir);
if !top && device != parent_device && is_pseudo_filesystem(&dir) {
continue;
}
let entries = match read_dir(&dir) {
Ok(entries) => entries,
Err(e) if top => return Err(e),
Err(_) => continue,
};
top = false;
for entry in entries {
if cancel.load(Ordering::Relaxed) {
return Err(Error::new(ErrorKind::Interrupted, "stopped"));
}
let Ok(entry) = entry else { continue };
let Ok(file_type) = entry.file_type() else { continue };
let name = entry.file_name();
let is_symlink = file_type.is_symlink();
let metadata = if is_symlink { fs::metadata(entry.path()).or_else(|_| entry.metadata()).ok() } else { entry.metadata().ok() };
if !options.show_hidden && is_hidden(&name.to_string_lossy(), metadata.as_ref()) {
continue;
}
if file_type.is_dir() {
stack.push((entry.path(), relative.join(&name), device));
continue;
}
if metadata.as_ref().is_some_and(|metadata| metadata.is_dir()) {
continue;
}
if children.len() >= crate::constants::FLAT_MAX + usize::from(path.parent().is_some()) {
truncated = true;
break 'walk;
}
let size_bytes = metadata.as_ref().map(|m| m.len()).unwrap_or(0);
let modified_at = metadata.as_ref().and_then(|m| m.modified().ok());
let attributes = metadata.as_ref().map(|metadata| format_attributes(metadata, false, is_symlink)).unwrap_or_default();
children.push(flat_row(relative.join(&name).into_os_string(), is_symlink, size_bytes, modified_at, attributes));
}
}
let sort_start = usize::from(path.parent().is_some());
children[sort_start..].sort_by(|a, b| compare_rows(a, b, options));
Ok((children, truncated))
}
pub fn archive_flat_rows(index: &crate::archive::Index, inner: &Path, options: &Options) -> Vec<Item> {
use crate::archive::EntryKind;
let hidden = |path: &Path| path.components().any(|part| part.as_os_str().as_encoded_bytes().starts_with(b"."));
let mut children = vec![parent_row()];
for at in index.subtree(inner) {
let entry = index.entry(at);
let Ok(relative) = entry.path.strip_prefix(inner) else { continue };
if entry.kind == EntryKind::Dir || relative.as_os_str().is_empty() || (!options.show_hidden && hidden(relative)) {
continue;
}
let kind = if entry.kind == EntryKind::Link { 'l' } else { '-' };
let attributes = entry.mode.map(|mode| format!("{kind}{}", mode_string(mode))).unwrap_or_default();
children.push(flat_row(relative.as_os_str().to_os_string(), entry.kind == EntryKind::Link, entry.size, entry.modified, attributes));
}
children[1..].sort_by(|a, b| compare_rows(a, b, options));
children
}
pub fn archive_rows(index: &crate::archive::Index, inner: &Path, options: &Options) -> Vec<Item> {
use crate::archive::EntryKind;
let mut children = vec![parent_row()];
for entry in index.children(inner) {
let name_os = entry.name().to_os_string();
if !options.show_hidden && name_os.as_encoded_bytes().starts_with(b".") {
continue;
}
let is_dir = entry.kind == EntryKind::Dir;
let is_symlink = entry.kind == EntryKind::Link;
let kind = match entry.kind {
EntryKind::Dir => 'd',
EntryKind::Link => 'l',
_ => '-',
};
let attributes = entry.mode.map(|mode| format!("{kind}{}", mode_string(mode))).unwrap_or_default();
children.push(row(name_os, is_dir, is_symlink, entry.size, entry.modified, attributes));
}
children[1..].sort_by(|a, b| compare_rows(a, b, options));
children
}
pub fn load_directory_rows(path: &Path, options: &Options) -> Result<Vec<Item>, Error> {
let entries: Vec<_> = read_dir(path)?.filter_map(|entry| entry.ok()).collect();
let has_parent = path.parent().is_some();
let mut children = Vec::with_capacity(entries.len() + usize::from(has_parent));
if has_parent {
children.push(parent_row());
}
for entry in &entries {
let entry_path = entry.path();
let is_symlink = entry.file_type().map(|file_type| file_type.is_symlink()).unwrap_or(false);
let metadata = if is_symlink { fs::metadata(&entry_path).or_else(|_| entry.metadata()).ok() } else { entry.metadata().ok() };
let is_dir = metadata.as_ref().map(|m| m.is_dir()).unwrap_or(false);
let name_os = entry.file_name();
if !options.show_hidden && is_hidden(&name_os.to_string_lossy(), metadata.as_ref()) {
continue;
}
let size_bytes = metadata.as_ref().map(|m| m.len()).unwrap_or(0);
let modified_at = metadata.as_ref().and_then(|m| m.modified().ok());
let attributes = metadata.as_ref().map(|metadata| format_attributes(metadata, is_dir, is_symlink)).unwrap_or_default();
children.push(row(name_os, is_dir, is_symlink, size_bytes, modified_at, attributes));
}
let sort_start = usize::from(has_parent);
children[sort_start..].sort_by(|a, b| compare_rows(a, b, options));
Ok(children)
}
fn compare_rows(a: &Item, b: &Item, options: &Options) -> std::cmp::Ordering {
use std::cmp::Ordering;
if options.dirs_first {
match (a.is_dir, b.is_dir) {
(true, false) => return Ordering::Less,
(false, true) => return Ordering::Greater,
_ => {}
}
}
let text = |a: &str, b: &str| if options.case_sensitive { a.cmp(b) } else { a.to_lowercase().cmp(&b.to_lowercase()) };
let name = || text(&a.name_full, &b.name_full);
let order = match options.sort_key {
SortKey::Name => name(),
SortKey::Extension => text(&a.extension, &b.extension).then_with(name),
SortKey::Size => a.size_bytes.cmp(&b.size_bytes).then_with(name),
SortKey::Modified => a.modified_at.cmp(&b.modified_at).then_with(name),
};
if options.sort_descending { order.reverse() } else { order }
}
pub fn git_branch(dir: &Path) -> Option<String> {
for ancestor in dir.ancestors() {
let dot_git = ancestor.join(".git");
let Ok(metadata) = fs::metadata(&dot_git) else {
continue;
};
let git_dir = if metadata.is_dir() {
dot_git
} else {
let text = fs::read_to_string(&dot_git).ok()?;
ancestor.join(text.trim().strip_prefix("gitdir:")?.trim())
};
let head = fs::read_to_string(git_dir.join("HEAD")).ok()?;
let head = head.trim();
return Some(match head.strip_prefix("ref:") {
Some(reference) => {
let reference = reference.trim();
reference.strip_prefix("refs/heads/").unwrap_or(reference).to_string()
}
None => head.chars().take(7).collect(),
});
}
None
}
#[cfg(unix)]
pub fn user_at_host() -> (String, bool) {
let uid = unsafe { libc::geteuid() };
let mut buffer = [0 as libc::c_char; 256];
let host = if unsafe { libc::gethostname(buffer.as_mut_ptr(), buffer.len() - 1) } == 0 {
unsafe { std::ffi::CStr::from_ptr(buffer.as_ptr()) }.to_string_lossy().into_owned()
} else {
String::new()
};
let host = host.split('.').next().unwrap_or_default().to_string();
(format!("{}@{}", user_name(uid), host), uid == 0)
}
#[cfg(not(unix))]
pub fn user_at_host() -> (String, bool) {
let variable = |name: &str| std::env::var(name).unwrap_or_default();
(format!("{}@{}", variable("USERNAME"), variable("COMPUTERNAME")), false)
}
pub fn calculate_dir_size(path: &Path) -> Result<u64, Error> {
SizeWalk {
cancel: &AtomicBool::new(false),
found: &mut |_| {},
linked: None,
}
.dir(path, None)
.map(Option::unwrap_or_default)
}
fn describe_in_archive(path: &Path) -> Option<Description> {
use crate::archive::EntryKind;
let (index, at) = crate::archive::entry_at(path)?;
let entry = index.entry(at);
let kind = match entry.kind {
EntryKind::Dir => 'd',
EntryKind::Link => 'l',
_ => '-',
};
Some(Description {
size_bytes: Some(if entry.kind == EntryKind::Dir { index.size_of(&entry.path) } else { entry.size }),
modified: entry.modified,
owner: entry.owner.clone().unwrap_or_default(),
attributes: [entry.mode.map(|mode| format!("{kind}{}", mode_string(mode))), entry.encrypted.then(|| "encrypted".to_string())]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" "),
link: entry.link.as_ref().map(|target| target.display().to_string()),
})
}
pub fn walk_dir_size(path: &Path, cancel: &AtomicBool, found: &mut dyn FnMut(u64)) -> Result<Option<u64>, Error> {
if path.symlink_metadata().is_err()
&& let Some((index, at)) = crate::archive::entry_at(path)
{
let size = index.size_of(&index.entry(at).path);
found(size);
return Ok(Some(size));
}
SizeWalk {
cancel,
found,
linked: Some(std::collections::HashSet::new()),
}
.dir(path, None)
}
struct SizeWalk<'a> {
cancel: &'a AtomicBool,
found: &'a mut dyn FnMut(u64),
linked: Option<std::collections::HashSet<(u64, u64)>>,
}
impl SizeWalk<'_> {
fn dir(&mut self, path: &Path, parent_device: Option<u64>) -> Result<Option<u64>, Error> {
let device = device_of(path);
if (parent_device.is_none() || device != parent_device) && is_pseudo_filesystem(path) {
return Ok(Some(0));
}
let mut total = 0u64;
for entry in read_dir(path)? {
if self.cancel.load(Ordering::Relaxed) {
return Ok(None);
}
let Ok(entry) = entry else { continue };
let Ok(file_type) = entry.file_type() else { continue };
if file_type.is_dir() {
match self.dir(&entry.path(), device) {
Ok(Some(size)) => total += size,
Ok(None) => return Ok(None),
Err(_) => {}
}
} else if let Ok(metadata) = entry.metadata()
&& self.first_sight(&metadata)
{
total += metadata.len();
(self.found)(metadata.len());
}
}
Ok(Some(total))
}
#[cfg(unix)]
fn first_sight(&mut self, metadata: &fs::Metadata) -> bool {
use std::os::unix::fs::MetadataExt;
match &mut self.linked {
Some(linked) if metadata.nlink() > 1 => linked.insert((metadata.dev(), metadata.ino())),
_ => true,
}
}
#[cfg(not(unix))]
fn first_sight(&mut self, _metadata: &fs::Metadata) -> bool {
true
}
}
#[cfg(unix)]
fn device_of(path: &Path) -> Option<u64> {
use std::os::unix::fs::MetadataExt;
path.symlink_metadata().ok().map(|metadata| metadata.dev())
}
#[cfg(not(unix))]
fn device_of(_path: &Path) -> Option<u64> {
None
}
#[cfg(target_os = "linux")]
fn is_pseudo_filesystem(path: &Path) -> bool {
use std::os::unix::ffi::OsStrExt;
const PSEUDO: [u32; 15] = [
0x9fa0, 0x6265_6572, 0x6462_6720, 0x7472_6163, 0x7363_6673, 0x0027_e0eb, 0x6367_7270, 0xcafe_4a11, 0x6165_676c, 0xde5e_81e4, 0x6265_6570, 0x1cd1, 0x6573_5543, 0x4249_4e4d, 0x1980_0202, ];
let Ok(c_path) = std::ffi::CString::new(path.as_os_str().as_bytes()) else {
return false;
};
let kind = unsafe {
let mut stats = std::mem::zeroed::<libc::statfs>();
if libc::statfs(c_path.as_ptr(), &mut stats) != 0 {
return false;
}
stats.f_type as u32
};
PSEUDO.contains(&kind)
}
#[cfg(not(target_os = "linux"))]
fn is_pseudo_filesystem(_path: &Path) -> bool {
false
}
pub struct Description {
pub size_bytes: Option<u64>,
pub modified: Option<std::time::SystemTime>,
pub owner: String,
pub attributes: String,
pub link: Option<String>,
}
pub fn describe(path: &Path, is_dir: bool) -> Option<Description> {
let Ok(metadata) = fs::symlink_metadata(path) else {
return describe_in_archive(path);
};
let is_symlink = metadata.file_type().is_symlink();
let followed = if is_symlink { fs::metadata(path).ok() } else { None };
let size_bytes = if is_symlink { followed.as_ref().map(|target| target.len()) } else { (!is_dir).then_some(metadata.len()) };
Some(Description {
size_bytes,
modified: metadata.modified().ok(),
owner: owner_of(&metadata),
attributes: format_attributes(&metadata, is_dir, is_symlink),
link: is_symlink.then(|| fs::read_link(path).ok()).flatten().map(|target| target.display().to_string()),
})
}