use crate::directives::{number_to_bytes, parse_directive};
use crate::options::HhhArgs;
use num::ToPrimitive;
use trivet::parse_from_string;
use trivet::parser::ParseResult;
pub fn set_groups(args: &mut HhhArgs) {
let mut cost = 0;
if !args.no_offset {
cost += 8.max(args.offset_width as u16) + 2 + if args.radix_prefixes { 2 } else { 0 };
}
let group = args.bytes_per_group as u32 * 2
+ args.group_separator.len() as u32
+ if args.radix_prefixes { 2 } else { 0 };
if group > u16::MAX as u32 {
args.groups_per_line = 1;
return;
}
let group = group as u16;
let mut line = match termsize::get() {
None => 80,
Some(termsize::Size { cols: value, .. }) => value,
};
line = line.checked_sub(cost).unwrap_or(40);
if args.no_ascii {
args.groups_per_line = line / group;
} else if line < 6 {
args.groups_per_line = 1;
} else {
args.groups_per_line = (line - 5) / (group + args.bytes_per_group);
}
args.groups_per_line = 1.max(args.groups_per_line)
}
type DirOp = fn(&mut HhhArgs, &Vec<Arg>) -> Option<String>;
type DirInitOp = fn(&mut HhhArgs) -> ();
#[derive(Copy, Clone, Debug)]
pub struct DirData {
pub name: &'static str,
pub input: bool,
pub output: bool,
pub init_closure: Option<DirInitOp>,
pub prototype: &'static str,
pub description: &'static str,
pub closure: DirOp,
}
impl DirData {
pub fn execute(&self, config: &mut HhhArgs, args: &Vec<Arg>) -> Option<String> {
(self.closure)(config, args)
}
}
pub const DIRECTIVES: &[DirData] = &[
DirData {
name: "ascii",
input: false,
output: true,
prototype: "",
init_closure: Some(|config| {
config.no_ascii = false;
}),
description: "Generate an ASCII preview.",
closure: |config, _args| {
config.no_ascii = false;
None
},
},
DirData {
name: "big_endian",
input: true,
output: true,
prototype: "",
init_closure: None,
description: "When generating, encode groups in big endian order. When parsing, assume non-prefixed \
groups are given in big endian order and numbers should be encoded in big endian order.",
closure: |config, _args| {
config.little_endian = false;
None
},
},
DirData {
name: "bytes_per_group",
input: false,
output: true,
prototype: "([1-255])",
init_closure: None,
description: "Specify the number of bytes to include in each group on a line.",
closure: |config, args| {
if args.len() != 1 {
return Some("bytes_per_group requires a single number argument".to_string());
}
match &args[0] {
Arg::String(_) | Arg::Bytes(_) => {
Some("bytes_per_group requires a single number argument".to_string())
},
Arg::Number(num) => {
match num.to_u8() {
None => {
Some("maximum bytes_per_group size is 255".to_string())
},
Some(value) => {
if value < 1 {
Some("bytes_per_group cannot be zero".to_string())
} else {
config.bytes_per_group = value as u16;
None
}
}
}
}
}
},
},
DirData {
name: "stoic",
input: true,
output: false,
prototype: "",
init_closure: None,
description: "Enable stoic mode. In stoic mode any error causes the current line to be discarded and the \
error suppressed.",
closure: |config, _args| {
config.stoic = true;
None
},
},
DirData {
name: "groups_per_line",
input: false,
output: true,
prototype: "([1,255])",
init_closure: None,
description: "Specify the number of groups to print per line. Use zero to try to use the terminal width.",
closure: |config, args| {
if args.len() != 1 {
return Some("groups_per_line requires a single number argument".to_string());
}
match &args[0] {
Arg::String(_) | Arg::Bytes(_) => {
Some("groups_per_line requires a single number argument".to_string())
},
Arg::Number(num) => {
match num.to_u8() {
None => {
Some("maximum groups_per_line is 255".to_string())
},
Some(value) => {
if value < 1 {
set_groups(config);
None
} else {
config.groups_per_line = value as u16;
None
}
}
}
},
}
},
},
DirData {
name: "little_endian",
input: true,
output: true,
prototype: "",
init_closure: None,
description: "When generating, encode groups in little endian order. When parsing, assume non-prefixed \
groups are given in little endian order and numbers should be written in little endian order.",
closure: |config, _args| {
config.little_endian = true;
None
},
},
DirData {
name: "lowercase",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Use lower case for hexadecimal.",
closure: |config, _args| {
config.uppercase = false;
None
},
},
DirData {
name: "metadata",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Enable metadata generation. To have an effect this must be done before any output is generated.",
closure: |config, _args| {
config.meta = true;
None
},
},
DirData {
name: "no_ascii",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Suppress the ASCII preview on output.",
closure: |config, _args| {
config.no_ascii = true;
None
},
},
DirData {
name: "no_stoic",
input: true,
output: false,
prototype: "",
init_closure: None,
description: "Disable stoic mode. Errors are printed and halt the parse.",
closure: |config, _args| {
config.stoic = false;
None
},
},
DirData {
name: "no_metadata",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Disable metadata generation. To have an effect this must be done before any output is \
generated.",
closure: |config, _args| {
config.meta = false;
None
},
},
DirData {
name: "no_offsets",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Do not print the file offset at the start of a line.",
closure: |config, _args| {
config.no_offset = true;
None
},
},
DirData {
name: "no_prefix",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Do not print radix prefixes on numbers, and do not look for radix prefixes when parsing.",
closure: |config, _args| {
config.radix_prefixes = false;
None
},
},
DirData {
name: "offset_limit",
input: true,
output: false,
prototype: "([0..0xffff_ffff_ffff_ffff])",
init_closure: None,
description: "Set a hard limit on the offset to prevent creating a huge file. By default the offset is \
limited to a 30-bit address (1 gigabyte). This limit is checked only when an offset is specified \
during parsing.",
closure: |config, args| {
if args.len() != 1 {
return Some("offset_limit requires a single number argument".to_string());
}
match &args[0] {
Arg::String(_) | Arg::Bytes(_) => {
Some("offset_limit requires a single number argument".to_string())
}
Arg::Number(num) => {
match num.to_u64() {
None => {
Some("maximum offset_width is 0xffff_ffff_ffff_ffff".to_string())
},
Some(value) => {
config.offset_limit = value;
None
}
}
}
}
}
},
DirData {
name: "offset_width",
input: false,
output: true,
prototype: "([0..255])",
init_closure: None,
description: "If offsets are being printed, this sets the width of the offset in hexadecimal digits. \
Zero turns it off, and any other number turns it on.",
closure: |config, args| {
if args.len() != 1 {
return Some("offset_width requires a single number argument".to_string());
}
match &args[0] {
Arg::String(_) | Arg::Bytes(_) => {
Some("offset_width requires a single number argument".to_string())
}
Arg::Number(num) => {
match num.to_u8() {
None => {
Some("maximum offset_width is 255".to_string())
},
Some(0) => {
config.offset_width = 8;
config.no_offset = true;
None
},
Some(value) => {
config.offset_width = value;
config.no_offset = false;
None
}
}
}
}
},
},
DirData {
name: "offsets",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Print the current file offset at the start of each line.",
closure: |config, _args| {
config.no_offset = false;
None
},
},
DirData {
name: "prefix",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Print radix prefixes on numbers, and look for radix prefixes when parsing.",
closure: |config, _args| {
config.radix_prefixes = true;
None
},
},
DirData {
name: "bias",
input: true,
output: true,
prototype: "(number)",
init_closure: None,
description: "Set an absolute bias. The bias is subtracted from offsets found in the file, so a bias of \
0x1000 converts offset 0x4000 into 0x3000.",
closure: |config, args| {
if args.len() != 1 {
return Some("rebase requires a single number argument".to_string());
}
match &args[0] {
Arg::String(_) | Arg::Bytes(_) => {
Some("rebase requires a single number argument".to_string())
}
Arg::Number(num) => {
config.bias = *num;
None
}
}
},
},
DirData {
name: "relative",
input: true,
output: false,
prototype: "(number)",
init_closure: None,
description: "Set a relative bias using an address. This sets the bias so that the current offset is \
treated as the given address. To undo this, use bias(0).",
closure: |config, args| {
if args.len() != 1 {
return Some("relative requires a single number argument".to_string());
}
match &args[0] {
Arg::String(_) | Arg::Bytes(_) => {
Some("relative requires a single number argument".to_string())
}
Arg::Number(num) => {
if *num < 0 {
Some("relative bias requires an address and cannot be negative".to_string())
} else {
let bias = num - config.last_offset as i64;
config.bias = bias;
None
}
}
}
},
},
DirData {
name: "set",
input: true,
output: false,
prototype: "(string,number)",
init_closure: None,
description: "Set a constant. Constants are assumed to be a byte sequence of the platform's pointer size. \
Specify the constant's name with a string; do not use the $ form or it will be treated as an expression.",
closure: |config, args| {
if args.len() != 2 {
return Some("set requires two arguments".to_string());
}
let name = match &args[0] {
Arg::String(name) => name,
_ => return Some("first argument to set must be a string".to_string()),
}.to_owned();
let value = match &args[1] {
Arg::Number(value) => value,
_ => return Some("second argument to set must be a number".to_string()),
};
config.set_variable(&name, &number_to_bytes(*value, Some(usize::BITS as usize / 8), config.little_endian));
None
}
},
DirData {
name: "set_metadata",
input: false,
output: true,
prototype: "(name, value)",
init_closure: None,
description: "Set or suppress a metadata item.",
closure: |config, args| {
if args.len() != 2 {
return Some("set-metadata requires two arguments".to_string());
}
let name = match &args[0] {
Arg::String(value) => value,
_ => return Some("first argument to set-metadata must be a string".to_string()),
}.to_owned();
let value: String = match &args[1] {
Arg::String(value) => value.clone(),
_ => return Some("second argument to set-metadatas must be a string".to_string()),
};
config.set_meta.push((name, value));
None
},
},
DirData {
name: "uppercase",
input: false,
output: true,
prototype: "",
init_closure: None,
description: "Use upper case for hexadecimal.",
closure: |config, _args| {
config.uppercase = true;
None
},
},
DirData {
name: "group_separator",
input: true,
output: true,
prototype: "(string)",
init_closure: None,
description: "Set the group separator for hex dump generation.",
closure: |config, args| {
if args.len() != 1 {
return Some("group_separator requires a single string argument".to_string());
}
match &args[0] {
Arg::Number(_) | Arg::Bytes(_) => {
Some("group_separator requires a single string argument".to_string())
}
Arg::String(separator) => {
config.group_separator = separator.to_owned();
None
}
}
},
},
];
#[derive(Debug, PartialEq)]
pub enum Arg {
String(String),
Number(i64),
Bytes(Vec<u8>),
}
impl Clone for Arg {
fn clone(&self) -> Arg {
match self {
Arg::String(value) => Arg::String(value.clone()),
Arg::Number(value) => Arg::Number(*value),
Arg::Bytes(value) => Arg::Bytes(value.clone()),
}
}
}
#[derive(Debug, Default, PartialEq)]
pub struct Directive {
pub name: String,
pub arguments: Vec<Arg>,
}
impl Directive {
pub fn execute(&self, config: &mut HhhArgs) -> Option<String> {
let mut matches = vec![];
for direct in DIRECTIVES {
if direct.name == self.name {
matches = vec![direct];
break;
}
if direct.name.starts_with(&self.name) {
matches.push(direct);
}
}
if matches.is_empty() {
return Some(format!("No directive matches '{}'.", self.name));
}
if matches.len() > 1 {
return Some(format!(
"The directive '{}' is ambiguous; it might be '{}' or '{}'.",
self.name, matches[0].name, matches[1].name
));
}
if !matches[0].prototype.is_empty() && self.arguments.is_empty() {
return Some(format!(
"The directive '{}' requires argument(s), but none were given.",
self.name
));
}
if matches[0].prototype.is_empty() && !self.arguments.is_empty() {
return Some(format!(
"The directive '{}' does not take arguments, but arguments were given.",
self.name
));
}
matches[0].execute(config, &self.arguments)
}
}
pub fn parse_and_do_directive(text: &str, config: &mut HhhArgs) -> ParseResult<Option<String>> {
let mut parser = parse_from_string(text);
let directive = parse_directive(&mut parser, config)?;
Ok(directive.execute(config))
}
pub fn directives_help() {
let mut size = termsize::get()
.unwrap_or(termsize::Size { rows: 24, cols: 80 })
.cols as usize;
let min_width = 30;
if size < min_width {
size = min_width;
}
let mut desc_indent = min_width;
let mut desc_width = size - desc_indent;
if desc_width < min_width {
desc_indent = 1.max(size - min_width);
desc_width = min_width;
}
let mut output = vec![];
for directive in DIRECTIVES {
let proto = directive.name.to_owned() + directive.prototype;
let mut desc = directive.description.to_string();
desc.push(' ');
desc.push(' ');
if directive.input {
desc.push('P')
}
if directive.output {
desc.push('G')
}
desc.push(')');
output.push((proto, desc));
}
println!("Directives: (P = usable during parsing, G = usable during generation)");
for (proto, desc_str) in output {
let desc = textwrap::wrap(&desc_str, desc_width);
let field = format!("{} ..", proto);
if field.len() > desc_indent {
println!("{}", field);
for line in &desc {
print!("{:>1$}", "", desc_indent);
println!("{:<1$}", line, desc_width);
}
} else {
print!("{:.<1$} ", field, desc_indent - 1);
println!("{}", desc[0]);
for line in &desc[1..] {
print!("{:>1$}", "", desc_indent);
println!("{:<1$}", line, desc_width);
}
}
}
}
#[cfg(test)]
mod test {
use super::{directives_help, parse_and_do_directive, set_groups, Arg, Directive};
use crate::options::HhhArgs;
#[test]
fn set_groups_test() {
let mut args = HhhArgs::default();
args.bytes_per_group = 3;
args.groups_per_line = 0;
set_groups(&mut args);
assert!(args.groups_per_line != 0);
}
#[test]
fn arg_test() {
let mut arg = Arg::Bytes(vec![1, 2, 3, 4]);
assert_eq!(arg, arg.clone());
arg = Arg::Number(-376);
assert_eq!(arg, arg.clone());
arg = Arg::String("Freddy".to_string());
assert_eq!(arg, arg.clone());
}
#[test]
fn directive_help_test() {
directives_help();
}
#[test]
fn directive_test_1() {
let mut args = HhhArgs::default();
args.radix_prefixes = false;
let mut directive = Directive {
name: "prefi".to_string(),
arguments: vec![],
};
assert!(directive.execute(&mut args).is_none());
assert!(args.radix_prefixes);
directive = Directive {
name: "no_pre".to_string(),
arguments: vec![],
};
assert!(directive.execute(&mut args).is_none());
assert_eq!(args.radix_prefixes, false);
directive = Directive {
name: "xyzzy".to_string(),
arguments: vec![],
};
assert!(directive.execute(&mut args).is_some());
directive = Directive {
name: "no_".to_string(),
arguments: vec![],
};
assert!(directive.execute(&mut args).is_some());
directive = Directive {
name: "groups_per_line".to_string(),
arguments: vec![],
};
assert!(directive.execute(&mut args).is_some());
directive = Directive {
name: "prefix".to_string(),
arguments: vec![Arg::String("freddy".to_string())],
};
assert!(directive.execute(&mut args).is_some());
}
#[test]
fn parse_directive_test() {
let mut args = HhhArgs::default();
args.groups_per_line = 1;
args.bytes_per_group = 1;
args.group_separator = " ".to_string();
args.radix_prefixes = false;
parse_and_do_directive("groups_per_l(4)", &mut args).unwrap();
assert_eq!(args.groups_per_line, 4);
parse_and_do_directive("bytes_per(17)", &mut args).unwrap();
assert_eq!(args.bytes_per_group, 17);
parse_and_do_directive("group_separator(\";\")", &mut args).unwrap();
assert_eq!(args.group_separator, ";");
parse_and_do_directive("prefix", &mut args).unwrap();
assert_eq!(args.radix_prefixes, true);
}
#[test]
fn large_group_test() {
let mut args = HhhArgs::default();
args.bytes_per_group = 65535;
set_groups(&mut args);
assert_eq!(args.groups_per_line, 1);
args.bytes_per_group = 80;
args.no_ascii = true;
set_groups(&mut args);
assert_eq!(args.groups_per_line, 1);
}
}