Skip to main content

async_deflate_zip/writer/
directory_writer.rs

1use crate::error::ZipError;
2use crate::header;
3
4use tokio::io::{AsyncWrite, AsyncWriteExt};
5
6use super::stored_entry::StoredEntry;
7use super::zip_writer::ZipWriter;
8
9/// A handle for finalizing a directory entry in a ZIP archive.
10///
11/// Obtained from [`ZipWriter::append_directory`]. Use [`set_mtime`](Self::set_mtime)
12/// and/or [`set_permissions`](Self::set_permissions) to attach metadata, then call
13/// [`close`](Self::close) to finalize the entry.
14///
15/// Dropping without calling `close` leaves the archive in an inconsistent state
16/// and poisons the parent [`ZipWriter`].
17pub struct DirectoryWriter<'a, W: AsyncWrite + Unpin> {
18    pub(crate) zip: &'a mut ZipWriter<W>,
19    pub(crate) writer: Option<W>,
20    pub(crate) name: String,
21    pub(crate) local_header_offset: u64,
22    pub(crate) mtime: Option<std::time::SystemTime>,
23    pub(crate) unix_permissions: Option<u32>,
24}
25
26impl<W: AsyncWrite + Unpin> DirectoryWriter<'_, W> {
27    /// Set the modification time for this directory entry.
28    pub fn set_mtime(&mut self, mtime: std::time::SystemTime) -> &mut Self {
29        self.mtime = Some(mtime);
30        self
31    }
32
33    /// Set Unix file permissions for this directory entry.
34    ///
35    /// Provide permission bits including setuid/setgid/sticky (e.g., `0o755`).
36    /// The crate automatically adds the `S_IFDIR` file type bit.
37    pub fn set_permissions(&mut self, mode: u32) -> &mut Self {
38        self.unix_permissions = Some(mode & 0o7777);
39        self
40    }
41
42    /// Finalize the directory entry by writing the Data Descriptor.
43    ///
44    /// This consumes the `DirectoryWriter`, writes the trailing Data Descriptor
45    /// (CRC-32 and zero sizes), and returns the inner writer to the parent
46    /// [`ZipWriter`].
47    ///
48    /// # Errors
49    ///
50    /// Returns [`ZipError`] if `close` is called more than once.
51    pub async fn close(mut self) -> Result<(), ZipError> {
52        let mut inner = self
53            .writer
54            .take()
55            .ok_or_else(|| ZipError::InvalidState("directory entry already closed".to_string()))?;
56
57        let dd = header::DataDescriptor {
58            crc32: 0,
59            compressed_size: 0,
60            uncompressed_size: 0,
61            zip64: self.local_header_offset > header::U32_MAX,
62        };
63        let dd_bytes = dd.serialize();
64        inner.write_all(&dd_bytes).await.map_err(|e| {
65            self.zip.poisoned = true;
66            ZipError::Io(e)
67        })?;
68        self.zip.pos += dd_bytes.len() as u64;
69
70        let (mtime_msdos, unix_mtime) = header::mtime_to_ms_dos_and_unix(self.mtime);
71
72        self.zip.entries.push(StoredEntry {
73            name: self.name.clone(),
74            crc32: 0,
75            compressed_size: 0,
76            uncompressed_size: 0,
77            local_header_offset: self.local_header_offset,
78            is_directory: true,
79            is_symlink: false,
80            is_stored: false,
81            mtime: mtime_msdos,
82            unix_mtime,
83            unix_permissions: self.unix_permissions,
84        });
85
86        self.zip.inner = Some(inner);
87        Ok(())
88    }
89}
90
91impl<W: AsyncWrite + Unpin> Drop for DirectoryWriter<'_, W> {
92    fn drop(&mut self) {
93        if self.writer.is_some() {
94            // close() was never called — mark the ZipWriter as poisoned
95            self.zip.poisoned = true;
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::super::*;
103    use tokio::io::AsyncWriteExt;
104
105    #[tokio::test]
106    async fn test_directory_entry() {
107        let mut buf = Vec::new();
108        let mut zip = ZipWriter::new(&mut buf);
109        zip.append_directory("emptydir/")
110            .await
111            .unwrap()
112            .close()
113            .await
114            .unwrap();
115
116        let mut entry = zip.append_file("emptydir/hello.txt").await.unwrap();
117        entry.write_all(b"nested").await.unwrap();
118        entry.close().await.unwrap();
119
120        zip.finalize().await.unwrap();
121
122        assert!(buf.windows(4).any(|w| w == b"PK\x03\x04"));
123    }
124
125    #[tokio::test]
126    async fn test_directory_metadata() {
127        let mut buf = Vec::new();
128        let mut zip = ZipWriter::new(&mut buf);
129
130        {
131            let mut dir = zip.append_directory("meta_dir/").await.unwrap();
132            dir.set_mtime(std::time::SystemTime::UNIX_EPOCH);
133            dir.set_permissions(0o755);
134            dir.close().await.unwrap();
135        }
136
137        zip.finalize().await.unwrap();
138
139        let pos = buf.windows(4).position(|w| w == b"PK\x01\x02").unwrap();
140        let cd = &buf[pos..];
141
142        let vmb = u16::from_le_bytes(cd[4..6].try_into().unwrap());
143        assert_eq!(vmb >> 8, 3, "expected Unix host OS when metadata is set");
144
145        let version_needed = u16::from_le_bytes(cd[6..8].try_into().unwrap());
146        assert_eq!(version_needed, 10, "expected VERSION_STORED for directory");
147
148        let method = u16::from_le_bytes(cd[10..12].try_into().unwrap());
149        assert_eq!(method, 0, "expected METHOD_STORED for directory");
150
151        let efa = u32::from_le_bytes(cd[38..42].try_into().unwrap());
152        assert_eq!(efa, ((0o755 | 0o040000) as u32) << 16);
153
154        let name_len = u16::from_le_bytes(cd[28..30].try_into().unwrap()) as usize;
155        let extra_len = u16::from_le_bytes(cd[30..32].try_into().unwrap()) as usize;
156        assert!(
157            extra_len >= 4,
158            "expected non-empty extra field when mtime is set, got {extra_len}"
159        );
160        let extra_start = 46 + name_len;
161        let extra = &cd[extra_start..extra_start + extra_len];
162        assert!(
163            extra.windows(2).any(|w| w == b"UT"),
164            "expected extended timestamp extra (0x5455/UT) in directory entry"
165        );
166    }
167
168    #[tokio::test]
169    async fn test_directory_version_needed() {
170        let mut buf = Vec::new();
171        let mut zip = ZipWriter::new(&mut buf);
172        zip.append_directory("mydir/")
173            .await
174            .unwrap()
175            .close()
176            .await
177            .unwrap();
178        zip.finalize().await.unwrap();
179
180        let pos = buf.windows(4).position(|w| w == b"PK\x01\x02").unwrap();
181        let cd = &buf[pos..];
182        let version_needed = u16::from_le_bytes(cd[6..8].try_into().unwrap());
183        assert_eq!(
184            version_needed, 10,
185            "directory CD version_needed should be 10 (STORED), got {version_needed}"
186        );
187    }
188}