Skip to main content

ofd_core/
package.rs

1//! OFD 容器方案(见 GB/T 33190—2016 第 6 章)。
2//!
3//! 容器功能由一个 ZIP 文件实现,包内各部件以 XML 文件形式组织。
4//! [`OfdPackage`] 封装底层 ZIP 归档,提供按路径读取字节、读取文本以及
5//! 直接反序列化为数据模型的能力。
6
7use 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
16/// OFD 包,封装一个 ZIP 归档。
17///
18/// 范型参数 `R` 为底层数据源,需实现 [`Read`] + [`Seek`],
19/// 常见为 [`std::fs::File`] 或 [`std::io::Cursor`]。
20pub struct OfdPackage<R> {
21    archive: ZipArchive<R>,
22}
23
24impl OfdPackage<File> {
25    /// 从文件系统路径打开一个 OFD 包。
26    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    /// 基于任意 [`Read`] + [`Seek`] 数据源创建 OFD 包。
33    pub fn new(reader: R) -> Result<Self> {
34        Ok(OfdPackage {
35            archive: ZipArchive::new(reader)?,
36        })
37    }
38
39    /// 读取包内指定条目的原始字节。
40    ///
41    /// `path` 允许以 `/` 开头,内部会归一化为 ZIP 条目名。
42    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    /// 读取包内指定条目为 UTF-8 文本。
54    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    /// 读取并将包内 XML 条目反序列化为数据模型 `T`。
61    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    /// 判断包内是否存在指定条目。
67    pub fn contains(&self, path: &str) -> bool {
68        let name = normalize(path);
69        self.archive.file_names().any(|n| n == name)
70    }
71
72    /// 返回包内所有条目名。
73    pub fn entries(&self) -> Vec<String> {
74        self.archive.file_names().map(|s| s.to_string()).collect()
75    }
76
77    /// 包内条目数量。
78    pub fn len(&self) -> usize {
79        self.archive.len()
80    }
81
82    /// 包是否为空。
83    pub fn is_empty(&self) -> bool {
84        self.archive.is_empty()
85    }
86}
87
88/// 归一化条目名:去除前导 `/`。
89fn 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    /// 构造一个含若干条目的内存 ZIP。
101    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        // 前导 `/` 归一化后命中。
130        assert_eq!(p.read("/OFD.xml").unwrap(), b"hello");
131        // 未找到条目走 EntryNotFound 分支。
132        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        // 非法 UTF-8 走 Structure 错误分支。
141        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        // XML 解析失败分支。
157        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        // File::open 失败走 `?` 的 Io 错误分支。
176        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}