async_zip/base/write/
mod.rs1pub(crate) mod compressed_writer;
53pub(crate) mod entry_seekable;
54pub(crate) mod entry_stream;
55pub(crate) mod entry_whole;
56pub(crate) mod io;
57
58pub use entry_seekable::EntrySeekableWriter;
59pub use entry_stream::EntryStreamWriter;
60
61#[cfg(feature = "tokio")]
62use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt};
63
64use crate::entry::ZipEntry;
65use crate::error::Result;
66use crate::spec::extra_field::ExtraFieldAsBytes;
67use crate::spec::header::{
68 CentralDirectoryRecord, EndOfCentralDirectoryHeader, ExtraField, InfoZipUnicodeCommentExtraField,
69 InfoZipUnicodePathExtraField, Zip64EndOfCentralDirectoryLocator, Zip64EndOfCentralDirectoryRecord,
70};
71
72#[cfg(feature = "tokio")]
73use crate::tokio::write::ZipFileWriter as TokioZipFileWriter;
74
75use entry_whole::EntryWholeWriter;
76use io::offset::AsyncOffsetWriter;
77
78use crate::spec::consts::{NON_ZIP64_MAX_NUM_FILES, NON_ZIP64_MAX_SIZE};
79use futures_lite::io::{AsyncSeek, AsyncWrite, AsyncWriteExt};
80
81pub(crate) struct CentralDirectoryEntry {
82 pub header: CentralDirectoryRecord,
83 pub entry: ZipEntry,
84}
85
86pub struct ZipFileWriter<W> {
91 pub(crate) writer: AsyncOffsetWriter<W>,
92 pub(crate) cd_entries: Vec<CentralDirectoryEntry>,
93 force_no_zip64: bool,
95 pub(crate) is_zip64: bool,
97 comment_opt: Option<String>,
98}
99
100impl<W: AsyncWrite + Unpin> ZipFileWriter<W> {
101 pub fn new(writer: W) -> Self {
103 Self {
104 writer: AsyncOffsetWriter::new(writer),
105 cd_entries: Vec::new(),
106 comment_opt: None,
107 is_zip64: false,
108 force_no_zip64: false,
109 }
110 }
111
112 pub fn force_no_zip64(mut self) -> Self {
115 self.force_no_zip64 = true;
116 self
117 }
118
119 pub fn force_zip64(mut self) -> Self {
122 self.is_zip64 = true;
123 self
124 }
125
126 pub async fn write_entry_whole<E: Into<ZipEntry>>(&mut self, entry: E, data: &[u8]) -> Result<()> {
128 EntryWholeWriter::from_raw(self, entry.into(), data).write().await
129 }
130
131 pub async fn write_entry_stream<E: Into<ZipEntry>>(&mut self, entry: E) -> Result<EntryStreamWriter<'_, W>> {
135 EntryStreamWriter::from_raw(self, entry.into()).await
136 }
137
138 pub async fn write_entry_seekable<E: Into<ZipEntry>>(&mut self, entry: E) -> Result<EntrySeekableWriter<'_, W>>
143 where
144 W: AsyncSeek,
145 {
146 EntrySeekableWriter::from_raw(self, entry.into()).await
147 }
148
149 pub fn comment(&mut self, comment: String) {
151 self.comment_opt = Some(comment);
152 }
153
154 pub fn inner_mut(&mut self) -> &mut W {
158 self.writer.inner_mut()
159 }
160
161 pub async fn close(mut self) -> Result<W> {
170 let file_comment_length = self
171 .comment_opt
172 .as_ref()
173 .map(|comment| comment.len().try_into())
174 .transpose()
175 .map_err(|_| crate::error::ZipError::CommentTooLarge)?
176 .unwrap_or_default();
177 let cd_offset = self.writer.offset();
178
179 for entry in &self.cd_entries {
180 let filename_basic =
181 entry.entry.filename().alternative().unwrap_or_else(|| entry.entry.filename().as_bytes());
182 let comment_basic = entry.entry.comment().alternative().unwrap_or_else(|| entry.entry.comment().as_bytes());
183
184 self.writer.write_all(&crate::spec::consts::CDH_SIGNATURE.to_le_bytes()).await?;
185 self.writer.write_all(&entry.header.as_slice()).await?;
186 self.writer.write_all(filename_basic).await?;
187 self.writer.write_all(&entry.entry.extra_fields().as_bytes()).await?;
188 self.writer.write_all(comment_basic).await?;
189 }
190
191 let central_directory_size = self.writer.offset() - cd_offset;
192 let central_directory_size_u32 =
193 central_directory_size_field(central_directory_size, self.force_no_zip64, &mut self.is_zip64)?;
194 let num_entries_in_directory = self.cd_entries.len() as u64;
195 let num_entries_in_directory_u16 = if num_entries_in_directory > NON_ZIP64_MAX_NUM_FILES as u64 {
196 NON_ZIP64_MAX_NUM_FILES
197 } else {
198 num_entries_in_directory as u16
199 };
200 let cd_offset_u32 = if cd_offset > NON_ZIP64_MAX_SIZE as u64 {
201 if self.force_no_zip64 {
202 return Err(crate::error::ZipError::Zip64Needed(crate::error::Zip64ErrorCase::LargeFile));
203 } else {
204 self.is_zip64 = true;
205 }
206 NON_ZIP64_MAX_SIZE
207 } else {
208 cd_offset as u32
209 };
210
211 if self.is_zip64 {
213 let eocdr_offset = self.writer.offset();
214
215 let eocdr = Zip64EndOfCentralDirectoryRecord {
216 size_of_zip64_end_of_cd_record: 44,
217 version_made_by: crate::spec::version::as_made_by(),
218 version_needed_to_extract: 46,
219 disk_number: 0,
220 disk_number_start_of_cd: 0,
221 num_entries_in_directory_on_disk: num_entries_in_directory,
222 num_entries_in_directory,
223 directory_size: central_directory_size,
224 offset_of_start_of_directory: cd_offset,
225 };
226 self.writer.write_all(&crate::spec::consts::ZIP64_EOCDR_SIGNATURE.to_le_bytes()).await?;
227 self.writer.write_all(&eocdr.as_bytes()).await?;
228
229 let eocdl = Zip64EndOfCentralDirectoryLocator {
230 number_of_disk_with_start_of_zip64_end_of_central_directory: 0,
231 relative_offset: eocdr_offset,
232 total_number_of_disks: 1,
233 };
234 self.writer.write_all(&crate::spec::consts::ZIP64_EOCDL_SIGNATURE.to_le_bytes()).await?;
235 self.writer.write_all(&eocdl.as_bytes()).await?;
236 }
237
238 let header = EndOfCentralDirectoryHeader {
239 disk_num: 0,
240 start_cent_dir_disk: 0,
241 num_of_entries_disk: num_entries_in_directory_u16,
242 num_of_entries: num_entries_in_directory_u16,
243 size_cent_dir: central_directory_size_u32,
244 cent_dir_offset: cd_offset_u32,
245 file_comm_length: file_comment_length,
246 };
247
248 self.writer.write_all(&crate::spec::consts::EOCDR_SIGNATURE.to_le_bytes()).await?;
249 self.writer.write_all(&header.as_slice()).await?;
250 if let Some(comment) = self.comment_opt {
251 self.writer.write_all(comment.as_bytes()).await?;
252 }
253
254 Ok(self.writer.into_inner())
255 }
256}
257
258pub(crate) fn central_directory_size_field(
259 central_directory_size: u64,
260 force_no_zip64: bool,
261 is_zip64: &mut bool,
262) -> Result<u32> {
263 if central_directory_size > NON_ZIP64_MAX_SIZE as u64 {
264 if force_no_zip64 {
265 return Err(crate::error::ZipError::Zip64Needed(crate::error::Zip64ErrorCase::LargeFile));
266 }
267 *is_zip64 = true;
268 Ok(NON_ZIP64_MAX_SIZE)
269 } else {
270 Ok(central_directory_size as u32)
271 }
272}
273
274#[cfg(feature = "tokio")]
275impl<W> ZipFileWriter<Compat<W>>
276where
277 W: tokio::io::AsyncWrite + Unpin,
278{
279 pub fn with_tokio(writer: W) -> TokioZipFileWriter<W> {
281 Self {
282 writer: AsyncOffsetWriter::new(writer.compat_write()),
283 cd_entries: Vec::new(),
284 comment_opt: None,
285 is_zip64: false,
286 force_no_zip64: false,
287 }
288 }
289}
290
291pub(crate) fn get_or_put_info_zip_unicode_path_extra_field_mut(
292 extra_fields: &mut Vec<ExtraField>,
293) -> &mut InfoZipUnicodePathExtraField {
294 if !extra_fields.iter().any(|field| matches!(field, ExtraField::InfoZipUnicodePath(_))) {
295 extra_fields
296 .push(ExtraField::InfoZipUnicodePath(InfoZipUnicodePathExtraField::V1 { crc32: 0, unicode: vec![] }));
297 }
298
299 for field in extra_fields.iter_mut() {
300 if let ExtraField::InfoZipUnicodePath(extra_field) = field {
301 return extra_field;
302 }
303 }
304
305 panic!("InfoZipUnicodePathExtraField not found after insertion")
306}
307
308pub(crate) fn get_or_put_info_zip_unicode_comment_extra_field_mut(
309 extra_fields: &mut Vec<ExtraField>,
310) -> &mut InfoZipUnicodeCommentExtraField {
311 if !extra_fields.iter().any(|field| matches!(field, ExtraField::InfoZipUnicodeComment(_))) {
312 extra_fields
313 .push(ExtraField::InfoZipUnicodeComment(InfoZipUnicodeCommentExtraField::V1 { crc32: 0, unicode: vec![] }));
314 }
315
316 for field in extra_fields.iter_mut() {
317 if let ExtraField::InfoZipUnicodeComment(extra_field) = field {
318 return extra_field;
319 }
320 }
321
322 panic!("InfoZipUnicodeCommentExtraField not found after insertion")
323}