use std::io::{self, BufWriter, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use clap::Parser;
use dnfile::DnPe;
use dnfile::lang::cil::instruction::Operand;
#[derive(Parser, Debug)]
#[command(
name = "dnstrings",
version,
about = "List every user string (ldstr) referenced by a .NET PE, with call-site context.",
long_about = None,
)]
struct Args {
file: PathBuf,
#[arg(long, default_value_t = 0)]
min_len: usize,
#[arg(long)]
tsv: bool,
}
fn main() -> ExitCode {
reset_sigpipe();
let args = Args::parse();
match run(args) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
let mut src = e.source();
while let Some(cause) = src {
eprintln!("caused by: {cause}");
src = cause.source();
}
ExitCode::from(1)
}
}
}
fn reset_sigpipe() {
#[cfg(unix)]
{
unsafe extern "C" {
fn signal(signum: i32, handler: usize) -> usize;
}
const SIGPIPE: i32 = 13;
const SIG_DFL: usize = 0;
unsafe {
signal(SIGPIPE, SIG_DFL);
}
}
}
fn run(args: Args) -> Result<(), Box<dyn std::error::Error>> {
let path = args.file.canonicalize().unwrap_or(args.file.clone());
let data = std::fs::read(&path)?;
let pe = DnPe::parse(&data)?;
let clr = pe.net()?;
let stdout = io::stdout();
let mut out = BufWriter::new(stdout.lock());
'outer: for func in clr.functions() {
for insn in &func.instructions {
if !insn.is_ldstr() {
continue;
}
let Operand::StringToken(tok) = &insn.operand else {
continue;
};
let Ok(s) = clr.get_us(tok.rid()) else {
continue;
};
if s.chars().count() < args.min_len {
continue;
}
let escaped = escape(&s);
let write_result = if args.tsv {
writeln!(
out,
"0x{:08X}\t0x{:08X}\t{}",
func.offset, insn.offset, escaped
)
} else {
writeln!(
out,
"0x{:08X}:0x{:08X} {}",
func.offset, insn.offset, escaped
)
};
if let Err(e) = write_result {
if e.kind() == io::ErrorKind::BrokenPipe {
break 'outer;
}
return Err(e.into());
}
}
}
if let Err(e) = out.flush() {
if e.kind() != io::ErrorKind::BrokenPipe {
return Err(e.into());
}
}
Ok(())
}
fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\\' => out.push_str("\\\\"),
c if c.is_control() => out.push_str(&format!("\\x{:02X}", c as u32)),
c => out.push(c),
}
}
out
}