use serde_json::json;
use std::time::{SystemTime, UNIX_EPOCH};
use zesven::read::{ArchiveInfo, Entry, ExtractResult, TestResult};
pub trait OutputFormatter {
fn format_list(&self, entries: &[Entry], technical: bool) -> String;
fn format_info(&self, info: &ArchiveInfo) -> String;
fn format_extract_result(&self, result: &ExtractResult) -> String;
fn format_test_result(&self, result: &TestResult) -> String;
}
pub struct HumanFormatter;
impl OutputFormatter for HumanFormatter {
fn format_list(&self, entries: &[Entry], technical: bool) -> String {
let mut output = String::new();
if technical {
output.push_str(&format!(
"{:>12} {:>12} {:>19} {:>10} {}\n",
"Size", "Packed", "Modified", "CRC", "Name"
));
} else {
output.push_str(&format!("{:>12} {:>19} {}\n", "Size", "Modified", "Name"));
}
output.push_str(&"-".repeat(70));
output.push('\n');
let mut total_size: u64 = 0;
let mut file_count = 0;
let mut dir_count = 0;
for entry in entries {
if entry.is_directory {
dir_count += 1;
} else {
file_count += 1;
total_size += entry.size;
}
let size_str = if entry.is_directory {
String::new()
} else {
humanize_bytes(entry.size)
};
let mtime_str = entry
.modified()
.map(format_timestamp)
.unwrap_or_else(|| "-".to_string());
let type_indicator = if entry.is_directory { "D" } else { "" };
if technical {
let crc_str = entry
.crc32
.map(|c| format!("{:08X}", c))
.unwrap_or_else(|| "-".to_string());
output.push_str(&format!(
"{:>12} {:>12} {:>19} {:>10} {}{}\n",
size_str,
"-", mtime_str,
crc_str,
entry.path.as_str(),
type_indicator
));
} else {
output.push_str(&format!(
"{:>12} {:>19} {}{}\n",
size_str,
mtime_str,
entry.path.as_str(),
type_indicator
));
}
}
output.push_str(&"-".repeat(70));
output.push('\n');
output.push_str(&format!(
"{} files, {} directories, {} total\n",
file_count,
dir_count,
humanize_bytes(total_size)
));
output
}
fn format_info(&self, info: &ArchiveInfo) -> String {
let mut output = String::new();
output.push_str("Archive Information:\n");
output.push_str(&"-".repeat(40));
output.push('\n');
output.push_str(&format!(" Entries: {}\n", info.entry_count));
output.push_str(&format!(
" Total size: {}\n",
humanize_bytes(info.total_size)
));
output.push_str(&format!(
" Packed size: {}\n",
humanize_bytes(info.packed_size)
));
output.push_str(&format!(
" Ratio: {:.1}%\n",
info.compression_ratio() * 100.0
));
output.push_str(&format!(
" Space savings: {:.1}%\n",
info.space_savings() * 100.0
));
output.push_str(&format!(
" Solid: {}\n",
if info.is_solid { "Yes" } else { "No" }
));
output.push_str(&format!(" Folders: {}\n", info.folder_count));
if !info.compression_methods.is_empty() {
let methods: Vec<_> = info
.compression_methods
.iter()
.map(|m| format!("{:?}", m))
.collect();
output.push_str(&format!(" Methods: {}\n", methods.join(", ")));
}
if info.has_encrypted_entries {
output.push_str(" Encrypted: Yes\n");
}
if info.has_encrypted_header {
output.push_str(" Header enc.: Yes\n");
}
output
}
fn format_extract_result(&self, result: &ExtractResult) -> String {
let mut output = String::new();
if result.is_ok() {
output.push_str(&format!(
"Extracted {} files ({} bytes)\n",
result.entries_extracted,
humanize_bytes(result.bytes_extracted)
));
if result.entries_skipped > 0 {
output.push_str(&format!("Skipped {} files\n", result.entries_skipped));
}
} else {
output.push_str("Extraction completed with errors:\n");
output.push_str(&format!(" Extracted: {}\n", result.entries_extracted));
output.push_str(&format!(" Skipped: {}\n", result.entries_skipped));
output.push_str(&format!(" Failed: {}\n", result.entries_failed));
if !result.failures.is_empty() {
output.push_str("\nFailures:\n");
for (path, error) in &result.failures {
output.push_str(&format!(" {}: {}\n", path, error));
}
}
}
output
}
fn format_test_result(&self, result: &TestResult) -> String {
let mut output = String::new();
if result.is_ok() {
output.push_str(&format!(
"OK - {} files tested, all passed\n",
result.entries_tested
));
} else {
output.push_str("Test completed with errors:\n");
output.push_str(&format!(" Tested: {}\n", result.entries_tested));
output.push_str(&format!(" Passed: {}\n", result.entries_passed));
output.push_str(&format!(" Failed: {}\n", result.entries_failed));
if !result.failures.is_empty() {
output.push_str("\nFailures:\n");
for (path, error) in &result.failures {
output.push_str(&format!(" {}: {}\n", path, error));
}
}
}
output
}
}
pub struct JsonFormatter;
impl OutputFormatter for JsonFormatter {
fn format_list(&self, entries: &[Entry], _technical: bool) -> String {
let items: Vec<_> = entries
.iter()
.map(|e| {
json!({
"path": e.path.as_str(),
"size": e.size,
"modified": e.modified().and_then(|t| t.duration_since(UNIX_EPOCH).ok()).map(|d| d.as_secs()),
"crc32": e.crc32,
"is_directory": e.is_directory,
"encrypted": e.is_encrypted,
})
})
.collect();
serde_json::to_string_pretty(&items).unwrap_or_else(|_| "[]".to_string())
}
fn format_info(&self, info: &ArchiveInfo) -> String {
let obj = json!({
"entry_count": info.entry_count,
"total_size": info.total_size,
"packed_size": info.packed_size,
"compression_ratio": info.compression_ratio(),
"space_savings": info.space_savings(),
"is_solid": info.is_solid,
"folder_count": info.folder_count,
"has_encrypted_entries": info.has_encrypted_entries,
"has_encrypted_header": info.has_encrypted_header,
"compression_methods": info.compression_methods.iter().map(|m| format!("{:?}", m)).collect::<Vec<_>>(),
});
serde_json::to_string_pretty(&obj).unwrap_or_else(|_| "{}".to_string())
}
fn format_extract_result(&self, result: &ExtractResult) -> String {
let obj = json!({
"success": result.is_ok(),
"entries_extracted": result.entries_extracted,
"entries_skipped": result.entries_skipped,
"entries_failed": result.entries_failed,
"bytes_extracted": result.bytes_extracted,
"failures": result.failures.iter().map(|(p, e)| json!({"path": p, "error": e})).collect::<Vec<_>>(),
});
serde_json::to_string_pretty(&obj).unwrap_or_else(|_| "{}".to_string())
}
fn format_test_result(&self, result: &TestResult) -> String {
let obj = json!({
"success": result.is_ok(),
"entries_tested": result.entries_tested,
"entries_passed": result.entries_passed,
"entries_failed": result.entries_failed,
"failures": result.failures.iter().map(|(p, e)| json!({"path": p, "error": e})).collect::<Vec<_>>(),
});
serde_json::to_string_pretty(&obj).unwrap_or_else(|_| "{}".to_string())
}
}
pub fn create_formatter(format: super::OutputFormat) -> Box<dyn OutputFormatter> {
match format {
super::OutputFormat::Human => Box::new(HumanFormatter),
super::OutputFormat::Json => Box::new(JsonFormatter),
}
}
pub fn humanize_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
const TB: u64 = GB * 1024;
if bytes >= TB {
format!("{:.1} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn format_timestamp(time: SystemTime) -> String {
match time.duration_since(UNIX_EPOCH) {
Ok(duration) => {
let secs = duration.as_secs();
let days_since_epoch = secs / 86400;
let time_of_day = secs % 86400;
let hours = time_of_day / 3600;
let minutes = (time_of_day % 3600) / 60;
let seconds = time_of_day % 60;
let mut year = 1970;
let mut remaining_days = days_since_epoch as i64;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if remaining_days < days_in_year {
break;
}
remaining_days -= days_in_year;
year += 1;
}
let (month, day) = days_to_month_day(remaining_days as u32, is_leap_year(year));
format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
year, month, day, hours, minutes, seconds
)
}
Err(_) => "-".to_string(),
}
}
fn is_leap_year(year: i64) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn days_to_month_day(day_of_year: u32, leap: bool) -> (u32, u32) {
let days_in_months: [u32; 12] = if leap {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut remaining = day_of_year;
for (i, &days) in days_in_months.iter().enumerate() {
if remaining < days {
return (i as u32 + 1, remaining + 1);
}
remaining -= days;
}
(12, 31) }