mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-11-14 08:05:40 +00:00
EXTH 113 is an ASIN only when it is ten letters and digits; a UUID goes under uuid, as the EPUB reader files it, and anything else under other. With no EXTH 201 or 202, the first record from the first image index that is a JPEG, PNG or GIF is the cover, stopping at the KF8 boundary (EXTH 121).
145 lines
5.2 KiB
Rust
145 lines
5.2 KiB
Rust
//! MOBI and AZW3 files as a converter writes them, and as they break.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
|
|
fn fixture(name: &str) -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures")
|
|
.join(name)
|
|
}
|
|
|
|
fn read(name: &str) -> bookmeta::Metadata {
|
|
bookmeta::read(&fixture(name))
|
|
}
|
|
|
|
/// What the two converted fixtures were given, and so must say.
|
|
fn says_what_it_was_given(meta: &bookmeta::Metadata) {
|
|
assert_eq!(meta.source, "mobi");
|
|
assert_eq!(meta.title.as_deref(), Some("A Lantern in the Orchard"));
|
|
assert_eq!(meta.authors, ["Ada Quill", "Tomás Reed"]);
|
|
assert_eq!(meta.publisher.as_deref(), Some("Orchard Press"));
|
|
assert_eq!(
|
|
meta.description.as_deref(),
|
|
Some("A short story written for testing.")
|
|
);
|
|
assert_eq!(meta.date.as_deref(), Some("2001-04-05T04:00:00+00:00"));
|
|
assert_eq!(meta.language.as_deref(), Some("en"));
|
|
assert_eq!(meta.subjects, ["Fiction", "Villages"]);
|
|
assert_eq!(meta.identifiers["isbn"], "9780306406157");
|
|
assert_eq!(meta.identifiers.get("asin"), None);
|
|
assert_eq!(meta.identifiers["uuid"].len(), 36);
|
|
|
|
let cover = meta.cover.as_ref().expect("a cover");
|
|
assert_eq!(cover.media_type, "image/jpeg");
|
|
let original = std::fs::read(fixture("lantern-cover.jpg")).expect("the cover fixture");
|
|
assert_eq!(cover.bytes, original);
|
|
}
|
|
|
|
#[test]
|
|
fn a_mobi_6_file_says_what_it_was_given() {
|
|
let meta = read("lantern.mobi");
|
|
says_what_it_was_given(&meta);
|
|
assert!(meta.notes.is_empty(), "{:?}", meta.notes);
|
|
}
|
|
|
|
#[test]
|
|
fn an_azw3_file_says_what_it_was_given() {
|
|
let meta = read("lantern.azw3");
|
|
says_what_it_was_given(&meta);
|
|
assert!(meta.notes.is_empty(), "{:?}", meta.notes);
|
|
}
|
|
|
|
#[test]
|
|
fn exth_113_uuid_is_not_an_asin() {
|
|
for name in ["lantern.mobi", "lantern.azw3"] {
|
|
let meta = read(name);
|
|
assert_eq!(meta.identifiers.get("asin"), None, "{name}");
|
|
assert!(meta.identifiers.contains_key("uuid"), "{name}");
|
|
}
|
|
}
|
|
|
|
/// `lantern.mobi` with its cover and thumbnail offsets (EXTH 201 and 202)
|
|
/// retyped to an unused record type, so the file names no cover.
|
|
fn lantern_naming_no_cover() -> Vec<u8> {
|
|
let mut bytes = std::fs::read(fixture("lantern.mobi")).expect("the fixture");
|
|
let exth = bytes.windows(4).position(|w| w == b"EXTH").expect("EXTH");
|
|
let count = u32::from_be_bytes(bytes[exth + 8..exth + 12].try_into().unwrap());
|
|
let mut cursor = exth + 12;
|
|
let mut retyped = 0;
|
|
for _ in 0..count {
|
|
let kind = u32::from_be_bytes(bytes[cursor..cursor + 4].try_into().unwrap());
|
|
let size = u32::from_be_bytes(bytes[cursor + 4..cursor + 8].try_into().unwrap());
|
|
if kind == 201 || kind == 202 {
|
|
bytes[cursor..cursor + 4].copy_from_slice(&999u32.to_be_bytes());
|
|
retyped += 1;
|
|
}
|
|
cursor += size as usize;
|
|
}
|
|
assert!(retyped > 0, "the fixture names a cover");
|
|
bytes
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_naming_no_cover_gets_its_first_image_record() {
|
|
let meta = bookmeta::from_bytes("x.mobi", &lantern_naming_no_cover());
|
|
let cover = meta.cover.expect("the first image record");
|
|
assert_eq!(cover.media_type, "image/jpeg");
|
|
let original = std::fs::read(fixture("lantern-cover.jpg")).expect("the cover fixture");
|
|
assert_eq!(cover.bytes, original);
|
|
}
|
|
|
|
#[test]
|
|
fn a_file_with_no_exth_still_has_its_full_name_and_locale() {
|
|
let meta = read("lantern-no-exth.mobi");
|
|
assert_eq!(meta.source, "mobi");
|
|
assert_eq!(meta.title.as_deref(), Some("A Lantern in the Orchard"));
|
|
assert_eq!(meta.language.as_deref(), Some("en"));
|
|
assert!(meta.authors.is_empty());
|
|
assert!(meta.identifiers.is_empty());
|
|
// No EXTH names no cover, so the first image record stands in for one.
|
|
let cover = meta.cover.expect("the first image record");
|
|
assert_eq!(cover.media_type, "image/jpeg");
|
|
}
|
|
|
|
#[test]
|
|
fn a_truncated_file_is_an_empty_record_with_a_reason() {
|
|
let meta = read("lantern-truncated.mobi");
|
|
assert_eq!(meta.source, "none");
|
|
assert!(meta.is_empty());
|
|
assert_eq!(meta.notes, ["the first record lies outside the file"]);
|
|
}
|
|
|
|
#[test]
|
|
fn a_drm_flagged_file_has_its_metadata_and_says_drm() {
|
|
let meta = read("lantern-drm.mobi");
|
|
says_what_it_was_given(&meta);
|
|
assert_eq!(meta.notes, ["drm"]);
|
|
}
|
|
|
|
#[test]
|
|
fn every_truncation_of_the_mobi_fixture_returns() {
|
|
let bytes = std::fs::read(fixture("lantern.mobi")).expect("the fixture");
|
|
let first_record_end = u32::from_be_bytes(bytes[86..90].try_into().unwrap()) as usize;
|
|
for len in 0..=bytes.len() {
|
|
let meta = bookmeta::from_bytes("cut.mobi", &bytes[..len]);
|
|
if len < first_record_end {
|
|
assert!(meta.is_empty(), "cut at {len}: {meta:?}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn every_single_byte_corruption_of_the_header_returns() {
|
|
let bytes = std::fs::read(fixture("lantern.mobi")).expect("the fixture");
|
|
// The Palm header, the record list, and the whole of the first record:
|
|
// every offset the reader follows is in there.
|
|
let first_record_end = u32::from_be_bytes(bytes[86..90].try_into().unwrap()) as usize;
|
|
for at in 0..first_record_end {
|
|
for value in [0x00, 0x7f, 0xff] {
|
|
let mut broken = bytes.clone();
|
|
broken[at] = value;
|
|
let _ = bookmeta::from_bytes("broken.mobi", &broken);
|
|
}
|
|
}
|
|
}
|