1//! The .ZIP File Format Specification is found here:
2//! https://pkwaredownloads.blob.core.windows.net/pem/APPNOTE.txt
3//!
4//! Note that this file uses the abbreviation "cd" for "central directory"
5
6const builtin = @import("builtin");
7const is_le = builtin.target.cpu.arch.endian() == .little;
8
9const std = @import("std");
10const Io = std.Io;
11const File = std.Io.File;
12const Writer = std.Io.Writer;
13const Reader = std.Io.Reader;
14const flate = std.compress.flate;
15
16pub const CompressionMethod = enum(u16) {
17 store = 0,
18 deflate = 8,
19 _,
20};
21
22pub const central_file_header_sig = [4]u8{ 'P', 'K', 1, 2 };
23pub const local_file_header_sig = [4]u8{ 'P', 'K', 3, 4 };
24pub const end_record_sig = [4]u8{ 'P', 'K', 5, 6 };
25pub const end_record64_sig = [4]u8{ 'P', 'K', 6, 6 };
26pub const end_locator64_sig = [4]u8{ 'P', 'K', 6, 7 };
27pub const ExtraHeader = enum(u16) {
28 zip64_info = 0x1,
29 _,
30};
31
32const GeneralPurposeFlags = packed struct(u16) {
33 encrypted: bool,
34 _: u15,
35};
36
37pub const LocalFileHeader = extern struct {
38 signature: [4]u8 align(1),
39 version_needed_to_extract: u16 align(1),
40 flags: GeneralPurposeFlags align(1),
41 compression_method: CompressionMethod align(1),
42 last_modification_time: u16 align(1),
43 last_modification_date: u16 align(1),
44 crc32: u32 align(1),
45 compressed_size: u32 align(1),
46 uncompressed_size: u32 align(1),
47 filename_len: u16 align(1),
48 extra_len: u16 align(1),
49};
50
51pub const CentralDirectoryFileHeader = extern struct {
52 signature: [4]u8 align(1),
53 version_made_by: u16 align(1),
54 version_needed_to_extract: u16 align(1),
55 flags: GeneralPurposeFlags align(1),
56 compression_method: CompressionMethod align(1),
57 last_modification_time: u16 align(1),
58 last_modification_date: u16 align(1),
59 crc32: u32 align(1),
60 compressed_size: u32 align(1),
61 uncompressed_size: u32 align(1),
62 filename_len: u16 align(1),
63 extra_len: u16 align(1),
64 comment_len: u16 align(1),
65 disk_number: u16 align(1),
66 internal_file_attributes: u16 align(1),
67 external_file_attributes: u32 align(1),
68 local_file_header_offset: u32 align(1),
69};
70
71pub const EndRecord64 = extern struct {
72 signature: [4]u8 align(1),
73 end_record_size: u64 align(1),
74 version_made_by: u16 align(1),
75 version_needed_to_extract: u16 align(1),
76 disk_number: u32 align(1),
77 central_directory_disk_number: u32 align(1),
78 record_count_disk: u64 align(1),
79 record_count_total: u64 align(1),
80 central_directory_size: u64 align(1),
81 central_directory_offset: u64 align(1),
82};
83
84pub const EndLocator64 = extern struct {
85 signature: [4]u8 align(1),
86 zip64_disk_count: u32 align(1),
87 record_file_offset: u64 align(1),
88 total_disk_count: u32 align(1),
89};
90
91pub const EndRecord = extern struct {
92 signature: [4]u8 align(1),
93 disk_number: u16 align(1),
94 central_directory_disk_number: u16 align(1),
95 record_count_disk: u16 align(1),
96 record_count_total: u16 align(1),
97 central_directory_size: u32 align(1),
98 central_directory_offset: u32 align(1),
99 comment_len: u16 align(1),
100
101 pub fn need_zip64(self: EndRecord) bool {
102 return isMaxInt(self.record_count_disk) or
103 isMaxInt(self.record_count_total) or
104 isMaxInt(self.central_directory_size) or
105 isMaxInt(self.central_directory_offset);
106 }
107
108 pub const FindBufferError = error{ ZipNoEndRecord, ZipTruncated };
109
110 /// TODO audit this logic
111 pub fn findBuffer(buffer: []const u8) FindBufferError!EndRecord {
112 const pos = std.mem.findLast(u8, buffer, &end_record_sig) orelse return error.ZipNoEndRecord;
113 if (pos + @sizeOf(EndRecord) > buffer.len) return error.EndOfStream;
114 const record_ptr: *EndRecord = @ptrCast(buffer[pos..][0..@sizeOf(EndRecord)]);
115 var record = record_ptr.*;
116 if (!is_le) std.mem.byteSwapAllFields(EndRecord, &record);
117 return record;
118 }
119
120 pub const FindFileError = File.Reader.SizeError || File.SeekError || File.Reader.Error || error{
121 ZipNoEndRecord,
122 EndOfStream,
123 ReadFailed,
124 };
125
126 pub fn findFile(fr: *File.Reader) FindFileError!EndRecord {
127 const end_pos = try fr.getSize();
128
129 var buf: [@sizeOf(EndRecord) + std.math.maxInt(u16)]u8 = undefined;
130 const record_len_max = @min(end_pos, buf.len);
131 var loaded_len: u32 = 0;
132 var comment_len: u16 = 0;
133 while (true) {
134 const record_len: u32 = @as(u32, comment_len) + @sizeOf(EndRecord);
135 if (record_len > record_len_max)
136 return error.ZipNoEndRecord;
137
138 if (record_len > loaded_len) {
139 const new_loaded_len = @min(loaded_len + 300, record_len_max);
140 const read_len = new_loaded_len - loaded_len;
141
142 try fr.seekTo(end_pos - @as(u64, new_loaded_len));
143 const read_buf: []u8 = buf[buf.len - new_loaded_len ..][0..read_len];
144 fr.interface.readSliceAll(read_buf) catch |err| switch (err) {
145 error.ReadFailed => return fr.err.?,
146 error.EndOfStream => |e| return e,
147 };
148 loaded_len = new_loaded_len;
149 }
150
151 const record_bytes = buf[buf.len - record_len ..][0..@sizeOf(EndRecord)];
152 if (std.mem.eql(u8, record_bytes[0..4], &end_record_sig) and
153 std.mem.readInt(u16, record_bytes[20..22], .little) == comment_len)
154 {
155 const record: *align(1) EndRecord = @ptrCast(record_bytes.ptr);
156 if (!is_le) std.mem.byteSwapAllFields(EndRecord, record);
157 return record.*;
158 }
159
160 if (comment_len == std.math.maxInt(u16))
161 return error.ZipNoEndRecord;
162 comment_len += 1;
163 }
164 }
165};
166
167fn isBadFilename(filename: []const u8) bool {
168 if (filename.len == 0 or filename[0] == '/')
169 return true;
170
171 var it = std.mem.splitScalar(u8, filename, '/');
172 while (it.next()) |part| {
173 if (std.mem.eql(u8, part, ".."))
174 return true;
175 }
176
177 return false;
178}
179
180fn isMaxInt(uint: anytype) bool {
181 return uint == std.math.maxInt(@TypeOf(uint));
182}
183
184const FileExtents = struct {
185 uncompressed_size: u64,
186 compressed_size: u64,
187 local_file_header_offset: u64,
188};
189
190fn readZip64FileExtents(comptime T: type, header: T, extents: *FileExtents, data: []u8) !void {
191 var data_offset: usize = 0;
192 if (isMaxInt(header.uncompressed_size)) {
193 if (data_offset + 8 > data.len)
194 return error.ZipBadCd64Size;
195 extents.uncompressed_size = std.mem.readInt(u64, data[data_offset..][0..8], .little);
196 data_offset += 8;
197 }
198 if (isMaxInt(header.compressed_size)) {
199 if (data_offset + 8 > data.len)
200 return error.ZipBadCd64Size;
201 extents.compressed_size = std.mem.readInt(u64, data[data_offset..][0..8], .little);
202 data_offset += 8;
203 }
204
205 switch (T) {
206 CentralDirectoryFileHeader => {
207 if (isMaxInt(header.local_file_header_offset)) {
208 if (data_offset + 8 > data.len)
209 return error.ZipBadCd64Size;
210 extents.local_file_header_offset = std.mem.readInt(u64, data[data_offset..][0..8], .little);
211 data_offset += 8;
212 }
213 if (isMaxInt(header.disk_number)) {
214 if (data_offset + 4 > data.len)
215 return error.ZipInvalid;
216 const disk_number = std.mem.readInt(u32, data[data_offset..][0..4], .little);
217 if (disk_number != 0)
218 return error.ZipMultiDiskUnsupported;
219 data_offset += 4;
220 }
221 if (data_offset > data.len)
222 return error.ZipBadCd64Size;
223 },
224 else => {},
225 }
226}
227
228pub const Iterator = struct {
229 input: *File.Reader,
230
231 cd_record_count: u64,
232 cd_zip_offset: u64,
233 cd_size: u64,
234
235 cd_record_index: u64 = 0,
236 cd_record_offset: u64 = 0,
237
238 pub fn init(input: *File.Reader) !Iterator {
239 const end_record = try EndRecord.findFile(input);
240
241 if (!isMaxInt(end_record.record_count_disk) and end_record.record_count_disk > end_record.record_count_total)
242 return error.ZipDiskRecordCountTooLarge;
243
244 if (end_record.disk_number != 0 or end_record.central_directory_disk_number != 0)
245 return error.ZipMultiDiskUnsupported;
246
247 {
248 const counts_valid = !isMaxInt(end_record.record_count_disk) and !isMaxInt(end_record.record_count_total);
249 if (counts_valid and end_record.record_count_disk != end_record.record_count_total)
250 return error.ZipMultiDiskUnsupported;
251 }
252
253 var result: Iterator = .{
254 .input = input,
255 .cd_record_count = end_record.record_count_total,
256 .cd_zip_offset = end_record.central_directory_offset,
257 .cd_size = end_record.central_directory_size,
258 };
259 if (!end_record.need_zip64()) return result;
260
261 const locator_end_offset: u64 = @as(u64, end_record.comment_len) + @sizeOf(EndRecord) + @sizeOf(EndLocator64);
262 const stream_len = try input.getSize();
263
264 if (locator_end_offset > stream_len)
265 return error.ZipTruncated;
266 try input.seekTo(stream_len - locator_end_offset);
267 const locator = input.interface.takeStruct(EndLocator64, .little) catch |err| switch (err) {
268 error.ReadFailed => return input.err.?,
269 error.EndOfStream => |e| return e,
270 };
271 if (!std.mem.eql(u8, &locator.signature, &end_locator64_sig))
272 return error.ZipBadLocatorSig;
273 if (locator.zip64_disk_count != 0)
274 return error.ZipUnsupportedZip64DiskCount;
275 if (locator.total_disk_count != 1)
276 return error.ZipMultiDiskUnsupported;
277
278 try input.seekTo(locator.record_file_offset);
279
280 const record64 = input.interface.takeStruct(EndRecord64, .little) catch |err| switch (err) {
281 error.ReadFailed => return input.err.?,
282 error.EndOfStream => |e| return e,
283 };
284
285 if (!std.mem.eql(u8, &record64.signature, &end_record64_sig))
286 return error.ZipBadEndRecord64Sig;
287
288 if (record64.end_record_size < @sizeOf(EndRecord64) - 12)
289 return error.ZipEndRecord64SizeTooSmall;
290 if (record64.end_record_size > @sizeOf(EndRecord64) - 12)
291 return error.ZipEndRecord64UnhandledExtraData;
292
293 if (record64.version_needed_to_extract > 45)
294 return error.ZipUnsupportedVersion;
295
296 {
297 const is_multidisk = record64.disk_number != 0 or
298 record64.central_directory_disk_number != 0 or
299 record64.record_count_disk != record64.record_count_total;
300 if (is_multidisk)
301 return error.ZipMultiDiskUnsupported;
302 }
303
304 if (isMaxInt(end_record.record_count_total)) {
305 result.cd_record_count = record64.record_count_total;
306 } else if (end_record.record_count_total != record64.record_count_total)
307 return error.Zip64RecordCountTotalMismatch;
308
309 if (isMaxInt(end_record.central_directory_offset)) {
310 result.cd_zip_offset = record64.central_directory_offset;
311 } else if (end_record.central_directory_offset != record64.central_directory_offset)
312 return error.Zip64CentralDirectoryOffsetMismatch;
313
314 if (isMaxInt(end_record.central_directory_size)) {
315 result.cd_size = record64.central_directory_size;
316 } else if (end_record.central_directory_size != record64.central_directory_size)
317 return error.Zip64CentralDirectorySizeMismatch;
318
319 return result;
320 }
321
322 pub fn next(self: *Iterator) !?Entry {
323 if (self.cd_record_index == self.cd_record_count) {
324 if (self.cd_record_offset != self.cd_size)
325 return if (self.cd_size > self.cd_record_offset)
326 error.ZipCdOversized
327 else
328 error.ZipCdUndersized;
329
330 return null;
331 }
332
333 const header_zip_offset = self.cd_zip_offset + self.cd_record_offset;
334 const input = self.input;
335 try input.seekTo(header_zip_offset);
336 const header = input.interface.takeStruct(CentralDirectoryFileHeader, .little) catch |err| switch (err) {
337 error.ReadFailed => return input.err.?,
338 error.EndOfStream => |e| return e,
339 };
340 if (!std.mem.eql(u8, &header.signature, &central_file_header_sig))
341 return error.ZipBadCdOffset;
342
343 self.cd_record_index += 1;
344 self.cd_record_offset += @sizeOf(CentralDirectoryFileHeader) + header.filename_len + header.extra_len + header.comment_len;
345
346 // Note: checking the version_needed_to_extract doesn't seem to be helpful, i.e. the zip file
347 // at https://github.com/ninja-build/ninja/releases/download/v1.12.0/ninja-linux.zip
348 // has an undocumented version 788 but extracts just fine.
349
350 if (header.flags.encrypted)
351 return error.ZipEncryptionUnsupported;
352 // TODO: check/verify more flags
353 if (header.disk_number != 0)
354 return error.ZipMultiDiskUnsupported;
355
356 var extents: FileExtents = .{
357 .uncompressed_size = header.uncompressed_size,
358 .compressed_size = header.compressed_size,
359 .local_file_header_offset = header.local_file_header_offset,
360 };
361
362 if (header.extra_len > 0) {
363 var extra_buf: [std.math.maxInt(u16)]u8 = undefined;
364 const extra = extra_buf[0..header.extra_len];
365
366 try input.seekTo(header_zip_offset + @sizeOf(CentralDirectoryFileHeader) + header.filename_len);
367 input.interface.readSliceAll(extra) catch |err| switch (err) {
368 error.ReadFailed => return input.err.?,
369 error.EndOfStream => |e| return e,
370 };
371
372 var extra_offset: usize = 0;
373 while (extra_offset + 4 <= extra.len) {
374 const header_id = std.mem.readInt(u16, extra[extra_offset..][0..2], .little);
375 const data_size = std.mem.readInt(u16, extra[extra_offset..][2..4], .little);
376 const end = extra_offset + 4 + data_size;
377 if (end > extra.len)
378 return error.ZipBadExtraFieldSize;
379 const data = extra[extra_offset + 4 .. end];
380 switch (@as(ExtraHeader, @fromBackingInt(@intCast(header_id)))) {
381 .zip64_info => try readZip64FileExtents(CentralDirectoryFileHeader, header, &extents, data),
382 else => {}, // ignore
383 }
384 extra_offset = end;
385 }
386 }
387
388 return .{
389 .version_needed_to_extract = header.version_needed_to_extract,
390 .flags = header.flags,
391 .compression_method = header.compression_method,
392 .last_modification_time = header.last_modification_time,
393 .last_modification_date = header.last_modification_date,
394 .header_zip_offset = header_zip_offset,
395 .crc32 = header.crc32,
396 .filename_len = header.filename_len,
397 .compressed_size = extents.compressed_size,
398 .uncompressed_size = extents.uncompressed_size,
399 .file_offset = extents.local_file_header_offset,
400 };
401 }
402
403 pub const Entry = struct {
404 version_needed_to_extract: u16,
405 flags: GeneralPurposeFlags,
406 compression_method: CompressionMethod,
407 last_modification_time: u16,
408 last_modification_date: u16,
409 header_zip_offset: u64,
410 crc32: u32,
411 filename_len: u32,
412 compressed_size: u64,
413 uncompressed_size: u64,
414 file_offset: u64,
415
416 pub fn getFilename(self: Entry, stream: *File.Reader, filename_buf: []u8, options: ExtractOptions) ![]u8 {
417 if (filename_buf.len < self.filename_len)
418 return error.ZipInsufficientBuffer;
419 switch (self.compression_method) {
420 .store, .deflate => {},
421 else => return error.UnsupportedCompressionMethod,
422 }
423 const filename = filename_buf[0..self.filename_len];
424 {
425 try stream.seekTo(self.header_zip_offset + @sizeOf(CentralDirectoryFileHeader));
426 try stream.interface.readSliceAll(filename);
427 }
428
429 if (options.allow_backslashes) {
430 std.mem.replaceScalar(u8, filename, '\\', '/');
431 } else {
432 if (std.mem.findScalar(u8, filename, '\\')) |_|
433 return error.ZipFilenameHasBackslash;
434 }
435
436 if (isBadFilename(filename))
437 return error.ZipBadFilename;
438
439 return filename;
440 }
441
442 pub fn extractTo(self: Entry, stream: *File.Reader, w: *Writer) !void {
443 switch (self.compression_method) {
444 .store, .deflate => {},
445 else => return error.UnsupportedCompressionMethod,
446 }
447
448 const local_data_header_offset: u64 = local_data_header_offset: {
449 const local_header = blk: {
450 try stream.seekTo(self.file_offset);
451 break :blk try stream.interface.takeStruct(LocalFileHeader, .little);
452 };
453 if (!std.mem.eql(u8, &local_header.signature, &local_file_header_sig))
454 return error.ZipBadFileOffset;
455 if (local_header.version_needed_to_extract != self.version_needed_to_extract)
456 return error.ZipMismatchVersionNeeded;
457 if (local_header.last_modification_time != self.last_modification_time)
458 return error.ZipMismatchModTime;
459 if (local_header.last_modification_date != self.last_modification_date)
460 return error.ZipMismatchModDate;
461
462 if (@as(u16, @bitCast(local_header.flags)) != @as(u16, @bitCast(self.flags)))
463 return error.ZipMismatchFlags;
464 if (local_header.crc32 != 0 and local_header.crc32 != self.crc32)
465 return error.ZipMismatchCrc32;
466 var extents: FileExtents = .{
467 .uncompressed_size = local_header.uncompressed_size,
468 .compressed_size = local_header.compressed_size,
469 .local_file_header_offset = 0,
470 };
471 if (local_header.extra_len > 0) {
472 var extra_buf: [std.math.maxInt(u16)]u8 = undefined;
473 const extra = extra_buf[0..local_header.extra_len];
474
475 {
476 try stream.seekTo(self.file_offset + @sizeOf(LocalFileHeader) + local_header.filename_len);
477 try stream.interface.readSliceAll(extra);
478 }
479
480 var extra_offset: usize = 0;
481 while (extra_offset + 4 <= local_header.extra_len) {
482 const header_id = std.mem.readInt(u16, extra[extra_offset..][0..2], .little);
483 const data_size = std.mem.readInt(u16, extra[extra_offset..][2..4], .little);
484 const end = extra_offset + 4 + data_size;
485 if (end > local_header.extra_len)
486 return error.ZipBadExtraFieldSize;
487 const data = extra[extra_offset + 4 .. end];
488 switch (@as(ExtraHeader, @fromBackingInt(@intCast(header_id)))) {
489 .zip64_info => try readZip64FileExtents(LocalFileHeader, local_header, &extents, data),
490 else => {}, // ignore
491 }
492 extra_offset = end;
493 }
494 }
495
496 if (extents.compressed_size != 0 and
497 extents.compressed_size != self.compressed_size)
498 return error.ZipMismatchCompLen;
499 if (extents.uncompressed_size != 0 and
500 extents.uncompressed_size != self.uncompressed_size)
501 return error.ZipMismatchUncompLen;
502
503 if (local_header.filename_len != self.filename_len)
504 return error.ZipMismatchFilenameLen;
505
506 break :local_data_header_offset @as(u64, local_header.filename_len) +
507 @as(u64, local_header.extra_len);
508 };
509
510 const local_data_file_offset: u64 =
511 @as(u64, self.file_offset) +
512 @as(u64, @sizeOf(LocalFileHeader)) +
513 local_data_header_offset;
514 try stream.seekTo(local_data_file_offset);
515
516 // TODO limit based on self.compressed_size
517
518 switch (self.compression_method) {
519 .store => {
520 stream.interface.streamExact64(w, self.uncompressed_size) catch |err| switch (err) {
521 error.ReadFailed => |e| return stream.err orelse e,
522 error.WriteFailed => |e| return e,
523 error.EndOfStream => return error.ZipDecompressTruncated,
524 };
525 },
526 .deflate => {
527 var flate_buffer: [flate.max_window_len]u8 = undefined;
528 var decompress: flate.Decompress = .init(&stream.interface, .raw, &flate_buffer);
529 decompress.reader.streamExact64(w, self.uncompressed_size) catch |err| switch (err) {
530 error.ReadFailed => |e| return decompress.err orelse (stream.err orelse e),
531 error.WriteFailed => |e| return e,
532 error.EndOfStream => return error.ZipDecompressTruncated,
533 };
534 },
535 else => return error.UnsupportedCompressionMethod,
536 }
537 }
538
539 pub fn extract(
540 self: Entry,
541 stream: *File.Reader,
542 options: ExtractOptions,
543 filename_buf: []u8,
544 dest: Io.Dir,
545 ) !void {
546 const io = stream.io;
547
548 const filename = try self.getFilename(stream, filename_buf, options);
549
550 // All entries that end in '/' are directories
551 if (filename[filename.len - 1] == '/') {
552 if (self.uncompressed_size != 0)
553 return error.ZipBadDirectorySize;
554 try dest.createDirPath(io, filename[0 .. filename.len - 1]);
555 return;
556 }
557
558 const out_file = blk: {
559 if (std.fs.path.dirname(filename)) |dirname| {
560 var parent_dir = try dest.createDirPathOpen(io, dirname, .{});
561 defer parent_dir.close(io);
562
563 const basename = std.fs.path.basename(filename);
564 break :blk try parent_dir.createFile(io, basename, .{ .exclusive = true });
565 }
566 break :blk try dest.createFile(io, filename, .{ .exclusive = true });
567 };
568 defer out_file.close(io);
569 var out_file_buffer: [1024]u8 = undefined;
570 var file_writer = out_file.writer(io, &out_file_buffer);
571 self.extractTo(stream, &file_writer.interface) catch |err| switch (err) {
572 error.WriteFailed => |e| return file_writer.err orelse e,
573 else => return err,
574 };
575 try file_writer.end();
576 }
577 };
578};
579
580// returns true if `filename` starts with `root` followed by a forward slash
581fn filenameInRoot(filename: []const u8, root: []const u8) bool {
582 return (filename.len >= root.len + 1) and
583 (filename[root.len] == '/') and
584 std.mem.eql(u8, filename[0..root.len], root);
585}
586
587pub const Diagnostics = struct {
588 allocator: std.mem.Allocator,
589
590 /// The common root directory for all extracted files if there is one.
591 root_dir: []const u8 = "",
592
593 saw_first_file: bool = false,
594
595 pub fn deinit(self: *Diagnostics) void {
596 self.allocator.free(self.root_dir);
597 self.* = undefined;
598 }
599
600 // This function assumes name is a filename from a zip file which has already been verified to
601 // not start with a slash, backslashes have been normalized to forward slashes, and directories
602 // always end in a slash.
603 pub fn nextFilename(self: *Diagnostics, name: []const u8) error{OutOfMemory}!void {
604 if (!self.saw_first_file) {
605 self.saw_first_file = true;
606 std.debug.assert(self.root_dir.len == 0);
607 const root_len = std.mem.findScalar(u8, name, '/') orelse return;
608 std.debug.assert(root_len > 0);
609 self.root_dir = try self.allocator.dupe(u8, name[0..root_len]);
610 } else if (self.root_dir.len > 0) {
611 if (!filenameInRoot(name, self.root_dir)) {
612 self.allocator.free(self.root_dir);
613 self.root_dir = "";
614 }
615 }
616 }
617};
618
619pub const ExtractOptions = struct {
620 /// Allow filenames within the zip to use backslashes. Back slashes are normalized
621 /// to forward slashes before forwarding them to platform APIs.
622 allow_backslashes: bool = false,
623 diagnostics: ?*Diagnostics = null,
624 verify_checksums: bool = false,
625};
626
627/// Extract the zipped files to the given `dest` directory.
628pub fn extract(dest: Io.Dir, fr: *File.Reader, options: ExtractOptions) !void {
629 if (options.verify_checksums) @panic("TODO unimplemented");
630
631 var iter = try Iterator.init(fr);
632
633 var filename_buf: [std.fs.max_path_bytes]u8 = undefined;
634 while (try iter.next()) |entry| {
635 try entry.extract(fr, options, &filename_buf, dest);
636 if (options.diagnostics) |d| {
637 try d.nextFilename(filename_buf[0..entry.filename_len]);
638 }
639 }
640}
641
642const testing = std.testing;
643
644test "extractTo" {
645 const io = testing.io;
646 var tmp = testing.tmpDir(.{});
647 defer tmp.cleanup();
648
649 try tmp.dir.writeFile(io, .{
650 .sub_path = "test.zip",
651 .data = @embedFile("zip/testdata/test.zip"),
652 });
653
654 var file = try tmp.dir.openFile(io, "test.zip", .{});
655 defer file.close(io);
656 var read_buf: [512]u8 = undefined;
657 var reader = file.reader(io, &read_buf);
658
659 const Expected = struct {
660 contents: []const u8,
661 compression: CompressionMethod,
662 };
663 const expected_map = std.StaticStringMap(Expected).initComptime(.{
664 .{ "deflate.txt", Expected{ .contents = "aaaaaaaaaaaaaaaaaaaaaaaa\n", .compression = .deflate } },
665 .{ "store.txt", Expected{ .contents = "hello world\n", .compression = .store } },
666 .{ "dir/", Expected{ .contents = "", .compression = .store } },
667 });
668
669 var iter = try Iterator.init(&reader);
670 var num_entries: usize = 0;
671 while (try iter.next()) |entry| {
672 var filename_buf: [256]u8 = undefined;
673 const filename = try entry.getFilename(&reader, &filename_buf, .{});
674 const expected = expected_map.get(filename) orelse {
675 std.debug.print("found unexpected filename: {f}\n", .{std.ascii.hexEscape(filename, .lower)});
676 return error.UnexpectedFilename;
677 };
678 var buf: [256]u8 = undefined;
679 var w: Writer = .fixed(&buf);
680 try entry.extractTo(&reader, &w);
681 try testing.expectEqualStrings(expected.contents, w.buffered());
682 try testing.expectEqual(expected.compression, entry.compression_method);
683 num_entries += 1;
684 }
685 try testing.expectEqual(expected_map.kvs.len, num_entries);
686}
687
688test "output buffers too small" {
689 const io = testing.io;
690 var tmp = testing.tmpDir(.{});
691 defer tmp.cleanup();
692
693 try tmp.dir.writeFile(io, .{
694 .sub_path = "test.zip",
695 .data = @embedFile("zip/testdata/test.zip"),
696 });
697
698 var file = try tmp.dir.openFile(io, "test.zip", .{});
699 defer file.close(io);
700 var read_buf: [512]u8 = undefined;
701 var reader = file.reader(io, &read_buf);
702
703 var iter = try Iterator.init(&reader);
704 var num_entries: usize = 0;
705 while (try iter.next()) |entry| {
706 try testing.expectError(
707 error.ZipInsufficientBuffer,
708 entry.getFilename(&reader, &.{}, .{}),
709 );
710
711 if (entry.uncompressed_size <= 1) continue;
712
713 var buf: [1]u8 = undefined;
714 var w: Writer = .fixed(&buf);
715 try testing.expectError(
716 error.WriteFailed,
717 entry.extractTo(&reader, &w),
718 );
719 num_entries += 1;
720 }
721 try std.testing.expect(num_entries > 0);
722}