use crate::VERSION;
use clap::Parser as ClapParser;
use std::{collections::BTreeMap, path::PathBuf};
use trivet::{numbers::Radix, parse_from_string};
fn _radix_parser(value: &str) -> Result<Radix, String> {
let value = value.to_ascii_lowercase();
if value.starts_with('b') {
Ok(Radix::Binary)
} else if value.starts_with('h') {
Ok(Radix::Hexadecimal)
} else if value.starts_with('d') {
Ok(Radix::Decimal)
} else if value.starts_with('o') {
Ok(Radix::Octal)
} else {
Err("Invalid radix; only binary, octal, decimal, and hexadecimal are allowed".to_string())
}
}
fn metadata_parser(value: &str) -> Result<(String, String), String> {
if let Some(index) = value.find('=') {
if index == 0 {
Err(format!(
"The metadata name (prior to the equal sign) cannot be empty: {}",
value
))
} else {
Ok((value[0..index].to_string(), value[index + 1..].to_string()))
}
} else {
Err("The metadata must be of the form name=value.".to_string())
}
}
fn u64_parser(value: &str) -> Result<u64, String> {
match parse_from_string(value).parse_u64() {
Ok(value) => Ok(value),
Err(err) => Err(err.to_string()),
}
}
fn i64_parser(value: &str) -> Result<i64, String> {
match parse_from_string(value).parse_i64() {
Ok(value) => Ok(value),
Err(err) => Err(err.to_string()),
}
}
fn u16_parser(value: &str) -> Result<u16, String> {
match u64_parser(value) {
Ok(value) => {
if value > u16::MAX as u64 {
Err("The maximum value is 65535".to_string())
} else {
Ok(value as u16)
}
}
Err(err) => Err(err),
}
}
#[derive(Debug, ClapParser, Clone)]
#[command(
name = "hhh",
version = VERSION,
author = "Stacy Prowell (sprowell@gmail.com)",
about = "Binary file manipulation tool",
long_about = r#"
Generate hexdumps and parse binary file descriptions to create a binary file.
The parsing of binary descriptions is fairly premissive and has features to
make creating a custom binary file easier."#
)]
pub struct HhhArgs {
#[clap(long, default_value_t = 0, value_parser = i64_parser)]
pub bias: i64,
#[clap(long, short = 'b', default_value_t = 1, value_parser = u16_parser)]
pub bytes_per_group: u16,
#[clap(long, default_value_t = 0, value_parser = u64_parser)]
pub count: u64,
#[clap(short = 'D')]
pub directives: Vec<String>,
#[clap(long, short = 'g', default_value_t = 16, value_parser = u16_parser)]
pub groups_per_line: u16,
#[clap(long, short = 's', default_value_t = String::from(" "))]
pub group_separator: String,
#[clap(long)]
pub list_directives: bool,
#[clap(long)]
pub little_endian: bool,
#[clap(long, short = 'm')]
pub meta: bool,
#[clap(long)]
pub no_ascii: bool,
#[clap(long)]
pub no_offset: bool,
#[clap(long, value_parser = u64_parser, default_value_t = 0x4000_0000)]
pub offset_limit: u64,
#[clap(long, short = 'o')]
pub output: Option<PathBuf>,
#[clap(short = 'p', long)]
pub parse: bool,
#[clap(long, short = 'M', value_parser = metadata_parser, value_name = "NAME=VALUE")]
pub set_meta: Vec<(String, String)>,
#[clap(long, default_value_t = 0, value_parser = u64_parser)]
pub start: u64,
#[clap(long)]
pub skip_zeros: bool,
#[clap(long)]
pub stoic: bool,
#[clap(long, default_value_t = false)]
pub uppercase: bool,
#[clap(short = 'r', long, default_value_t = false)]
pub radix_prefixes: bool,
#[clap(long, default_value_t = 8)]
pub offset_width: u8,
#[clap(skip)]
pub last_offset: u64,
#[clap(skip)]
pub variables: BTreeMap<String, Vec<u8>>,
#[clap(skip)]
pub bias_stack: Vec<i64>,
#[clap(long, default_value_t = false)]
pub no_configuration_file: bool,
pub files: Vec<PathBuf>,
}
impl HhhArgs {
pub fn get_variable(&self, name: &str) -> Option<Vec<u8>> {
if name == "_" {
Some(self.last_offset.to_be_bytes().to_vec())
} else if name == "__" {
Some(
self.bias_stack
.last()
.unwrap_or(&-self.bias)
.to_be_bytes()
.to_vec(),
)
} else {
self.variables.get(name).cloned()
}
}
pub fn set_variable(&mut self, name: &str, value: &[u8]) {
if name == "_" || name == "__" {
return;
}
self.variables.insert(name.to_string(), value.to_vec());
}
pub fn unset_variable(&mut self, name: &str) {
self.variables.remove(name);
}
}
impl Default for HhhArgs {
fn default() -> Self {
Self {
files: vec![],
skip_zeros: false,
start: 0,
set_meta: vec![],
output: None,
no_offset: false,
no_ascii: false,
meta: false,
little_endian: false,
bias: 0,
bytes_per_group: 1,
groups_per_line: 16,
group_separator: " ".to_string(),
stoic: false,
uppercase: false,
radix_prefixes: false,
offset_width: 8,
last_offset: 0,
variables: BTreeMap::new(),
directives: vec![],
parse: false,
list_directives: false,
offset_limit: 0x4000_0000,
count: 0,
no_configuration_file: false,
bias_stack: vec![],
}
}
}
#[cfg(test)]
mod test {
use super::{i64_parser, u16_parser, u64_parser, HhhArgs};
use crate::options::{_radix_parser, metadata_parser};
use trivet::numbers::Radix;
#[test]
fn default_test() {
let args: HhhArgs = Default::default();
assert_eq!(args.offset_limit, 0x4000_0000);
}
#[test]
fn radix_test() {
assert_eq!(
_radix_parser("binary").unwrap().value(),
Radix::Binary.value()
);
assert_eq!(
_radix_parser("octal").unwrap().value(),
Radix::Octal.value()
);
assert_eq!(
_radix_parser("decimal").unwrap().value(),
Radix::Decimal.value()
);
assert_eq!(
_radix_parser("hexadecimal").unwrap().value(),
Radix::Hexadecimal.value()
);
assert_eq!(_radix_parser("b").unwrap().value(), Radix::Binary.value());
assert_eq!(_radix_parser("o").unwrap().value(), Radix::Octal.value());
assert_eq!(_radix_parser("d").unwrap().value(), Radix::Decimal.value());
assert_eq!(
_radix_parser("h").unwrap().value(),
Radix::Hexadecimal.value()
);
assert_eq!(_radix_parser("Bin").unwrap().value(), Radix::Binary.value());
assert_eq!(_radix_parser("Oct").unwrap().value(), Radix::Octal.value());
assert_eq!(
_radix_parser("Dec").unwrap().value(),
Radix::Decimal.value()
);
assert_eq!(
_radix_parser("Hex").unwrap().value(),
Radix::Hexadecimal.value()
);
assert!(_radix_parser("Quinary").is_err())
}
#[test]
fn metadata_parser_test() {
assert_eq!(
metadata_parser("dog=fido"),
Ok(("dog".to_string(), "fido".to_string()))
);
assert_eq!(
metadata_parser("dog = fido"),
Ok(("dog ".to_string(), " fido".to_string()))
);
assert_eq!(
metadata_parser("dog=fido=good doggy"),
Ok(("dog".to_string(), "fido=good doggy".to_string()))
);
assert!(metadata_parser("=man with no name").is_err());
assert!(metadata_parser("authoritarianism").is_err());
}
#[test]
fn parse_numbers_zeros() {
assert_eq!(u64_parser("0").unwrap(), 0);
assert_eq!(i64_parser("0").unwrap(), 0);
assert_eq!(u16_parser("0").unwrap(), 0);
assert_eq!(u64_parser("0x0").unwrap(), 0);
assert_eq!(i64_parser("0x0").unwrap(), 0);
assert_eq!(u16_parser("0x0").unwrap(), 0);
assert_eq!(u64_parser("0o0").unwrap(), 0);
assert_eq!(i64_parser("0o0").unwrap(), 0);
assert_eq!(u16_parser("0o0").unwrap(), 0);
assert_eq!(u64_parser("0b0").unwrap(), 0);
assert_eq!(i64_parser("0b0").unwrap(), 0);
assert_eq!(u16_parser("0b0").unwrap(), 0);
assert!(u64_parser("").is_err());
assert!(u64_parser("0x").is_err());
assert!(u64_parser("0o").is_err());
assert!(u64_parser("0b").is_err());
assert!(i64_parser("").is_err());
assert!(i64_parser("0x").is_err());
assert!(i64_parser("0o").is_err());
assert!(i64_parser("0b").is_err());
assert!(u16_parser("").is_err());
assert!(u16_parser("0x").is_err());
assert!(u16_parser("0o").is_err());
assert!(u16_parser("0b").is_err());
}
#[test]
fn parse_numbers() {
assert_eq!(
u64_parser("0xffffffff_ffffffff").unwrap(),
0xffffffff_ffffffff
);
assert_eq!(
i64_parser("0x7fffffff_ffffffff").unwrap(),
0x7fffffff_ffffffff
);
assert_eq!(
i64_parser("-0x7fffffff_ffffffff").unwrap(),
-0x7fffffff_ffffffff
);
assert_eq!(i64_parser("-1").unwrap(), -1);
assert_eq!(u16_parser("0xffff").unwrap(), 0xffff);
assert!(i64_parser("0xffffffff_ffffffff").is_err());
assert!(u16_parser("0x100000000").is_err());
assert!(u16_parser("0b21").is_err());
}
#[test]
fn constant_test() {
let mut args = HhhArgs::default();
args.set_variable("fido", b"good doggy");
args.set_variable("", b"");
assert_eq!(args.get_variable("fido").unwrap(), b"good doggy");
assert_eq!(args.get_variable("").unwrap(), b"");
args.unset_variable("thomas");
args.unset_variable("");
assert_eq!(args.get_variable(""), None);
assert_eq!(args.get_variable("rover"), None);
}
}