use std::collections::BTreeMap;
use std::io::IsTerminal;
use crate::backend::MemoryOps;
use crate::error::Result;
use crate::expr::{Expr, NumberRadix};
use crate::output;
#[cfg(feature = "python")]
use crate::python::embed;
use crate::target::meta::{
ErrorCodeDetail, TargetTimeDetail, TargetVersionDetail, decode_error_code,
decode_error_code_as_ntstatus,
};
use crate::target::{CODE_BITNESS_AMD64, CODE_BITNESS_X86, Target};
use crate::types::VirtAddr;
use crate::repl::*;
const PRINTF_C_STRING_LIMIT: usize = 4096;
const PRINTF_WIDE_STRING_LIMIT: usize = 2048;
repl_command! {
cmd_reload_scripts();
names: ["reload-scripts"],
usage: "reload-scripts",
summary: "Reload custom commands and aliases.",
}
repl_command! {
cmd_radix;
names: ["n"],
usage: "n [8|10|16]",
summary: "Display or set the default numeric radix for REPL expressions.",
}
repl_command! {
cmd_effmach;
names: [".effmach"],
usage: ".effmach [x86|amd64|auto|.]",
summary: "Display or set the effective code machine.",
details: "With no argument, display the selected machine; x86 and amd64 override automatic code-bitness detection, while auto or . clears the override.",
}
repl_command! {
cmd_version();
names: ["vertarget", "version"],
usage: "vertarget",
summary: "Display target, kernel, symbol, processor, and debugger version information.",
}
repl_command! {
cmd_time();
names: [".time"],
usage: ".time",
summary: "Display target UTC time and system uptime.",
}
repl_command! {
cmd_echo;
names: [".echo", "echo"],
usage: ".echo <text>",
summary: "Print text without expression interpretation.",
style: ExpressionTail,
}
repl_command! {
cmd_printf;
names: [".printf"],
usage: ".printf \"format\" [arguments...]",
summary: "Format debugger values using WinDbg-style printf specifiers.",
completion: Expression,
style: ExpressionTail,
}
repl_command! {
cmd_cls();
names: [".cls"],
usage: ".cls",
summary: "Clear the terminal screen when stdout is a terminal.",
}
repl_command! {
cmd_logopen;
names: [".logopen"],
usage: ".logopen <file>",
summary: "Start a debugger transcript, replacing any existing file.",
}
repl_command! {
cmd_logappend;
names: [".logappend"],
usage: ".logappend <file>",
summary: "Start a debugger transcript, appending to the file.",
}
repl_command! {
cmd_logclose();
names: [".logclose"],
usage: ".logclose",
summary: "Close the debugger transcript.",
}
repl_command! {
cmd_help;
names: [".hh", "help", ".help"],
usage: ".hh [command]",
summary: "List commands or display detailed help for one command.",
}
repl_command! {
cmd_error;
names: ["!error", "!ntstatus"],
usage: "!error <code>",
summary: "Decode an NTSTATUS, Win32, or HRESULT error code.",
completion: Expression,
}
repl_command! {
names: ["q", "quit"],
usage: "q",
summary: "Exit the application.",
flow: Quit,
}
impl ReplState<'_> {
fn cmd_reload_scripts(&mut self) -> Result<()> {
#[cfg(feature = "python")]
{
let py_report = embed::load_commands_dir();
embed::print_script_load_report(&py_report);
*self.caches.user_commands.write().unwrap() = initial_user_commands();
}
let alias_report = self.reload_aliases();
print_alias_load_report(&alias_report);
Ok(())
}
fn cmd_radix(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
if let Some(value) = invocation.arg(0) {
self.radix = match value {
"8" => NumberRadix::Octal,
"10" => NumberRadix::Decimal,
"16" => NumberRadix::Hexadecimal,
_ => {
error!("invalid radix '{value}' (use 8, 10, or 16)");
return Ok(());
}
};
}
let name = match self.radix {
NumberRadix::Octal => "octal",
NumberRadix::Decimal => "decimal",
NumberRadix::Hexadecimal => "hexadecimal",
};
outln!("radix {} ({name})\n", self.radix.value());
Ok(())
}
fn cmd_effmach(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
if invocation.argv.len() > 1 {
outln!("{}\n", command_help(invocation.name));
return Ok(());
}
if let Some(machine) = invocation.arg(0) {
let machine = machine.to_ascii_lowercase();
self.ctx.target.effmach = match machine.as_str() {
"x86" => Some(CODE_BITNESS_X86),
"amd64" => Some(CODE_BITNESS_AMD64),
"." | "auto" => None,
_ => {
error!("invalid effective machine '{machine}' (use x86, amd64, or auto)");
return Ok(());
}
};
}
let machine = match self.ctx.target.effmach {
Some(CODE_BITNESS_X86) => "x86",
Some(CODE_BITNESS_AMD64) => "AMD64",
_ => "auto",
};
outln!("effective machine: {machine}\n");
Ok(())
}
fn cmd_version(&mut self) -> Result<()> {
let detail = self.ctx.target_version()?;
print_target_version(&detail);
outln!();
Ok(())
}
fn cmd_time(&mut self) -> Result<()> {
let detail = self.ctx.target.target_time()?;
print_target_time(&detail);
outln!();
Ok(())
}
fn cmd_echo(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let mut text = invocation.raw_tail;
if text.len() >= 2 && text.starts_with('"') && text.ends_with('"') {
text = &text[1..text.len() - 1];
}
outln!("{text}");
Ok(())
}
fn cmd_printf(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let Some((format, args)) = parse_printf_tail(invocation.raw_tail) else {
outln!("{}\n", command_help(invocation.name));
return Ok(());
};
let text = match format_printf(&format, &args, &self.ctx.target, self.radix) {
Ok(text) => text,
Err(error) => {
error!("{}", error);
return Ok(());
}
};
out!("{text}");
Ok(())
}
fn cmd_cls(&mut self) -> Result<()> {
if std::io::stdout().is_terminal() {
out!("\x1b[2J\x1b[H");
}
Ok(())
}
fn cmd_logopen(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
self.open_log_file(&invocation, false)
}
fn cmd_logappend(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
self.open_log_file(&invocation, true)
}
fn open_log_file(&mut self, invocation: &CommandInvocation<'_>, append: bool) -> Result<()> {
let Some(path) = invocation.arg(0) else {
outln!("{}\n", command_help(invocation.name));
return Ok(());
};
if invocation.argv.len() != 1 {
outln!("{}\n", command_help(invocation.name));
return Ok(());
}
match output::open_log(path, append) {
Ok(()) => outln!(
"log {} {}\n",
if append { "appending to" } else { "opened" },
path
),
Err(error) => error!("failed to open log '{}': {error}", path),
}
Ok(())
}
fn cmd_logclose(&mut self) -> Result<()> {
output::close_log();
outln!("log closed\n");
Ok(())
}
fn cmd_help(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
if let Some(name) = invocation.arg(0) {
if let Some((_, help, _)) = self
.caches
.user_commands
.read()
.unwrap()
.iter()
.find(|(command, _, _)| command == name)
{
outln!("{name}\n{help}\n");
return Ok(());
}
if let Some((_, expansion)) = self
.aliases
.entries()
.into_iter()
.find(|(alias, _)| alias == name)
{
outln!("alias {name} {expansion}\n");
return Ok(());
}
if command_registry().get(name).is_none() {
error!("unknown command '{name}'");
} else {
outln!("{}\n", command_help(name));
}
return Ok(());
}
let mut groups: BTreeMap<&'static str, BTreeMap<&'static str, &'static CommandSpec>> =
BTreeMap::new();
for (_, spec) in command_registry().command_names() {
let canonical = spec.names[0];
groups
.entry(command_category(canonical))
.or_default()
.insert(canonical, spec);
}
for (category, specs) in groups {
outln!("{}", ui::label(category));
for (_, spec) in specs {
let aliases = spec.names[1..].join(", ");
if aliases.is_empty() {
outln!(" {:<24} {}", spec.names[0], spec.summary);
} else {
outln!(
" {:<24} {} (aliases: {})",
spec.names[0],
spec.summary,
aliases
);
}
}
outln!();
}
let user_commands = self.caches.user_commands.read().unwrap().clone();
if !user_commands.is_empty() {
outln!("{}", ui::label("python commands"));
for (name, help, _) in user_commands {
outln!(" {:<24} {}", name, help.lines().next().unwrap_or(""));
}
outln!();
}
let aliases = self.aliases.entries();
if !aliases.is_empty() {
outln!("{}", ui::label("user aliases"));
for (name, expansion) in aliases {
outln!(" {name:<24} {expansion}");
}
outln!();
}
Ok(())
}
fn cmd_error(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
let Some(text) = invocation.arg(0) else {
outln!("{}\n", command_help(invocation.name));
return Ok(());
};
let code = match Expr::eval_with_radix(text, &self.ctx.target, self.radix) {
Ok(value) => value.0,
Err(error) => {
error!("invalid error code '{text}': {error}");
return Ok(());
}
};
if u32::try_from(code).is_err() {
error!("error code '{text}' exceeds 32 bits");
return Ok(());
}
let detail = if invocation.name == "!ntstatus" {
decode_error_code_as_ntstatus(code)
} else {
decode_error_code(code)
};
print_error_code(&detail);
Ok(())
}
pub fn cmd_user(&mut self, invocation: CommandInvocation<'_>) -> Result<()> {
#[cfg(feature = "python")]
if embed::has_command(invocation.name) {
let args: Vec<&str> = invocation.argv.iter().map(|arg| arg.as_ref()).collect();
if let Err(error) = embed::dispatch(invocation.name, &args, self.ctx) {
error!("{}: {}", invocation.name, error);
}
return Ok(());
}
outln!(
"unknown command: '{}' (try pressing tab to see available commands)\n",
invocation.name
);
Ok(())
}
}
const COMMAND_CATEGORIES: &[(&str, &str)] = &[
("dS", "memory and disassembly"),
("dW", "memory and disassembly"),
("da", "memory and disassembly"),
("db", "memory and disassembly"),
("dc", "memory and disassembly"),
("dd", "memory and disassembly"),
("dds", "memory and disassembly"),
("dl", "memory and disassembly"),
("dq", "memory and disassembly"),
("dp", "memory and disassembly"),
("dpp", "memory and disassembly"),
("dqs", "memory and disassembly"),
("ds", "memory and disassembly"),
("du", "memory and disassembly"),
("dw", "memory and disassembly"),
("dyb", "memory and disassembly"),
("eb", "memory and disassembly"),
("ed", "memory and disassembly"),
("eq", "memory and disassembly"),
("ew", "memory and disassembly"),
("ea", "memory and disassembly"),
("eu", "memory and disassembly"),
("eza", "memory and disassembly"),
("ezu", "memory and disassembly"),
("f", "memory and disassembly"),
("s", "memory and disassembly"),
("u", "memory and disassembly"),
("ub", "memory and disassembly"),
("uf", "memory and disassembly"),
("!db", "memory and disassembly"),
("!dd", "memory and disassembly"),
("!dq", "memory and disassembly"),
("!dw", "memory and disassembly"),
("!eb", "memory and disassembly"),
("!ed", "memory and disassembly"),
("!eq", "memory and disassembly"),
("break", "execution and stack"),
("g", "execution and stack"),
("gh", "execution and stack"),
("gn", "execution and stack"),
("gu", "execution and stack"),
("k", "execution and stack"),
("pa", "execution and stack"),
("p", "execution and stack"),
("pc", "execution and stack"),
("ph", "execution and stack"),
("pt", "execution and stack"),
("r", "execution and stack"),
("t", "execution and stack"),
("ta", "execution and stack"),
("tc", "execution and stack"),
("th", "execution and stack"),
("tt", "execution and stack"),
("wt", "execution and stack"),
("!analyze", "analysis"),
("!apc", "execution and stack"),
("!stacks", "execution and stack"),
("!process", "processes and modules"),
("!session", "processes and modules"),
("!sprocess", "processes and modules"),
("!thread", "processes and modules"),
("!vad", "processes and modules"),
(".effmach", "processes and modules"),
("~", "processes and modules"),
("attach", "processes and modules"),
("detach", "processes and modules"),
("drivers", "processes and modules"),
("ld", "processes and modules"),
("lm", "processes and modules"),
("lmv", "processes and modules"),
("ps", "processes and modules"),
("threads", "processes and modules"),
("vcpu", "processes and modules"),
("!dlls", "user mode"),
("!heap", "user mode"),
("!peb", "user mode"),
("!teb", "user mode"),
("!gle", "user mode"),
("ba", "breakpoints and events"),
("bc", "breakpoints and events"),
("bd", "breakpoints and events"),
("be", "breakpoints and events"),
("bl", "breakpoints and events"),
("bm", "breakpoints and events"),
("bp", "breakpoints and events"),
("bpc", "breakpoints and events"),
("bpp", "breakpoints and events"),
("br", "breakpoints and events"),
("bs", "breakpoints and events"),
("bu", "breakpoints and events"),
("sx", "breakpoints and events"),
("sxd", "breakpoints and events"),
("sxe", "breakpoints and events"),
("sxi", "breakpoints and events"),
("sxn", "breakpoints and events"),
("sxr", "breakpoints and events"),
("rdmsr", "cpu"),
("wrmsr", "cpu"),
("!cpuinfo", "cpu"),
("!gdt", "cpu"),
("!idt", "cpu"),
("!irql", "cpu"),
("!pcr", "cpu"),
("!prcb", "cpu"),
("!dpcs", "cpu"),
("!ready", "cpu"),
("!running", "cpu"),
("!timer", "cpu"),
("!lookaside", "memory manager"),
("!memusage", "memory manager"),
("!pfn", "memory manager"),
("!pool", "memory manager"),
("!poolfind", "memory manager"),
("!poolused", "memory manager"),
("!pte", "memory manager"),
("!ptov", "memory manager"),
("!vm", "memory manager"),
("!vtop", "memory manager"),
("callbacks", "objects and I/O"),
("!devnode", "objects and I/O"),
("!devobj", "objects and I/O"),
("!devstack", "objects and I/O"),
("!drvobj", "objects and I/O"),
("!fileobj", "objects and I/O"),
("!handle", "objects and I/O"),
("!irp", "objects and I/O"),
("!list", "objects and I/O"),
("!locks", "objects and I/O"),
("!object", "objects and I/O"),
("!pnptriage", "objects and I/O"),
("ssdt", "objects and I/O"),
("!acl", "security"),
("!objsd", "security"),
("!sd", "security"),
("!sid", "security"),
("!token", "security"),
("!chkimg", "analysis"),
("!error", "analysis"),
("!verifier", "analysis"),
("?", "symbols, types, and expressions"),
("dt", "symbols, types, and expressions"),
("dv", "symbols, types, and expressions"),
("ev", "symbols, types, and expressions"),
("ln", "symbols, types, and expressions"),
("set", "symbols, types, and expressions"),
("unset", "symbols, types, and expressions"),
("vars", "symbols, types, and expressions"),
("x", "symbols, types, and expressions"),
];
fn command_category(name: &str) -> &'static str {
if let Some((_, category)) = COMMAND_CATEGORIES
.iter()
.find(|(canonical, _)| *canonical == name)
{
return category;
}
if name.starts_with('.') {
"dot commands"
} else if name.starts_with('!') {
"extension commands"
} else {
"general"
}
}
fn print_target_version(detail: &TargetVersionDetail) {
outln!("{}", ui::label("target version"));
outln!(
" {} Windows {}.{} build {}{}",
ui::muted("target"),
detail
.major_version
.map_or_else(|| "?".to_string(), |value| value.to_string()),
detail
.minor_version
.map_or_else(|| "?".to_string(), |value| value.to_string()),
detail
.build_number
.map_or_else(|| "?".to_string(), |value| value.to_string()),
detail
.build_lab
.as_ref()
.map(|lab| format!(" ({lab})"))
.unwrap_or_default()
);
outln!(" {} {}", ui::muted("arch"), detail.architecture);
if let Some(kernel) = &detail.kernel {
match kernel.size {
Some(size) => outln!(
" {} {} size {:#x}",
ui::muted("kernel"),
ui::addr(kernel.base.0),
size
),
None => outln!(
" {} {} size unknown",
ui::muted("kernel"),
ui::addr(kernel.base.0)
),
}
} else {
outln!(" {} unavailable", ui::muted("kernel"));
}
let pdb_guid = detail
.kernel
.as_ref()
.and_then(|kernel| kernel.pdb_guid.as_deref());
let pdb_age = detail.kernel.as_ref().and_then(|kernel| kernel.pdb_age);
if let (Some(guid), Some(age)) = (pdb_guid, pdb_age) {
outln!(
" {} ntoskrnl.pdb GUID {} age {}",
ui::muted("pdb"),
guid,
age
);
} else {
outln!(" {} unavailable", ui::muted("pdb"));
}
if let Some(kernel) = &detail.kernel {
if let Some(version) = &kernel.file_version {
outln!(" {} file version {}", ui::muted("kernel"), version);
}
if let Some(version) = &kernel.product_version {
outln!(" {} product version {}", ui::muted("kernel"), version);
}
}
outln!(
" {} {}",
ui::muted("processors"),
detail
.processors
.map_or_else(|| "unknown".to_string(), |value| value.to_string())
);
outln!(" {} {}", ui::muted("product"), detail.product);
outln!(
" {} {}",
ui::muted("uptime"),
detail.uptime.as_deref().unwrap_or("unknown")
);
outln!(
" {} {}",
ui::muted("backend"),
detail.backend.as_deref().unwrap_or("unknown")
);
outln!(" {} {}", ui::muted("ntoseye"), detail.debugger_version);
outln!(" {} {}", ui::muted("symbol path"), detail.symbol_path);
outln!(
" {} {}",
ui::muted("symbol status"),
detail.symbol_status.as_deref().unwrap_or("unknown")
);
if let Some(time) = detail.system_time_iso.as_deref() {
outln!(" {} {}", ui::muted("system time"), time);
}
if let Some(dump) = &detail.dump {
outln!(
" {} {} (bugcheck: {:#x})",
ui::muted("dump"),
if dump.is_triage { "yes" } else { "no" },
dump.bugcheck_code
);
outln!(
" {} {} processors, machine {:#x}, service-pack build {}",
ui::muted("dump metadata"),
dump.number_processors,
dump.machine_image_type,
dump.service_pack_build
);
outln!(
" {} dtb {:#x}, kernel {:#x}, exception {}",
ui::muted("dump metadata"),
dump.directory_table_base.0,
dump.kernel_base.map_or(0, |base| base.0),
dump.exception_code
.map_or_else(|| "none".to_string(), |code| format!("{code:#x}"))
);
let parameters = dump
.bugcheck_parameters
.iter()
.map(|value| format!("{value:#x}"))
.collect::<Vec<_>>()
.join(", ");
outln!(
" {} parameters [{}]",
ui::muted("dump metadata"),
parameters
);
outln!(
" {} Windows {}.{} product type {}",
ui::muted("dump metadata"),
dump.major_version,
dump.minor_version,
dump.product_type
);
outln!(
" {} system time {}, uptime {}",
ui::muted("dump metadata"),
dump.system_time
.map_or_else(|| "unknown".to_string(), |time| format!("{time:#x}")),
dump.uptime_seconds
.map_or_else(|| "unknown".to_string(), |seconds| format!("{seconds}s"))
);
outln!(
" {} triage overflowed {}",
ui::muted("dump metadata"),
if dump.triage_overflowed { "yes" } else { "no" }
);
}
}
fn print_target_time(detail: &TargetTimeDetail) {
outln!("{}", ui::label("target time"));
match (detail.system_time, detail.system_time_iso.as_deref()) {
(Some(raw), Some(iso)) => outln!(" {} {} ({raw:#x})", ui::muted("system time"), iso),
(Some(raw), None) => outln!(" {} unavailable ({raw:#x})", ui::muted("system time")),
_ => outln!(" {} unavailable", ui::muted("system time")),
}
match (detail.uptime_seconds, detail.uptime.as_deref()) {
(Some(seconds), Some(formatted)) => outln!(
" {} {} ({seconds} seconds)",
ui::muted("uptime"),
formatted
),
_ => outln!(" {} unavailable", ui::muted("uptime")),
}
}
fn parse_printf_tail(text: &str) -> Option<(String, Vec<String>)> {
let line = format!(".printf {text}");
let parsed = parse_command(&line).ok()??;
let invocation = parsed.invocation(CommandStyle::StructuredArgs).ok()?;
let format = invocation.arg(0)?.to_string();
let args = invocation
.argv
.into_iter()
.skip(1)
.map(|argument| argument.into_owned())
.collect();
Some((format, args))
}
fn read_wide_string(target: &Target, address: VirtAddr, max_chars: usize) -> Option<String> {
let count = max_chars.checked_mul(2)?;
let mut bytes = vec![0u8; count];
target
.current_process()
.ok()?
.memory()
.read_bytes(address, &mut bytes)
.ok()?;
let mut text = String::new();
for chunk in bytes.as_chunks::<2>().0 {
let value = u16::from_le_bytes(*chunk);
if value == 0 {
break;
}
text.push(char::from_u32(value as u32).unwrap_or('\u{fffd}'));
}
Some(text)
}
fn format_printf(
format: &str,
args: &[String],
target: &Target,
radix: NumberRadix,
) -> Result<String> {
let chars: Vec<char> = format.chars().collect();
let mut output = String::new();
let mut index = 0;
let mut arg_index = 0;
while index < chars.len() {
if chars[index] == '\\' && index + 1 < chars.len() {
let (escaped, width) = match chars[index + 1] {
'n' => ('\n', 2),
't' => ('\t', 2),
'r' => ('\r', 2),
'b' => ('\u{8}', 2),
'0' => ('\0', 2),
'\\' => ('\\', 2),
'"' => ('"', 2),
_ => ('\\', 1),
};
output.push(escaped);
index += width;
continue;
}
if chars[index] != '%' || index + 1 >= chars.len() {
output.push(chars[index]);
index += 1;
continue;
}
let start = index;
index += 1;
let spec = chars[index];
if spec == '%' {
output.push('%');
index += 1;
continue;
}
let extended = matches!(spec, 'm' | 's')
&& index + 1 < chars.len()
&& matches!(chars[index + 1], 'a' | 'u');
if extended {
index += 1;
}
let Some(argument) = args.get(arg_index) else {
output.extend(chars[start..index + 1].iter());
index += 1;
continue;
};
let value = || Expr::eval_with_radix(argument, target, radix).map(|value| value.0);
let rendered = match (spec, extended.then_some(chars[index])) {
('d', None) => (value()? as i64).to_string(),
('u', None) => value()?.to_string(),
('x', None) => format!("{:x}", value()?),
('p', None) => ui::addr(value()?),
('c', None) => char::from_u32(value()? as u32)
.unwrap_or('\u{fffd}')
.to_string(),
('s', None) => argument.to_string(),
('m', Some('a')) => {
let address = value()?;
target
.read_c_string(VirtAddr(address), PRINTF_C_STRING_LIMIT)
.unwrap_or_else(|_| format!("<unreadable {address:#x}>"))
}
('m', Some('u')) => {
let address = value()?;
read_wide_string(target, VirtAddr(address), PRINTF_WIDE_STRING_LIMIT)
.unwrap_or_else(|| format!("<unreadable {address:#x}>"))
}
('y', None) => {
let address = value()?;
target
.closest_symbol_current_context(VirtAddr(address))
.unwrap_or_else(|| format!("{address:#x}"))
}
_ => {
output.extend(chars[start..=index].iter());
index += 1;
continue;
}
};
output.push_str(&rendered);
arg_index += 1;
index += 1;
}
Ok(output)
}
fn print_error_code(detail: &ErrorCodeDetail) {
match detail.kind.as_str() {
"NTSTATUS" => {
outln!("NTSTATUS {:#010x}: {}", detail.code, detail.name);
outln!(" {}", detail.description);
}
"HRESULT" => {
outln!("HRESULT {:#010x}: {}", detail.code, detail.name);
outln!(" {}", detail.description);
if let Some(win32) = detail.win32_code {
outln!(" Win32 code: {win32} ({win32:#x})");
}
}
"Win32" => outln!(
"Win32 error {} ({:#x}): {}",
detail.code,
detail.code,
detail.name
),
_ => outln!(
"Unknown error code {} ({:#x}): {}",
detail.code,
detail.code,
detail.description
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::output::capture;
use crate::repl::ReplState;
use crate::session::session_over_memory;
#[test]
fn printf_reports_expression_errors_instead_of_printing_placeholders() {
let session = session_over_memory(0x1000, &[0; 8]);
let result = format_printf(
"major[%p] wired",
&["missing_local".into()],
&session.target,
NumberRadix::Hexadecimal,
);
assert!(matches!(result, Err(Error::SymbolNotFound(name)) if name == "missing_local"));
}
#[test]
fn printf_interprets_control_characters_and_adds_nothing() {
let mut session = session_over_memory(0x1000, &[0u8; 8]);
let mut state = ReplState::for_oneshot(&mut session);
let (result, text) =
capture(|| state.dispatch_line("\u{2e}printf \"a\\tb\\nc=%u\\n\" 0n42"));
result.unwrap();
assert_eq!(text, "a\tb\nc=42\n");
let (result, text) = capture(|| state.dispatch_line("\u{2e}printf \"C:\\dir\\x\""));
result.unwrap();
assert_eq!(text, "C:\\dir\\x");
}
#[test]
fn printf_keeps_literal_strings_separate_from_numeric_expressions() {
let session = session_over_memory(0x1000, &[0; 8]);
let result = format_printf(
"%s=%u %%",
&["index".into(), "0n27+1".into()],
&session.target,
NumberRadix::Hexadecimal,
)
.unwrap();
assert_eq!(result, "index=28 %");
}
}