use async_trait::async_trait;
use std::path::Path;
use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData};
use crate::tools::{ExecContext, ParamSchema, Tool, ToolArgs, ToolSchema};
pub struct Xxd;
#[async_trait]
impl Tool for Xxd {
fn name(&self) -> &str {
"xxd"
}
fn schema(&self) -> ToolSchema {
ToolSchema::new("xxd", "Make a hex dump or reverse it")
.param(ParamSchema::optional(
"path",
"string",
Value::Null,
"File to read (reads stdin if not provided)",
))
.param(
ParamSchema::optional(
"plain",
"bool",
Value::Bool(false),
"Plain hex dump — no address, no ASCII (-p)",
)
.with_aliases(["-p"]),
)
.param(
ParamSchema::optional(
"reverse",
"bool",
Value::Bool(false),
"Reverse: convert hex dump back to binary (-r)",
)
.with_aliases(["-r"]),
)
.param(
ParamSchema::optional(
"length",
"int",
Value::Null,
"Limit output to N bytes (-l)",
)
.with_aliases(["-l"]),
)
.param(
ParamSchema::optional(
"seek",
"int",
Value::Null,
"Skip N bytes from start (-s)",
)
.with_aliases(["-s"]),
)
.example("Hex dump", "xxd file.bin")
.example("Plain hex", "echo hello | xxd -p")
.example("Reverse hex", "echo 68656c6c6f | xxd -r -p")
}
async fn execute(&self, args: ToolArgs, ctx: &mut ExecContext) -> ExecResult {
let plain = args.has_flag("plain") || args.has_flag("p");
let reverse = args.has_flag("reverse") || args.has_flag("r");
let length = args.get("length", usize::MAX).and_then(|v| match v {
Value::Int(i) => Some(*i as usize),
Value::String(s) => s.parse().ok(),
_ => None,
});
let seek = args
.get("seek", usize::MAX)
.and_then(|v| match v {
Value::Int(i) => Some(*i as usize),
Value::String(s) => s.parse().ok(),
_ => None,
})
.unwrap_or(0);
let paths = match ctx.expand_paths(&args.positional).await {
Ok(p) => p,
Err(e) => return ExecResult::failure(1, format!("xxd: {}", e)),
};
let input = match paths.first() {
Some(path) => {
let resolved = ctx.resolve_path(path);
match ctx.backend.read(Path::new(&resolved), None).await {
Ok(data) => String::from_utf8_lossy(&data).into_owned(),
Err(e) => return ExecResult::failure(1, format!("xxd: {}: {}", path, e)),
}
}
None => ctx.read_stdin_to_string().await.unwrap_or_default(),
};
if reverse {
return reverse_hex(&input, plain);
}
let bytes = input.as_bytes();
let bytes = if seek < bytes.len() {
&bytes[seek..]
} else {
&[]
};
let bytes = match length {
Some(n) if n < bytes.len() => &bytes[..n],
_ => bytes,
};
let output = if plain {
plain_hex(bytes)
} else {
classic_hex(bytes, seek)
};
ExecResult::with_output(OutputData::text(output))
}
}
fn classic_hex(bytes: &[u8], base_offset: usize) -> String {
let mut output = String::new();
for (i, chunk) in bytes.chunks(16).enumerate() {
let addr = base_offset + i * 16;
output.push_str(&format!("{:08x}: ", addr));
for (j, byte) in chunk.iter().enumerate() {
output.push_str(&format!("{:02x}", byte));
if j % 2 == 1 {
output.push(' ');
}
}
let hex_width = chunk.len() * 2 + chunk.len() / 2;
let full_width = 16 * 2 + 8; for _ in hex_width..full_width {
output.push(' ');
}
output.push(' ');
for byte in chunk {
if byte.is_ascii_graphic() || *byte == b' ' {
output.push(*byte as char);
} else {
output.push('.');
}
}
output.push('\n');
}
if output.ends_with('\n') {
output.pop();
}
output
}
fn plain_hex(bytes: &[u8]) -> String {
let mut output = String::new();
for (i, byte) in bytes.iter().enumerate() {
output.push_str(&format!("{:02x}", byte));
if i > 0 && (i + 1) % 30 == 0 {
output.push('\n');
}
}
output
}
fn reverse_hex(input: &str, plain: bool) -> ExecResult {
let hex_str = if plain {
input
.chars()
.filter(|c| c.is_ascii_hexdigit())
.collect::<String>()
} else {
let mut hex = String::new();
for line in input.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Some(after_addr) = line.split(": ").nth(1) {
let hex_part = after_addr.split(" ").next().unwrap_or("");
for ch in hex_part.chars() {
if ch.is_ascii_hexdigit() {
hex.push(ch);
}
}
}
}
hex
};
let mut bytes = Vec::with_capacity(hex_str.len() / 2);
let chars: Vec<char> = hex_str.chars().collect();
let mut i = 0;
while i + 1 < chars.len() {
let high = chars[i].to_digit(16);
let low = chars[i + 1].to_digit(16);
match (high, low) {
(Some(h), Some(l)) => bytes.push((h * 16 + l) as u8),
_ => {
return ExecResult::failure(
1,
format!("xxd: invalid hex at position {}", i),
)
}
}
i += 2;
}
let text = String::from_utf8_lossy(&bytes);
ExecResult::with_output(OutputData::text(text.into_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::{MemoryFs, VfsRouter};
use std::sync::Arc;
async fn make_ctx() -> ExecContext {
let mut vfs = VfsRouter::new();
vfs.mount("/", MemoryFs::new());
ExecContext::new(Arc::new(vfs))
}
#[tokio::test]
async fn test_xxd_classic() {
let mut ctx = make_ctx().await;
ctx.set_stdin("hello".to_string());
let args = ToolArgs::new();
let result = Xxd.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.text_out().as_ref().starts_with("00000000:"));
assert!(result.text_out().as_ref().contains("6865"));
assert!(result.text_out().as_ref().contains("hello"));
}
#[tokio::test]
async fn test_xxd_plain() {
let mut ctx = make_ctx().await;
ctx.set_stdin("hello".to_string());
let mut args = ToolArgs::new();
args.named.insert("plain".to_string(), Value::Bool(true));
let result = Xxd.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().as_ref(), "68656c6c6f");
}
#[tokio::test]
async fn test_xxd_reverse_plain() {
let mut ctx = make_ctx().await;
ctx.set_stdin("68656c6c6f".to_string());
let mut args = ToolArgs::new();
args.named
.insert("reverse".to_string(), Value::Bool(true));
args.named.insert("plain".to_string(), Value::Bool(true));
let result = Xxd.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().as_ref(), "hello");
}
#[tokio::test]
async fn test_xxd_roundtrip() {
let mut ctx = make_ctx().await;
let original = "test data 123";
ctx.set_stdin(original.to_string());
let mut args = ToolArgs::new();
args.named.insert("plain".to_string(), Value::Bool(true));
let hex = Xxd.execute(args, &mut ctx).await;
assert!(hex.ok());
ctx.set_stdin(hex.text_out().into_owned());
let mut args2 = ToolArgs::new();
args2
.named
.insert("reverse".to_string(), Value::Bool(true));
args2.named.insert("plain".to_string(), Value::Bool(true));
let decoded = Xxd.execute(args2, &mut ctx).await;
assert!(decoded.ok());
assert_eq!(decoded.text_out().as_ref(), original);
}
#[tokio::test]
async fn test_xxd_seek() {
let mut ctx = make_ctx().await;
ctx.set_stdin("abcdef".to_string());
let mut args = ToolArgs::new();
args.named.insert("seek".to_string(), Value::Int(3));
args.named.insert("plain".to_string(), Value::Bool(true));
let result = Xxd.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().as_ref(), "646566"); }
#[tokio::test]
async fn test_xxd_length() {
let mut ctx = make_ctx().await;
ctx.set_stdin("abcdef".to_string());
let mut args = ToolArgs::new();
args.named.insert("length".to_string(), Value::Int(3));
args.named.insert("plain".to_string(), Value::Bool(true));
let result = Xxd.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().as_ref(), "616263"); }
#[tokio::test]
async fn test_xxd_empty() {
let mut ctx = make_ctx().await;
ctx.set_stdin("".to_string());
let args = ToolArgs::new();
let result = Xxd.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().as_ref(), "");
}
#[tokio::test]
async fn test_xxd_reverse_classic_format() {
let mut ctx = make_ctx().await;
ctx.set_stdin("00000000: 6865 6c6c 6f hello".to_string());
let mut args = ToolArgs::new();
args.named
.insert("reverse".to_string(), Value::Bool(true));
let result = Xxd.execute(args, &mut ctx).await;
assert!(result.ok());
assert_eq!(result.text_out().as_ref(), "hello");
}
}