use std::fmt::Write as _;
use std::path::PathBuf;
use clap::{Args, Subcommand, ValueEnum};
pub const DEFAULT_ALIGN: usize = 8;
pub const DEFAULT_BEFORE_WORDS: usize = 2;
pub const DEFAULT_AFTER_WORDS: usize = 6;
pub const MAX_CONTEXT_WORDS: usize = 16;
pub const MAX_PATTERN_WORDS: usize = 64;
pub const DEFAULT_MAX_HITS: usize = 32;
pub const MAX_HITS_CAP: usize = 1024;
pub const DEFAULT_MAX_BYTES: u64 = 268_435_456;
pub const DEFAULT_TIMEOUT_MS: u64 = 5000;
pub const DEFAULT_PROBE_DURATION_MS: u64 = 1000;
pub const MAX_PROBE_DURATION_MS: u64 = 60_000;
pub const DEFAULT_MAX_PROBE_EVENTS: usize = 4096;
pub const MAX_PROBE_EVENTS_CAP: usize = 65_536;
pub const MAX_PROBES_PER_SESSION: usize = 16;
pub const MAX_WATCHES_PER_SESSION: usize = 4;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WatchAccessMode {
#[default]
Write,
Rw,
}
impl WatchAccessMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Write => "write",
Self::Rw => "rw",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize)]
pub enum WatchLen {
B1 = 1,
B2 = 2,
B4 = 4,
#[default]
B8 = 8,
}
impl WatchLen {
#[must_use]
pub const fn as_u8(self) -> u8 {
match self {
Self::B1 => 1,
Self::B2 => 2,
Self::B4 => 4,
Self::B8 => 8,
}
}
}
pub fn parse_watch_len(text: &str) -> Result<WatchLen, String> {
match text.trim() {
"1" => Ok(WatchLen::B1),
"2" => Ok(WatchLen::B2),
"4" => Ok(WatchLen::B4),
"8" => Ok(WatchLen::B8),
_ => Err(format!("invalid --len {text}: allowed hardware watchpoint lengths are 1, 2, 4, or 8")),
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
pub enum ProbeBackend {
#[default]
Perf,
Tracefs,
}
impl ProbeBackend {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Perf => "perf",
Self::Tracefs => "tracefs",
}
}
}
#[derive(Clone, Debug, Args)]
pub struct LiveCommand {
#[command(subcommand)]
pub subcommand: LiveSubcommand,
}
#[derive(Clone, Debug, Subcommand)]
pub enum LiveSubcommand {
Maps {
#[arg(long)]
pid: u32,
#[arg(long)]
force_bin: bool,
#[arg(long)]
json: bool,
},
Resolve {
#[arg(long)]
pid: u32,
#[arg(long)]
force_bin: bool,
#[arg(long)]
json: bool,
#[arg(required = true, value_name = "ADDR", value_parser = crate::cli::parse_number)]
addrs: Vec<u64>,
},
Memscan {
#[arg(long)]
pid: u32,
#[arg(long, default_value_t = DEFAULT_ALIGN, value_parser = parse_align)]
align: usize,
#[arg(long, default_value_t = DEFAULT_BEFORE_WORDS, value_parser = parse_context_words)]
before_words: usize,
#[arg(long, default_value_t = DEFAULT_AFTER_WORDS, value_parser = parse_context_words)]
after_words: usize,
#[arg(long, default_value_t = DEFAULT_MAX_HITS)]
max_hits: usize,
#[arg(long, default_value_t = DEFAULT_MAX_BYTES)]
max_bytes: u64,
#[arg(long, default_value_t = DEFAULT_TIMEOUT_MS)]
timeout_ms: u64,
#[arg(long)]
force_bin: bool,
#[arg(long)]
json: bool,
#[arg(required = true, value_name = "VALUES", value_parser = crate::cli::parse_number)]
values: Vec<u64>,
},
Probe {
#[arg(long)]
pid: u32,
#[arg(long, value_enum, default_value_t = ProbeBackend::Perf)]
backend: ProbeBackend,
#[arg(long)]
count: bool,
#[arg(long)]
callers: bool,
#[arg(long)]
regs: bool,
#[arg(long, default_value_t = DEFAULT_PROBE_DURATION_MS)]
duration_ms: u64,
#[arg(long, default_value_t = DEFAULT_MAX_PROBE_EVENTS)]
max_events: usize,
#[arg(long)]
force_bin: bool,
#[arg(long)]
json: bool,
#[arg(required = true, value_name = "ADDR", value_parser = crate::cli::parse_number)]
addrs: Vec<u64>,
},
Watch {
#[arg(long)]
pid: u32,
#[arg(long, default_value = "8", value_parser = parse_watch_len)]
len: WatchLen,
#[arg(long, value_enum, default_value_t = WatchAccessMode::Write)]
access: WatchAccessMode,
#[arg(long)]
regs: bool,
#[arg(long, default_value_t = DEFAULT_PROBE_DURATION_MS)]
duration_ms: u64,
#[arg(long, default_value_t = DEFAULT_MAX_PROBE_EVENTS)]
max_events: usize,
#[arg(long)]
force_bin: bool,
#[arg(long)]
json: bool,
#[arg(long = "follow", value_name = "SPEC")]
follow: Vec<String>,
#[arg(value_name = "ADDR", value_parser = crate::cli::parse_number)]
addrs: Vec<u64>,
},
#[command(hide = true)]
TracefsWatchdog {
#[arg(long)]
tracefs_root: PathBuf,
#[arg(long)]
instance: String,
#[arg(long = "event")]
events: Vec<String>,
#[arg(long)]
parent_test_hold: bool,
},
}
impl LiveCommand {
#[cfg(all(test, target_os = "linux"))]
#[must_use]
pub const fn maps(pid: u32, force_bin: bool, json: bool) -> Self {
Self { subcommand: LiveSubcommand::Maps { pid, force_bin, json } }
}
#[cfg(all(test, target_os = "linux"))]
#[must_use]
pub const fn resolve(pid: u32, force_bin: bool, json: bool, addrs: Vec<u64>) -> Self {
Self { subcommand: LiveSubcommand::Resolve { pid, force_bin, json, addrs } }
}
#[must_use]
pub const fn pid(&self) -> u32 {
match &self.subcommand {
LiveSubcommand::Maps { pid, .. }
| LiveSubcommand::Resolve { pid, .. }
| LiveSubcommand::Memscan { pid, .. }
| LiveSubcommand::Probe { pid, .. }
| LiveSubcommand::Watch { pid, .. } => *pid,
LiveSubcommand::TracefsWatchdog { .. } => 0,
}
}
pub fn validate(&self) -> Result<(), String> {
if matches!(self.subcommand, LiveSubcommand::TracefsWatchdog { .. }) {
return Ok(());
}
if self.pid() == 0 {
return Err("invalid --pid 0: process ID must be non-zero".to_owned());
}
match &self.subcommand {
LiveSubcommand::Maps { .. } | LiveSubcommand::TracefsWatchdog { .. } => Ok(()),
LiveSubcommand::Resolve { addrs, .. } => {
if addrs.is_empty() {
return Err("resolve requires at least one <ADDR>".to_owned());
}
Ok(())
}
LiveSubcommand::Memscan { align, before_words, after_words, max_hits, values, .. } => {
validate_align(*align)?;
validate_context_words(*before_words, "--before-words")?;
validate_context_words(*after_words, "--after-words")?;
if *max_hits == 0 || *max_hits > MAX_HITS_CAP {
return Err(format!("--max-hits {max_hits} must be in 1..={MAX_HITS_CAP}"));
}
if values.is_empty() || values.len() > MAX_PATTERN_WORDS {
return Err(format!("memscan requires 1..={MAX_PATTERN_WORDS} <VALUES> words"));
}
Ok(())
}
LiveSubcommand::Probe { duration_ms, max_events, addrs, .. } => {
if *duration_ms == 0 || *duration_ms > MAX_PROBE_DURATION_MS {
return Err(format!("--duration-ms {duration_ms} must be in 1..={MAX_PROBE_DURATION_MS}"));
}
if *max_events == 0 || *max_events > MAX_PROBE_EVENTS_CAP {
return Err(format!("--max-events {max_events} must be in 1..={MAX_PROBE_EVENTS_CAP}"));
}
if addrs.is_empty() || addrs.len() > MAX_PROBES_PER_SESSION {
return Err(format!("probe requires 1..={MAX_PROBES_PER_SESSION} <ADDR> targets"));
}
Ok(())
}
LiveSubcommand::Watch { duration_ms, max_events, len, access, follow, addrs, .. } => {
if *duration_ms == 0 || *duration_ms > MAX_PROBE_DURATION_MS {
return Err(format!("--duration-ms {duration_ms} must be in 1..={MAX_PROBE_DURATION_MS}"));
}
if *max_events == 0 || *max_events > MAX_PROBE_EVENTS_CAP {
return Err(format!("--max-events {max_events} must be in 1..={MAX_PROBE_EVENTS_CAP}"));
}
let mut total_slots = addrs.len();
for spec_str in follow {
let parsed = crate::live::watch::rebind::parse_follow_spec(spec_str, *len, *access)?;
total_slots = total_slots.saturating_add(parsed.hardware_slots_required());
}
if total_slots == 0 || total_slots > MAX_WATCHES_PER_SESSION {
return Err(format!(
"watch requires 1..={MAX_WATCHES_PER_SESSION} total hardware watchpoint slots across <ADDR> and --follow (requested {total_slots}; x86-64 DR0..DR3 hardware limit)"
));
}
Ok(())
}
}
}
}
pub fn parse_align(text: &str) -> Result<usize, String> {
let align = text.parse::<usize>().map_err(|err| format!("invalid --align {text}: {err}"))?;
validate_align(align)?;
Ok(align)
}
pub fn validate_align(align: usize) -> Result<(), String> {
if matches!(align, 1 | 2 | 4 | 8) {
Ok(())
} else {
Err(format!("invalid --align {align}: allowed values are 1, 2, 4, or 8"))
}
}
pub fn parse_context_words(text: &str) -> Result<usize, String> {
let words = text.parse::<usize>().map_err(|err| format!("invalid context word count {text}: {err}"))?;
validate_context_words(words, "context words")?;
Ok(words)
}
pub fn validate_context_words(words: usize, flag: &str) -> Result<(), String> {
if words <= MAX_CONTEXT_WORDS { Ok(()) } else { Err(format!("{flag} {words} exceeds maximum {MAX_CONTEXT_WORDS}")) }
}
#[must_use]
pub fn encode_values_le(values: &[u64]) -> Vec<u8> {
let mut needle = Vec::with_capacity(values.len().saturating_mul(8));
for &word in values {
needle.extend_from_slice(&word.to_le_bytes());
}
needle
}
#[must_use]
pub fn format_bytes_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(2 + bytes.len().saturating_mul(2));
out.push_str("0x");
for &byte in bytes {
let _ = write!(out, "{byte:02x}");
}
out
}