From 26ec46835c898d104984cd16648ce70ac5b2588f Mon Sep 17 00:00:00 2001 From: Wim Hueskes Date: Tue, 16 Aug 2016 00:37:33 +0200 Subject: [PATCH] od: implement +size to skip bytes --- src/od/od.rs | 48 ++++---- src/od/parse_inputs.rs | 268 +++++++++++++++++++++++++++++++++++++++++ tests/test_od.rs | 27 +++++ 3 files changed, 322 insertions(+), 21 deletions(-) create mode 100644 src/od/parse_inputs.rs diff --git a/src/od/od.rs b/src/od/od.rs index 42546a638..b4f128f06 100644 --- a/src/od/od.rs +++ b/src/od/od.rs @@ -26,6 +26,7 @@ mod prn_char; mod prn_float; mod parse_nrofbytes; mod parse_formats; +mod parse_inputs; #[cfg(test)] mod mockstream; @@ -37,9 +38,10 @@ use multifilereader::*; use partialreader::*; use peekreader::*; use formatteriteminfo::*; -use parse_nrofbytes::*; +use parse_nrofbytes::parse_number_of_bytes; use parse_formats::{parse_format_flags, ParsedFormatterItemInfo}; use prn_char::format_ascii_dump; +use parse_inputs::{parse_inputs, CommandLineInputs}; static VERSION: &'static str = env!("CARGO_PKG_VERSION"); const MAX_BYTES_PER_UNIT: usize = 8; @@ -134,17 +136,33 @@ pub fn uumain(args: Vec) -> i32 { } }; - // Gather up file names - let mut inputs = matches.free + let mut skip_bytes = match matches.opt_default("skip-bytes", "0") { + None => 0, + Some(s) => { + match parse_number_of_bytes(&s) { + Ok(i) => { i } + Err(_) => { + disp_err!("Invalid argument --skip-bytes={}", s); + return 1; + } + } + } + }; + + let input_strings = match parse_inputs(&matches) { + CommandLineInputs::FileNames(v) => v, + CommandLineInputs::FileAndOffset((f, s, _)) => { + skip_bytes = s; + vec!{f} + }, + }; + let inputs = input_strings .iter() - .filter_map(|w| match w as &str { - "-" => Some(InputSource::Stdin), - x => Some(InputSource::FileName(x)), + .map(|w| match w as &str { + "-" => InputSource::Stdin, + x => InputSource::FileName(x), }) .collect::>(); - if inputs.len() == 0 { - inputs.push(InputSource::Stdin); - } let formats = match parse_format_flags(&args) { Ok(f) => f, @@ -171,18 +189,6 @@ pub fn uumain(args: Vec) -> i32 { let output_duplicates = matches.opt_present("v"); - let skip_bytes = match matches.opt_default("skip-bytes", "0") { - None => 0, - Some(s) => { - match parse_number_of_bytes(&s) { - Ok(i) => { i } - Err(_) => { - disp_err!("Invalid argument --skip-bytes={}", s); - return 1; - } - } - } - }; let read_bytes = match matches.opt_str("read-bytes") { None => None, Some(s) => { diff --git a/src/od/parse_inputs.rs b/src/od/parse_inputs.rs new file mode 100644 index 000000000..4db4826e2 --- /dev/null +++ b/src/od/parse_inputs.rs @@ -0,0 +1,268 @@ +use getopts::Matches; + +/// Abstraction for getopts +pub trait CommandLineOpts { + /// returns all commandline parameters which do not belong to an option. + fn inputs(&self) -> Vec; + /// tests if any of the specified options is present. + fn opts_present(&self, &[&str]) -> bool; +} + +/// Implementation for `getopts` +impl CommandLineOpts for Matches { + fn inputs(&self) -> Vec { + self.free.clone() + } + fn opts_present(&self, opts: &[&str]) -> bool { + self.opts_present(&opts.iter().map(|s| s.to_string()).collect::>()) + } +} + +/// Contains the Input filename(s) with an optional offset. +/// +/// `FileNames` is used for one or more file inputs ("-" = stdin) +/// `FileAndOffset` is used for a single file input, with an offset +/// and an optional label. Offset and label are specified in bytes. +/// `FileAndOffset` will be only used if an offset is specified, +/// but it might be 0. +#[derive(PartialEq, Debug)] +pub enum CommandLineInputs { + FileNames(Vec), + FileAndOffset((String, usize, Option)), +} + + +/// Interprets the commandline inputs of od. +/// +/// Returns either an unspecified number of filenames. +/// Or it will return a single filename, with an offset and optional label. +/// Offset and label are specified in bytes. +/// '-' is used as filename if stdin is meant. This is also returned if +/// there is no input, as stdin is the default input. +pub fn parse_inputs(matches: &CommandLineOpts) -> CommandLineInputs { + + let mut input_strings: Vec = matches.inputs(); + + // test if commandline contains: [file] + if input_strings.len() == 1 || input_strings.len() == 2 { + // if any of the options -A, -j, -N, -t, -v or -w are present there is no offset + if !matches.opts_present(&["A", "j", "N", "t", "v", "w"]) { + // test if the last input can be parsed as an offset. + let offset=parse_offset_operand(&input_strings[input_strings.len()-1]); + match offset { + Ok(n) => { + // if there is just 1 input (stdin), an offset must start with '+' + if input_strings.len() == 1 && input_strings[0].starts_with("+") { + return CommandLineInputs::FileAndOffset(("-".to_string(), n, None)); + } + if input_strings.len() == 2 { + return CommandLineInputs::FileAndOffset((input_strings[0].clone(), n, None)); + } + } + _ => { + // if it cannot be parsed, it is considered a filename + } + } + } + } + + if input_strings.len() == 0 { + input_strings.push("-".to_string()); + } + CommandLineInputs::FileNames(input_strings) +} + +/// parses format used by offset and label on the commandline +pub fn parse_offset_operand(s: &String) -> Result { + let mut start = 0; + let mut len = s.len(); + let mut radix = 8; + let mut multiply = 1; + + if s.starts_with("+") { + start += 1; + } + + if s[start..len].starts_with("0x") || s[start..len].starts_with("0X") { + start += 2; + radix = 16; + } + else { + if s[start..len].ends_with("b") { + len -= 1; + multiply = 512; + } + if s[start..len].ends_with(".") { + len -= 1; + radix = 10; + } + } + match usize::from_str_radix(&s[start..len], radix) { + Ok(i) => Ok(i * multiply), + Err(_) => Err("parse failed"), + } +} + + +#[cfg(test)] +mod tests { + use super::*; + + /// A mock for the commandline options type + /// + /// `inputs` are all commandline parameters which do not belong to an option. + /// `option_names` are the names of the options on the commandline. + struct MockOptions<'a> { + inputs: Vec, + option_names: Vec<&'a str>, + } + + impl<'a> MockOptions<'a> { + fn new(inputs: Vec<&'a str>, option_names: Vec<&'a str>) -> MockOptions<'a> { + MockOptions { + inputs: inputs.iter().map(|s| s.to_string()).collect::>(), + option_names: option_names, + } + } + } + + impl<'a> CommandLineOpts for MockOptions<'a> { + fn inputs(&self) -> Vec { + self.inputs.clone() + } + fn opts_present(&self, opts: &[&str]) -> bool { + for expected in opts.iter() { + for actual in self.option_names.iter() { + if *expected==*actual { + return true; + } + } + } + false + } + } + + #[test] + fn test_parse_inputs_normal() { + + assert_eq!(CommandLineInputs::FileNames(vec!{"-".to_string()}), + parse_inputs(&MockOptions::new( + vec!{}, + vec!{}))); + + assert_eq!(CommandLineInputs::FileNames(vec!{"-".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"-"}, + vec!{}))); + + assert_eq!(CommandLineInputs::FileNames(vec!{"file1".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"file1"}, + vec!{}))); + + assert_eq!(CommandLineInputs::FileNames(vec!{"file1".to_string(), "file2".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"file1", "file2"}, + vec!{}))); + + assert_eq!(CommandLineInputs::FileNames(vec!{"-".to_string(), "file1".to_string(), "file2".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"-", "file1", "file2"}, + vec!{}))); + } + + #[test] + fn test_parse_inputs_with_offset() { + // offset is found without filename, so stdin will be used. + assert_eq!(CommandLineInputs::FileAndOffset(("-".to_string(), 8, None)), + parse_inputs(&MockOptions::new( + vec!{"+10"}, + vec!{}))); + + // offset must start with "+" if no input is specified. + assert_eq!(CommandLineInputs::FileNames(vec!{"10".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"10"}, + vec!{""}))); + + // offset is not valid, so it is considered a filename. + assert_eq!(CommandLineInputs::FileNames(vec!{"+10a".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"+10a"}, + vec!{""}))); + + // if -j is included in the commandline, there cannot be an offset. + assert_eq!(CommandLineInputs::FileNames(vec!{"+10".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"+10"}, + vec!{"j"}))); + + // if -v is included in the commandline, there cannot be an offset. + assert_eq!(CommandLineInputs::FileNames(vec!{"+10".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"+10"}, + vec!{"o", "v"}))); + + assert_eq!(CommandLineInputs::FileAndOffset(("file1".to_string(), 8, None)), + parse_inputs(&MockOptions::new( + vec!{"file1", "+10"}, + vec!{}))); + + // offset does not need to start with "+" if a filename is included. + assert_eq!(CommandLineInputs::FileAndOffset(("file1".to_string(), 8, None)), + parse_inputs(&MockOptions::new( + vec!{"file1", "10"}, + vec!{}))); + + assert_eq!(CommandLineInputs::FileNames(vec!{"file1".to_string(), "+10a".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"file1", "+10a"}, + vec!{""}))); + + assert_eq!(CommandLineInputs::FileNames(vec!{"file1".to_string(), "+10".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"file1", "+10"}, + vec!{"j"}))); + + // offset must be last on the commandline + assert_eq!(CommandLineInputs::FileNames(vec!{"+10".to_string(), "file1".to_string()}), + parse_inputs(&MockOptions::new( + vec!{"+10", "file1"}, + vec!{""}))); + } + + fn parse_offset_operand_str(s: &str) -> Result { + parse_offset_operand(&String::from(s)) + } + + #[test] + fn test_parse_offset_operand_invalid() { + parse_offset_operand_str("").unwrap_err(); + parse_offset_operand_str("a").unwrap_err(); + parse_offset_operand_str("+").unwrap_err(); + parse_offset_operand_str("+b").unwrap_err(); + parse_offset_operand_str("0x1.").unwrap_err(); + parse_offset_operand_str("0x1.b").unwrap_err(); + parse_offset_operand_str("-").unwrap_err(); + parse_offset_operand_str("-1").unwrap_err(); + parse_offset_operand_str("1e10").unwrap_err(); + } + + #[test] + fn test_parse_offset_operand() { + assert_eq!(8, parse_offset_operand_str("10").unwrap()); // default octal + assert_eq!(0, parse_offset_operand_str("0").unwrap()); + assert_eq!(8, parse_offset_operand_str("+10").unwrap()); // optional leading '+' + assert_eq!(16, parse_offset_operand_str("0x10").unwrap()); // hex + assert_eq!(16, parse_offset_operand_str("0X10").unwrap()); // hex + assert_eq!(16, parse_offset_operand_str("+0X10").unwrap()); // hex + assert_eq!(10, parse_offset_operand_str("10.").unwrap()); // decimal + assert_eq!(10, parse_offset_operand_str("+10.").unwrap()); // decimal + assert_eq!(4096, parse_offset_operand_str("10b").unwrap()); // b suffix = *512 + assert_eq!(4096, parse_offset_operand_str("+10b").unwrap()); // b suffix = *512 + assert_eq!(5120, parse_offset_operand_str("10.b").unwrap()); // b suffix = *512 + assert_eq!(5120, parse_offset_operand_str("+10.b").unwrap()); // b suffix = *512 + assert_eq!(267, parse_offset_operand_str("0x10b").unwrap()); // hex + } + +} diff --git a/tests/test_od.rs b/tests/test_od.rs index 58567053c..5aa0a1185 100644 --- a/tests/test_od.rs +++ b/tests/test_od.rs @@ -564,3 +564,30 @@ fn test_filename_parsing(){ 000012 ")); } + +#[test] +fn test_stdin_offset(){ + + let input = "abcdefghijklmnopq"; + let result = new_ucmd!().arg("-c").arg("+5").run_piped_stdin(input.as_bytes()); + + assert_empty_stderr!(result); + assert!(result.success); + assert_eq!(result.stdout, unindent(" + 0000005 f g h i j k l m n o p q + 0000021 + ")); +} + +#[test] +fn test_file_offset(){ + + let result = new_ucmd!().arg("-c").arg("--").arg("-f").arg("10").run(); + + assert_empty_stderr!(result); + assert!(result.success); + assert_eq!(result.stdout, unindent(r" + 0000010 w e r c a s e f \n + 0000022 + ")); +}