use std::{
io::{stdout, Write},
os::{fd::AsRawFd, unix::ffi::OsStrExt},
process::ExitCode,
};
use bitflags::Flags as BitFlags;
use btoi::btoi;
use libc::pid_t;
use nix::{
errno::Errno,
fcntl::{open, OFlag},
sys::stat::Mode,
unistd::Pid,
};
use syd::{
fd::{close_static_files, open_static_files},
lookup::{safe_canonicalize, FsFlags},
path::XPathBuf,
sandbox::{Flags, Sandbox},
syslog::LogLevel,
};
#[cfg(all(
not(coverage),
not(feature = "prof"),
not(target_os = "android"),
not(target_arch = "riscv64"),
target_page_size_4k,
target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;
syd::main! {
use lexopt::prelude::*;
syd::set_sigpipe_dfl()?;
syd::log::log_init_simple(LogLevel::Warn)?;
let mut paths = Vec::new();
let mut flags = Flags::empty();
let mut fsflags = FsFlags::empty();
let mut opt_delimiter = b"\n";
let mut opt_dtrailing = true;
let mut opt_cnt = 1;
let mut opt_dir = None;
let mut opt_pid = Pid::this();
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Short('c') => {
opt_cnt = btoi::<usize>(parser.value()?.as_bytes()).or(Err(Errno::EINVAL))?;
}
Short('d') => opt_dir = Some(parser.value().map(XPathBuf::from)?),
Short('p') => {
opt_pid = btoi::<pid_t>(parser.value()?.as_bytes())
.map(Pid::from_raw)
.or(Err(Errno::EINVAL))?
}
Short('B') => fsflags.insert(FsFlags::RESOLVE_BENEATH),
Short('R') => fsflags.insert(FsFlags::RESOLVE_IN_ROOT),
Short('D') => fsflags.insert(FsFlags::NO_RESOLVE_DOTDOT),
Short('F') => fsflags.insert(FsFlags::NO_RESOLVE_PATH),
Short('M') => fsflags.insert(FsFlags::MISS_LAST),
Short('N') => fsflags.insert(FsFlags::NO_FOLLOW_LAST),
Short('P') => fsflags.insert(FsFlags::NO_RESOLVE_PROC),
Short('U') => flags.insert(Flags::FL_ALLOW_UNSAFE_MAGICLINKS),
Short('X') => fsflags.insert(FsFlags::NO_RESOLVE_XDEV),
Short('m') => fsflags.insert(FsFlags::MUST_PATH),
Short('n') => opt_dtrailing = false,
Short('z') => opt_delimiter = b"\0",
Value(path) => {
paths.push(XPathBuf::from(path));
paths.extend(parser.raw_args()?.map(XPathBuf::from));
}
_ => return Err(arg.unexpected().into()),
}
}
if paths.is_empty() {
help();
return Ok(ExitCode::FAILURE);
}
if fsflags.contains(FsFlags::MUST_PATH | FsFlags::MISS_LAST) {
eprintln!("syd-read: -m and -M options are mutually exclusive!");
return Err(Errno::EINVAL.into());
}
if fsflags.contains(FsFlags::RESOLVE_BENEATH | FsFlags::RESOLVE_IN_ROOT) {
eprintln!("syd-read: -B and -R options are mutually exclusive!");
return Err(Errno::EINVAL.into());
}
#[expect(clippy::disallowed_methods)]
let opt_dir = if let Some(ref dir) = opt_dir {
Some(open(
dir,
OFlag::O_DIRECTORY | OFlag::O_PATH,
Mode::empty(),
)?)
} else {
None
};
open_static_files()?;
let mut sandbox = Sandbox::default();
sandbox.flags.clear();
sandbox.flags.insert(flags);
sandbox.state.clear();
for (idx, path) in paths
.iter()
.cycle()
.take(opt_cnt.saturating_mul(paths.len()))
.enumerate()
{
let path = match safe_canonicalize(
opt_pid,
opt_dir.as_ref().map(|fd| fd.as_raw_fd()),
path,
fsflags,
None,
Some(&sandbox),
) {
Ok(path) => path.take(),
Err(errno) => {
eprintln!("syd-read: Error canonicalizing path `{path}': {errno}!");
return Err(errno.into());
}
};
if idx > 0 {
stdout().write_all(opt_delimiter)?;
}
stdout().write_all(path.as_bytes())?;
}
if opt_dtrailing {
stdout().write_all(opt_delimiter)?;
}
close_static_files();
Ok(ExitCode::SUCCESS)
}
fn help() {
println!("Usage: syd-read [-hmnzBDFMNPRUX] [-c n] [-d dir] [-p pid] path...");
println!("Print resolved symbolic links or canonical file names.");
println!("By default last component may exist, other components must exist.");
println!(" -h Print this help message and exit.");
println!(" -c <n> Cycle through the path list n times, useful for benchmarking.");
println!(" -d <dir> Resolve relative to the given directory.");
println!(" -p <pid> Resolve from the perspective of the given process ID.");
println!(" -m All components of the paths must exist, conflicts with -M.");
println!(
" -M Last component must not exist, other components must exist, conflicts with -m."
);
println!(" -B Resolve beneath the given directory, useful with -d <dir>. Implies -P, conflicts with -R");
println!(" -R Treat the directory as root directory, useful with -d <dir>. Implies -P, conflicts with -B");
println!(" -D Do not traverse through `..` components.");
println!(" -X Do not traverse through mount points.");
println!(" -F Do not follow symbolic links for any of the path components.");
println!(" -N Do not follow symbolic links for the last path component.");
println!(" -P Do not resolve /proc magic symbolic links.");
println!(" -U Resolve unsafe /proc magic symbolic links.");
println!(" -n Do not output the trailing delimiter.");
println!(" -z End each output line with NUL not newline.");
}