Skip to main content

easyofd_reader/
zip_util.rs

1//! ZIP 文件解压工具。
2//!
3//! 对应 Java: org.ofdrw.reader.ZipUtil
4//!
5//! Java 版使用 Apache Commons Compress 解压 ZIP 文件到文件系统。
6//! Rust 版提供内存解压和文件系统解压两种方式,底层使用 `zip` crate。
7
8use easyofd_core::{OfdError, OfdResult};
9use std::io::{Read, Seek};
10use std::path::Path;
11
12/// 从 ZIP 归档中读取指定条目的全部字节。
13///
14/// 对应 Java: `ZipUtil` 中的文件读取逻辑
15///
16/// # 错误
17///
18/// 条目不存在或读取失败时返回错误。
19pub fn read_entry<R: Read + Seek>(
20    archive: &mut zip::ZipArchive<R>,
21    name: &str,
22) -> OfdResult<Vec<u8>> {
23    let mut file = archive
24        .by_name(name)
25        .map_err(|e| OfdError::Zip(format!("{name}: {e}")))?;
26    let mut buf = Vec::new();
27    file.read_to_end(&mut buf).map_err(OfdError::Io)?;
28    Ok(buf)
29}
30
31/// 列出 ZIP 归档中所有条目名称。
32///
33/// 对应 Java: `ZipUtil` 中的目录遍历逻辑
34///
35/// # 错误
36///
37/// 归档读取失败时返回错误。
38pub fn list_entries<R: Read + Seek>(archive: &mut zip::ZipArchive<R>) -> OfdResult<Vec<String>> {
39    let mut names = Vec::new();
40    for i in 0..archive.len() {
41        let file = archive
42            .by_index(i)
43            .map_err(|e| OfdError::Zip(format!("index {i}: {e}")))?;
44        names.push(file.name().to_string());
45    }
46    Ok(names)
47}
48
49/// 将 ZIP 归档解压到指定目录。
50///
51/// 对应 Java: `ZipUtil.unZipFileByApacheCommonCompress`
52///
53/// # 错误
54///
55/// IO 操作失败或归档格式错误时返回错误。
56pub fn extract_to_dir<R: Read + Seek>(
57    archive: &mut zip::ZipArchive<R>,
58    dest: &Path,
59) -> OfdResult<()> {
60    use std::fs;
61
62    for i in 0..archive.len() {
63        let mut file = archive
64            .by_index(i)
65            .map_err(|e| OfdError::Zip(format!("index {i}: {e}")))?;
66        let outpath = dest.join(file.name());
67
68        if file.name().ends_with('/') {
69            fs::create_dir_all(&outpath).map_err(OfdError::Io)?;
70        } else {
71            if let Some(parent) = outpath.parent() {
72                fs::create_dir_all(parent).map_err(OfdError::Io)?;
73            }
74            let mut outfile = fs::File::create(&outpath).map_err(OfdError::Io)?;
75            std::io::copy(&mut file, &mut outfile).map_err(OfdError::Io)?;
76        }
77    }
78    Ok(())
79}
80
81/// 将内存中的 ZIP 字节解压到指定目录。
82///
83/// 对应 Java: `ZipUtil.unZipFiles(InputStream, String)`
84///
85/// # 错误
86///
87/// ZIP 格式错误或 IO 操作失败时返回错误。
88pub fn extract_bytes_to_dir(data: &[u8], dest: &Path) -> OfdResult<()> {
89    let reader = std::io::Cursor::new(data);
90    let mut archive = zip::ZipArchive::new(reader).map_err(|e| OfdError::Zip(format!("{e}")))?;
91    extract_to_dir(&mut archive, dest)
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use std::io::{Cursor, Write};
98
99    fn create_test_zip() -> Vec<u8> {
100        let mut buf = Vec::new();
101        {
102            let writer = Cursor::new(&mut buf);
103            let mut zip = zip::ZipWriter::new(writer);
104            let options = zip::write::SimpleFileOptions::default()
105                .compression_method(zip::CompressionMethod::Stored);
106
107            zip.start_file("OFD.xml", options).unwrap();
108            zip.write_all(b"<OFD/>").unwrap();
109
110            zip.start_file("Doc_0/Document.xml", options).unwrap();
111            zip.write_all(b"<Document/>").unwrap();
112
113            zip.finish().unwrap();
114        }
115        buf
116    }
117
118    #[test]
119    fn test_read_entry() {
120        let data = create_test_zip();
121        let reader = Cursor::new(&data);
122        let mut archive = zip::ZipArchive::new(reader).unwrap();
123        let content = read_entry(&mut archive, "OFD.xml").unwrap();
124        assert_eq!(content, b"<OFD/>");
125    }
126
127    #[test]
128    fn test_read_entry_not_found() {
129        let data = create_test_zip();
130        let reader = Cursor::new(&data);
131        let mut archive = zip::ZipArchive::new(reader).unwrap();
132        let result = read_entry(&mut archive, "nonexistent.xml");
133        assert!(result.is_err());
134    }
135
136    #[test]
137    fn test_list_entries() {
138        let data = create_test_zip();
139        let reader = Cursor::new(&data);
140        let mut archive = zip::ZipArchive::new(reader).unwrap();
141        let entries = list_entries(&mut archive).unwrap();
142        assert_eq!(entries.len(), 2);
143        assert!(entries.contains(&"OFD.xml".to_string()));
144        assert!(entries.contains(&"Doc_0/Document.xml".to_string()));
145    }
146
147    #[test]
148    fn test_extract_to_dir() {
149        let data = create_test_zip();
150        let reader = Cursor::new(&data);
151        let mut archive = zip::ZipArchive::new(reader).unwrap();
152        let dest = std::env::temp_dir().join("easyofd_zip_util_test");
153        let _ = std::fs::remove_dir_all(&dest);
154        std::fs::create_dir_all(&dest).unwrap();
155
156        extract_to_dir(&mut archive, &dest).unwrap();
157
158        assert!(dest.join("OFD.xml").exists());
159        assert!(dest.join("Doc_0/Document.xml").exists());
160
161        let content = std::fs::read_to_string(dest.join("OFD.xml")).unwrap();
162        assert_eq!(content, "<OFD/>");
163
164        let _ = std::fs::remove_dir_all(&dest);
165    }
166
167    #[test]
168    fn test_extract_bytes_to_dir() {
169        let data = create_test_zip();
170        let dest = std::env::temp_dir().join("easyofd_zip_util_bytes_test");
171        let _ = std::fs::remove_dir_all(&dest);
172        std::fs::create_dir_all(&dest).unwrap();
173
174        extract_bytes_to_dir(&data, &dest).unwrap();
175        assert!(dest.join("OFD.xml").exists());
176
177        let _ = std::fs::remove_dir_all(&dest);
178    }
179}