use std::ffi::OsString;
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::{Component, Path, PathBuf};
use super::{Result, WrapErr};
pub const FILELIST_MAX_BYTES: u64 = 50 << 20;
#[derive(Debug)]
pub struct Selection {
pub files: Vec<PathBuf>,
pub explicit: bool,
pub unreadable_dirs: usize,
}
pub fn select_inputs(
named: Vec<PathBuf>,
filelists: &[PathBuf],
recursive: bool,
follow_links: bool,
verbosity: i32,
) -> Result<Selection> {
let mut files = Vec::with_capacity(named.len());
let named_count = named.len();
for input in named {
if !follow_links && input != Path::new("-") && is_symlink(&input) && !is_fifo(&input) {
display!(
verbosity,
2,
"Warning : {} is a symbolic link, ignoring",
input.display()
);
continue;
}
files.push(input);
}
for list in filelists {
files.extend(read_filelist(list)?);
}
if files.is_empty() && named_count > 0 {
bail!("every named input is a symbolic link; pass -f to follow them");
}
let explicit = named_count > 0 || !filelists.is_empty();
let mut walk = Walk {
follow_links,
verbosity,
unreadable_dirs: 0,
};
if recursive {
let mut expanded = Vec::with_capacity(files.len());
for input in files {
match fs::metadata(&input) {
Ok(metadata) if metadata.is_dir() => {
let mut ancestors = Vec::new();
walk.descend(&input, &metadata, &mut expanded, &mut ancestors)?;
}
_ => expanded.push(input),
}
}
files = expanded;
}
Ok(Selection {
files,
explicit,
unreadable_dirs: walk.unreadable_dirs,
})
}
struct Walk {
follow_links: bool,
verbosity: i32,
unreadable_dirs: usize,
}
#[cfg(unix)]
type DirId = (u64, u64);
#[cfg(not(unix))]
type DirId = PathBuf;
#[cfg(unix)]
fn dir_id(path: &Path, metadata: &fs::Metadata) -> Option<DirId> {
use std::os::unix::fs::MetadataExt;
let _ = path;
Some((metadata.dev(), metadata.ino()))
}
#[cfg(not(unix))]
fn dir_id(path: &Path, metadata: &fs::Metadata) -> Option<DirId> {
let _ = metadata;
fs::canonicalize(path).ok()
}
impl Walk {
fn descend(
&mut self,
dir: &Path,
metadata: &fs::Metadata,
out: &mut Vec<PathBuf>,
ancestors: &mut Vec<DirId>,
) -> Result<()> {
let Some(id) = dir_id(dir, metadata) else {
return self.walk_directory(dir, out, ancestors);
};
if ancestors.contains(&id) {
display!(
self.verbosity,
2,
"Warning : {} leads back into a directory being walked, ignoring",
dir.display()
);
return Ok(());
}
ancestors.push(id);
let walked = self.walk_directory(dir, out, ancestors);
ancestors.pop();
walked
}
fn walk_directory(
&mut self,
dir: &Path,
out: &mut Vec<PathBuf>,
ancestors: &mut Vec<DirId>,
) -> Result<()> {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(err) => {
display!(
self.verbosity,
1,
"Cannot open directory '{}': {err}",
dir.display()
);
self.unreadable_dirs += 1;
return Ok(());
}
};
let mut names: Vec<OsString> = Vec::new();
for entry in entries {
match entry {
Ok(entry) => names.push(entry.file_name()),
Err(err) => bail!("readdir({}) error: {err}", dir.display()),
}
}
names.sort();
for name in names {
let path = dir.join(name);
if !self.follow_links && is_symlink(&path) {
display!(
self.verbosity,
2,
"Warning : {} is a symbolic link, ignoring",
path.display()
);
continue;
}
match fs::metadata(&path) {
Ok(metadata) if metadata.is_dir() => {
self.descend(&path, &metadata, out, ancestors)?;
}
_ => out.push(path),
}
}
Ok(())
}
}
fn is_symlink(path: &Path) -> bool {
fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_symlink())
}
fn is_fifo(path: &Path) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
fs::metadata(path).is_ok_and(|m| m.file_type().is_fifo())
}
#[cfg(not(unix))]
{
let _ = path;
false
}
}
fn read_filelist(list: &Path) -> Result<Vec<PathBuf>> {
let metadata =
fs::metadata(list).wrap_err_with(|| format!("error reading {}", list.display()))?;
if !metadata.is_file() {
bail!("error reading {}: not a regular file", list.display());
}
if metadata.len() > FILELIST_MAX_BYTES {
bail!(
"error reading {}: file list is larger than {} bytes",
list.display(),
FILELIST_MAX_BYTES
);
}
let file =
fs::File::open(list).wrap_err_with(|| format!("error reading {}", list.display()))?;
parse_filelist(file, FILELIST_MAX_BYTES, list)
}
fn parse_filelist(file: impl std::io::Read, limit: u64, list: &Path) -> Result<Vec<PathBuf>> {
let mut names = Vec::new();
let mut reader = BufReader::new(file.take(limit + 1));
let mut line = Vec::new();
let mut total: u64 = 0;
loop {
line.clear();
let read = reader
.read_until(b'\n', &mut line)
.wrap_err_with(|| format!("error reading {}", list.display()))?;
if read == 0 {
break;
}
total += read as u64;
if total > limit {
bail!(
"error reading {}: file list is larger than {limit} bytes",
list.display()
);
}
if line.last() == Some(&b'\n') {
line.pop();
}
if line.last() == Some(&b'\r') {
line.pop();
}
if line.is_empty() {
continue;
}
names.push(bytes_to_path(&line));
}
Ok(names)
}
#[cfg(unix)]
fn bytes_to_path(bytes: &[u8]) -> PathBuf {
use std::os::unix::ffi::OsStrExt;
PathBuf::from(std::ffi::OsStr::from_bytes(bytes))
}
#[cfg(not(unix))]
fn bytes_to_path(bytes: &[u8]) -> PathBuf {
PathBuf::from(String::from_utf8_lossy(bytes).into_owned())
}
pub fn flat_output_path(src: &Path, dir: &Path) -> PathBuf {
dir.join(src.file_name().unwrap_or(src.as_os_str()))
}
fn mirrored_relative(src: &Path) -> Option<PathBuf> {
let mut relative = PathBuf::new();
for component in src.components() {
match component {
Component::Prefix(_) | Component::RootDir | Component::CurDir => {}
Component::ParentDir => return None,
Component::Normal(part) => relative.push(part),
}
}
Some(relative)
}
pub fn mirrored_output_dir(src: &Path, root: &Path) -> Option<PathBuf> {
let relative = mirrored_relative(src)?;
Some(match relative.parent() {
Some(parent) => root.join(parent),
None => root.to_path_buf(),
})
}
pub fn create_mirrored_dirs(src: &Path, root: &Path) -> Result<()> {
let Some(relative) = mirrored_relative(src) else {
return Ok(());
};
create_dir_if_missing(root, None)
.wrap_err_with(|| format!("failed to create DIR {}", root.display()))?;
let Some(parent) = relative.parent() else {
return Ok(());
};
let source_count = src.components().count();
let relative_count = relative.components().count();
debug_assert!(
relative_count <= source_count,
"the mirrored path is a subset of the source's components"
);
let stripped = source_count - relative_count;
let source_root: PathBuf = src.components().take(stripped).collect();
let mut source_level = source_root;
let mut destination = root.to_path_buf();
for level in parent.components() {
source_level.push(level);
destination.push(level);
let mode = fs::metadata(&source_level)
.ok()
.map(|metadata| metadata.permissions());
create_dir_if_missing(&destination, mode)
.wrap_err_with(|| format!("failed to create DIR {}", destination.display()))?;
}
Ok(())
}
fn create_dir_if_missing(dir: &Path, permissions: Option<fs::Permissions>) -> std::io::Result<()> {
match fs::metadata(dir) {
Ok(existing) if existing.is_dir() => return Ok(()),
Ok(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"exists and is not a directory",
));
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
Err(err) => return Err(err),
}
#[cfg(unix)]
let builder = {
use std::os::unix::fs::{DirBuilderExt, PermissionsExt};
let mut builder = fs::DirBuilder::new();
if let Some(permissions) = &permissions {
builder.mode(permissions.mode() & 0o7777);
}
builder
};
#[cfg(not(unix))]
let builder = {
let _ = permissions;
fs::DirBuilder::new()
};
builder.create(dir)
}
pub fn shared_file_names(files: &[PathBuf]) -> Vec<OsString> {
let mut names: Vec<&std::ffi::OsStr> =
files.iter().filter_map(|file| file.file_name()).collect();
names.sort_unstable();
let mut shared = Vec::new();
for pair in names.windows(2) {
if pair[0] == pair[1] && shared.last() != Some(&pair[0].to_os_string()) {
shared.push(pair[0].to_os_string());
}
}
shared
}
const COMPRESSED_EXTENSIONS: &[&str] = &[
".zst", ".tzst", ".gz", ".tgz", ".lzma", ".xz", ".txz", ".lz4", ".tlz4", ".7z", ".aa3", ".aac",
".aar", ".ace", ".alac", ".ape", ".apk", ".apng", ".arc", ".archive", ".arj", ".ark", ".asf",
".avi", ".avif", ".ba", ".br", ".bz2", ".cab", ".cdx", ".chm", ".cr2", ".divx", ".dmg", ".dng",
".docm", ".docx", ".dotm", ".dotx", ".dsft", ".ear", ".eftx", ".emz", ".eot", ".epub", ".f4v",
".flac", ".flv", ".gho", ".gif", ".gifv", ".gnp", ".iso", ".jar", ".jpeg", ".jpg", ".jxl",
".lz", ".lzh", ".m4a", ".m4v", ".mkv", ".mov", ".mp2", ".mp3", ".mp4", ".mpa", ".mpc", ".mpe",
".mpeg", ".mpg", ".mpl", ".mpv", ".msi", ".odp", ".ods", ".odt", ".ogg", ".ogv", ".otp",
".ots", ".ott", ".pea", ".png", ".pptx", ".qt", ".rar", ".s7z", ".sfx", ".sit", ".sitx",
".sqx", ".svgz", ".swf", ".tbz2", ".tib", ".tlz", ".vob", ".war", ".webm", ".webp", ".wma",
".wmv", ".woff", ".woff2", ".wvl", ".xlsx", ".xpi", ".xps", ".zip", ".zipx", ".zoo", ".zpaq",
];
pub fn has_compressed_extension(path: &Path) -> bool {
let Some(name) = path.file_name() else {
return false;
};
let name = name.as_encoded_bytes();
match name.iter().rposition(|&byte| byte == b'.') {
Some(0) | None => false,
Some(at) => COMPRESSED_EXTENSIONS
.iter()
.any(|extension| extension.as_bytes() == &name[at..]),
}
}
#[cfg(test)]
mod tests;