#[cfg(test)]
mod unit_tests;
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, Parser as SizeParser};
use uucore::show_warning;
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
UnrecognizedOperand(String),
MultipleFmtTable,
MultipleUCaseLCase,
MultipleBlockUnblock,
MultipleExclNoCreate,
FlagNoMatch(String),
ConvFlagNoMatch(String),
MultiplierStringParseFailure(String),
MultiplierStringOverflow(String),
BlockUnblockWithoutCBS,
StatusLevelNotRecognized(String),
Unimplemented(String),
BsOutOfRange(String),
InvalidNumber(String),
}
#[derive(Debug, PartialEq)]
pub struct Parser {
infile: Option<String>,
outfile: Option<String>,
ibs: usize,
obs: usize,
cbs: Option<usize>,
skip: Num,
seek: Num,
count: Option<Num>,
conv: ConvFlags,
iflag: IFlags,
oflag: OFlags,
status: Option<StatusLevel>,
}
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),
}
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<Settings, ParseError> {
self.read(operands)?.validate()
}
pub(crate) fn read(mut self, operands: &[&str]) -> Result<Self, ParseError> {
for operand in operands {
self.parse_operand(operand)?;
}
Ok(self)
}
pub(crate) fn validate(self) -> Result<Settings, ParseError> {
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);
}
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<Num, ParseError> {
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<usize, ParseError> {
parse_bytes_with_opt_multiplier(val)?
.try_into()
.map_err(|_| ParseError::BsOutOfRange(arg.to_string()))
}
fn parse_status_level(&self, val: &str) -> Result<StatusLevel, ParseError> {
match val {
"none" => Ok(StatusLevel::None),
"noxfer" => Ok(StatusLevel::Noxfer),
"progress" => Ok(StatusLevel::Progress),
_ => Err(ParseError::StatusLevelNotRecognized(val.to_string())),
}
}
#[allow(clippy::cognitive_complexity)]
fn parse_input_flags(&mut self, val: &str) -> Result<(), ParseError> {
let i = &mut self.iflag;
for f in val.split(',') {
match f {
"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" => linux_only!(f, i.nocache = true),
"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())),
"fullblock" => i.fullblock = true,
"count_bytes" => i.count_bytes = true,
"skip_bytes" => i.skip_bytes = true,
"append" | "seek_bytes" => {}
_ => return Err(ParseError::FlagNoMatch(f.to_string())),
}
}
Ok(())
}
#[allow(clippy::cognitive_complexity)]
fn parse_output_flags(&mut self, val: &str) -> Result<(), ParseError> {
let o = &mut self.oflag;
for f in val.split(',') {
match f {
"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" => linux_only!(f, o.nocache = true),
"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())),
"append" => o.append = true,
"seek_bytes" => o.seek_bytes = true,
"fullblock" | "count_bytes" | "skip_bytes" => {}
_ => return Err(ParseError::FlagNoMatch(f.to_string())),
}
}
Ok(())
}
fn parse_conv_flags(&mut self, val: &str) -> Result<(), ParseError> {
let c = &mut self.conv;
for f in val.split(',') {
match f {
"ascii" => c.ascii = true,
"ebcdic" => c.ebcdic = true,
"ibm" => c.ibm = true,
"lcase" => c.lcase = true,
"ucase" => c.ucase = true,
"block" => c.block = true,
"unblock" => c.unblock = true,
"swab" => c.swab = true,
"sync" => c.sync = true,
"noerror" => c.noerror = true,
"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,
"Only one of conv=ascii conv=ebcdic or conv=ibm may be specified"
)
}
Self::MultipleUCaseLCase => {
write!(f, "Only one of conv=lcase or conv=ucase may be specified")
}
Self::MultipleBlockUnblock => {
write!(f, "Only one of conv=block or conv=unblock may be specified")
}
Self::MultipleExclNoCreate => {
write!(f, "Only one ov conv=excl or conv=nocreat may be specified")
}
Self::FlagNoMatch(arg) => {
write!(
f,
"invalid input flag: ‘{}’\nTry '{} --help' for more information.",
arg,
uucore::execution_phrase()
)
}
Self::ConvFlagNoMatch(arg) => {
write!(f, "Unrecognized conv=CONV -> {arg}")
}
Self::MultiplierStringParseFailure(arg) => {
write!(f, "Unrecognized byte multiplier -> {arg}")
}
Self::MultiplierStringOverflow(arg) => {
write!(
f,
"Multiplier string would overflow on current system -> {arg}"
)
}
Self::BlockUnblockWithoutCBS => {
write!(f, "conv=block or conv=unblock specified without cbs=N")
}
Self::StatusLevelNotRecognized(arg) => {
write!(f, "status=LEVEL not recognized -> {arg}")
}
Self::BsOutOfRange(arg) => {
write!(f, "{arg}=N cannot fit into memory")
}
Self::Unimplemented(arg) => {
write!(f, "feature not implemented on this system -> {arg}")
}
Self::InvalidNumber(arg) => {
write!(f, "invalid number: ‘{arg}’")
}
}
}
}
impl Error for ParseError {}
impl UError for ParseError {
fn code(&self) -> i32 {
1
}
}
fn show_zero_multiplier_warning() {
show_warning!(
"{} is a zero multiplier; use {} if that is intended",
"0x".quote(),
"00x".quote()
);
}
fn parse_bytes_only(s: &str) -> Result<u64, ParseError> {
s.parse()
.map_err(|_| ParseError::MultiplierStringParseFailure(s.to_string()))
}
fn parse_bytes_no_x(full: &str, s: &str) -> Result<u64, ParseError> {
let parser = SizeParser {
capital_b_bytes: true,
..Default::default()
};
let (num, multiplier) = match (s.find('c'), s.rfind('w'), s.rfind('b')) {
(None, None, None) => match parser.parse(s) {
Ok(n) => (n, 1),
Err(ParseSizeError::InvalidSuffix(_) | ParseSizeError::ParseFailure(_)) => {
return Err(ParseError::InvalidNumber(full.to_string()))
}
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(full.to_string())),
};
num.checked_mul(multiplier)
.ok_or_else(|| ParseError::MultiplierStringOverflow(full.to_string()))
}
pub fn parse_bytes_with_opt_multiplier(s: &str) -> Result<u64, ParseError> {
let parts: Vec<&str> = s.split('x').collect();
if parts.len() == 1 {
parse_bytes_no_x(s, parts[0])
} else {
let mut total: u64 = 1;
for part in parts {
if part == "0" {
show_zero_multiplier_warning();
}
let num = parse_bytes_no_x(s, part)?;
total = total
.checked_mul(num)
.ok_or_else(|| ParseError::InvalidNumber(s.to_string()))?;
}
Ok(total)
}
}
fn get_ctable(
conversion: Option<Conversion>,
case: Option<Case>,
) -> 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,
},
})
}
fn conversion_mode(
ctable: Option<&'static ConversionTable>,
block: Option<Block>,
is_ascii: bool,
is_sync: bool,
) -> Option<ConversionMode> {
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), Some(Block::Unblock(cbs))) => {
if is_ascii {
Some(ConversionMode::ConvertThenUnblock(ct, cbs))
} else {
Some(ConversionMode::UnblockThenConvert(ct, cbs))
}
}
(None, Some(Block::Block(cbs))) => Some(ConversionMode::BlockOnly(cbs, is_sync)),
(None, Some(Block::Unblock(cbs))) => Some(ConversionMode::UnblockOnly(cbs)),
(None, None) => None,
}
}
#[cfg(test)]
mod tests {
use crate::parseargs::parse_bytes_with_opt_multiplier;
#[test]
fn test_parse_bytes_with_opt_multiplier() {
assert_eq!(parse_bytes_with_opt_multiplier("123").unwrap(), 123);
assert_eq!(parse_bytes_with_opt_multiplier("123c").unwrap(), 123); assert_eq!(parse_bytes_with_opt_multiplier("123w").unwrap(), 123 * 2);
assert_eq!(parse_bytes_with_opt_multiplier("123b").unwrap(), 123 * 512);
assert_eq!(parse_bytes_with_opt_multiplier("123x3").unwrap(), 123 * 3);
assert_eq!(parse_bytes_with_opt_multiplier("123k").unwrap(), 123 * 1024);
assert_eq!(parse_bytes_with_opt_multiplier("1x2x3").unwrap(), 6); assert_eq!(
parse_bytes_with_opt_multiplier("1wx2cx3w").unwrap(),
2 * 2 * (3 * 2) );
assert!(parse_bytes_with_opt_multiplier("123asdf").is_err());
}
}