Skip to content

Commit

Permalink
std.zip: Add ZIP64 support for local file header extra field
Browse files Browse the repository at this point in the history
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: ziglang#22329
  • Loading branch information
axlEscalada authored and Fri3dNstuff committed Jan 27, 2025
1 parent 7c36666 commit 14a6bde
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 24 deletions.
95 changes: 74 additions & 21 deletions lib/std/zip.zig
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ const FileExtents = struct {
local_file_header_offset: u64,
};

fn readZip64FileExtents(header: CentralDirectoryFileHeader, extents: *FileExtents, data: []u8) !void {
fn readZip64FileExtents(comptime T: type, header: T, extents: *FileExtents, data: []u8) !void {
var data_offset: usize = 0;
if (isMaxInt(header.uncompressed_size)) {
if (data_offset + 8 > data.len)
Expand All @@ -229,22 +229,28 @@ fn readZip64FileExtents(header: CentralDirectoryFileHeader, extents: *FileExtent
extents.compressed_size = std.mem.readInt(u64, data[data_offset..][0..8], .little);
data_offset += 8;
}
if (isMaxInt(header.local_file_header_offset)) {
if (data_offset + 8 > data.len)
return error.ZipBadCd64Size;
extents.local_file_header_offset = std.mem.readInt(u64, data[data_offset..][0..8], .little);
data_offset += 8;
}
if (isMaxInt(header.disk_number)) {
if (data_offset + 4 > data.len)
return error.ZipInvalid;
const disk_number = std.mem.readInt(u32, data[data_offset..][0..4], .little);
if (disk_number != 0)
return error.ZipMultiDiskUnsupported;
data_offset += 4;

switch (T) {
CentralDirectoryFileHeader => {
if (isMaxInt(header.local_file_header_offset)) {
if (data_offset + 8 > data.len)
return error.ZipBadCd64Size;
extents.local_file_header_offset = std.mem.readInt(u64, data[data_offset..][0..8], .little);
data_offset += 8;
}
if (isMaxInt(header.disk_number)) {
if (data_offset + 4 > data.len)
return error.ZipInvalid;
const disk_number = std.mem.readInt(u32, data[data_offset..][0..4], .little);
if (disk_number != 0)
return error.ZipMultiDiskUnsupported;
data_offset += 4;
}
if (data_offset > data.len)
return error.ZipBadCd64Size;
},
else => {},
}
if (data_offset > data.len)
return error.ZipBadCd64Size;
}

pub fn Iterator(comptime SeekableStream: type) type {
Expand Down Expand Up @@ -394,7 +400,7 @@ pub fn Iterator(comptime SeekableStream: type) type {
return error.ZipBadExtraFieldSize;
const data = extra[extra_offset + 4 .. end];
switch (@as(ExtraHeader, @enumFromInt(header_id))) {
.zip64_info => try readZip64FileExtents(header, &extents, data),
.zip64_info => try readZip64FileExtents(CentralDirectoryFileHeader, header, &extents, data),
else => {}, // ignore
}
extra_offset = end;
Expand Down Expand Up @@ -466,12 +472,45 @@ pub fn Iterator(comptime SeekableStream: type) type {
return error.ZipMismatchFlags;
if (local_header.crc32 != 0 and local_header.crc32 != self.crc32)
return error.ZipMismatchCrc32;
if (local_header.compressed_size != 0 and
local_header.compressed_size != self.compressed_size)
var extents: FileExtents = .{
.uncompressed_size = local_header.uncompressed_size,
.compressed_size = local_header.compressed_size,
.local_file_header_offset = 0,
};
if (local_header.extra_len > 0) {
var extra_buf: [std.math.maxInt(u16)]u8 = undefined;
const extra = extra_buf[0..local_header.extra_len];

{
try stream.seekTo(self.file_offset + @sizeOf(LocalFileHeader) + local_header.filename_len);
const len = try stream.context.reader().readAll(extra);
if (len != extra.len)
return error.ZipTruncated;
}

var extra_offset: usize = 0;
while (extra_offset + 4 <= local_header.extra_len) {
const header_id = std.mem.readInt(u16, extra[extra_offset..][0..2], .little);
const data_size = std.mem.readInt(u16, extra[extra_offset..][2..4], .little);
const end = extra_offset + 4 + data_size;
if (end > local_header.extra_len)
return error.ZipBadExtraFieldSize;
const data = extra[extra_offset + 4 .. end];
switch (@as(ExtraHeader, @enumFromInt(header_id))) {
.zip64_info => try readZip64FileExtents(LocalFileHeader, local_header, &extents, data),
else => {}, // ignore
}
extra_offset = end;
}
}

if (extents.compressed_size != 0 and
extents.compressed_size != self.compressed_size)
return error.ZipMismatchCompLen;
if (local_header.uncompressed_size != 0 and
local_header.uncompressed_size != self.uncompressed_size)
if (extents.uncompressed_size != 0 and
extents.uncompressed_size != self.uncompressed_size)
return error.ZipMismatchUncompLen;

if (local_header.filename_len != self.filename_len)
return error.ZipMismatchFilenameLen;

Expand Down Expand Up @@ -695,6 +734,20 @@ test "zip64" {
.central_directory_offset = std.math.maxInt(u32), // trigger zip64
},
});
try testZip(.{}, &test_files, .{
.end = .{
.zip64 = .{},
.central_directory_offset = std.math.maxInt(u32), // trigger zip64
},
.local_header = .{
.zip64 = .{ // trigger local header zip64
.data_size = 16,
},
.compressed_size = std.math.maxInt(u32),
.uncompressed_size = std.math.maxInt(u32),
.extra_len = 20,
},
});
}

test "bad zip files" {
Expand Down
37 changes: 34 additions & 3 deletions lib/std/zip/test.zig
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ pub fn makeZipWithStore(

pub const WriteZipOptions = struct {
end: ?EndRecordOptions = null,
local_header: ?LocalHeaderOptions = null,
};
pub const LocalHeaderOptions = struct {
zip64: ?LocalHeaderZip64Options = null,
compressed_size: ?u32 = null,
uncompressed_size: ?u32 = null,
extra_len: ?u16 = null,
};
pub const LocalHeaderZip64Options = struct {
data_size: ?u16 = null,
};
pub const EndRecordOptions = struct {
zip64: ?Zip64Options = null,
Expand Down Expand Up @@ -105,6 +115,7 @@ pub fn writeZip(
.name = file.name,
.content = file.content,
.compression = file.compression,
.write_options = options,
});
}
for (files, 0..) |file, i| {
Expand Down Expand Up @@ -136,14 +147,24 @@ pub fn Zipper(comptime Writer: type) type {
name: []const u8,
content: []const u8,
compression: zip.CompressionMethod,
write_options: WriteZipOptions,
},
) !FileStore {
const writer = self.counting_writer.writer();

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

const header_options = opt.write_options.local_header;
{
var compressed_size: u32 = 0;
var uncompressed_size: u32 = 0;
var extra_len: u16 = 0;
if (header_options) |hdr_options| {
compressed_size = if (hdr_options.compressed_size) |size| size else 0;
uncompressed_size = if (hdr_options.uncompressed_size) |size| size else @intCast(opt.content.len);
extra_len = if (hdr_options.extra_len) |len| len else 0;
}
const hdr: zip.LocalFileHeader = .{
.signature = zip.local_file_header_sig,
.version_needed_to_extract = 10,
Expand All @@ -152,15 +173,25 @@ pub fn Zipper(comptime Writer: type) type {
.last_modification_time = 0,
.last_modification_date = 0,
.crc32 = crc32,
.compressed_size = 0,
.uncompressed_size = @intCast(opt.content.len),
.compressed_size = compressed_size,
.uncompressed_size = uncompressed_size,
.filename_len = @intCast(opt.name.len),
.extra_len = 0,
.extra_len = extra_len,
};
try writer.writeStructEndian(hdr, .little);
}
try writer.writeAll(opt.name);

if (header_options) |hdr| {
if (hdr.zip64) |options| {
try writer.writeInt(u16, 0x0001, .little);
const data_size = if (options.data_size) |size| size else 8;
try writer.writeInt(u16, data_size, .little);
try writer.writeInt(u64, 0, .little);
try writer.writeInt(u64, @intCast(opt.content.len), .little);
}
}

var compressed_size: u32 = undefined;
switch (opt.compression) {
.store => {
Expand Down

0 comments on commit 14a6bde

Please sign in to comment.