use super::args::SerializationFormat;
use crate::repl::state::DictionaryBackend;
use anyhow::{bail, Context, Result};
use std::io::Read;
use std::path::Path;
#[derive(Debug, Clone, Copy)]
pub struct DictFormat {
pub backend: DictionaryBackend,
pub format: SerializationFormat,
}
#[derive(Debug)]
pub struct FormatDetection {
pub format: DictFormat,
pub method: DetectionMethod,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectionMethod {
Exact,
Extension,
Content,
UserSpecified,
}
impl std::fmt::Display for DetectionMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Exact => write!(f, "exact (magic bytes)"),
Self::Extension => write!(f, "heuristic (file extension)"),
Self::Content => write!(f, "heuristic (content analysis)"),
Self::UserSpecified => write!(f, "user specified"),
}
}
}
pub fn detect_format(
path: &Path,
user_backend: Option<DictionaryBackend>,
user_format: Option<SerializationFormat>,
) -> Result<FormatDetection> {
if let (Some(backend), Some(format)) = (user_backend, user_format) {
return Ok(FormatDetection {
format: DictFormat { backend, format },
method: DetectionMethod::UserSpecified,
});
}
if let Ok(detection) = detect_exact(path) {
if let Some(backend) = user_backend {
if backend != detection.format.backend {
bail!(
"Backend mismatch: file contains {} but --backend specified {}",
detection.format.backend,
backend
);
}
}
if let Some(format) = user_format {
if format != detection.format.format {
bail!(
"Format mismatch: file contains {} but --format specified {}",
detection.format.format,
format
);
}
}
return Ok(detection);
}
if let Ok(detection) = detect_by_extension(path) {
let mut format = detection.format;
if let Some(backend) = user_backend {
format.backend = backend;
}
if let Some(fmt) = user_format {
format.format = fmt;
}
return Ok(FormatDetection {
format,
method: detection.method,
});
}
if let Ok(detection) = detect_by_content(path) {
let mut format = detection.format;
if let Some(backend) = user_backend {
format.backend = backend;
}
if let Some(fmt) = user_format {
format.format = fmt;
}
return Ok(FormatDetection {
format,
method: detection.method,
});
}
Ok(FormatDetection {
format: DictFormat {
backend: user_backend.unwrap_or(DictionaryBackend::PathMap),
format: user_format.unwrap_or(SerializationFormat::Text),
},
method: if user_backend.is_some() || user_format.is_some() {
DetectionMethod::UserSpecified
} else {
DetectionMethod::Extension
},
})
}
fn detect_exact(path: &Path) -> Result<FormatDetection> {
let mut file = std::fs::File::open(path)
.with_context(|| format!("Failed to open file: {}", path.display()))?;
let mut header = [0u8; 16];
let bytes_read = file
.read(&mut header)
.with_context(|| format!("Failed to read file header: {}", path.display()))?;
if bytes_read < 4 {
bail!("File too small for magic byte detection");
}
if header[0] == b'{' || header[0] == b'[' {
return Ok(FormatDetection {
format: DictFormat {
backend: DictionaryBackend::PathMap, format: SerializationFormat::Json,
},
method: DetectionMethod::Exact,
});
}
if bytes_read > 0 && header[0].is_ascii_alphabetic() {
let is_text = header[..bytes_read]
.iter()
.all(|&b| b.is_ascii() && (b.is_ascii_graphic() || b.is_ascii_whitespace()));
if is_text {
return Ok(FormatDetection {
format: DictFormat {
backend: DictionaryBackend::PathMap,
format: SerializationFormat::Text,
},
method: DetectionMethod::Content,
});
}
}
bail!("Could not detect format from magic bytes")
}
fn detect_by_extension(path: &Path) -> Result<FormatDetection> {
let ext = path
.extension()
.and_then(|s| s.to_str())
.context("No file extension")?;
let format = match ext.to_lowercase().as_str() {
"txt" | "text" | "dict" => SerializationFormat::Text,
"bin" | "bincode" => SerializationFormat::Bincode,
"json" => SerializationFormat::Json,
#[cfg(feature = "protobuf")]
"pb" | "protobuf" => SerializationFormat::Protobuf,
"paths" => SerializationFormat::PathsNative,
_ => bail!("Unknown file extension: {}", ext),
};
let filename = path
.file_name()
.and_then(|s| s.to_str())
.context("Invalid filename")?
.to_lowercase();
let backend =
if filename.contains("dawg") || filename.contains("dynamic") || filename.contains("dyn") {
DictionaryBackend::DynamicDawg
} else {
DictionaryBackend::PathMap
};
Ok(FormatDetection {
format: DictFormat { backend, format },
method: DetectionMethod::Extension,
})
}
fn detect_by_content(path: &Path) -> Result<FormatDetection> {
let mut file = std::fs::File::open(path)
.with_context(|| format!("Failed to open file: {}", path.display()))?;
let mut buffer = vec![0u8; 1024.min(file.metadata()?.len() as usize)];
file.read_exact(&mut buffer)
.with_context(|| format!("Failed to read file: {}", path.display()))?;
if let Ok(text) = std::str::from_utf8(&buffer) {
if text.lines().take(10).all(|line| {
let trimmed = line.trim();
trimmed.is_empty()
|| trimmed.starts_with('#')
|| trimmed
.chars()
.all(|c| c.is_alphanumeric() || c.is_whitespace() || "-_'".contains(c))
}) {
return Ok(FormatDetection {
format: DictFormat {
backend: DictionaryBackend::PathMap,
format: SerializationFormat::Text,
},
method: DetectionMethod::Content,
});
}
}
Ok(FormatDetection {
format: DictFormat {
backend: DictionaryBackend::PathMap,
format: SerializationFormat::Bincode,
},
method: DetectionMethod::Content,
})
}