@@ -2,6 +2,9 @@ use gix_worktree_stream::{Entry, Stream};
22
33use crate :: { Error , Format , Options } ;
44
5+ #[ cfg( feature = "zip" ) ]
6+ use std:: io:: Write ;
7+
58/// Write all stream entries in `stream` as provided by `next_entry(stream)` to `out` configured according to `opts` which
69/// also includes the streaming format.
710///
@@ -135,24 +138,9 @@ where
135138
136139 #[ cfg( feature = "zip" ) ]
137140 {
138- let mut ar = zip :: write :: ZipWriter :: new ( out) ;
141+ let mut ar = rawzip :: ZipArchiveWriter :: new ( out) ;
139142 let mut buf = Vec :: new ( ) ;
140- let zdt = jiff:: Timestamp :: from_second ( opts. modification_time )
141- . map_err ( |err| Error :: InvalidModificationTime ( Box :: new ( err) ) ) ?
142- . to_zoned ( jiff:: tz:: TimeZone :: UTC ) ;
143- let mtime = zip:: DateTime :: from_date_and_time (
144- zdt. year ( )
145- . try_into ( )
146- . map_err ( |err| Error :: InvalidModificationTime ( Box :: new ( err) ) ) ?,
147- // These are all OK because month, day, hour, minute and second
148- // are always positive.
149- zdt. month ( ) . try_into ( ) . expect ( "non-negative" ) ,
150- zdt. day ( ) . try_into ( ) . expect ( "non-negative" ) ,
151- zdt. hour ( ) . try_into ( ) . expect ( "non-negative" ) ,
152- zdt. minute ( ) . try_into ( ) . expect ( "non-negative" ) ,
153- zdt. second ( ) . try_into ( ) . expect ( "non-negative" ) ,
154- )
155- . map_err ( |err| Error :: InvalidModificationTime ( Box :: new ( err) ) ) ?;
143+ let mtime = rawzip:: time:: UtcDateTime :: from_unix ( opts. modification_time ) ;
156144 while let Some ( entry) = next_entry ( stream) ? {
157145 append_zip_entry (
158146 & mut ar,
@@ -171,35 +159,87 @@ where
171159
172160#[ cfg( feature = "zip" ) ]
173161fn append_zip_entry < W : std:: io:: Write + std:: io:: Seek > (
174- ar : & mut zip :: write :: ZipWriter < W > ,
162+ ar : & mut rawzip :: ZipArchiveWriter < W > ,
175163 mut entry : gix_worktree_stream:: Entry < ' _ > ,
176164 buf : & mut Vec < u8 > ,
177- mtime : zip :: DateTime ,
165+ mtime : rawzip :: time :: UtcDateTime ,
178166 compression_level : Option < i64 > ,
179167 tree_prefix : Option < & bstr:: BString > ,
180168) -> Result < ( ) , Error > {
181- let file_opts = zip:: write:: SimpleFileOptions :: default ( )
182- . compression_method ( zip:: CompressionMethod :: Deflated )
183- . compression_level ( compression_level)
184- . large_file ( entry. bytes_remaining ( ) . is_none_or ( |len| len > u32:: MAX as usize ) )
185- . last_modified_time ( mtime)
186- . unix_permissions ( if entry. mode . is_executable ( ) { 0o755 } else { 0o644 } ) ;
169+ use bstr:: ByteSlice ;
187170 let path = add_prefix ( entry. relative_path ( ) , tree_prefix) . into_owned ( ) ;
171+ let unix_permissions = if entry. mode . is_executable ( ) { 0o755 } else { 0o644 } ;
172+ let path = path. to_str ( ) . map_err ( |_| {
173+ Error :: Io ( std:: io:: Error :: new (
174+ std:: io:: ErrorKind :: InvalidData ,
175+ format ! ( "Invalid UTF-8 in entry path: {path:?}" ) ,
176+ ) )
177+ } ) ?;
178+
188179 match entry. mode . kind ( ) {
189180 gix_object:: tree:: EntryKind :: Blob | gix_object:: tree:: EntryKind :: BlobExecutable => {
190- ar. start_file ( path. to_string ( ) , file_opts)
191- . map_err ( std:: io:: Error :: other) ?;
192- std:: io:: copy ( & mut entry, ar) ?;
181+ let file_builder = ar
182+ . new_file ( path)
183+ . compression_method ( rawzip:: CompressionMethod :: Deflate )
184+ . last_modified ( mtime)
185+ . unix_permissions ( unix_permissions) ;
186+
187+ let ( mut zip_entry, config) = file_builder. start ( ) . map_err ( std:: io:: Error :: other) ?;
188+
189+ // Use flate2 for compression. Level 9 is the maximum compression level for deflate.
190+ let encoder = flate2:: write:: DeflateEncoder :: new (
191+ & mut zip_entry,
192+ match compression_level {
193+ None => flate2:: Compression :: default ( ) ,
194+ Some ( level) => flate2:: Compression :: new ( level. clamp ( 0 , 9 ) as u32 ) ,
195+ } ,
196+ ) ;
197+ let mut writer = config. wrap ( encoder) ;
198+ std:: io:: copy ( & mut entry, & mut writer) ?;
199+ let ( encoder, descriptor) = writer. finish ( ) . map_err ( std:: io:: Error :: other) ?;
200+ encoder. finish ( ) ?;
201+ zip_entry. finish ( descriptor) . map_err ( std:: io:: Error :: other) ?;
193202 }
194203 gix_object:: tree:: EntryKind :: Tree | gix_object:: tree:: EntryKind :: Commit => {
195- ar. add_directory ( path. to_string ( ) , file_opts)
204+ // rawzip requires directory paths to end with '/'
205+ let mut dir_path = path. to_owned ( ) ;
206+ if !dir_path. ends_with ( '/' ) {
207+ dir_path. push ( '/' ) ;
208+ }
209+ ar. new_dir ( & dir_path)
210+ . last_modified ( mtime)
211+ . unix_permissions ( unix_permissions)
212+ . create ( )
196213 . map_err ( std:: io:: Error :: other) ?;
197214 }
198215 gix_object:: tree:: EntryKind :: Link => {
199- use bstr :: ByteSlice ;
216+ buf . clear ( ) ;
200217 std:: io:: copy ( & mut entry, buf) ?;
201- ar. add_symlink ( path. to_string ( ) , buf. as_bstr ( ) . to_string ( ) , file_opts)
218+
219+ // For symlinks, we need to create a file with symlink permissions
220+ let symlink_path = path;
221+ let target = buf. as_bstr ( ) . to_str ( ) . map_err ( |_| {
222+ Error :: Io ( std:: io:: Error :: new (
223+ std:: io:: ErrorKind :: InvalidData ,
224+ format ! (
225+ "Invalid UTF-8 in symlink target for entry '{symlink_path}': {:?}" ,
226+ buf. as_bstr( )
227+ ) ,
228+ ) )
229+ } ) ?;
230+
231+ let ( mut zip_entry, config) = ar
232+ . new_file ( symlink_path)
233+ . compression_method ( rawzip:: CompressionMethod :: Store )
234+ . last_modified ( mtime)
235+ . unix_permissions ( 0o120644 ) // Symlink mode
236+ . start ( )
202237 . map_err ( std:: io:: Error :: other) ?;
238+
239+ let mut writer = config. wrap ( & mut zip_entry) ;
240+ writer. write_all ( target. as_bytes ( ) ) ?;
241+ let ( _, descriptor) = writer. finish ( ) . map_err ( std:: io:: Error :: other) ?;
242+ zip_entry. finish ( descriptor) . map_err ( std:: io:: Error :: other) ?;
203243 }
204244 }
205245 Ok ( ( ) )
0 commit comments