use std::{
io::{BufRead, BufReader, Write},
os::unix::ffi::OsStrExt,
process::ExitCode,
};
use data_encoding::{HEXLOWER, HEXLOWER_PERMISSIVE};
use memchr::arch::all::is_equal;
use nix::{errno::Errno, unistd::isatty};
#[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()?;
let mut opt_encode = true;
let mut opt_force = false;
let mut opt_stream = false;
let mut opt_continue_on_failure = false;
let mut opt_limit = None;
let mut opt_input = None;
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Short('d') => opt_encode = false,
Short('e') => opt_encode = true,
Short('f') | Long("force-tty") => opt_force = true,
Short('s') | Long("stream") => opt_stream = true,
Short('C') | Long("continue-on-failure") => opt_continue_on_failure = true,
Short('l') => {
opt_limit = Some(
parse_size::Config::new()
.with_binary()
.parse_size(parser.value()?.as_bytes())?,
)
}
Value(input) if opt_input.is_none() => opt_input = Some(input),
_ => return Err(arg.unexpected().into()),
}
}
if opt_encode && opt_stream {
eprintln!("syd-hex: -s option must be used with the -d option.");
return Err(Errno::EINVAL.into());
}
if opt_continue_on_failure && !opt_stream {
eprintln!("syd-hex: -C option must be used with the -s option.");
return Err(Errno::EINVAL.into());
}
if !opt_encode && !opt_force && isatty(std::io::stdout())? {
eprintln!("syd-hex: Refusing to write unsafe output to the terminal.");
eprintln!("syd-hex: Use -f or --force-tty to override this check.");
return Err(Errno::EBADF.into());
}
#[expect(clippy::disallowed_methods)]
#[expect(clippy::disallowed_types)]
let mut input: Box<dyn BufRead> = match opt_input {
None => Box::new(std::io::stdin().lock()),
Some(path) if is_equal(path.as_bytes(), b"-") => Box::new(std::io::stdin().lock()),
Some(path) => Box::new(BufReader::new(std::fs::File::open(path)?)),
};
if !opt_stream {
assert!(
!opt_continue_on_failure,
"attempt to continue-on-failure in batch mode!"
);
let mut nwrite: usize = 0;
let mut buffer = [0; 64 * 1024]; while let Ok(count) = input.read(&mut buffer[..]) {
let buffer = if count == 0 {
break; } else if let Some(lim) = opt_limit.map(|lim| lim as usize) {
let buffer = if nwrite.checked_add(count).map(|c| c >= lim).unwrap_or(true) {
let offset = match lim.checked_sub(nwrite) {
Some(0) | None => break, Some(n) => n,
};
&buffer[..offset]
} else {
&buffer[..count]
};
nwrite = nwrite.saturating_add(count);
buffer
} else {
&buffer[..count]
};
if opt_encode {
let encoded = HEXLOWER.encode(buffer);
print!("{encoded}");
} else {
let data = std::str::from_utf8(buffer)?;
let data = data.split_whitespace().collect::<String>();
match HEXLOWER_PERMISSIVE.decode(data.as_bytes()) {
Ok(decoded) => {
std::io::stdout().write_all(&decoded)?;
}
Err(error) => {
eprintln!("syd-hex: Error decoding hex: {error}");
return Ok(ExitCode::FAILURE);
}
}
}
}
} else {
assert!(!opt_encode, "attempt to hex-encode stream!");
let line_limit: usize = opt_limit.unwrap_or(0).try_into().unwrap_or(0);
for (idx, line) in input.lines().enumerate() {
if line_limit != 0 && idx > line_limit {
break;
}
let line = match line {
Ok(line) => line,
Err(error) => {
eprintln!("syd-hex: Error reading line {idx}: {error}!");
if !opt_continue_on_failure {
return Ok(ExitCode::FAILURE);
} else {
continue;
}
}
};
let line = line.trim_end();
let line = line.split_whitespace().collect::<String>();
match HEXLOWER_PERMISSIVE.decode(line.as_bytes()) {
Ok(decoded) => {
std::io::stdout().write_all(&decoded)?;
std::io::stdout().write_all(b"\n")?;
}
Err(error) => {
eprintln!("syd-hex: Error decoding hex on line {idx}: {error}!");
if !opt_continue_on_failure {
return Ok(ExitCode::FAILURE);
} else {
continue;
}
}
}
}
}
Ok(ExitCode::SUCCESS)
}
fn help() {
println!("Usage: syd-hex [-hdefkls] <file|->");
println!("Given a file, hex-encode and print.");
println!("Given no positional arguments, hex-encode standard input.");
println!("Use -d to hex-decode rather than hex-encode.");
println!("Use -s with -d to hex-decode with newline-delimited chunks.");
println!("Use -C with -s to warn and continue in case of read or encoding errors.");
println!("Use -f to force print decoded hex to TTY (\x1b[91minsecure\x1b[0m).");
println!("Use -l <human-size> to exit after size bytes are read.");
println!("Use -l <line-count> with -s to exit after count lines are read.");
}