//! Tables of contents and `encryption.xml`, read.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::io::{Cursor, Write};
use craft_epub::{Encrypted, Epub, MAX_TOC_ENTRIES, TocEntry, parse_encryption, parse_nav, parse_ncx};
fn zip_of(entries: &[(&str, &[u8])]) -> Vec {
let mut z = zip::ZipWriter::new(Cursor::new(Vec::new()));
let o = zip::write::SimpleFileOptions::default();
for (name, data) in entries {
z.start_file(*name, o).unwrap();
z.write_all(data).unwrap();
}
z.finish().unwrap().into_inner()
}
const CONTAINER: &[u8] = br#""#;
fn e(label: &str, path: Option<&str>, fragment: Option<&str>, level: u8) -> TocEntry {
TocEntry { label: label.into(), path: path.map(str::to_string), fragment: fragment.map(str::to_string), level }
}
const NAV: &[u8] = br##"
"##;
#[test]
fn a_navigation_document_is_flattened_with_levels() {
let toc = parse_nav(NAV, "OPS/nav/nav.xhtml").unwrap();
assert_eq!(
toc,
[
e("Chapter One & more", Some("OPS/text/c1.xhtml"), Some("start"), 1),
e("Section", Some("OPS/nav/c 1.xhtml"), None, 2),
e("Not a link", None, None, 2),
e("Deep", Some("OPS/nav/nav.xhtml"), Some("local"), 3),
e("Two", Some("abs.xhtml"), None, 1),
]
);
// Without a `toc` the first `nav` is used; without any there is no table of contents.
assert_eq!(parse_nav(br#""#, "nav.xhtml").unwrap().len(), 1);
assert_eq!(parse_nav(b"", "nav.xhtml").unwrap(), []);
assert_eq!(parse_nav(b"", "nav.xhtml").unwrap(), []);
assert!(parse_nav(b"", "nav.xhtml").is_err());
// An HTML entity is not XML: it is dropped, not a reason to refuse the table of contents.
assert_eq!(parse_nav(br#""#, "nav.xhtml").unwrap()[0].label, "AB");
}
const NCX: &[u8] = br#"
TOne One one Two"#;
#[test]
fn an_ncx_is_flattened_with_levels() {
let toc = parse_ncx(NCX, "OPS/toc.ncx").unwrap();
assert_eq!(toc, [e("One", Some("OPS/text/c1.html"), None, 1), e("One one", Some("OPS/text/c1.html"), Some("s"), 2), e("Two", None, None, 1)]);
assert_eq!(parse_ncx(b"", "toc.ncx").unwrap(), []);
}
#[test]
fn the_package_gives_its_navigation_or_else_its_ncx() {
let opf3 = br#""#;
let bytes = zip_of(&[("META-INF/container.xml", CONTAINER), ("OPS/book.opf", opf3), ("OPS/nav/nav.xhtml", NAV), ("OPS/toc.ncx", NCX)]);
assert_eq!(Epub::open(&bytes).unwrap().toc().unwrap().len(), 5, "the navigation document comes first");
// The navigation document is missing: the NCX is used.
let bytes = zip_of(&[("META-INF/container.xml", CONTAINER), ("OPS/book.opf", opf3), ("OPS/toc.ncx", NCX)]);
assert_eq!(Epub::open(&bytes).unwrap().toc().unwrap().len(), 3);
// Neither is there.
let bytes = zip_of(&[("META-INF/container.xml", CONTAINER), ("OPS/book.opf", opf3)]);
assert_eq!(Epub::open(&bytes).unwrap().toc().unwrap(), []);
let bytes = zip_of(&[("META-INF/container.xml", CONTAINER), ("OPS/book.opf", b"")]);
assert_eq!(Epub::open(&bytes).unwrap().toc().unwrap(), []);
}
/// Nesting as deep as the XML reader allows is walked without recursion, and a table of contents
/// with more entries than the limit is cut there.
#[test]
fn deep_and_huge_tables_of_contents_are_bounded() {
let deep = format!("", "