use clap::CommandFactory;
use std::fs;
use std::path::Path;
use std::process;
use std::time::UNIX_EPOCH;
use revelo_core::computed_fields::fill_computed_fields;
use revelo_core::multi_file::MultiFileLoader;
use revelo_core::multi_file::find_duplicate_streams;
use revelo_core::{FileAnalyze, FileLevelInfo, StreamKind, fill_file_level_fields};
use revelo_dispatcher::detect;
use revelo_export::{to_csv, to_json, to_summary, to_text, to_xml};
use revelo_parsers_tag::parse_tags;
mod cli;
use cli::Cli;
fn main() -> process::ExitCode {
let cli = <Cli as clap::Parser>::parse();
let path = match cli.path {
Some(p) => p,
None => {
let _ = Cli::command().print_help();
println!();
return process::ExitCode::SUCCESS;
}
};
let bytes = match fs::read(&path) {
Ok(b) => b,
Err(e) => {
eprintln!("{path}: {e}");
return process::ExitCode::from(1);
}
};
let metadata = fs::metadata(&path).ok();
let mut parsed = false;
let mut extra_data: Option<Vec<u8>> = None;
if cli.multi_file {
let mut loader = MultiFileLoader::new();
loader.scan_references(std::path::Path::new(&path), &Default::default());
if let Some((data, _count)) = loader.load_all() {
extra_data = Some(data);
}
}
let parse_buf: Vec<u8> = if let Some(ref extra) = extra_data {
let mut combined = bytes.clone();
combined.extend_from_slice(extra);
combined
} else {
bytes.clone()
};
let winner = detect(&parse_buf);
if let Some(winner) = winner {
let mut fa = FileAnalyze::new(&parse_buf);
fa.set_option("demux", &cli.demux);
fa.set_option("trace_level", &cli.trace);
fa.set_option("multi_file", if cli.multi_file { "1" } else { "0" });
if extra_data.is_some() {
fa.reference_count = 1;
}
if winner(&mut fa) {
parsed = true;
let modified_unix_secs = metadata
.as_ref()
.and_then(|m| m.modified().ok())
.and_then(|t| t.duration_since(UNIX_EPOCH).ok())
.map(|d| d.as_secs() as i64);
let info = FileLevelInfo {
file_size: metadata.as_ref().map(|m| m.len()).unwrap_or(bytes.len() as u64),
extension: Path::new(&path).extension().and_then(|s| s.to_str()),
modified_unix_secs,
local_offset_secs: local_offset_seconds(),
};
fill_file_level_fields(&mut fa, &info);
fill_computed_fields(fa.streams_mut());
fa.duplicate_indices = find_duplicate_streams(fa.streams());
let _ = parse_tags(&mut fa);
if cli.verify {
let is_complete = if fa.truncated() { "No" } else { "Yes" };
fa.force_field(StreamKind::General, 0, "IsComplete", is_complete);
if fa.truncated() {
fa.force_field(
StreamKind::General,
0,
"Warning",
"File appears truncated — parser was unable to read the full structure",
);
}
}
if cli.video_only || cli.audio_only || !cli.stream.is_empty() {
let mut keep_kinds = vec![StreamKind::General];
if cli.video_only {
keep_kinds.push(StreamKind::Video);
}
if cli.audio_only {
keep_kinds.push(StreamKind::Audio);
}
if cli.video_only && !cli.audio_only && cli.stream.is_empty() {
} else if !cli.video_only && cli.audio_only && cli.stream.is_empty() {
} else if !cli.video_only && !cli.audio_only && cli.stream.is_empty() {
keep_kinds.extend_from_slice(&[
StreamKind::Video,
StreamKind::Audio,
StreamKind::Text,
StreamKind::Other,
StreamKind::Image,
StreamKind::Menu,
StreamKind::Exif,
StreamKind::Iptc,
StreamKind::Xmp,
StreamKind::Icc,
StreamKind::C2pa,
StreamKind::MakerNotes,
]);
}
fa.streams_mut().filter_keep(&keep_kinds, &cli.stream);
}
let output = if cli.json {
to_json(fa.streams(), &path)
} else if cli.xml {
to_xml(fa.streams(), &path)
} else if cli.csv {
to_csv(fa.streams(), &path)
} else if cli.summary {
to_summary(fa.streams(), &path)
} else {
format_text_output(
&to_text(fa.streams(), &path),
cli.inform_version,
cli.inform_timestamp,
)
};
if let Some(ref log_file) = cli.log_file {
let _ = fs::write(log_file, &output);
} else {
println!("{output}");
}
}
}
if !parsed {
eprintln!("{path}: no parser matched ({} bytes)", bytes.len());
return process::ExitCode::from(1);
}
process::ExitCode::SUCCESS
}
fn format_text_output(text: &str, add_version: bool, add_timestamp: bool) -> String {
if !add_version && !add_timestamp {
return text.to_owned();
}
let mut header = String::new();
if add_timestamp {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
header.push_str(&format!("Report created: {}\n", now));
}
if add_version {
header.push_str(&format!("Library version: revelo {}\n", env!("CARGO_PKG_VERSION")));
}
if !header.is_empty() {
header.push('\n');
}
header + text
}
fn local_offset_seconds() -> i64 {
let Ok(out) = process::Command::new("date").arg("+%z").output() else {
return 0;
};
let s = String::from_utf8_lossy(&out.stdout);
let s = s.trim();
if s.len() < 5 {
return 0;
}
let sign = if s.starts_with('-') { -1 } else { 1 };
let hh: i64 = s[1..3].parse().unwrap_or(0);
let mm: i64 = s[3..5].parse().unwrap_or(0);
sign * (hh * 3600 + mm * 60)
}