Files
guara/crates/guara-core/src/search.rs
T
Lucas RayanandClaude Sonnet 5.5 45d535bdb7
Release / Cria a release (push) Canceled after 0s
Release / CLI (linux-aarch64) (push) Canceled after 0s
Release / CLI (linux-x86_64) (push) Canceled after 0s
Release / CLI (macos-aarch64) (push) Canceled after 0s
Release / CLI (macos-x86_64) (push) Canceled after 0s
Release / CLI (windows-x86_64) (push) Canceled after 0s
Release / App desktop (linux-x86_64) (push) Canceled after 0s
Release / App desktop (macos-aarch64) (push) Canceled after 0s
Release / App desktop (macos-x86_64) (push) Canceled after 0s
Release / App desktop (windows-x86_64) (push) Canceled after 0s
Release / Publica a release (push) Canceled after 0s
feat: explorador de arquivos, miniaturas, metadados e recursos periciais (0.3.0)
- Explorador com grade/lista virtualizada, navegação por tipo, data EXIF, pasta e
  câmera, seleção estilo gerenciador de arquivos e visualização rápida.
- Miniaturas no núcleo, metadados (EXIF, PDF, Office, MP4), datas MACB do NTFS/FAT/exFAT.
- Hashes MD5/SHA-1/SHA-256, busca de texto no disco, exportação CSV, relatório HTML,
  detecção de duplicadas, tema, correções de interface e textos sem travessões.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 16:16:20 -03:00

195 lines
5.4 KiB
Rust

//! Busca de texto bruta em todo o disco (inclusive espaço livre e não particionado),
//! em UTF-8 e UTF-16LE, útil quando não há estrutura de arquivo para recuperar.
use crate::source::Source;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchOpts {
pub terms: Vec<String>,
/// Ignora maiúsculas/minúsculas (letras ASCII).
#[serde(default = "yes")]
pub ignore_case: bool,
/// Procura também a codificação UTF-16 (Windows, Office, bancos de dados).
#[serde(default = "yes")]
pub utf16: bool,
#[serde(default = "default_max")]
pub max_hits: usize,
}
fn yes() -> bool {
true
}
fn default_max() -> usize {
20_000
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Hit {
pub offset: u64,
pub term: String,
pub encoding: String,
pub context: String,
}
const CHUNK: usize = 8 * 1024 * 1024;
const CTX: usize = 48;
struct Pat {
term: String,
enc: &'static str,
bytes: Vec<u8>,
skip: [usize; 256],
}
fn make_pats(o: &SearchOpts) -> Vec<Pat> {
let mut v = vec![];
for t in o.terms.iter().filter(|t| !t.is_empty()) {
let folded = if o.ignore_case {
t.to_lowercase()
} else {
t.clone()
};
let mut add = |enc: &'static str, bytes: Vec<u8>| {
if bytes.is_empty() {
return;
}
let mut skip = [bytes.len(); 256];
for (i, &b) in bytes[..bytes.len() - 1].iter().enumerate() {
skip[b as usize] = bytes.len() - 1 - i;
}
v.push(Pat {
term: t.clone(),
enc,
bytes,
skip,
});
};
add("UTF-8", folded.as_bytes().to_vec());
if o.utf16 {
add(
"UTF-16",
folded
.encode_utf16()
.flat_map(|c| c.to_le_bytes())
.collect(),
);
}
}
v
}
fn find_all(hay: &[u8], p: &Pat, mut f: impl FnMut(usize) -> bool) {
let n = p.bytes.len();
if hay.len() < n {
return;
}
let mut i = 0;
while i + n <= hay.len() {
let last = hay[i + n - 1];
if last == p.bytes[n - 1] && hay[i..i + n] == p.bytes[..] {
if !f(i) {
return;
}
i += 1;
} else {
i += p.skip[last as usize];
}
}
}
fn snippet(buf: &[u8], at: usize, len: usize, utf16: bool) -> String {
let a = at.saturating_sub(CTX);
let b = (at + len + CTX).min(buf.len());
let s: String = if utf16 {
let a = a & !1;
let u: Vec<u16> = buf[a..b]
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16_lossy(&u)
} else {
String::from_utf8_lossy(&buf[a..b]).into_owned()
};
s.chars()
.map(|c| if c.is_control() { '·' } else { c })
.collect()
}
/// Varre `src` inteira. Retorna o total de ocorrências. `on_hit` pode devolver false para parar.
pub fn search(
src: &dyn Source,
o: &SearchOpts,
cancelled: &(dyn Fn() -> bool + Sync),
progress: &mut dyn FnMut(u64, u64),
on_hit: &mut dyn FnMut(Hit) -> bool,
) -> usize {
let pats = make_pats(o);
if pats.is_empty() {
return 0;
}
let overlap = pats.iter().map(|p| p.bytes.len()).max().unwrap_or(1) + 2 * CTX;
let size = src.size();
let mut pos = 0u64;
let mut hits = 0usize;
let mut buf = vec![0u8; CHUNK + overlap];
'outer: while pos < size {
if cancelled() {
break;
}
let want = ((size - pos) as usize).min(CHUNK + overlap);
let mut got = 0;
while got < want {
match src.read_at(pos + got as u64, &mut buf[got..want]) {
Ok(0) => break,
Ok(n) => got += n,
Err(_) => {
// setor ruim: pula este trecho
for b in &mut buf[got..want] {
*b = 0;
}
got = want;
}
}
}
if got == 0 {
break;
}
let fresh = got.min(CHUNK); // início de ocorrências aceitas neste bloco
let folded: Vec<u8>;
let hay: &[u8] = if o.ignore_case {
folded = buf[..got].to_ascii_lowercase();
&folded
} else {
&buf[..got]
};
let mut chunk_hits: Vec<(usize, usize)> = vec![]; // (offset no bloco, índice do padrão)
for (pi, p) in pats.iter().enumerate() {
find_all(hay, p, |i| {
if i < fresh {
chunk_hits.push((i, pi));
}
true
});
}
chunk_hits.sort();
for (i, pi) in chunk_hits {
let p = &pats[pi];
hits += 1;
let h = Hit {
offset: pos + i as u64,
term: p.term.clone(),
encoding: p.enc.to_string(),
context: snippet(&buf[..got], i, p.bytes.len(), p.enc == "UTF-16"),
};
if !on_hit(h) || hits >= o.max_hits {
break 'outer;
}
}
pos += CHUNK as u64;
progress(pos.min(size), size);
}
progress(size, size);
hits
}