use crate::error::{Error, Result};
use std::io::{Cursor, Read, Seek};
use std::path::Path;
use zip::ZipArchive;
#[allow(dead_code)]
mod paths {
pub const MIMETYPE: &str = "mimetype";
pub const CONTENT_HPF: &str = "Contents/content.hpf";
pub const HEADER_XML: &str = "Contents/header.xml";
pub const SETTINGS_XML: &str = "Contents/settings.xml";
pub const BINDATA_DIR: &str = "BinData/";
pub const CONTENTS_DIR: &str = "Contents/";
}
pub struct HwpxContainer {
archive: ZipArchive<Cursor<Vec<u8>>>,
}
impl HwpxContainer {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let data = std::fs::read(path)?;
Self::from_bytes(data)
}
pub fn from_reader<R: Read + Seek>(mut reader: R) -> Result<Self> {
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
Self::from_bytes(data)
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self> {
let cursor = Cursor::new(data);
let archive = ZipArchive::new(cursor)?;
Ok(Self { archive })
}
pub fn verify_mimetype(&mut self) -> Result<bool> {
if let Ok(content) = self.read_file(paths::MIMETYPE) {
Ok(content.contains("hwp") || content.contains("owpml"))
} else {
Ok(true)
}
}
pub fn read_file(&mut self, path: &str) -> Result<String> {
let mut file = self
.archive
.by_name(path)
.map_err(|_| Error::MissingComponent(path.to_string()))?;
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
pub fn read_binary(&mut self, path: &str) -> Result<Vec<u8>> {
let mut file = self
.archive
.by_name(path)
.map_err(|_| Error::MissingComponent(path.to_string()))?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
Ok(data)
}
pub fn read_content_hpf(&mut self) -> Result<String> {
self.read_file(paths::CONTENT_HPF)
}
pub fn list_sections(&mut self) -> Result<Vec<String>> {
let mut sections = Vec::new();
if let Ok(hpf) = self.read_content_hpf() {
sections = parse_section_order(&hpf);
}
if sections.is_empty() {
for i in 0..self.archive.len() {
if let Ok(file) = self.archive.by_index(i) {
let name = file.name().to_string();
if name.starts_with("Contents/section") && name.ends_with(".xml") {
sections.push(name);
}
}
}
sections.sort();
}
if sections.is_empty() {
return Err(Error::MissingComponent("section files".into()));
}
Ok(sections)
}
pub fn list_bindata(&mut self) -> Result<Vec<String>> {
let mut resources = Vec::new();
for i in 0..self.archive.len() {
if let Ok(file) = self.archive.by_index(i) {
let name = file.name().to_string();
if name.starts_with(paths::BINDATA_DIR) && !name.ends_with('/') {
resources.push(name);
}
}
}
Ok(resources)
}
pub fn file_exists(&mut self, path: &str) -> bool {
self.archive.by_name(path).is_ok()
}
}
fn parse_section_order(hpf_content: &str) -> Vec<String> {
let mut sections = Vec::new();
for line in hpf_content.lines() {
if let Some(start) = line.find("idref=\"section") {
let rest = &line[start + 7..]; if let Some(end) = rest.find('"') {
let section_id = &rest[..end];
let section_path = format!("Contents/{}.xml", section_id);
sections.push(section_path);
}
}
}
sections
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_section_order() {
let hpf = r#"
<package>
<spine>
<itemref idref="section0"/>
<itemref idref="section1"/>
</spine>
</package>
"#;
let sections = parse_section_order(hpf);
assert_eq!(sections.len(), 2);
assert_eq!(sections[0], "Contents/section0.xml");
assert_eq!(sections[1], "Contents/section1.xml");
}
}