use std::path::Path;
use super::{Categorization, FileCategorizer};
use limnifs_core::codec::CODEC_FSST_BROTLI;
const FSST_ENABLED: bool = true;
const MIN_FSST_SIZE: usize = 4 * 1024;
pub struct CsvTextCategorizer;
impl FileCategorizer for CsvTextCategorizer {
fn name(&self) -> &'static str {
"csv-text"
}
fn categories(&self) -> &'static [&'static str] {
&["csv-text/composite"]
}
fn categorize(&self, path: &Path, data: &[u8]) -> Option<Categorization> {
if !FSST_ENABLED {
return None;
}
if data.len() < MIN_FSST_SIZE {
return None;
}
if !looks_like_csv_text(path, data) {
return None;
}
Some(Categorization {
codec_id: CODEC_FSST_BROTLI,
codec_params: Vec::new(),
category: "csv-text/composite",
})
}
}
#[must_use]
fn looks_like_csv_text(path: &Path, data: &[u8]) -> bool {
let ext = match path.extension().and_then(|e| e.to_str()) {
Some(e) => e.to_ascii_lowercase(),
None => return false,
};
if !matches!(ext.as_str(), "csv" | "tsv" | "json" | "jsonl" | "ndjson") {
return false;
}
let sample = if data.len() > 4096 {
&data[..4096]
} else {
data
};
let printables = sample
.iter()
.filter(|&&b| (0x20..=0x7E).contains(&b) || b == b'\n' || b == b'\r' || b == b'\t')
.count();
if (printables as f32 / sample.len() as f32) < 0.95 {
return false;
}
let punct_count = sample
.iter()
.filter(|&&b| matches!(b, b',' | b'"' | b'{' | b'}' | b'[' | b']'))
.count();
(punct_count as f32 / sample.len() as f32) > 0.01
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn csv_extension_routes_when_enabled() {
let c = CsvTextCategorizer;
let csv = "a,b,c\n".repeat(2000);
let cat = c
.categorize(&PathBuf::from("/x.csv"), csv.as_bytes())
.expect("csv extension claims");
assert_eq!(cat.codec_id, limnifs_core::codec::CODEC_FSST_BROTLI);
}
#[test]
fn csv_extension_detected_when_enabled() {
let csv = "a,b,c\n".repeat(2000);
assert!(looks_like_csv_text(
&PathBuf::from("/x.csv"),
csv.as_bytes()
));
assert!(looks_like_csv_text(
&PathBuf::from("/x.json"),
csv.as_bytes()
));
}
#[test]
fn binary_data_not_misdetected() {
let mut state: u64 = 0xDEAD_BEEF_CAFE_BABE;
let mut binary = Vec::with_capacity(8192);
for _ in 0..8192 {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
binary.push(u8::try_from(state & 0xFF).unwrap());
}
assert!(!looks_like_csv_text(&PathBuf::from("/x.csv"), &binary));
}
#[test]
fn empty_extension_does_not_trigger_fsst() {
let csv = "alpha,beta,gamma\n".repeat(500);
assert!(!looks_like_csv_text(
&PathBuf::from("/noext"),
csv.as_bytes()
));
}
}