dd-lib 0.2.1

library functions for a clone of the unix coreutil dd
Documentation
//! Configuration and parsing.
//!
//! - bs=n. Set both input and output block size to n bytes, superseding the
//! ibs and obs operands.  If no conversion values other than noerror, notrunc
//! or sync are specified, then each input block is copied to the output as a
//! single block without any aggregation of short blocks.
//!
//! - cbs=n Set the conversion record size to n bytes.  The conversion record
//!   size is required by the record oriented conversion values.
//!
//! - conv=value\[,value ...\] Set conversion flags according to the
//!   comma-separated symbol lsit.
//!  See [flags/cflag]
//!
//! - files=n:   <UNIMPLEMENTED> Copy n input files before terminating.  This
//!   operand is only   applicable when the input device is a tape.
//!
//! - ibs=n:   Set the input block size to n bytes instead of the default 512.
//!
//! - if=file:   Read input from file instead of the standard input.
//!
//! - iflags=value\[,value ...\] [opts::IFlag] Set input flags according to the
//!   comma-separated symbol list. See [flags/iflag]
//!
//!
//! - iseek=n:  Seek on the input file n blocks.  This is synonymous with
//!   skip=n.
//!
//!
//! - obs=n: Set the output block size to n bytes instead of the default 512.
//!
//! - of=file Write output to file instead of the standard output.  Any regular
//!   output file is truncated unless the notrunc conversion value is specified.
//!   If
//! an initial portion of the output file is seeked past (see the oseek
//! operand), the output file is truncated at that point. - oseek=n   Seek on
//! the output file n blocks.  This is synonymous with seek=n.
//!
//! - oflags=value\[,value ...\]:  [flags::Out] Set output flags according to
//!   the   comma-separated symbol list. See [`flags/oflag`]
//!
//! - seek=n    Seek n blocks from the beginning of the output before copying.
//!   On non-tape
//! devices, an lseek(2) operation is used.  Otherwise, existing blocks are read
//! and the data discarded.  If the user does not have read permission for the
//! tape, it is positioned using the tape ioctl(2) function calls. If the seek
//! operation is past the end of file, space from the current end of file to the
//! specified offset is filled with blocks of NUL bytes.
//!
//! - skip=n    Skip n blocks from the beginning of the input before copying. If
//!   the input
//! doesn't support seeks (i.e, is [Stdin][std::io::stdin])  Otherwise, input
//! data is read and discarded.  For pipes, the correct number of bytes is read.
//! For all other devices, the correct number of blocks is read without
//! distinguishing between a partial or complete block being read.
//! error types and handling for command line options
//!
//! - status=[LEVEL][StatusLevel] The LEVEL of information to   print to stderr
//!     - none suppresses everything but error messages
//!     - noxfer suppresses the final transfer statistics,
//!     - progress shows periodic transfer statistics


pub use self::flags::cflag::CFlag;

/// Input (read) flags, specified by the `iflag` option
pub use self::flags::iflag::IFlag;

/// Output (write) flags, specified by the `oflag` option
pub use self::flags::oflag::OFlag;

/// errors dealing with handling user options
pub use self::error::{Error, Result};

pub use self::number::parse;
// parsing of numbers
mod number;

mod error;
/// parsing of input, output, and conversion flags
mod flags;
/// conversion of strings to unsized integers & unit conversions

/// input, output, and conversion flags

/// Conversion flags, specified by the `conv` option

#[derive(Clone, Debug, Default)]
/// The parsed and handled user options.
pub struct Opts {
    /// convert the file as per the comma separated symbol list
    pub cflags: CFlag,

