improvement: make diagnostics for flags involving byte count parsing actually good

This adds an error type called `ByteCountParseError` with a vanilla
`std::error::Error` implementation. The code could be significantly
compressed with a dependency like `thiserror`.

I opted to implement a separate error type for `parse_byte_count` rather
than using `anyhow`'s small ecosystem of error types there (though they
are used in `<binary>::run`) because it allows pairing diagnostic
results with tests, incl. those already written. This can be helpful in
preventing regressions in error diagnostics.
This commit is contained in:
Erich Gubler authored and David Peter committed 2020-06-17 18:05:42 +02:00
1 parent e0da7b3f82
commit 13aec946a3
3 files changed
+217 -66

No files matched your search

Generated
+56
View File
@@ -67,6 +67,7 @@ dependencies = [
"atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -74,11 +75,37 @@ name = "libc"
version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "proc-macro2"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "syn"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "term_size"
version = "0.3.2"
@@ -97,11 +124,34 @@ dependencies = [
"unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thiserror"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"thiserror-impl 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thiserror-impl"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "unicode-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi"
version = "0.3.8"
@@ -130,10 +180,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum clap 2.33.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bdfa80d47f954d53a35a64987ca1422f495b8d6483c0fe9f7117b36c2a792129"
"checksum hermit-abi 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "91780f809e750b0a89f5544be56617ff6b1227ee485bcb06ebe10cdf89bd3b71"
"checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
"checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
"checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "b5304cfdf27365b7585c25d4af91b35016ed21ef88f17ced89c7093b43dba8b6"
"checksum term_size 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9"
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum thiserror 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "b13f926965ad00595dd129fa12823b04bbf866e9085ab0a5f2b05b850fbfc344"
"checksum thiserror-impl 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)" = "893582086c2f98cde18f906265a65b5030a074b1046c674ae898be6519a7f479"
"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+1
View File
@@ -15,6 +15,7 @@ ansi_term = "0.12"
anyhow = "1.0.31"
atty = "0.2"
libc = "0.2"
thiserror = "1.0.19"
[dependencies.clap]
version = "2"
+160 -66
View File
@@ -9,7 +9,9 @@ use clap::{App, AppSettings, Arg};
use atty::Stream;
use anyhow::{anyhow, Error as AnyhowError};
use anyhow::{anyhow, Context, Error as AnyhowError};
use thiserror::Error as ThisError;
use hexyl::{BorderStyle, Input, Printer};
@@ -124,22 +126,45 @@ fn run() -> Result<(), AnyhowError> {
let block_size = matches
.value_of("block_size")
.and_then(|bs| bs.parse().ok().and_then(PositiveI64::new))
.map(|bs| {
bs.parse::<i64>()
.map_err(|e| anyhow!(e))
.and_then(|x| {
PositiveI64::new(x)
.ok_or_else(|| anyhow!("negative block sizes don't make sense"))
})
.context(anyhow!(
"failed to parse `--block-size` arg {:?} as positive integer",
bs
))
})
.transpose()?
.unwrap_or_else(|| PositiveI64::new(512).unwrap());
let skip_arg = matches
.value_of("skip")
.and_then(|s| parse_byte_count(s, block_size));
.map(|s| {
parse_byte_count(s, block_size).context(anyhow!(
"failed to parse `--skip` arg {:?} as byte count",
s
))
})
.transpose()?;
if let Some(skip) = skip_arg {
reader.seek(SeekFrom::Current(skip.into_inner()))?;
}
let length_arg = matches
let mut reader = if let Some(length) = matches
.value_of("length")
.or_else(|| matches.value_of("bytes"));
let mut reader = if let Some(length) = length_arg.and_then(|s| parse_byte_count(s, block_size))
.or_else(|| matches.value_of("bytes"))
.map(|s| {
parse_byte_count(s, block_size).context(anyhow!(
"failed to parse `--length` arg {:?} as byte count",
s
))
})
.transpose()?
{
Box::new(reader.take(length.into()))
} else {
@@ -162,7 +187,13 @@ fn run() -> Result<(), AnyhowError> {
let display_offset = matches
.value_of("display_offset")
.and_then(|s| parse_byte_count(s, block_size))
.map(|s| {
parse_byte_count(s, block_size).context(anyhow!(
"failed to parse `--display-offset` arg {:?} as byte count",
s
))
})
.transpose()?
.or(skip_arg)
.unwrap_or_default()
.into();
@@ -226,67 +257,118 @@ impl Into<u64> for PositiveI64 {
}
}
fn parse_byte_count(n: &str, block_size: PositiveI64) -> Option<PositiveI64> {
const HEX_PREFIX: &str = "0x";
const HEX_PREFIX: &str = "0x";
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
enum ByteCountParseError {
#[error("no character data found, did you forget to write it?")]
Empty,
#[error("no digits found after sign, did you forget to write them?")]
EmptyAfterSign,
#[error("negative byte offsets are not accepted (yet)")]
Negative,
#[error(
"found {0:?} sign after hex prefix ({:?}); signs should go before it",
HEX_PREFIX
)]
SignFoundAfterHexPrefix(char),
#[error("{0:?} is not of the expected form <pos-integer>[<unit>]")]
InvalidNumAndUnit(String),
#[error("{0:?} is a valid unit, but an integer should come before it")]
EmptyWithUnit(String),
#[error("invalid unit {0:?}")]
InvalidUnit(String),
#[error("failed to parse integer part")]
ParseNum(#[source] std::num::ParseIntError),
#[error("count multipled by the unit overflowed a signed 64-bit integer; are you sure it should be that big?")]
UnitMultiplicationOverflow,
}
fn parse_byte_count(n: &str, block_size: PositiveI64) -> Result<PositiveI64, ByteCountParseError> {
use ByteCountParseError::*;
let n = {
let mut chars = n.chars();
match chars.next()? {
'+' => chars.as_str(),
'-' => return None,
match chars.next() {
Some('+') => {
if chars.clone().next().is_none() {
return Err(EmptyAfterSign);
}
chars.as_str()
}
Some('-') => return Err(Negative),
None => return Err(Empty),
_ => n,
}
};
if n.starts_with(HEX_PREFIX) {
let n = &n[HEX_PREFIX.len()..];
match n.chars().next() {
Some('+') | Some('-') => return None,
let mut chars = n.chars();
match chars.next() {
Some(c @ '+') | Some(c @ '-') => {
return if chars.next().is_none() {
Err(EmptyAfterSign)
} else {
Err(SignFoundAfterHexPrefix(c))
}
}
_ => (),
}
return i64::from_str_radix(n, 16).ok().and_then(PositiveI64::new);
return i64::from_str_radix(n, 16)
.map(|x| Ok(PositiveI64::new(x).unwrap()))
.map_err(ParseNum)?;
}
let (n, unit_multiplier) = match n.chars().position(|c| !c.is_ascii_digit()) {
let (num, unit) = match n.chars().position(|c| !c.is_ascii_digit()) {
Some(unit_begin_idx) => {
let (n, raw_unit) = n.split_at(unit_begin_idx);
let raw_unit = raw_unit.to_lowercase();
(
n,
[
("b", 1),
("kb", 1000i64.pow(1)),
("mb", 1000i64.pow(2)),
("gb", 1000i64.pow(3)),
("tb", 1000i64.pow(4)),
("kib", 1024i64.pow(1)),
("mib", 1024i64.pow(2)),
("gib", 1024i64.pow(3)),
("tib", 1024i64.pow(4)),
("block", block_size.into_inner()),
]
.iter()
.cloned()
.find_map(|(unit, multiplier)| {
if unit == raw_unit {
Some(multiplier)
} else {
None
}
})?,
)
let raw_unit_lower = raw_unit.to_lowercase();
let multiplier = [
("b", 1),
("kb", 1000i64.pow(1)),
("mb", 1000i64.pow(2)),
("gb", 1000i64.pow(3)),
("tb", 1000i64.pow(4)),
("kib", 1024i64.pow(1)),
("mib", 1024i64.pow(2)),
("gib", 1024i64.pow(3)),
("tib", 1024i64.pow(4)),
("block", block_size.into_inner()),
]
.iter()
.cloned()
.find_map(|(unit, multiplier)| {
if unit == raw_unit_lower {
Some(multiplier)
} else {
None
}
})
.ok_or_else(|| InvalidUnit(raw_unit.to_owned()));
(n, multiplier.map(|m| (Some(raw_unit), m)))
}
None => (n, 1),
None => (n, Ok((None, 1))),
};
n.parse::<i64>()
.ok()?
.checked_mul(unit_multiplier)
.and_then(PositiveI64::new)
match (num.parse::<i64>(), unit) {
(Ok(num), Ok((_raw_unit, unit_multiplier))) => num
.checked_mul(unit_multiplier)
.ok_or_else(|| UnitMultiplicationOverflow)
.and_then(|x| PositiveI64::new(x).ok_or(Negative)),
(Ok(_), Err(e)) => Err(e),
(Err(e), Ok((raw_unit, _unit_multiplier))) => match raw_unit {
Some(raw_unit) if num.is_empty() => Err(EmptyWithUnit(raw_unit.to_owned())),
_ => Err(ParseNum(e)),
},
(Err(_), Err(_)) => Err(InvalidNumAndUnit(n.to_owned())),
}
}
#[test]
fn test_parse_byte_count() {
use ByteCountParseError::*;
macro_rules! success {
($input: expr, $expected: expr) => {
success!($input, 512, $expected)
@@ -294,16 +376,16 @@ fn test_parse_byte_count() {
($input: expr, $block_size: expr, $expected: expr) => {
assert_eq!(
parse_byte_count($input, PositiveI64::new($block_size).unwrap()),
Some(PositiveI64::new($expected).unwrap())
Ok(PositiveI64::new($expected).unwrap()),
);
};
}
macro_rules! error {
($input: expr) => {
($input: expr, $expected_err: expr) => {
assert_eq!(
parse_byte_count($input, PositiveI64::new(512).unwrap()),
None
Err($expected_err),
);
};
}
@@ -314,6 +396,10 @@ fn test_parse_byte_count() {
success!("100", 100);
success!("+100", 100);
success!("0x0", 0);
success!("0xf", 15);
success!("0xdeadbeef", 3_735_928_559);
success!("1KB", 1000);
success!("2MB", 2000000);
success!("3GB", 3000000000);
@@ -334,27 +420,35 @@ fn test_parse_byte_count() {
success!("2block", 4, 8);
// empty string is invalid
error!("");
error!("", Empty);
// These are also bad.
error!("+");
error!("-");
error!("K");
error!("k");
error!("m");
error!("block");
error!("+", EmptyAfterSign);
error!("-", Negative);
error!("K", InvalidNumAndUnit("K".to_owned()));
error!("k", InvalidNumAndUnit("k".to_owned()));
error!("m", InvalidNumAndUnit("m".to_owned()));
error!("block", EmptyWithUnit("block".to_owned()));
// leading/trailing space is invalid
error!(" 0");
error!("0 ");
error!(" 0", InvalidNumAndUnit(" 0".to_owned()));
error!("0 ", InvalidUnit(" ".to_owned()));
// Negatives make no sense for byte counts
error!("-1");
error!("0x-12");
error!("-1", Negative);
error!("-0x-12", Negative);
error!("0x-12", SignFoundAfterHexPrefix('-'));
// This was previously accepted but shouldn't be.
error!("0x+12");
error!("0x+12", SignFoundAfterHexPrefix('+'));
// invalid suffix
error!("1234asdf");
error!("1234asdf", InvalidUnit("asdf".to_owned()));
// bad numbers
error!("asdf1234");
error!("a1s2d3f4");
error!("asdf1234", InvalidNumAndUnit("asdf1234".to_owned()));
error!("a1s2d3f4", InvalidNumAndUnit("a1s2d3f4".to_owned()));
// multiplication overflows u64
error!("20000000TiB");
error!("20000000TiB", UnitMultiplicationOverflow);
assert!(match parse_byte_count("99999999999999999999", PositiveI64::new(512).unwrap()) {
// We can't check against the kind of the `ParseIntError`, so we'll just make sure it's the
// same as trying to do the parse directly.
Err(ParseNum(e)) => e == "99999999999999999999".parse::<i64>().unwrap_err(),
_ => false,
});
}