use std::io::Result;
use ratatui::Frame;
use ratatui::crossterm::event::{Event, KeyCode};
use ratatui::widgets::Paragraph;
use regex::bytes::Regex;
use tui_input::Input;
use tui_input::backend::crossterm::EventHandler;
use crate::widgets::{Message, MessageType};
use crate::{app::App, editor::UIState};
#[derive(Default, Debug)]
pub struct Search {
pub input_text: Input,
pub mode: SearchMode,
pub direction: SearchDirection,
pub input_hex: Input,
}
#[derive(Default, Debug, PartialEq)]
pub enum SearchMode {
#[default]
Utf8,
Hex,
}
impl SearchMode {
pub fn next(&mut self) {
if *self == SearchMode::Utf8 {
*self = SearchMode::Hex;
} else {
*self = SearchMode::Utf8
}
}
}
#[derive(Default, PartialEq, Debug)]
pub enum SearchDirection {
#[default]
Forward,
Backward,
}
fn hex_string_to_u8(hex_string: &str) -> Option<Vec<u8>> {
let s = hex_string.replace(" ", "");
if s.is_empty() || !s.len().is_multiple_of(2) {
return None;
}
let bytes = hex::decode(s).unwrap();
Some(bytes)
}
fn find_nocase(haystack: &[u8], needle: &[u8]) -> Option<usize> {
let first = needle.first()?;
let needle_len = needle.len();
if needle_len > haystack.len() {
return None;
}
if first.is_ascii_alphabetic() {
for pos in memchr::memchr2_iter(
first.to_ascii_lowercase(),
first.to_ascii_uppercase(),
haystack,
) {
let mut matches = 1;
for c in needle.iter().skip(1) {
if !c.eq_ignore_ascii_case(haystack.get(pos + matches)?) {
break;
}
matches += 1;
}
if matches == needle_len {
return Some(pos);
}
}
} else {
for pos in memchr::memchr_iter(*first, haystack) {
let mut matches = 1;
for c in needle.iter().skip(1) {
if !c.eq_ignore_ascii_case(haystack.get(pos + matches)?) {
break;
}
matches += 1;
}
if matches == needle_len {
return Some(pos);
}
}
}
None
}
fn rfind_nocase(haystack: &[u8], needle: &[u8]) -> Option<usize> {
let first = needle.first()?;
let needle_len = needle.len();
if needle_len > haystack.len() {
return None;
}
if first.is_ascii_alphabetic() {
for pos in memchr::memrchr2_iter(
first.to_ascii_lowercase(),
first.to_ascii_uppercase(),
haystack,
) {
let mut matches = 1;
for c in needle.iter().skip(1) {
if haystack.len() - pos + matches < needle_len {
continue;
}
if !c.eq_ignore_ascii_case(haystack.get(pos + matches)?) {
break;
}
matches += 1;
}
if matches == needle_len {
return Some(pos);
}
}
} else {
for pos in memchr::memrchr_iter(*first, haystack) {
let mut matches = 1;
for c in needle.iter().skip(1) {
if haystack.len() - pos + matches < needle_len {
continue;
}
if !c.eq_ignore_ascii_case(haystack.get(pos + matches)?) {
break;
}
matches += 1;
}
if matches == needle_len {
return Some(pos);
}
}
}
None
}
pub fn search(app: &mut App, needle: &str, next: bool) -> Option<usize> {
if app.hex_view.search.mode == SearchMode::Hex {
if needle.contains(|c| ['?', '[', ']'].contains(&c)) {
return search_pattern(app, needle, next);
} else {
let n = hex_string_to_u8(needle)?;
return search_literal(app, n, next)
}
}
search_literal(app, needle, next)
}
pub fn search_literal<T: AsRef<[u8]>>(app: &mut App, needle: T, next: bool) -> Option<usize> {
let text = needle.as_ref();
let filesize = app.file_info.size;
let buffer = app.file_info.get_buffer();
if filesize == 0 || text.is_empty() {
return None;
}
let ofs = if app.hex_view.search.direction == SearchDirection::Forward {
let start = if next {
app.hex_view.offset.checked_add(1)?
} else {
app.hex_view.offset
};
let contais_capital_letter = text.iter().any(|b| b.is_ascii_uppercase());
let smart_search = contais_capital_letter && app.config.search_smartcase;
if start < filesize {
if app.config.search_ignorecase && !smart_search {
find_nocase(buffer.get(start..)?, text).map(|pos| start + pos)
} else {
memchr::memmem::find(buffer.get(start..)?, text).map(|pos| start + pos)
}
} else {
None
}
} else {
let start = if next {
app.hex_view.offset.checked_sub(1)?
} else {
app.hex_view.offset
};
let contais_capital_letter = text.iter().any(|b| b.is_ascii_uppercase());
let smart_search = contais_capital_letter && app.config.search_smartcase;
if start < filesize && start != 0 {
if app.config.search_ignorecase && !smart_search {
rfind_nocase(buffer.get(..start)?, text)
} else {
memchr::memmem::rfind(buffer.get(..start)?, text)
}
} else {
None
}
};
if ofs.is_some() {
return ofs;
}
if app.config.search_wrapscan {
let ofs = if app.hex_view.search.direction == SearchDirection::Forward {
memchr::memmem::find(buffer, text)
} else {
memchr::memmem::rfind(buffer, text)
};
if ofs.is_some() {
return ofs;
}
}
crate::beep!();
None
}
fn translate_pattern(pattern: &str) -> Option<String> {
let mut t = String::new();
let ptn = pattern.replace(" ", "");
if !ptn
.chars()
.filter(|c| c.is_ascii_hexdigit() || *c == '?')
.count()
.is_multiple_of(2)
{
return None;
}
let mut next_is_first = true;
for c in ptn.chars() {
if c.is_ascii_hexdigit() {
if next_is_first {
t.push_str("\\x");
next_is_first = false;
} else {
next_is_first = true;
}
t.push(c);
} else if c == '?' {
if next_is_first {
t.push('.');
next_is_first = false;
} else {
next_is_first = true;
}
} else if ['[', ']', '-'].contains(&c) {
t.push(c);
next_is_first = true;
}
}
t.insert_str(0, "(?s-u)");
Some(t)
}
pub fn search_pattern(app: &mut App, pattern: &str, next: bool) -> Option<usize> {
if pattern.is_empty() {
return None;
}
if let Some(ptn) = &translate_pattern(pattern)
&& let Ok(re) = Regex::new(ptn)
{
let buffer = app.file_info.get_buffer();
if app.hex_view.search.direction == SearchDirection::Forward {
let start = if next {
app.hex_view.offset.checked_add(1)?
} else {
app.hex_view.offset
};
if let Some(m) = re.find(buffer.get(start..)?) {
return Some(m.start() + start);
} else if app.config.search_wrapscan
&& let Some(m) = re.find(buffer)
{
return Some(m.start() + start);
}
} else {
let start = app.hex_view.offset;
if let Some(m) = re.find_iter(buffer.get(..start)?).last() {
return Some(m.start());
} else if app.config.search_wrapscan
&& let Some(m) = re.find_iter(buffer).last()
{
return Some(m.start());
}
}
}
None
}
pub fn dialog_search_draw(app: &mut App, frame: &mut Frame) {
let prompt_char = if app.hex_view.search.direction == SearchDirection::Forward {
'/'
} else {
'?'
};
let (para, x) = match app.hex_view.search.mode {
SearchMode::Utf8 => (
Paragraph::new(format!(
"{}{}",
prompt_char,
app.hex_view.search.input_text.value()
)),
app.hex_view.search.input_text.visual_cursor(),
),
SearchMode::Hex => (
Paragraph::new(format!(
"{}{}",
prompt_char,
app.hex_view.search.input_hex.value()
)),
app.hex_view.search.input_hex.visual_cursor(),
),
};
frame.render_widget(para, app.command_area);
frame.set_cursor_position((app.command_area.x + 1 + x as u16, app.command_area.y));
}
pub fn dialog_search_events(app: &mut App, event: &Event) -> Result<bool> {
if let Event::Key(key) = event {
match key.code {
KeyCode::Esc => {
app.dialog_renderer = None;
app.state = UIState::Normal;
}
KeyCode::Backspace => match app.hex_view.search.mode {
SearchMode::Utf8 => {
if app.hex_view.search.input_text.value().is_empty() {
app.dialog_renderer = None;
app.state = UIState::Normal;
} else {
app.hex_view.search.input_text.handle_event(event);
}
}
SearchMode::Hex => {
if app.hex_view.search.input_hex.value().is_empty() {
app.dialog_renderer = None;
app.state = UIState::Normal;
} else {
app.hex_view.search.input_hex.handle_event(event);
}
}
},
KeyCode::Enter => {
let needle = match app.hex_view.search.mode {
SearchMode::Utf8 => app.hex_view.search.input_text.value().to_string(),
SearchMode::Hex => app.hex_view.search.input_hex.value().to_string(),
};
if needle.is_empty() {
app.dialog_renderer = None;
app.state = UIState::Normal;
return Ok(true);
}
if let Some(ofs) = search(app, &needle, false) {
app.goto(ofs);
app.dialog_renderer = None;
} else {
app.dialog_renderer = Some(dialog_search_error_draw);
crate::beep!();
}
app.state = UIState::Normal;
}
KeyCode::Tab => {
app.hex_view.search.mode.next();
}
KeyCode::Char(c) => {
match app.hex_view.search.mode {
SearchMode::Utf8 => app.hex_view.search.input_text.handle_event(event),
SearchMode::Hex => {
let allowed = [' ', '?'];
if c.is_ascii_hexdigit() || allowed.contains(&c) {
app.hex_view.search.input_hex.handle_event(event)
} else {
None
}
}
};
}
_ => {
match app.hex_view.search.mode {
SearchMode::Utf8 => app.hex_view.search.input_text.handle_event(event),
SearchMode::Hex => app.hex_view.search.input_hex.handle_event(event),
};
}
}
}
Ok(false)
}
pub fn dialog_search_error_draw(app: &mut App, frame: &mut Frame) {
let mut dialog = Message::from("Not found");
dialog.kind = MessageType::Error;
dialog.render(app, frame);
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_find_nocase() {
for needle in [
b"opengl", b"OpenGL", b"OpengL", b"opengL", b"Opengl", b"OPENGL",
] {
let r = find_nocase(b"I think OpenGL is nice", needle);
assert_eq!(r, Some(8));
}
for needle in [b"i think", b"I think", b"i Think", b"I thinK"] {
let r = find_nocase(b"I think OpenGL is nice", needle);
assert_eq!(r, Some(0));
}
for needle in [b"ce", b"cE", b"Ce", b"CE"] {
let r = find_nocase(b"I think OpenGL is nice", needle);
assert_eq!(r, Some(20));
}
for needle in [b"x", b"X"] {
let r = find_nocase(b"I think OpenGL is nicX", needle);
assert_eq!(r, Some(21));
}
for needle in [
b"Aopengl", b"OpeanGL", b"OpengaL", b"ope-ngL", b"Open2gl", b"OPExNGL",
] {
let r = find_nocase(b"I think OpenGL is nice", needle);
assert_eq!(r, None);
}
for needle in [b"i t?hink", b"I thi]nk", b"i Thin/k", b"I |thinK"] {
let r = find_nocase(b"I think OpenGL is nice", needle);
assert_eq!(r, None);
}
for needle in [b"c]e", b"c,E", b"Ce0", b"CE-"] {
let r = find_nocase(b"I think OpenGL is nice", needle);
assert_eq!(r, None);
}
for needle in [b"*", b"?"] {
let r = find_nocase(b"I think OpenGL is nicX", needle);
assert_eq!(r, None);
}
assert_eq!(
find_nocase(b"\x00\x80\xCD\x6F\x00\x00\xCE\x6F\x00\x80", b"opengl"),
None
);
}
#[test]
fn test_translate_pattern() {
assert_eq!(
translate_pattern("FEC01F803F??07"),
Some(r"(?s-u)\xFE\xC0\x1F\x80\x3F.\x07".to_string())
);
assert_eq!(
translate_pattern("FEC01F803F[00-ff]07"),
Some(r"(?s-u)\xFE\xC0\x1F\x80\x3F[\x00-\xff]\x07".to_string())
);
}
}