use std::{
ffi::OsString,
io::{Read, Seek},
path::Path,
time::Instant,
};
use zarust::{ArchiveReader, EntryKind, NodeHandle, ROOT_NODE};
use crate::cli::{
Failure, Outcome,
args::Options,
extract::{Totals, measure},
ui::{Reporter, counted, display_path, format_bytes, format_duration},
};
pub fn list(
reporter: &mut Reporter,
archive: &Path,
path: Option<OsString>,
options: &Options,
) -> Outcome {
let reader = super::open_archive(archive)?;
let (root, prefix) = match &path {
Some(path) => {
let text = path.to_string_lossy();
let node = reader.lookup(text.as_bytes()).ok_or_else(|| {
Failure::new(format!("`{text}` is not in `{}`", display_path(archive)))
})?;
(node, text.trim_matches('/').to_owned())
}
None => (ROOT_NODE, String::new()),
};
if reader.entry_kind(root) == Some(EntryKind::File) {
let size = reader.file_size(root).map_err(Failure::from)?;
reporter.line(format_args!("{:>10} {prefix}", format_bytes(size as f64)))?;
return Ok(());
}
if options.tree {
reporter.line(format_args!(
"{}{}{}",
reporter.palette().bold,
if prefix.is_empty() { "." } else { &prefix },
reporter.palette().reset
))?;
print_tree(reporter, &reader, root, &mut String::new())?;
} else {
print_flat(reporter, &reader, root, &prefix)?;
}
let mut totals = Totals::default();
measure(&reader, root, &mut totals)?;
reporter.info(format_args!(
"{dim}{}, {} · {}{reset}",
counted(totals.files, "file", "files"),
counted(totals.directories, "directory", "directories"),
format_bytes(totals.bytes as f64),
dim = reporter.palette().dim,
reset = reporter.palette().reset,
))
}
fn print_flat<R: Read + Seek>(
reporter: &mut Reporter,
reader: &ArchiveReader<R>,
directory: NodeHandle,
prefix: &str,
) -> Outcome {
let count = reader.directory_len(directory).map_err(Failure::from)?;
for index in 0..count {
let entry = reader
.directory_entry(directory, index)
.map_err(Failure::from)?;
let name = String::from_utf8_lossy(entry.name);
let path = if prefix.is_empty() {
name.into_owned()
} else {
format!("{prefix}/{name}")
};
let p = reporter.palette();
match entry.kind {
EntryKind::Directory => {
reporter.line(format_args!(
"{d}{:>10}{r} {c}{path}/{r}",
"-",
d = p.dim,
c = p.cyan,
r = p.reset
))?;
print_flat(reporter, reader, entry.handle, &path)?;
}
EntryKind::File => reporter.line(format_args!(
"{y}{:>10}{r} {path}",
format_bytes(entry.size as f64),
y = p.yellow,
r = p.reset
))?,
}
}
Ok(())
}
fn print_tree<R: Read + Seek>(
reporter: &mut Reporter,
reader: &ArchiveReader<R>,
directory: NodeHandle,
indent: &mut String,
) -> Outcome {
let count = reader.directory_len(directory).map_err(Failure::from)?;
for index in 0..count {
let entry = reader
.directory_entry(directory, index)
.map_err(Failure::from)?;
let last = index + 1 == count;
let branch = if last { "└── " } else { "├── " };
let name = String::from_utf8_lossy(entry.name);
let p = reporter.palette();
match entry.kind {
EntryKind::Directory => {
reporter.line(format_args!(
"{d}{indent}{branch}{r}{c}{b}{name}/{r}",
d = p.dim,
c = p.cyan,
b = p.bold,
r = p.reset
))?;
let width = indent.len();
indent.push_str(if last { " " } else { "│ " });
print_tree(reporter, reader, entry.handle, indent)?;
indent.truncate(width);
}
EntryKind::File => reporter.line(format_args!(
"{d}{indent}{branch}{r}{name} {y}{}{r}",
format_bytes(entry.size as f64),
d = p.dim,
y = p.yellow,
r = p.reset
))?,
}
}
Ok(())
}
pub fn info(reporter: &mut Reporter, archive: &Path) -> Outcome {
let reader = super::open_archive(archive)?;
let mut totals = Totals::default();
measure(&reader, ROOT_NODE, &mut totals)?;
let stored = reader.archive_size();
let compressed = reader.compressed_data_size();
let ratio = if totals.bytes == 0 {
100.0
} else {
compressed as f64 * 100.0 / totals.bytes as f64
};
let p = reporter.palette();
reporter.line(format_args!(
"{c}{b}{}{r}",
display_path(archive),
c = p.cyan,
b = p.bold,
r = p.reset
))?;
let mut field = |name: &str, value: String| -> Outcome {
reporter.line(format_args!(
" {d}{name:<13}{r}{value}",
d = p.dim,
r = p.reset
))
};
let number = |text: String| format!("{}{text}{}", p.yellow, p.reset);
field("files", number(totals.files.to_string()))?;
field("directories", number(totals.directories.to_string()))?;
field("contents", number(format_bytes(totals.bytes as f64)))?;
field(
"compressed",
format!(
"{} {d}({ratio:.1}% of contents){r}",
number(format_bytes(compressed as f64)),
d = p.dim,
r = p.reset
),
)?;
field("archive", number(format_bytes(stored as f64)))?;
Ok(())
}
pub fn verify(reporter: &mut Reporter, archive: &Path) -> Outcome {
let mut reader = super::open_archive(archive)?;
let started = Instant::now();
let size = reader.archive_size();
reporter.start_bar("verifying", size);
let intact = reader
.verify_integrity()
.map_err(|error| Failure::new(format!("cannot read `{}`: {error}", display_path(archive))));
reporter.finish_bar();
let palette = reporter.palette();
match intact? {
true => {
reporter.status(
palette.green,
"ok",
format_args!(
"{bold}{}{reset}",
display_path(archive),
bold = palette.bold,
reset = palette.reset
),
)?;
reporter.sub(format_args!(
"{} hashed in {}",
format_bytes(size as f64),
format_duration(Some(started.elapsed()))
))?;
Ok(())
}
false => Err(Failure::new(format!(
"`{}` does not match its integrity hash",
display_path(archive)
))),
}
}