Skip to content

Commit 25099c8

Browse files
authored
Merge pull request #2294 from GitoxideLabs/copilot/replace-zip-crate-with-rawzip
Replace zip crate with rawzip in gix-archive
2 parents b77744f + fb6386b commit 25099c8

File tree

4 files changed

+112
-78
lines changed

4 files changed

+112
-78
lines changed

Cargo.lock

Lines changed: 7 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gix-archive/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ tar = ["dep:tar", "dep:gix-path"]
2323
tar_gz = ["tar", "dep:flate2"]
2424

2525
## Enable the `zip` archive format.
26-
zip = ["dep:zip"]
26+
zip = ["dep:rawzip", "dep:flate2"]
2727

2828

2929
[dependencies]
@@ -33,7 +33,7 @@ gix-path = { version = "^0.10.22", path = "../gix-path", optional = true }
3333
gix-date = { version = "^0.11.0", path = "../gix-date" }
3434

3535
flate2 = { version = "1.1.1", optional = true, default-features = false, features = ["zlib-rs"] }
36-
zip = { version = "6.0.0", optional = true, default-features = false, features = ["deflate-flate2-zlib-rs"] }
36+
rawzip = { version = "0.4.2", optional = true }
3737
jiff = { version = "0.2.15", default-features = false, features = ["std"] }
3838

3939
thiserror = "2.0.17"

gix-archive/src/write.rs

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ use gix_worktree_stream::{Entry, Stream};
22

33
use 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")]
173161
fn 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(())

gix-archive/tests/archive.rs

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,18 @@ mod from_tree {
167167
},
168168
|buf| {
169169
assert!(
170-
buf.len() < 1280,
171-
"much bigger than uncompressed for some reason (565): {} < 1270",
170+
buf.len() < 1400,
171+
"much bigger than uncompressed for some reason (565): {} < 1400",
172172
buf.len()
173173
);
174-
let mut ar = zip::ZipArchive::new(std::io::Cursor::new(buf.as_slice()))?;
174+
let ar = rawzip::ZipArchive::from_slice(buf.as_slice())?;
175175
assert_eq!(
176176
{
177-
let mut n: Vec<_> = ar.file_names().collect();
177+
let mut n: Vec<_> = Vec::new();
178+
for entry_result in ar.entries() {
179+
let entry = entry_result?;
180+
n.push(String::from_utf8_lossy(entry.file_path().as_ref()).to_string());
181+
}
178182
n.sort();
179183
n
180184
},
@@ -190,13 +194,30 @@ mod from_tree {
190194
"prefix/symlink-to-a"
191195
]
192196
);
193-
let mut link = ar.by_name("prefix/symlink-to-a")?;
194-
assert!(!link.is_dir());
195-
assert!(link.is_symlink(), "symlinks are supported as well, but only on Unix");
196-
assert_eq!(link.unix_mode(), Some(0o120644), "the mode specifies what it should be");
197-
let mut buf = Vec::new();
198-
link.read_to_end(&mut buf)?;
199-
assert_eq!(buf.as_bstr(), "a");
197+
198+
// assertions for the symlink entry.
199+
let ar = rawzip::ZipArchive::from_slice(buf.as_slice())?;
200+
let mut found_link = false;
201+
for entry_result in ar.entries() {
202+
let entry = entry_result?;
203+
if String::from_utf8_lossy(entry.file_path().as_ref()) == "prefix/symlink-to-a" {
204+
let mode = entry.mode();
205+
assert!(mode.is_symlink(), "symlinks are supported as well, but only on Unix");
206+
assert_eq!(mode.value(), 0o120644, "the mode specifies what it should be");
207+
208+
let wayfinder = entry.wayfinder();
209+
let zip_entry = ar.get_entry(wayfinder)?;
210+
let data = zip_entry.data();
211+
assert_eq!(
212+
data.as_bstr(),
213+
"a",
214+
"For symlinks stored with Store compression, the data is uncompressed"
215+
);
216+
found_link = true;
217+
break;
218+
}
219+
}
220+
assert!(found_link, "symlink entry should be found");
200221
Ok(())
201222
},
202223
)

0 commit comments

Comments
 (0)