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}