use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use crate::error::DetectionError;
pub(crate) const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
pub(crate) const MAX_STDOUT_BYTES: usize = 1024 * 1024;
const MAX_STDERR_BYTES: usize = 4096;
const SANITIZED_ENV_VARS: &[&str] = &[
"LD_PRELOAD",
"LD_LIBRARY_PATH",
"DYLD_INSERT_LIBRARIES",
"DYLD_LIBRARY_PATH",
];
#[derive(Debug)]
pub(crate) struct ToolOutput {
pub stdout: String,
}
pub(crate) fn run_tool(
tool: &str,
args: &[&str],
timeout: Duration,
) -> Result<ToolOutput, DetectionError> {
let abs_path = which(tool).ok_or_else(|| DetectionError::ToolNotFound { tool: tool.into() })?;
let mut cmd = Command::new(&abs_path);
cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
for var in SANITIZED_ENV_VARS {
cmd.env_remove(var);
}
let mut child = cmd
.spawn()
.map_err(|_| DetectionError::ToolNotFound { tool: tool.into() })?;
let start = Instant::now();
loop {
match child.try_wait() {
Ok(Some(_)) => break,
Ok(None) if start.elapsed() > timeout => {
let _ = child.kill();
for _ in 0..10 {
if let Ok(Some(_)) = child.try_wait() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
return Err(DetectionError::Timeout {
tool: tool.into(),
timeout_secs: timeout.as_secs_f64(),
});
}
Ok(None) => std::thread::sleep(Duration::from_millis(10)),
Err(e) => {
return Err(DetectionError::ToolFailed {
tool: tool.into(),
exit_code: None,
stderr: e.to_string(),
});
}
}
}
let stdout_bytes = read_limited(child.stdout.take(), MAX_STDOUT_BYTES);
let stderr_bytes = read_limited(child.stderr.take(), MAX_STDERR_BYTES);
let status = child.wait().map_err(|e| DetectionError::ToolFailed {
tool: tool.into(),
exit_code: None,
stderr: e.to_string(),
})?;
if !status.success() {
return Err(DetectionError::ToolFailed {
tool: tool.into(),
exit_code: status.code(),
stderr: String::from_utf8_lossy(&stderr_bytes).into_owned(),
});
}
Ok(ToolOutput {
stdout: String::from_utf8_lossy(&stdout_bytes).into_owned(),
})
}
fn read_limited(pipe: Option<impl Read>, limit: usize) -> Vec<u8> {
let Some(mut reader) = pipe else {
return Vec::new();
};
let mut out = Vec::with_capacity(limit.min(8192));
let mut buf = [0u8; 8192];
loop {
let remaining = limit.saturating_sub(out.len());
if remaining == 0 {
break;
}
let to_read = buf.len().min(remaining);
match reader.read(&mut buf[..to_read]) {
Ok(0) | Err(_) => break,
Ok(n) => out.extend_from_slice(&buf[..n]),
}
}
out
}
fn which(name: &str) -> Option<PathBuf> {
let path_var = std::env::var("PATH").ok()?;
let sep = if cfg!(windows) { ';' } else { ':' };
let extensions: &[&str] = if cfg!(windows) && !name.contains('.') {
&["", ".exe", ".cmd", ".bat"]
} else {
&[""]
};
for dir in path_var.split(sep) {
for ext in extensions {
let mut candidate = Path::new(dir).join(name);
if !ext.is_empty() {
let mut with_ext = candidate.as_os_str().to_os_string();
with_ext.push(ext);
candidate = PathBuf::from(with_ext);
}
if candidate.is_file() {
return Some(candidate);
}
}
}
None
}
#[cfg(feature = "async-detect")]
pub(crate) async fn run_tool_async(
tool: &str,
args: &[&str],
timeout: Duration,
) -> Result<ToolOutput, DetectionError> {
let abs_path = which(tool).ok_or_else(|| DetectionError::ToolNotFound { tool: tool.into() })?;
let mut cmd = tokio::process::Command::new(&abs_path);
cmd.args(args)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
for var in SANITIZED_ENV_VARS {
cmd.env_remove(var);
}
let child = cmd
.spawn()
.map_err(|_| DetectionError::ToolNotFound { tool: tool.into() })?;
let result: Result<std::process::Output, _> =
match tokio::time::timeout(timeout, child.wait_with_output()).await {
Ok(r) => r,
Err(_elapsed) => {
return Err(DetectionError::Timeout {
tool: tool.into(),
timeout_secs: timeout.as_secs_f64(),
});
}
};
let output = result.map_err(|e| DetectionError::ToolFailed {
tool: tool.into(),
exit_code: None,
stderr: e.to_string(),
})?;
if !output.status.success() {
let stderr = &output.stderr[..output.stderr.len().min(MAX_STDERR_BYTES)];
return Err(DetectionError::ToolFailed {
tool: tool.into(),
exit_code: output.status.code(),
stderr: String::from_utf8_lossy(stderr).into_owned(),
});
}
let stdout = &output.stdout[..output.stdout.len().min(MAX_STDOUT_BYTES)];
Ok(ToolOutput {
stdout: String::from_utf8_lossy(stdout).into_owned(),
})
}
pub(crate) fn validate_device_id(raw: &str, backend: &str) -> Result<u32, DetectionError> {
let id: u32 = raw.parse().map_err(|e| DetectionError::ParseError {
backend: backend.into(),
message: format!("invalid device id '{}': {}", raw, e),
})?;
if id > 1024 {
return Err(DetectionError::ParseError {
backend: backend.into(),
message: format!("device id {} exceeds maximum (1024)", id),
});
}
Ok(id)
}
pub(crate) fn parse_csv_line<'a>(
line: &'a str,
min_fields: usize,
backend: &str,
) -> Result<Vec<&'a str>, DetectionError> {
let fields: Vec<&str> = line.split(',').take(20).map(|s| s.trim()).collect();
if fields.len() < min_fields {
return Err(DetectionError::ParseError {
backend: backend.into(),
message: format!(
"expected {}+ CSV fields, got {}: {}",
min_fields,
fields.len(),
line
),
});
}
Ok(fields)
}
pub(crate) fn validate_memory_mb(raw: &str, backend: &str) -> Result<u64, DetectionError> {
let mb: u64 = raw.parse().map_err(|e| DetectionError::ParseError {
backend: backend.into(),
message: format!("invalid memory '{}': {}", raw, e),
})?;
if mb > 16 * 1024 * 1024 {
return Err(DetectionError::ParseError {
backend: backend.into(),
message: format!("memory {} MB exceeds sanity limit (16 TiB)", mb),
});
}
Ok(mb)
}