mirror of
https://github.com/sharkdp/hexyl.git
synced 2026-10-30 09:05:54 +00:00
feat: make signs significant for byte offsets
This commit is contained in:
1 parent
7217a60e6e
commit
153b1cccdf
1 file changed
+122
-66
+122
-66
@@ -56,7 +56,8 @@ fn run() -> Result<(), AnyhowError> {
|
|||||||
.value_name("N")
|
.value_name("N")
|
||||||
.help(
|
.help(
|
||||||
"Skip the first N bytes of the input. The N argument can also include \
|
"Skip the first N bytes of the input. The N argument can also include \
|
||||||
a unit (see `--length` for details)",
|
a unit (see `--length` for details)\n\
|
||||||
|
A negative value is valid and will seek from the end of the file.",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
@@ -111,7 +112,9 @@ fn run() -> Result<(), AnyhowError> {
|
|||||||
.value_name("N")
|
.value_name("N")
|
||||||
.help(
|
.help(
|
||||||
"Add N bytes to the displayed file position. The N argument can also \
|
"Add N bytes to the displayed file position. The N argument can also \
|
||||||
include a unit (see `--length` for details)",
|
include a unit (see `--length` for details)\n\
|
||||||
|
A negative value is valid and calculates an offset relative to the \
|
||||||
|
end of the file.",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -144,29 +147,43 @@ fn run() -> Result<(), AnyhowError> {
|
|||||||
let skip_arg = matches
|
let skip_arg = matches
|
||||||
.value_of("skip")
|
.value_of("skip")
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
parse_byte_count(s, block_size).context(anyhow!(
|
parse_byte_offset(s, block_size).context(anyhow!(
|
||||||
"failed to parse `--skip` arg {:?} as byte count",
|
"failed to parse `--skip` arg {:?} as byte count",
|
||||||
s
|
s
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
if let Some(skip) = skip_arg {
|
let default_display_offset = if let Some(ByteOffset { kind, value }) = skip_arg {
|
||||||
reader.seek(SeekFrom::Current(skip.into_inner()))?;
|
let value = value.into_inner();
|
||||||
}
|
reader.seek(match kind {
|
||||||
|
ByteOffsetKind::ForwardFromBeginning | ByteOffsetKind::ForwardFromLastOffset => {
|
||||||
|
SeekFrom::Current(value)
|
||||||
|
}
|
||||||
|
ByteOffsetKind::BackwardFromEnd => SeekFrom::End(value.checked_neg().unwrap()),
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let parse_byte_count = |s| -> Result<u64, AnyhowError> {
|
||||||
|
Ok(parse_byte_offset(s, block_size)?
|
||||||
|
.assume_forward_offset_from_start()?.into())
|
||||||
|
};
|
||||||
|
|
||||||
let mut reader = if let Some(length) = matches
|
let mut reader = if let Some(length) = matches
|
||||||
.value_of("length")
|
.value_of("length")
|
||||||
.or_else(|| matches.value_of("bytes"))
|
.or_else(|| matches.value_of("bytes"))
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
parse_byte_count(s, block_size).context(anyhow!(
|
parse_byte_count(s)
|
||||||
"failed to parse `--length` arg {:?} as byte count",
|
.context(anyhow!(
|
||||||
s
|
"failed to parse `--length` arg {:?} as byte count",
|
||||||
))
|
s
|
||||||
|
))
|
||||||
})
|
})
|
||||||
.transpose()?
|
.transpose()?
|
||||||
{
|
{
|
||||||
Box::new(reader.take(length.into()))
|
Box::new(reader.take(length))
|
||||||
} else {
|
} else {
|
||||||
reader.into_inner()
|
reader.into_inner()
|
||||||
};
|
};
|
||||||
@@ -188,14 +205,13 @@ fn run() -> Result<(), AnyhowError> {
|
|||||||
let display_offset = matches
|
let display_offset = matches
|
||||||
.value_of("display_offset")
|
.value_of("display_offset")
|
||||||
.map(|s| {
|
.map(|s| {
|
||||||
parse_byte_count(s, block_size).context(anyhow!(
|
parse_byte_count(s).context(anyhow!(
|
||||||
"failed to parse `--display-offset` arg {:?} as byte count",
|
"failed to parse `--display-offset` arg {:?} as byte count",
|
||||||
s
|
s
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.or(skip_arg)
|
.unwrap_or(default_display_offset)
|
||||||
.unwrap_or_default()
|
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
let stdout = io::stdout();
|
let stdout = io::stdout();
|
||||||
@@ -259,14 +275,41 @@ impl Into<u64> for PositiveI64 {
|
|||||||
|
|
||||||
const HEX_PREFIX: &str = "0x";
|
const HEX_PREFIX: &str = "0x";
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
enum ByteOffsetKind {
|
||||||
|
ForwardFromBeginning,
|
||||||
|
ForwardFromLastOffset,
|
||||||
|
BackwardFromEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
struct ByteOffset {
|
||||||
|
value: PositiveI64,
|
||||||
|
kind: ByteOffsetKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, ThisError)]
|
||||||
|
#[error("negative offset specified, but only positive offsets (counts) are accepted in this context")]
|
||||||
|
struct NegativeOffsetSpecifiedError;
|
||||||
|
|
||||||
|
impl ByteOffset {
|
||||||
|
fn assume_forward_offset_from_start(&self) -> Result<PositiveI64, NegativeOffsetSpecifiedError> {
|
||||||
|
let &Self { value, kind } = self;
|
||||||
|
match kind {
|
||||||
|
ByteOffsetKind::ForwardFromBeginning | ByteOffsetKind::ForwardFromLastOffset => {
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
ByteOffsetKind::BackwardFromEnd => Err(NegativeOffsetSpecifiedError),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
|
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
|
||||||
enum ByteCountParseError {
|
enum ByteOffsetParseError {
|
||||||
#[error("no character data found, did you forget to write it?")]
|
#[error("no character data found, did you forget to write it?")]
|
||||||
Empty,
|
Empty,
|
||||||
#[error("no digits found after sign, did you forget to write them?")]
|
#[error("no digits found after sign, did you forget to write them?")]
|
||||||
EmptyAfterSign,
|
EmptyAfterSign,
|
||||||
#[error("negative byte offsets are not accepted (yet)")]
|
|
||||||
Negative,
|
|
||||||
#[error(
|
#[error(
|
||||||
"found {0:?} sign after hex prefix ({:?}); signs should go before it",
|
"found {0:?} sign after hex prefix ({:?}); signs should go before it",
|
||||||
HEX_PREFIX
|
HEX_PREFIX
|
||||||
@@ -284,24 +327,34 @@ enum ByteCountParseError {
|
|||||||
UnitMultiplicationOverflow,
|
UnitMultiplicationOverflow,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_byte_count(n: &str, block_size: PositiveI64) -> Result<PositiveI64, ByteCountParseError> {
|
fn parse_byte_offset(n: &str, block_size: PositiveI64) -> Result<ByteOffset, ByteOffsetParseError> {
|
||||||
use ByteCountParseError::*;
|
use ByteOffsetParseError::*;
|
||||||
|
|
||||||
let n = {
|
let (n, kind) = {
|
||||||
let mut chars = n.chars();
|
let mut chars = n.chars();
|
||||||
match chars.next() {
|
let next_char = chars.next();
|
||||||
Some('+') => {
|
let check_empty_after_sign = || {
|
||||||
if chars.clone().next().is_none() {
|
if chars.clone().next().is_none() {
|
||||||
return Err(EmptyAfterSign);
|
Err(EmptyAfterSign)
|
||||||
}
|
} else {
|
||||||
chars.as_str()
|
Ok(chars.as_str())
|
||||||
}
|
}
|
||||||
Some('-') => return Err(Negative),
|
};
|
||||||
|
match next_char {
|
||||||
|
Some('+') => (check_empty_after_sign()?, ByteOffsetKind::ForwardFromLastOffset),
|
||||||
|
Some('-') => (check_empty_after_sign()?, ByteOffsetKind::BackwardFromEnd),
|
||||||
None => return Err(Empty),
|
None => return Err(Empty),
|
||||||
_ => n,
|
_ => (n, ByteOffsetKind::ForwardFromBeginning),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let into_byte_offset = |value| {
|
||||||
|
Ok(ByteOffset {
|
||||||
|
value: PositiveI64::new(value).unwrap(),
|
||||||
|
kind,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
if n.starts_with(HEX_PREFIX) {
|
if n.starts_with(HEX_PREFIX) {
|
||||||
let n = &n[HEX_PREFIX.len()..];
|
let n = &n[HEX_PREFIX.len()..];
|
||||||
let mut chars = n.chars();
|
let mut chars = n.chars();
|
||||||
@@ -316,7 +369,7 @@ fn parse_byte_count(n: &str, block_size: PositiveI64) -> Result<PositiveI64, Byt
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
return i64::from_str_radix(n, 16)
|
return i64::from_str_radix(n, 16)
|
||||||
.map(|x| Ok(PositiveI64::new(x).unwrap()))
|
.map(into_byte_offset)
|
||||||
.map_err(ParseNum)?;
|
.map_err(ParseNum)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,7 +408,7 @@ fn parse_byte_count(n: &str, block_size: PositiveI64) -> Result<PositiveI64, Byt
|
|||||||
(Ok(num), Ok((_raw_unit, unit_multiplier))) => num
|
(Ok(num), Ok((_raw_unit, unit_multiplier))) => num
|
||||||
.checked_mul(unit_multiplier)
|
.checked_mul(unit_multiplier)
|
||||||
.ok_or_else(|| UnitMultiplicationOverflow)
|
.ok_or_else(|| UnitMultiplicationOverflow)
|
||||||
.and_then(|x| PositiveI64::new(x).ok_or(Negative)),
|
.and_then(into_byte_offset),
|
||||||
(Ok(_), Err(e)) => Err(e),
|
(Ok(_), Err(e)) => Err(e),
|
||||||
(Err(e), Ok((raw_unit, _unit_multiplier))) => match raw_unit {
|
(Err(e), Ok((raw_unit, _unit_multiplier))) => match raw_unit {
|
||||||
Some(raw_unit) if num.is_empty() => Err(EmptyWithUnit(raw_unit.to_owned())),
|
Some(raw_unit) if num.is_empty() => Err(EmptyWithUnit(raw_unit.to_owned())),
|
||||||
@@ -366,17 +419,22 @@ fn parse_byte_count(n: &str, block_size: PositiveI64) -> Result<PositiveI64, Byt
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_byte_count() {
|
fn test_parse_byte_offset() {
|
||||||
use ByteCountParseError::*;
|
use ByteOffsetParseError::*;
|
||||||
|
|
||||||
macro_rules! success {
|
macro_rules! success {
|
||||||
($input: expr, $expected: expr) => {
|
($input: expr, $expected_kind: ident $expected_value: expr) => {
|
||||||
success!($input, 512, $expected)
|
success!($input, $expected_kind $expected_value; block_size: 512)
|
||||||
};
|
};
|
||||||
($input: expr, $block_size: expr, $expected: expr) => {
|
($input: expr, $expected_kind: ident $expected_value: expr; block_size: $block_size: expr) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_byte_count($input, PositiveI64::new($block_size).unwrap()),
|
parse_byte_offset($input, PositiveI64::new($block_size).unwrap()),
|
||||||
Ok(PositiveI64::new($expected).unwrap()),
|
Ok(
|
||||||
|
ByteOffset {
|
||||||
|
value: PositiveI64::new($expected_value).unwrap(),
|
||||||
|
kind: ByteOffsetKind::$expected_kind,
|
||||||
|
}
|
||||||
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -384,46 +442,46 @@ fn test_parse_byte_count() {
|
|||||||
macro_rules! error {
|
macro_rules! error {
|
||||||
($input: expr, $expected_err: expr) => {
|
($input: expr, $expected_err: expr) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_byte_count($input, PositiveI64::new(512).unwrap()),
|
parse_byte_offset($input, PositiveI64::new(512).unwrap()),
|
||||||
Err($expected_err),
|
Err($expected_err),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
success!("0", 0);
|
success!("0", ForwardFromBeginning 0);
|
||||||
success!("1", 1);
|
success!("1", ForwardFromBeginning 1);
|
||||||
success!("1", 1);
|
success!("1", ForwardFromBeginning 1);
|
||||||
success!("100", 100);
|
success!("100", ForwardFromBeginning 100);
|
||||||
success!("+100", 100);
|
success!("+100", ForwardFromLastOffset 100);
|
||||||
|
|
||||||
success!("0x0", 0);
|
success!("0x0", ForwardFromBeginning 0);
|
||||||
success!("0xf", 15);
|
success!("0xf", ForwardFromBeginning 15);
|
||||||
success!("0xdeadbeef", 3_735_928_559);
|
success!("0xdeadbeef", ForwardFromBeginning 3_735_928_559);
|
||||||
|
|
||||||
success!("1KB", 1000);
|
success!("1KB", ForwardFromBeginning 1000);
|
||||||
success!("2MB", 2000000);
|
success!("2MB", ForwardFromBeginning 2000000);
|
||||||
success!("3GB", 3000000000);
|
success!("3GB", ForwardFromBeginning 3000000000);
|
||||||
success!("4TB", 4000000000000);
|
success!("4TB", ForwardFromBeginning 4000000000000);
|
||||||
success!("+4TB", 4000000000000);
|
success!("+4TB", ForwardFromLastOffset 4000000000000);
|
||||||
|
|
||||||
success!("1GiB", 1073741824);
|
success!("1GiB", ForwardFromBeginning 1073741824);
|
||||||
success!("2TiB", 2199023255552);
|
success!("2TiB", ForwardFromBeginning 2199023255552);
|
||||||
success!("+2TiB", 2199023255552);
|
success!("+2TiB", ForwardFromLastOffset 2199023255552);
|
||||||
|
|
||||||
success!("0xff", 255);
|
success!("0xff", ForwardFromBeginning 255);
|
||||||
success!("0xEE", 238);
|
success!("0xEE", ForwardFromBeginning 238);
|
||||||
success!("+0xFF", 255);
|
success!("+0xFF", ForwardFromLastOffset 255);
|
||||||
|
|
||||||
success!("1block", 512, 512);
|
success!("1block", ForwardFromBeginning 512; block_size: 512);
|
||||||
success!("2block", 512, 1024);
|
success!("2block", ForwardFromBeginning 1024; block_size: 512);
|
||||||
success!("1block", 4, 4);
|
success!("1block", ForwardFromBeginning 4; block_size: 4);
|
||||||
success!("2block", 4, 8);
|
success!("2block", ForwardFromBeginning 8; block_size: 4);
|
||||||
|
|
||||||
// empty string is invalid
|
// empty string is invalid
|
||||||
error!("", Empty);
|
error!("", Empty);
|
||||||
// These are also bad.
|
// These are also bad.
|
||||||
error!("+", EmptyAfterSign);
|
error!("+", EmptyAfterSign);
|
||||||
error!("-", Negative);
|
error!("-", EmptyAfterSign);
|
||||||
error!("K", InvalidNumAndUnit("K".to_owned()));
|
error!("K", InvalidNumAndUnit("K".to_owned()));
|
||||||
error!("k", InvalidNumAndUnit("k".to_owned()));
|
error!("k", InvalidNumAndUnit("k".to_owned()));
|
||||||
error!("m", InvalidNumAndUnit("m".to_owned()));
|
error!("m", InvalidNumAndUnit("m".to_owned()));
|
||||||
@@ -431,9 +489,7 @@ fn test_parse_byte_count() {
|
|||||||
// leading/trailing space is invalid
|
// leading/trailing space is invalid
|
||||||
error!(" 0", InvalidNumAndUnit(" 0".to_owned()));
|
error!(" 0", InvalidNumAndUnit(" 0".to_owned()));
|
||||||
error!("0 ", InvalidUnit(" ".to_owned()));
|
error!("0 ", InvalidUnit(" ".to_owned()));
|
||||||
// Negatives make no sense for byte counts
|
// Signs after the hex prefix make no sense
|
||||||
error!("-1", Negative);
|
|
||||||
error!("-0x-12", Negative);
|
|
||||||
error!("0x-12", SignFoundAfterHexPrefix('-'));
|
error!("0x-12", SignFoundAfterHexPrefix('-'));
|
||||||
// This was previously accepted but shouldn't be.
|
// This was previously accepted but shouldn't be.
|
||||||
error!("0x+12", SignFoundAfterHexPrefix('+'));
|
error!("0x+12", SignFoundAfterHexPrefix('+'));
|
||||||
@@ -445,7 +501,7 @@ fn test_parse_byte_count() {
|
|||||||
// multiplication overflows u64
|
// multiplication overflows u64
|
||||||
error!("20000000TiB", UnitMultiplicationOverflow);
|
error!("20000000TiB", UnitMultiplicationOverflow);
|
||||||
|
|
||||||
assert!(match parse_byte_count("99999999999999999999", PositiveI64::new(512).unwrap()) {
|
assert!(match parse_byte_offset("99999999999999999999", PositiveI64::new(512).unwrap()) {
|
||||||
// We can't check against the kind of the `ParseIntError`, so we'll just make sure it's the
|
// 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.
|
// same as trying to do the parse directly.
|
||||||
Err(ParseNum(e)) => e == "99999999999999999999".parse::<i64>().unwrap_err(),
|
Err(ParseNum(e)) => e == "99999999999999999999".parse::<i64>().unwrap_err(),
|
||||||
|
|||||||
Reference in new issue
Block a user