use codewalk::CodeWalker;
use keyhog_core::merkle_index::MerkleIndex;
use keyhog_core::{Chunk, Source, SourceError};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, Mutex};
mod extract;
mod filter;
mod read;
use extract::process_entry;
use filter::walker_config;
const DEFAULT_WINDOW_SIZE: usize = 1024 * 1024;
pub(crate) fn display_path(path: &Path) -> String {
let raw = path.display().to_string();
if cfg!(windows) {
strip_unc_prefix(&raw).to_string()
} else {
raw
}
}
pub(crate) fn strip_unc_prefix(s: &str) -> &str {
if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
let _ = rest;
s.strip_prefix(r"\\?\").unwrap_or(s)
} else if let Some(rest) = s.strip_prefix(r"\\?\") {
rest
} else {
s
}
}
const DEFAULT_WINDOW_OVERLAP: usize = 128 * 1024;
const MAX_READER_THREADS: usize = 4;
fn reader_thread_count(scanner_threads: usize) -> usize {
if let Ok(raw) = std::env::var("KEYHOG_READER_THREADS") {
if let Ok(n) = raw.trim().parse::<usize>() {
if n > 0 {
return n.min(scanner_threads.max(1));
}
}
}
let crew = (scanner_threads / 4).clamp(2, MAX_READER_THREADS);
crew.min(scanner_threads.max(1))
}
#[doc(hidden)]
pub fn reader_pool_thread_count_for_test(scanner_threads: usize) -> usize {
reader_thread_count(scanner_threads)
}
pub struct FilesystemSource {
root: PathBuf,
max_file_size: u64,
ignore_paths: Vec<String>,
include_paths: Vec<PathBuf>,
respect_gitignore: bool,
merkle: Option<Arc<MerkleIndex>>,
skipped: Arc<AtomicUsize>,
window_size: usize,
window_overlap: usize,
}
impl FilesystemSource {
pub fn new(root: PathBuf) -> Self {
let root = root.canonicalize().unwrap_or(root);
Self {
root,
max_file_size: 100 * 1024 * 1024, ignore_paths: Vec::new(),
include_paths: Vec::new(),
respect_gitignore: true,
merkle: None,
skipped: Arc::new(AtomicUsize::new(0)),
window_size: DEFAULT_WINDOW_SIZE,
window_overlap: DEFAULT_WINDOW_OVERLAP,
}
}
pub fn with_window_config(mut self, window_size: usize, overlap: usize) -> Self {
assert!(window_size > overlap, "window must exceed overlap");
self.window_size = window_size;
self.window_overlap = overlap;
self
}
pub fn with_merkle_skip(mut self, merkle: Arc<MerkleIndex>) -> Self {
self.merkle = Some(merkle);
self
}
pub fn skipped_counter(&self) -> Arc<AtomicUsize> {
self.skipped.clone()
}
pub fn with_include_paths(mut self, paths: Vec<PathBuf>) -> Self {
self.include_paths = paths;
self
}
pub fn with_max_file_size(mut self, bytes: u64) -> Self {
self.max_file_size = bytes;
self
}
pub fn with_ignore_paths(mut self, paths: Vec<String>) -> Self {
self.ignore_paths = paths;
self
}
pub fn with_respect_gitignore(mut self, respect: bool) -> Self {
self.respect_gitignore = respect;
self
}
}
impl Source for FilesystemSource {
fn name(&self) -> &str {
"filesystem"
}
fn chunks(&self) -> Box<dyn Iterator<Item = Result<Chunk, SourceError>> + '_> {
let max_size = self.max_file_size;
let mut config = walker_config(self.max_file_size, &self.ignore_paths);
if !self.respect_gitignore {
config = config.respect_gitignore(false);
}
fn forward_entries(
rx: std::sync::mpsc::Receiver<codewalk::error::Result<codewalk::FileEntry>>,
) -> impl Iterator<Item = codewalk::FileEntry> + Send {
rx.into_iter().filter_map(|result| match result {
Ok(entry) => Some(entry),
Err(error) => {
tracing::warn!(
%error,
"skipping unreadable filesystem entry; scan continues"
);
None
}
})
}
let entries: Box<dyn Iterator<Item = codewalk::FileEntry> + Send> =
if !self.include_paths.is_empty() {
let allowed: HashSet<PathBuf> = self
.include_paths
.iter()
.map(|p| p.canonicalize().unwrap_or_else(|_| p.clone()))
.collect();
Box::new(allowed.into_iter().flat_map(move |path| {
let inner: Box<dyn Iterator<Item = codewalk::FileEntry> + Send> =
if path.is_dir() {
let sub_walker = CodeWalker::new(&path, config.clone());
Box::new(forward_entries(sub_walker.walk_parallel(0)))
} else if path.is_file() {
match std::fs::metadata(&path) {
Ok(meta) => Box::new(std::iter::once(codewalk::FileEntry {
path,
size: meta.len(),
is_binary: false,
})),
Err(_) => Box::new(std::iter::empty()),
}
} else {
Box::new(std::iter::empty())
};
inner
}))
} else {
let walker = CodeWalker::new(&self.root, config);
Box::new(forward_entries(walker.walk_parallel(0)))
};
let merkle = self.merkle.clone();
let skipped = self.skipped.clone();
let window_size = self.window_size;
let window_overlap = self.window_overlap;
let (tx, rx) = std::sync::mpsc::sync_channel::<Result<Chunk, SourceError>>(64);
let cursor: Arc<Mutex<Box<dyn Iterator<Item = codewalk::FileEntry> + Send>>> =
Arc::new(Mutex::new(entries));
let reader_count = reader_thread_count(rayon::current_num_threads());
let run_reader =
move |cursor: Arc<Mutex<Box<dyn Iterator<Item = codewalk::FileEntry> + Send>>>,
tx: std::sync::mpsc::SyncSender<Result<Chunk, SourceError>>,
merkle: Option<Arc<MerkleIndex>>,
skipped: Arc<AtomicUsize>| {
loop {
let entry = {
let mut guard = match cursor.lock() {
Ok(g) => g,
Err(_) => return,
};
guard.next()
};
let Some(entry) = entry else {
return; };
let mut sender_alive = true;
let mut emit = |chunk: Result<Chunk, SourceError>| {
let ok = tx.send(chunk).is_ok();
sender_alive = ok;
ok
};
process_entry(
entry,
&merkle,
&skipped,
max_size,
window_size,
window_overlap,
&mut emit,
);
if !sender_alive {
return; }
}
};
let mut spawned = 0usize;
for i in 0..reader_count {
let cursor = Arc::clone(&cursor);
let tx = tx.clone();
let merkle = merkle.clone();
let skipped = skipped.clone();
let run_reader = run_reader.clone();
match std::thread::Builder::new()
.name(format!("keyhog-reader-{i}"))
.spawn(move || run_reader(cursor, tx, merkle, skipped))
{
Ok(_) => spawned += 1,
Err(error) => {
tracing::warn!(%error, reader = i, "failed to spawn file-reader thread; continuing with fewer readers");
}
}
}
if spawned == 0 {
let cursor_fb = Arc::clone(&cursor);
let tx_fb = tx.clone();
let merkle_fb = merkle.clone();
let skipped_fb = skipped.clone();
let run_reader_fb = run_reader.clone();
if std::thread::Builder::new()
.name("keyhog-reader-fallback".to_string())
.spawn(move || run_reader_fb(cursor_fb, tx_fb, merkle_fb, skipped_fb))
.is_err()
{
run_reader(cursor, tx.clone(), merkle.clone(), skipped.clone());
}
}
drop(tx);
Box::new(rx.into_iter())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}