Skip to content

Commit f9a4377

Browse files
authored
std.zip: Add ZIP64 support for local file header extra field
This PR adds support for handling ZIP64 format in local file headers, when a zip file contains entries where the compressed or uncompressed size fields are set to 0xFFFFFFFF, and the extra field contains ZIP64 extended information tag (0x0001) The code now: Reads the actual sizes from the ZIP64 extra field data Validates these sizes against the entry's compressed and uncompressed sizes Zip file format spec.: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT This change allows proper extraction of ZIP files that use ZIP64 format in their local file headers. Fixes: #22329
1 parent c748eb2 commit f9a4377

File tree

2 files changed

+108
-24
lines changed

2 files changed

+108
-24
lines changed

lib/std/zip.zig

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ const FileExtents = struct {
215215
local_file_header_offset: u64,
216216
};
217217

218-
fn readZip64FileExtents(header: CentralDirectoryFileHeader, extents: *FileExtents, data: []u8) !void {
218+
fn readZip64FileExtents(comptime T: type, header: T, extents: *FileExtents, data: []u8) !void {
219219
var data_offset: usize = 0;
220220
if (isMaxInt(header.uncompressed_size)) {
221221
if (data_offset + 8 > data.len)
@@ -229,22 +229,28 @@ fn readZip64FileExtents(header: CentralDirectoryFileHeader, extents: *FileExtent
229229
extents.compressed_size = std.mem.readInt(u64, data[data_offset..][0..8], .little);
230230
data_offset += 8;
231231
}
232-
if (isMaxInt(header.local_file_header_offset)) {
233-
if (data_offset + 8 > data.len)
234-
return error.ZipBadCd64Size;
235-
extents.local_file_header_offset = std.mem.readInt(u64, data[data_offset..][0..8], .little);
236-
data_offset += 8;
237-
}
238-
if (isMaxInt(header.disk_number)) {
239-
if (data_offset + 4 > data.len)
240-
return error.ZipInvalid;
241-
const disk_number = std.mem.readInt(u32, data[data_offset..][0..4], .little);
242-
if (disk_number != 0)
243-
return error.ZipMultiDiskUnsupported;
244-
data_offset += 4;
232+
233+
switch (T) {
234+
CentralDirectoryFileHeader => {
235+
if (isMaxInt(header.local_file_header_offset)) {
236+
if (data_offset + 8 > data.len)
237+
return error.ZipBadCd64Size;
238+
extents.local_file_header_offset = std.mem.readInt(u64, data[data_offset..][0..8], .little);
239+
data_offset += 8;
240+
}
241+
if (isMaxInt(header.disk_number)) {
242+
if (data_offset + 4 > data.len)
243+
return error.ZipInvalid;
244+
const disk_number = std.mem.readInt(u32, data[data_offset..][0..4], .little);
245+
if (disk_number != 0)
246+
return error.ZipMultiDiskUnsupported;
247+
data_offset += 4;
248+
}
249+
if (data_offset > data.len)
250+
return error.ZipBadCd64Size;
251+
},
252+
else => {},
245253
}
246-
if (data_offset > data.len)
247-
return error.ZipBadCd64Size;
248254
}
249255

250256
pub fn Iterator(comptime SeekableStream: type) type {
@@ -394,7 +400,7 @@ pub fn Iterator(comptime SeekableStream: type) type {
394400
return error.ZipBadExtraFieldSize;
395401
const data = extra[extra_offset + 4 .. end];
396402
switch (@as(ExtraHeader, @enumFromInt(header_id))) {
397-
.zip64_info => try readZip64FileExtents(header, &extents, data),
403+
.zip64_info => try readZip64FileExtents(CentralDirectoryFileHeader, header, &extents, data),
398404
else => {}, // ignore
399405
}
400406
extra_offset = end;
@@ -466,12 +472,45 @@ pub fn Iterator(comptime SeekableStream: type) type {
466472
return error.ZipMismatchFlags;
467473
if (local_header.crc32 != 0 and local_header.crc32 != self.crc32)
468474
return error.ZipMismatchCrc32;
469-
if (local_header.compressed_size != 0 and
470-
local_header.compressed_size != self.compressed_size)
475+
var extents: FileExtents = .{
476+
.uncompressed_size = local_header.uncompressed_size,
477+
.compressed_size = local_header.compressed_size,
478+
.local_file_header_offset = 0,
479+
};
480+
if (local_header.extra_len > 0) {
481+
var extra_buf: [std.math.maxInt(u16)]u8 = undefined;
482+
const extra = extra_buf[0..local_header.extra_len];
483+
484+
{
485+
try stream.seekTo(self.file_offset + @sizeOf(LocalFileHeader) + local_header.filename_len);
486+
const len = try stream.context.reader().readAll(extra);
487+
if (len != extra.len)
488+
return error.ZipTruncated;
489+
}
490+
491+
var extra_offset: usize = 0;
492+
while (extra_offset + 4 <= local_header.extra_len) {
493+
const header_id = std.mem.readInt(u16, extra[extra_offset..][0..2], .little);
494+
const data_size = std.mem.readInt(u16, extra[extra_offset..][2..4], .little);
495+
const end = extra_offset + 4 + data_size;
496+
if (end > local_header.extra_len)
497+
return error.ZipBadExtraFieldSize;
498+
const data = extra[extra_offset + 4 .. end];
499+
switch (@as(ExtraHeader, @enumFromInt(header_id))) {
500+
.zip64_info => try readZip64FileExtents(LocalFileHeader, local_header, &extents, data),
501+
else => {}, // ignore
502+
}
503+
extra_offset = end;
504+
}
505+
}
506+
507+
if (extents.compressed_size != 0 and
508+
extents.compressed_size != self.compressed_size)
471509
return error.ZipMismatchCompLen;
472-
if (local_header.uncompressed_size != 0 and
473-
local_header.uncompressed_size != self.uncompressed_size)
510+
if (extents.uncompressed_size != 0 and
511+
extents.uncompressed_size != self.uncompressed_size)
474512
return error.ZipMismatchUncompLen;
513+
475514
if (local_header.filename_len != self.filename_len)
476515
return error.ZipMismatchFilenameLen;
477516

@@ -695,6 +734,20 @@ test "zip64" {
695734
.central_directory_offset = std.math.maxInt(u32), // trigger zip64
696735
},
697736
});
737+
try testZip(.{}, &test_files, .{
738+
.end = .{
739+
.zip64 = .{},
740+
.central_directory_offset = std.math.maxInt(u32), // trigger zip64
741+
},
742+
.local_header = .{
743+
.zip64 = .{ // trigger local header zip64
744+
.data_size = 16,
745+
},
746+
.compressed_size = std.math.maxInt(u32),
747+
.uncompressed_size = std.math.maxInt(u32),
748+
.extra_len = 20,
749+
},
750+
});
698751
}
699752

700753
test "bad zip files" {

lib/std/zip/test.zig

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ pub fn makeZipWithStore(
7070

7171
pub const WriteZipOptions = struct {
7272
end: ?EndRecordOptions = null,
73+
local_header: ?LocalHeaderOptions = null,
74+
};
75+
pub const LocalHeaderOptions = struct {
76+
zip64: ?LocalHeaderZip64Options = null,
77+
compressed_size: ?u32 = null,
78+
uncompressed_size: ?u32 = null,
79+
extra_len: ?u16 = null,
80+
};
81+
pub const LocalHeaderZip64Options = struct {
82+
data_size: ?u16 = null,
7383
};
7484
pub const EndRecordOptions = struct {
7585
zip64: ?Zip64Options = null,
@@ -105,6 +115,7 @@ pub fn writeZip(
105115
.name = file.name,
106116
.content = file.content,
107117
.compression = file.compression,
118+
.write_options = options,
108119
});
109120
}
110121
for (files, 0..) |file, i| {
@@ -136,14 +147,24 @@ pub fn Zipper(comptime Writer: type) type {
136147
name: []const u8,
137148
content: []const u8,
138149
compression: zip.CompressionMethod,
150+
write_options: WriteZipOptions,
139151
},
140152
) !FileStore {
141153
const writer = self.counting_writer.writer();
142154

143155
const file_offset: u64 = @intCast(self.counting_writer.bytes_written);
144156
const crc32 = std.hash.Crc32.hash(opt.content);
145157

158+
const header_options = opt.write_options.local_header;
146159
{
160+
var compressed_size: u32 = 0;
161+
var uncompressed_size: u32 = 0;
162+
var extra_len: u16 = 0;
163+
if (header_options) |hdr_options| {
164+
compressed_size = if (hdr_options.compressed_size) |size| size else 0;
165+
uncompressed_size = if (hdr_options.uncompressed_size) |size| size else @intCast(opt.content.len);
166+
extra_len = if (hdr_options.extra_len) |len| len else 0;
167+
}
147168
const hdr: zip.LocalFileHeader = .{
148169
.signature = zip.local_file_header_sig,
149170
.version_needed_to_extract = 10,
@@ -152,15 +173,25 @@ pub fn Zipper(comptime Writer: type) type {
152173
.last_modification_time = 0,
153174
.last_modification_date = 0,
154175
.crc32 = crc32,
155-
.compressed_size = 0,
156-
.uncompressed_size = @intCast(opt.content.len),
176+
.compressed_size = compressed_size,
177+
.uncompressed_size = uncompressed_size,
157178
.filename_len = @intCast(opt.name.len),
158-
.extra_len = 0,
179+
.extra_len = extra_len,
159180
};
160181
try writer.writeStructEndian(hdr, .little);
161182
}
162183
try writer.writeAll(opt.name);
163184

185+
if (header_options) |hdr| {
186+
if (hdr.zip64) |options| {
187+
try writer.writeInt(u16, 0x0001, .little);
188+
const data_size = if (options.data_size) |size| size else 8;
189+
try writer.writeInt(u16, data_size, .little);
190+
try writer.writeInt(u64, 0, .little);
191+
try writer.writeInt(u64, @intCast(opt.content.len), .little);
192+
}
193+
}
194+
164195
var compressed_size: u32 = undefined;
165196
switch (opt.compression) {
166197
.store => {

0 commit comments

Comments
 (0)