authorgravatar for 87334103+axlEscalada@users.noreply.github.comaxel escalada <87334103+axlEscalada@users.noreply.github.com> 2025-01-17 18:55:55-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-01-17 22:55:55+01:00
logf9a43770c87d4475a8e05b6539917ee26172b7c6
tree9b46610062fd58a0d15d441308f3d74c448ed612
parentc748eb2416d067688d1f4f40af78478e7dfdc4bf
signaturebadge-check Signed by PGP key B5690EEEBB952194

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

2 files changed, 108 insertions(+), 24 deletions(-)

lib/std/zip.zig+74-21
...@@ -215,7 +215,7 @@ const FileExtents = struct {...@@ -215,7 +215,7 @@ const FileExtents = struct {
215 local_file_header_offset: u64,215 local_file_header_offset: u64,
216};216};
217217
218fn readZip64FileExtents(header: CentralDirectoryFileHeader, extents: *FileExtents, data: []u8) !void {218fn readZip64FileExtents(comptime T: type, header: T, extents: *FileExtents, data: []u8) !void {
219 var data_offset: usize = 0;219 var data_offset: usize = 0;
220 if (isMaxInt(header.uncompressed_size)) {220 if (isMaxInt(header.uncompressed_size)) {
221 if (data_offset + 8 > data.len)221 if (data_offset + 8 > data.len)
...@@ -229,22 +229,28 @@ fn readZip64FileExtents(header: CentralDirectoryFileHeader, extents: *FileExtent...@@ -229,22 +229,28 @@ fn readZip64FileExtents(header: CentralDirectoryFileHeader, extents: *FileExtent
229 extents.compressed_size = std.mem.readInt(u64, data[data_offset..][0..8], .little);229 extents.compressed_size = std.mem.readInt(u64, data[data_offset..][0..8], .little);
230 data_offset += 8;230 data_offset += 8;
231 }231 }
232 if (isMaxInt(header.local_file_header_offset)) {232
233 if (data_offset + 8 > data.len)233 switch (T) {
234 return error.ZipBadCd64Size;234 CentralDirectoryFileHeader => {
235 extents.local_file_header_offset = std.mem.readInt(u64, data[data_offset..][0..8], .little);235 if (isMaxInt(header.local_file_header_offset)) {
236 data_offset += 8;236 if (data_offset + 8 > data.len)
237 }237 return error.ZipBadCd64Size;
238 if (isMaxInt(header.disk_number)) {238 extents.local_file_header_offset = std.mem.readInt(u64, data[data_offset..][0..8], .little);
239 if (data_offset + 4 > data.len)239 data_offset += 8;
240 return error.ZipInvalid;240 }
241 const disk_number = std.mem.readInt(u32, data[data_offset..][0..4], .little);241 if (isMaxInt(header.disk_number)) {
242 if (disk_number != 0)242 if (data_offset + 4 > data.len)
243 return error.ZipMultiDiskUnsupported;243 return error.ZipInvalid;
244 data_offset += 4;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 => {},
245 }253 }
246 if (data_offset > data.len)
247 return error.ZipBadCd64Size;
248}254}
249255
250pub fn Iterator(comptime SeekableStream: type) type {256pub fn Iterator(comptime SeekableStream: type) type {
...@@ -394,7 +400,7 @@ pub fn Iterator(comptime SeekableStream: type) type {...@@ -394,7 +400,7 @@ pub fn Iterator(comptime SeekableStream: type) type {
394 return error.ZipBadExtraFieldSize;400 return error.ZipBadExtraFieldSize;
395 const data = extra[extra_offset + 4 .. end];401 const data = extra[extra_offset + 4 .. end];
396 switch (@as(ExtraHeader, @enumFromInt(header_id))) {402 switch (@as(ExtraHeader, @enumFromInt(header_id))) {
397 .zip64_info => try readZip64FileExtents(header, &extents, data),403 .zip64_info => try readZip64FileExtents(CentralDirectoryFileHeader, header, &extents, data),
398 else => {}, // ignore404 else => {}, // ignore
399 }405 }
400 extra_offset = end;406 extra_offset = end;
...@@ -466,12 +472,45 @@ pub fn Iterator(comptime SeekableStream: type) type {...@@ -466,12 +472,45 @@ pub fn Iterator(comptime SeekableStream: type) type {
466 return error.ZipMismatchFlags;472 return error.ZipMismatchFlags;
467 if (local_header.crc32 != 0 and local_header.crc32 != self.crc32)473 if (local_header.crc32 != 0 and local_header.crc32 != self.crc32)
468 return error.ZipMismatchCrc32;474 return error.ZipMismatchCrc32;
469 if (local_header.compressed_size != 0 and475 var extents: FileExtents = .{
470 local_header.compressed_size != self.compressed_size)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)
471 return error.ZipMismatchCompLen;509 return error.ZipMismatchCompLen;
472 if (local_header.uncompressed_size != 0 and510 if (extents.uncompressed_size != 0 and
473 local_header.uncompressed_size != self.uncompressed_size)511 extents.uncompressed_size != self.uncompressed_size)
474 return error.ZipMismatchUncompLen;512 return error.ZipMismatchUncompLen;
513
475 if (local_header.filename_len != self.filename_len)514 if (local_header.filename_len != self.filename_len)
476 return error.ZipMismatchFilenameLen;515 return error.ZipMismatchFilenameLen;
477516
...@@ -695,6 +734,20 @@ test "zip64" {...@@ -695,6 +734,20 @@ test "zip64" {
695 .central_directory_offset = std.math.maxInt(u32), // trigger zip64734 .central_directory_offset = std.math.maxInt(u32), // trigger zip64
696 },735 },
697 });736 });
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 });
698}751}
699752
700test "bad zip files" {753test "bad zip files" {
lib/std/zip/test.zig+34-3
...@@ -70,6 +70,16 @@ pub fn makeZipWithStore(...@@ -70,6 +70,16 @@ pub fn makeZipWithStore(
7070
71pub const WriteZipOptions = struct {71pub const WriteZipOptions = struct {
72 end: ?EndRecordOptions = null,72 end: ?EndRecordOptions = null,
73 local_header: ?LocalHeaderOptions = null,
74};
75pub const LocalHeaderOptions = struct {
76 zip64: ?LocalHeaderZip64Options = null,
77 compressed_size: ?u32 = null,
78 uncompressed_size: ?u32 = null,
79 extra_len: ?u16 = null,
80};
81pub const LocalHeaderZip64Options = struct {
82 data_size: ?u16 = null,
73};83};
74pub const EndRecordOptions = struct {84pub const EndRecordOptions = struct {
75 zip64: ?Zip64Options = null,85 zip64: ?Zip64Options = null,
...@@ -105,6 +115,7 @@ pub fn writeZip(...@@ -105,6 +115,7 @@ pub fn writeZip(
105 .name = file.name,115 .name = file.name,
106 .content = file.content,116 .content = file.content,
107 .compression = file.compression,117 .compression = file.compression,
118 .write_options = options,
108 });119 });
109 }120 }
110 for (files, 0..) |file, i| {121 for (files, 0..) |file, i| {
...@@ -136,6 +147,7 @@ pub fn Zipper(comptime Writer: type) type {...@@ -136,6 +147,7 @@ pub fn Zipper(comptime Writer: type) type {
136 name: []const u8,147 name: []const u8,
137 content: []const u8,148 content: []const u8,
138 compression: zip.CompressionMethod,149 compression: zip.CompressionMethod,
150 write_options: WriteZipOptions,
139 },151 },
140 ) !FileStore {152 ) !FileStore {
141 const writer = self.counting_writer.writer();153 const writer = self.counting_writer.writer();
...@@ -143,7 +155,16 @@ pub fn Zipper(comptime Writer: type) type {...@@ -143,7 +155,16 @@ pub fn Zipper(comptime Writer: type) type {
143 const file_offset: u64 = @intCast(self.counting_writer.bytes_written);155 const file_offset: u64 = @intCast(self.counting_writer.bytes_written);
144 const crc32 = std.hash.Crc32.hash(opt.content);156 const crc32 = std.hash.Crc32.hash(opt.content);
145157
158 const header_options = opt.write_options.local_header;
146 {159 {
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 }
147 const hdr: zip.LocalFileHeader = .{168 const hdr: zip.LocalFileHeader = .{
148 .signature = zip.local_file_header_sig,169 .signature = zip.local_file_header_sig,
149 .version_needed_to_extract = 10,170 .version_needed_to_extract = 10,
...@@ -152,15 +173,25 @@ pub fn Zipper(comptime Writer: type) type {...@@ -152,15 +173,25 @@ pub fn Zipper(comptime Writer: type) type {
152 .last_modification_time = 0,173 .last_modification_time = 0,
153 .last_modification_date = 0,174 .last_modification_date = 0,
154 .crc32 = crc32,175 .crc32 = crc32,
155 .compressed_size = 0,176 .compressed_size = compressed_size,
156 .uncompressed_size = @intCast(opt.content.len),177 .uncompressed_size = uncompressed_size,
157 .filename_len = @intCast(opt.name.len),178 .filename_len = @intCast(opt.name.len),
158 .extra_len = 0,179 .extra_len = extra_len,
159 };180 };
160 try writer.writeStructEndian(hdr, .little);181 try writer.writeStructEndian(hdr, .little);
161 }182 }
162 try writer.writeAll(opt.name);183 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
164 var compressed_size: u32 = undefined;195 var compressed_size: u32 = undefined;
165 switch (opt.compression) {196 switch (opt.compression) {
166 .store => {197 .store => {