use async_trait::async_trait;
use super::{Builtin, Context};
use crate::error::Result;
use crate::interpreter::ExecResult;
pub struct Od;
pub struct Xxd;
pub struct Hexdump;
struct OdOptions {
addr_radix: AddrRadix,
output_type: OutputType,
count: Option<usize>,
skip: usize,
}
#[derive(Clone, Copy)]
enum AddrRadix {
Octal,
Decimal,
Hex,
None,
}
#[derive(Clone, Copy)]
enum OutputType {
Octal,
Hex,
Decimal,
Char,
}
fn parse_od_args(args: &[String]) -> std::result::Result<(OdOptions, Vec<String>), String> {
let mut opts = OdOptions {
addr_radix: AddrRadix::Octal,
output_type: OutputType::Octal,
count: None,
skip: 0,
};
let mut files = Vec::new();
let mut p = super::arg_parser::ArgParser::new(args);
while !p.is_done() {
if let Some(val) = p.flag_value("-A", "od")? {
opts.addr_radix = match val {
"d" => AddrRadix::Decimal,
"o" => AddrRadix::Octal,
"x" => AddrRadix::Hex,
"n" => AddrRadix::None,
other => return Err(format!("od: invalid address radix: '{}'", other)),
};
} else if let Some(val) = p.flag_value("-t", "od")? {
opts.output_type = match val.chars().next() {
Some('o') => OutputType::Octal,
Some('x') => OutputType::Hex,
Some('d') => OutputType::Decimal,
Some('c') => OutputType::Char,
_ => return Err(format!("od: invalid output type: '{}'", val)),
};
} else if let Some(val) = p.flag_value("-N", "od")? {
opts.count = Some(
val.parse()
.map_err(|_| format!("od: invalid count: '{}'", val))?,
);
} else if let Some(val) = p.flag_value("-j", "od")? {
opts.skip = val
.parse()
.map_err(|_| format!("od: invalid skip: '{}'", val))?;
} else if p.flag("-x") {
opts.output_type = OutputType::Hex;
} else if p.flag("-c") {
opts.output_type = OutputType::Char;
} else if p.flag("-d") {
opts.output_type = OutputType::Decimal;
} else if let Some(arg) = p.positional() {
files.push(arg.to_string());
}
}
Ok((opts, files))
}
fn format_od_addr(offset: usize, radix: AddrRadix) -> String {
match radix {
AddrRadix::Octal => format!("{:07o}", offset),
AddrRadix::Decimal => format!("{:07}", offset),
AddrRadix::Hex => format!("{:07x}", offset),
AddrRadix::None => String::new(),
}
}
fn format_od_byte(byte: u8, output_type: OutputType) -> String {
match output_type {
OutputType::Octal => format!(" {:03o}", byte),
OutputType::Hex => format!(" {:02x}", byte),
OutputType::Decimal => format!(" {:3}", byte),
OutputType::Char => {
let c = match byte {
0 => "\\0".to_string(),
7 => "\\a".to_string(),
8 => "\\b".to_string(),
9 => "\\t".to_string(),
10 => "\\n".to_string(),
11 => "\\v".to_string(),
12 => "\\f".to_string(),
13 => "\\r".to_string(),
0x20..=0x7e => format!(" {}", byte as char),
_ => format!(" {:03o}", byte),
};
format!(" {}", c.trim_start())
}
}
}
fn od_dump(data: &[u8], opts: &OdOptions) -> String {
let bytes_per_line = 16;
let mut output = String::new();
let data = if opts.skip < data.len() {
&data[opts.skip..]
} else {
&[]
};
let data = match opts.count {
Some(n) => &data[..data.len().min(n)],
None => data,
};
for (chunk_idx, chunk) in data.chunks(bytes_per_line).enumerate() {
let offset = opts.skip + chunk_idx * bytes_per_line;
let addr = format_od_addr(offset, opts.addr_radix);
if !addr.is_empty() {
output.push_str(&addr);
}
for byte in chunk {
output.push_str(&format_od_byte(*byte, opts.output_type));
}
output.push('\n');
}
if !data.is_empty() {
let final_offset = opts.skip + data.len();
let addr = format_od_addr(final_offset, opts.addr_radix);
if !addr.is_empty() {
output.push_str(&addr);
output.push('\n');
}
}
output
}
#[async_trait]
impl Builtin for Od {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let (opts, files) = match parse_od_args(ctx.args) {
Ok(v) => v,
Err(e) => return Ok(ExecResult::err(format!("{}\n", e), 1)),
};
let data = collect_input(ctx.stdin, &files, ctx.cwd, &ctx.fs).await?;
let output = od_dump(&data, &opts);
Ok(ExecResult::ok(output))
}
}
struct XxdOptions {
length: Option<usize>,
offset: usize,
cols: usize,
group: usize,
plain: bool,
reverse: bool,
}
fn parse_xxd_args(args: &[String]) -> std::result::Result<(XxdOptions, Vec<String>), String> {
let mut opts = XxdOptions {
length: None,
offset: 0,
cols: 16,
group: 2,
plain: false,
reverse: false,
};
let mut files = Vec::new();
let mut p = super::arg_parser::ArgParser::new(args);
while !p.is_done() {
if let Some(val) = p.flag_value("-l", "xxd")? {
opts.length = Some(
val.parse()
.map_err(|_| format!("xxd: invalid length: '{}'", val))?,
);
} else if let Some(val) = p.flag_value("-s", "xxd")? {
opts.offset = val
.parse()
.map_err(|_| format!("xxd: invalid offset: '{}'", val))?;
} else if let Some(val) = p.flag_value("-c", "xxd")? {
opts.cols = val
.parse()
.map_err(|_| format!("xxd: invalid cols: '{}'", val))?;
if opts.cols == 0 {
opts.cols = 16;
}
} else if let Some(val) = p.flag_value("-g", "xxd")? {
opts.group = val
.parse()
.map_err(|_| format!("xxd: invalid group: '{}'", val))?;
} else if p.flag("-p") {
opts.plain = true;
} else if p.flag("-r") {
opts.reverse = true;
} else if let Some(arg) = p.positional() {
files.push(arg.to_string());
}
}
Ok((opts, files))
}
fn xxd_dump(data: &[u8], opts: &XxdOptions) -> String {
let mut output = String::new();
let data = if opts.offset < data.len() {
&data[opts.offset..]
} else {
&[]
};
let data = match opts.length {
Some(n) => &data[..data.len().min(n)],
None => data,
};
if opts.plain {
for byte in data {
output.push_str(&format!("{:02x}", byte));
}
if !data.is_empty() {
output.push('\n');
}
return output;
}
for (chunk_idx, chunk) in data.chunks(opts.cols).enumerate() {
let offset = opts.offset + chunk_idx * opts.cols;
output.push_str(&format!("{:08x}: ", offset));
for (j, byte) in chunk.iter().enumerate() {
if j > 0 && opts.group > 0 && j % opts.group == 0 {
output.push(' ');
}
output.push_str(&format!("{:02x}", byte));
}
let missing = opts.cols - chunk.len();
for k in 0..missing {
if (chunk.len() + k) > 0 && opts.group > 0 && (chunk.len() + k) % opts.group == 0 {
output.push(' ');
}
output.push_str(" ");
}
output.push_str(" ");
for byte in chunk {
if *byte >= 0x20 && *byte < 0x7f {
output.push(*byte as char);
} else {
output.push('.');
}
}
output.push('\n');
}
output
}
fn decode_hex(hex: &str) -> Vec<u8> {
let clean: String = hex.chars().filter(|c| c.is_ascii_hexdigit()).collect();
clean
.as_bytes()
.chunks(2)
.filter_map(|pair| {
if pair.len() == 2 {
u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()
} else {
None
}
})
.collect()
}
fn xxd_reverse(data: &[u8], plain: bool) -> Vec<u8> {
let text = String::from_utf8_lossy(data);
if plain {
return decode_hex(&text);
}
let mut result = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let hex_part = match line.find(':') {
Some(idx) => &line[idx + 1..],
None => line,
};
let hex_part = match hex_part.find(" ") {
Some(idx) => &hex_part[..idx],
None => hex_part,
};
result.extend(decode_hex(hex_part));
}
result
}
#[async_trait]
impl Builtin for Xxd {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let (opts, files) = match parse_xxd_args(ctx.args) {
Ok(v) => v,
Err(e) => return Ok(ExecResult::err(format!("{}\n", e), 1)),
};
let data = collect_input(ctx.stdin, &files, ctx.cwd, &ctx.fs).await?;
if opts.reverse {
let bytes = xxd_reverse(&data, opts.plain);
let output = String::from_utf8_lossy(&bytes).to_string();
Ok(ExecResult::ok(output))
} else {
let output = xxd_dump(&data, &opts);
Ok(ExecResult::ok(output))
}
}
}
struct HexdumpOptions {
canonical: bool,
length: Option<usize>,
offset: usize,
}
fn parse_hexdump_args(
args: &[String],
) -> std::result::Result<(HexdumpOptions, Vec<String>), String> {
let mut opts = HexdumpOptions {
canonical: false,
length: None,
offset: 0,
};
let mut files = Vec::new();
let mut p = super::arg_parser::ArgParser::new(args);
while !p.is_done() {
if p.flag("-C") {
opts.canonical = true;
} else if let Some(val) = p.flag_value("-n", "hexdump")? {
opts.length = Some(
val.parse()
.map_err(|_| format!("hexdump: invalid length: '{}'", val))?,
);
} else if let Some(val) = p.flag_value("-s", "hexdump")? {
opts.offset = val
.parse()
.map_err(|_| format!("hexdump: invalid offset: '{}'", val))?;
} else if let Some(arg) = p.positional() {
files.push(arg.to_string());
}
}
Ok((opts, files))
}
fn hexdump_dump(data: &[u8], opts: &HexdumpOptions) -> String {
let mut output = String::new();
let data = if opts.offset < data.len() {
&data[opts.offset..]
} else {
&[]
};
let data = match opts.length {
Some(n) => &data[..data.len().min(n)],
None => data,
};
if opts.canonical {
for (chunk_idx, chunk) in data.chunks(16).enumerate() {
let offset = opts.offset + chunk_idx * 16;
output.push_str(&format!("{:08x} ", offset));
for j in 0..8 {
if j < chunk.len() {
output.push_str(&format!("{:02x} ", chunk[j]));
} else {
output.push_str(" ");
}
}
output.push(' ');
for j in 8..16 {
if j < chunk.len() {
output.push_str(&format!("{:02x} ", chunk[j]));
} else {
output.push_str(" ");
}
}
output.push_str(" |");
for byte in chunk {
if *byte >= 0x20 && *byte < 0x7f {
output.push(*byte as char);
} else {
output.push('.');
}
}
output.push_str("|\n");
}
if !data.is_empty() {
let final_offset = opts.offset + data.len();
output.push_str(&format!("{:08x}\n", final_offset));
}
} else {
for (chunk_idx, chunk) in data.chunks(16).enumerate() {
let offset = opts.offset + chunk_idx * 16;
output.push_str(&format!("{:07x}", offset));
for pair in chunk.chunks(2) {
if pair.len() == 2 {
let word = (pair[1] as u16) << 8 | pair[0] as u16;
output.push_str(&format!(" {:04x}", word));
} else {
output.push_str(&format!(" {:04x}", pair[0] as u16));
}
}
output.push('\n');
}
if !data.is_empty() {
let final_offset = opts.offset + data.len();
output.push_str(&format!("{:07x}\n", final_offset));
}
}
output
}
#[async_trait]
impl Builtin for Hexdump {
async fn execute(&self, ctx: Context<'_>) -> Result<ExecResult> {
let (opts, files) = match parse_hexdump_args(ctx.args) {
Ok(v) => v,
Err(e) => return Ok(ExecResult::err(format!("{}\n", e), 1)),
};
let data = collect_input(ctx.stdin, &files, ctx.cwd, &ctx.fs).await?;
let output = hexdump_dump(&data, &opts);
Ok(ExecResult::ok(output))
}
}
async fn collect_input(
stdin: Option<&str>,
files: &[String],
cwd: &std::path::Path,
fs: &std::sync::Arc<dyn crate::fs::FileSystem>,
) -> Result<Vec<u8>> {
let mut data = Vec::new();
if files.is_empty() {
if let Some(stdin) = stdin {
data.extend_from_slice(stdin.as_bytes());
}
} else {
for file in files {
if file == "-" {
if let Some(stdin) = stdin {
data.extend_from_slice(stdin.as_bytes());
}
} else {
let path = if file.starts_with('/') {
std::path::PathBuf::from(file)
} else {
cwd.join(file)
};
let content = fs
.read_file(&path)
.await
.map_err(|e| crate::error::Error::Internal(format!("{}: {}", file, e)))?;
data.extend_from_slice(&content);
}
}
}
Ok(data)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use crate::fs::{FileSystem, InMemoryFs};
async fn run_od(args: &[&str], stdin: Option<&str>) -> ExecResult {
let fs = Arc::new(InMemoryFs::new());
let mut variables = HashMap::new();
let env = HashMap::new();
let mut cwd = PathBuf::from("/");
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
Od.execute(ctx).await.unwrap()
}
async fn run_xxd(args: &[&str], stdin: Option<&str>) -> ExecResult {
let fs = Arc::new(InMemoryFs::new());
let mut variables = HashMap::new();
let env = HashMap::new();
let mut cwd = PathBuf::from("/");
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
Xxd.execute(ctx).await.unwrap()
}
async fn run_hexdump(args: &[&str], stdin: Option<&str>) -> ExecResult {
let fs = Arc::new(InMemoryFs::new());
let mut variables = HashMap::new();
let env = HashMap::new();
let mut cwd = PathBuf::from("/");
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
Hexdump.execute(ctx).await.unwrap()
}
async fn run_od_with_fs(args: &[&str], files: &[(&str, &[u8])]) -> ExecResult {
let fs = Arc::new(InMemoryFs::new());
for (path, content) in files {
fs.write_file(std::path::Path::new(path), content)
.await
.unwrap();
}
let mut variables = HashMap::new();
let env = HashMap::new();
let mut cwd = PathBuf::from("/");
let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let ctx = Context {
args: &args,
env: &env,
variables: &mut variables,
cwd: &mut cwd,
fs,
stdin: None,
#[cfg(feature = "http_client")]
http_client: None,
#[cfg(feature = "git")]
git_client: None,
#[cfg(feature = "ssh")]
ssh_client: None,
shell: None,
};
Od.execute(ctx).await.unwrap()
}
#[tokio::test]
async fn test_od_basic() {
let result = run_od(&[], Some("AB")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("101")); assert!(result.stdout.contains("102")); }
#[tokio::test]
async fn test_od_hex() {
let result = run_od(&["-t", "x"], Some("AB")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("41")); assert!(result.stdout.contains("42")); }
#[tokio::test]
async fn test_od_decimal() {
let result = run_od(&["-t", "d"], Some("A")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains(" 65")); }
#[tokio::test]
async fn test_od_char() {
let result = run_od(&["-t", "c"], Some("A\n")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("A"));
assert!(result.stdout.contains("\\n"));
}
#[tokio::test]
async fn test_od_hex_addr() {
let result = run_od(&["-A", "x"], Some("test")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.starts_with("0000000"));
}
#[tokio::test]
async fn test_od_no_addr() {
let result = run_od(&["-A", "n", "-t", "x"], Some("AB")).await;
assert_eq!(result.exit_code, 0);
assert!(!result.stdout.starts_with("0"));
assert!(result.stdout.contains("41"));
}
#[tokio::test]
async fn test_od_count() {
let result = run_od(&["-N", "2", "-t", "x"], Some("ABCD")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("41"));
assert!(result.stdout.contains("42"));
assert!(!result.stdout.contains("43"));
}
#[tokio::test]
async fn test_od_skip() {
let result = run_od(&["-j", "2", "-t", "x"], Some("ABCD")).await;
assert_eq!(result.exit_code, 0);
assert!(!result.stdout.contains(" 41"));
assert!(result.stdout.contains("43"));
}
#[tokio::test]
async fn test_od_empty_input() {
let result = run_od(&[], Some("")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "");
}
#[tokio::test]
async fn test_od_from_file() {
let result =
run_od_with_fs(&["-t", "x", "/test.bin"], &[("/test.bin", &[0x41, 0x42])]).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("41"));
}
#[tokio::test]
async fn test_xxd_basic() {
let result = run_xxd(&[], Some("Hello")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("00000000:"));
assert!(result.stdout.contains("4865 6c6c 6f"));
assert!(result.stdout.contains("Hello"));
}
#[tokio::test]
async fn test_xxd_plain() {
let result = run_xxd(&["-p"], Some("Hi")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "4869\n");
}
#[tokio::test]
async fn test_xxd_length() {
let result = run_xxd(&["-l", "3", "-p"], Some("Hello World")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "48656c\n");
}
#[tokio::test]
async fn test_xxd_offset() {
let result = run_xxd(&["-s", "2", "-p"], Some("Hello")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "6c6c6f\n");
}
#[tokio::test]
async fn test_xxd_cols() {
let result = run_xxd(&["-c", "4"], Some("ABCDEFGH")).await;
assert_eq!(result.exit_code, 0);
let lines: Vec<&str> = result.stdout.lines().collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("00000000:"));
assert!(lines[1].contains("00000004:"));
}
#[tokio::test]
async fn test_xxd_empty() {
let result = run_xxd(&[], Some("")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "");
}
#[tokio::test]
async fn test_xxd_group() {
let result = run_xxd(&["-g", "1"], Some("AB")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("41 42"));
}
#[tokio::test]
async fn test_xxd_non_printable() {
let result = run_xxd(&["-p"], Some("\x00\x01\x02")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "000102\n");
}
#[tokio::test]
async fn test_xxd_reverse_plain() {
let result = run_xxd(&["-r", "-p"], Some("48656c6c6f")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "Hello");
}
#[tokio::test]
async fn test_xxd_reverse_plain_whitespace() {
let result = run_xxd(&["-r", "-p"], Some("4865 6c6c\n6f")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "Hello");
}
#[tokio::test]
async fn test_xxd_reverse_normal() {
let result = run_xxd(
&["-r"],
Some("00000000: 4865 6c6c 6f Hello"),
)
.await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "Hello");
}
#[tokio::test]
async fn test_hexdump_default() {
let result = run_hexdump(&[], Some("AB")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("4241")); }
#[tokio::test]
async fn test_hexdump_canonical() {
let result = run_hexdump(&["-C"], Some("Hello")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("48 65 6c 6c 6f"));
assert!(result.stdout.contains("|Hello|"));
}
#[tokio::test]
async fn test_hexdump_canonical_non_printable() {
let result = run_hexdump(&["-C"], Some("\x00\x01\x02")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("00 01 02"));
assert!(result.stdout.contains("|...|"));
}
#[tokio::test]
async fn test_hexdump_length() {
let result = run_hexdump(&["-C", "-n", "3"], Some("Hello World")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("48 65 6c"));
assert!(!result.stdout.contains("6f")); }
#[tokio::test]
async fn test_hexdump_offset() {
let result = run_hexdump(&["-C", "-s", "2"], Some("Hello")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("6c 6c 6f"));
}
#[tokio::test]
async fn test_hexdump_empty() {
let result = run_hexdump(&[], Some("")).await;
assert_eq!(result.exit_code, 0);
assert_eq!(result.stdout, "");
}
#[tokio::test]
async fn test_hexdump_canonical_final_offset() {
let result = run_hexdump(&["-C"], Some("AB")).await;
assert_eq!(result.exit_code, 0);
assert!(result.stdout.contains("00000002")); }
}