1use std::fs::File;
8use std::io::{Read, Seek};
9use std::path::Path;
10
11use serde::de::DeserializeOwned;
12use zip::ZipArchive;
13
14use crate::error::{OfdError, Result};
15
16pub struct OfdPackage<R> {
21 archive: ZipArchive<R>,
22}
23
24impl OfdPackage<File> {
25 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
27 Self::new(File::open(path)?)
28 }
29}
30
31impl<R: Read + Seek> OfdPackage<R> {
32 pub fn new(reader: R) -> Result<Self> {
34 Ok(OfdPackage {
35 archive: ZipArchive::new(reader)?,
36 })
37 }
38
39 pub fn read(&mut self, path: &str) -> Result<Vec<u8>> {
43 let name = normalize(path);
44 let mut entry = self
45 .archive
46 .by_name(&name)
47 .map_err(|_| OfdError::EntryNotFound(name.clone()))?;
48 let mut buf = Vec::with_capacity(entry.size() as usize);
49 entry.read_to_end(&mut buf)?;
50 Ok(buf)
51 }
52
53 pub fn read_to_string(&mut self, path: &str) -> Result<String> {
55 let bytes = self.read(path)?;
56 String::from_utf8(bytes)
57 .map_err(|e| OfdError::Structure(format!("entry {path} is not valid UTF-8: {e}")))
58 }
59
60 pub fn parse<T: DeserializeOwned>(&mut self, path: &str) -> Result<T> {
62 let text = self.read_to_string(path)?;
63 Ok(quick_xml::de::from_str(&text)?)
64 }
65
66 pub fn contains(&self, path: &str) -> bool {
68 let name = normalize(path);
69 self.archive.file_names().any(|n| n == name)
70 }
71
72 pub fn entries(&self) -> Vec<String> {
74 self.archive.file_names().map(|s| s.to_string()).collect()
75 }
76
77 pub fn len(&self) -> usize {
79 self.archive.len()
80 }
81
82 pub fn is_empty(&self) -> bool {
84 self.archive.is_empty()
85 }
86}
87
88fn normalize(path: &str) -> String {
90 path.trim_start_matches('/').to_string()
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use std::io::{Cursor, Write};
97 use zip::ZipWriter;
98 use zip::write::SimpleFileOptions;
99
100 fn build_zip(files: &[(&str, &[u8])]) -> Vec<u8> {
102 let mut buf = Vec::new();
103 {
104 let mut zip = ZipWriter::new(Cursor::new(&mut buf));
105 let opts =
106 SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
107 for (name, content) in files {
108 zip.start_file(*name, opts).unwrap();
109 zip.write_all(content).unwrap();
110 }
111 zip.finish().unwrap();
112 }
113 buf
114 }
115
116 fn pkg(files: &[(&str, &[u8])]) -> OfdPackage<Cursor<Vec<u8>>> {
117 OfdPackage::new(Cursor::new(build_zip(files))).unwrap()
118 }
119
120 #[test]
121 fn normalize_strips_leading_slash() {
122 assert_eq!(normalize("/OFD.xml"), "OFD.xml");
123 assert_eq!(normalize("Doc_0/Document.xml"), "Doc_0/Document.xml");
124 }
125
126 #[test]
127 fn read_hit_and_miss() {
128 let mut p = pkg(&[("OFD.xml", b"hello")]);
129 assert_eq!(p.read("/OFD.xml").unwrap(), b"hello");
131 let err = p.read("missing.xml").unwrap_err();
133 assert!(matches!(err, OfdError::EntryNotFound(_)));
134 }
135
136 #[test]
137 fn read_to_string_utf8_and_invalid() {
138 let mut p = pkg(&[("a.txt", "文本".as_bytes()), ("bad.bin", &[0xff, 0xfe])]);
139 assert_eq!(p.read_to_string("a.txt").unwrap(), "文本");
140 assert!(matches!(
142 p.read_to_string("bad.bin").unwrap_err(),
143 OfdError::Structure(_)
144 ));
145 }
146
147 #[test]
148 fn parse_ok_and_xml_error() {
149 #[derive(serde::Deserialize, PartialEq, Debug)]
150 struct Doc {
151 #[serde(rename = "@v")]
152 v: String,
153 }
154 let mut p = pkg(&[("ok.xml", br#"<Doc v="1"/>"#), ("bad.xml", b"<Doc")]);
155 assert_eq!(p.parse::<Doc>("ok.xml").unwrap(), Doc { v: "1".into() });
156 assert!(p.parse::<Doc>("bad.xml").is_err());
158 }
159
160 #[test]
161 fn contains_entries_len_empty() {
162 let p = pkg(&[("OFD.xml", b"x"), ("Doc_0/Document.xml", b"y")]);
163 assert!(p.contains("/OFD.xml"));
164 assert!(!p.contains("nope.xml"));
165 let mut entries = p.entries();
166 entries.sort();
167 assert_eq!(entries, vec!["Doc_0/Document.xml", "OFD.xml"]);
168 assert_eq!(p.len(), 2);
169 assert!(!p.is_empty());
170 assert!(pkg(&[]).is_empty());
171 }
172
173 #[test]
174 fn open_missing_file_errors() {
175 assert!(OfdPackage::open("/nonexistent/path/does-not-exist.ofd").is_err());
177 }
178
179 #[test]
180 fn new_rejects_non_zip() {
181 assert!(OfdPackage::new(Cursor::new(b"not a zip".to_vec())).is_err());
182 }
183}