lib3mf_async/zip.rs
1//! Async ZIP archive implementation.
2//!
3//! This module provides [`AsyncZipArchive`], an async implementation of the [`AsyncArchiveReader`]
4//! trait using the async-zip crate for non-blocking ZIP file access.
5//!
6//! ## Implementation Details
7//!
8//! - Uses `async-zip` with tokio compatibility layer (`tokio-util::compat`)
9//! - Wraps readers in `BufReader` for efficient I/O
10//! - Converts between tokio's `AsyncRead` and futures' `AsyncRead` traits
11//!
12//! ## Examples
13//!
14//! ```no_run
15//! use lib3mf_async::zip::AsyncZipArchive;
16//! use lib3mf_async::archive::AsyncArchiveReader;
17//! use tokio::fs::File;
18//!
19//! #[tokio::main]
20//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! // Open a 3MF file (ZIP archive)
22//! let file = File::open("model.3mf").await?;
23//! let mut archive = AsyncZipArchive::new(file).await?;
24//!
25//! // Read an entry
26//! let model_rels = archive.read_entry("_rels/.rels").await?;
27//! println!("Relationships XML: {} bytes", model_rels.len());
28//!
29//! Ok(())
30//! }
31//! ```
32//!
33//! [`AsyncArchiveReader`]: crate::archive::AsyncArchiveReader
34
35use crate::archive::AsyncArchiveReader;
36use async_trait::async_trait;
37use async_zip::StoredZipEntry;
38use async_zip::tokio::read::seek::ZipFileReader;
39use futures_lite::io::AsyncReadExt;
40use lib3mf_core::error::{Lib3mfError, Result};
41use tokio::io::{AsyncRead, AsyncSeek, BufReader};
42use tokio_util::compat::TokioAsyncReadCompatExt;
43
44/// Async ZIP archive reader implementing [`AsyncArchiveReader`].
45///
46/// This type wraps the async-zip crate's `ZipFileReader` and provides async access to ZIP
47/// archive entries without blocking the tokio runtime.
48///
49/// # Type Parameters
50///
51/// * `R` - The underlying reader type, must implement:
52/// - [`AsyncRead`]: For reading data
53/// - [`AsyncSeek`]: For random access to ZIP entries
54/// - [`Unpin`]: Required by tokio's async traits
55///
56/// Common types that satisfy these bounds:
57/// - `tokio::fs::File`
58/// - `std::io::Cursor<Vec<u8>>`
59/// - `tokio::io::BufReader<File>`
60///
61/// [`AsyncArchiveReader`]: crate::archive::AsyncArchiveReader
62pub struct AsyncZipArchive<R: AsyncRead + AsyncSeek + Unpin> {
63 // ZipFileReader from tokio module is likely an alias: ZipFileReader<R> = Base<Compat<R>>.
64 // So we pass BufReader<R> here, and it expands to Base<Compat<BufReader<R>>>.
65 reader: ZipFileReader<BufReader<R>>,
66}
67
68impl<R: AsyncRead + AsyncSeek + Unpin> AsyncZipArchive<R> {
69 /// Creates a new async ZIP archive reader.
70 ///
71 /// # Arguments
72 ///
73 /// * `reader` - Any async reader implementing `AsyncRead + AsyncSeek + Unpin`
74 ///
75 /// # Returns
76 ///
77 /// An initialized `AsyncZipArchive` ready to read entries.
78 ///
79 /// # Errors
80 ///
81 /// Returns [`Lib3mfError::Io`] if:
82 /// - The ZIP file header cannot be read
83 /// - The ZIP central directory is corrupt or missing
84 /// - The file is not a valid ZIP archive
85 ///
86 /// # Implementation Notes
87 ///
88 /// - Wraps the reader in a `BufReader` for efficient I/O
89 /// - Uses `tokio_util::compat` to bridge tokio and futures AsyncRead traits
90 /// - Reads the ZIP central directory during construction
91 ///
92 /// # Examples
93 ///
94 /// ```no_run
95 /// use lib3mf_async::zip::AsyncZipArchive;
96 /// use tokio::fs::File;
97 ///
98 /// #[tokio::main]
99 /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
100 /// let file = File::open("model.3mf").await?;
101 /// let archive = AsyncZipArchive::new(file).await?;
102 /// // Archive is ready to read entries
103 /// Ok(())
104 /// }
105 /// ```
106 ///
107 /// [`Lib3mfError::Io`]: lib3mf_core::error::Lib3mfError::Io
108 pub async fn new(reader: R) -> Result<Self> {
109 let buf_reader = BufReader::new(reader);
110 let compat_reader = buf_reader.compat();
111 // Construct the Base reader. since we use the Alias type to define the field,
112 // we might need to cast or construct carefully.
113 // Actually, if ZipFileReader is an alias, ZipFileReader::new is Base::new.
114 // Base::new takes the inner reader (Compat<Buf>).
115 // And returns Base<Compat<Buf>>.
116 // This matches the Alias<Buf>.
117 let zip = ZipFileReader::new(compat_reader)
118 .await
119 .map_err(|e| Lib3mfError::Io(std::io::Error::other(e.to_string())))?;
120 Ok(Self { reader: zip })
121 }
122}
123
124#[async_trait]
125impl<R: AsyncRead + AsyncSeek + Unpin + Send + Sync> AsyncArchiveReader for AsyncZipArchive<R> {
126 async fn read_entry(&mut self, name: &str) -> Result<Vec<u8>> {
127 let entries = self.reader.file().entries();
128 let index = entries
129 .iter()
130 .position(|e: &StoredZipEntry| e.filename().as_str().ok() == Some(name))
131 .ok_or(Lib3mfError::ResourceNotFound(0))?;
132
133 let mut reader = self
134 .reader
135 .reader_with_entry(index)
136 .await
137 .map_err(|e| Lib3mfError::Io(std::io::Error::other(e.to_string())))?;
138
139 let mut buffer = Vec::new();
140 // reader implements futures::io::AsyncRead.
141 // We imported futures_lite::io::AsyncReadExt so read_to_end should work.
142 reader
143 .read_to_end(&mut buffer)
144 .await
145 .map_err(|e| Lib3mfError::Io(std::io::Error::other(e.to_string())))?;
146
147 Ok(buffer)
148 }
149
150 async fn entry_exists(&mut self, name: &str) -> bool {
151 self.reader
152 .file()
153 .entries()
154 .iter()
155 .any(|e: &StoredZipEntry| e.filename().as_str().ok() == Some(name))
156 }
157
158 async fn list_entries(&mut self) -> Result<Vec<String>> {
159 let names = self
160 .reader
161 .file()
162 .entries()
163 .iter()
164 .filter_map(|e: &StoredZipEntry| e.filename().as_str().map(|s| s.to_string()).ok())
165 .collect();
166 Ok(names)
167 }
168}