From e377e4f046a1f57f5e05785f0920d5459563d66e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Mon, 12 Sep 2022 16:06:31 +0200 Subject: [PATCH] dd: custom positional argument parsing --- src/uu/dd/dd.md | 122 +++ src/uu/dd/src/datastructures.rs | 40 +- src/uu/dd/src/dd.rs | 623 ++++---------- src/uu/dd/src/parseargs.rs | 1071 ++++++++++--------------- src/uu/dd/src/parseargs/unit_tests.rs | 759 ++++++------------ tests/by-util/test_dd.rs | 27 +- tests/by-util/test_ls.rs | 8 +- 7 files changed, 1013 insertions(+), 1637 deletions(-) create mode 100644 src/uu/dd/dd.md diff --git a/src/uu/dd/dd.md b/src/uu/dd/dd.md new file mode 100644 index 00000000000..b5e16a1bc47 --- /dev/null +++ b/src/uu/dd/dd.md @@ -0,0 +1,122 @@ + +# dd + +## About +Copy, and optionally convert, a file system resource + +## After Help + +OPERANDS: + + bs=BYTES read and write up to BYTES bytes at a time (default: 512); + overwrites ibs and obs. + cbs=BYTES the 'conversion block size' in bytes. Applies to + the conv=block, and conv=unblock operations. + conv=CONVS a comma-separated list of conversion options or + (for legacy reasons) file flags. + count=N stop reading input after N ibs-sized read operations rather + than proceeding until EOF. See iflag=count_bytes if stopping + after N bytes is preferred + ibs=N the size of buffer used for reads (default: 512) + if=FILE the file used for input. When not specified, stdin is used instead + iflag=FLAGS a comma-separated list of input flags which specify how the input + source is treated. FLAGS may be any of the input-flags or + general-flags specified below. + skip=N (or iseek=N) skip N ibs-sized records into input before beginning + copy/convert operations. See iflag=seek_bytes if seeking N bytes + is preferred. + obs=N the size of buffer used for writes (default: 512) + of=FILE the file used for output. When not specified, stdout is used + instead + oflag=FLAGS comma separated list of output flags which specify how the output + source is treated. FLAGS may be any of the output flags or + general flags specified below + seek=N (or oseek=N) seeks N obs-sized records into output before + beginning copy/convert operations. See oflag=seek_bytes if + seeking N bytes is preferred + status=LEVEL controls whether volume and performance stats are written to + stderr. + + When unspecified, dd will print stats upon completion. An example is below. + 6+0 records in + 16+0 records out + 8192 bytes (8.2 kB, 8.0 KiB) copied, 0.00057009 s, 14.4 MB/s + The first two lines are the 'volume' stats and the final line is + the 'performance' stats. + The volume stats indicate the number of complete and partial + ibs-sized reads, or obs-sized writes that took place during the + copy. The format of the volume stats is + +. If records have been truncated (see + conv=block), the volume stats will contain the number of + truncated records. + + Possible LEVEL values are: + progress: Print periodic performance stats as the copy + proceeds. + noxfer: Print final volume stats, but not performance stats. + none: Do not print any stats. + + Printing performance stats is also triggered by the INFO signal + (where supported), or the USR1 signal. Setting the + POSIXLY_CORRECT environment variable to any value (including an + empty value) will cause the USR1 signal to be ignored. + +CONVERSION OPTIONS: + + ascii convert from EBCDIC to ASCII. This is the inverse of the 'ebcdic' + option. Implies conv=unblock. + ebcdic convert from ASCII to EBCDIC. This is the inverse of the 'ascii' + option. Implies conv=block. + ibm convert from ASCII to EBCDIC, applying the conventions for '[', ']' + and '~' specified in POSIX. Implies conv=block. + + ucase convert from lower-case to upper-case + lcase converts from upper-case to lower-case. + + block for each newline less than the size indicated by cbs=BYTES, remove + the newline and pad with spaces up to cbs. Lines longer than cbs are + truncated. + unblock for each block of input of the size indicated by cbs=BYTES, remove + right-trailing spaces and replace with a newline character. + + sparse attempts to seek the output when an obs-sized block consists of only + zeros. + swab swaps each adjacent pair of bytes. If an odd number of bytes is + present, the final byte is omitted. + sync pad each ibs-sided block with zeros. If 'block' or 'unblock' is + specified, pad with spaces instead. + excl the output file must be created. Fail if the output file is already + present. + nocreat the output file will not be created. Fail if the output file in not + already present. + notrunc the output file will not be truncated. If this option is not + present, output will be truncated when opened. + noerror all read errors will be ignored. If this option is not present, dd + will only ignore Error::Interrupted. + fdatasync data will be written before finishing. + fsync data and metadata will be written before finishing. + +INPUT FLAGS: + + count_bytes a value to count=N will be interpreted as bytes. + skip_bytes a value to skip=N will be interpreted as bytes. + fullblock wait for ibs bytes from each read. zero-length reads are still + considered EOF. + +OUTPUT FLAGS: + + append open file in append mode. Consider setting conv=notrunc as well. + seek_bytes a value to seek=N will be interpreted as bytes. + +GENERAL FLAGS: + + direct use direct I/O for data. + directory fail unless the given input (if used as an iflag) or output (if used + as an oflag) is a directory. + dsync use synchronized I/O for data. + sync use synchronized I/O for data and metadata. + nonblock use non-blocking I/O. + noatime do not update access time. + nocache request that OS drop cache. + noctty do not assign a controlling tty. + nofollow do not follow system links. diff --git a/src/uu/dd/src/datastructures.rs b/src/uu/dd/src/datastructures.rs index 23ada466f9f..6830df63e3b 100644 --- a/src/uu/dd/src/datastructures.rs +++ b/src/uu/dd/src/datastructures.rs @@ -17,20 +17,20 @@ type Cbs = usize; /// certain order. The variants of this enumeration give the different /// ways of combining those three operations. #[derive(Debug, PartialEq)] -pub(crate) enum ConversionMode<'a> { - ConvertOnly(&'a ConversionTable), +pub(crate) enum ConversionMode { + ConvertOnly(&'static ConversionTable), BlockOnly(Cbs, bool), UnblockOnly(Cbs), - BlockThenConvert(&'a ConversionTable, Cbs, bool), - ConvertThenBlock(&'a ConversionTable, Cbs, bool), - UnblockThenConvert(&'a ConversionTable, Cbs), - ConvertThenUnblock(&'a ConversionTable, Cbs), + BlockThenConvert(&'static ConversionTable, Cbs, bool), + ConvertThenBlock(&'static ConversionTable, Cbs, bool), + UnblockThenConvert(&'static ConversionTable, Cbs), + ConvertThenUnblock(&'static ConversionTable, Cbs), } /// Stores all Conv Flags that apply to the input #[derive(Debug, Default, PartialEq)] pub(crate) struct IConvFlags { - pub mode: Option>, + pub mode: Option, pub swab: bool, pub sync: Option, pub noerror: bool, @@ -88,30 +88,6 @@ pub struct OFlags { pub seek_bytes: bool, } -/// The value of count=N -/// Defaults to Reads(N) -/// if iflag=count_bytes -/// then becomes Bytes(N) -#[derive(Debug, PartialEq, Eq)] -pub enum CountType { - Reads(u64), - Bytes(u64), -} - pub mod options { - pub const INFILE: &str = "if"; - pub const OUTFILE: &str = "of"; - pub const IBS: &str = "ibs"; - pub const OBS: &str = "obs"; - pub const BS: &str = "bs"; - pub const CBS: &str = "cbs"; - pub const COUNT: &str = "count"; - pub const SKIP: &str = "skip"; - pub const SEEK: &str = "seek"; - pub const ISEEK: &str = "iseek"; - pub const OSEEK: &str = "oseek"; - pub const STATUS: &str = "status"; - pub const CONV: &str = "conv"; - pub const IFLAG: &str = "iflag"; - pub const OFLAG: &str = "oflag"; + pub const OPERANDS: &str = "operands"; } diff --git a/src/uu/dd/src/dd.rs b/src/uu/dd/src/dd.rs index 6cd3b0b79c2..8e4493e4e2e 100644 --- a/src/uu/dd/src/dd.rs +++ b/src/uu/dd/src/dd.rs @@ -5,15 +5,15 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable canonicalized Canonicalized icflags ocflags ifname ofname +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rremain, rsofar, rstat, sigusr, wlen, wstat seekable oconv canonicalized mod datastructures; use datastructures::*; mod parseargs; +use parseargs::Parser; mod conversion_tables; -use conversion_tables::*; mod progress; use progress::{gen_prog_updater, ProgUpdate, ReadStat, StatusLevel, WriteStat}; @@ -33,44 +33,75 @@ use std::sync::mpsc; use std::thread; use std::time; -use clap::{crate_version, Arg, ArgMatches, Command}; +use clap::{crate_version, Arg, Command}; use gcd::Gcd; use uucore::display::Quotable; use uucore::error::{FromIo, UResult}; +use uucore::help_section; use uucore::show_error; -const ABOUT: &str = "copy, and optionally convert, a file system resource"; +const ABOUT: &str = help_section!("about", "dd.md"); +const AFTER_HELP: &str = help_section!("after help", "dd.md"); const BUF_INIT_BYTE: u8 = 0xDD; -struct Input { - src: R, +/// Final settings after parsing +#[derive(Default)] +struct Settings { + infile: Option, + outfile: Option, ibs: usize, - print_level: Option, - count: Option, - cflags: IConvFlags, + obs: usize, + skip: u64, + seek: u64, + count: Option, + iconv: IConvFlags, iflags: IFlags, + oconv: OConvFlags, + oflags: OFlags, + status: Option, +} + +/// A number in blocks or bytes +/// +/// Some values (seek, skip, iseek, oseek) can have values either in blocks or in bytes. +/// We need to remember this because the size of the blocks (ibs) is only known after parsing +/// all the arguments. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Num { + Blocks(u64), + Bytes(u64), +} + +impl Num { + fn force_bytes_if(self, force: bool) -> Self { + match self { + Self::Blocks(n) if force => Self::Bytes(n), + count => count, + } + } + + fn to_bytes(self, block_size: u64) -> u64 { + match self { + Self::Blocks(n) => n * block_size, + Self::Bytes(n) => n, + } + } } -impl Input { - fn new( - ibs: usize, - print_level: Option, - count: Option, - cflags: IConvFlags, - iflags: IFlags, - skip_amount: u64, - ) -> UResult { - let mut i = Self { +struct Input<'a, R: Read> { + src: R, + settings: &'a Settings, +} + +impl<'a> Input<'a, io::Stdin> { + fn new(settings: &'a Settings) -> UResult { + let mut input = Self { src: io::stdin(), - ibs, - print_level, - count, - cflags, - iflags, + settings, }; - if skip_amount > 0 { - if let Err(e) = i.read_skip(skip_amount) { + if settings.skip > 0 { + if let Err(e) = input.read_skip(settings.skip) { if let io::ErrorKind::UnexpectedEof = e.kind() { show_error!("'standard input': cannot skip to specified offset"); } else { @@ -80,7 +111,7 @@ impl Input { } } - Ok(i) + Ok(input) } } @@ -120,53 +151,38 @@ fn make_linux_iflags(iflags: &IFlags) -> Option { } } -impl Input { - fn new( - ibs: usize, - print_level: Option, - count: Option, - cflags: IConvFlags, - iflags: IFlags, - fname: &str, - skip_amount: u64, - ) -> UResult { +impl<'a> Input<'a, File> { + fn new(filename: &Path, settings: &'a Settings) -> UResult { let mut src = { let mut opts = OpenOptions::new(); opts.read(true); #[cfg(any(target_os = "linux", target_os = "android"))] - if let Some(libc_flags) = make_linux_iflags(&iflags) { + if let Some(libc_flags) = make_linux_iflags(&settings.iflags) { opts.custom_flags(libc_flags); } - opts.open(fname) - .map_err_context(|| format!("failed to open {}", fname.quote()))? + opts.open(filename) + .map_err_context(|| format!("failed to open {}", filename.quote()))? }; - if skip_amount > 0 { - src.seek(io::SeekFrom::Start(skip_amount)) + if settings.skip > 0 { + src.seek(io::SeekFrom::Start(settings.skip)) .map_err_context(|| "failed to seek in input file".to_string())?; } - Ok(Self { - src, - ibs, - print_level, - count, - cflags, - iflags, - }) + Ok(Self { src, settings }) } } -impl Read for Input { +impl<'a, R: Read> Read for Input<'a, R> { fn read(&mut self, buf: &mut [u8]) -> io::Result { let mut base_idx = 0; let target_len = buf.len(); loop { match self.src.read(&mut buf[base_idx..]) { Ok(0) => return Ok(base_idx), - Ok(rlen) if self.iflags.fullblock => { + Ok(rlen) if self.settings.iflags.fullblock => { base_idx += rlen; if base_idx >= target_len { @@ -175,14 +191,14 @@ impl Read for Input { } Ok(len) => return Ok(len), Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, - Err(_) if self.cflags.noerror => return Ok(base_idx), + Err(_) if self.settings.iconv.noerror => return Ok(base_idx), Err(e) => return Err(e), } } } } -impl Input { +impl<'a, R: Read> Input<'a, R> { /// Fills a given buffer. /// Reads in increments of 'self.ibs'. /// The start of each ibs-sized read follows the previous one. @@ -191,9 +207,9 @@ impl Input { let mut reads_partial = 0; let mut bytes_total = 0; - for chunk in buf.chunks_mut(self.ibs) { + for chunk in buf.chunks_mut(self.settings.ibs) { match self.read(chunk)? { - rlen if rlen == self.ibs => { + rlen if rlen == self.settings.ibs => { bytes_total += rlen; reads_complete += 1; } @@ -223,7 +239,7 @@ impl Input { let mut base_idx = 0; while base_idx < buf.len() { - let next_blk = cmp::min(base_idx + self.ibs, buf.len()); + let next_blk = cmp::min(base_idx + self.settings.ibs, buf.len()); let target_len = next_blk - base_idx; match self.read(&mut buf[base_idx..next_blk])? { @@ -238,7 +254,7 @@ impl Input { } } - base_idx += self.ibs; + base_idx += self.settings.ibs; } buf.truncate(base_idx); @@ -265,42 +281,30 @@ impl Input { } trait OutputTrait: Sized + Write { - fn new( - obs: usize, - oflags: OFlags, - cflags: OConvFlags, - seek_amount: u64, - fname: &str, - ) -> UResult; fn fsync(&mut self) -> io::Result<()>; fn fdatasync(&mut self) -> io::Result<()>; } -struct Output { +struct Output<'a, W: Write> { dst: W, - obs: usize, - cflags: OConvFlags, + settings: &'a Settings, } -impl OutputTrait for Output { - fn new( - obs: usize, - _oflags: OFlags, - cflags: OConvFlags, - seek_amount: u64, - _fname: &str, - ) -> UResult { +impl<'a> Output<'a, io::Stdout> { + fn new(settings: &'a Settings) -> UResult { let mut dst = io::stdout(); // stdout is not seekable, so we just write null bytes. - if seek_amount > 0 { - io::copy(&mut io::repeat(0u8).take(seek_amount), &mut dst) + if settings.seek > 0 { + io::copy(&mut io::repeat(0u8).take(settings.seek as u64), &mut dst) .map_err_context(|| String::from("write error"))?; } - Ok(Self { dst, obs, cflags }) + Ok(Self { dst, settings }) } +} +impl<'a> OutputTrait for Output<'a, io::Stdout> { fn fsync(&mut self) -> io::Result<()> { self.dst.flush() } @@ -310,7 +314,7 @@ impl OutputTrait for Output { } } -impl Output +impl<'a, W: Write> Output<'a, W> where Self: OutputTrait, { @@ -319,9 +323,9 @@ where let mut writes_partial = 0; let mut bytes_total = 0; - for chunk in buf.chunks(self.obs) { + for chunk in buf.chunks(self.settings.obs) { let wlen = self.write(chunk)?; - if wlen < self.obs { + if wlen < self.settings.obs { writes_partial += 1; } else { writes_complete += 1; @@ -338,9 +342,9 @@ where /// Flush the output to disk, if configured to do so. fn sync(&mut self) -> std::io::Result<()> { - if self.cflags.fsync { + if self.settings.oconv.fsync { self.fsync() - } else if self.cflags.fdatasync { + } else if self.settings.oconv.fdatasync { self.fdatasync() } else { // Intentionally do nothing in this case. @@ -381,7 +385,7 @@ where // // This is an educated guess about a good buffer size based on // the input and output block sizes. - let bsize = calc_bsize(i.ibs, self.obs); + let bsize = calc_bsize(i.settings.ibs, self.settings.obs); // Start a thread that reports transfer progress. // @@ -394,7 +398,7 @@ where // to the receives `rx`, and the receiver prints the transfer // information. let (prog_tx, rx) = mpsc::channel(); - let output_thread = thread::spawn(gen_prog_updater(rx, i.print_level)); + let output_thread = thread::spawn(gen_prog_updater(rx, i.settings.status)); let mut progress_as_secs = 0; // Create a common buffer with a capacity of the block size. @@ -407,13 +411,14 @@ where // blocks to this output. Read/write statistics are updated on // each iteration and cumulative statistics are reported to // the progress reporting thread. - while below_count_limit(&i.count, &rstat, &wstat) { + while below_count_limit(&i.settings.count, &rstat, &wstat) { // Read a block from the input then write the block to the output. // // As an optimization, make an educated guess about the // best buffer size for reading based on the number of // blocks already read and the number of blocks remaining. - let loop_bsize = calc_loop_bsize(&i.count, &rstat, &wstat, i.ibs, bsize); + let loop_bsize = + calc_loop_bsize(&i.settings.count, &rstat, &wstat, i.settings.ibs, bsize); let rstat_update = read_helper(&mut i, &mut buf, loop_bsize)?; if rstat_update.is_empty() { break; @@ -489,14 +494,8 @@ fn make_linux_oflags(oflags: &OFlags) -> Option { } } -impl OutputTrait for Output { - fn new( - obs: usize, - oflags: OFlags, - cflags: OConvFlags, - seek_amount: u64, - fname: &str, - ) -> UResult { +impl<'a> Output<'a, File> { + fn new(filename: &Path, settings: &'a Settings) -> UResult { fn open_dst(path: &Path, cflags: &OConvFlags, oflags: &OFlags) -> Result { let mut opts = OpenOptions::new(); opts.write(true) @@ -512,8 +511,8 @@ impl OutputTrait for Output { opts.open(path) } - let mut dst = open_dst(Path::new(&fname), &cflags, &oflags) - .map_err_context(|| format!("failed to open {}", fname.quote()))?; + let mut dst = open_dst(filename, &settings.oconv, &settings.oflags) + .map_err_context(|| format!("failed to open {}", filename.quote()))?; // Seek to the index in the output file, truncating if requested. // @@ -524,15 +523,17 @@ impl OutputTrait for Output { // the behavior of GNU `dd` when given the command-line // argument `of=/dev/null`. - if !cflags.notrunc { - dst.set_len(seek_amount).ok(); + if !settings.oconv.notrunc { + dst.set_len(settings.seek).ok(); } - dst.seek(io::SeekFrom::Start(seek_amount)) + dst.seek(io::SeekFrom::Start(settings.seek)) .map_err_context(|| "failed to seek in output file".to_string())?; - Ok(Self { dst, obs, cflags }) + Ok(Self { dst, settings }) } +} +impl<'a> OutputTrait for Output<'a, File> { fn fsync(&mut self) -> io::Result<()> { self.dst.flush()?; self.dst.sync_all() @@ -544,19 +545,19 @@ impl OutputTrait for Output { } } -impl Seek for Output { +impl<'a> Seek for Output<'a, File> { fn seek(&mut self, pos: io::SeekFrom) -> io::Result { self.dst.seek(pos) } } -impl Write for Output { +impl<'a> Write for Output<'a, File> { fn write(&mut self, buf: &[u8]) -> io::Result { fn is_sparse(buf: &[u8]) -> bool { buf.iter().all(|&e| e == 0u8) } // ----------------------------- - if self.cflags.sparse && is_sparse(buf) { + if self.settings.oconv.sparse && is_sparse(buf) { let seek_amt: i64 = buf .len() .try_into() @@ -573,7 +574,7 @@ impl Write for Output { } } -impl Write for Output { +impl<'a> Write for Output<'a, io::Stdout> { fn write(&mut self, buf: &[u8]) -> io::Result { self.dst.write(buf) } @@ -600,7 +601,7 @@ fn read_helper( // Resize the buffer to the bsize. Any garbage data in the buffer is overwritten or truncated, so there is no need to fill with BUF_INIT_BYTE first. buf.resize(bsize, BUF_INIT_BYTE); - let mut rstat = match i.cflags.sync { + let mut rstat = match i.settings.iconv.sync { Some(ch) => i.fill_blocks(buf, ch)?, _ => i.fill_consecutive(buf)?, }; @@ -610,11 +611,11 @@ fn read_helper( } // Perform any conv=x[,x...] options - if i.cflags.swab { + if i.settings.iconv.swab { perform_swab(buf); } - match i.cflags.mode { + match i.settings.iconv.mode { Some(ref mode) => { *buf = conv_block_unblock_helper(buf.clone(), mode, &mut rstat); Ok(rstat) @@ -638,19 +639,19 @@ fn calc_bsize(ibs: usize, obs: usize) -> usize { // Calculate the buffer size appropriate for this loop iteration, respecting // a count=N if present. fn calc_loop_bsize( - count: &Option, + count: &Option, rstat: &ReadStat, wstat: &WriteStat, ibs: usize, ideal_bsize: usize, ) -> usize { match count { - Some(CountType::Reads(rmax)) => { + Some(Num::Blocks(rmax)) => { let rsofar = rstat.reads_complete + rstat.reads_partial; let rremain = rmax - rsofar; cmp::min(ideal_bsize as u64, rremain * ibs as u64) as usize } - Some(CountType::Bytes(bmax)) => { + Some(Num::Bytes(bmax)) => { let bmax: u128 = (*bmax).try_into().unwrap(); let bremain: u128 = bmax - wstat.bytes_total; cmp::min(ideal_bsize as u128, bremain as u128) as usize @@ -661,13 +662,13 @@ fn calc_loop_bsize( // Decide if the current progress is below a count=N limit or return // true if no such limit is set. -fn below_count_limit(count: &Option, rstat: &ReadStat, wstat: &WriteStat) -> bool { +fn below_count_limit(count: &Option, rstat: &ReadStat, wstat: &WriteStat) -> bool { match count { - Some(CountType::Reads(n)) => { + Some(Num::Blocks(n)) => { let n = *n; rstat.reads_complete + rstat.reads_partial <= n } - Some(CountType::Bytes(n)) => { + Some(Num::Bytes(n)) => { let n = (*n).try_into().unwrap(); wstat.bytes_total <= n } @@ -675,14 +676,6 @@ fn below_count_limit(count: &Option, rstat: &ReadStat, wstat: &WriteS } } -fn append_dashes_if_not_present(mut acc: Vec, mut s: String) -> Vec { - if !s.starts_with("--") && !s.starts_with('-') { - s.insert_str(0, "--"); - } - acc.push(s); - acc -} - /// Canonicalized file name of `/dev/stdout`. /// /// For example, if this process were invoked from the command line as @@ -728,82 +721,48 @@ fn is_stdout_redirected_to_seekable_file() -> bool { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let dashed_args = args - .collect_ignore() - .into_iter() - .fold(Vec::new(), append_dashes_if_not_present); - - let matches = uu_app() - //.after_help(TODO: Add note about multiplier strings here.) - .try_get_matches_from(dashed_args)?; - - // Parse options for reading from input. - let ibs = parseargs::parse_ibs(&matches)?; - let print_level = parseargs::parse_status_level(&matches)?; - let icflags = parseargs::parse_conv_flag_input(&matches)?; - let iflags = parseargs::parse_iflags(&matches)?; - let skip = parseargs::parse_seek_skip_amt(&ibs, iflags.skip_bytes, &matches, options::SKIP)?; - let iseek = parseargs::parse_seek_skip_amt(&ibs, iflags.skip_bytes, &matches, options::ISEEK)?; - let count = parseargs::parse_count(&iflags, &matches)?; - // The --skip and --iseek flags are additive. On a file, they seek. - let skip_amount = skip.unwrap_or(0) + iseek.unwrap_or(0); - - // Parse options for writing to the output. - let obs = parseargs::parse_obs(&matches)?; - let ocflags = parseargs::parse_conv_flag_output(&matches)?; - let oflags = parseargs::parse_oflags(&matches)?; - let seek = parseargs::parse_seek_skip_amt(&obs, oflags.seek_bytes, &matches, options::SEEK)?; - let oseek = parseargs::parse_seek_skip_amt(&obs, oflags.seek_bytes, &matches, options::OSEEK)?; - // The --seek and --oseek flags are additive. - let seek_amount = seek.unwrap_or(0) + oseek.unwrap_or(0); - - match ( - matches.contains_id(options::INFILE), - matches.contains_id(options::OUTFILE), - ) { - (true, true) => { - let ifname = matches.value_of(options::INFILE).unwrap(); - let ofname = matches.value_of(options::OUTFILE).unwrap(); - - let i = Input::::new( - ibs, - print_level, - count, - icflags, - iflags, - ifname, - skip_amount, - )?; - let o = Output::::new(obs, oflags, ocflags, seek_amount, ofname)?; + let args = args.collect_ignore(); + + let matches = uu_app().try_get_matches_from(args)?; + + let settings: Settings = Parser::new().parse( + &matches + .get_many::(options::OPERANDS) + .unwrap_or_default() + .map(|s| s.as_ref()) + .collect::>()[..], + )?; + + match (&settings.infile, &settings.outfile) { + (Some(infile), Some(outfile)) => { + let i = Input::::new(Path::new(&infile), &settings)?; + let o = Output::::new(Path::new(&outfile), &settings)?; o.dd_out(i).map_err_context(|| "IO error".to_string()) } - (false, true) => { - let ofname = matches.value_of(options::OUTFILE).unwrap(); - let i = Input::::new(ibs, print_level, count, icflags, iflags, skip_amount)?; - let o = Output::::new(obs, oflags, ocflags, seek_amount, ofname)?; + (None, Some(outfile)) => { + let i = Input::::new(&settings)?; + let o = Output::::new(Path::new(&outfile), &settings)?; o.dd_out(i).map_err_context(|| "IO error".to_string()) } - (true, false) => { - let fname = matches.value_of(options::INFILE).unwrap(); - let i = - Input::::new(ibs, print_level, count, icflags, iflags, fname, skip_amount)?; + (Some(infile), None) => { + let i = Input::::new(Path::new(&infile), &settings)?; if is_stdout_redirected_to_seekable_file() { - let ofname = stdout_canonicalized().into_string().unwrap(); - let o = Output::::new(obs, oflags, ocflags, seek_amount, &ofname)?; + let filename = stdout_canonicalized(); + let o = Output::::new(Path::new(&filename), &settings)?; o.dd_out(i).map_err_context(|| "IO error".to_string()) } else { - let o = Output::::new(obs, oflags, ocflags, seek_amount, "-")?; + let o = Output::::new(&settings)?; o.dd_out(i).map_err_context(|| "IO error".to_string()) } } - (false, false) => { - let i = Input::::new(ibs, print_level, count, icflags, iflags, skip_amount)?; + (None, None) => { + let i = Input::::new(&settings)?; if is_stdout_redirected_to_seekable_file() { - let ofname = stdout_canonicalized().into_string().unwrap(); - let o = Output::::new(obs, oflags, ocflags, seek_amount, &ofname)?; + let filename = stdout_canonicalized(); + let o = Output::::new(Path::new(&filename), &settings)?; o.dd_out(i).map_err_context(|| "IO error".to_string()) } else { - let o = Output::::new(obs, oflags, ocflags, seek_amount, "-")?; + let o = Output::::new(&settings)?; o.dd_out(i).map_err_context(|| "IO error".to_string()) } } @@ -814,246 +773,22 @@ pub fn uu_app<'a>() -> Command<'a> { Command::new(uucore::util_name()) .version(crate_version!()) .about(ABOUT) + .after_help(AFTER_HELP) .infer_long_args(true) - .arg( - Arg::new(options::INFILE) - .long(options::INFILE) - .overrides_with(options::INFILE) - .takes_value(true) - .value_hint(clap::ValueHint::FilePath) - .require_equals(true) - .value_name("FILE") - .help("(alternatively if=FILE) specifies the file used for input. When not specified, stdin is used instead") - ) - .arg( - Arg::new(options::OUTFILE) - .long(options::OUTFILE) - .overrides_with(options::OUTFILE) - .takes_value(true) - .value_hint(clap::ValueHint::FilePath) - .require_equals(true) - .value_name("FILE") - .help("(alternatively of=FILE) specifies the file used for output. When not specified, stdout is used instead") - ) - .arg( - Arg::new(options::IBS) - .long(options::IBS) - .overrides_with(options::IBS) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively ibs=N) specifies the size of buffer used for reads (default: 512). Multiplier strings permitted.") - ) - .arg( - Arg::new(options::OBS) - .long(options::OBS) - .overrides_with(options::OBS) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively obs=N) specifies the size of buffer used for writes (default: 512). Multiplier strings permitted.") - ) - .arg( - Arg::new(options::BS) - .long(options::BS) - .overrides_with(options::BS) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively bs=N) specifies ibs=N and obs=N (default: 512). If ibs or obs are also specified, bs=N takes precedence. Multiplier strings permitted.") - ) - .arg( - Arg::new(options::CBS) - .long(options::CBS) - .overrides_with(options::CBS) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively cbs=BYTES) specifies the 'conversion block size' in bytes. Applies to the conv=block, and conv=unblock operations. Multiplier strings permitted.") - ) - .arg( - Arg::new(options::SKIP) - .long(options::SKIP) - .overrides_with(options::SKIP) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively skip=N) causes N ibs-sized records of input to be skipped before beginning copy/convert operations. See iflag=count_bytes if skipping N bytes is preferred. Multiplier strings permitted.") - ) - .arg( - Arg::new(options::SEEK) - .long(options::SEEK) - .overrides_with(options::SEEK) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively seek=N) seeks N obs-sized records into output before beginning copy/convert operations. See oflag=seek_bytes if seeking N bytes is preferred. Multiplier strings permitted.") - ) - .arg( - Arg::new(options::ISEEK) - .long(options::ISEEK) - .overrides_with(options::ISEEK) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively iseek=N) seeks N obs-sized records into input before beginning copy/convert operations. See iflag=seek_bytes if seeking N bytes is preferred. Multiplier strings permitted.") - ) - .arg( - Arg::new(options::OSEEK) - .long(options::OSEEK) - .overrides_with(options::OSEEK) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively oseek=N) seeks N obs-sized records into output before beginning copy/convert operations. See oflag=seek_bytes if seeking N bytes is preferred. Multiplier strings permitted.") - ) - .arg( - Arg::new(options::COUNT) - .long(options::COUNT) - .overrides_with(options::COUNT) - .takes_value(true) - .require_equals(true) - .value_name("N") - .help("(alternatively count=N) stop reading input after N ibs-sized read operations rather than proceeding until EOF. See iflag=count_bytes if stopping after N bytes is preferred. Multiplier strings permitted.") - ) - .arg( - Arg::new(options::STATUS) - .long(options::STATUS) - .overrides_with(options::STATUS) - .takes_value(true) - .require_equals(true) - .value_name("LEVEL") - .help("(alternatively status=LEVEL) controls whether volume and performance stats are written to stderr. - -When unspecified, dd will print stats upon completion. An example is below. -\t6+0 records in -\t16+0 records out -\t8192 bytes (8.2 kB, 8.0 KiB) copied, 0.00057009 s, 14.4 MB/s -The first two lines are the 'volume' stats and the final line is the 'performance' stats. -The volume stats indicate the number of complete and partial ibs-sized reads, or obs-sized writes that took place during the copy. The format of the volume stats is +. If records have been truncated (see conv=block), the volume stats will contain the number of truncated records. - -Permissible LEVEL values are: -\t progress: Print periodic performance stats as the copy proceeds. -\t noxfer: Print final volume stats, but not performance stats. -\t none: Do not print any stats. - -Printing performance stats is also triggered by the INFO signal (where supported), or the USR1 signal. Setting the POSIXLY_CORRECT environment variable to any value (including an empty value) will cause the USR1 signal to be ignored. - -") - ) - .arg( - Arg::new(options::CONV) - .long(options::CONV) - .takes_value(true) - .multiple_occurrences(true) - .use_value_delimiter(true) - .require_value_delimiter(true) - .multiple_values(true) - .require_equals(true) - .value_name("CONV") - .help("(alternatively conv=CONV[,CONV]) specifies a comma-separated list of conversion options or (for legacy reasons) file flags. Conversion options and file flags may be intermixed. - -Conversion options: -\t One of {ascii, ebcdic, ibm} will perform an encoding conversion. -\t\t 'ascii' converts from EBCDIC to ASCII. This is the inverse of the 'ebcdic' option. -\t\t 'ebcdic' converts from ASCII to EBCDIC. This is the inverse of the 'ascii' option. -\t\t 'ibm' converts from ASCII to EBCDIC, applying the conventions for '[', ']' and '~' specified in POSIX. - -\t One of {ucase, lcase} will perform a case conversion. Works in conjunction with option {ascii, ebcdic, ibm} to infer input encoding. If no other conversion option is specified, input is assumed to be ascii. -\t\t 'ucase' converts from lower-case to upper-case -\t\t 'lcase' converts from upper-case to lower-case. - -\t One of {block, unblock}. Convert between lines terminated by newline characters, and fixed-width lines padded by spaces (without any newlines). Both the 'block' and 'unblock' options require cbs=BYTES be specified. -\t\t 'block' for each newline less than the size indicated by cbs=BYTES, remove the newline and pad with spaces up to cbs. Lines longer than cbs are truncated. -\t\t 'unblock' for each block of input of the size indicated by cbs=BYTES, remove right-trailing spaces and replace with a newline character. - -\t 'sparse' attempts to seek the output when an obs-sized block consists of only zeros. -\t 'swab' swaps each adjacent pair of bytes. If an odd number of bytes is present, the final byte is omitted. -\t 'sync' pad each ibs-sided block with zeros. If 'block' or 'unblock' is specified, pad with spaces instead. - -Conversion Flags: -\t One of {excl, nocreat} -\t\t 'excl' the output file must be created. Fail if the output file is already present. -\t\t 'nocreat' the output file will not be created. Fail if the output file in not already present. -\t 'notrunc' the output file will not be truncated. If this option is not present, output will be truncated when opened. -\t 'noerror' all read errors will be ignored. If this option is not present, dd will only ignore Error::Interrupted. -\t 'fdatasync' data will be written before finishing. -\t 'fsync' data and metadata will be written before finishing. - -") - ) - .arg( - Arg::new(options::IFLAG) - .long(options::IFLAG) - .takes_value(true) - .multiple_occurrences(true) - .use_value_delimiter(true) - .require_value_delimiter(true) - .multiple_values(true) - .require_equals(true) - .value_name("FLAG") - .help("(alternatively iflag=FLAG[,FLAG]) a comma separated list of input flags which specify how the input source is treated. FLAG may be any of the input-flags or general-flags specified below. - -Input-Flags -\t 'count_bytes' a value to count=N will be interpreted as bytes. -\t 'skip_bytes' a value to skip=N will be interpreted as bytes. -\t 'fullblock' wait for ibs bytes from each read. zero-length reads are still considered EOF. - -General-Flags -\t 'direct' use direct I/O for data. -\t 'directory' fail unless the given input (if used as an iflag) or output (if used as an oflag) is a directory. -\t 'dsync' use synchronized I/O for data. -\t 'sync' use synchronized I/O for data and metadata. -\t 'nonblock' use non-blocking I/O. -\t 'noatime' do not update access time. -\t 'nocache' request that OS drop cache. -\t 'noctty' do not assign a controlling tty. -\t 'nofollow' do not follow system links. - -") - ) - .arg( - Arg::new(options::OFLAG) - .long(options::OFLAG) - .takes_value(true) - .multiple_occurrences(true) - .use_value_delimiter(true) - .require_value_delimiter(true) - .multiple_values(true) - .require_equals(true) - .value_name("FLAG") - .help("(alternatively oflag=FLAG[,FLAG]) a comma separated list of output flags which specify how the output source is treated. FLAG may be any of the output-flags or general-flags specified below. - -Output-Flags -\t 'append' open file in append mode. Consider setting conv=notrunc as well. -\t 'seek_bytes' a value to seek=N will be interpreted as bytes. - -General-Flags -\t 'direct' use direct I/O for data. -\t 'directory' fail unless the given input (if used as an iflag) or output (if used as an oflag) is a directory. -\t 'dsync' use synchronized I/O for data. -\t 'sync' use synchronized I/O for data and metadata. -\t 'nonblock' use non-blocking I/O. -\t 'noatime' do not update access time. -\t 'nocache' request that OS drop cache. -\t 'noctty' do not assign a controlling tty. -\t 'nofollow' do not follow system links. - -") - ) + .arg(Arg::new(options::OPERANDS).multiple_values(true)) } #[cfg(test)] mod tests { - - use crate::datastructures::{IConvFlags, IFlags, OConvFlags, OFlags}; - use crate::{calc_bsize, Input, Output, OutputTrait}; + use crate::datastructures::{IConvFlags, IFlags}; + use crate::{calc_bsize, Input, Output, Parser, Settings}; use std::cmp; use std::fs; use std::fs::File; use std::io; use std::io::{BufReader, Read}; + use std::path::Path; struct LazyReader { src: R, @@ -1137,40 +872,36 @@ mod tests { } #[test] - #[should_panic] fn test_nocreat_causes_failure_when_ofile_doesnt_exist() { - let obs = 1; - let oflags = OFlags::default(); - let ocflags = OConvFlags { - nocreat: true, - ..Default::default() - }; - let seek_amount = 0; - let ofname = "not-a-real.file"; - - let _ = Output::::new(obs, oflags, ocflags, seek_amount, ofname).unwrap(); + let args = &["conv=nocreat", "of=not-a-real.file"]; + let settings = Parser::new().parse(args).unwrap(); + assert!( + Output::::new(Path::new(settings.outfile.as_ref().unwrap()), &settings).is_err() + ); } #[test] fn test_deadbeef_16_delayed() { - let input = Input { - src: LazyReader { - src: File::open("./test-resources/deadbeef-16.test").unwrap(), - }, + let settings = Settings { ibs: 16, - print_level: None, + obs: 32, count: None, - cflags: IConvFlags { + iconv: IConvFlags { sync: Some(0), - ..IConvFlags::default() + ..Default::default() }, - iflags: IFlags::default(), + ..Default::default() + }; + let input = Input { + src: LazyReader { + src: File::open("./test-resources/deadbeef-16.test").unwrap(), + }, + settings: &settings, }; let output = Output { dst: File::create("./test-resources/FAILED-deadbeef-16-delayed.test").unwrap(), - obs: 32, - cflags: OConvFlags::default(), + settings: &settings, }; output.dd_out(input).unwrap(); @@ -1198,26 +929,28 @@ mod tests { #[test] fn test_random_73k_test_lazy_fullblock() { - let input = Input { - src: LazyReader { - src: File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test") - .unwrap(), - }, + let settings = Settings { ibs: 521, - print_level: None, + obs: 1031, count: None, - cflags: IConvFlags::default(), iflags: IFlags { fullblock: true, ..IFlags::default() }, + ..Default::default() + }; + let input = Input { + src: LazyReader { + src: File::open("./test-resources/random-5828891cb1230748e146f34223bbd3b5.test") + .unwrap(), + }, + settings: &settings, }; let output = Output { dst: File::create("./test-resources/FAILED-random_73k_test_lazy_fullblock.test") .unwrap(), - obs: 1031, - cflags: OConvFlags::default(), + settings: &settings, }; output.dd_out(input).unwrap(); diff --git a/src/uu/dd/src/parseargs.rs b/src/uu/dd/src/parseargs.rs index 701134ee14c..ecca621823b 100644 --- a/src/uu/dd/src/parseargs.rs +++ b/src/uu/dd/src/parseargs.rs @@ -4,22 +4,23 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore ctty, ctable, iseek, oseek, iconvflags, oconvflags parseargs +// spell-checker:ignore ctty, ctable, iseek, oseek, iconvflags, oconvflags parseargs outfile oconv #[cfg(test)] mod unit_tests; -use super::*; +use super::{ConversionMode, IConvFlags, IFlags, Num, OConvFlags, OFlags, Settings, StatusLevel}; +use crate::conversion_tables::ConversionTable; use std::error::Error; +use uucore::display::Quotable; use uucore::error::UError; use uucore::parse_size::ParseSizeError; use uucore::show_warning; -pub type Matches = ArgMatches; - /// Parser Errors describe errors with parser input #[derive(Debug, PartialEq, Eq)] pub enum ParseError { + UnrecognizedOperand(String), MultipleFmtTable, MultipleUCaseLCase, MultipleBlockUnblock, @@ -31,40 +32,370 @@ pub enum ParseError { BlockUnblockWithoutCBS, StatusLevelNotRecognized(String), Unimplemented(String), - BsOutOfRange, - IbsOutOfRange, - ObsOutOfRange, - CbsOutOfRange, + BsOutOfRange(String), InvalidNumber(String), } -impl ParseError { - /// Replace the argument, if any, with the given string, consuming self. - fn with_arg(self, s: String) -> Self { - match self { - Self::MultipleFmtTable => Self::MultipleFmtTable, - Self::MultipleUCaseLCase => Self::MultipleUCaseLCase, - Self::MultipleBlockUnblock => Self::MultipleBlockUnblock, - Self::MultipleExclNoCreate => Self::MultipleExclNoCreate, - Self::FlagNoMatch(_) => Self::FlagNoMatch(s), - Self::ConvFlagNoMatch(_) => Self::ConvFlagNoMatch(s), - Self::MultiplierStringParseFailure(_) => Self::MultiplierStringParseFailure(s), - Self::MultiplierStringOverflow(_) => Self::MultiplierStringOverflow(s), - Self::BlockUnblockWithoutCBS => Self::BlockUnblockWithoutCBS, - Self::StatusLevelNotRecognized(_) => Self::StatusLevelNotRecognized(s), - Self::Unimplemented(_) => Self::Unimplemented(s), - Self::BsOutOfRange => Self::BsOutOfRange, - Self::IbsOutOfRange => Self::IbsOutOfRange, - Self::ObsOutOfRange => Self::ObsOutOfRange, - Self::CbsOutOfRange => Self::CbsOutOfRange, - Self::InvalidNumber(_) => Self::InvalidNumber(s), +/// Contains a temporary state during parsing of the arguments +#[derive(Debug, PartialEq)] +pub struct Parser { + infile: Option, + outfile: Option, + ibs: usize, + obs: usize, + cbs: Option, + skip: Num, + seek: Num, + count: Option, + conv: ConvFlags, + iflag: IFlags, + oflag: OFlags, + status: Option, +} + +impl Default for Parser { + fn default() -> Self { + Self { + ibs: 512, + obs: 512, + cbs: None, + infile: None, + outfile: None, + skip: Num::Blocks(0), + seek: Num::Blocks(0), + count: None, + conv: ConvFlags::default(), + iflag: IFlags::default(), + oflag: OFlags::default(), + status: None, + } + } +} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct ConvFlags { + ascii: bool, + ebcdic: bool, + ibm: bool, + ucase: bool, + lcase: bool, + block: bool, + unblock: bool, + swab: bool, + sync: bool, + noerror: bool, + sparse: bool, + excl: bool, + nocreat: bool, + notrunc: bool, + fdatasync: bool, + fsync: bool, +} + +#[derive(Clone, Copy, PartialEq)] +enum Conversion { + Ascii, + Ebcdic, + Ibm, +} + +#[derive(Clone, Copy)] +enum Case { + Lower, + Upper, +} + +#[derive(Clone, Copy)] +enum Block { + Block(usize), + Unblock(usize), +} + +/// Return an Unimplemented error when the target is not Linux or Android +macro_rules! linux_only { + ($s: expr, $val: expr) => { + if cfg!(any(target_os = "linux", target_os = "android")) { + $val + } else { + return Err(ParseError::Unimplemented($s.to_string()).into()); + } + }; +} + +impl Parser { + pub(crate) fn new() -> Self { + Self::default() + } + + pub(crate) fn parse(self, operands: &[&str]) -> Result { + self.read(operands)?.validate() + } + + pub(crate) fn read(mut self, operands: &[&str]) -> Result { + for operand in operands { + self.parse_operand(operand)?; + } + + Ok(self) + } + + pub(crate) fn validate(self) -> Result { + let conv = self.conv; + let conversion = match (conv.ascii, conv.ebcdic, conv.ibm) { + (false, false, false) => None, + (true, false, false) => Some(Conversion::Ascii), + (false, true, false) => Some(Conversion::Ebcdic), + (false, false, true) => Some(Conversion::Ibm), + _ => return Err(ParseError::MultipleFmtTable), + }; + + let case = match (conv.ucase, conv.lcase) { + (false, false) => None, + (true, false) => Some(Case::Upper), + (false, true) => Some(Case::Lower), + (true, true) => return Err(ParseError::MultipleUCaseLCase), + }; + + let non_ascii = matches!(conversion, Some(Conversion::Ascii)); + let conversion_table = get_ctable(conversion, case); + + if conv.nocreat && conv.excl { + return Err(ParseError::MultipleExclNoCreate); + } + + // The GNU docs state that + // - ascii implies unblock + // - ebcdic and ibm imply block + // This has a side effect in how it's implemented in GNU, because this errors: + // conv=block,unblock + // but these don't: + // conv=ascii,block,unblock + // conv=block,ascii,unblock + // conv=block,unblock,ascii + // conv=block conv=unblock conv=ascii + let block = if let Some(cbs) = self.cbs { + match conversion { + Some(Conversion::Ascii) => Some(Block::Unblock(cbs)), + Some(_) => Some(Block::Block(cbs)), + None => match (conv.block, conv.unblock) { + (false, false) => None, + (true, false) => Some(Block::Block(cbs)), + (false, true) => Some(Block::Unblock(cbs)), + (true, true) => return Err(ParseError::MultipleBlockUnblock), + }, + } + } else if conv.block || conv.unblock { + return Err(ParseError::BlockUnblockWithoutCBS); + } else { + None + }; + + let iconv = IConvFlags { + mode: conversion_mode(conversion_table, block, non_ascii, conv.sync), + swab: conv.swab, + sync: if conv.sync { + if block.is_some() { + Some(b' ') + } else { + Some(0u8) + } + } else { + None + }, + noerror: conv.noerror, + }; + + let oconv = OConvFlags { + sparse: conv.sparse, + excl: conv.excl, + nocreat: conv.nocreat, + notrunc: conv.notrunc, + fdatasync: conv.fdatasync, + fsync: conv.fsync, + }; + + let skip = self + .skip + .force_bytes_if(self.iflag.skip_bytes) + .to_bytes(self.ibs as u64); + + let seek = self + .seek + .force_bytes_if(self.oflag.seek_bytes) + .to_bytes(self.obs as u64); + + let count = self.count.map(|c| c.force_bytes_if(self.iflag.count_bytes)); + + Ok(Settings { + skip, + seek, + count, + iconv, + oconv, + ibs: self.ibs, + obs: self.obs, + infile: self.infile, + outfile: self.outfile, + iflags: self.iflag, + oflags: self.oflag, + status: self.status, + }) + } + + fn parse_operand(&mut self, operand: &str) -> Result<(), ParseError> { + match operand.split_once('=') { + None => return Err(ParseError::UnrecognizedOperand(operand.to_string())), + Some((k, v)) => match k { + "bs" => { + let bs = self.parse_bytes(k, v)?; + self.ibs = bs; + self.obs = bs; + } + "cbs" => self.cbs = Some(self.parse_bytes(k, v)?), + "conv" => self.parse_conv_flags(v)?, + "count" => self.count = Some(self.parse_n(v)?), + "ibs" => self.ibs = self.parse_bytes(k, v)?, + "if" => self.infile = Some(v.to_string()), + "iflag" => self.parse_input_flags(v)?, + "obs" => self.obs = self.parse_bytes(k, v)?, + "of" => self.outfile = Some(v.to_string()), + "oflag" => self.parse_output_flags(v)?, + "seek" | "oseek" => self.seek = self.parse_n(v)?, + "skip" | "iseek" => self.skip = self.parse_n(v)?, + "status" => self.status = Some(self.parse_status_level(v)?), + _ => return Err(ParseError::UnrecognizedOperand(operand.to_string())), + }, + } + Ok(()) + } + + fn parse_n(&self, val: &str) -> Result { + let n = parse_bytes_with_opt_multiplier(val)?; + Ok(if val.ends_with('B') { + Num::Bytes(n) + } else { + Num::Blocks(n) + }) + } + + fn parse_bytes(&self, arg: &str, val: &str) -> Result { + parse_bytes_with_opt_multiplier(val)? + .try_into() + .map_err(|_| ParseError::BsOutOfRange(arg.to_string())) + } + + fn parse_status_level(&self, val: &str) -> Result { + match val { + "none" => Ok(StatusLevel::None), + "noxfer" => Ok(StatusLevel::Noxfer), + "progress" => Ok(StatusLevel::Progress), + _ => Err(ParseError::StatusLevelNotRecognized(val.to_string())), + } + } + + fn parse_input_flags(&mut self, val: &str) -> Result<(), ParseError> { + let mut i = &mut self.iflag; + for f in val.split(',') { + match f { + // Common flags + "cio" => return Err(ParseError::Unimplemented(f.to_string())), + "direct" => linux_only!(f, i.direct = true), + "directory" => linux_only!(f, i.directory = true), + "dsync" => linux_only!(f, i.dsync = true), + "sync" => linux_only!(f, i.sync = true), + "nocache" => return Err(ParseError::Unimplemented(f.to_string())), + "nonblock" => linux_only!(f, i.nonblock = true), + "noatime" => linux_only!(f, i.noatime = true), + "noctty" => linux_only!(f, i.noctty = true), + "nofollow" => linux_only!(f, i.nofollow = true), + "nolinks" => return Err(ParseError::Unimplemented(f.to_string())), + "binary" => return Err(ParseError::Unimplemented(f.to_string())), + "text" => return Err(ParseError::Unimplemented(f.to_string())), + + // Input-only flags + "fullblock" => i.fullblock = true, + "count_bytes" => i.count_bytes = true, + "skip_bytes" => i.skip_bytes = true, + // GNU silently ignores oflags given as iflag. + "append" | "seek_bytes" => {} + _ => return Err(ParseError::FlagNoMatch(f.to_string())), + } + } + Ok(()) + } + + fn parse_output_flags(&mut self, val: &str) -> Result<(), ParseError> { + let mut o = &mut self.oflag; + for f in val.split(',') { + match f { + // Common flags + "cio" => return Err(ParseError::Unimplemented(val.to_string())), + "direct" => linux_only!(f, o.direct = true), + "directory" => linux_only!(f, o.directory = true), + "dsync" => linux_only!(f, o.dsync = true), + "sync" => linux_only!(f, o.sync = true), + "nocache" => return Err(ParseError::Unimplemented(f.to_string())), + "nonblock" => linux_only!(f, o.nonblock = true), + "noatime" => linux_only!(f, o.noatime = true), + "noctty" => linux_only!(f, o.noctty = true), + "nofollow" => linux_only!(f, o.nofollow = true), + "nolinks" => return Err(ParseError::Unimplemented(f.to_string())), + "binary" => return Err(ParseError::Unimplemented(f.to_string())), + "text" => return Err(ParseError::Unimplemented(f.to_string())), + + // Output-only flags + "append" => o.append = true, + "seek_bytes" => o.seek_bytes = true, + // GNU silently ignores iflags given as oflag. + "fullblock" | "count_bytes" | "skip_bytes" => {} + _ => return Err(ParseError::FlagNoMatch(f.to_string())), + } + } + Ok(()) + } + + fn parse_conv_flags(&mut self, val: &str) -> Result<(), ParseError> { + let mut c = &mut self.conv; + for f in val.split(',') { + match f { + // Conversion + "ascii" => c.ascii = true, + "ebcdic" => c.ebcdic = true, + "ibm" => c.ibm = true, + + // Case + "lcase" => c.lcase = true, + "ucase" => c.ucase = true, + + // Block + "block" => c.block = true, + "unblock" => c.unblock = true, + + // Other input + "swab" => c.swab = true, + "sync" => c.sync = true, + "noerror" => c.noerror = true, + + // Output + "sparse" => c.sparse = true, + "excl" => c.excl = true, + "nocreat" => c.nocreat = true, + "notrunc" => c.notrunc = true, + "fdatasync" => c.fdatasync = true, + "fsync" => c.fsync = true, + _ => return Err(ParseError::ConvFlagNoMatch(f.to_string())), + } } + Ok(()) } } impl std::fmt::Display for ParseError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + Self::UnrecognizedOperand(arg) => { + write!(f, "Unrecognized operand '{}'", arg) + } Self::MultipleFmtTable => { write!( f, @@ -107,17 +438,8 @@ impl std::fmt::Display for ParseError { Self::StatusLevelNotRecognized(arg) => { write!(f, "status=LEVEL not recognized -> {}", arg) } - Self::BsOutOfRange => { - write!(f, "bs=N cannot fit into memory") - } - Self::IbsOutOfRange => { - write!(f, "ibs=N cannot fit into memory") - } - Self::ObsOutOfRange => { - write!(f, "obs=N cannot fit into memory") - } - Self::CbsOutOfRange => { - write!(f, "cbs=N cannot fit into memory") + Self::BsOutOfRange(arg) => { + write!(f, "{}=N cannot fit into memory", arg) } Self::Unimplemented(arg) => { write!(f, "feature not implemented on this system -> {}", arg) @@ -137,224 +459,6 @@ impl UError for ParseError { } } -/// Some flags specified as part of a conv=CONV\[,CONV\]... block -/// relate to the input file, others to the output file. -#[derive(Debug, PartialEq)] -enum ConvFlag { - // Input - FmtAtoE, - FmtEtoA, - FmtAtoI, - Block, - Unblock, - UCase, - LCase, - Swab, - Sync, - NoError, - // Output - Sparse, - Excl, - NoCreat, - NoTrunc, - FDataSync, - FSync, -} - -impl std::str::FromStr for ConvFlag { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match s { - // Input - "ascii" => Ok(Self::FmtEtoA), - "ebcdic" => Ok(Self::FmtAtoE), - "ibm" => Ok(Self::FmtAtoI), - "lcase" => Ok(Self::LCase), - "ucase" => Ok(Self::UCase), - "block" => Ok(Self::Block), - "unblock" => Ok(Self::Unblock), - "swab" => Ok(Self::Swab), - "sync" => Ok(Self::Sync), - "noerror" => Ok(Self::NoError), - // Output - "sparse" => Ok(Self::Sparse), - "excl" => Ok(Self::Excl), - "nocreat" => Ok(Self::NoCreat), - "notrunc" => Ok(Self::NoTrunc), - "fdatasync" => Ok(Self::FDataSync), - "fsync" => Ok(Self::FSync), - _ => Err(ParseError::ConvFlagNoMatch(String::from(s))), - } - } -} - -#[derive(Debug, PartialEq)] -enum Flag { - // Input only - FullBlock, - CountBytes, - SkipBytes, - // Either - #[allow(unused)] - Cio, - #[allow(unused)] - Direct, - #[allow(unused)] - Directory, - #[allow(unused)] - Dsync, - #[allow(unused)] - Sync, - #[allow(unused)] - NoCache, - #[allow(unused)] - NonBlock, - #[allow(unused)] - NoATime, - #[allow(unused)] - NoCtty, - #[allow(unused)] - NoFollow, - #[allow(unused)] - NoLinks, - #[allow(unused)] - Binary, - #[allow(unused)] - Text, - // Output only - Append, - SeekBytes, -} - -impl std::str::FromStr for Flag { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match s { - // Input only - "fullblock" => Ok(Self::FullBlock), - "count_bytes" => Ok(Self::CountBytes), - "skip_bytes" => Ok(Self::SkipBytes), - // Either - "cio" => - // Ok(Self::Cio), - { - Err(ParseError::Unimplemented(s.to_string())) - } - "direct" => - // Ok(Self::Direct), - { - if cfg!(any(target_os = "linux", target_os = "android")) { - Ok(Self::Direct) - } else { - Err(ParseError::Unimplemented(s.to_string())) - } - } - "directory" => - // Ok(Self::Directory), - { - if cfg!(any(target_os = "linux", target_os = "android")) { - Ok(Self::Directory) - } else { - Err(ParseError::Unimplemented(s.to_string())) - } - } - "dsync" => - // Ok(Self::Dsync), - { - if cfg!(any(target_os = "linux", target_os = "android")) { - Ok(Self::Dsync) - } else { - Err(ParseError::Unimplemented(s.to_string())) - } - } - "sync" => - // Ok(Self::Sync), - { - if cfg!(any(target_os = "linux", target_os = "android")) { - Ok(Self::Sync) - } else { - Err(ParseError::Unimplemented(s.to_string())) - } - } - "nocache" => - // Ok(Self::NoCache), - { - Err(ParseError::Unimplemented(s.to_string())) - } - "nonblock" => - // Ok(Self::NonBlock), - { - if cfg!(any(target_os = "linux", target_os = "android")) { - Ok(Self::NonBlock) - } else { - Err(ParseError::Unimplemented(s.to_string())) - } - } - "noatime" => - // Ok(Self::NoATime), - { - if cfg!(any(target_os = "linux", target_os = "android")) { - Ok(Self::NoATime) - } else { - Err(ParseError::Unimplemented(s.to_string())) - } - } - "noctty" => - // Ok(Self::NoCtty), - { - if cfg!(any(target_os = "linux", target_os = "android")) { - Ok(Self::NoCtty) - } else { - Err(ParseError::Unimplemented(s.to_string())) - } - } - "nofollow" => - // Ok(Self::NoFollow), - { - if cfg!(any(target_os = "linux", target_os = "android")) { - Ok(Self::NoFollow) - } else { - Err(ParseError::Unimplemented(s.to_string())) - } - } - "nolinks" => - // Ok(Self::NoLinks), - { - Err(ParseError::Unimplemented(s.to_string())) - } - "binary" => - // Ok(Self::Binary), - { - Err(ParseError::Unimplemented(s.to_string())) - } - "text" => - // Ok(Self::Text), - { - Err(ParseError::Unimplemented(s.to_string())) - } - // Output only - "append" => Ok(Self::Append), - "seek_bytes" => Ok(Self::SeekBytes), - _ => Err(ParseError::FlagNoMatch(String::from(s))), - } - } -} - -impl std::str::FromStr for StatusLevel { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - match s { - "none" => Ok(Self::None), - "noxfer" => Ok(Self::Noxfer), - "progress" => Ok(Self::Progress), - _ => Err(ParseError::StatusLevelNotRecognized(s.to_string())), - } - } -} - fn show_zero_multiplier_warning() { show_warning!( "{} is a zero multiplier; use {} if that is intended", @@ -388,36 +492,36 @@ fn parse_bytes_only(s: &str) -> Result { /// # Examples /// /// ```rust,ignore -/// assert_eq!(parse_bytes_no_x("123").unwrap(), 123); -/// assert_eq!(parse_bytes_no_x("2c").unwrap(), 2 * 1); -/// assert_eq!(parse_bytes_no_x("3w").unwrap(), 3 * 2); -/// assert_eq!(parse_bytes_no_x("2b").unwrap(), 2 * 512); -/// assert_eq!(parse_bytes_no_x("2k").unwrap(), 2 * 1024); +/// assert_eq!(parse_bytes_no_x("123", "123").unwrap(), 123); +/// assert_eq!(parse_bytes_no_x("2c", "2c").unwrap(), 2 * 1); +/// assert_eq!(parse_bytes_no_x("3w", "3w").unwrap(), 3 * 2); +/// assert_eq!(parse_bytes_no_x("2b", "2b").unwrap(), 2 * 512); +/// assert_eq!(parse_bytes_no_x("2k", "2k").unwrap(), 2 * 1024); /// ``` -fn parse_bytes_no_x(s: &str) -> Result { +fn parse_bytes_no_x(full: &str, s: &str) -> Result { let (num, multiplier) = match (s.find('c'), s.rfind('w'), s.rfind('b')) { (None, None, None) => match uucore::parse_size::parse_size(s) { Ok(n) => (n, 1), - Err(ParseSizeError::InvalidSuffix(s)) | Err(ParseSizeError::ParseFailure(s)) => { - return Err(ParseError::InvalidNumber(s)) + Err(ParseSizeError::InvalidSuffix(_)) | Err(ParseSizeError::ParseFailure(_)) => { + return Err(ParseError::InvalidNumber(full.to_string())) } - Err(ParseSizeError::SizeTooBig(s)) => { - return Err(ParseError::MultiplierStringOverflow(s)) + Err(ParseSizeError::SizeTooBig(_)) => { + return Err(ParseError::MultiplierStringOverflow(full.to_string())) } }, (Some(i), None, None) => (parse_bytes_only(&s[..i])?, 1), (None, Some(i), None) => (parse_bytes_only(&s[..i])?, 2), (None, None, Some(i)) => (parse_bytes_only(&s[..i])?, 512), - _ => return Err(ParseError::MultiplierStringParseFailure(s.to_string())), + _ => return Err(ParseError::MultiplierStringParseFailure(full.to_string())), }; num.checked_mul(multiplier) - .ok_or_else(|| ParseError::MultiplierStringOverflow(s.to_string())) + .ok_or_else(|| ParseError::MultiplierStringOverflow(full.to_string())) } /// Parse byte and multiplier like 512, 5KiB, or 1G. /// Uses uucore::parse_size, and adds the 'w' and 'c' suffixes which are mentioned /// in dd's info page. -fn parse_bytes_with_opt_multiplier(s: &str) -> Result { +pub fn parse_bytes_with_opt_multiplier(s: &str) -> Result { // TODO On my Linux system, there seems to be a maximum block size of 4096 bytes: // // $ printf "%0.sa" {1..10000} | dd bs=4095 count=1 status=none | wc -c @@ -434,115 +538,47 @@ fn parse_bytes_with_opt_multiplier(s: &str) -> Result { // individually, then multiplied together. let parts: Vec<&str> = s.split('x').collect(); if parts.len() == 1 { - parse_bytes_no_x(parts[0]).map_err(|e| e.with_arg(s.to_string())) + parse_bytes_no_x(s, parts[0]) } else { - let mut total = 1; + let mut total: u64 = 1; for part in parts { if part == "0" { show_zero_multiplier_warning(); } - let num = parse_bytes_no_x(part).map_err(|e| e.with_arg(s.to_string()))?; - total *= num; + let num = parse_bytes_no_x(s, part)?; + total = total + .checked_mul(num) + .ok_or_else(|| ParseError::InvalidNumber(s.to_string()))?; } Ok(total) } } -pub fn parse_ibs(matches: &Matches) -> Result { - if let Some(mixed_str) = matches.value_of(options::BS) { - parse_bytes_with_opt_multiplier(mixed_str)? - .try_into() - .map_err(|_| ParseError::BsOutOfRange) - } else if let Some(mixed_str) = matches.value_of(options::IBS) { - parse_bytes_with_opt_multiplier(mixed_str)? - .try_into() - .map_err(|_| ParseError::IbsOutOfRange) - } else { - Ok(512) - } -} - -pub fn parse_obs(matches: &Matches) -> Result { - if let Some(mixed_str) = matches.value_of("bs") { - parse_bytes_with_opt_multiplier(mixed_str)? - .try_into() - .map_err(|_| ParseError::BsOutOfRange) - } else if let Some(mixed_str) = matches.value_of("obs") { - parse_bytes_with_opt_multiplier(mixed_str)? - .try_into() - .map_err(|_| ParseError::ObsOutOfRange) - } else { - Ok(512) - } -} - -fn parse_cbs(matches: &Matches) -> Result, ParseError> { - if let Some(s) = matches.value_of(options::CBS) { - let bytes = parse_bytes_with_opt_multiplier(s)? - .try_into() - .map_err(|_| ParseError::CbsOutOfRange)?; - Ok(Some(bytes)) - } else { - Ok(None) - } -} - -pub(crate) fn parse_status_level(matches: &Matches) -> Result, ParseError> { - match matches.value_of(options::STATUS) { - Some(s) => { - let st = s.parse()?; - Ok(Some(st)) - } - None => Ok(None), - } -} - -fn parse_ctable(fmt: Option, case: Option) -> Option<&'static ConversionTable> { - fn parse_conv_and_case_table( - fmt: &ConvFlag, - case: &ConvFlag, - ) -> Option<&'static ConversionTable> { - match (fmt, case) { - (ConvFlag::FmtAtoE, ConvFlag::UCase) => Some(&ASCII_TO_EBCDIC_LCASE_TO_UCASE), - (ConvFlag::FmtAtoE, ConvFlag::LCase) => Some(&ASCII_TO_EBCDIC_UCASE_TO_LCASE), - (ConvFlag::FmtEtoA, ConvFlag::UCase) => Some(&EBCDIC_TO_ASCII_LCASE_TO_UCASE), - (ConvFlag::FmtEtoA, ConvFlag::LCase) => Some(&EBCDIC_TO_ASCII_UCASE_TO_LCASE), - (ConvFlag::FmtAtoI, ConvFlag::UCase) => Some(&ASCII_TO_IBM_UCASE_TO_LCASE), - (ConvFlag::FmtAtoI, ConvFlag::LCase) => Some(&ASCII_TO_IBM_LCASE_TO_UCASE), - (_, _) => None, - } - } - fn parse_conv_table_only(fmt: &ConvFlag) -> Option<&'static ConversionTable> { - match fmt { - ConvFlag::FmtAtoE => Some(&ASCII_TO_EBCDIC), - ConvFlag::FmtEtoA => Some(&EBCDIC_TO_ASCII), - ConvFlag::FmtAtoI => Some(&ASCII_TO_IBM), - _ => None, - } - } - // ------------------------------------------------------------------------ - match (fmt, case) { - // Both [ascii | ebcdic | ibm] and [lcase | ucase] specified - (Some(fmt), Some(case)) => parse_conv_and_case_table(&fmt, &case), - // Only [ascii | ebcdic | ibm] specified - (Some(fmt), None) => parse_conv_table_only(&fmt), - // Only [lcase | ucase] specified - (None, Some(ConvFlag::UCase)) => Some(&ASCII_LCASE_TO_UCASE), - (None, Some(ConvFlag::LCase)) => Some(&ASCII_UCASE_TO_LCASE), - // ST else... - (_, _) => None, - } -} - -fn parse_flag_list>( - tag: &str, - matches: &Matches, -) -> Result, ParseError> { - matches - .get_many::(tag) - .unwrap_or_default() - .map(|f| f.parse()) - .collect() +fn get_ctable( + conversion: Option, + case: Option, +) -> Option<&'static ConversionTable> { + use crate::conversion_tables::*; + Some(match (conversion, case) { + (None, None) => return None, + (Some(conv), None) => match conv { + Conversion::Ascii => &EBCDIC_TO_ASCII, + Conversion::Ebcdic => &ASCII_TO_EBCDIC, + Conversion::Ibm => &ASCII_TO_IBM, + }, + (None, Some(case)) => match case { + Case::Lower => &ASCII_UCASE_TO_LCASE, + Case::Upper => &ASCII_LCASE_TO_UCASE, + }, + (Some(conv), Some(case)) => match (conv, case) { + (Conversion::Ascii, Case::Upper) => &EBCDIC_TO_ASCII_LCASE_TO_UCASE, + (Conversion::Ascii, Case::Lower) => &EBCDIC_TO_ASCII_UCASE_TO_LCASE, + (Conversion::Ebcdic, Case::Upper) => &ASCII_TO_EBCDIC_LCASE_TO_UCASE, + (Conversion::Ebcdic, Case::Lower) => &ASCII_TO_EBCDIC_UCASE_TO_LCASE, + (Conversion::Ibm, Case::Upper) => &ASCII_TO_IBM_UCASE_TO_LCASE, + (Conversion::Ibm, Case::Lower) => &ASCII_TO_IBM_LCASE_TO_UCASE, + }, + }) } /// Given the various command-line parameters, determine the conversion mode. @@ -554,303 +590,30 @@ fn parse_flag_list>( /// parameters. This function translates those settings to a /// [`ConversionMode`]. fn conversion_mode( - ctable: Option<&ConversionTable>, - block: Option, - unblock: Option, - non_ascii: bool, + ctable: Option<&'static ConversionTable>, + block: Option, + is_ascii: bool, is_sync: bool, ) -> Option { - match (ctable, block, unblock) { - (Some(ct), None, None) => Some(ConversionMode::ConvertOnly(ct)), - (Some(ct), Some(cbs), None) => { - if non_ascii { + match (ctable, block) { + (Some(ct), None) => Some(ConversionMode::ConvertOnly(ct)), + (Some(ct), Some(Block::Block(cbs))) => { + if is_ascii { Some(ConversionMode::ConvertThenBlock(ct, cbs, is_sync)) } else { Some(ConversionMode::BlockThenConvert(ct, cbs, is_sync)) } } - (Some(ct), None, Some(cbs)) => { - if non_ascii { + (Some(ct), Some(Block::Unblock(cbs))) => { + if is_ascii { Some(ConversionMode::ConvertThenUnblock(ct, cbs)) } else { Some(ConversionMode::UnblockThenConvert(ct, cbs)) } } - (None, Some(cbs), None) => Some(ConversionMode::BlockOnly(cbs, is_sync)), - (None, None, Some(cbs)) => Some(ConversionMode::UnblockOnly(cbs)), - (None, None, None) => None, - // The remaining variants should never happen because the - // argument parsing above should result in an error before - // getting to this line of code. - _ => unreachable!(), - } -} - -/// Parse Conversion Options (Input Variety) -/// Construct and validate a IConvFlags -pub(crate) fn parse_conv_flag_input(matches: &Matches) -> Result { - let mut iconvflags = IConvFlags::default(); - let mut fmt = None; - let mut case = None; - let mut is_sync = false; - - let flags = parse_flag_list(options::CONV, matches)?; - let cbs = parse_cbs(matches)?; - - let mut block = None; - let mut unblock = None; - - for flag in flags { - match flag { - ConvFlag::FmtEtoA => { - if fmt.is_some() { - return Err(ParseError::MultipleFmtTable); - } else { - fmt = Some(flag); - // From the GNU documentation: - // - // > ‘ascii’ - // > - // > Convert EBCDIC to ASCII, using the conversion - // > table specified by POSIX. This provides a 1:1 - // > translation for all 256 bytes. This implies - // > ‘conv=unblock’; input is converted to ASCII - // > before trailing spaces are deleted. - // - // -- https://www.gnu.org/software/coreutils/manual/html_node/dd-invocation.html - if cbs.is_some() { - unblock = cbs; - } - } - } - ConvFlag::FmtAtoE => { - if fmt.is_some() { - return Err(ParseError::MultipleFmtTable); - } else { - fmt = Some(flag); - // From the GNU documentation: - // - // > ‘ebcdic’ - // > - // > Convert ASCII to EBCDIC. This is the inverse - // > of the ‘ascii’ conversion. This implies - // > ‘conv=block’; trailing spaces are added before - // > being converted to EBCDIC. - // - // -- https://www.gnu.org/software/coreutils/manual/html_node/dd-invocation.html - if cbs.is_some() { - block = cbs; - } - } - } - ConvFlag::FmtAtoI => { - if fmt.is_some() { - return Err(ParseError::MultipleFmtTable); - } else { - fmt = Some(flag); - } - } - ConvFlag::UCase | ConvFlag::LCase => { - if case.is_some() { - return Err(ParseError::MultipleUCaseLCase); - } else { - case = Some(flag); - } - } - ConvFlag::Block => match (cbs, unblock) { - (Some(cbs), None) => block = Some(cbs), - (None, _) => return Err(ParseError::BlockUnblockWithoutCBS), - (_, Some(_)) => return Err(ParseError::MultipleBlockUnblock), - }, - ConvFlag::Unblock => match (cbs, block) { - (Some(cbs), None) => unblock = Some(cbs), - (None, _) => return Err(ParseError::BlockUnblockWithoutCBS), - (_, Some(_)) => return Err(ParseError::MultipleBlockUnblock), - }, - ConvFlag::Swab => iconvflags.swab = true, - ConvFlag::Sync => is_sync = true, - ConvFlag::NoError => iconvflags.noerror = true, - _ => {} - } - } - - // The final conversion table depends on both - // fmt (eg. ASCII -> EBCDIC) - // case (eg. UCASE -> LCASE) - // So the final value can't be set until all flags are parsed. - let ctable = parse_ctable(fmt, case); - - // The final value of sync depends on block/unblock - // block implies sync with ' ' - // unblock implies sync with 0 - // So the final value can't be set until all flags are parsed. - let sync = if is_sync && (block.is_some() || unblock.is_some()) { - Some(b' ') - } else if is_sync { - Some(0u8) - } else { - None - }; - - // Some user options, such as the presence of conversion tables, - // will determine whether the input is assumed to be ascii. This - // parser sets the non_ascii flag accordingly. - // - // Examples: - // - // - If conv=ebcdic or conv=ibm is specified then block, - // unblock or swab must be performed before the conversion - // happens since the source will start in ascii. - // - If conv=ascii is specified then block, unblock or swab - // must be performed after the conversion since the source - // starts in ebcdic. - // - If no conversion is specified then the source is assumed - // to be in ascii. - // - // For more info see `info dd`. - let non_ascii = parseargs::parse_input_non_ascii(matches)?; - let mode = conversion_mode(ctable, block, unblock, non_ascii, is_sync); - - Ok(IConvFlags { - mode, - sync, - ..iconvflags - }) -} - -/// Parse Conversion Options (Output Variety) -/// Construct and validate a OConvFlags -pub fn parse_conv_flag_output(matches: &Matches) -> Result { - let mut oconvflags = OConvFlags::default(); - - let flags = parse_flag_list(options::CONV, matches)?; - - for flag in flags { - match flag { - ConvFlag::Sparse => oconvflags.sparse = true, - ConvFlag::Excl => { - if !oconvflags.nocreat { - oconvflags.excl = true; - } else { - return Err(ParseError::MultipleExclNoCreate); - } - } - ConvFlag::NoCreat => { - if !oconvflags.excl { - oconvflags.nocreat = true; - } else { - return Err(ParseError::MultipleExclNoCreate); - } - } - ConvFlag::NoTrunc => oconvflags.notrunc = true, - ConvFlag::FDataSync => oconvflags.fdatasync = true, - ConvFlag::FSync => oconvflags.fsync = true, - _ => {} - } - } - - Ok(oconvflags) -} - -/// Parse IFlags struct from CL-input -pub fn parse_iflags(matches: &Matches) -> Result { - let mut iflags = IFlags::default(); - - let flags = parse_flag_list(options::IFLAG, matches)?; - - for flag in flags { - match flag { - Flag::Cio => iflags.cio = true, - Flag::Direct => iflags.direct = true, - Flag::Directory => iflags.directory = true, - Flag::Dsync => iflags.dsync = true, - Flag::Sync => iflags.sync = true, - Flag::NoCache => iflags.nocache = true, - Flag::NonBlock => iflags.nonblock = true, - Flag::NoATime => iflags.noatime = true, - Flag::NoCtty => iflags.noctty = true, - Flag::NoFollow => iflags.nofollow = true, - Flag::NoLinks => iflags.nolinks = true, - Flag::Binary => iflags.binary = true, - Flag::Text => iflags.text = true, - Flag::FullBlock => iflags.fullblock = true, - Flag::CountBytes => iflags.count_bytes = true, - Flag::SkipBytes => iflags.skip_bytes = true, - _ => {} - } - } - - Ok(iflags) -} - -/// Parse OFlags struct from CL-input -pub fn parse_oflags(matches: &Matches) -> Result { - let mut oflags = OFlags::default(); - - let flags = parse_flag_list(options::OFLAG, matches)?; - - for flag in flags { - match flag { - Flag::Append => oflags.append = true, - Flag::Cio => oflags.cio = true, - Flag::Direct => oflags.direct = true, - Flag::Directory => oflags.directory = true, - Flag::Dsync => oflags.dsync = true, - Flag::Sync => oflags.sync = true, - Flag::NoCache => oflags.nocache = true, - Flag::NonBlock => oflags.nonblock = true, - Flag::NoATime => oflags.noatime = true, - Flag::NoCtty => oflags.noctty = true, - Flag::NoFollow => oflags.nofollow = true, - Flag::NoLinks => oflags.nolinks = true, - Flag::Binary => oflags.binary = true, - Flag::Text => oflags.text = true, - Flag::SeekBytes => oflags.seek_bytes = true, - _ => {} - } - } - - Ok(oflags) -} - -pub fn parse_seek_skip_amt( - ibs: &usize, - bytes: bool, - matches: &Matches, - option: &str, -) -> Result, ParseError> { - if let Some(amt) = matches.value_of(option) { - let n = parse_bytes_with_opt_multiplier(amt)?; - if bytes { - Ok(Some(n)) - } else { - Ok(Some(*ibs as u64 * n)) - } - } else { - Ok(None) - } -} - -/// Parse the value of count=N and the type of N implied by iflags -pub fn parse_count(iflags: &IFlags, matches: &Matches) -> Result, ParseError> { - if let Some(amt) = matches.value_of(options::COUNT) { - let n = parse_bytes_with_opt_multiplier(amt)?; - if iflags.count_bytes { - Ok(Some(CountType::Bytes(n))) - } else { - Ok(Some(CountType::Reads(n))) - } - } else { - Ok(None) - } -} - -/// Parse whether the args indicate the input is not ascii -pub fn parse_input_non_ascii(matches: &Matches) -> Result { - if let Some(conv_opts) = matches.value_of(options::CONV) { - Ok(conv_opts.contains("ascii")) - } else { - Ok(false) + (None, Some(Block::Block(cbs))) => Some(ConversionMode::BlockOnly(cbs, is_sync)), + (None, Some(Block::Unblock(cbs))) => Some(ConversionMode::UnblockOnly(cbs)), + (None, None) => None, } } diff --git a/src/uu/dd/src/parseargs/unit_tests.rs b/src/uu/dd/src/parseargs/unit_tests.rs index 9d834987376..18eeb33a2b5 100644 --- a/src/uu/dd/src/parseargs/unit_tests.rs +++ b/src/uu/dd/src/parseargs/unit_tests.rs @@ -1,7 +1,11 @@ -// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat +// spell-checker:ignore fname, tname, fpath, specfile, testfile, unspec, ifile, ofile, outfile, fullblock, urand, fileio, atoe, atoibm, behaviour, bmax, bremain, btotal, cflags, creat, ctable, ctty, datastructures, doesnt, etoa, fileout, fname, gnudd, iconvflags, iseek, nocache, noctty, noerror, nofollow, nolinks, nonblock, oconvflags, oseek, outfile, parseargs, rlen, rmax, rposition, rremain, rsofar, rstat, sigusr, sigval, wlen, wstat, oconv use super::*; +use crate::conversion_tables::{ + ASCII_TO_EBCDIC_UCASE_TO_LCASE, ASCII_TO_IBM, EBCDIC_TO_ASCII_LCASE_TO_UCASE, +}; +use crate::parseargs::Parser; use crate::StatusLevel; #[cfg(not(any(target_os = "linux", target_os = "android")))] @@ -20,18 +24,22 @@ fn unimplemented_flags_should_error_non_linux() { "noctty", "nofollow", ] { - let args = vec![ - String::from("dd"), - format!("--iflag={}", flag), - format!("--oflag={}", flag), - ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - if parse_iflags(&matches).is_ok() { + let args = vec![format!("iflag={}", flag)]; + + if Parser::new() + .parse(&args.iter().map(AsRef::as_ref).collect::>()[..]) + .is_ok() + { succeeded.push(format!("iflag={}", flag)); } - if parse_oflags(&matches).is_ok() { - succeeded.push(format!("oflag={}", flag)); + + let args = vec![format!("oflag={}", flag)]; + + if Parser::new() + .parse(&args.iter().map(AsRef::as_ref).collect::>()[..]) + .is_ok() + { + succeeded.push(format!("iflag={}", flag)); } } @@ -48,18 +56,22 @@ fn unimplemented_flags_should_error() { // The following flags are not implemented for flag in ["cio", "nocache", "nolinks", "text", "binary"] { - let args = vec![ - String::from("dd"), - format!("--iflag={}", flag), - format!("--oflag={}", flag), - ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - if parse_iflags(&matches).is_ok() { + let args = vec![format!("iflag={}", flag)]; + + if Parser::new() + .parse(&args.iter().map(AsRef::as_ref).collect::>()[..]) + .is_ok() + { succeeded.push(format!("iflag={}", flag)); } - if parse_oflags(&matches).is_ok() { - succeeded.push(format!("oflag={}", flag)); + + let args = vec![format!("oflag={}", flag)]; + + if Parser::new() + .parse(&args.iter().map(AsRef::as_ref).collect::>()[..]) + .is_ok() + { + succeeded.push(format!("iflag={}", flag)); } } @@ -72,205 +84,62 @@ fn unimplemented_flags_should_error() { #[test] fn test_status_level_absent() { - let args = vec![ - String::from("dd"), - String::from("--if=foo.file"), - String::from("--of=bar.file"), - ]; + let args = &["if=foo.file", "of=bar.file"]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - let st = parse_status_level(&matches).unwrap(); - - assert_eq!(st, None); + assert_eq!(Parser::new().parse(args).unwrap().status, None) } #[test] fn test_status_level_none() { - let args = vec![ - String::from("dd"), - String::from("--status=none"), - String::from("--if=foo.file"), - String::from("--of=bar.file"), - ]; - - let matches = uu_app().try_get_matches_from(args).unwrap(); - let st = parse_status_level(&matches).unwrap().unwrap(); + let args = &["status=none", "if=foo.file", "of=bar.file"]; - assert_eq!(st, StatusLevel::None); + assert_eq!( + Parser::new().parse(args).unwrap().status, + Some(StatusLevel::None) + ) } #[test] fn test_all_top_level_args_no_leading_dashes() { - let args = vec![ - String::from("dd"), - String::from("if=foo.file"), - String::from("of=bar.file"), - String::from("ibs=10"), - String::from("obs=10"), - String::from("cbs=1"), - String::from("bs=100"), - String::from("count=2"), - String::from("skip=2"), - String::from("seek=2"), - String::from("iseek=2"), - String::from("oseek=2"), - String::from("status=progress"), - String::from("conv=ascii,ucase"), - String::from("iflag=count_bytes,skip_bytes"), - String::from("oflag=append,seek_bytes"), + let args = &[ + "if=foo.file", + "of=bar.file", + "ibs=10", + "obs=10", + "cbs=1", + "bs=100", + "count=2", + "skip=2", + "seek=2", + "iseek=2", + "oseek=2", + "status=progress", + "conv=ascii,ucase", + "iflag=count_bytes,skip_bytes", + "oflag=append,seek_bytes", ]; - let args = args - .into_iter() - .fold(Vec::new(), append_dashes_if_not_present); - let matches = uu_app().try_get_matches_from(args).unwrap(); + let settings = Parser::new().parse(args).unwrap(); - assert_eq!(100, parse_ibs(&matches).unwrap()); - assert_eq!(100, parse_obs(&matches).unwrap()); - assert_eq!(1, parse_cbs(&matches).unwrap().unwrap()); - assert_eq!( - CountType::Bytes(2), - parse_count( - &IFlags { - count_bytes: true, - ..IFlags::default() - }, - &matches - ) - .unwrap() - .unwrap() - ); - assert_eq!( - 200, - parse_seek_skip_amt(&100, IFlags::default().skip_bytes, &matches, options::SKIP) - .unwrap() - .unwrap() - ); - assert_eq!( - 200, - parse_seek_skip_amt(&100, OFlags::default().seek_bytes, &matches, options::SEEK) - .unwrap() - .unwrap() - ); - assert_eq!( - 200, - parse_seek_skip_amt(&100, IFlags::default().skip_bytes, &matches, options::ISEEK) - .unwrap() - .unwrap() - ); - assert_eq!( - 200, - parse_seek_skip_amt(&100, OFlags::default().seek_bytes, &matches, options::OSEEK) - .unwrap() - .unwrap() - ); - assert_eq!( - StatusLevel::Progress, - parse_status_level(&matches).unwrap().unwrap() - ); - assert_eq!( - IConvFlags { - // ascii implies unblock - mode: Some(ConversionMode::ConvertThenUnblock( - &EBCDIC_TO_ASCII_LCASE_TO_UCASE, - 1 - )), - ..IConvFlags::default() - }, - parse_conv_flag_input(&matches).unwrap() - ); - assert_eq!( - OConvFlags::default(), - parse_conv_flag_output(&matches).unwrap() - ); - assert_eq!( - IFlags { - count_bytes: true, - skip_bytes: true, - ..IFlags::default() - }, - parse_iflags(&matches).unwrap() - ); - assert_eq!( - OFlags { - append: true, - seek_bytes: true, - ..OFlags::default() - }, - parse_oflags(&matches).unwrap() - ); -} + // ibs=10 and obs=10 are overwritten by bs=100 + assert_eq!(settings.ibs, 100); + assert_eq!(settings.obs, 100); -#[test] -fn test_all_top_level_args_with_leading_dashes() { - let args = vec![ - String::from("dd"), - String::from("--if=foo.file"), - String::from("--of=bar.file"), - String::from("--ibs=10"), - String::from("--obs=10"), - String::from("--cbs=1"), - String::from("--bs=100"), - String::from("--count=2"), - String::from("--skip=2"), - String::from("--seek=2"), - String::from("--iseek=2"), - String::from("--oseek=2"), - String::from("--status=progress"), - String::from("--conv=ascii,ucase"), - String::from("--iflag=count_bytes,skip_bytes"), - String::from("--oflag=append,seek_bytes"), - ]; - let args = args - .into_iter() - .fold(Vec::new(), append_dashes_if_not_present); + // count=2 iflag=count_bytes + assert_eq!(settings.count, Some(Num::Bytes(2))); - let matches = uu_app().try_get_matches_from(args).unwrap(); + // seek=2 oflag=seek_bytes + assert_eq!(settings.seek, 2); - assert_eq!(100, parse_ibs(&matches).unwrap()); - assert_eq!(100, parse_obs(&matches).unwrap()); - assert_eq!(1, parse_cbs(&matches).unwrap().unwrap()); - assert_eq!( - CountType::Bytes(2), - parse_count( - &IFlags { - count_bytes: true, - ..IFlags::default() - }, - &matches - ) - .unwrap() - .unwrap() - ); - assert_eq!( - 200, - parse_seek_skip_amt(&100, IFlags::default().skip_bytes, &matches, options::SKIP) - .unwrap() - .unwrap() - ); - assert_eq!( - 200, - parse_seek_skip_amt(&100, OFlags::default().seek_bytes, &matches, options::SEEK) - .unwrap() - .unwrap() - ); - assert_eq!( - 200, - parse_seek_skip_amt(&100, IFlags::default().skip_bytes, &matches, options::ISEEK) - .unwrap() - .unwrap() - ); - assert_eq!( - 200, - parse_seek_skip_amt(&100, OFlags::default().seek_bytes, &matches, options::OSEEK) - .unwrap() - .unwrap() - ); - assert_eq!( - StatusLevel::Progress, - parse_status_level(&matches).unwrap().unwrap() - ); + // skip=2 iflag=skip_bytes + assert_eq!(settings.skip, 2); + + // status=progress + assert_eq!(settings.status, Some(StatusLevel::Progress)); + + // conv=ascii,ucase assert_eq!( + settings.iconv, IConvFlags { // ascii implies unblock mode: Some(ConversionMode::ConvertThenUnblock( @@ -279,398 +148,294 @@ fn test_all_top_level_args_with_leading_dashes() { )), ..IConvFlags::default() }, - parse_conv_flag_input(&matches).unwrap() - ); - assert_eq!( - OConvFlags::default(), - parse_conv_flag_output(&matches).unwrap() ); + + // no conv flags apply to output + assert_eq!(settings.oconv, OConvFlags::default(),); + + // iconv=count_bytes,skip_bytes assert_eq!( + settings.iflags, IFlags { count_bytes: true, skip_bytes: true, ..IFlags::default() }, - parse_iflags(&matches).unwrap() ); + + // oconv=append,seek_bytes assert_eq!( + settings.oflags, OFlags { append: true, seek_bytes: true, ..OFlags::default() }, - parse_oflags(&matches).unwrap() ); } #[test] fn test_status_level_progress() { - let args = vec![ - String::from("dd"), - String::from("--if=foo.file"), - String::from("--of=bar.file"), - String::from("--status=progress"), - ]; + let args = &["if=foo.file", "of=bar.file", "status=progress"]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - let st = parse_status_level(&matches).unwrap().unwrap(); + let settings = Parser::new().parse(args).unwrap(); - assert_eq!(st, StatusLevel::Progress); + assert_eq!(settings.status, Some(StatusLevel::Progress)); } #[test] fn test_status_level_noxfer() { - let args = vec![ - String::from("dd"), - String::from("--if=foo.file"), - String::from("--status=noxfer"), - String::from("--of=bar.file"), - ]; + let args = &["if=foo.file", "status=noxfer", "of=bar.file"]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - let st = parse_status_level(&matches).unwrap().unwrap(); + let settings = Parser::new().parse(args).unwrap(); - assert_eq!(st, StatusLevel::Noxfer); + assert_eq!(settings.status, Some(StatusLevel::Noxfer)); } #[test] fn test_multiple_flags_options() { - let args = vec![ - String::from("dd"), - String::from("--iflag=fullblock,count_bytes"), - String::from("--iflag=skip_bytes"), - String::from("--oflag=append"), - String::from("--oflag=seek_bytes"), - String::from("--conv=ascii,ucase"), - String::from("--conv=unblock"), + let args = &[ + "iflag=fullblock,count_bytes", + "iflag=skip_bytes", + "oflag=append", + "oflag=seek_bytes", + "conv=ascii,ucase", + "conv=unblock", + "cbs=512", ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); + let settings = Parser::new().parse(args).unwrap(); // iflag - let iflags = parse_flag_list::(options::IFLAG, &matches).unwrap(); assert_eq!( - vec![Flag::FullBlock, Flag::CountBytes, Flag::SkipBytes], - iflags + settings.iflags, + IFlags { + fullblock: true, + count_bytes: true, + skip_bytes: true, + ..Default::default() + } ); // oflag - let oflags = parse_flag_list::(options::OFLAG, &matches).unwrap(); - assert_eq!(vec![Flag::Append, Flag::SeekBytes], oflags); + assert_eq!( + settings.oflags, + OFlags { + append: true, + seek_bytes: true, + ..Default::default() + } + ); // conv - let conv = parse_flag_list::(options::CONV, &matches).unwrap(); assert_eq!( - vec![ConvFlag::FmtEtoA, ConvFlag::UCase, ConvFlag::Unblock], - conv + settings.iconv, + IConvFlags { + mode: Some(ConversionMode::ConvertThenUnblock( + &EBCDIC_TO_ASCII_LCASE_TO_UCASE, + 512 + )), + ..Default::default() + } ); } #[test] fn test_override_multiple_options() { - let args = vec![ - String::from("dd"), - String::from("--if=foo.file"), - String::from("--if=correct.file"), - String::from("--of=bar.file"), - String::from("--of=correct.file"), - String::from("--ibs=256"), - String::from("--ibs=1024"), - String::from("--obs=256"), - String::from("--obs=1024"), - String::from("--cbs=1"), - String::from("--cbs=2"), - String::from("--skip=0"), - String::from("--skip=2"), - String::from("--seek=0"), - String::from("--seek=2"), - String::from("--iseek=0"), - String::from("--iseek=2"), - String::from("--oseek=0"), - String::from("--oseek=2"), - String::from("--status=none"), - String::from("--status=noxfer"), - String::from("--count=512"), - String::from("--count=1024"), + let args = &[ + "if=foo.file", + "if=correct.file", + "of=bar.file", + "of=correct.file", + "ibs=256", + "ibs=1024", + "obs=256", + "obs=1024", + "cbs=1", + "cbs=2", + "skip=0", + "skip=2", + "seek=0", + "seek=2", + "iseek=0", + "iseek=2", + "oseek=0", + "oseek=2", + "status=none", + "status=noxfer", + "count=512", + "count=1024", + "iflag=count_bytes", ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - // if - assert_eq!("correct.file", matches.value_of(options::INFILE).unwrap()); - - // of - assert_eq!("correct.file", matches.value_of(options::OUTFILE).unwrap()); - - // ibs - assert_eq!(1024, parse_ibs(&matches).unwrap()); + let settings = Parser::new().parse(args).unwrap(); - // obs - assert_eq!(1024, parse_obs(&matches).unwrap()); - - // cbs - assert_eq!(2, parse_cbs(&matches).unwrap().unwrap()); - - // status - assert_eq!( - StatusLevel::Noxfer, - parse_status_level(&matches).unwrap().unwrap() - ); - - // skip - assert_eq!( - 200, - parse_seek_skip_amt(&100, IFlags::default().skip_bytes, &matches, options::SKIP) - .unwrap() - .unwrap() - ); - - // seek - assert_eq!( - 200, - parse_seek_skip_amt(&100, OFlags::default().seek_bytes, &matches, options::SEEK) - .unwrap() - .unwrap() - ); - - // iseek - assert_eq!( - 200, - parse_seek_skip_amt(&100, IFlags::default().skip_bytes, &matches, options::ISEEK) - .unwrap() - .unwrap() - ); - - // oseek - assert_eq!( - 200, - parse_seek_skip_amt(&100, OFlags::default().seek_bytes, &matches, options::OSEEK) - .unwrap() - .unwrap() - ); - - // count - assert_eq!( - CountType::Bytes(1024), - parse_count( - &IFlags { - count_bytes: true, - ..IFlags::default() - }, - &matches - ) - .unwrap() - .unwrap() - ); + assert_eq!(settings.infile, Some("correct.file".into())); + assert_eq!(settings.outfile, Some("correct.file".into())); + assert_eq!(settings.ibs, 1024); + assert_eq!(settings.obs, 1024); + assert_eq!(settings.status, Some(StatusLevel::Noxfer)); + assert_eq!(settings.skip, 2048); + assert_eq!(settings.seek, 2048); + assert_eq!(settings.count, Some(Num::Bytes(1024))); } -// ----- IConvFlags/Output ----- +// // ----- IConvFlags/Output ----- #[test] -#[should_panic] fn icf_ctable_error() { - let args = vec![String::from("dd"), String::from("--conv=ascii,ebcdic,ibm")]; - - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let _ = parse_conv_flag_input(&matches).unwrap(); + let args = &["conv=ascii,ebcdic,ibm"]; + assert!(Parser::new().parse(args).is_err()); } #[test] -#[should_panic] fn icf_case_error() { - let args = vec![String::from("dd"), String::from("--conv=ucase,lcase")]; - - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let _ = parse_conv_flag_input(&matches).unwrap(); + let args = &["conv=ucase,lcase"]; + assert!(Parser::new().parse(args).is_err()); } #[test] -#[should_panic] fn icf_block_error() { - let args = vec![String::from("dd"), String::from("--conv=block,unblock")]; - - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let _ = parse_conv_flag_input(&matches).unwrap(); + let args = &["conv=block,unblock"]; + assert!(Parser::new().parse(args).is_err()); } #[test] -#[should_panic] fn icf_creat_error() { - let args = vec![String::from("dd"), String::from("--conv=excl,nocreat")]; - - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let _ = parse_conv_flag_output(&matches).unwrap(); + let args = &["conv=excl,nocreat"]; + assert!(Parser::new().parse(args).is_err()); } #[test] fn parse_icf_token_ibm() { - let exp = vec![ConvFlag::FmtAtoI]; + let args = &["conv=ibm"]; + let settings = Parser::new().parse(args).unwrap(); - let args = vec![String::from("dd"), String::from("--conv=ibm")]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let act = parse_flag_list::("conv", &matches).unwrap(); - - assert_eq!(exp.len(), act.len()); - for cf in &exp { - assert!(exp.contains(cf)); - } + assert_eq!( + settings.iconv, + IConvFlags { + mode: Some(ConversionMode::ConvertOnly(&ASCII_TO_IBM)), + ..Default::default() + } + ); } #[test] fn parse_icf_tokens_elu() { - let exp = vec![ConvFlag::FmtEtoA, ConvFlag::LCase, ConvFlag::Unblock]; - - let args = vec![ - String::from("dd"), - String::from("--conv=ebcdic,lcase,unblock"), - ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - let act = parse_flag_list::("conv", &matches).unwrap(); + let args = &["conv=ebcdic,lcase"]; + let settings = Parser::new().parse(args).unwrap(); - assert_eq!(exp.len(), act.len()); - for cf in &exp { - assert!(exp.contains(cf)); - } + assert_eq!( + settings.iconv, + IConvFlags { + mode: Some(ConversionMode::ConvertOnly(&ASCII_TO_EBCDIC_UCASE_TO_LCASE)), + ..Default::default() + } + ); } #[test] fn parse_icf_tokens_remaining() { - let exp = vec![ - ConvFlag::FmtAtoE, - ConvFlag::UCase, - ConvFlag::Block, - ConvFlag::Sparse, - ConvFlag::Swab, - ConvFlag::Sync, - ConvFlag::NoError, - ConvFlag::Excl, - ConvFlag::NoCreat, - ConvFlag::NoTrunc, - ConvFlag::NoError, - ConvFlag::FDataSync, - ConvFlag::FSync, - ]; - - let args = vec![ - String::from("dd"), - String::from("--conv=ascii,ucase,block,sparse,swab,sync,noerror,excl,nocreat,notrunc,noerror,fdatasync,fsync"), - ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let act = parse_flag_list::("conv", &matches).unwrap(); - - assert_eq!(exp.len(), act.len()); - for cf in &exp { - assert!(exp.contains(cf)); - } + let args = &["conv=ascii,ucase,block,sparse,swab,sync,noerror,excl,nocreat,notrunc,noerror,fdatasync,fsync"]; + assert_eq!( + Parser::new().read(args), + Ok(Parser { + conv: ConvFlags { + ascii: true, + ucase: true, + block: true, + sparse: true, + swab: true, + sync: true, + noerror: true, + excl: true, + nocreat: true, + notrunc: true, + fdatasync: true, + fsync: true, + ..Default::default() + }, + ..Default::default() + }) + ); } #[test] fn parse_iflag_tokens() { - let exp = vec![ - Flag::FullBlock, - Flag::CountBytes, - Flag::SkipBytes, - Flag::Append, - Flag::SeekBytes, - ]; - - let args = vec![ - String::from("dd"), - String::from("--iflag=fullblock,count_bytes,skip_bytes,append,seek_bytes"), - ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let act = parse_flag_list::("iflag", &matches).unwrap(); - - assert_eq!(exp.len(), act.len()); - for cf in &exp { - assert!(exp.contains(cf)); - } + let args = &["iflag=fullblock,count_bytes,skip_bytes"]; + assert_eq!( + Parser::new().read(args), + Ok(Parser { + iflag: IFlags { + fullblock: true, + count_bytes: true, + skip_bytes: true, + ..Default::default() + }, + ..Default::default() + }) + ); } #[test] fn parse_oflag_tokens() { - let exp = vec![ - Flag::FullBlock, - Flag::CountBytes, - Flag::SkipBytes, - Flag::Append, - Flag::SeekBytes, - ]; - - let args = vec![ - String::from("dd"), - String::from("--oflag=fullblock,count_bytes,skip_bytes,append,seek_bytes"), - ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let act = parse_flag_list::("oflag", &matches).unwrap(); - - assert_eq!(exp.len(), act.len()); - for cf in &exp { - assert!(exp.contains(cf)); - } + let args = &["oflag=append,seek_bytes"]; + assert_eq!( + Parser::new().read(args), + Ok(Parser { + oflag: OFlags { + append: true, + seek_bytes: true, + ..Default::default() + }, + ..Default::default() + }) + ); } #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn parse_iflag_tokens_linux() { - let exp = vec![ - Flag::Direct, - Flag::Directory, - Flag::Dsync, - Flag::Sync, - Flag::NonBlock, - Flag::NoATime, - Flag::NoCtty, - Flag::NoFollow, - ]; - - let args = vec![ - String::from("dd"), - String::from("--iflag=direct,directory,dsync,sync,nonblock,noatime,noctty,nofollow"), - ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let act = parse_flag_list::("iflag", &matches).unwrap(); - - assert_eq!(exp.len(), act.len()); - for cf in &exp { - assert!(exp.contains(cf)); - } + let args = &["iflag=direct,directory,dsync,sync,nonblock,noatime,noctty,nofollow"]; + assert_eq!( + Parser::new().read(args), + Ok(Parser { + iflag: IFlags { + direct: true, + directory: true, + dsync: true, + sync: true, + nonblock: true, + noatime: true, + noctty: true, + nofollow: true, + ..Default::default() + }, + ..Default::default() + }) + ); } #[cfg(any(target_os = "linux", target_os = "android"))] #[test] fn parse_oflag_tokens_linux() { - let exp = vec![ - Flag::Direct, - Flag::Directory, - Flag::Dsync, - Flag::Sync, - Flag::NonBlock, - Flag::NoATime, - Flag::NoCtty, - Flag::NoFollow, - ]; - - let args = vec![ - String::from("dd"), - String::from("--oflag=direct,directory,dsync,sync,nonblock,noatime,noctty,nofollow"), - ]; - let matches = uu_app().try_get_matches_from(args).unwrap(); - - let act = parse_flag_list::("oflag", &matches).unwrap(); - - assert_eq!(exp.len(), act.len()); - for cf in &exp { - assert!(exp.contains(cf)); - } + let args = &["oflag=direct,directory,dsync,sync,nonblock,noatime,noctty,nofollow"]; + assert_eq!( + Parser::new().read(args), + Ok(Parser { + oflag: OFlags { + direct: true, + directory: true, + dsync: true, + sync: true, + nonblock: true, + noatime: true, + noctty: true, + nofollow: true, + ..Default::default() + }, + ..Default::default() + }) + ); } // ----- Multiplier Strings etc. ----- diff --git a/tests/by-util/test_dd.rs b/tests/by-util/test_dd.rs index 89699426c6e..69a94144348 100644 --- a/tests/by-util/test_dd.rs +++ b/tests/by-util/test_dd.rs @@ -1165,12 +1165,12 @@ fn test_bytes_iseek_bytes_iflag() { } #[test] -fn test_bytes_iseek_skip_additive() { +fn test_bytes_iseek_skip_not_additive() { new_ucmd!() - .args(&["iseek=5", "skip=5", "iflag=skip_bytes", "bs=2"]) + .args(&["iseek=4", "skip=4", "iflag=skip_bytes", "bs=2"]) .pipe_in("0123456789abcdefghijklm") .succeeds() - .stdout_is("abcdefghijklm"); + .stdout_is("456789abcdefghijklm"); } #[test] @@ -1193,9 +1193,9 @@ fn test_bytes_oseek_bytes_trunc_oflag() { } #[test] -fn test_bytes_oseek_seek_additive() { +fn test_bytes_oseek_seek_not_additive() { new_ucmd!() - .args(&["oseek=4", "seek=4", "oflag=seek_bytes", "bs=2"]) + .args(&["oseek=8", "seek=8", "oflag=seek_bytes", "bs=2"]) .pipe_in("abcdefghijklm") .succeeds() .stdout_is_fixture_bytes("dd-bytes-alphabet-null.spec"); @@ -1278,3 +1278,20 @@ fn test_invalid_file_arg_gnu_compatibility() { .pipe_in("") .succeeds(); } + +#[test] +fn test_ucase_lcase() { + new_ucmd!() + .arg("conv=ucase,lcase") + .fails() + .stderr_contains("lcase") + .stderr_contains("ucase"); +} + +#[test] +fn test_big_multiplication() { + new_ucmd!() + .arg("ibs=10x10x10x10x10x10x10x10x10x10x10x10x10x10x10x10x10x10x10x10x10x10x10") + .fails() + .stderr_contains("invalid number"); +} diff --git a/tests/by-util/test_ls.rs b/tests/by-util/test_ls.rs index 2b11961b811..79f3b07afae 100644 --- a/tests/by-util/test_ls.rs +++ b/tests/by-util/test_ls.rs @@ -90,16 +90,16 @@ fn test_ls_allocation_size() { // fill empty file with zeros scene .ccmd("dd") - .arg("--if=/dev/zero") - .arg("--of=some-dir1/zero-file") + .arg("if=/dev/zero") + .arg("of=some-dir1/zero-file") .arg("bs=1024") .arg("count=4096") .succeeds(); scene .ccmd("dd") - .arg("--if=/dev/zero") - .arg("--of=irregular-file") + .arg("if=/dev/zero") + .arg("of=irregular-file") .arg("bs=1") .arg("count=777") .succeeds();