mirror of
https://github.com/sharkdp/hexyl.git
synced 2026-10-26 20:16:34 +00:00
769 lines
23 KiB
Rust
769 lines
23 KiB
Rust
use std::fs::File;
|
||
use std::io::{self, prelude::*, BufWriter, SeekFrom};
|
||
use std::num::{NonZeroI64, NonZeroU64};
|
||
use std::path::PathBuf;
|
||
|
||
use clap::builder::ArgPredicate;
|
||
use clap::{ArgAction, Parser, ValueEnum};
|
||
|
||
use anyhow::{anyhow, bail, Context, Result};
|
||
|
||
use const_format::formatcp;
|
||
|
||
use thiserror::Error as ThisError;
|
||
|
||
use terminal_size::terminal_size;
|
||
|
||
use hexyl::{Base, BorderStyle, CharacterTable, Endianness, Input, PrinterBuilder};
|
||
|
||
use hexyl::{
|
||
COLOR_ASCII_OTHER, COLOR_ASCII_PRINTABLE, COLOR_ASCII_WHITESPACE, COLOR_NONASCII, COLOR_NULL,
|
||
COLOR_RESET,
|
||
};
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|
||
|
||
const DEFAULT_BLOCK_SIZE: i64 = 512;
|
||
|
||
const LENGTH_HELP_TEXT: &str = "Only read N bytes from the input. The N argument can also include \
|
||
a unit with a decimal prefix (kB, MB, ..) or binary prefix (kiB, \
|
||
MiB, ..), or can be specified using a hex number. The short \
|
||
option '-l' can be used as an alias.
|
||
Examples: --length=64, --length=4KiB, --length=0xff";
|
||
|
||
const SKIP_HELP_TEXT: &str = "Skip the first N bytes of the input. The N argument can also \
|
||
include a unit (see `--length` for details).
|
||
A negative value is valid and will seek from the end of the file.";
|
||
|
||
const BLOCK_SIZE_HELP_TEXT: &str = "Sets the size of the `block` unit to SIZE.
|
||
Examples: --block-size=1024, --block-size=4kB";
|
||
|
||
const DISPLAY_OFFSET_HELP_TEXT: &str = "Add N bytes to the displayed file position. The N \
|
||
argument can also include a unit (see `--length` for \
|
||
details).
|
||
A negative value is valid and calculates an offset relative to the end of the file.";
|
||
|
||
const TERMINAL_WIDTH_HELP_TEXT: &str = "Sets the number of terminal columns to be displayed.
|
||
Since the terminal width may not be an evenly divisible by the width per hex data column, this \
|
||
will use the greatest number of hex data panels that can \
|
||
fit in the requested width but still leave some space to \
|
||
the right.
|
||
Cannot be used with other width-setting options.";
|
||
|
||
#[derive(Debug, Parser)]
|
||
#[command(version, about, max_term_width(90))]
|
||
struct Opt {
|
||
/// The file to display. If no FILE argument is given, read from STDIN.
|
||
#[arg(value_name("FILE"))]
|
||
file: Option<PathBuf>,
|
||
|
||
#[arg(
|
||
help(LENGTH_HELP_TEXT),
|
||
short('n'),
|
||
long,
|
||
visible_short_alias('c'),
|
||
visible_alias("bytes"),
|
||
short_alias('l'),
|
||
value_name("N")
|
||
)]
|
||
length: Option<String>,
|
||
|
||
#[arg(help(SKIP_HELP_TEXT), short, long, value_name("N"))]
|
||
skip: Option<String>,
|
||
|
||
#[arg(
|
||
help(BLOCK_SIZE_HELP_TEXT),
|
||
long,
|
||
default_value(formatcp!("{DEFAULT_BLOCK_SIZE}")),
|
||
value_name("SIZE")
|
||
)]
|
||
block_size: String,
|
||
|
||
/// 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.
|
||
#[arg(short('v'), long)]
|
||
no_squeezing: bool,
|
||
|
||
/// When to use colors.
|
||
#[arg(
|
||
long,
|
||
value_enum,
|
||
default_value_t,
|
||
value_name("WHEN"),
|
||
default_value_if("plain", ArgPredicate::IsPresent, Some("never"))
|
||
)]
|
||
color: ColorWhen,
|
||
|
||
/// Whether to draw a border.
|
||
#[arg(
|
||
long,
|
||
value_enum,
|
||
default_value_t,
|
||
value_name("STYLE"),
|
||
default_value_if("plain", ArgPredicate::IsPresent, Some("none"))
|
||
)]
|
||
border: BorderStyle,
|
||
|
||
/// Display output with --no-characters, --no-position, --border=none, and
|
||
/// --color=never.
|
||
#[arg(short, long)]
|
||
plain: bool,
|
||
|
||
/// Do not show the character panel on the right.
|
||
#[arg(long)]
|
||
no_characters: bool,
|
||
|
||
/// Show the character panel on the right. This is the default, unless
|
||
/// --no-characters has been specified.
|
||
#[arg(
|
||
short('C'),
|
||
long,
|
||
action(ArgAction::SetTrue),
|
||
overrides_with("no_characters")
|
||
)]
|
||
characters: (),
|
||
|
||
/// Defines how bytes are mapped to characters.
|
||
#[arg(long, value_enum, default_value_t, value_name("FORMAT"))]
|
||
character_table: CharacterTable,
|
||
|
||
/// Whether to display the position panel on the left.
|
||
#[arg(short('P'), long)]
|
||
no_position: bool,
|
||
|
||
#[arg(
|
||
help(DISPLAY_OFFSET_HELP_TEXT),
|
||
short('o'),
|
||
long,
|
||
default_value("0"),
|
||
value_name("N")
|
||
)]
|
||
display_offset: String,
|
||
|
||
/// Sets the number of hex data panels to be displayed. `--panels=auto` will
|
||
/// display the maximum number of hex data panels based on the current
|
||
/// terminal width. By default, hexyl will show two panels, unless the
|
||
/// terminal is not wide enough for that.
|
||
#[arg(long, value_name("N"))]
|
||
panels: Option<String>,
|
||
|
||
/// Number of bytes/octets that should be grouped together. You can use the
|
||
/// '--endianness' option to control the ordering of the bytes within a
|
||
/// group. '--groupsize' can be used as an alias (xxd-compatibility).
|
||
#[arg(
|
||
short('g'),
|
||
long,
|
||
value_enum,
|
||
default_value_t,
|
||
alias("groupsize"),
|
||
value_name("N")
|
||
)]
|
||
group_size: GroupSize,
|
||
|
||
/// Whether to print out groups in little-endian or big-endian format. This
|
||
/// option only has an effect if the '--group-size' is larger than 1. '-e'
|
||
/// can be used as an alias for '--endianness=little'.
|
||
#[arg(long, value_enum, default_value_t, value_name("FORMAT"))]
|
||
endianness: Endianness,
|
||
|
||
/// An alias for '--endianness=little'.
|
||
#[arg(short('e'), hide(true), overrides_with("endianness"))]
|
||
little_endian_format: bool,
|
||
|
||
/// Sets the base used for the bytes. The possible options are binary,
|
||
/// octal, decimal, and hexadecimal.
|
||
#[arg(short('b'), long, default_value("hexadecimal"), value_name("B"))]
|
||
base: String,
|
||
|
||
#[arg(
|
||
help(TERMINAL_WIDTH_HELP_TEXT),
|
||
long,
|
||
value_name("N"),
|
||
conflicts_with("panels")
|
||
)]
|
||
terminal_width: Option<NonZeroU64>,
|
||
|
||
/// Print a table showing how different types of bytes are colored.
|
||
#[arg(short('t'), long)]
|
||
print_color_table: bool,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default, ValueEnum)]
|
||
enum ColorWhen {
|
||
/// Always use colorized output.
|
||
#[default]
|
||
Always,
|
||
|
||
/// Only displays colors if the output goes to an interactive terminal.
|
||
Auto,
|
||
|
||
/// Do not use colorized output.
|
||
Never,
|
||
|
||
/// Override the NO_COLOR environment variable.
|
||
Force,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default, ValueEnum)]
|
||
enum GroupSize {
|
||
/// Grouped together every byte/octet.
|
||
#[default]
|
||
#[value(name = "1")]
|
||
One,
|
||
|
||
/// Grouped together every 2 bytes/octets.
|
||
#[value(name = "2")]
|
||
Two,
|
||
|
||
/// Grouped together every 4 bytes/octets.
|
||
#[value(name = "4")]
|
||
Four,
|
||
|
||
/// Grouped together every 8 bytes/octets.
|
||
#[value(name = "8")]
|
||
Eight,
|
||
}
|
||
|
||
impl From<GroupSize> for u8 {
|
||
fn from(number: GroupSize) -> Self {
|
||
match number {
|
||
GroupSize::One => 1,
|
||
GroupSize::Two => 2,
|
||
GroupSize::Four => 4,
|
||
GroupSize::Eight => 8,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn run() -> Result<()> {
|
||
let opt = Opt::parse();
|
||
|
||
if opt.print_color_table {
|
||
return print_color_table().map_err(|e| anyhow!(e));
|
||
}
|
||
|
||
let stdin = io::stdin();
|
||
|
||
let mut reader = match opt.file {
|
||
Some(filename) => {
|
||
if filename.is_dir() {
|
||
bail!("'{}' is a directory.", filename.to_string_lossy());
|
||
}
|
||
let file = File::open(&filename)?;
|
||
|
||
Input::File(file)
|
||
}
|
||
None => Input::Stdin(stdin.lock()),
|
||
};
|
||
|
||
if let Some(hex_number) = try_parse_as_hex_number(&opt.block_size) {
|
||
return hex_number
|
||
.map_err(|e| anyhow!(e))
|
||
.and_then(|x| {
|
||
PositiveI64::new(x).ok_or_else(|| anyhow!("block size argument must be positive"))
|
||
})
|
||
.map(|_| ());
|
||
}
|
||
let (num, unit) = extract_num_and_unit_from(&opt.block_size)?;
|
||
if let Unit::Block { custom_size: _ } = unit {
|
||
return Err(anyhow!(
|
||
"can not use 'block(s)' as a unit to specify block size"
|
||
));
|
||
};
|
||
let block_size = 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"))
|
||
})?;
|
||
|
||
let skip_arg = opt
|
||
.skip
|
||
.as_ref()
|
||
.map(|s| {
|
||
parse_byte_offset(s, block_size).context(anyhow!(
|
||
"failed to parse `--skip` arg {:?} as byte count",
|
||
s
|
||
))
|
||
})
|
||
.transpose()?;
|
||
|
||
let skip_offset = if let Some(ByteOffset { kind, value }) = skip_arg {
|
||
let value = value.into_inner();
|
||
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)."
|
||
)
|
||
})?
|
||
} else {
|
||
0
|
||
};
|
||
|
||
let parse_byte_count = |s| -> Result<u64> {
|
||
Ok(parse_byte_offset(s, block_size)?
|
||
.assume_forward_offset_from_start()?
|
||
.into())
|
||
};
|
||
|
||
let mut reader = if let Some(ref length) = opt.length {
|
||
let length = parse_byte_count(length).context(anyhow!(
|
||
"failed to parse `--length` arg {:?} as byte count",
|
||
length
|
||
))?;
|
||
Box::new(reader.take(length))
|
||
} else {
|
||
reader.into_inner()
|
||
};
|
||
|
||
let no_color = std::env::var_os("NO_COLOR").is_some();
|
||
let show_color = match opt.color {
|
||
ColorWhen::Never => false,
|
||
ColorWhen::Always => !no_color,
|
||
ColorWhen::Force => true,
|
||
ColorWhen::Auto => {
|
||
if no_color {
|
||
false
|
||
} else {
|
||
supports_color::on(supports_color::Stream::Stdout)
|
||
.map(|level| level.has_basic)
|
||
.unwrap_or(false)
|
||
}
|
||
}
|
||
};
|
||
|
||
let border_style = opt.border;
|
||
|
||
let &squeeze = &!opt.no_squeezing;
|
||
|
||
let show_char_panel = !opt.no_characters && !opt.plain;
|
||
|
||
let show_position_panel = !opt.no_position && !opt.plain;
|
||
|
||
let display_offset: u64 = parse_byte_count(&opt.display_offset).context(anyhow!(
|
||
"failed to parse `--display-offset` arg {:?} as byte count",
|
||
opt.display_offset
|
||
))?;
|
||
|
||
let max_panels_fn = |terminal_width: u64, base_digits: u64, group_size: u64| {
|
||
let offset = if show_position_panel { 10 } else { 1 };
|
||
let col_width = if show_char_panel {
|
||
((8 / group_size) * (base_digits * group_size + 1)) + 2 + 8
|
||
} else {
|
||
((8 / group_size) * (base_digits * group_size + 1)) + 2
|
||
};
|
||
if (terminal_width - offset) / col_width < 1 {
|
||
1
|
||
} else {
|
||
(terminal_width - offset) / col_width
|
||
}
|
||
};
|
||
|
||
let base = if let Ok(base_num) = opt.base.parse::<u8>() {
|
||
match base_num {
|
||
2 => Ok(Base::Binary),
|
||
8 => Ok(Base::Octal),
|
||
10 => Ok(Base::Decimal),
|
||
16 => Ok(Base::Hexadecimal),
|
||
_ => Err(anyhow!(
|
||
"The number provided is not a valid base. Valid bases are 2, 8, 10, and 16."
|
||
)),
|
||
}
|
||
} else {
|
||
match opt.base.as_str() {
|
||
"b" | "bin" | "binary" => Ok(Base::Binary),
|
||
"o" | "oct" | "octal" => Ok(Base::Octal),
|
||
"d" | "dec" | "decimal" => Ok(Base::Decimal),
|
||
"x" | "hex" | "hexadecimal" => Ok(Base::Hexadecimal),
|
||
_ => Err(anyhow!(
|
||
"The base provided is not valid. Valid bases are \"b\", \"o\", \"d\", and \"x\"."
|
||
)),
|
||
}
|
||
}?;
|
||
|
||
let base_digits = match base {
|
||
Base::Binary => 8,
|
||
Base::Octal => 3,
|
||
Base::Decimal => 3,
|
||
Base::Hexadecimal => 2,
|
||
};
|
||
|
||
let group_size = u8::from(opt.group_size);
|
||
|
||
let terminal_width = terminal_size().map(|s| s.0 .0 as u64).unwrap_or(80);
|
||
|
||
let panels = if opt.panels.as_deref() == Some("auto") {
|
||
max_panels_fn(terminal_width, base_digits, group_size.into())
|
||
} else if let Some(panels) = opt.panels {
|
||
panels
|
||
.parse::<NonZeroU64>()
|
||
.map(u64::from)
|
||
.context(anyhow!(
|
||
"failed to parse `--panels` arg {:?} as unsigned nonzero integer",
|
||
panels
|
||
))?
|
||
} else if let Some(terminal_width) = opt.terminal_width {
|
||
max_panels_fn(terminal_width.into(), base_digits, group_size.into())
|
||
} else {
|
||
std::cmp::min(
|
||
2,
|
||
max_panels_fn(terminal_width, base_digits, group_size.into()),
|
||
)
|
||
};
|
||
|
||
let endianness = if opt.little_endian_format {
|
||
Endianness::Little
|
||
} else {
|
||
opt.endianness
|
||
};
|
||
|
||
let character_table = opt.character_table;
|
||
|
||
let stdout = io::stdout();
|
||
let mut stdout_lock = BufWriter::new(stdout.lock());
|
||
|
||
let mut printer = PrinterBuilder::new(&mut stdout_lock)
|
||
.show_color(show_color)
|
||
.show_char_panel(show_char_panel)
|
||
.show_position_panel(show_position_panel)
|
||
.with_border_style(border_style)
|
||
.enable_squeezing(squeeze)
|
||
.num_panels(panels)
|
||
.group_size(group_size)
|
||
.with_base(base)
|
||
.endianness(endianness)
|
||
.character_table(character_table)
|
||
.build();
|
||
printer.display_offset(skip_offset + display_offset);
|
||
printer.print_all(&mut reader).map_err(|e| anyhow!(e))?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn main() {
|
||
let result = run();
|
||
|
||
if let Err(err) = result {
|
||
if let Some(io_error) = err.downcast_ref::<io::Error>() {
|
||
if io_error.kind() == ::std::io::ErrorKind::BrokenPipe {
|
||
std::process::exit(0);
|
||
}
|
||
}
|
||
eprintln!("Error: {err:?}");
|
||
std::process::exit(1);
|
||
}
|
||
}
|
||
|
||
#[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
|
||
}
|
||
}
|
||
|
||
impl From<NonNegativeI64> for u64 {
|
||
fn from(x: NonNegativeI64) -> u64 {
|
||
u64::try_from(x.0)
|
||
.expect("invariant broken: NonNegativeI64 should contain a non-negative i64 value")
|
||
}
|
||
}
|
||
|
||
fn print_color_table() -> io::Result<()> {
|
||
let stdout = io::stdout();
|
||
let mut stdout_lock = BufWriter::new(stdout.lock());
|
||
|
||
writeln!(stdout_lock, "hexyl color reference:\n")?;
|
||
|
||
// NULL bytes
|
||
stdout_lock.write_all(COLOR_NULL)?;
|
||
writeln!(stdout_lock, "⋄ NULL bytes (0x00)")?;
|
||
stdout_lock.write_all(COLOR_RESET)?;
|
||
|
||
// ASCII printable
|
||
stdout_lock.write_all(COLOR_ASCII_PRINTABLE)?;
|
||
writeln!(stdout_lock, "a ASCII printable characters (0x20 - 0x7E)")?;
|
||
stdout_lock.write_all(COLOR_RESET)?;
|
||
|
||
// ASCII whitespace
|
||
stdout_lock.write_all(COLOR_ASCII_WHITESPACE)?;
|
||
writeln!(stdout_lock, "_ ASCII whitespace (0x09 - 0x0D, 0x20)")?;
|
||
stdout_lock.write_all(COLOR_RESET)?;
|
||
|
||
// ASCII other
|
||
stdout_lock.write_all(COLOR_ASCII_OTHER)?;
|
||
writeln!(
|
||
stdout_lock,
|
||
"• ASCII control characters (except NULL and whitespace)"
|
||
)?;
|
||
stdout_lock.write_all(COLOR_RESET)?;
|
||
|
||
// Non-ASCII
|
||
stdout_lock.write_all(COLOR_NONASCII)?;
|
||
writeln!(stdout_lock, "× Non-ASCII bytes (0x80 - 0xFF)")?;
|
||
stdout_lock.write_all(COLOR_RESET)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, Default, Hash, Eq, Ord, PartialEq, PartialOrd)]
|
||
pub struct PositiveI64(i64);
|
||
|
||
impl PositiveI64 {
|
||
pub fn new(x: i64) -> Option<Self> {
|
||
if x < 1 {
|
||
None
|
||
} else {
|
||
Some(Self(x))
|
||
}
|
||
}
|
||
|
||
pub fn into_inner(self) -> i64 {
|
||
self.0
|
||
}
|
||
}
|
||
|
||
impl From<PositiveI64> for u64 {
|
||
fn from(x: PositiveI64) -> u64 {
|
||
u64::try_from(x.0)
|
||
.expect("invariant broken: PositiveI64 should contain a positive i64 value")
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, PartialEq)]
|
||
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,
|
||
Self::Kilobyte => 1000,
|
||
Self::Megabyte => 1_000_000,
|
||
Self::Gigabyte => 1_000_000_000,
|
||
Self::Terabyte => 1_000_000_000_000,
|
||
Self::Kibibyte => 1 << 10,
|
||
Self::Mebibyte => 1 << 20,
|
||
Self::Gibibyte => 1 << 30,
|
||
Self::Tebibyte => 1 << 40,
|
||
Self::Block {
|
||
custom_size: Some(size),
|
||
} => size.get(),
|
||
Self::Block { custom_size: None } => DEFAULT_BLOCK_SIZE,
|
||
}
|
||
}
|
||
}
|
||
|
||
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: NonNegativeI64,
|
||
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<NonNegativeI64, NegativeOffsetSpecifiedError> {
|
||
let &Self { value, kind } = self;
|
||
match kind {
|
||
ByteOffsetKind::ForwardFromBeginning | ByteOffsetKind::ForwardFromLastOffset => {
|
||
Ok(value)
|
||
}
|
||
ByteOffsetKind::BackwardFromEnd => Err(NegativeOffsetSpecifiedError),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Eq, PartialEq, ThisError)]
|
||
enum ByteOffsetParseError {
|
||
#[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),
|
||
#[error("count multiplied by the unit overflowed a signed 64-bit integer; are you sure it should be that big?")]
|
||
UnitMultiplicationOverflow,
|
||
}
|
||
|
||
fn parse_byte_offset(n: &str, block_size: PositiveI64) -> Result<ByteOffset, ByteOffsetParseError> {
|
||
use ByteOffsetParseError::*;
|
||
|
||
let (n, kind) = process_sign_of(n)?;
|
||
|
||
let into_byte_offset = |value| {
|
||
Ok(ByteOffset {
|
||
value: NonNegativeI64::new(value).unwrap(),
|
||
kind,
|
||
})
|
||
};
|
||
|
||
if let Some(hex_number) = try_parse_as_hex_number(n) {
|
||
return hex_number.map(into_byte_offset)?;
|
||
}
|
||
|
||
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"),
|
||
),
|
||
};
|
||
}
|
||
|
||
num.checked_mul(unit.get_multiplier())
|
||
.ok_or(UnitMultiplicationOverflow)
|
||
.and_then(into_byte_offset)
|
||
}
|
||
|
||
/// 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))
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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)),
|
||
}
|
||
}
|
||
|
||
/// 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)
|
||
})
|
||
}
|