use std::path::{Path, PathBuf};
use slpc::toml_edit::DocumentMut;
use crate::i18n::{fill, t, tn};
pub struct Detail {
pub path: PathBuf,
pub relative: String,
pub outcome: Outcome,
}
pub enum Outcome {
Unreadable(String),
Read(Box<Contents>),
}
pub struct Contents {
pub flyleaf: DocumentMut,
pub content: ContentFile,
}
pub struct ContentFile {
pub name: String,
pub size: u64,
pub unreadable: Option<String>,
}
impl ContentFile {
#[must_use]
pub fn can_be_opened(&self) -> bool {
self.unreadable.is_none()
}
#[must_use]
pub fn size_line(&self) -> String {
let n = self.size;
if n < 1024 {
return fill(tn("{n} byte", "{n} bytes", n), &[("n", &n.to_string())]);
}
let units = ["KiB", "MiB", "GiB", "TiB", "PiB"];
#[allow(clippy::cast_precision_loss)]
let mut scaled = n as f64 / 1024.0;
let mut unit = units[0];
for next in &units[1..] {
if scaled < 1024.0 {
break;
}
scaled /= 1024.0;
unit = next;
}
fill(
t("{size} {unit} ({n} bytes)"),
&[
("size", &format!("{scaled:.1}")),
("unit", unit),
("n", &n.to_string()),
],
)
}
}
impl Detail {
#[must_use]
pub fn open(root: &Path, relative: &str) -> Self {
let path = root.join(relative);
let outcome = match slpc::Container::open(&path) {
Ok(container) => Outcome::Read(Box::new(Contents::of(&container))),
Err(e) => Outcome::Unreadable(e.to_string()),
};
Self {
path,
relative: relative.to_owned(),
outcome,
}
}
#[must_use]
pub fn content(&self) -> Option<&ContentFile> {
match &self.outcome {
Outcome::Unreadable(_) => None,
Outcome::Read(contents) => Some(&contents.content),
}
}
pub fn extract_to(&self, dir: &Path) -> slpc::Result<PathBuf> {
let mut container = slpc::Container::open(&self.path)?;
let out = slpc::content_path(dir, container.content_name())?;
let mut reader = container.content()?;
let mut file = std::fs::File::create(&out)?;
std::io::copy(&mut reader, &mut file)?;
Ok(out)
}
}
impl Contents {
fn of<R: std::io::Read + std::io::Seek>(container: &slpc::Container<R>) -> Self {
Self {
flyleaf: container.flyleaf().clone(),
content: ContentFile {
name: slpc::display_name(container.content_name()).into_owned(),
size: container.content_size().unwrap_or(0),
unreadable: container
.check_content_readable()
.err()
.map(|why| why.to_string()),
},
}
}
}
#[cfg(test)]
mod tests {
use super::ContentFile;
fn content_file(size: u64) -> ContentFile {
ContentFile {
name: "a.pdf".to_owned(),
size,
unreadable: None,
}
}
#[test]
fn a_scaled_size_still_states_the_bytes() {
let line = content_file(1_536).size_line();
assert!(line.contains("1.5 KiB"), "{line}");
assert!(line.contains("1536"), "{line}");
}
#[test]
fn a_small_content_file_is_stated_in_bytes_alone() {
assert_eq!(content_file(900).size_line(), "900 bytes");
assert_eq!(content_file(1).size_line(), "1 byte");
assert_eq!(content_file(0).size_line(), "0 bytes");
}
#[test]
fn scaling_climbs_past_the_first_unit() {
assert!(content_file(5 * 1024 * 1024).size_line().contains("5.0 MiB"));
assert!(content_file(3 * 1024 * 1024 * 1024)
.size_line()
.contains("3.0 GiB"));
}
#[test]
fn a_content_file_that_cannot_be_decoded_is_not_offered() {
assert!(content_file(10).can_be_opened());
let encrypted = ContentFile {
unreadable: Some("encrypted".to_owned()),
..content_file(10)
};
assert!(!encrypted.can_be_opened());
}
}