use std::io::Read;
use std::path::Path;
const SNIFF_BYTES: usize = 8 * 1024;
const MAX_BYTES: usize = 1024 * 1024; const MAX_LINES: usize = 5000;
pub fn is_probably_text(path: &Path) -> bool {
let Ok(mut f) = std::fs::File::open(path) else {
return false;
};
let mut buf = [0u8; SNIFF_BYTES];
let Ok(n) = f.read(&mut buf) else {
return false;
};
if n == 0 {
return true; }
!buf[..n].contains(&0)
}
pub struct TextContent {
pub lines: Vec<String>,
pub truncated: bool,
}
pub fn load(path: &Path) -> std::io::Result<TextContent> {
let mut f = std::fs::File::open(path)?;
let mut buf = Vec::new();
let mut chunk = [0u8; SNIFF_BYTES];
let mut truncated = false;
loop {
let n = f.read(&mut chunk)?;
if n == 0 {
break;
}
if buf.len() + n > MAX_BYTES {
let take = MAX_BYTES - buf.len();
buf.extend_from_slice(&chunk[..take]);
truncated = true;
break;
}
buf.extend_from_slice(&chunk[..n]);
}
let text = String::from_utf8_lossy(&buf);
let mut lines: Vec<String> = text.lines().map(|l| l.to_string()).collect();
if lines.len() > MAX_LINES {
lines.truncate(MAX_LINES);
truncated = true;
}
Ok(TextContent { lines, truncated })
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn tmp(name: &str, bytes: &[u8]) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("konoma_text_test_{name}"));
let mut f = std::fs::File::create(&p).unwrap();
f.write_all(bytes).unwrap();
p
}
#[test]
fn detects_text_and_loads_content() {
let p = tmp("plain.txt", b"hello\nworld\n");
assert!(is_probably_text(&p));
let c = load(&p).unwrap();
assert_eq!(c.lines, vec!["hello".to_string(), "world".to_string()]);
assert!(!c.truncated);
std::fs::remove_file(&p).ok();
}
#[test]
fn detects_binary_via_nul_byte() {
let p = tmp("blob.bin", &[0x89, 0x50, 0x00, 0x01, 0x02]);
assert!(!is_probably_text(&p));
std::fs::remove_file(&p).ok();
}
#[test]
fn empty_file_is_text() {
let p = tmp("empty", b"");
assert!(is_probably_text(&p));
std::fs::remove_file(&p).ok();
}
}