use std::{
fs::File,
io::{self, prelude::*, SeekFrom},
marker::PhantomData,
path::PathBuf,
sync::atomic::{AtomicBool, Ordering},
};
use crossterm::{queue, style::*};
use structopt::StructOpt;
macro_rules! colorize {
(reset) => {
queue!(io::stdout(), ResetColor, SetAttribute(Attribute::Reset));
};
(Fg::$color:tt) => {
SetForegroundColor(Color::$color)
};
(Bg::$color:tt) => {
SetBackgroundColor(Color::$color)
};
(Attr::$attr:tt) => {
SetAttribute(Attribute::$attr)
};
(in $($cmd:tt::$arg:tt),+) => {
queue!(io::stdout(), $(colorize!($cmd::$arg)),+);
};
(if $flag:expr => $(in $($cmd:tt::$arg:tt),+ $code:block)+) => {
#[allow(unused_must_use)] {
$({
if $flag {
colorize!(in $($cmd::$arg),+);
}
$code
})+
if $flag {
colorize!(reset);
}
}
};
}
#[derive(StructOpt)]
#[structopt(author, about)]
pub struct Hexed {
#[structopt(name = "FILE")]
path: PathBuf,
#[structopt(flatten)]
opts: HexedOpts,
#[structopt(flatten)]
flags: HexedFlags,
}
#[derive(StructOpt)]
#[structopt(rename_all = "screaming-snake")]
struct HexedOpts {
#[structopt(short = "n", long = "length")]
length: Option<usize>,
#[structopt(short = "s", long = "skip")]
offset: Option<u64>,
}
#[derive(StructOpt)]
struct HexedFlags {
#[structopt(short, long)]
octal: bool,
#[structopt(short = "G", long)]
no_guides: bool,
#[structopt(short = "C", long)]
no_colors: bool,
#[structopt(short = "A", long)]
no_ascii: bool,
}
struct Bytes<'b> {
file: File,
max: usize,
buf: [u8; 0x10],
phantom: PhantomData<&'b u8>,
}
impl Bytes<'_> {
fn get(&self, n: usize) -> &[u8] {
&self.buf[..n]
}
}
impl Iterator for Bytes<'_> {
type Item = usize;
fn next(&mut self) -> Option<Self::Item> {
match self.file.read(&mut self.buf[0..self.max]) {
Ok(num) if num > 0 => Some(num),
_ => None,
}
}
}
static ABORT: AtomicBool = AtomicBool::new(false);
impl Hexed {
fn is_colored(&self) -> bool {
!self.flags.no_colors
}
fn row_width(&self) -> usize {
if self.flags.octal {
0o10
} else {
0x10
}
}
fn byte_width(&self) -> usize {
if self.flags.octal {
4
} else {
2
}
}
fn row_character_width(&self) -> usize {
self.row_width() * (self.byte_width() + 1)
}
fn bytes<'b>(&self) -> io::Result<Bytes<'b>> {
let mut file = File::open(&self.path)?;
file.seek(SeekFrom::Start(self.opts.offset.unwrap_or_default()))?;
Ok(Bytes {
file,
max: self.row_width(),
buf: Default::default(),
phantom: PhantomData,
})
}
fn print_ruler(&self, mid: char) {
print!("─────────{}", mid);
for _ in 0..self.row_character_width() {
print!("─");
}
}
fn print_header(&self) {
if self.flags.no_guides {
return;
}
colorize!(if self.is_colored() => in Fg::Green, Attr::Bold {
print!(" Offset │ ");
for i in 0..self.row_width() {
if self.flags.octal {
print!("{:04o} ", i);
} else {
print!("{:02X} ", i);
}
}
println!();
self.print_ruler('┼');
});
}
fn print_vert_guide(&self, row: usize) {
if self.flags.no_guides {
return;
}
colorize!(if self.is_colored() => in Fg::Green, Attr::Bold {
if self.flags.octal {
print!("{:08o} │ ", row * 0o10);
} else {
print!("{:08X} │ ", row * 0x10);
}
});
}
#[allow(unused_must_use)]
fn print_row(&self, bytes: &[u8]) {
for i in 0..self.row_width() {
let byte = bytes.get(i).unwrap_or(&0);
if self.is_colored() {
match byte {
0 => colorize!(in Fg::DarkGrey),
0x01..=0x1F | 0x7F => colorize!(in Fg::Yellow),
b' '..=b'~' => colorize!(in Fg::Cyan),
_ => colorize!(in Fg::Reset),
};
}
if i >= bytes.len() {
for _ in 0..self.byte_width() + 1 {
print!(" ");
}
continue;
}
if self.flags.octal {
print!("{:04o} ", byte);
} else {
print!("{:02X} ", byte);
}
}
if self.is_colored() {
colorize!(reset);
}
}
fn print_ascii_sidebar(&self, bytes: &[u8]) {
if self.flags.no_ascii {
return;
}
colorize!(if self.is_colored() => in Fg::DarkGrey {
print!(" ");
for mut ascii in bytes.iter().copied().map(<char as From<u8>>::from) {
if !ascii.is_ascii_graphic() {
ascii = ' ';
}
print!("{}", ascii);
}
});
}
fn print_footer(&self, bytes: u64) {
colorize!(if self.is_colored() =>
in Fg::Green, Attr::Bold {
if !self.flags.no_guides {
self.print_ruler('┴');
}
}
in Attr::Reset {
print!("\n{} bytes in `{}`", bytes, self.path.to_string_lossy());
}
);
}
#[allow(unused_must_use)]
pub fn run(self) -> io::Result<()> {
let limit = self.opts.length.unwrap_or_else(usize::max_value);
let mut bytes_read = 0;
let mut bytes = self.bytes()?;
let mut row = 0;
ctrlc::set_handler(|| ABORT.store(true, Ordering::Release));
self.print_header();
println!();
while let Some(available_bytes) = bytes.next() {
if ABORT.load(Ordering::Acquire) {
break;
}
let bytes = bytes.get(available_bytes.min(limit - bytes_read));
self.print_vert_guide(row);
self.print_row(bytes);
self.print_ascii_sidebar(bytes);
println!();
row += 1;
bytes_read += available_bytes;
if bytes_read >= limit {
break;
}
}
self.print_footer(bytes.file.seek(SeekFrom::End(0)).unwrap());
println!();
Ok(())
}
}