use std::collections::BTreeMap;
use std::io::IsTerminal;
use crate::backend::MemoryOps;
use crate::cpu_state::processor_count;
use crate::dbg_backend::DebugBackend;
use crate::error::Result;
use crate::expr::{Expr, NumberRadix};
use crate::kuser_shared::{
read_interrupt_time, read_nt_build_number, read_nt_major_version, read_nt_minor_version,
read_nt_product_type, read_system_time,
};
use crate::ntstatus::{ntstatus_name, win32_error_name};
use crate::output;
#[cfg(feature = "python")]
use crate::python::embed;
use crate::target::Target;
use crate::triage_report::filetime_to_iso;
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_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_version(&mut self) -> Result<()> {
print_target_version(
&self.ctx.target,
self.ctx.backend.name(),
&mut *self.ctx.backend,
);
outln!();
Ok(())
}
fn cmd_time(&mut self) -> Result<()> {
let target = &self.ctx.target;
let (system_time, uptime) = target_times(target);
outln!("{}", ui::label("target time"));
match system_time.and_then(filetime_to_iso) {
Some(time) => outln!(" {} {}", ui::muted("system time"), time),
None => outln!(" {} unavailable", ui::muted("system time")),
}
match uptime {
Some(ticks) => outln!(" {} {}", ui::muted("uptime"), format_uptime(ticks)),
None => outln!(" {} unavailable", ui::muted("uptime")),
}
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(());
}
};
let Ok(code) = u32::try_from(code) else {
error!("error code '{text}' exceeds 32 bits");
return Ok(());
};
print_error_code(code, invocation.name == "!ntstatus");
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"),
("~", "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"),
("!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"),
("!devobj", "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"),
("ssdt", "objects and I/O"),
("!acl", "security"),
("!objsd", "security"),
("!sd", "security"),
("!sid", "security"),
("!token", "security"),
("!chkimg", "analysis"),
("!error", "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 dump_system_time(target: &Target) -> Option<u64> {
target
.phys
.dmp_info()
.and_then(|info| info.system_info.as_ref())
.map(|info| info.system_time)
.filter(|time| *time > 0)
.map(|time| time as u64)
}
fn dump_uptime(target: &Target) -> Option<u64> {
target
.phys
.dmp_info()
.and_then(|info| info.system_info.as_ref())
.map(|info| info.system_up_time)
.filter(|time| *time > 0)
.map(|time| time as u64)
}
fn target_times(target: &Target) -> (Option<u64>, Option<u64>) {
let system_time = dump_system_time(target).or_else(|| read_system_time(target));
let uptime = dump_uptime(target).or_else(|| read_interrupt_time(target));
(system_time, uptime)
}
fn shared_build_lab(target: &Target) -> Option<String> {
let guest = target.guest().ok()?;
let address = guest.ntoskrnl.symbol("NtBuildLab").ok()?.address();
let mut bytes = [0u8; 128];
guest
.ntoskrnl
.memory()
.read_bytes(address, &mut bytes)
.ok()?;
let end = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(bytes.len());
let text = String::from_utf8_lossy(&bytes[..end]).trim().to_string();
(!text.is_empty()).then_some(text)
}
fn symbol_build_number(target: &Target) -> Option<u64> {
let guest = target.guest().ok()?;
guest
.ntoskrnl
.symbol("NtBuildNumber")
.and_then(|symbol| symbol.read::<u16>())
.ok()
.map(u64::from)
}
fn format_uptime(ticks: u64) -> String {
let seconds = ticks / 10_000_000;
let days = seconds / 86_400;
let hours = (seconds / 3_600) % 24;
let minutes = (seconds / 60) % 60;
let seconds = seconds % 60;
format!("{days}d {hours:02}:{minutes:02}:{seconds:02}")
}
fn print_target_version(target: &Target, backend: &str, client: &mut dyn DebugBackend) {
let dump_info = target
.phys
.dmp_info()
.and_then(|info| info.system_info.as_ref());
let major = dump_info
.map(|info| info.major_version as u64)
.filter(|value| *value != 0)
.or_else(|| read_nt_major_version(target));
let minor = dump_info
.map(|info| info.minor_version as u64)
.filter(|value| *value != 0)
.or_else(|| read_nt_minor_version(target));
let build = symbol_build_number(target)
.or_else(|| read_nt_build_number(target))
.map(|value| value & 0xffff);
let product = dump_info
.map(|info| info.product_type as u64)
.filter(|value| *value != 0)
.or_else(|| read_nt_product_type(target));
let (system_time, uptime) = target_times(target);
let modules = target.kernel_modules().unwrap_or_default();
let kernel = target
.kernel_base()
.and_then(|base| {
modules
.iter()
.find(|module| module.base_address == base)
.cloned()
})
.or_else(|| {
modules
.iter()
.find(|module| module.short_name == "nt")
.cloned()
});
let base = kernel
.as_ref()
.map(|module| module.base_address)
.or_else(|| target.kernel_base());
let identity = base.and_then(|base| {
target
.symbols
.module_pdb_identity(target.kernel_dtb(), base)
});
let processors = processor_count(target).ok().or_else(|| {
client
.thread_list()
.ok()
.map(|threads| threads.len() as u16)
});
let product_label = match product {
Some(1) => "Workstation",
Some(2) => "DomainController",
Some(3) => "Server",
_ => "unknown",
};
outln!("{}", ui::label("target version"));
outln!(
" {} Windows {}.{} build {}{}",
ui::muted("target"),
major.map_or_else(|| "?".into(), |value| value.to_string()),
minor.map_or_else(|| "?".into(), |value| value.to_string()),
build.map_or_else(|| "?".into(), |value| value.to_string()),
shared_build_lab(target)
.map(|lab| format!(" ({lab})"))
.unwrap_or_default()
);
outln!(" {} {}", ui::muted("arch"), target.arch().label());
match (base, kernel.as_ref().map(|module| module.size)) {
(Some(base), Some(size)) => outln!(
" {} {} size {:#x}",
ui::muted("kernel"),
ui::addr(base.0),
size
),
(Some(base), None) => outln!(
" {} {} size unknown",
ui::muted("kernel"),
ui::addr(base.0)
),
_ => outln!(" {} unavailable", ui::muted("kernel")),
}
if let Some(identity) = identity {
outln!(
" {} ntoskrnl.pdb GUID {:032X} age {}",
ui::muted("pdb"),
identity.guid,
identity.age
);
} else {
outln!(" {} unavailable", ui::muted("pdb"));
}
outln!(
" {} {}",
ui::muted("processors"),
processors.map_or_else(|| "unknown".into(), |value| value.to_string())
);
outln!(" {} {}", ui::muted("product"), product_label);
outln!(
" {} {}",
ui::muted("uptime"),
uptime.map_or_else(|| "unknown".into(), format_uptime)
);
outln!(" {} {}", ui::muted("backend"), backend);
outln!(" {} {}", ui::muted("ntoseye"), env!("CARGO_PKG_VERSION"));
let symbol_path = target
.symbols
.symbol_sources()
.into_iter()
.map(|source| source.to_string())
.collect::<Vec<_>>()
.join("; ");
outln!(" {} {}", ui::muted("symbol path"), symbol_path);
if let Some(time) = system_time.and_then(filetime_to_iso) {
outln!(" {} {}", ui::muted("system time"), time);
}
}
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.chunks_exact(2) {
let value = u16::from_le_bytes([chunk[0], chunk[1]]);
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(code: u32, force_ntstatus: bool) {
let is_ntstatus = force_ntstatus || code >= 0xc000_0000;
if is_ntstatus {
let severity = match code >> 30 {
0 => "success",
1 => "informational",
2 => "warning",
_ => "error",
};
let facility = (code >> 16) & 0x0fff;
let name = ntstatus_name(code).unwrap_or("STATUS_UNKNOWN");
outln!("NTSTATUS {code:#010x}: {name}");
outln!(
" severity: {severity}; facility: {facility:#x}; customer: {}",
if code & 0x2000_0000 != 0 { "yes" } else { "no" }
);
return;
}
if code & 0x8000_0000 != 0 {
let severity = "error";
let facility = (code >> 16) & 0x1fff;
let win32 = if facility == 7 {
Some(code & 0xffff)
} else {
None
};
let name = win32
.and_then(win32_error_name)
.unwrap_or("HRESULT_UNKNOWN");
outln!("HRESULT {code:#010x}: {name}");
outln!(
" severity: {severity}; facility: {facility:#x}; customer: {}",
if code & 0x2000_0000 != 0 { "yes" } else { "no" }
);
if let Some(win32) = win32 {
outln!(" Win32 code: {win32} ({win32:#x})");
}
return;
}
let name = win32_error_name(code).unwrap_or("ERROR_UNKNOWN");
outln!("Win32 error {code} ({code:#x}): {name}");
}
#[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 %");
}
}