dd-lib 0.1.0

library functions for a clone of the unix coreutil dd
Documentation
//! #### 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 ...]
//! See `flags/cflag`
//!
//! #### files=n  
//! 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.
//!
//! #### iflag=FLAGS
//! 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.
//!
//! #### oflag=FLAGS
//! write as per the comma separated symbol list
//!
//! #### 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.  On input
//! which supports seeks, an lseek(2) operation is used.  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
//! 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

/// errors dealing with handling user options
pub mod error;
/// parsing of input, output, and conversion flags
pub mod flags;
/// conversion of strings to unsized integers & unit conversions
pub mod number;

pub use self::error::{Error, Result, Unimplemented};
use std::str::FromStr;

#[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,
}

#[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: flags::Conv,

    /// handle the input as per the comma-separated symbol list (see
    /// `flags::In`)
    pub iflags: flags::In,
    /// handle the output as per the comma-separated symbol list (see
    /// `flags::Out`)
    pub oflags: flags::Out,
    /// 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
    /// `flags::In::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,
}

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

/// 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,
}

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

impl 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 conversions
    Standard,
    /// Fixed-sized records separated by newlines, padded or trunctated to
    /// `usize`
    Block(usize),
    /// Read fixed numbers of bytes and output separated by newlines, ignoring
    /// trailing spaces
    Unblock(usize),
}

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

impl Mode {
    fn new(cflags: flags::Conv, conversion_record_size: Option<usize>) -> Result<Self> {
        match (
            cflags.contains(flags::Conv::BLOCK),
            cflags.contains(flags::Conv::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 'u'nblock' 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 = flags::Conv::default();
        let mut conv_block_size = None;
        let (mut iflags, mut oflags) = (flags::In::default(), flags::Out::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 = flags::Conv::new(val)?,
                    "iflag" => iflags = flags::In::new(val)?,
                    "oflag" => oflags = flags::Out::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(flags::In::DIRECTORY))?;
        validate_directory(&output_file, oflags.contains(flags::Out::DIRECTORY))?;
        Ok(Opts {
            mode: Mode::new(cflags, 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,
        })
    }

    #[inline]
    /// whether a flag is set
    pub fn iflag(&self, flag: flags::In) -> bool { self.iflags.contains(flag) }

    #[inline]
    // whether an output flag is set
    pub fn oflag(&self, flag: flags::Out) -> bool { self.oflags.contains(flag) }

    #[inline]
    // whether a conversion flag is set
    pub fn cflag(&self, flag: flags::Conv) -> bool { self.cflags.contains(flag) }
}

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.
fn keyval(s: &String) -> Option<(&str, &str)> {
    let mut split = s.split("=");
    if let (Some(key), Some(val)) = (split.next(), split.next()) {
        Some((key, val))
    } else {
        None
    }
}