use std::path::Path;
use std::sync::OnceLock;
pub mod csv_text;
pub mod executable;
pub mod fits;
pub mod pcm_audio;
pub mod registry;
pub use registry::FileCategorizerRegistry;
#[must_use]
pub fn default_registry() -> &'static FileCategorizerRegistry {
static REGISTRY: OnceLock<FileCategorizerRegistry> = OnceLock::new();
REGISTRY.get_or_init(|| {
FileCategorizerRegistry::new()
.register(Box::new(fits::FitsCategorizer))
.register(Box::new(pcm_audio::PcmAudioCategorizer))
.register(Box::new(csv_text::CsvTextCategorizer))
.register(Box::new(executable::ExecutableCategorizer))
})
}
#[derive(Clone, Debug)]
pub struct Categorization {
pub codec_id: u8,
pub codec_params: Vec<u8>,
pub category: &'static str,
}
pub trait FileCategorizer: Sync + Send {
fn name(&self) -> &'static str;
fn categories(&self) -> &'static [&'static str];
fn categorize(&self, path: &Path, data: &[u8]) -> Option<Categorization>;
fn first_byte_hint(&self) -> Option<&'static [u8]> {
None
}
}
#[derive(Debug)]
pub struct ConfigCategorizer {
entries: Vec<super::config::CategorizerConfig>,
}
impl ConfigCategorizer {
#[must_use]
pub fn new(entries: Vec<super::config::CategorizerConfig>) -> Self {
Self { entries }
}
}
impl FileCategorizer for ConfigCategorizer {
fn name(&self) -> &'static str {
"config"
}
fn categories(&self) -> &'static [&'static str] {
&["config"]
}
fn categorize(&self, path: &Path, data: &[u8]) -> Option<Categorization> {
use super::config::CategorizerConfig;
let ext_lower: Option<String> = path
.extension()
.and_then(|e| e.to_str())
.map(|s| s.to_ascii_lowercase());
for entry in &self.entries {
if !entry.enabled {
continue;
}
let by_ext = ext_lower
.as_deref()
.is_some_and(|e| entry.extensions.iter().any(|x| x == e));
let by_magic = !entry.magic_bytes.is_empty() && data.starts_with(&entry.magic_bytes);
if !by_ext && !by_magic {
continue;
}
if let Some(max) = entry.max_size {
if u64::try_from(data.len()).unwrap_or(u64::MAX) > u64::from(max) {
continue;
}
}
return Some(Categorization {
codec_id: 0, codec_params: encode_config_ref(entry),
category: "config",
});
}
None
}
}
fn encode_config_ref(entry: &super::config::CategorizerConfig) -> Vec<u8> {
let mut out = Vec::with_capacity(4 + entry.name.len() + 1);
let len = u32::try_from(entry.name.len()).unwrap_or(0);
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(entry.name.as_bytes());
out.push(u8::from(entry.enabled));
out
}
pub fn resolve_config_categorization(
cat: &Categorization,
entries: &[super::config::CategorizerConfig],
codec_resolver: &dyn Fn(&str) -> Option<u8>,
) -> Option<u8> {
if cat.category != "config" {
return None;
}
if cat.codec_params.len() < 5 {
return None;
}
let name_len = u32::from_le_bytes(cat.codec_params[..4].try_into().ok()?) as usize;
let rest = &cat.codec_params[4..];
if rest.len() < name_len + 1 {
return None;
}
let name = std::str::from_utf8(&rest[..name_len]).ok()?;
let entry = entries.iter().find(|c| c.name == name)?;
if !entry.enabled {
return None;
}
codec_resolver(&entry.codec)
}