pub mod code;
mod column_stats;
pub mod container;
pub mod media;
mod media_helpers;
mod office;
pub mod settings;
pub mod sqlite;
pub mod structured;
pub mod text;
pub mod traits;
pub use media_helpers::{BitrateMode, CompressionMode};
use crate::config::RuntimeConfig;
use crate::results::{CompressionStats, FileMetadata, MiningResult, Output, OutputMode, Template};
use crate::utils;
use anyhow::Result;
use memmap2::Mmap;
use serde_json;
use std::fs::{File, OpenOptions};
use std::io::Write;
#[macro_export]
macro_rules! extract_metadata_with_fallback {
($field:expr, $extract_fn:expr, $stats:expr, $metadata_type:path, $type_name:expr) => {
$field = match $extract_fn {
Ok(metadata) => Some(metadata),
Err(e) => {
let error_msg = if let Some(source) = e.source() {
format!("{}: {}", e, source)
} else {
format!("{}", e)
};
log::debug!(
"{} metadata extraction failed for '{}': {}",
$type_name,
$stats.file_path,
error_msg
);
Some($crate::results::create_minimal_fallback::<$metadata_type>(
$stats.byte_count,
))
}
};
};
}
#[macro_export]
macro_rules! process_with_metadata {
($stats:expr, $mmap:expr, $config:expr, $field:ident, $extract_meta:expr, $metadata_type:path, $file_type:expr, $extract_templates:expr) => {{
if !$config.flags.skip_media_metadata {
$crate::extract_metadata_with_fallback!(
$stats.$field,
$extract_meta,
$stats,
$metadata_type,
$file_type.as_metadata_name()
);
}
$extract_templates
}};
}
#[macro_export]
macro_rules! ok_anyhow {
($value:expr) => {
::core::result::Result::<_, ::anyhow::Error>::Ok($value)
};
}
#[macro_export]
macro_rules! copy_metadata_fields {
($from:expr, $to:expr) => {
$to.image_metadata = $from.image_metadata.clone();
$to.video_metadata = $from.video_metadata.clone();
$to.audio_metadata = $from.audio_metadata.clone();
$to.csv_metadata = $from.csv_metadata.clone();
$to.pdf_metadata = $from.pdf_metadata.clone();
$to.docx_metadata = $from.docx_metadata.clone();
$to.sqlite_metadata = $from.sqlite_metadata.clone();
$to.toml_metadata = $from.toml_metadata.clone();
$to.zip_metadata = $from.zip_metadata.clone();
$to.xml_metadata = $from.xml_metadata.clone();
$to.html_metadata = $from.html_metadata.clone();
$to.yaml_metadata = $from.yaml_metadata.clone();
$to.ini_metadata = $from.ini_metadata.clone();
$to.pptx_metadata = $from.pptx_metadata.clone();
$to.epub_metadata = $from.epub_metadata.clone();
$to.archive_metadata = $from.archive_metadata.clone();
$to.code_metadata = $from.code_metadata.clone();
$to.log_metadata = $from.log_metadata.clone();
$to.json_metadata = $from.json_metadata.clone();
};
}
#[macro_export]
macro_rules! no_template_mining {
($name:ident, $doc:expr) => {
#[doc = $doc]
#[doc = "# Errors\n\nAlways returns `Ok` (metadata-only stub; no template mining)."]
pub fn $name(
_content: &[u8],
stats: &$crate::parsers::ParseResult,
_config: &$crate::config::RuntimeConfig,
) -> anyhow::Result<$crate::results::MiningResult> {
Ok($crate::parsers::traits::empty_mining_result(stats))
}
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParserCategory {
Media, Office, Structured, Settings, Container, Sqlite,
Code,
}
impl ParserCategory {
pub fn process(
self,
stats: &mut ParseResult,
mmap: &Mmap,
config: &RuntimeConfig,
) -> Result<MiningResult> {
match self {
ParserCategory::Media => media::process(stats, mmap, config),
ParserCategory::Office => office::process(stats, mmap, config),
ParserCategory::Structured => structured::process(stats, mmap, config),
ParserCategory::Settings => settings::process(stats, mmap, config),
ParserCategory::Container => container::process(stats, mmap, config),
ParserCategory::Sqlite => sqlite::process(stats, mmap, config),
ParserCategory::Code => code::process(stats, mmap, config),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum FileType {
Log = 0,
Json,
Text,
Markdown,
Image,
Video,
Audio,
Csv,
Pdf,
Docx,
Xlsx,
Sqlite,
Toml,
Zip,
Xml,
Html,
Yaml,
Ini,
Pptx,
Epub,
Archive,
Code,
#[default]
Unknown,
}
const METADATA_NAMES: [&str; 23] = [
"Log", "JSON", "Text", "Markdown", "Image", "Video", "Audio", "CSV", "PDF", "DOCX", "XLSX",
"SQLite", "TOML", "ZIP", "XML", "HTML", "YAML", "INI", "PPTX", "EPUB", "Archive", "Code",
"Unknown",
];
impl FileType {
#[must_use]
pub fn as_metadata_name(&self) -> &'static str {
METADATA_NAMES[*self as u8 as usize]
}
#[must_use]
pub fn from_metadata_name(s: &str) -> Option<Self> {
METADATA_NAMES
.iter()
.position(|&name| name == s)
.map(|i| match i {
0 => FileType::Log,
1 => FileType::Json,
2 => FileType::Text,
3 => FileType::Markdown,
4 => FileType::Image,
5 => FileType::Video,
6 => FileType::Audio,
7 => FileType::Csv,
8 => FileType::Pdf,
9 => FileType::Docx,
10 => FileType::Xlsx,
11 => FileType::Sqlite,
12 => FileType::Toml,
13 => FileType::Zip,
14 => FileType::Xml,
15 => FileType::Html,
16 => FileType::Yaml,
17 => FileType::Ini,
18 => FileType::Pptx,
19 => FileType::Epub,
20 => FileType::Archive,
21 => FileType::Code,
22 => FileType::Unknown,
_ => unreachable!("METADATA_NAMES and match arms must stay in sync"),
})
}
#[must_use]
pub fn binary_needs_processing(&self) -> bool {
self.parser_category().is_some()
}
#[must_use]
pub fn parser_category(self) -> Option<ParserCategory> {
match self {
FileType::Image | FileType::Video | FileType::Audio => Some(ParserCategory::Media),
FileType::Docx | FileType::Xlsx | FileType::Pptx => Some(ParserCategory::Office),
FileType::Csv | FileType::Html | FileType::Json | FileType::Epub | FileType::Pdf => {
Some(ParserCategory::Structured)
}
FileType::Toml | FileType::Yaml | FileType::Xml | FileType::Ini => {
Some(ParserCategory::Settings)
}
FileType::Zip | FileType::Archive => Some(ParserCategory::Container),
FileType::Sqlite => Some(ParserCategory::Sqlite),
FileType::Code => Some(ParserCategory::Code),
_ => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ParseResult {
pub file_path: String,
pub file_type: FileType,
pub line_count: usize,
pub byte_count: usize,
pub token_count: usize,
pub duration: std::time::Duration,
pub is_binary: bool,
pub mining_result: Option<MiningResult>,
pub image_metadata: Option<crate::results::ImageMetadata>,
pub video_metadata: Option<crate::results::VideoMetadata>,
pub audio_metadata: Option<crate::results::AudioMetadata>,
pub csv_metadata: Option<crate::results::CsvMetadata>,
pub pdf_metadata: Option<crate::results::PdfMetadata>,
pub docx_metadata: Option<crate::results::DocumentMetadata>,
pub sqlite_metadata: Option<crate::results::SqliteMetadata>,
pub toml_metadata: Option<crate::results::TomlMetadata>,
pub zip_metadata: Option<crate::results::ZipMetadata>,
pub xml_metadata: Option<crate::results::XmlMetadata>,
pub html_metadata: Option<crate::results::HtmlMetadata>,
pub yaml_metadata: Option<crate::results::YamlMetadata>,
pub ini_metadata: Option<crate::results::IniMetadata>,
pub pptx_metadata: Option<crate::results::PptxMetadata>,
pub epub_metadata: Option<crate::results::EpubMetadata>,
pub archive_metadata: Option<crate::results::ArchiveMetadata>,
pub code_metadata: Option<crate::results::CodeMetadata>,
pub log_metadata: Option<crate::results::LogMetadata>,
pub json_metadata: Option<crate::results::JsonMetadata>,
}
impl ParseResult {
fn set_metadata_fields(&self, output: &mut Output) {
copy_metadata_fields!(self, output);
}
#[must_use]
pub fn to_output(&self, mode: OutputMode, config: &RuntimeConfig) -> Output {
match mode {
OutputMode::Templates => self.build_templates_output(config),
OutputMode::Full => self.build_full_output(config),
}
}
fn check_for_writing_footprint(&self, output: &mut Output) {
if let Some(ref mining) = self.mining_result {
output
.writing_footprint
.clone_from(&mining.writing_footprint);
}
}
fn build_templates_output(&self, config: &RuntimeConfig) -> Output {
let source_path = if config.flags.redact_paths {
utils::path_string_helper::redact_path(&self.file_path)
} else {
self.file_path.clone()
};
let templates = self
.mining_result
.as_ref()
.map(|m| m.templates.clone())
.unwrap_or_default();
let mut output = Output::templates_only(
templates,
Some(source_path),
Some(format!("{:?}", self.file_type)),
);
self.check_for_writing_footprint(&mut output);
self.set_metadata_fields(&mut output);
output
}
fn build_full_output(&self, config: &RuntimeConfig) -> Output {
let source_path = if config.flags.redact_paths {
utils::path_string_helper::redact_path(&self.file_path)
} else {
self.file_path.clone()
};
let metadata = FileMetadata {
source: source_path,
file_type: format!("{:?}", self.file_type),
line_count: self.line_count,
byte_count: self.byte_count,
token_count: self.token_count,
processing_time_ms: self.duration.as_secs_f64() * 1000.0,
is_binary: self.is_binary,
};
let (templates, compression) = if let Some(ref mining) = self.mining_result {
(
mining.templates.clone(),
CompressionStats {
original_tokens: mining.original_tokens,
compressed_tokens: mining.compressed_tokens,
reduction_percent: mining.token_reduction_percent,
},
)
} else {
(
vec![],
CompressionStats {
original_tokens: self.token_count,
compressed_tokens: 0,
reduction_percent: 0.0,
},
)
};
let mut output = Output::full(templates, metadata, compression);
self.check_for_writing_footprint(&mut output);
self.set_metadata_fields(&mut output);
output
}
pub fn write_to_file(
&self,
output_path: &str,
mode: OutputMode,
config: &crate::config::RuntimeConfig,
) -> Result<()> {
let mut output_file = OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(output_path)?;
let output = self.to_output(mode, config);
let json = serde_json::to_string_pretty(&output)?;
writeln!(output_file, "{json}")?;
Ok(())
}
}
pub(crate) fn open_mmap(path: &str) -> Result<Mmap> {
let file = File::open(path)?;
let mmap = unsafe { Mmap::map(&file)? };
Ok(mmap)
}
pub fn initial_file_scan(path: &str) -> Result<ParseResult> {
let (stats, _) = initial_file_scan_with_mmap(path)?;
Ok(stats)
}
pub(crate) fn initial_file_scan_with_mmap(path: &str) -> Result<(ParseResult, Mmap)> {
let file_type = utils::filetypes::detect_file_type(path);
let mmap = open_mmap(path)?;
let byte_count = mmap.len();
let (line_count, token_count, is_binary) = match std::str::from_utf8(&mmap) {
Ok(content) => {
let line_count = content.lines().count();
let token_count = byte_count / 4;
(line_count, token_count, false)
}
Err(_) => (0, 0, true),
};
let stats = ParseResult {
file_path: path.to_string(),
file_type,
line_count,
byte_count,
token_count,
duration: std::time::Duration::ZERO,
is_binary,
..Default::default()
};
Ok((stats, mmap))
}
fn process_text_or_unknown(
stats: &mut ParseResult,
mmap: &Mmap,
config: &RuntimeConfig,
) -> Result<MiningResult> {
if stats.is_binary {
return Ok(traits::empty_mining_result(stats));
}
let content = std::str::from_utf8(mmap)?;
match stats.file_type {
FileType::Log => {
stats.log_metadata = Some(text::log::extract_log_metadata(content, stats));
text::log::extract_log_templates(content, stats, config)
}
FileType::Markdown => text::markdown::extract_markdown_templates(content, stats, config),
FileType::Text => text::plain_text::extract_text_templates(content, stats, config),
FileType::Unknown => extract_unknown_templates(content, stats, config),
file_type if file_type.binary_needs_processing() => unreachable!(),
_ => unreachable!(),
}
}
pub fn extract_templates(
stats: &mut ParseResult,
config: &RuntimeConfig,
mmap: Option<&Mmap>,
) -> Result<MiningResult> {
let owned_mmap;
let mmap_ref: &Mmap = if let Some(m) = mmap {
m
} else {
owned_mmap = open_mmap(&stats.file_path)?;
&owned_mmap
};
match stats.file_type.parser_category() {
Some(cat) => cat.process(stats, mmap_ref, config),
None => process_text_or_unknown(stats, mmap_ref, config),
}
}
pub(crate) fn estimate_compressed_tokens_with_footprint(
templates: &[Template],
_total_lines: usize,
config: &RuntimeConfig,
writing_footprint: Option<&crate::results::WritingFootprint>,
) -> usize {
let template_tokens: usize = templates
.iter()
.map(|t| t.pattern.split_whitespace().count())
.sum();
let example_limit = config.max_examples_per_placeholder.min(5);
let example_tokens: usize = templates
.iter()
.flat_map(|t| t.examples.values())
.map(|v| v.len().min(example_limit))
.sum();
let footprint_tokens = if let Some(footprint) = writing_footprint {
let mut tokens = config.footprint_base_overhead_tokens; if let Some(ref svo) = footprint.svo_analysis {
tokens += config.footprint_svo_metrics_tokens; tokens += svo.common_pivots.len().min(config.max_common_pivots); }
tokens
} else {
0
};
template_tokens + example_tokens + footprint_tokens + config.json_overhead_tokens
}
fn extract_unknown_templates(
content: &str,
stats: &mut ParseResult,
config: &RuntimeConfig,
) -> Result<MiningResult> {
serde_json::from_str::<serde_json::Value>(content.lines().next().unwrap_or("")).map_or_else(
|_| text::log::extract_log_templates(content, stats, config),
|_| structured::extract_json_templates(content, stats, config),
)
}