use rich::measure::Measurement;
use rich::{Console, ConsoleOptions, Overflow, Renderable, Segment, Style, Text};
use crate::event::theme_style;
pub fn find_all(haystack: &[u8], needle: &[u8]) -> Vec<usize> {
if needle.is_empty() || needle.len() > haystack.len() {
return Vec::new();
}
haystack
.windows(needle.len())
.enumerate()
.filter(|(_, window)| *window == needle)
.map(|(at, _)| at)
.collect()
}
pub fn parse_needle(s: &str) -> Result<Vec<u8>, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty needle".into());
}
let quoted = s.len() >= 2
&& ((s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')));
if quoted {
let bytes = unescape(&s[1..s.len() - 1])?;
if bytes.is_empty() {
return Err("empty needle".into());
}
return Ok(bytes);
}
let mut out = Vec::new();
for token in s.split(|c: char| c.is_whitespace() || c == ',' || c == ':') {
if token.is_empty() {
continue;
}
let digits = token
.strip_prefix("0x")
.or_else(|| token.strip_prefix("0X"))
.unwrap_or(token);
if digits.is_empty() {
return Err(format!("{token:?} has no hex digits"));
}
if let Some(bad) = digits.chars().find(|c| !c.is_ascii_hexdigit()) {
return Err(format!(
"{bad:?} is not a hex digit (quote text to search for it, e.g. \"\\\"{token}\\\"\")"
));
}
if digits.len() % 2 != 0 {
return Err(format!("{token:?} has an odd number of hex digits"));
}
for pair in digits.as_bytes().chunks(2) {
let pair = std::str::from_utf8(pair).expect("ASCII hex digits");
out.push(u8::from_str_radix(pair, 16).expect("validated hex"));
}
}
if out.is_empty() {
return Err("empty needle".into());
}
Ok(out)
}
fn unescape(s: &str) -> Result<Vec<u8>, String> {
let mut out = Vec::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\\' {
let mut buf = [0; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
continue;
}
match chars.next() {
Some('\\') => out.push(b'\\'),
Some('"') => out.push(b'"'),
Some('\'') => out.push(b'\''),
Some('n') => out.push(b'\n'),
Some('r') => out.push(b'\r'),
Some('t') => out.push(b'\t'),
Some('0') => out.push(0),
Some('x') => {
let hex: String = chars.by_ref().take(2).collect();
match u8::from_str_radix(&hex, 16) {
Ok(b) if hex.len() == 2 => out.push(b),
_ => return Err(format!("\\x{hex} is not a two-digit hex escape")),
}
}
Some(other) => return Err(format!("unknown escape \\{other}")),
None => return Err("trailing backslash".into()),
}
}
Ok(out)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ByteClass {
Null,
Printable,
Whitespace,
Control,
High,
}
impl ByteClass {
pub fn of(byte: u8) -> Self {
match byte {
0 => ByteClass::Null,
b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c => ByteClass::Whitespace,
0x21..=0x7e => ByteClass::Printable,
0x80..=0xff => ByteClass::High,
_ => ByteClass::Control,
}
}
fn style(self, console: &Console) -> Style {
match self {
ByteClass::Null => theme_style(console, "hex.null", "dim"),
ByteClass::Printable => theme_style(console, "hex.printable", ""),
ByteClass::Whitespace => theme_style(console, "hex.whitespace", "green"),
ByteClass::Control => theme_style(console, "hex.control", "yellow"),
ByteClass::High => theme_style(console, "hex.high", "magenta"),
}
}
}
#[derive(Clone, Debug)]
pub struct HexView {
bytes: Vec<u8>,
offset: u64,
bytes_per_line: Option<usize>,
group: usize,
needle: Vec<u8>,
ascii_panel: bool,
collapse: bool,
}
pub const MAX_BYTES_PER_LINE: usize = 4096;
const PREFERRED_PER_LINE: usize = 16;
impl HexView {
pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
HexView {
bytes: bytes.into(),
offset: 0,
bytes_per_line: None,
group: 8,
needle: Vec::new(),
ascii_panel: true,
collapse: true,
}
}
pub fn offset(mut self, base: u64) -> Self {
self.offset = base;
self
}
pub fn bytes_per_line(mut self, n: Option<usize>) -> Self {
self.bytes_per_line = n.map(|n| n.clamp(1, MAX_BYTES_PER_LINE));
self
}
pub fn group(mut self, n: usize) -> Self {
self.group = n.max(1);
self
}
pub fn highlight(mut self, needle: &[u8]) -> Self {
self.needle = needle.to_vec();
self
}
pub fn ascii_panel(mut self, on: bool) -> Self {
self.ascii_panel = on;
self
}
pub fn collapse(mut self, on: bool) -> Self {
self.collapse = on;
self
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn offset_digits(&self) -> usize {
let end = self.offset.saturating_add(self.bytes.len() as u64);
let digits = if end == 0 {
1
} else {
(64 - end.leading_zeros() as usize).div_ceil(4)
};
digits.max(8)
}
pub fn line_width(&self, n: usize) -> usize {
let n = n.max(1);
let groups = n.div_ceil(self.group);
let hex = n
.saturating_mul(3)
.saturating_sub(1)
.saturating_add(groups - 1);
let panel = if self.ascii_panel {
n.saturating_add(4)
} else {
0
};
self.offset_digits()
.saturating_add(2)
.saturating_add(hex)
.saturating_add(panel)
}
pub fn resolved_bytes_per_line(&self, width: usize) -> usize {
if let Some(n) = self.bytes_per_line {
return n.clamp(1, MAX_BYTES_PER_LINE);
}
let fits = |n: usize| self.line_width(n) <= width;
let candidates: Vec<usize> = (1..=32 / self.group.min(32))
.map(|k| k * self.group)
.filter(|n| (8..=32).contains(n))
.collect();
if candidates.contains(&PREFERRED_PER_LINE) && fits(PREFERRED_PER_LINE) {
return PREFERRED_PER_LINE;
}
let below = candidates
.iter()
.rev()
.find(|&&n| n <= PREFERRED_PER_LINE && fits(n));
let above = candidates
.iter()
.find(|&&n| n > PREFERRED_PER_LINE && fits(n));
if let Some(&n) = below.or(above) {
return n;
}
(1..8).rev().find(|&n| fits(n)).unwrap_or(1)
}
fn lines(&self, console: &Console, per_line: usize) -> Vec<Text> {
let ascii = console.ascii_only();
let bar = if ascii { "|" } else { "│" };
let digits = self.offset_digits();
let offset_style = theme_style(console, "hex.offset", "cyan");
let border_style = theme_style(console, "hex.border", "dim");
let match_style = theme_style(console, "hex.match", "reverse");
let mut matched = vec![false; self.bytes.len()];
for at in find_all(&self.bytes, &self.needle) {
matched[at..at + self.needle.len()].fill(true);
}
let byte_style = |i: usize| {
let base = ByteClass::of(self.bytes[i]).style(console);
if matched[i] {
base.combine(&match_style)
} else {
base
}
};
let pad_bytes = per_line.min(self.bytes.len().max(PREFERRED_PER_LINE));
let groups = pad_bytes.div_ceil(self.group);
let hex_width = 3 * pad_bytes - 1 + (groups - 1);
let mut out = Vec::new();
let mut previous: Option<&[u8]> = None;
let mut starred = false;
for (index, chunk) in self.bytes.chunks(per_line).enumerate() {
let start = index * per_line;
let full = chunk.len() == per_line;
let has_match = matched[start..start + chunk.len()].iter().any(|m| *m);
if self.collapse && full && !has_match && previous == Some(chunk) {
if !starred {
out.push(Text::new("*"));
starred = true;
}
continue;
}
starred = false;
previous = if full && !has_match {
Some(chunk)
} else {
None
};
let mut line = Text::new("");
let address = self.offset.saturating_add(start as u64);
line.append(
&format!("{address:0digits$x}"),
Some(offset_style.clone().into()),
);
line.append(" ", None);
let mut used = 0;
for (k, &byte) in chunk.iter().enumerate() {
if k > 0 {
let gap = if k % self.group == 0 { " " } else { " " };
line.append(gap, None);
used += gap.len();
}
line.append(&format!("{byte:02x}"), Some(byte_style(start + k).into()));
used += 2;
}
if self.ascii_panel {
line.append(&" ".repeat(hex_width.saturating_sub(used) + 2), None);
line.append(bar, Some(border_style.clone().into()));
for (k, &byte) in chunk.iter().enumerate() {
let shown = if (0x20..=0x7e).contains(&byte) {
byte as char
} else {
'.'
};
line.append(&shown.to_string(), Some(byte_style(start + k).into()));
}
line.append(bar, Some(border_style.clone().into()));
}
out.push(line);
}
let end = self.offset.saturating_add(self.bytes.len() as u64);
out.push(Text::styled(format!("{end:0digits$x}"), offset_style));
out
}
}
impl Renderable for HexView {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let per_line = self.resolved_bytes_per_line(options.max_width);
let mut out = Vec::new();
for line in self.lines(console, per_line) {
if out
.last()
.is_some_and(|s: &Segment| !s.text.ends_with('\n'))
{
out.push(Segment::line());
}
let line = line.overflow(Overflow::Fold);
out.extend(line.rich_render(console, options));
}
while out.last().is_some_and(|s| s.text == "\n") {
out.pop();
}
out
}
fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
let (min, max) = match self.bytes_per_line {
Some(n) => (self.line_width(n), self.line_width(n)),
None => (self.line_width(1), self.line_width(PREFERRED_PER_LINE)),
};
Measurement::new(min, max).clamp(None, Some(options.max_width))
}
}