    /// handle the input as per the comma-separated symbol list (see
    /// [`opts::IFlag`])
    pub iflags: IFlag,
    /// handle the output as per the comma-separated symbol list (see
    /// [`flags::Out`])
    pub oflags: OFlag,
    /// The main mode to run in. Can be `Mode::Standard, Mode::Block(usize),
    /// Mode::Unblock(usize)`
    pub mode: Mode,
    /// The limit in bytes or blocks to read
    pub count: Option<usize>,
    /// <NOT IMPLEMENTED> Copy n input files before terminating.  This operand
    /// is only applicable when the input device is a tape.
    pub files: Option<usize>,
    /// The block size of the input, in bytes. Default is 512.
    pub input_block_size: usize,
    /// The input file, if any. `input_file == None` -> stdin
    pub input_file: Option<String>,
    /// How many `input_block_sized` blocks to seek on the input. If also
    /// `opts::IFlag::SEEK_BYTES`, seek that many _bytes_ instead
    pub input_seek: Option<usize>,
    /// The block size of the output, in bytes. Default is 512.
    pub output_block_size: usize,
    /// The output file, if any. `output_file == None` -> stdout
    pub output_file: Option<String>,
    /// How many `output_block_sized` blocks  to seek on the output file.
    /// Incompatible with `output_file==None`
    pub output_seek: Option<usize>,

    pub status: StatusLevel,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// Enumeration of the four kinds of command line options: BasicOptions,
/// conversion flags, input flags, output flags
pub enum Kind {
    BasicOption,
    CFlag,
    IFlag,
    OFlag,
}

/// The LEVEL of information to print to stderr
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum StatusLevel {
    /// default: report only on function completion
    TransferOnly,
    /// suppress everything but error messages
    SuppressAll,
    /// suppress the final transfer statistics
    NoTransfer,
    /// report periodic transfer statistics
    Progress,
}
/// Helper trait for reporting errors
#[doc(hidden)]
pub trait Unimplemented {
    /// Kind of option; used for reporting errors
    const KIND: Kind;
    /// Slice of valid but as yet unimplemented options
    const UNIMPLEMENTED: &'static [&'static str];

    /// check if a flag is valid but unimplemented
    fn check_if_implemented(flag: &str) -> Result<()> {
        for s in Self::UNIMPLEMENTED {
            if flag == *s {
                return Err(Error::Unimplemented(Self::KIND, *s));
            }
        }
        Ok(())
    }
    /// helper function; an invalid flag key
    fn unknown<T>(flag: String) -> Result<T> { Err(Error::Unknown(Self::KIND, flag)) }
}

impl Unimplemented for Opts {
    const KIND: Kind = Kind::BasicOption;
    const UNIMPLEMENTED: &'static [&'static str] = &["count", "files"];
}

impl Default for StatusLevel {
    fn default() -> Self { StatusLevel::TransferOnly }
}

impl std::str::FromStr for StatusLevel {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        match s {
            "none" => Ok(StatusLevel::SuppressAll),
            "noxfer" => Ok(StatusLevel::NoTransfer),
            "progress" => Ok(StatusLevel::Progress),
            s => Err(Error::Unknown(Kind::BasicOption, format!("status={}", s))),
        }
    }
}

/// The Mode that DD runs in.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Mode {
    /// Byte for byte IO. This is the default.
    Standard,
    /// Fixed-sized records separated by newlines, padded or trunctated to
    /// `usize`. Specified by input option "block"
    Block(usize),
    /// Read fixed numbers of bytes and output separated by newlines, ignoring
    /// trailing spaces. Specified by input option "unblock"
    Unblock(usize),
}

impl Default for Mode {
    fn default() -> Self { Mode::Standard }
}

impl Mode {
    fn new(block: bool, unblock: bool, conversion_record_size: Option<usize>) -> Result<Self> {
        match (block, unblock, conversion_record_size) {
            (false, false, None) => Ok(Mode::Standard),
            (true, false, Some(n)) => Ok(Mode::Block(n)),
            (false, true, Some(n)) => Ok(Mode::Unblock(n)),
            (false, false, Some(_)) => Err(Error::ConflictingOption(
                "a conversion record size option is meaningless if 'block' or 'unblock' are not specified",
            )),
            (true, true, _) => Err(Error::ConflictingOption(
                "can't specify both 'block' and 'unblock' conversion options",
            )),

            (true, false, None) | (false, true, None) => Err(Error::ConflictingOption(
                "must specify a conversion record size if 'block' or 'unblock' is selected",
            )),
        }
    }
}

impl Opts {
    const DEFAULT_INPUT_BLOCKSIZE: usize = 512;
    const DEFAULT_OUTPUT_BLOCKSIZE: usize = 512;

