Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option for reading between two offsets #75

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ ctrlc = "3.1"
version = "2"
features = ["suggestions", "color", "wrap_help"]

[dependencies.error-chain]
version = "0.12"
default-features = false
features = []

[profile.release]
lto = true
codegen-units = 1
159 changes: 147 additions & 12 deletions src/bin/hexyl.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// `error_chain!` can recurse deeply
#![recursion_limit = "1024"]

#[macro_use]
extern crate error_chain;

#[macro_use]
extern crate clap;

Expand All @@ -15,7 +21,19 @@ use atty::Stream;

use hexyl::{BorderStyle, Printer};

fn run() -> Result<(), Box<dyn std::error::Error>> {
mod errors {
error_chain! {
foreign_links {
Clap(::clap::Error);
Io(::std::io::Error);
ParseIntError(::std::num::ParseIntError);
}
}
}

use crate::errors::*;

fn run() -> Result<()> {
let app = App::new(crate_name!())
.setting(AppSettings::ColorAuto)
.setting(AppSettings::ColoredHelp)
Expand All @@ -40,6 +58,23 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.value_name("N")
.help("An alias for -n/--length"),
)
.arg(
Arg::with_name("range")
.short("r")
.long("range")
.takes_value(true)
.value_name("N:M")
.conflicts_with("length")
.conflicts_with("bytes")
.help("Read only bytes N through M from the input")
.long_help(
"Only print the specified range of bytes. \
For example:\n \
'--range 512:1024' prints bytes 512 to 1024\n \
'--range 512:+512' skips 512 bytes and prints the next 512 bytes (equivalent to 512:1024)\n \
'--range :512' prints the first 512 bytes\n \
'--range 512:' skips 512 bytes and prints the rest of the input")
)
.arg(
Arg::with_name("nosqueezing")
.short("v")
Expand Down Expand Up @@ -94,6 +129,19 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
reader = Box::new(reader.take(length));
}

let byterange_arg = matches.value_of("range");
let mut range_offset = 0;
if let Some(range) = byterange_arg {
if let Ok((offset, num_bytes)) = parse_range(range) {
range_offset = offset;
let mut discard = vec![0u8; offset as usize];
reader
.read_exact(&mut discard)
Comment on lines +137 to +139
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, that seems a bit hacky, but I also couldn't find a better solution after a quick search.

I'm not sure if we should leave it like this. If somebody wants to skip the first few MB or even GB of a file/input, we would actually allocate a buffer of this size on the heap which could easily fail. Even if it doesn't fail, it seems wrong to allocate a huge buffer for simply discarding everything inside it.

If that helps, I think we could actually use the BufRead trait instead of just Read. We would simply need to create a BufReader for the input file. The StdinLock already implements BufRead.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did a bit of research on this and Stdin doesn't appear to be seekable no matter what I do. I'll continue exploring other options for this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe read to a 1024 bytes or so big slice multiple times as necessary and then read to a smaller slice as necessary.

.map_err(|_| format!("Unable to start reading at {}, input too small", offset))?;
reader = Box::new(reader.take(num_bytes));
}
}

let show_color = match matches.value_of("color") {
Some("never") => false,
Some("auto") => atty::is(Stream::Stdout),
Expand All @@ -111,7 +159,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let display_offset = matches
.value_of("display_offset")
.and_then(parse_hex_or_int)
.unwrap_or(0);
.unwrap_or(range_offset);

// Set up Ctrl-C handler
let cancelled = Arc::new(AtomicBool::new(false));
Expand All @@ -127,7 +175,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {

let mut printer = Printer::new(&mut stdout_lock, show_color, border_style, squeeze);
printer.display_offset(display_offset as usize);
printer.print_all(&mut reader, Some(cancelled))?;
printer
.print_all(&mut reader, Some(cancelled))
.map_err(|err| format!("{}", err))?;

Ok(())
}
Expand All @@ -140,27 +190,112 @@ fn main() {
let result = run();

if let Err(err) = result {
if let Some(clap_err) = err.downcast_ref::<clap::Error>() {
eprint!("{}", clap_err); // Clap errors already have newlines
match err {
Error(ErrorKind::Clap(ref clap_error), _) => {
eprint!("{}", clap_error); // Clap errors already have newlines

match clap_err.kind {
// The exit code should not indicate an error for --help / --version
clap::ErrorKind::HelpDisplayed | clap::ErrorKind::VersionDisplayed => {
std::process::exit(0)
match clap_error.kind {
// The exit code should not indicate an error for --help / --version
clap::ErrorKind::HelpDisplayed | clap::ErrorKind::VersionDisplayed => {
std::process::exit(0)
}
_ => (),
}
_ => (),
}
} else {
eprintln!("Error: {}", err);
Error(err, _) => eprintln!("Error: {}", err),
}
std::process::exit(1);
}
}

fn parse_hex_or_int(n: &str) -> Option<u64> {
let n = n.trim_start_matches('+');
if n.starts_with("0x") {
u64::from_str_radix(n.trim_start_matches("0x"), 16).ok()
} else {
n.parse::<u64>().ok()
}
}

fn parse_range(range_raw: &str) -> Result<(u64, u64)> {
match range_raw.split(':').collect::<Vec<&str>>()[..] {
[offset_raw, bytes_to_read_raw] => {
let offset = parse_hex_or_int(&offset_raw).unwrap_or_else(u64::min_value);
let bytes_to_read = match parse_hex_or_int(bytes_to_read_raw) {
Some(num) if bytes_to_read_raw.starts_with('+') => num,
Some(num) if offset <= num => num - offset,
Some(num) => {
return Err(format!(
"cannot start reading at {} and stop reading at {}",
offset, num
)
.into())
}
None if bytes_to_read_raw != "" => return Err("unable to parse range".into()),
None => u64::max_value(),
};
Ok((offset, bytes_to_read))
}
[offset_raw] => {
let offset = parse_hex_or_int(&offset_raw).unwrap_or_else(u64::min_value);
let bytes_to_read = u64::max_value();
Ok((offset, bytes_to_read))
}
_ => Err("expected single ':' character".into()),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_empty_range() {
let (offset, bytes_to_read) = parse_range(":").expect("Not allowed to fail test.");
assert_eq!(offset, 0);
assert_eq!(bytes_to_read, u64::max_value());

let (offset, bytes_to_read) = parse_range("").expect("Not allowed to fail test.");
assert_eq!(offset, 0);
assert_eq!(bytes_to_read, u64::max_value());
}

#[test]
fn parse_starting_offset() {
let (offset, bytes_to_read) = parse_range("0x200:").expect("Not allowed to fail test.");
assert_eq!(offset, 512);
assert_eq!(bytes_to_read, u64::max_value());

let (offset, bytes_to_read) = parse_range("0x200").expect("Not allowed to fail test.");
assert_eq!(offset, 512);
assert_eq!(bytes_to_read, u64::max_value());
}

#[test]
fn parse_ending_offset() {
let (offset, bytes_to_read) = parse_range(":0x200").expect("Not allowed to fail test.");
assert_eq!(offset, 0);
assert_eq!(bytes_to_read, 512);

let (offset, bytes_to_read) = parse_range(":+512").expect("Not allowed to fail test.");
assert_eq!(offset, 0);
assert_eq!(bytes_to_read, 512);

let (offset, bytes_to_read) = parse_range("512:512").expect("Not allowed to fail test.");
assert_eq!(offset, 512);
assert_eq!(bytes_to_read, 0);

let (offset, bytes_to_read) = parse_range("512:+512").expect("Not allowed to fail test.");
assert_eq!(offset, 512);
assert_eq!(bytes_to_read, 512);
}

#[test]
fn parse_bad_input() {
MaxJohansen marked this conversation as resolved.
Show resolved Hide resolved
let result = parse_range("1024:512");
assert!(result.is_err());

let result = parse_range("512:-512");
assert!(result.is_err());
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ impl<'a, Writer: Write> Printer<'a, Writer> {
&mut self,
mut reader: Reader,
canceller: Option<Cancelled>,
) -> Result<(), Box<dyn std::error::Error>> {
) -> Result<(), std::io::Error> {
let mut buffer = [0; BUFFER_SIZE];
'mainloop: loop {
let size = reader.read(&mut buffer)?;
Expand Down