2018-11-05 21:30:39 +01:00
|
|
|
#[macro_use]
|
|
|
|
|
extern crate clap;
|
2019-04-13 18:25:12 +02:00
|
|
|
|
2020-06-01 12:30:27 -06:00
|
|
|
use std::convert::TryFrom;
|
2018-11-05 21:30:39 +01:00
|
|
|
use std::fs::File;
|
2020-04-29 08:22:03 +02:00
|
|
|
use std::io::{self, prelude::*, SeekFrom};
|
2021-11-01 21:15:00 +01:00
|
|
|
use std::num::NonZeroI64;
|
2018-11-05 21:30:39 +01:00
|
|
|
|
|
|
|
|
use clap::{App, AppSettings, Arg};
|
|
|
|
|
|
2019-05-29 23:01:38 +02:00
|
|
|
use atty::Stream;
|
2019-07-14 17:38:30 +01:00
|
|
|
|
2020-06-11 12:25:56 -06:00
|
|
|
use anyhow::{anyhow, Context, Error as AnyhowError};
|
|
|
|
|
|
2021-10-31 15:29:41 +01:00
|
|
|
use const_format::formatcp;
|
|
|
|
|
|
2020-06-11 12:25:56 -06:00
|
|
|
use thiserror::Error as ThisError;
|
2020-06-10 13:00:10 -06:00
|
|
|
|
2020-04-29 08:22:03 +02:00
|
|
|
use hexyl::{BorderStyle, Input, Printer};
|
2019-01-14 09:05:12 +01:00
|
|
|
|
2021-10-31 15:29:41 +01:00
|
|
|
const DEFAULT_BLOCK_SIZE: i64 = 512;
|
|
|
|
|
|
2020-06-10 13:00:10 -06:00
|
|
|
fn run() -> Result<(), AnyhowError> {
|
2018-11-05 21:30:39 +01:00
|
|
|
let app = App::new(crate_name!())
|
|
|
|
|
.setting(AppSettings::ColorAuto)
|
|
|
|
|
.setting(AppSettings::ColoredHelp)
|
|
|
|
|
.setting(AppSettings::DeriveDisplayOrder)
|
|
|
|
|
.setting(AppSettings::UnifiedHelpMessage)
|
2020-06-08 23:19:56 +02:00
|
|
|
.max_term_width(90)
|
2018-11-05 21:30:39 +01:00
|
|
|
.version(crate_version!())
|
2018-11-05 23:23:06 +01:00
|
|
|
.about(crate_description!())
|
2020-06-07 14:37:17 +02:00
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("FILE")
|
|
|
|
|
.help("The file to display. If no FILE argument is given, read from STDIN."),
|
|
|
|
|
)
|
2018-11-06 20:18:15 +01:00
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("length")
|
|
|
|
|
.short("n")
|
|
|
|
|
.long("length")
|
|
|
|
|
.takes_value(true)
|
|
|
|
|
.value_name("N")
|
2020-06-07 14:37:17 +02:00
|
|
|
.help(
|
|
|
|
|
"Only read N bytes from the input. The N argument can also include a \
|
2021-10-20 22:40:53 +02:00
|
|
|
unit with a decimal prefix (kB, MB, ..) or binary prefix (kiB, MiB, ..), \
|
|
|
|
|
or can be specified using a hex number. \
|
2021-07-11 19:52:38 +02:00
|
|
|
The short option '-l' can be used as an alias.\n\
|
2021-10-20 22:40:53 +02:00
|
|
|
Examples: --length=64, --length=4KiB, --length=0xff",
|
2020-06-07 14:37:17 +02:00
|
|
|
),
|
2019-01-11 22:10:20 +10:00
|
|
|
)
|
2019-02-04 17:04:26 -06:00
|
|
|
.arg(
|
2019-02-05 06:45:45 -06:00
|
|
|
Arg::with_name("bytes")
|
2019-02-04 17:04:26 -06:00
|
|
|
.short("c")
|
2019-02-05 06:45:45 -06:00
|
|
|
.long("bytes")
|
2019-02-04 17:04:26 -06:00
|
|
|
.takes_value(true)
|
2019-02-05 16:37:27 -06:00
|
|
|
.value_name("N")
|
2020-10-29 21:11:17 +01:00
|
|
|
.conflicts_with("length")
|
2019-02-05 16:37:27 -06:00
|
|
|
.help("An alias for -n/--length"),
|
2019-02-04 17:04:26 -06:00
|
|
|
)
|
2021-07-02 19:34:06 +05:30
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("count")
|
|
|
|
|
.short("l")
|
|
|
|
|
.takes_value(true)
|
|
|
|
|
.value_name("N")
|
2022-03-25 22:54:56 +09:00
|
|
|
.conflicts_with_all(&["length", "bytes"])
|
2021-07-02 19:34:06 +05:30
|
|
|
.hidden(true)
|
|
|
|
|
.help("Yet another alias for -n/--length"),
|
|
|
|
|
)
|
2020-04-29 08:22:03 +02:00
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("skip")
|
|
|
|
|
.short("s")
|
|
|
|
|
.long("skip")
|
|
|
|
|
.takes_value(true)
|
|
|
|
|
.value_name("N")
|
2020-06-07 14:37:17 +02:00
|
|
|
.help(
|
|
|
|
|
"Skip the first N bytes of the input. The N argument can also include \
|
2020-05-28 10:20:17 -06:00
|
|
|
a unit (see `--length` for details)\n\
|
|
|
|
|
A negative value is valid and will seek from the end of the file.",
|
2020-06-07 14:37:17 +02:00
|
|
|
),
|
2020-04-29 08:22:03 +02:00
|
|
|
)
|
2020-05-20 09:35:43 -06:00
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("block_size")
|
|
|
|
|
.long("block-size")
|
|
|
|
|
.takes_value(true)
|
|
|
|
|
.value_name("SIZE")
|
2021-10-31 15:29:41 +01:00
|
|
|
.help(formatcp!(
|
|
|
|
|
"Sets the size of the `block` unit to SIZE (default is {}).\n\
|
2020-06-10 17:01:01 -06:00
|
|
|
Examples: --block-size=1024, --block-size=4kB",
|
2021-10-31 15:29:41 +01:00
|
|
|
DEFAULT_BLOCK_SIZE
|
|
|
|
|
)),
|
2020-05-20 09:35:43 -06:00
|
|
|
)
|
2019-03-26 23:24:55 +01:00
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("nosqueezing")
|
|
|
|
|
.short("v")
|
|
|
|
|
.long("no-squeezing")
|
|
|
|
|
.help(
|
|
|
|
|
"Displays all input data. Otherwise any number of groups of output \
|
|
|
|
|
lines which would be identical to the preceding group of lines, are \
|
|
|
|
|
replaced with a line comprised of a single asterisk.",
|
|
|
|
|
),
|
|
|
|
|
)
|
2019-01-11 22:10:20 +10:00
|
|
|
.arg(
|
2019-01-14 09:05:12 +01:00
|
|
|
Arg::with_name("color")
|
|
|
|
|
.long("color")
|
|
|
|
|
.takes_value(true)
|
2020-06-07 14:37:17 +02:00
|
|
|
.value_name("WHEN")
|
2019-01-14 09:05:12 +01:00
|
|
|
.possible_values(&["always", "auto", "never"])
|
|
|
|
|
.default_value("always")
|
2019-01-14 09:27:09 +01:00
|
|
|
.help(
|
|
|
|
|
"When to use colors. The auto-mode only displays colors if the output \
|
|
|
|
|
goes to an interactive terminal",
|
|
|
|
|
),
|
2019-03-05 21:59:52 +01:00
|
|
|
)
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("border")
|
|
|
|
|
.long("border")
|
|
|
|
|
.takes_value(true)
|
2020-06-07 14:37:17 +02:00
|
|
|
.value_name("STYLE")
|
2019-03-05 21:59:52 +01:00
|
|
|
.possible_values(&["unicode", "ascii", "none"])
|
|
|
|
|
.default_value("unicode")
|
2020-06-10 17:01:01 -06:00
|
|
|
.help(
|
|
|
|
|
"Whether to draw a border with Unicode characters, ASCII characters, \
|
|
|
|
|
or none at all",
|
|
|
|
|
),
|
2019-10-07 19:20:02 +01:00
|
|
|
)
|
|
|
|
|
.arg(
|
|
|
|
|
Arg::with_name("display_offset")
|
|
|
|
|
.short("o")
|
|
|
|
|
.long("display-offset")
|
|
|
|
|
.takes_value(true)
|
2020-06-07 14:37:17 +02:00
|
|
|
.value_name("N")
|
2020-06-10 17:01:01 -06:00
|
|
|
.help(
|
|
|
|
|
"Add N bytes to the displayed file position. The N argument can also \
|
2020-05-28 10:20:17 -06:00
|
|
|
include a unit (see `--length` for details)\n\
|
|
|
|
|
A negative value is valid and calculates an offset relative to the \
|
|
|
|
|
end of the file.",
|
2020-06-10 17:01:01 -06:00
|
|
|
),
|
2018-11-06 20:18:15 +01:00
|
|
|
);
|
2018-11-05 21:30:39 +01:00
|
|
|
|
2019-01-11 14:06:23 +00:00
|
|
|
let matches = app.get_matches_safe()?;
|
2018-11-05 21:30:39 +01:00
|
|
|
|
2018-11-05 23:21:54 +01:00
|
|
|
let stdin = io::stdin();
|
|
|
|
|
|
2020-06-10 16:00:54 -06:00
|
|
|
let mut reader = match matches.value_of("FILE") {
|
2020-04-29 08:22:03 +02:00
|
|
|
Some(filename) => Input::File(File::open(filename)?),
|
2020-04-30 09:08:41 +02:00
|
|
|
None => Input::Stdin(stdin.lock()),
|
2018-11-05 23:21:54 +01:00
|
|
|
};
|
2018-11-05 21:30:39 +01:00
|
|
|
|
2020-05-20 09:35:43 -06:00
|
|
|
let block_size = matches
|
|
|
|
|
.value_of("block_size")
|
2020-06-11 12:25:56 -06:00
|
|
|
.map(|bs| {
|
2021-11-10 18:52:52 +01:00
|
|
|
if let Some(hex_number) = try_parse_as_hex_number(bs) {
|
|
|
|
|
return hex_number.map_err(|e| anyhow!(e)).and_then(|x| {
|
|
|
|
|
PositiveI64::new(x)
|
|
|
|
|
.ok_or_else(|| anyhow!("block size argument must be positive"))
|
|
|
|
|
});
|
2021-11-07 22:10:21 +01:00
|
|
|
}
|
2021-11-07 22:10:21 +01:00
|
|
|
let (num, unit) = extract_num_and_unit_from(bs)?;
|
2021-11-07 22:19:08 +01:00
|
|
|
if let Unit::Block { custom_size: _ } = unit {
|
|
|
|
|
return Err(anyhow!(
|
|
|
|
|
"can not use 'block(s)' as a unit to specify block size"
|
|
|
|
|
));
|
|
|
|
|
};
|
2021-11-07 22:10:21 +01:00
|
|
|
num.checked_mul(unit.get_multiplier())
|
|
|
|
|
.ok_or_else(|| anyhow!(ByteOffsetParseError::UnitMultiplicationOverflow))
|
|
|
|
|
.and_then(|x| {
|
|
|
|
|
PositiveI64::new(x)
|
|
|
|
|
.ok_or_else(|| anyhow!("block size argument must be positive"))
|
|
|
|
|
})
|
2020-06-11 12:25:56 -06:00
|
|
|
})
|
|
|
|
|
.transpose()?
|
2021-10-31 15:29:41 +01:00
|
|
|
.unwrap_or_else(|| PositiveI64::new(DEFAULT_BLOCK_SIZE).unwrap());
|
2020-05-20 09:35:43 -06:00
|
|
|
|
|
|
|
|
let skip_arg = matches
|
|
|
|
|
.value_of("skip")
|
2020-06-11 12:25:56 -06:00
|
|
|
.map(|s| {
|
2020-05-28 10:20:17 -06:00
|
|
|
parse_byte_offset(s, block_size).context(anyhow!(
|
2020-06-11 12:25:56 -06:00
|
|
|
"failed to parse `--skip` arg {:?} as byte count",
|
|
|
|
|
s
|
|
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
.transpose()?;
|
2020-04-29 08:22:03 +02:00
|
|
|
|
2020-10-30 22:26:18 +00:00
|
|
|
let skip_offset = if let Some(ByteOffset { kind, value }) = skip_arg {
|
2020-05-28 10:20:17 -06:00
|
|
|
let value = value.into_inner();
|
2020-11-01 21:12:34 +01:00
|
|
|
reader
|
|
|
|
|
.seek(match kind {
|
|
|
|
|
ByteOffsetKind::ForwardFromBeginning | ByteOffsetKind::ForwardFromLastOffset => {
|
|
|
|
|
SeekFrom::Current(value)
|
|
|
|
|
}
|
|
|
|
|
ByteOffsetKind::BackwardFromEnd => SeekFrom::End(value.checked_neg().unwrap()),
|
|
|
|
|
})
|
|
|
|
|
.map_err(|_| {
|
|
|
|
|
anyhow!(
|
|
|
|
|
"Failed to jump to the desired input position. \
|
|
|
|
|
This could be caused by a negative offset that is too large or by \
|
|
|
|
|
an input that is not seek-able (e.g. if the input comes from a pipe)."
|
|
|
|
|
)
|
|
|
|
|
})?
|
2020-05-28 10:20:17 -06:00
|
|
|
} else {
|
|
|
|
|
0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let parse_byte_count = |s| -> Result<u64, AnyhowError> {
|
|
|
|
|
Ok(parse_byte_offset(s, block_size)?
|
2020-11-01 21:04:39 +01:00
|
|
|
.assume_forward_offset_from_start()?
|
|
|
|
|
.into())
|
2020-05-28 10:20:17 -06:00
|
|
|
};
|
2020-04-29 08:22:03 +02:00
|
|
|
|
2020-06-11 12:25:56 -06:00
|
|
|
let mut reader = if let Some(length) = matches
|
2020-05-20 09:45:14 -06:00
|
|
|
.value_of("length")
|
2020-06-11 12:25:56 -06:00
|
|
|
.or_else(|| matches.value_of("bytes"))
|
2021-07-02 19:34:06 +05:30
|
|
|
.or_else(|| matches.value_of("count"))
|
2020-06-11 12:25:56 -06:00
|
|
|
.map(|s| {
|
2020-11-01 21:04:39 +01:00
|
|
|
parse_byte_count(s).context(anyhow!(
|
|
|
|
|
"failed to parse `--length` arg {:?} as byte count",
|
|
|
|
|
s
|
|
|
|
|
))
|
2020-06-11 12:25:56 -06:00
|
|
|
})
|
|
|
|
|
.transpose()?
|
2020-05-20 09:35:43 -06:00
|
|
|
{
|
2020-05-28 10:20:17 -06:00
|
|
|
Box::new(reader.take(length))
|
2020-04-29 08:22:03 +02:00
|
|
|
} else {
|
|
|
|
|
reader.into_inner()
|
|
|
|
|
};
|
2019-02-04 17:04:26 -06:00
|
|
|
|
2019-01-14 09:05:12 +01:00
|
|
|
let show_color = match matches.value_of("color") {
|
|
|
|
|
Some("never") => false,
|
|
|
|
|
Some("auto") => atty::is(Stream::Stdout),
|
|
|
|
|
_ => true,
|
|
|
|
|
};
|
2019-01-11 22:10:20 +10:00
|
|
|
|
2019-03-05 21:59:52 +01:00
|
|
|
let border_style = match matches.value_of("border") {
|
|
|
|
|
Some("unicode") => BorderStyle::Unicode,
|
|
|
|
|
Some("ascii") => BorderStyle::Ascii,
|
|
|
|
|
_ => BorderStyle::None,
|
|
|
|
|
};
|
|
|
|
|
|
2019-03-26 23:24:55 +01:00
|
|
|
let squeeze = !matches.is_present("nosqueezing");
|
|
|
|
|
|
2020-10-30 22:26:18 +00:00
|
|
|
let display_offset: u64 = matches
|
2019-10-07 19:20:02 +01:00
|
|
|
.value_of("display_offset")
|
2020-06-11 12:25:56 -06:00
|
|
|
.map(|s| {
|
2020-05-28 10:20:17 -06:00
|
|
|
parse_byte_count(s).context(anyhow!(
|
2020-06-11 12:25:56 -06:00
|
|
|
"failed to parse `--display-offset` arg {:?} as byte count",
|
|
|
|
|
s
|
|
|
|
|
))
|
|
|
|
|
})
|
|
|
|
|
.transpose()?
|
2021-11-10 22:39:41 +01:00
|
|
|
.unwrap_or(0);
|
2019-10-07 19:20:02 +01:00
|
|
|
|
2018-11-05 21:38:29 +01:00
|
|
|
let stdout = io::stdout();
|
2019-07-14 18:18:13 +01:00
|
|
|
let mut stdout_lock = stdout.lock();
|
|
|
|
|
|
|
|
|
|
let mut printer = Printer::new(&mut stdout_lock, show_color, border_style, squeeze);
|
2020-10-30 22:26:18 +00:00
|
|
|
printer.display_offset(skip_offset + display_offset);
|
2020-06-10 13:00:10 -06:00
|
|
|
printer.print_all(&mut reader).map_err(|e| anyhow!(e))?;
|
2018-11-05 21:30:39 +01:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() {
|
2018-11-09 20:04:52 +01:00
|
|
|
// Enable ANSI support for Windows
|
|
|
|
|
#[cfg(windows)]
|
|
|
|
|
let _ = ansi_term::enable_ansi_support();
|
|
|
|
|
|
2018-11-05 21:30:39 +01:00
|
|
|
let result = run();
|
2019-01-11 14:07:27 +00:00
|
|
|
|
|
|
|
|
if let Err(err) = result {
|
2019-01-11 14:17:01 +00:00
|
|
|
if let Some(clap_err) = err.downcast_ref::<clap::Error>() {
|
2019-01-13 17:24:22 +00:00
|
|
|
match clap_err.kind {
|
|
|
|
|
// The exit code should not indicate an error for --help / --version
|
2021-04-22 23:15:35 -06:00
|
|
|
clap::ErrorKind::HelpDisplayed => {
|
|
|
|
|
eprint!("{}", clap_err); // Clap errors already have newlines
|
2019-01-13 17:24:22 +00:00
|
|
|
std::process::exit(0)
|
2021-07-11 20:00:06 +02:00
|
|
|
}
|
2021-04-22 23:15:35 -06:00
|
|
|
clap::ErrorKind::VersionDisplayed => {
|
|
|
|
|
// Version output in clap 2.33.1 (dep as of now) doesn't have a newline
|
|
|
|
|
// and the fix is not included even in the latest stable release
|
2021-11-10 22:39:41 +01:00
|
|
|
println!();
|
2021-04-22 23:15:35 -06:00
|
|
|
std::process::exit(0)
|
2021-07-11 20:00:06 +02:00
|
|
|
}
|
2019-01-13 17:24:22 +00:00
|
|
|
_ => (),
|
|
|
|
|
}
|
2019-01-11 14:17:01 +00:00
|
|
|
} else {
|
2020-06-10 13:00:10 -06:00
|
|
|
eprintln!("Error: {:?}", err);
|
2019-01-11 14:17:01 +00:00
|
|
|
}
|
2019-01-11 14:18:13 +00:00
|
|
|
std::process::exit(1);
|
2018-11-05 21:30:39 +01:00
|
|
|
}
|
|
|
|
|
}
|
2019-10-07 19:20:02 +01:00
|
|
|
|
2020-10-31 10:10:46 +01:00
|
|
|
#[derive(Clone, Copy, Debug, Default, Hash, Eq, Ord, PartialEq, PartialOrd)]
|
|
|
|
|
pub struct NonNegativeI64(i64);
|
|
|
|
|
|
|
|
|
|
impl NonNegativeI64 {
|
|
|
|
|
pub fn new(x: i64) -> Option<Self> {
|
|
|
|
|
if x.is_negative() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(Self(x))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn into_inner(self) -> i64 {
|
|
|
|
|
self.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-25 21:19:52 +09:00
|
|
|
impl From<NonNegativeI64> for u64 {
|
|
|
|
|
fn from(x: NonNegativeI64) -> u64 {
|
|
|
|
|
u64::try_from(x.0)
|
2020-10-31 10:10:46 +01:00
|
|
|
.expect("invariant broken: NonNegativeI64 should contain a non-negative i64 value")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-01 12:30:27 -06:00
|
|
|
#[derive(Clone, Copy, Debug, Default, Hash, Eq, Ord, PartialEq, PartialOrd)]
|
|
|
|
|
pub struct PositiveI64(i64);
|
|
|
|
|
|
|
|
|
|
impl PositiveI64 {
|
|
|
|
|
pub fn new(x: i64) -> Option<Self> {
|
2020-10-31 10:10:46 +01:00
|
|
|
if x < 1 {
|
2020-06-01 12:30:27 -06:00
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(Self(x))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn into_inner(self) -> i64 {
|
|
|
|
|
self.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-25 21:19:52 +09:00
|
|
|
impl From<PositiveI64> for u64 {
|
|
|
|
|
fn from(x: PositiveI64) -> u64 {
|
|
|
|
|
u64::try_from(x.0)
|
2020-06-01 12:30:27 -06:00
|
|
|
.expect("invariant broken: PositiveI64 should contain a positive i64 value")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-06 23:22:52 +01:00
|
|
|
#[derive(Debug, PartialEq)]
|
2021-11-01 21:15:00 +01:00
|
|
|
enum Unit {
|
|
|
|
|
Byte,
|
|
|
|
|
Kilobyte,
|
|
|
|
|
Megabyte,
|
|
|
|
|
Gigabyte,
|
|
|
|
|
Terabyte,
|
|
|
|
|
Kibibyte,
|
|
|
|
|
Mebibyte,
|
|
|
|
|
Gibibyte,
|
|
|
|
|
Tebibyte,
|
|
|
|
|
/// a customizable amount of bytes
|
|
|
|
|
Block {
|
|
|
|
|
custom_size: Option<NonZeroI64>,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Unit {
|
|
|
|
|
const fn get_multiplier(self) -> i64 {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Byte => 1,
|
2021-11-10 22:47:17 +01:00
|
|
|
Self::Kilobyte => 1000,
|
2022-03-25 21:19:52 +09:00
|
|
|
Self::Megabyte => 1_000_000,
|
|
|
|
|
Self::Gigabyte => 1_000_000_000,
|
|
|
|
|
Self::Terabyte => 1_000_000_000_000,
|
2021-11-10 22:47:17 +01:00
|
|
|
Self::Kibibyte => 1 << 10,
|
|
|
|
|
Self::Mebibyte => 1 << 20,
|
|
|
|
|
Self::Gibibyte => 1 << 30,
|
|
|
|
|
Self::Tebibyte => 1 << 40,
|
2021-11-01 21:15:00 +01:00
|
|
|
Self::Block {
|
|
|
|
|
custom_size: Some(size),
|
|
|
|
|
} => size.get(),
|
|
|
|
|
Self::Block { custom_size: None } => DEFAULT_BLOCK_SIZE,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-11 12:25:56 -06:00
|
|
|
const HEX_PREFIX: &str = "0x";
|
|
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
|
|
|
enum ByteOffsetKind {
|
|
|
|
|
ForwardFromBeginning,
|
|
|
|
|
ForwardFromLastOffset,
|
|
|
|
|
BackwardFromEnd,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
|
|
|
|
struct ByteOffset {
|
2020-10-31 10:10:46 +01:00
|
|
|
value: NonNegativeI64,
|
2020-05-28 10:20:17 -06:00
|
|
|
kind: ByteOffsetKind,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, ThisError)]
|
2020-11-01 21:04:39 +01:00
|
|
|
#[error(
|
|
|
|
|
"negative offset specified, but only positive offsets (counts) are accepted in this context"
|
|
|
|
|
)]
|
2020-05-28 10:20:17 -06:00
|
|
|
struct NegativeOffsetSpecifiedError;
|
|
|
|
|
|
|
|
|
|
impl ByteOffset {
|
2020-11-01 21:04:39 +01:00
|
|
|
fn assume_forward_offset_from_start(
|
|
|
|
|
&self,
|
|
|
|
|
) -> Result<NonNegativeI64, NegativeOffsetSpecifiedError> {
|
2020-05-28 10:20:17 -06:00
|
|
|
let &Self { value, kind } = self;
|
|
|
|
|
match kind {
|
|
|
|
|
ByteOffsetKind::ForwardFromBeginning | ByteOffsetKind::ForwardFromLastOffset => {
|
|
|
|
|
Ok(value)
|
|
|
|
|
}
|
|
|
|
|
ByteOffsetKind::BackwardFromEnd => Err(NegativeOffsetSpecifiedError),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-11 12:25:56 -06:00
|
|
|
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
|
2020-05-28 10:20:17 -06:00
|
|
|
enum ByteOffsetParseError {
|
2020-06-11 12:25:56 -06:00
|
|
|
#[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(
|
|
|
|
|
"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),
|
2022-05-17 15:12:45 +08:00
|
|
|
#[error("count multiplied by the unit overflowed a signed 64-bit integer; are you sure it should be that big?")]
|
2020-06-11 12:25:56 -06:00
|
|
|
UnitMultiplicationOverflow,
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
fn parse_byte_offset(n: &str, block_size: PositiveI64) -> Result<ByteOffset, ByteOffsetParseError> {
|
|
|
|
|
use ByteOffsetParseError::*;
|
2020-05-18 13:54:43 -06:00
|
|
|
|
2021-11-10 18:52:52 +01:00
|
|
|
let (n, kind) = process_sign_of(n)?;
|
2020-05-20 09:32:10 -06:00
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
let into_byte_offset = |value| {
|
|
|
|
|
Ok(ByteOffset {
|
2020-10-31 10:10:46 +01:00
|
|
|
value: NonNegativeI64::new(value).unwrap(),
|
2020-05-28 10:20:17 -06:00
|
|
|
kind,
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
2021-11-10 18:52:52 +01:00
|
|
|
if let Some(hex_number) = try_parse_as_hex_number(n) {
|
|
|
|
|
return hex_number.map(into_byte_offset)?;
|
2019-10-07 19:20:02 +01:00
|
|
|
}
|
2020-05-20 09:32:10 -06:00
|
|
|
|
2021-11-06 23:46:33 +01:00
|
|
|
let (num, mut unit) = extract_num_and_unit_from(n)?;
|
|
|
|
|
if let Unit::Block { custom_size: None } = unit {
|
|
|
|
|
unit = Unit::Block {
|
|
|
|
|
custom_size: Some(
|
|
|
|
|
NonZeroI64::new(block_size.into_inner()).expect("PositiveI64 was zero"),
|
|
|
|
|
),
|
|
|
|
|
};
|
2020-06-11 12:25:56 -06:00
|
|
|
}
|
2021-11-06 23:46:33 +01:00
|
|
|
|
|
|
|
|
num.checked_mul(unit.get_multiplier())
|
2021-11-10 22:39:41 +01:00
|
|
|
.ok_or(UnitMultiplicationOverflow)
|
2021-11-06 23:46:33 +01:00
|
|
|
.and_then(into_byte_offset)
|
2020-05-20 09:32:10 -06:00
|
|
|
}
|
|
|
|
|
|
2021-11-06 23:22:52 +01:00
|
|
|
/// Takes a string containing a base-10 number and an optional unit, and returns them with their proper types.
|
|
|
|
|
/// The unit must directly follow the number (e.g. no whitespace is allowed between them).
|
|
|
|
|
/// When no unit is given, [Unit::Byte] is assumed.
|
|
|
|
|
/// When the unit is [Unit::Block], it is returned without custom size.
|
|
|
|
|
/// No normalization is performed, that is "1024" is extracted to (1024, Byte), not (1, Kibibyte).
|
|
|
|
|
fn extract_num_and_unit_from(n: &str) -> Result<(i64, Unit), ByteOffsetParseError> {
|
|
|
|
|
use ByteOffsetParseError::*;
|
|
|
|
|
if n.is_empty() {
|
|
|
|
|
return Err(Empty);
|
|
|
|
|
}
|
|
|
|
|
match n.chars().position(|c| !c.is_ascii_digit()) {
|
|
|
|
|
Some(unit_begin_idx) => {
|
|
|
|
|
let (n, raw_unit) = n.split_at(unit_begin_idx);
|
|
|
|
|
let unit = match raw_unit.to_lowercase().as_str() {
|
|
|
|
|
"" => Unit::Byte, // no "b" => Byte to allow hex nums with units
|
|
|
|
|
"kb" => Unit::Kilobyte,
|
|
|
|
|
"mb" => Unit::Megabyte,
|
|
|
|
|
"gb" => Unit::Gigabyte,
|
|
|
|
|
"tb" => Unit::Terabyte,
|
|
|
|
|
"kib" => Unit::Kibibyte,
|
|
|
|
|
"mib" => Unit::Mebibyte,
|
|
|
|
|
"gib" => Unit::Gibibyte,
|
|
|
|
|
"tib" => Unit::Tebibyte,
|
|
|
|
|
"block" | "blocks" => Unit::Block { custom_size: None },
|
|
|
|
|
_ => {
|
|
|
|
|
return if n.is_empty() {
|
|
|
|
|
Err(InvalidNumAndUnit(raw_unit.to_string()))
|
|
|
|
|
} else {
|
|
|
|
|
Err(InvalidUnit(raw_unit.to_string()))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let num = n.parse::<i64>().map_err(|e| {
|
|
|
|
|
if n.is_empty() {
|
|
|
|
|
EmptyWithUnit(raw_unit.to_owned())
|
|
|
|
|
} else {
|
|
|
|
|
ParseNum(e)
|
|
|
|
|
}
|
|
|
|
|
})?;
|
|
|
|
|
Ok((num, unit))
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
// no unit part
|
|
|
|
|
let num = n.parse::<i64>().map_err(ParseNum)?;
|
|
|
|
|
Ok((num, Unit::Byte))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 18:52:52 +01:00
|
|
|
/// Extracts a [ByteOffsetKind] based on the sign at the beginning of the given string.
|
|
|
|
|
/// Returns the input string without the sign (or an equal string if there wasn't any sign).
|
|
|
|
|
fn process_sign_of(n: &str) -> Result<(&str, ByteOffsetKind), ByteOffsetParseError> {
|
|
|
|
|
use ByteOffsetParseError::*;
|
|
|
|
|
let mut chars = n.chars();
|
|
|
|
|
let next_char = chars.next();
|
|
|
|
|
let check_empty_after_sign = || {
|
|
|
|
|
if chars.clone().next().is_none() {
|
|
|
|
|
Err(EmptyAfterSign)
|
|
|
|
|
} else {
|
|
|
|
|
Ok(chars.as_str())
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
match next_char {
|
|
|
|
|
Some('+') => Ok((
|
|
|
|
|
check_empty_after_sign()?,
|
|
|
|
|
ByteOffsetKind::ForwardFromLastOffset,
|
|
|
|
|
)),
|
|
|
|
|
Some('-') => Ok((check_empty_after_sign()?, ByteOffsetKind::BackwardFromEnd)),
|
|
|
|
|
None => Err(Empty),
|
|
|
|
|
_ => Ok((n, ByteOffsetKind::ForwardFromBeginning)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 18:52:52 +01:00
|
|
|
/// If `n` starts with a hex prefix, its remaining part is returned as some number (if possible),
|
|
|
|
|
/// otherwise None is returned.
|
|
|
|
|
fn try_parse_as_hex_number(n: &str) -> Option<Result<i64, ByteOffsetParseError>> {
|
|
|
|
|
use ByteOffsetParseError::*;
|
|
|
|
|
n.strip_prefix(HEX_PREFIX).map(|num| {
|
|
|
|
|
let mut chars = num.chars();
|
|
|
|
|
match chars.next() {
|
|
|
|
|
Some(c @ '+') | Some(c @ '-') => {
|
|
|
|
|
return if chars.next().is_none() {
|
|
|
|
|
Err(EmptyAfterSign)
|
|
|
|
|
} else {
|
|
|
|
|
Err(SignFoundAfterHexPrefix(c))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => (),
|
|
|
|
|
}
|
|
|
|
|
i64::from_str_radix(num, 16).map_err(ParseNum)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-01 21:15:00 +01:00
|
|
|
#[test]
|
2021-11-06 23:22:52 +01:00
|
|
|
fn unit_multipliers() {
|
2021-11-01 21:15:00 +01:00
|
|
|
use Unit::*;
|
|
|
|
|
assert_eq!(Kilobyte.get_multiplier(), 1000 * Byte.get_multiplier());
|
|
|
|
|
assert_eq!(Megabyte.get_multiplier(), 1000 * Kilobyte.get_multiplier());
|
|
|
|
|
assert_eq!(Gigabyte.get_multiplier(), 1000 * Megabyte.get_multiplier());
|
|
|
|
|
assert_eq!(Terabyte.get_multiplier(), 1000 * Gigabyte.get_multiplier());
|
|
|
|
|
|
|
|
|
|
assert_eq!(Kibibyte.get_multiplier(), 1024 * Byte.get_multiplier());
|
|
|
|
|
assert_eq!(Mebibyte.get_multiplier(), 1024 * Kibibyte.get_multiplier());
|
|
|
|
|
assert_eq!(Gibibyte.get_multiplier(), 1024 * Mebibyte.get_multiplier());
|
|
|
|
|
assert_eq!(Tebibyte.get_multiplier(), 1024 * Gibibyte.get_multiplier());
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 18:52:52 +01:00
|
|
|
#[test]
|
|
|
|
|
fn test_process_sign() {
|
|
|
|
|
use ByteOffsetKind::*;
|
|
|
|
|
use ByteOffsetParseError::*;
|
|
|
|
|
assert_eq!(process_sign_of("123"), Ok(("123", ForwardFromBeginning)));
|
|
|
|
|
assert_eq!(process_sign_of("+123"), Ok(("123", ForwardFromLastOffset)));
|
|
|
|
|
assert_eq!(process_sign_of("-123"), Ok(("123", BackwardFromEnd)));
|
|
|
|
|
assert_eq!(process_sign_of("-"), Err(EmptyAfterSign));
|
|
|
|
|
assert_eq!(process_sign_of("+"), Err(EmptyAfterSign));
|
|
|
|
|
assert_eq!(process_sign_of(""), Err(Empty));
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-10 18:52:52 +01:00
|
|
|
#[test]
|
|
|
|
|
fn test_parse_as_hex() {
|
|
|
|
|
assert_eq!(try_parse_as_hex_number("73"), None);
|
|
|
|
|
assert_eq!(try_parse_as_hex_number("0x1337"), Some(Ok(0x1337)));
|
|
|
|
|
assert!(if let Some(Err(_)) = try_parse_as_hex_number("0xnope") {
|
|
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
});
|
|
|
|
|
assert!(if let Some(Err(_)) = try_parse_as_hex_number("0x-1") {
|
|
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2021-11-06 23:22:52 +01:00
|
|
|
#[test]
|
|
|
|
|
fn extract_num_and_unit() {
|
|
|
|
|
use ByteOffsetParseError::*;
|
|
|
|
|
use Unit::*;
|
|
|
|
|
// byte is default unit
|
|
|
|
|
assert_eq!(extract_num_and_unit_from("4"), Ok((4, Byte)));
|
|
|
|
|
// blocks are returned without customization
|
|
|
|
|
assert_eq!(
|
|
|
|
|
extract_num_and_unit_from("2blocks"),
|
|
|
|
|
Ok((2, Block { custom_size: None }))
|
|
|
|
|
);
|
|
|
|
|
// no normalization is performed
|
|
|
|
|
assert_eq!(extract_num_and_unit_from("1024kb"), Ok((1024, Kilobyte)));
|
|
|
|
|
|
|
|
|
|
// unit without number results in error
|
|
|
|
|
assert_eq!(
|
|
|
|
|
extract_num_and_unit_from("gib"),
|
|
|
|
|
Err(EmptyWithUnit("gib".to_string()))
|
|
|
|
|
);
|
|
|
|
|
// empty string results in error
|
|
|
|
|
assert_eq!(extract_num_and_unit_from(""), Err(Empty));
|
|
|
|
|
// an invalid unit results in an error
|
|
|
|
|
assert_eq!(
|
|
|
|
|
extract_num_and_unit_from("25litres"),
|
|
|
|
|
Err(InvalidUnit("litres".to_string()))
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-20 09:32:10 -06:00
|
|
|
#[test]
|
2020-05-28 10:20:17 -06:00
|
|
|
fn test_parse_byte_offset() {
|
|
|
|
|
use ByteOffsetParseError::*;
|
2020-06-11 12:25:56 -06:00
|
|
|
|
2020-05-20 09:32:10 -06:00
|
|
|
macro_rules! success {
|
2020-05-28 10:20:17 -06:00
|
|
|
($input: expr, $expected_kind: ident $expected_value: expr) => {
|
2021-10-31 15:29:41 +01:00
|
|
|
success!($input, $expected_kind $expected_value; block_size: DEFAULT_BLOCK_SIZE)
|
2020-05-20 09:35:43 -06:00
|
|
|
};
|
2020-05-28 10:20:17 -06:00
|
|
|
($input: expr, $expected_kind: ident $expected_value: expr; block_size: $block_size: expr) => {
|
2020-06-01 12:30:27 -06:00
|
|
|
assert_eq!(
|
2020-05-28 10:20:17 -06:00
|
|
|
parse_byte_offset($input, PositiveI64::new($block_size).unwrap()),
|
|
|
|
|
Ok(
|
|
|
|
|
ByteOffset {
|
2020-10-31 10:10:46 +01:00
|
|
|
value: NonNegativeI64::new($expected_value).unwrap(),
|
2020-05-28 10:20:17 -06:00
|
|
|
kind: ByteOffsetKind::$expected_kind,
|
|
|
|
|
}
|
|
|
|
|
),
|
2020-06-01 12:30:27 -06:00
|
|
|
);
|
2020-05-20 09:32:10 -06:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macro_rules! error {
|
2020-06-11 12:25:56 -06:00
|
|
|
($input: expr, $expected_err: expr) => {
|
2020-06-01 12:30:27 -06:00
|
|
|
assert_eq!(
|
2021-10-31 15:29:41 +01:00
|
|
|
parse_byte_offset($input, PositiveI64::new(DEFAULT_BLOCK_SIZE).unwrap()),
|
2020-06-11 12:25:56 -06:00
|
|
|
Err($expected_err),
|
2020-06-01 12:30:27 -06:00
|
|
|
);
|
2020-05-20 09:32:10 -06:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
success!("0", ForwardFromBeginning 0);
|
|
|
|
|
success!("1", ForwardFromBeginning 1);
|
|
|
|
|
success!("1", ForwardFromBeginning 1);
|
|
|
|
|
success!("100", ForwardFromBeginning 100);
|
|
|
|
|
success!("+100", ForwardFromLastOffset 100);
|
2020-05-20 09:32:10 -06:00
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
success!("0x0", ForwardFromBeginning 0);
|
|
|
|
|
success!("0xf", ForwardFromBeginning 15);
|
|
|
|
|
success!("0xdeadbeef", ForwardFromBeginning 3_735_928_559);
|
2020-06-11 12:25:56 -06:00
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
success!("1KB", ForwardFromBeginning 1000);
|
|
|
|
|
success!("2MB", ForwardFromBeginning 2000000);
|
|
|
|
|
success!("3GB", ForwardFromBeginning 3000000000);
|
|
|
|
|
success!("4TB", ForwardFromBeginning 4000000000000);
|
|
|
|
|
success!("+4TB", ForwardFromLastOffset 4000000000000);
|
2020-05-20 09:32:10 -06:00
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
success!("1GiB", ForwardFromBeginning 1073741824);
|
|
|
|
|
success!("2TiB", ForwardFromBeginning 2199023255552);
|
|
|
|
|
success!("+2TiB", ForwardFromLastOffset 2199023255552);
|
2020-05-20 09:32:10 -06:00
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
success!("0xff", ForwardFromBeginning 255);
|
|
|
|
|
success!("0xEE", ForwardFromBeginning 238);
|
|
|
|
|
success!("+0xFF", ForwardFromLastOffset 255);
|
2020-05-20 09:32:10 -06:00
|
|
|
|
2020-05-28 10:20:17 -06:00
|
|
|
success!("1block", ForwardFromBeginning 512; block_size: 512);
|
|
|
|
|
success!("2block", ForwardFromBeginning 1024; block_size: 512);
|
|
|
|
|
success!("1block", ForwardFromBeginning 4; block_size: 4);
|
|
|
|
|
success!("2block", ForwardFromBeginning 8; block_size: 4);
|
2020-05-20 09:35:43 -06:00
|
|
|
|
2020-05-20 09:32:10 -06:00
|
|
|
// empty string is invalid
|
2020-06-11 12:25:56 -06:00
|
|
|
error!("", Empty);
|
2020-05-20 09:32:10 -06:00
|
|
|
// These are also bad.
|
2020-06-11 12:25:56 -06:00
|
|
|
error!("+", EmptyAfterSign);
|
2020-05-28 10:20:17 -06:00
|
|
|
error!("-", EmptyAfterSign);
|
2020-06-11 12:25:56 -06:00
|
|
|
error!("K", InvalidNumAndUnit("K".to_owned()));
|
|
|
|
|
error!("k", InvalidNumAndUnit("k".to_owned()));
|
|
|
|
|
error!("m", InvalidNumAndUnit("m".to_owned()));
|
|
|
|
|
error!("block", EmptyWithUnit("block".to_owned()));
|
2020-05-20 09:32:10 -06:00
|
|
|
// leading/trailing space is invalid
|
2020-06-11 12:25:56 -06:00
|
|
|
error!(" 0", InvalidNumAndUnit(" 0".to_owned()));
|
|
|
|
|
error!("0 ", InvalidUnit(" ".to_owned()));
|
2020-05-28 10:20:17 -06:00
|
|
|
// Signs after the hex prefix make no sense
|
2020-06-11 12:25:56 -06:00
|
|
|
error!("0x-12", SignFoundAfterHexPrefix('-'));
|
2020-05-20 09:32:10 -06:00
|
|
|
// This was previously accepted but shouldn't be.
|
2020-06-11 12:25:56 -06:00
|
|
|
error!("0x+12", SignFoundAfterHexPrefix('+'));
|
2020-05-20 09:32:10 -06:00
|
|
|
// invalid suffix
|
2020-06-11 12:25:56 -06:00
|
|
|
error!("1234asdf", InvalidUnit("asdf".to_owned()));
|
2020-05-20 09:32:10 -06:00
|
|
|
// bad numbers
|
2020-06-11 12:25:56 -06:00
|
|
|
error!("asdf1234", InvalidNumAndUnit("asdf1234".to_owned()));
|
|
|
|
|
error!("a1s2d3f4", InvalidNumAndUnit("a1s2d3f4".to_owned()));
|
2020-05-20 09:32:10 -06:00
|
|
|
// multiplication overflows u64
|
2020-06-11 12:25:56 -06:00
|
|
|
error!("20000000TiB", UnitMultiplicationOverflow);
|
|
|
|
|
|
2020-11-01 21:04:39 +01:00
|
|
|
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
|
|
|
|
|
// same as trying to do the parse directly.
|
|
|
|
|
Err(ParseNum(e)) => e == "99999999999999999999".parse::<i64>().unwrap_err(),
|
|
|
|
|
_ => false,
|
|
|
|
|
}
|
|
|
|
|
);
|
2019-10-07 19:20:02 +01:00
|
|
|
}
|