async_deflate_zip/writer/
directory_writer.rs1use crate::error::ZipError;
2use crate::header;
3
4use tokio::io::{AsyncWrite, AsyncWriteExt};
5
6use super::stored_entry::StoredEntry;
7use super::zip_writer::ZipWriter;
8
9pub 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 pub fn set_mtime(&mut self, mtime: std::time::SystemTime) -> &mut Self {
29 self.mtime = Some(mtime);
30 self
31 }
32
33 pub fn set_permissions(&mut self, mode: u32) -> &mut Self {
38 self.unix_permissions = Some(mode & 0o7777);
39 self
40 }
41
42 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 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}