    #[allow(unreachable_patterns)]
    pub fn new<I>(args: I) -> Result<Self>
    where
        I: IntoIterator<Item = String>,
    {
        let mut block_size = <Option<usize>>::default();
        let (mut count, mut files) = <(Option<usize>, Option<usize>)>::default(); // unimplemented!
        let mut cflags = CFlag::default();
        let mut conv_block_size = None;
        let (mut iflags, mut oflags) = (IFlag::default(), OFlag::default());
        let (mut in_block_size, mut out_block_size) = (Self::DEFAULT_INPUT_BLOCKSIZE, Self::DEFAULT_OUTPUT_BLOCKSIZE);
        let (mut input_file, mut output_file) = <(Option<String>, Option<String>)>::default();
        let (mut input_seek, mut output_seek) = <(Option<usize>, Option<usize>)>::default();
        let mut status = StatusLevel::default();
        for arg in args.into_iter().skip(1) {
            if let Some((key, val)) = keyval(&arg) {
                Self::check_if_implemented(key)?;
                match key {
                    "conv" => cflags = CFlag::new(val)?,
                    "iflag" => iflags = IFlag::new(val)?,
                    "oflag" => oflags = OFlag::new(val)?,
                    "bs" => block_size = number::parse_opt(val)?,
                    "cbs" => conv_block_size = number::parse_opt(val)?,
                    "count" => count = number::parse_opt(val)?, // not yet implemented
                    "files" => files = number::parse_opt(val)?, // not yet implemented
                    "ibs" => in_block_size = number::parse(val)?,
                    "if" => input_file = Some(val.to_string()),
                    "iseek" | "skip" => input_seek = number::parse_opt(val)?,
                    "obs" => out_block_size = val.parse()?,
                    "of" => output_file = Some(val.to_string()),
                    "os" | "seek" => output_seek = number::parse_opt(val)?,
                    "status" => status = val.parse()?,

                    _ => return Self::unknown(key.to_owned()),
                }
                continue;
            };

            return Self::unknown(arg);
        }

        validate_directory(&input_file, iflags.contains(IFlag::DIRECTORY))?;
        validate_directory(&output_file, oflags.contains(OFlag::DIRECTORY))?;
        Ok(Opts {
            mode: Mode::new(
                cflags.contains(CFlag::BLOCK),
                cflags.contains(CFlag::UNBLOCK),
                conv_block_size,
            )?,

            count,
            files,
            status,

            cflags,
            iflags,
            oflags,

            input_block_size: block_size.unwrap_or(in_block_size),
            output_block_size: block_size.unwrap_or(out_block_size),

            input_file,
            output_file,

            input_seek,
            output_seek,
        })
    }

    /// iflag returns true if the specified iflag is set
    /// ```
    /// # use dd_lib::opts;
    /// # use dd_lib::opts::{Opts, IFlag};
    /// let mut o = opts::Opts::default();
    /// o.iflags = IFlag::APPEND | IFlag::SYNC;
    /// assert!(o.iflag(IFlag::APPEND));
    /// assert!(!o.iflag(IFlag::NOFOLLOW));
    /// ```
    pub fn iflag(&self, iflag: IFlag) -> bool { self.iflags.contains(iflag) }

    /// oflag returns true if the specified conversion flag is set
    pub fn oflag(&self, oflag: OFlag) -> bool { self.oflags.contains(oflag) }

    /// cflag returns true if the specified conversion flag is set
    pub fn cflag(&self, cflag: CFlag) -> bool { self.cflags.contains(cflag) }
}

fn validate_directory(path: &Option<String>, dir_flag: bool) -> Result<()> {
    match path {
        Some(path) if !std::path::Path::is_dir(path.as_ref()) && dir_flag => Err(Error::NotDirectory(path.to_string())),
        _ => Ok(()),
    }
}

/// split a string of the form {key}={val} into a two-tuple, if possible.
/// if it would be larger than a two-tuple, returns None
/// ```
/// # use dd_lib::opts::keyval;
/// assert_eq!(Some(("foo", "bar")), keyval("foo=bar"));
/// assert!(keyval("asd").is_none());
/// assert!(keyval("hello=myasndlaksnd=name").is_none());
/// ```
pub fn keyval(s: &str) -> Option<(&str, &str)> {
    let mut split = s.split("=");
    if let (Some(key), Some(val), None) = (split.next(), split.next(), split.next()) {
        Some((key, val))
    } else {
        None
    }
}