authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-05-06 20:21:01-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-01 16:35:29-07:00
loge76afef8477ae6f1df7c8132342aa6ddea51d4fb
tree36cb05fc273c9f378aac0f919b0b121fbb9d3554
parent576702ca40cef65964709ad70595ad913f1944ab

std.zip: operate on fs.File.Reader rather than anytype


6 files changed, 614 insertions(+), 601 deletions(-)

lib/std/io/BufferedReader.zig-15
...@@ -192,21 +192,6 @@ fn passthruReadVec(context: ?*anyopaque, data: []const []u8) Reader.Error!usize...@@ -192,21 +192,6 @@ fn passthruReadVec(context: ?*anyopaque, data: []const []u8) Reader.Error!usize
192 return readVecLimit(br, data, .unlimited);192 return readVecLimit(br, data, .unlimited);
193}193}
194194
195pub fn seekBy(br: *BufferedReader, seek_by: i64) !void {
196 if (seek_by < 0) try br.seekBackwardBy(@abs(seek_by)) else try br.seekForwardBy(@abs(seek_by));
197}
198
199pub fn seekBackwardBy(br: *BufferedReader, seek_by: u64) !void {
200 if (seek_by > br.end - br.seek) return error.Unseekable; // TODO
201 br.seek += @abs(seek_by);
202}
203
204pub fn seekForwardBy(br: *BufferedReader, seek_by: u64) !void {
205 const seek, const need_unbuffered_seek = @subWithOverflow(br.seek, @abs(seek_by));
206 if (need_unbuffered_seek > 0) return error.Unseekable; // TODO
207 br.seek = seek;
208}
209
210/// Returns the next `len` bytes from `unbuffered_reader`, filling the buffer as195/// Returns the next `len` bytes from `unbuffered_reader`, filling the buffer as
211/// necessary.196/// necessary.
212///197///
lib/std/io/Writer/Null.zig+1-1
...@@ -35,7 +35,7 @@ fn writeFile(...@@ -35,7 +35,7 @@ fn writeFile(
35 limit: Writer.Limit,35 limit: Writer.Limit,
36 headers_and_trailers: []const []const u8,36 headers_and_trailers: []const []const u8,
37 headers_len: usize,37 headers_len: usize,
38) Writer.Error!usize {38) Writer.FileError!usize {
39 const nw: *NullWriter = @alignCast(@ptrCast(context));39 const nw: *NullWriter = @alignCast(@ptrCast(context));
40 var n: usize = 0;40 var n: usize = 0;
41 if (offset == .none) {41 if (offset == .none) {
lib/std/zip.zig+334-491
...@@ -5,11 +5,8 @@...@@ -5,11 +5,8 @@
55
6const builtin = @import("builtin");6const builtin = @import("builtin");
7const std = @import("std");7const std = @import("std");
8const testing = std.testing;8const File = std.fs.File;
99const is_le = builtin.target.cpu.arch.endian() == .little;
10pub const testutil = @import("zip/test.zig");
11const File = testutil.File;
12const FileStore = testutil.FileStore;
1310
14pub const CompressionMethod = enum(u16) {11pub const CompressionMethod = enum(u16) {
15 store = 0,12 store = 0,
...@@ -95,57 +92,71 @@ pub const EndRecord = extern struct {...@@ -95,57 +92,71 @@ pub const EndRecord = extern struct {
95 central_directory_size: u32 align(1),92 central_directory_size: u32 align(1),
96 central_directory_offset: u32 align(1),93 central_directory_offset: u32 align(1),
97 comment_len: u16 align(1),94 comment_len: u16 align(1),
95
98 pub fn need_zip64(self: EndRecord) bool {96 pub fn need_zip64(self: EndRecord) bool {
99 return isMaxInt(self.record_count_disk) or97 return isMaxInt(self.record_count_disk) or
100 isMaxInt(self.record_count_total) or98 isMaxInt(self.record_count_total) or
101 isMaxInt(self.central_directory_size) or99 isMaxInt(self.central_directory_size) or
102 isMaxInt(self.central_directory_offset);100 isMaxInt(self.central_directory_offset);
103 }101 }
104};
105102
106/// Find and return the end record for the given seekable zip stream.103 pub const FindBufferError = error{ ZipNoEndRecord, ZipTruncated };
107/// Note that `seekable_stream` must be an instance of `std.io.SeekableStream` and
108/// its context must also have a `.reader()` method that returns an instance of
109/// `std.io.Reader`.
110pub fn findEndRecord(seekable_stream: anytype, stream_len: u64) !EndRecord {
111 var buf: [@sizeOf(EndRecord) + std.math.maxInt(u16)]u8 = undefined;
112 const record_len_max = @min(stream_len, buf.len);
113 var loaded_len: u32 = 0;
114
115 var comment_len: u16 = 0;
116 while (true) {
117 const record_len: u32 = @as(u32, comment_len) + @sizeOf(EndRecord);
118 if (record_len > record_len_max)
119 return error.ZipNoEndRecord;
120
121 if (record_len > loaded_len) {
122 const new_loaded_len = @min(loaded_len + 300, record_len_max);
123 const read_len = new_loaded_len - loaded_len;
124
125 try seekable_stream.seekTo(stream_len - @as(u64, new_loaded_len));
126 const read_buf: []u8 = buf[buf.len - new_loaded_len ..][0..read_len];
127 const len = try seekable_stream.context.reader().readAll(read_buf);
128 if (len != read_len)
129 return error.ZipTruncated;
130 loaded_len = new_loaded_len;
131 }
132104
133 const record_bytes = buf[buf.len - record_len ..][0..@sizeOf(EndRecord)];105 /// TODO audit this logic
134 if (std.mem.eql(u8, record_bytes[0..4], &end_record_sig) and106 pub fn findBuffer(buffer: []const u8) FindBufferError!EndRecord {
135 std.mem.readInt(u16, record_bytes[20..22], .little) == comment_len)107 const pos = std.mem.lastIndexOf(u8, buffer, &end_record_sig) orelse return error.ZipNoEndRecord;
136 {108 if (pos + @sizeOf(EndRecord) > buffer.len) return error.EndOfStream;
137 const record: *align(1) EndRecord = @ptrCast(record_bytes.ptr);109 const record_ptr: *EndRecord = @ptrCast(buffer[pos..][0..@sizeOf(EndRecord)]);
138 if (builtin.target.cpu.arch.endian() != .little) {110 var record = record_ptr.*;
139 std.mem.byteSwapAllFields(@TypeOf(record.*), record);111 if (!is_le) std.mem.byteSwapAllFields(EndRecord, &record);
112 return record;
113 }
114
115 pub const FindFileError = File.GetEndPosError || File.SeekError || error{
116 ZipNoEndRecord,
117 EndOfStream,
118 };
119
120 pub fn findFile(fr: *File.Reader) FindFileError!EndRecord {
121 const end_pos = try fr.getSize();
122
123 var buf: [@sizeOf(EndRecord) + std.math.maxInt(u16)]u8 = undefined;
124 const record_len_max = @min(end_pos, buf.len);
125 var loaded_len: u32 = 0;
126 var comment_len: u16 = 0;
127 while (true) {
128 const record_len: u32 = @as(u32, comment_len) + @sizeOf(EndRecord);
129 if (record_len > record_len_max)
130 return error.ZipNoEndRecord;
131
132 if (record_len > loaded_len) {
133 const new_loaded_len = @min(loaded_len + 300, record_len_max);
134 const read_len = new_loaded_len - loaded_len;
135
136 try fr.seekTo(end_pos - @as(u64, new_loaded_len));
137 const read_buf: []u8 = buf[buf.len - new_loaded_len ..][0..read_len];
138 var br = fr.interface().unbuffered();
139 br.readSlice(read_buf) catch |err| switch (err) {
140 error.ReadFailed => return fr.err.?,
141 };
142 loaded_len = new_loaded_len;
143 }
144
145 const record_bytes = buf[buf.len - record_len ..][0..@sizeOf(EndRecord)];
146 if (std.mem.eql(u8, record_bytes[0..4], &end_record_sig) and
147 std.mem.readInt(u16, record_bytes[20..22], .little) == comment_len)
148 {
149 const record: *align(1) EndRecord = @ptrCast(record_bytes.ptr);
150 if (!is_le) std.mem.byteSwapAllFields(EndRecord, record);
151 return record.*;
140 }152 }
141 return record.*;
142 }
143153
144 if (comment_len == std.math.maxInt(u16))154 if (comment_len == std.math.maxInt(u16))
145 return error.ZipNoEndRecord;155 return error.ZipNoEndRecord;
146 comment_len += 1;156 comment_len += 1;
157 }
147 }158 }
148}159};
149160
150/// Decompresses the given data from `reader` into `writer`. Stops early if more161/// Decompresses the given data from `reader` into `writer`. Stops early if more
151/// than `uncompressed_size` bytes are processed and verifies that exactly that162/// than `uncompressed_size` bytes are processed and verifies that exactly that
...@@ -248,319 +259,322 @@ fn readZip64FileExtents(comptime T: type, header: T, extents: *FileExtents, data...@@ -248,319 +259,322 @@ fn readZip64FileExtents(comptime T: type, header: T, extents: *FileExtents, data
248 }259 }
249}260}
250261
251pub fn Iterator(comptime SeekableStream: type) type {262pub const Iterator = struct {
252 return struct {263 input: *File.Reader,
253 stream: SeekableStream,
254
255 cd_record_count: u64,
256 cd_zip_offset: u64,
257 cd_size: u64,
258264
259 cd_record_index: u64 = 0,265 cd_record_count: u64,
260 cd_record_offset: u64 = 0,266 cd_zip_offset: u64,
267 cd_size: u64,
261268
262 const Self = @This();269 cd_record_index: u64 = 0,
270 cd_record_offset: u64 = 0,
263271
264 pub fn init(stream: SeekableStream) !Self {272 pub fn init(input: *File.Reader) !Iterator {
265 const stream_len = try stream.getEndPos();273 const end_record = try EndRecord.findFile(input);
266274
267 const end_record = try findEndRecord(stream, stream_len);275 if (!isMaxInt(end_record.record_count_disk) and end_record.record_count_disk > end_record.record_count_total)
276 return error.ZipDiskRecordCountTooLarge;
268277
269 if (!isMaxInt(end_record.record_count_disk) and end_record.record_count_disk > end_record.record_count_total)278 if (end_record.disk_number != 0 or end_record.central_directory_disk_number != 0)
270 return error.ZipDiskRecordCountTooLarge;279 return error.ZipMultiDiskUnsupported;
271280
272 if (end_record.disk_number != 0 or end_record.central_directory_disk_number != 0)281 {
273 return error.ZipMultiDiskUnsupported;282 const counts_valid = !isMaxInt(end_record.record_count_disk) and !isMaxInt(end_record.record_count_total);
274283 if (counts_valid and end_record.record_count_disk != end_record.record_count_total)
275 {
276 const counts_valid = !isMaxInt(end_record.record_count_disk) and !isMaxInt(end_record.record_count_total);
277 if (counts_valid and end_record.record_count_disk != end_record.record_count_total)
278 return error.ZipMultiDiskUnsupported;
279 }
280
281 var result = Self{
282 .stream = stream,
283 .cd_record_count = end_record.record_count_total,
284 .cd_zip_offset = end_record.central_directory_offset,
285 .cd_size = end_record.central_directory_size,
286 };
287 if (!end_record.need_zip64()) return result;
288
289 const locator_end_offset: u64 = @as(u64, end_record.comment_len) + @sizeOf(EndRecord) + @sizeOf(EndLocator64);
290 if (locator_end_offset > stream_len)
291 return error.ZipTruncated;
292 try stream.seekTo(stream_len - locator_end_offset);
293 const locator = try stream.context.reader().readStructEndian(EndLocator64, .little);
294 if (!std.mem.eql(u8, &locator.signature, &end_locator64_sig))
295 return error.ZipBadLocatorSig;
296 if (locator.zip64_disk_count != 0)
297 return error.ZipUnsupportedZip64DiskCount;
298 if (locator.total_disk_count != 1)
299 return error.ZipMultiDiskUnsupported;284 return error.ZipMultiDiskUnsupported;
285 }
300286
301 try stream.seekTo(locator.record_file_offset);287 var result: Iterator = .{
302288 .input = input,
303 const record64 = try stream.context.reader().readStructEndian(EndRecord64, .little);289 .cd_record_count = end_record.record_count_total,
290 .cd_zip_offset = end_record.central_directory_offset,
291 .cd_size = end_record.central_directory_size,
292 };
293 if (!end_record.need_zip64()) return result;
304294
305 if (!std.mem.eql(u8, &record64.signature, &end_record64_sig))295 const locator_end_offset: u64 = @as(u64, end_record.comment_len) + @sizeOf(EndRecord) + @sizeOf(EndLocator64);
306 return error.ZipBadEndRecord64Sig;296 const stream_len = try input.getSize();
307297
308 if (record64.end_record_size < @sizeOf(EndRecord64) - 12)298 if (locator_end_offset > stream_len)
309 return error.ZipEndRecord64SizeTooSmall;299 return error.ZipTruncated;
310 if (record64.end_record_size > @sizeOf(EndRecord64) - 12)300 try input.seekTo(stream_len - locator_end_offset);
311 return error.ZipEndRecord64UnhandledExtraData;301 var br = input.interface().unbuffered();
302 const locator = br.readStructEndian(EndLocator64, .little) catch |err| switch (err) {
303 error.ReadFailed => return input.err.?,
304 };
305 if (!std.mem.eql(u8, &locator.signature, &end_locator64_sig))
306 return error.ZipBadLocatorSig;
307 if (locator.zip64_disk_count != 0)
308 return error.ZipUnsupportedZip64DiskCount;
309 if (locator.total_disk_count != 1)
310 return error.ZipMultiDiskUnsupported;
312311
313 if (record64.version_needed_to_extract > 45)312 try input.seekTo(locator.record_file_offset);
314 return error.ZipUnsupportedVersion;
315313
316 {314 const record64 = br.readStructEndian(EndRecord64, .little) catch |err| switch (err) {
317 const is_multidisk = record64.disk_number != 0 or315 error.ReadFailed => return input.err.?,
318 record64.central_directory_disk_number != 0 or316 };
319 record64.record_count_disk != record64.record_count_total;
320 if (is_multidisk)
321 return error.ZipMultiDiskUnsupported;
322 }
323317
324 if (isMaxInt(end_record.record_count_total)) {318 if (!std.mem.eql(u8, &record64.signature, &end_record64_sig))
325 result.cd_record_count = record64.record_count_total;319 return error.ZipBadEndRecord64Sig;
326 } else if (end_record.record_count_total != record64.record_count_total)
327 return error.Zip64RecordCountTotalMismatch;
328320
329 if (isMaxInt(end_record.central_directory_offset)) {321 if (record64.end_record_size < @sizeOf(EndRecord64) - 12)
330 result.cd_zip_offset = record64.central_directory_offset;322 return error.ZipEndRecord64SizeTooSmall;
331 } else if (end_record.central_directory_offset != record64.central_directory_offset)323 if (record64.end_record_size > @sizeOf(EndRecord64) - 12)
332 return error.Zip64CentralDirectoryOffsetMismatch;324 return error.ZipEndRecord64UnhandledExtraData;
333325
334 if (isMaxInt(end_record.central_directory_size)) {326 if (record64.version_needed_to_extract > 45)
335 result.cd_size = record64.central_directory_size;327 return error.ZipUnsupportedVersion;
336 } else if (end_record.central_directory_size != record64.central_directory_size)
337 return error.Zip64CentralDirectorySizeMismatch;
338328
339 return result;329 {
330 const is_multidisk = record64.disk_number != 0 or
331 record64.central_directory_disk_number != 0 or
332 record64.record_count_disk != record64.record_count_total;
333 if (is_multidisk)
334 return error.ZipMultiDiskUnsupported;
340 }335 }
341336
342 pub fn next(self: *Self) !?Entry {337 if (isMaxInt(end_record.record_count_total)) {
343 if (self.cd_record_index == self.cd_record_count) {338 result.cd_record_count = record64.record_count_total;
344 if (self.cd_record_offset != self.cd_size)339 } else if (end_record.record_count_total != record64.record_count_total)
345 return if (self.cd_size > self.cd_record_offset)340 return error.Zip64RecordCountTotalMismatch;
346 error.ZipCdOversized
347 else
348 error.ZipCdUndersized;
349341
350 return null;342 if (isMaxInt(end_record.central_directory_offset)) {
351 }343 result.cd_zip_offset = record64.central_directory_offset;
344 } else if (end_record.central_directory_offset != record64.central_directory_offset)
345 return error.Zip64CentralDirectoryOffsetMismatch;
352346
353 const header_zip_offset = self.cd_zip_offset + self.cd_record_offset;347 if (isMaxInt(end_record.central_directory_size)) {
354 try self.stream.seekTo(header_zip_offset);348 result.cd_size = record64.central_directory_size;
355 const header = try self.stream.context.reader().readStructEndian(CentralDirectoryFileHeader, .little);349 } else if (end_record.central_directory_size != record64.central_directory_size)
356 if (!std.mem.eql(u8, &header.signature, &central_file_header_sig))350 return error.Zip64CentralDirectorySizeMismatch;
357 return error.ZipBadCdOffset;
358351
359 self.cd_record_index += 1;352 return result;
360 self.cd_record_offset += @sizeOf(CentralDirectoryFileHeader) + header.filename_len + header.extra_len + header.comment_len;353 }
361354
362 // Note: checking the version_needed_to_extract doesn't seem to be helpful, i.e. the zip file355 pub fn next(self: *Iterator) !?Entry {
363 // at https://github.com/ninja-build/ninja/releases/download/v1.12.0/ninja-linux.zip356 if (self.cd_record_index == self.cd_record_count) {
364 // has an undocumented version 788 but extracts just fine.357 if (self.cd_record_offset != self.cd_size)
358 return if (self.cd_size > self.cd_record_offset)
359 error.ZipCdOversized
360 else
361 error.ZipCdUndersized;
365362
366 if (header.flags.encrypted)363 return null;
367 return error.ZipEncryptionUnsupported;364 }
368 // TODO: check/verify more flags
369 if (header.disk_number != 0)
370 return error.ZipMultiDiskUnsupported;
371365
372 var extents: FileExtents = .{366 const header_zip_offset = self.cd_zip_offset + self.cd_record_offset;
373 .uncompressed_size = header.uncompressed_size,367 const input = self.input;
374 .compressed_size = header.compressed_size,368 try input.seekTo(header_zip_offset);
375 .local_file_header_offset = header.local_file_header_offset,369 var br = input.interface().unbuffered();
376 };370 const header = br.readStructEndian(CentralDirectoryFileHeader, .little) catch |err| switch (err) {
371 error.ReadFailed => return input.err.?,
372 };
373 if (!std.mem.eql(u8, &header.signature, &central_file_header_sig))
374 return error.ZipBadCdOffset;
375
376 self.cd_record_index += 1;
377 self.cd_record_offset += @sizeOf(CentralDirectoryFileHeader) + header.filename_len + header.extra_len + header.comment_len;
378
379 // Note: checking the version_needed_to_extract doesn't seem to be helpful, i.e. the zip file
380 // at https://github.com/ninja-build/ninja/releases/download/v1.12.0/ninja-linux.zip
381 // has an undocumented version 788 but extracts just fine.
382
383 if (header.flags.encrypted)
384 return error.ZipEncryptionUnsupported;
385 // TODO: check/verify more flags
386 if (header.disk_number != 0)
387 return error.ZipMultiDiskUnsupported;
388
389 var extents: FileExtents = .{
390 .uncompressed_size = header.uncompressed_size,
391 .compressed_size = header.compressed_size,
392 .local_file_header_offset = header.local_file_header_offset,
393 };
377394
378 if (header.extra_len > 0) {395 if (header.extra_len > 0) {
379 var extra_buf: [std.math.maxInt(u16)]u8 = undefined;396 var extra_buf: [std.math.maxInt(u16)]u8 = undefined;
380 const extra = extra_buf[0..header.extra_len];397 const extra = extra_buf[0..header.extra_len];
381398
382 {399 try input.seekTo(header_zip_offset + @sizeOf(CentralDirectoryFileHeader) + header.filename_len);
383 try self.stream.seekTo(header_zip_offset + @sizeOf(CentralDirectoryFileHeader) + header.filename_len);400 br.readSlice(extra) catch |err| switch (err) {
384 const len = try self.stream.context.reader().readAll(extra);401 error.ReadFailed => return input.err.?,
385 if (len != extra.len)402 };
386 return error.ZipTruncated;
387 }
388403
389 var extra_offset: usize = 0;404 var extra_offset: usize = 0;
390 while (extra_offset + 4 <= extra.len) {405 while (extra_offset + 4 <= extra.len) {
391 const header_id = std.mem.readInt(u16, extra[extra_offset..][0..2], .little);406 const header_id = std.mem.readInt(u16, extra[extra_offset..][0..2], .little);
392 const data_size = std.mem.readInt(u16, extra[extra_offset..][2..4], .little);407 const data_size = std.mem.readInt(u16, extra[extra_offset..][2..4], .little);
393 const end = extra_offset + 4 + data_size;408 const end = extra_offset + 4 + data_size;
394 if (end > extra.len)409 if (end > extra.len)
395 return error.ZipBadExtraFieldSize;410 return error.ZipBadExtraFieldSize;
396 const data = extra[extra_offset + 4 .. end];411 const data = extra[extra_offset + 4 .. end];
397 switch (@as(ExtraHeader, @enumFromInt(header_id))) {412 switch (@as(ExtraHeader, @enumFromInt(header_id))) {
398 .zip64_info => try readZip64FileExtents(CentralDirectoryFileHeader, header, &extents, data),413 .zip64_info => try readZip64FileExtents(CentralDirectoryFileHeader, header, &extents, data),
399 else => {}, // ignore414 else => {}, // ignore
400 }
401 extra_offset = end;
402 }415 }
416 extra_offset = end;
403 }417 }
404
405 return .{
406 .version_needed_to_extract = header.version_needed_to_extract,
407 .flags = header.flags,
408 .compression_method = header.compression_method,
409 .last_modification_time = header.last_modification_time,
410 .last_modification_date = header.last_modification_date,
411 .header_zip_offset = header_zip_offset,
412 .crc32 = header.crc32,
413 .filename_len = header.filename_len,
414 .compressed_size = extents.compressed_size,
415 .uncompressed_size = extents.uncompressed_size,
416 .file_offset = extents.local_file_header_offset,
417 };
418 }418 }
419419
420 pub const Entry = struct {420 return .{
421 version_needed_to_extract: u16,421 .version_needed_to_extract = header.version_needed_to_extract,
422 flags: GeneralPurposeFlags,422 .flags = header.flags,
423 compression_method: CompressionMethod,423 .compression_method = header.compression_method,
424 last_modification_time: u16,424 .last_modification_time = header.last_modification_time,
425 last_modification_date: u16,425 .last_modification_date = header.last_modification_date,
426 header_zip_offset: u64,426 .header_zip_offset = header_zip_offset,
427 crc32: u32,427 .crc32 = header.crc32,
428 filename_len: u32,428 .filename_len = header.filename_len,
429 compressed_size: u64,429 .compressed_size = extents.compressed_size,
430 uncompressed_size: u64,430 .uncompressed_size = extents.uncompressed_size,
431 file_offset: u64,431 .file_offset = extents.local_file_header_offset,
432432 };
433 pub fn extract(433 }
434 self: Entry,
435 stream: SeekableStream,
436 options: ExtractOptions,
437 filename_buf: []u8,
438 dest: std.fs.Dir,
439 ) !u32 {
440 if (filename_buf.len < self.filename_len)
441 return error.ZipInsufficientBuffer;
442 const filename = filename_buf[0..self.filename_len];
443
444 try stream.seekTo(self.header_zip_offset + @sizeOf(CentralDirectoryFileHeader));
445
446 {
447 const len = try stream.context.reader().readAll(filename);
448 if (len != filename.len)
449 return error.ZipBadFileOffset;
450 }
451434
452 const local_data_header_offset: u64 = local_data_header_offset: {435 pub const Entry = struct {
453 const local_header = blk: {436 version_needed_to_extract: u16,
454 try stream.seekTo(self.file_offset);437 flags: GeneralPurposeFlags,
455 break :blk try stream.context.reader().readStructEndian(LocalFileHeader, .little);438 compression_method: CompressionMethod,
456 };439 last_modification_time: u16,
457 if (!std.mem.eql(u8, &local_header.signature, &local_file_header_sig))440 last_modification_date: u16,
458 return error.ZipBadFileOffset;441 header_zip_offset: u64,
459 if (local_header.version_needed_to_extract != self.version_needed_to_extract)442 crc32: u32,
460 return error.ZipMismatchVersionNeeded;443 filename_len: u32,
461 if (local_header.last_modification_time != self.last_modification_time)444 compressed_size: u64,
462 return error.ZipMismatchModTime;445 uncompressed_size: u64,
463 if (local_header.last_modification_date != self.last_modification_date)446 file_offset: u64,
464 return error.ZipMismatchModDate;447
465448 pub fn extract(
466 if (@as(u16, @bitCast(local_header.flags)) != @as(u16, @bitCast(self.flags)))449 self: Entry,
467 return error.ZipMismatchFlags;450 stream: *File.Reader,
468 if (local_header.crc32 != 0 and local_header.crc32 != self.crc32)451 options: ExtractOptions,
469 return error.ZipMismatchCrc32;452 filename_buf: []u8,
470 var extents: FileExtents = .{453 dest: std.fs.Dir,
471 .uncompressed_size = local_header.uncompressed_size,454 ) !u32 {
472 .compressed_size = local_header.compressed_size,455 if (filename_buf.len < self.filename_len)
473 .local_file_header_offset = 0,456 return error.ZipInsufficientBuffer;
474 };457 const filename = filename_buf[0..self.filename_len];
475 if (local_header.extra_len > 0) {458
476 var extra_buf: [std.math.maxInt(u16)]u8 = undefined;459 try stream.seekTo(self.header_zip_offset + @sizeOf(CentralDirectoryFileHeader));
477 const extra = extra_buf[0..local_header.extra_len];
478
479 {
480 try stream.seekTo(self.file_offset + @sizeOf(LocalFileHeader) + local_header.filename_len);
481 const len = try stream.context.reader().readAll(extra);
482 if (len != extra.len)
483 return error.ZipTruncated;
484 }
485460
486 var extra_offset: usize = 0;461 {
487 while (extra_offset + 4 <= local_header.extra_len) {462 const len = try stream.context.reader().readAll(filename);
488 const header_id = std.mem.readInt(u16, extra[extra_offset..][0..2], .little);463 if (len != filename.len)
489 const data_size = std.mem.readInt(u16, extra[extra_offset..][2..4], .little);464 return error.ZipBadFileOffset;
490 const end = extra_offset + 4 + data_size;465 }
491 if (end > local_header.extra_len)466
492 return error.ZipBadExtraFieldSize;467 const local_data_header_offset: u64 = local_data_header_offset: {
493 const data = extra[extra_offset + 4 .. end];468 const local_header = blk: {
494 switch (@as(ExtraHeader, @enumFromInt(header_id))) {469 try stream.seekTo(self.file_offset);
495 .zip64_info => try readZip64FileExtents(LocalFileHeader, local_header, &extents, data),470 break :blk try stream.context.reader().readStructEndian(LocalFileHeader, .little);
496 else => {}, // ignore471 };
497 }472 if (!std.mem.eql(u8, &local_header.signature, &local_file_header_sig))
498 extra_offset = end;473 return error.ZipBadFileOffset;
474 if (local_header.version_needed_to_extract != self.version_needed_to_extract)
475 return error.ZipMismatchVersionNeeded;
476 if (local_header.last_modification_time != self.last_modification_time)
477 return error.ZipMismatchModTime;
478 if (local_header.last_modification_date != self.last_modification_date)
479 return error.ZipMismatchModDate;
480
481 if (@as(u16, @bitCast(local_header.flags)) != @as(u16, @bitCast(self.flags)))
482 return error.ZipMismatchFlags;
483 if (local_header.crc32 != 0 and local_header.crc32 != self.crc32)
484 return error.ZipMismatchCrc32;
485 var extents: FileExtents = .{
486 .uncompressed_size = local_header.uncompressed_size,
487 .compressed_size = local_header.compressed_size,
488 .local_file_header_offset = 0,
489 };
490 if (local_header.extra_len > 0) {
491 var extra_buf: [std.math.maxInt(u16)]u8 = undefined;
492 const extra = extra_buf[0..local_header.extra_len];
493
494 {
495 try stream.seekTo(self.file_offset + @sizeOf(LocalFileHeader) + local_header.filename_len);
496 const len = try stream.context.reader().readAll(extra);
497 if (len != extra.len)
498 return error.ZipTruncated;
499 }
500
501 var extra_offset: usize = 0;
502 while (extra_offset + 4 <= local_header.extra_len) {
503 const header_id = std.mem.readInt(u16, extra[extra_offset..][0..2], .little);
504 const data_size = std.mem.readInt(u16, extra[extra_offset..][2..4], .little);
505 const end = extra_offset + 4 + data_size;
506 if (end > local_header.extra_len)
507 return error.ZipBadExtraFieldSize;
508 const data = extra[extra_offset + 4 .. end];
509 switch (@as(ExtraHeader, @enumFromInt(header_id))) {
510 .zip64_info => try readZip64FileExtents(LocalFileHeader, local_header, &extents, data),
511 else => {}, // ignore
499 }512 }
513 extra_offset = end;
500 }514 }
515 }
501516
502 if (extents.compressed_size != 0 and517 if (extents.compressed_size != 0 and
503 extents.compressed_size != self.compressed_size)518 extents.compressed_size != self.compressed_size)
504 return error.ZipMismatchCompLen;519 return error.ZipMismatchCompLen;
505 if (extents.uncompressed_size != 0 and520 if (extents.uncompressed_size != 0 and
506 extents.uncompressed_size != self.uncompressed_size)521 extents.uncompressed_size != self.uncompressed_size)
507 return error.ZipMismatchUncompLen;522 return error.ZipMismatchUncompLen;
508523
509 if (local_header.filename_len != self.filename_len)524 if (local_header.filename_len != self.filename_len)
510 return error.ZipMismatchFilenameLen;525 return error.ZipMismatchFilenameLen;
511526
512 break :local_data_header_offset @as(u64, local_header.filename_len) +527 break :local_data_header_offset @as(u64, local_header.filename_len) +
513 @as(u64, local_header.extra_len);528 @as(u64, local_header.extra_len);
514 };529 };
515530
516 if (isBadFilename(filename))531 if (isBadFilename(filename))
517 return error.ZipBadFilename;532 return error.ZipBadFilename;
518533
519 if (options.allow_backslashes) {534 if (options.allow_backslashes) {
520 std.mem.replaceScalar(u8, filename, '\\', '/');535 std.mem.replaceScalar(u8, filename, '\\', '/');
521 } else {536 } else {
522 if (std.mem.indexOfScalar(u8, filename, '\\')) |_|537 if (std.mem.indexOfScalar(u8, filename, '\\')) |_|
523 return error.ZipFilenameHasBackslash;538 return error.ZipFilenameHasBackslash;
524 }539 }
525540
526 // All entries that end in '/' are directories541 // All entries that end in '/' are directories
527 if (filename[filename.len - 1] == '/') {542 if (filename[filename.len - 1] == '/') {
528 if (self.uncompressed_size != 0)543 if (self.uncompressed_size != 0)
529 return error.ZipBadDirectorySize;544 return error.ZipBadDirectorySize;
530 try dest.makePath(filename[0 .. filename.len - 1]);545 try dest.makePath(filename[0 .. filename.len - 1]);
531 return std.hash.Crc32.hash(&.{});546 return std.hash.Crc32.hash(&.{});
532 }547 }
533548
534 const out_file = blk: {549 const out_file = blk: {
535 if (std.fs.path.dirname(filename)) |dirname| {550 if (std.fs.path.dirname(filename)) |dirname| {
536 var parent_dir = try dest.makeOpenPath(dirname, .{});551 var parent_dir = try dest.makeOpenPath(dirname, .{});
537 defer parent_dir.close();552 defer parent_dir.close();
538553
539 const basename = std.fs.path.basename(filename);554 const basename = std.fs.path.basename(filename);
540 break :blk try parent_dir.createFile(basename, .{ .exclusive = true });555 break :blk try parent_dir.createFile(basename, .{ .exclusive = true });
541 }556 }
542 break :blk try dest.createFile(filename, .{ .exclusive = true });557 break :blk try dest.createFile(filename, .{ .exclusive = true });
543 };558 };
544 defer out_file.close();559 defer out_file.close();
545 const local_data_file_offset: u64 =560 const local_data_file_offset: u64 =
546 @as(u64, self.file_offset) +561 @as(u64, self.file_offset) +
547 @as(u64, @sizeOf(LocalFileHeader)) +562 @as(u64, @sizeOf(LocalFileHeader)) +
548 local_data_header_offset;563 local_data_header_offset;
549 try stream.seekTo(local_data_file_offset);564 try stream.seekTo(local_data_file_offset);
550 var compressed_remaining: u64 = self.compressed_size;565 var compressed_remaining: u64 = self.compressed_size;
551 const crc = try decompress(566 const crc = try decompress(
552 self.compression_method,567 self.compression_method,
553 self.uncompressed_size,568 self.uncompressed_size,
554 stream.context.reader(),569 stream.context.reader(),
555 out_file.writer(),570 out_file.writer(),
556 &compressed_remaining,571 &compressed_remaining,
557 );572 );
558 if (compressed_remaining != 0) return error.ZipDecompressTruncated;573 if (compressed_remaining != 0) return error.ZipDecompressTruncated;
559 return crc;574 return crc;
560 }575 }
561 };
562 };576 };
563}577};
564578
565// returns true if `filename` starts with `root` followed by a forward slash579// returns true if `filename` starts with `root` followed by a forward slash
566fn filenameInRoot(filename: []const u8, root: []const u8) bool {580fn filenameInRoot(filename: []const u8, root: []const u8) bool {
...@@ -609,17 +623,13 @@ pub const ExtractOptions = struct {...@@ -609,17 +623,13 @@ pub const ExtractOptions = struct {
609 diagnostics: ?*Diagnostics = null,623 diagnostics: ?*Diagnostics = null,
610};624};
611625
612/// Extract the zipped files inside `seekable_stream` to the given `dest` directory.626/// Extract the zipped files to the given `dest` directory.
613/// Note that `seekable_stream` must be an instance of `std.io.SeekableStream` and627pub fn extract(dest: std.fs.Dir, fr: *File.Reader, options: ExtractOptions) !void {
614/// its context must also have a `.reader()` method that returns an instance of628 var iter = try Iterator.init(fr);
615/// `std.io.Reader`.
616pub fn extract(dest: std.fs.Dir, seekable_stream: anytype, options: ExtractOptions) !void {
617 const SeekableStream = @TypeOf(seekable_stream);
618 var iter = try Iterator(SeekableStream).init(seekable_stream);
619629
620 var filename_buf: [std.fs.max_path_bytes]u8 = undefined;630 var filename_buf: [std.fs.max_path_bytes]u8 = undefined;
621 while (try iter.next()) |entry| {631 while (try iter.next()) |entry| {
622 const crc32 = try entry.extract(seekable_stream, options, &filename_buf, dest);632 const crc32 = try entry.extract(fr, options, &filename_buf, dest);
623 if (crc32 != entry.crc32)633 if (crc32 != entry.crc32)
624 return error.ZipCrcMismatch;634 return error.ZipCrcMismatch;
625 if (options.diagnostics) |d| {635 if (options.diagnostics) |d| {
...@@ -628,173 +638,6 @@ pub fn extract(dest: std.fs.Dir, seekable_stream: anytype, options: ExtractOptio...@@ -628,173 +638,6 @@ pub fn extract(dest: std.fs.Dir, seekable_stream: anytype, options: ExtractOptio
628 }638 }
629}639}
630640
631fn testZip(options: ExtractOptions, comptime files: []const File, write_opt: testutil.WriteZipOptions) !void {641test {
632 var store: [files.len]FileStore = undefined;642 _ = @import("zip/test.zig");
633 try testZipWithStore(options, files, write_opt, &store);
634}
635fn testZipWithStore(
636 options: ExtractOptions,
637 test_files: []const File,
638 write_opt: testutil.WriteZipOptions,
639 store: []FileStore,
640) !void {
641 var zip_buf: [4096]u8 = undefined;
642 var fbs = try testutil.makeZipWithStore(&zip_buf, test_files, write_opt, store);
643
644 var tmp = testing.tmpDir(.{ .no_follow = true });
645 defer tmp.cleanup();
646 try extract(tmp.dir, fbs.seekableStream(), options);
647 try testutil.expectFiles(test_files, tmp.dir, .{});
648}
649fn testZipError(expected_error: anyerror, file: File, options: ExtractOptions) !void {
650 var zip_buf: [4096]u8 = undefined;
651 var store: [1]FileStore = undefined;
652 var fbs = try testutil.makeZipWithStore(&zip_buf, &[_]File{file}, .{}, &store);
653 var tmp = testing.tmpDir(.{ .no_follow = true });
654 defer tmp.cleanup();
655 try testing.expectError(expected_error, extract(tmp.dir, fbs.seekableStream(), options));
656}
657
658test "zip one file" {
659 try testZip(.{}, &[_]File{
660 .{ .name = "onefile.txt", .content = "Just a single file\n", .compression = .store },
661 }, .{});
662}
663test "zip multiple files" {
664 try testZip(.{ .allow_backslashes = true }, &[_]File{
665 .{ .name = "foo", .content = "a foo file\n", .compression = .store },
666 .{ .name = "subdir/bar", .content = "bar is this right?\nanother newline\n", .compression = .store },
667 .{ .name = "subdir\\whoa", .content = "you can do backslashes", .compression = .store },
668 .{ .name = "subdir/another/baz", .content = "bazzy mc bazzerson", .compression = .store },
669 }, .{});
670}
671test "zip deflated" {
672 try testZip(.{}, &[_]File{
673 .{ .name = "deflateme", .content = "This is a deflated file.\nIt should be smaller in the Zip file1\n", .compression = .deflate },
674 // TODO: re-enable this if/when we add support for deflate64
675 //.{ .name = "deflateme64", .content = "The 64k version of deflate!\n", .compression = .deflate64 },
676 .{ .name = "raw", .content = "Not all files need to be deflated in the same Zip.\n", .compression = .store },
677 }, .{});
678}
679test "zip verify filenames" {
680 // no empty filenames
681 try testZipError(error.ZipBadFilename, .{ .name = "", .content = "", .compression = .store }, .{});
682 // no absolute paths
683 try testZipError(error.ZipBadFilename, .{ .name = "/", .content = "", .compression = .store }, .{});
684 try testZipError(error.ZipBadFilename, .{ .name = "/foo", .content = "", .compression = .store }, .{});
685 try testZipError(error.ZipBadFilename, .{ .name = "/foo/bar", .content = "", .compression = .store }, .{});
686 // no '..' components
687 try testZipError(error.ZipBadFilename, .{ .name = "..", .content = "", .compression = .store }, .{});
688 try testZipError(error.ZipBadFilename, .{ .name = "foo/..", .content = "", .compression = .store }, .{});
689 try testZipError(error.ZipBadFilename, .{ .name = "foo/bar/..", .content = "", .compression = .store }, .{});
690 try testZipError(error.ZipBadFilename, .{ .name = "foo/bar/../", .content = "", .compression = .store }, .{});
691 // no backslashes
692 try testZipError(error.ZipFilenameHasBackslash, .{ .name = "foo\\bar", .content = "", .compression = .store }, .{});
693}
694
695test "zip64" {
696 const test_files = [_]File{
697 .{ .name = "fram", .content = "fram foo fro fraba", .compression = .store },
698 .{ .name = "subdir/barro", .content = "aljdk;jal;jfd;lajkf", .compression = .store },
699 };
700
701 try testZip(.{}, &test_files, .{
702 .end = .{
703 .zip64 = .{},
704 .record_count_disk = std.math.maxInt(u16), // trigger zip64
705 },
706 });
707 try testZip(.{}, &test_files, .{
708 .end = .{
709 .zip64 = .{},
710 .record_count_total = std.math.maxInt(u16), // trigger zip64
711 },
712 });
713 try testZip(.{}, &test_files, .{
714 .end = .{
715 .zip64 = .{},
716 .record_count_disk = std.math.maxInt(u16), // trigger zip64
717 .record_count_total = std.math.maxInt(u16), // trigger zip64
718 },
719 });
720 try testZip(.{}, &test_files, .{
721 .end = .{
722 .zip64 = .{},
723 .central_directory_size = std.math.maxInt(u32), // trigger zip64
724 },
725 });
726 try testZip(.{}, &test_files, .{
727 .end = .{
728 .zip64 = .{},
729 .central_directory_offset = std.math.maxInt(u32), // trigger zip64
730 },
731 });
732 try testZip(.{}, &test_files, .{
733 .end = .{
734 .zip64 = .{},
735 .central_directory_offset = std.math.maxInt(u32), // trigger zip64
736 },
737 .local_header = .{
738 .zip64 = .{ // trigger local header zip64
739 .data_size = 16,
740 },
741 .compressed_size = std.math.maxInt(u32),
742 .uncompressed_size = std.math.maxInt(u32),
743 .extra_len = 20,
744 },
745 });
746}
747
748test "bad zip files" {
749 var tmp = testing.tmpDir(.{ .no_follow = true });
750 defer tmp.cleanup();
751 var zip_buf: [4096]u8 = undefined;
752
753 const file_a = [_]File{.{ .name = "a", .content = "", .compression = .store }};
754
755 {
756 var fbs = try testutil.makeZip(&zip_buf, &.{}, .{ .end = .{ .sig = [_]u8{ 1, 2, 3, 4 } } });
757 try testing.expectError(error.ZipNoEndRecord, extract(tmp.dir, fbs.seekableStream(), .{}));
758 }
759 {
760 var fbs = try testutil.makeZip(&zip_buf, &.{}, .{ .end = .{ .comment_len = 1 } });
761 try testing.expectError(error.ZipNoEndRecord, extract(tmp.dir, fbs.seekableStream(), .{}));
762 }
763 {
764 var fbs = try testutil.makeZip(&zip_buf, &.{}, .{ .end = .{ .comment = "a", .comment_len = 0 } });
765 try testing.expectError(error.ZipNoEndRecord, extract(tmp.dir, fbs.seekableStream(), .{}));
766 }
767 {
768 var fbs = try testutil.makeZip(&zip_buf, &.{}, .{ .end = .{ .disk_number = 1 } });
769 try testing.expectError(error.ZipMultiDiskUnsupported, extract(tmp.dir, fbs.seekableStream(), .{}));
770 }
771 {
772 var fbs = try testutil.makeZip(&zip_buf, &.{}, .{ .end = .{ .central_directory_disk_number = 1 } });
773 try testing.expectError(error.ZipMultiDiskUnsupported, extract(tmp.dir, fbs.seekableStream(), .{}));
774 }
775 {
776 var fbs = try testutil.makeZip(&zip_buf, &.{}, .{ .end = .{ .record_count_disk = 1 } });
777 try testing.expectError(error.ZipDiskRecordCountTooLarge, extract(tmp.dir, fbs.seekableStream(), .{}));
778 }
779 {
780 var fbs = try testutil.makeZip(&zip_buf, &.{}, .{ .end = .{ .central_directory_size = 1 } });
781 try testing.expectError(error.ZipCdOversized, extract(tmp.dir, fbs.seekableStream(), .{}));
782 }
783 {
784 var fbs = try testutil.makeZip(&zip_buf, &file_a, .{ .end = .{ .central_directory_size = 0 } });
785 try testing.expectError(error.ZipCdUndersized, extract(tmp.dir, fbs.seekableStream(), .{}));
786 }
787 {
788 var fbs = try testutil.makeZip(&zip_buf, &file_a, .{ .end = .{ .central_directory_offset = 0 } });
789 try testing.expectError(error.ZipBadCdOffset, extract(tmp.dir, fbs.seekableStream(), .{}));
790 }
791 {
792 var fbs = try testutil.makeZip(&zip_buf, &file_a, .{
793 .end = .{
794 .zip64 = .{ .locator_sig = [_]u8{ 1, 2, 3, 4 } },
795 .central_directory_size = std.math.maxInt(u32), // trigger 64
796 },
797 });
798 try testing.expectError(error.ZipBadLocatorSig, extract(tmp.dir, fbs.seekableStream(), .{}));
799 }
800}643}
lib/std/zip/test.zig+224-53
...@@ -2,14 +2,15 @@ const std = @import("std");...@@ -2,14 +2,15 @@ const std = @import("std");
2const testing = std.testing;2const testing = std.testing;
3const zip = @import("../zip.zig");3const zip = @import("../zip.zig");
4const maxInt = std.math.maxInt;4const maxInt = std.math.maxInt;
5const assert = std.debug.assert;
56
6pub const File = struct {7const File = struct {
7 name: []const u8,8 name: []const u8,
8 content: []const u8,9 content: []const u8,
9 compression: zip.CompressionMethod,10 compression: zip.CompressionMethod,
10};11};
1112
12pub fn expectFiles(13fn expectFiles(
13 test_files: []const File,14 test_files: []const File,
14 dir: std.fs.Dir,15 dir: std.fs.Dir,
15 opt: struct {16 opt: struct {
...@@ -40,7 +41,7 @@ pub fn expectFiles(...@@ -40,7 +41,7 @@ pub fn expectFiles(
4041
41// Used to store any data from writing a file to the zip archive that's needed42// Used to store any data from writing a file to the zip archive that's needed
42// when writing the corresponding central directory record.43// when writing the corresponding central directory record.
43pub const FileStore = struct {44const FileStore = struct {
44 compression: zip.CompressionMethod,45 compression: zip.CompressionMethod,
45 file_offset: u64,46 file_offset: u64,
46 crc32: u32,47 crc32: u32,
...@@ -48,40 +49,40 @@ pub const FileStore = struct {...@@ -48,40 +49,40 @@ pub const FileStore = struct {
48 uncompressed_size: usize,49 uncompressed_size: usize,
49};50};
5051
51pub fn makeZip(52fn makeZip(buf: []u8, files: []const File, options: WriteZipOptions) !std.io.BufferedReader {
52 buf: []u8,53 const store = try std.testing.allocator.alloc(FileStore, files.len);
53 comptime files: []const File,54 defer std.testing.allocator.free(store);
54 options: WriteZipOptions,55 return makeZipWithStore(buf, files, options, store);
55) !std.io.FixedBufferStream([]u8) {
56 var store: [files.len]FileStore = undefined;
57 return try makeZipWithStore(buf, files, options, &store);
58}56}
5957
60pub fn makeZipWithStore(58fn makeZipWithStore(
61 buf: []u8,59 buf: []u8,
62 files: []const File,60 files: []const File,
63 options: WriteZipOptions,61 options: WriteZipOptions,
64 store: []FileStore,62 store: []FileStore,
65) !std.io.FixedBufferStream([]u8) {63) !std.io.BufferedReader {
66 var fbs = std.io.fixedBufferStream(buf);64 var out: std.io.BufferedWriter = undefined;
67 try writeZip(fbs.writer(), files, store, options);65 out.initFixed(buf);
68 return std.io.fixedBufferStream(buf[0..fbs.pos]);66 try writeZip(&out, files, store, options);
67 var result: std.io.BufferedReader = undefined;
68 result.initFixed(buf[0..out.end]);
69 return result;
69}70}
7071
71pub const WriteZipOptions = struct {72const WriteZipOptions = struct {
72 end: ?EndRecordOptions = null,73 end: ?EndRecordOptions = null,
73 local_header: ?LocalHeaderOptions = null,74 local_header: ?LocalHeaderOptions = null,
74};75};
75pub const LocalHeaderOptions = struct {76const LocalHeaderOptions = struct {
76 zip64: ?LocalHeaderZip64Options = null,77 zip64: ?LocalHeaderZip64Options = null,
77 compressed_size: ?u32 = null,78 compressed_size: ?u32 = null,
78 uncompressed_size: ?u32 = null,79 uncompressed_size: ?u32 = null,
79 extra_len: ?u16 = null,80 extra_len: ?u16 = null,
80};81};
81pub const LocalHeaderZip64Options = struct {82const LocalHeaderZip64Options = struct {
82 data_size: ?u16 = null,83 data_size: ?u16 = null,
83};84};
84pub const EndRecordOptions = struct {85const EndRecordOptions = struct {
85 zip64: ?Zip64Options = null,86 zip64: ?Zip64Options = null,
86 sig: ?[4]u8 = null,87 sig: ?[4]u8 = null,
87 disk_number: ?u16 = null,88 disk_number: ?u16 = null,
...@@ -93,7 +94,7 @@ pub const EndRecordOptions = struct {...@@ -93,7 +94,7 @@ pub const EndRecordOptions = struct {
93 comment_len: ?u16 = null,94 comment_len: ?u16 = null,
94 comment: ?[]const u8 = null,95 comment: ?[]const u8 = null,
95};96};
96pub const Zip64Options = struct {97const Zip64Options = struct {
97 locator_sig: ?[4]u8 = null,98 locator_sig: ?[4]u8 = null,
98 locator_zip64_disk_count: ?u32 = null,99 locator_zip64_disk_count: ?u32 = null,
99 locator_record_file_offset: ?u64 = null,100 locator_record_file_offset: ?u64 = null,
...@@ -102,7 +103,7 @@ pub const Zip64Options = struct {...@@ -102,7 +103,7 @@ pub const Zip64Options = struct {
102 central_directory_size: ?u64 = null,103 central_directory_size: ?u64 = null,
103};104};
104105
105pub fn writeZip(106fn writeZip(
106 writer: *std.io.BufferedWriter,107 writer: *std.io.BufferedWriter,
107 files: []const File,108 files: []const File,
108 store: []FileStore,109 store: []FileStore,
...@@ -128,21 +129,19 @@ pub fn writeZip(...@@ -128,21 +129,19 @@ pub fn writeZip(
128129
129/// Provides methods to format and write the contents of a zip archive130/// Provides methods to format and write the contents of a zip archive
130/// to the underlying Writer.131/// to the underlying Writer.
131pub const Zipper = struct {132const Zipper = struct {
132 writer: *std.io.BufferedWriter,133 writer: *std.io.BufferedWriter,
133 bytes_written: u64,134 init_count: u64,
134 central_count: u64 = 0,135 central_count: u64 = 0,
135 first_central_offset: ?u64 = null,136 first_central_offset: ?u64 = null,
136 last_central_limit: ?u64 = null,137 last_central_limit: ?u64 = null,
137138
138 const Self = @This();139 fn init(writer: *std.io.BufferedWriter) Zipper {
139140 return .{ .writer = writer, .init_count = writer.count };
140 pub fn init(writer: *std.io.BufferedWriter) Zipper {
141 return .{ .writer = writer, .bytes_written = 0 };
142 }141 }
143142
144 pub fn writeFile(143 fn writeFile(
145 self: *Self,144 self: *Zipper,
146 opt: struct {145 opt: struct {
147 name: []const u8,146 name: []const u8,
148 content: []const u8,147 content: []const u8,
...@@ -152,7 +151,7 @@ pub const Zipper = struct {...@@ -152,7 +151,7 @@ pub const Zipper = struct {
152 ) !FileStore {151 ) !FileStore {
153 const writer = self.writer;152 const writer = self.writer;
154153
155 const file_offset: u64 = @intCast(self.bytes_written);154 const file_offset: u64 = writer.count - self.init_count;
156 const crc32 = std.hash.Crc32.hash(opt.content);155 const crc32 = std.hash.Crc32.hash(opt.content);
157156
158 const header_options = opt.write_options.local_header;157 const header_options = opt.write_options.local_header;
...@@ -178,32 +177,33 @@ pub const Zipper = struct {...@@ -178,32 +177,33 @@ pub const Zipper = struct {
178 .filename_len = @intCast(opt.name.len),177 .filename_len = @intCast(opt.name.len),
179 .extra_len = extra_len,178 .extra_len = extra_len,
180 };179 };
181 self.bytes_written += try writer.writeStructEndian(hdr, .little);180 try writer.writeStructEndian(hdr, .little);
182 }181 }
183 self.bytes_written += try writer.writeAll(opt.name);182 try writer.writeAll(opt.name);
184183
185 if (header_options) |hdr| {184 if (header_options) |hdr| {
186 if (hdr.zip64) |options| {185 if (hdr.zip64) |options| {
187 self.bytes_written += try writer.writeInt(u16, 0x0001, .little);186 try writer.writeInt(u16, 0x0001, .little);
188 const data_size = if (options.data_size) |size| size else 8;187 const data_size = if (options.data_size) |size| size else 8;
189 self.bytes_written += try writer.writeInt(u16, data_size, .little);188 try writer.writeInt(u16, data_size, .little);
190 self.bytes_written += try writer.writeInt(u64, 0, .little);189 try writer.writeInt(u64, 0, .little);
191 self.bytes_written += try writer.writeInt(u64, @intCast(opt.content.len), .little);190 try writer.writeInt(u64, @intCast(opt.content.len), .little);
192 }191 }
193 }192 }
194193
195 var compressed_size: u32 = undefined;194 var compressed_size: u32 = undefined;
196 switch (opt.compression) {195 switch (opt.compression) {
197 .store => {196 .store => {
198 self.bytes_written += try writer.writeAll(opt.content);197 try writer.writeAll(opt.content);
199 compressed_size = @intCast(opt.content.len);198 compressed_size = @intCast(opt.content.len);
200 },199 },
201 .deflate => {200 .deflate => {
202 const offset = self.bytes_written;201 const offset = writer.count;
203 var fbs = std.io.fixedBufferStream(opt.content);202 var br: std.io.BufferedReader = undefined;
204 self.bytes_written += try std.compress.flate.deflate.compress(.raw, fbs.reader(), writer, .{});203 br.initFixed(@constCast(opt.content));
205 std.debug.assert(fbs.pos == opt.content.len);204 try std.compress.flate.deflate.compress(.raw, &br, writer, .{});
206 compressed_size = @intCast(self.bytes_written - offset);205 assert(br.seek == opt.content.len);
206 compressed_size = @intCast(writer.count - offset);
207 },207 },
208 else => unreachable,208 else => unreachable,
209 }209 }
...@@ -216,8 +216,8 @@ pub const Zipper = struct {...@@ -216,8 +216,8 @@ pub const Zipper = struct {
216 };216 };
217 }217 }
218218
219 pub fn writeCentralRecord(219 fn writeCentralRecord(
220 self: *Self,220 self: *Zipper,
221 store: FileStore,221 store: FileStore,
222 opt: struct {222 opt: struct {
223 name: []const u8,223 name: []const u8,
...@@ -225,7 +225,7 @@ pub const Zipper = struct {...@@ -225,7 +225,7 @@ pub const Zipper = struct {
225 },225 },
226 ) !void {226 ) !void {
227 if (self.first_central_offset == null) {227 if (self.first_central_offset == null) {
228 self.first_central_offset = self.bytes_written;228 self.first_central_offset = self.writer.count - self.init_count;
229 }229 }
230 self.central_count += 1;230 self.central_count += 1;
231231
...@@ -248,12 +248,12 @@ pub const Zipper = struct {...@@ -248,12 +248,12 @@ pub const Zipper = struct {
248 .external_file_attributes = 0,248 .external_file_attributes = 0,
249 .local_file_header_offset = @intCast(store.file_offset),249 .local_file_header_offset = @intCast(store.file_offset),
250 };250 };
251 self.bytes_written += try self.writer.writeStructEndian(hdr, .little);251 try self.writer.writeStructEndian(hdr, .little);
252 self.bytes_written += try self.writer.writeAll(opt.name);252 try self.writer.writeAll(opt.name);
253 self.last_central_limit = self.bytes_written;253 self.last_central_limit = self.writer.count - self.init_count;
254 }254 }
255255
256 pub fn writeEndRecord(self: *Self, opt: EndRecordOptions) !void {256 fn writeEndRecord(self: *Zipper, opt: EndRecordOptions) !void {
257 const cd_offset = self.first_central_offset orelse 0;257 const cd_offset = self.first_central_offset orelse 0;
258 const cd_end = self.last_central_limit orelse 0;258 const cd_end = self.last_central_limit orelse 0;
259259
...@@ -271,14 +271,14 @@ pub const Zipper = struct {...@@ -271,14 +271,14 @@ pub const Zipper = struct {
271 .central_directory_size = @intCast(cd_end - cd_offset),271 .central_directory_size = @intCast(cd_end - cd_offset),
272 .central_directory_offset = @intCast(cd_offset),272 .central_directory_offset = @intCast(cd_offset),
273 };273 };
274 self.bytes_written += try self.writer.writeStructEndian(fixed, .little);274 try self.writer.writeStructEndian(fixed, .little);
275 const locator: zip.EndLocator64 = .{275 const locator: zip.EndLocator64 = .{
276 .signature = if (zip64.locator_sig) |s| s else zip.end_locator64_sig,276 .signature = if (zip64.locator_sig) |s| s else zip.end_locator64_sig,
277 .zip64_disk_count = if (zip64.locator_zip64_disk_count) |c| c else 0,277 .zip64_disk_count = if (zip64.locator_zip64_disk_count) |c| c else 0,
278 .record_file_offset = if (zip64.locator_record_file_offset) |o| o else @intCast(end64_off),278 .record_file_offset = if (zip64.locator_record_file_offset) |o| o else @intCast(end64_off),
279 .total_disk_count = if (zip64.locator_total_disk_count) |c| c else 1,279 .total_disk_count = if (zip64.locator_total_disk_count) |c| c else 1,
280 };280 };
281 self.bytes_written += try self.writer.writeStructEndian(locator, .little);281 try self.writer.writeStructEndian(locator, .little);
282 }282 }
283 const hdr: zip.EndRecord = .{283 const hdr: zip.EndRecord = .{
284 .signature = if (opt.sig) |s| s else zip.end_record_sig,284 .signature = if (opt.sig) |s| s else zip.end_record_sig,
...@@ -290,8 +290,179 @@ pub const Zipper = struct {...@@ -290,8 +290,179 @@ pub const Zipper = struct {
290 .central_directory_offset = if (opt.central_directory_offset) |o| o else @intCast(cd_offset),290 .central_directory_offset = if (opt.central_directory_offset) |o| o else @intCast(cd_offset),
291 .comment_len = if (opt.comment_len) |l| l else (if (opt.comment) |c| @as(u16, @intCast(c.len)) else 0),291 .comment_len = if (opt.comment_len) |l| l else (if (opt.comment) |c| @as(u16, @intCast(c.len)) else 0),
292 };292 };
293 self.bytes_written += try self.writer.writeStructEndian(hdr, .little);293 try self.writer.writeStructEndian(hdr, .little);
294 if (opt.comment) |c|294 if (opt.comment) |c|
295 self.bytes_written += try self.writer.writeAll(c);295 try self.writer.writeAll(c);
296 }296 }
297};297};
298
299fn testZip(options: zip.ExtractOptions, comptime files: []const File, write_opt: WriteZipOptions) !void {
300 var store: [files.len]FileStore = undefined;
301 try testZipWithStore(options, files, write_opt, &store);
302}
303fn testZipWithStore(
304 options: zip.ExtractOptions,
305 test_files: []const File,
306 write_opt: WriteZipOptions,
307 store: []FileStore,
308) !void {
309 var zip_buf: [4096]u8 = undefined;
310 var fbs = try makeZipWithStore(&zip_buf, test_files, write_opt, store);
311
312 var tmp = testing.tmpDir(.{ .no_follow = true });
313 defer tmp.cleanup();
314 try zip.extract(tmp.dir, fbs.seekableStream(), options);
315 try expectFiles(test_files, tmp.dir, .{});
316}
317fn testZipError(expected_error: anyerror, file: File, options: zip.ExtractOptions) !void {
318 var zip_buf: [4096]u8 = undefined;
319 var store: [1]FileStore = undefined;
320 var fbs = try makeZipWithStore(&zip_buf, &[_]File{file}, .{}, &store);
321 var tmp = testing.tmpDir(.{ .no_follow = true });
322 defer tmp.cleanup();
323 try testing.expectError(expected_error, zip.extract(tmp.dir, fbs.seekableStream(), options));
324}
325
326test "zip one file" {
327 try testZip(.{}, &[_]File{
328 .{ .name = "onefile.txt", .content = "Just a single file\n", .compression = .store },
329 }, .{});
330}
331test "zip multiple files" {
332 try testZip(.{ .allow_backslashes = true }, &[_]File{
333 .{ .name = "foo", .content = "a foo file\n", .compression = .store },
334 .{ .name = "subdir/bar", .content = "bar is this right?\nanother newline\n", .compression = .store },
335 .{ .name = "subdir\\whoa", .content = "you can do backslashes", .compression = .store },
336 .{ .name = "subdir/another/baz", .content = "bazzy mc bazzerson", .compression = .store },
337 }, .{});
338}
339test "zip deflated" {
340 try testZip(.{}, &[_]File{
341 .{ .name = "deflateme", .content = "This is a deflated file.\nIt should be smaller in the Zip file1\n", .compression = .deflate },
342 // TODO: re-enable this if/when we add support for deflate64
343 //.{ .name = "deflateme64", .content = "The 64k version of deflate!\n", .compression = .deflate64 },
344 .{ .name = "raw", .content = "Not all files need to be deflated in the same Zip.\n", .compression = .store },
345 }, .{});
346}
347test "zip verify filenames" {
348 // no empty filenames
349 try testZipError(error.ZipBadFilename, .{ .name = "", .content = "", .compression = .store }, .{});
350 // no absolute paths
351 try testZipError(error.ZipBadFilename, .{ .name = "/", .content = "", .compression = .store }, .{});
352 try testZipError(error.ZipBadFilename, .{ .name = "/foo", .content = "", .compression = .store }, .{});
353 try testZipError(error.ZipBadFilename, .{ .name = "/foo/bar", .content = "", .compression = .store }, .{});
354 // no '..' components
355 try testZipError(error.ZipBadFilename, .{ .name = "..", .content = "", .compression = .store }, .{});
356 try testZipError(error.ZipBadFilename, .{ .name = "foo/..", .content = "", .compression = .store }, .{});
357 try testZipError(error.ZipBadFilename, .{ .name = "foo/bar/..", .content = "", .compression = .store }, .{});
358 try testZipError(error.ZipBadFilename, .{ .name = "foo/bar/../", .content = "", .compression = .store }, .{});
359 // no backslashes
360 try testZipError(error.ZipFilenameHasBackslash, .{ .name = "foo\\bar", .content = "", .compression = .store }, .{});
361}
362
363test "zip64" {
364 const test_files = [_]File{
365 .{ .name = "fram", .content = "fram foo fro fraba", .compression = .store },
366 .{ .name = "subdir/barro", .content = "aljdk;jal;jfd;lajkf", .compression = .store },
367 };
368
369 try testZip(.{}, &test_files, .{
370 .end = .{
371 .zip64 = .{},
372 .record_count_disk = std.math.maxInt(u16), // trigger zip64
373 },
374 });
375 try testZip(.{}, &test_files, .{
376 .end = .{
377 .zip64 = .{},
378 .record_count_total = std.math.maxInt(u16), // trigger zip64
379 },
380 });
381 try testZip(.{}, &test_files, .{
382 .end = .{
383 .zip64 = .{},
384 .record_count_disk = std.math.maxInt(u16), // trigger zip64
385 .record_count_total = std.math.maxInt(u16), // trigger zip64
386 },
387 });
388 try testZip(.{}, &test_files, .{
389 .end = .{
390 .zip64 = .{},
391 .central_directory_size = std.math.maxInt(u32), // trigger zip64
392 },
393 });
394 try testZip(.{}, &test_files, .{
395 .end = .{
396 .zip64 = .{},
397 .central_directory_offset = std.math.maxInt(u32), // trigger zip64
398 },
399 });
400 try testZip(.{}, &test_files, .{
401 .end = .{
402 .zip64 = .{},
403 .central_directory_offset = std.math.maxInt(u32), // trigger zip64
404 },
405 .local_header = .{
406 .zip64 = .{ // trigger local header zip64
407 .data_size = 16,
408 },
409 .compressed_size = std.math.maxInt(u32),
410 .uncompressed_size = std.math.maxInt(u32),
411 .extra_len = 20,
412 },
413 });
414}
415
416test "bad zip files" {
417 var tmp = testing.tmpDir(.{ .no_follow = true });
418 defer tmp.cleanup();
419 var zip_buf: [4096]u8 = undefined;
420
421 const file_a = [_]File{.{ .name = "a", .content = "", .compression = .store }};
422
423 {
424 var fbs = try makeZip(&zip_buf, &.{}, .{ .end = .{ .sig = [_]u8{ 1, 2, 3, 4 } } });
425 try testing.expectError(error.ZipNoEndRecord, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
426 }
427 {
428 var fbs = try makeZip(&zip_buf, &.{}, .{ .end = .{ .comment_len = 1 } });
429 try testing.expectError(error.ZipNoEndRecord, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
430 }
431 {
432 var fbs = try makeZip(&zip_buf, &.{}, .{ .end = .{ .comment = "a", .comment_len = 0 } });
433 try testing.expectError(error.ZipNoEndRecord, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
434 }
435 {
436 var fbs = try makeZip(&zip_buf, &.{}, .{ .end = .{ .disk_number = 1 } });
437 try testing.expectError(error.ZipMultiDiskUnsupported, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
438 }
439 {
440 var fbs = try makeZip(&zip_buf, &.{}, .{ .end = .{ .central_directory_disk_number = 1 } });
441 try testing.expectError(error.ZipMultiDiskUnsupported, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
442 }
443 {
444 var fbs = try makeZip(&zip_buf, &.{}, .{ .end = .{ .record_count_disk = 1 } });
445 try testing.expectError(error.ZipDiskRecordCountTooLarge, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
446 }
447 {
448 var fbs = try makeZip(&zip_buf, &.{}, .{ .end = .{ .central_directory_size = 1 } });
449 try testing.expectError(error.ZipCdOversized, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
450 }
451 {
452 var fbs = try makeZip(&zip_buf, &file_a, .{ .end = .{ .central_directory_size = 0 } });
453 try testing.expectError(error.ZipCdUndersized, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
454 }
455 {
456 var fbs = try makeZip(&zip_buf, &file_a, .{ .end = .{ .central_directory_offset = 0 } });
457 try testing.expectError(error.ZipBadCdOffset, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
458 }
459 {
460 var fbs = try makeZip(&zip_buf, &file_a, .{
461 .end = .{
462 .zip64 = .{ .locator_sig = [_]u8{ 1, 2, 3, 4 } },
463 .central_directory_size = std.math.maxInt(u32), // trigger 64
464 },
465 });
466 try testing.expectError(error.ZipBadLocatorSig, zip.extract(tmp.dir, fbs.seekableStream(), .{}));
467 }
468}
lib/std/zon/parse.zig+11-10
...@@ -440,11 +440,9 @@ const Parser = struct {...@@ -440,11 +440,9 @@ const Parser = struct {
440 };440 };
441 }441 }
442442
443 fn parseExprInner(443 const InnerError = error{ ParseZon, OutOfMemory, WrongType };
444 self: *@This(),444
445 T: type,445 fn parseExprInner(self: *@This(), T: type, node: Zoir.Node.Index) InnerError!T {
446 node: Zoir.Node.Index,
447 ) error{ ParseZon, OutOfMemory, WrongType }!T {
448 if (T == Zoir.Node.Index) {446 if (T == Zoir.Node.Index) {
449 return node;447 return node;
450 }448 }
...@@ -624,7 +622,7 @@ const Parser = struct {...@@ -624,7 +622,7 @@ const Parser = struct {
624 }622 }
625 }623 }
626624
627 fn parseSlicePointer(self: *@This(), T: type, node: Zoir.Node.Index) !T {625 fn parseSlicePointer(self: *@This(), T: type, node: Zoir.Node.Index) InnerError!T {
628 switch (node.get(self.zoir)) {626 switch (node.get(self.zoir)) {
629 .string_literal => return self.parseString(T, node),627 .string_literal => return self.parseString(T, node),
630 .array_literal => |nodes| return self.parseSlice(T, nodes),628 .array_literal => |nodes| return self.parseSlice(T, nodes),
...@@ -633,7 +631,7 @@ const Parser = struct {...@@ -633,7 +631,7 @@ const Parser = struct {
633 }631 }
634 }632 }
635633
636 fn parseString(self: *@This(), T: type, node: Zoir.Node.Index) !T {634 fn parseString(self: *@This(), T: type, node: Zoir.Node.Index) InnerError!T {
637 const ast_node = node.getAstNode(self.zoir);635 const ast_node = node.getAstNode(self.zoir);
638 const pointer = @typeInfo(T).pointer;636 const pointer = @typeInfo(T).pointer;
639 var size_hint = ZonGen.strLitSizeHint(self.ast, ast_node);637 var size_hint = ZonGen.strLitSizeHint(self.ast, ast_node);
...@@ -643,7 +641,10 @@ const Parser = struct {...@@ -643,7 +641,10 @@ const Parser = struct {
643 var aw: std.io.AllocatingWriter = undefined;641 var aw: std.io.AllocatingWriter = undefined;
644 try aw.initCapacity(gpa, size_hint);642 try aw.initCapacity(gpa, size_hint);
645 defer aw.deinit();643 defer aw.deinit();
646 switch (try ZonGen.parseStrLit(self.ast, ast_node, &aw.buffered_writer)) {644 const parsed = ZonGen.parseStrLit(self.ast, ast_node, &aw.buffered_writer) catch |err| switch (err) {
645 error.WriteFailed => return error.OutOfMemory,
646 };
647 switch (parsed) {
647 .success => {},648 .success => {},
648 .failure => |err| {649 .failure => |err| {
649 const token = self.ast.nodeMainToken(ast_node);650 const token = self.ast.nodeMainToken(ast_node);
...@@ -662,9 +663,9 @@ const Parser = struct {...@@ -662,9 +663,9 @@ const Parser = struct {
662 }663 }
663664
664 if (pointer.sentinel() != null) {665 if (pointer.sentinel() != null) {
665 return aw.toOwnedSliceSentinel(gpa, 0);666 return aw.toOwnedSliceSentinel(0);
666 } else {667 } else {
667 return aw.toOwnedSlice(gpa);668 return aw.toOwnedSlice();
668 }669 }
669 }670 }
670671
lib/std/zon/stringify.zig+44-31
...@@ -22,6 +22,7 @@...@@ -22,6 +22,7 @@
2222
23const std = @import("std");23const std = @import("std");
24const assert = std.debug.assert;24const assert = std.debug.assert;
25const BufferedWriter = std.io.BufferedWriter;
2526
26/// Options for `serialize`.27/// Options for `serialize`.
27pub const SerializeOptions = struct {28pub const SerializeOptions = struct {
...@@ -40,7 +41,7 @@ pub const SerializeOptions = struct {...@@ -40,7 +41,7 @@ pub const SerializeOptions = struct {
40/// Serialize the given value as ZON.41/// Serialize the given value as ZON.
41///42///
42/// It is asserted at comptime that `@TypeOf(val)` is not a recursive type.43/// It is asserted at comptime that `@TypeOf(val)` is not a recursive type.
43pub fn serialize(val: anytype, options: SerializeOptions, writer: *std.io.BufferedWriter) std.io.Writer.Error!void {44pub fn serialize(val: anytype, options: SerializeOptions, writer: *BufferedWriter) std.io.Writer.Error!void {
44 var s: Serializer = .{45 var s: Serializer = .{
45 .writer = writer,46 .writer = writer,
46 .options = .{ .whitespace = options.whitespace },47 .options = .{ .whitespace = options.whitespace },
...@@ -59,9 +60,9 @@ pub fn serialize(val: anytype, options: SerializeOptions, writer: *std.io.Buffer...@@ -59,9 +60,9 @@ pub fn serialize(val: anytype, options: SerializeOptions, writer: *std.io.Buffer
59pub fn serializeMaxDepth(60pub fn serializeMaxDepth(
60 val: anytype,61 val: anytype,
61 options: SerializeOptions,62 options: SerializeOptions,
62 writer: *std.io.BufferedWriter,63 writer: *BufferedWriter,
63 depth: usize,64 depth: usize,
64) std.io.Writer.Error!void {65) Serializer.DepthError!void {
65 var s: Serializer = .{66 var s: Serializer = .{
66 .writer = writer,67 .writer = writer,
67 .options = .{ .whitespace = options.whitespace },68 .options = .{ .whitespace = options.whitespace },
...@@ -79,8 +80,8 @@ pub fn serializeMaxDepth(...@@ -79,8 +80,8 @@ pub fn serializeMaxDepth(
79pub fn serializeArbitraryDepth(80pub fn serializeArbitraryDepth(
80 val: anytype,81 val: anytype,
81 options: SerializeOptions,82 options: SerializeOptions,
82 writer: *std.io.BufferedWriter,83 writer: *BufferedWriter,
83) std.io.Writer.Error!void {84) Serializer.Error!void {
84 var s: Serializer = .{85 var s: Serializer = .{
85 .writer = writer,86 .writer = writer,
86 .options = .{ .whitespace = options.whitespace },87 .options = .{ .whitespace = options.whitespace },
...@@ -436,9 +437,10 @@ pub const SerializeContainerOptions = struct {...@@ -436,9 +437,10 @@ pub const SerializeContainerOptions = struct {
436pub const Serializer = struct {437pub const Serializer = struct {
437 options: Options = .{},438 options: Options = .{},
438 indent_level: u8 = 0,439 indent_level: u8 = 0,
439 writer: *std.io.BufferedWriter,440 writer: *BufferedWriter,
440441
441 pub const Error = std.io.Writer.Error;442 pub const Error = std.io.Writer.Error;
443 pub const DepthError = Error || error{ExceededMaxDepth};
442444
443 pub const Options = struct {445 pub const Options = struct {
444 /// If false, only syntactically necessary whitespace is emitted.446 /// If false, only syntactically necessary whitespace is emitted.
...@@ -453,7 +455,7 @@ pub const Serializer = struct {...@@ -453,7 +455,7 @@ pub const Serializer = struct {
453455
454 /// Serialize a value, similar to `serializeMaxDepth`.456 /// Serialize a value, similar to `serializeMaxDepth`.
455 /// Can return `error.ExceededMaxDepth`.457 /// Can return `error.ExceededMaxDepth`.
456 pub fn valueMaxDepth(self: *Serializer, val: anytype, options: ValueOptions, depth: usize) Error!void {458 pub fn valueMaxDepth(self: *Serializer, val: anytype, options: ValueOptions, depth: usize) DepthError!void {
457 try checkValueDepth(val, depth);459 try checkValueDepth(val, depth);
458 return self.valueArbitraryDepth(val, options);460 return self.valueArbitraryDepth(val, options);
459 }461 }
...@@ -618,13 +620,12 @@ pub const Serializer = struct {...@@ -618,13 +620,12 @@ pub const Serializer = struct {
618 try self.writer.print(".{fp_}", .{std.zig.fmtId(name)});620 try self.writer.print(".{fp_}", .{std.zig.fmtId(name)});
619 }621 }
620622
623 pub const CodePointError = Error || error{InvalidCodepoint};
624
621 /// Serialize `val` as a Unicode codepoint.625 /// Serialize `val` as a Unicode codepoint.
622 ///626 ///
623 /// Returns `error.InvalidCodepoint` if `val` is not a valid Unicode codepoint.627 /// Returns `error.InvalidCodepoint` if `val` is not a valid Unicode codepoint.
624 pub fn codePoint(628 pub fn codePoint(self: *Serializer, val: u21) CodePointError!void {
625 self: *Serializer,
626 val: u21,
627 ) Error!void {
628 var buf: [8]u8 = undefined;629 var buf: [8]u8 = undefined;
629 const len = std.unicode.utf8Encode(val, &buf) catch return error.InvalidCodepoint;630 const len = std.unicode.utf8Encode(val, &buf) catch return error.InvalidCodepoint;
630 const str = buf[0..len];631 const str = buf[0..len];
...@@ -647,7 +648,7 @@ pub const Serializer = struct {...@@ -647,7 +648,7 @@ pub const Serializer = struct {
647 val: anytype,648 val: anytype,
648 options: ValueOptions,649 options: ValueOptions,
649 depth: usize,650 depth: usize,
650 ) Error!void {651 ) DepthError!void {
651 try checkValueDepth(val, depth);652 try checkValueDepth(val, depth);
652 try self.tupleArbitraryDepth(val, options);653 try self.tupleArbitraryDepth(val, options);
653 }654 }
...@@ -697,6 +698,8 @@ pub const Serializer = struct {...@@ -697,6 +698,8 @@ pub const Serializer = struct {
697 top_level: bool = false,698 top_level: bool = false,
698 };699 };
699700
701 pub const MultilineStringError = Error || error{InnerCarriageReturn};
702
700 /// Like `value`, but always serializes to a multiline string literal.703 /// Like `value`, but always serializes to a multiline string literal.
701 ///704 ///
702 /// Returns `error.InnerCarriageReturn` if `val` contains a CR not followed by a newline,705 /// Returns `error.InnerCarriageReturn` if `val` contains a CR not followed by a newline,
...@@ -705,7 +708,7 @@ pub const Serializer = struct {...@@ -705,7 +708,7 @@ pub const Serializer = struct {
705 self: *Serializer,708 self: *Serializer,
706 val: []const u8,709 val: []const u8,
707 options: MultilineStringOptions,710 options: MultilineStringOptions,
708 ) Error!void {711 ) MultilineStringError!void {
709 // Make sure the string does not contain any carriage returns not followed by a newline712 // Make sure the string does not contain any carriage returns not followed by a newline
710 var i: usize = 0;713 var i: usize = 0;
711 while (i < val.len) : (i += 1) {714 while (i < val.len) : (i += 1) {
...@@ -818,7 +821,7 @@ pub const Serializer = struct {...@@ -818,7 +821,7 @@ pub const Serializer = struct {
818 val: anytype,821 val: anytype,
819 options: ValueOptions,822 options: ValueOptions,
820 depth: usize,823 depth: usize,
821 ) Error!void {824 ) DepthError!void {
822 try self.container.fieldMaxDepth(null, val, options, depth);825 try self.container.fieldMaxDepth(null, val, options, depth);
823 }826 }
824827
...@@ -893,7 +896,7 @@ pub const Serializer = struct {...@@ -893,7 +896,7 @@ pub const Serializer = struct {
893 val: anytype,896 val: anytype,
894 options: ValueOptions,897 options: ValueOptions,
895 depth: usize,898 depth: usize,
896 ) Error!void {899 ) DepthError!void {
897 try self.container.fieldMaxDepth(name, val, options, depth);900 try self.container.fieldMaxDepth(name, val, options, depth);
898 }901 }
899902
...@@ -1012,7 +1015,7 @@ pub const Serializer = struct {...@@ -1012,7 +1015,7 @@ pub const Serializer = struct {
1012 val: anytype,1015 val: anytype,
1013 options: ValueOptions,1016 options: ValueOptions,
1014 depth: usize,1017 depth: usize,
1015 ) Error!void {1018 ) DepthError!void {
1016 try checkValueDepth(val, depth);1019 try checkValueDepth(val, depth);
1017 try self.fieldArbitraryDepth(name, val, options);1020 try self.fieldArbitraryDepth(name, val, options);
1018 }1021 }
...@@ -1037,13 +1040,13 @@ pub const Serializer = struct {...@@ -1037,13 +1040,13 @@ pub const Serializer = struct {
1037};1040};
10381041
1039test Serializer {1042test Serializer {
1040 var s: Serializer = .{1043 var null_writer: std.io.Writer.Null = undefined;
1041 .writer = std.io.null_writer,1044 var bw = null_writer.writer().unbuffered();
1042 };1045 var s: Serializer = .{ .writer = &bw };
1043 var vec2 = try s.beginStruct(.{});1046 var vec2 = try s.beginStruct(.{});
1044 try vec2.field("x", 1.5, .{});1047 try vec2.field("x", 1.5, .{});
1045 try vec2.fieldPrefix();1048 try vec2.fieldPrefix("prefix");
1046 try s.value(2.5);1049 try s.value(2.5, .{});
1047 try vec2.end();1050 try vec2.end();
1048}1051}
10491052
...@@ -1053,7 +1056,8 @@ fn expectSerializeEqual(...@@ -1053,7 +1056,8 @@ fn expectSerializeEqual(
1053 options: SerializeOptions,1056 options: SerializeOptions,
1054) !void {1057) !void {
1055 var aw: std.io.AllocatingWriter = undefined;1058 var aw: std.io.AllocatingWriter = undefined;
1056 const bw = aw.init(std.testing.allocator);1059 aw.init(std.testing.allocator);
1060 const bw = &aw.buffered_writer;
1057 defer aw.deinit();1061 defer aw.deinit();
10581062
1059 try serialize(value, options, bw);1063 try serialize(value, options, bw);
...@@ -1155,7 +1159,8 @@ test "std.zon stringify whitespace, high level API" {...@@ -1155,7 +1159,8 @@ test "std.zon stringify whitespace, high level API" {
11551159
1156test "std.zon stringify whitespace, low level API" {1160test "std.zon stringify whitespace, low level API" {
1157 var aw: std.io.AllocatingWriter = undefined;1161 var aw: std.io.AllocatingWriter = undefined;
1158 var s: Serializer = .{ .writer = aw.init(std.testing.allocator) };1162 aw.init(std.testing.allocator);
1163 var s: Serializer = .{ .writer = &aw.buffered_writer };
1159 defer aw.deinit();1164 defer aw.deinit();
11601165
1161 for ([2]bool{ true, false }) |whitespace| {1166 for ([2]bool{ true, false }) |whitespace| {
...@@ -1512,7 +1517,8 @@ test "std.zon stringify whitespace, low level API" {...@@ -1512,7 +1517,8 @@ test "std.zon stringify whitespace, low level API" {
15121517
1513test "std.zon stringify utf8 codepoints" {1518test "std.zon stringify utf8 codepoints" {
1514 var aw: std.io.AllocatingWriter = undefined;1519 var aw: std.io.AllocatingWriter = undefined;
1515 var s: Serializer = .{ .writer = aw.init(std.testing.allocator) };1520 aw.init(std.testing.allocator);
1521 var s: Serializer = .{ .writer = &aw.buffered_writer };
1516 defer aw.deinit();1522 defer aw.deinit();
15171523
1518 // Printable ASCII1524 // Printable ASCII
...@@ -1622,7 +1628,8 @@ test "std.zon stringify utf8 codepoints" {...@@ -1622,7 +1628,8 @@ test "std.zon stringify utf8 codepoints" {
16221628
1623test "std.zon stringify strings" {1629test "std.zon stringify strings" {
1624 var aw: std.io.AllocatingWriter = undefined;1630 var aw: std.io.AllocatingWriter = undefined;
1625 var s: Serializer = .{ .writer = aw.init(std.testing.allocator) };1631 aw.init(std.testing.allocator);
1632 var s: Serializer = .{ .writer = &aw.buffered_writer };
1626 defer aw.deinit();1633 defer aw.deinit();
16271634
1628 // Minimal case1635 // Minimal case
...@@ -1692,7 +1699,8 @@ test "std.zon stringify strings" {...@@ -1692,7 +1699,8 @@ test "std.zon stringify strings" {
16921699
1693test "std.zon stringify multiline strings" {1700test "std.zon stringify multiline strings" {
1694 var aw: std.io.AllocatingWriter = undefined;1701 var aw: std.io.AllocatingWriter = undefined;
1695 var s: Serializer = .{ .writer = aw.init(std.testing.allocator) };1702 aw.init(std.testing.allocator);
1703 var s: Serializer = .{ .writer = &aw.buffered_writer };
1696 defer aw.deinit();1704 defer aw.deinit();
16971705
1698 inline for (.{ true, false }) |whitespace| {1706 inline for (.{ true, false }) |whitespace| {
...@@ -1912,7 +1920,8 @@ test "std.zon stringify skip default fields" {...@@ -1912,7 +1920,8 @@ test "std.zon stringify skip default fields" {
19121920
1913test "std.zon depth limits" {1921test "std.zon depth limits" {
1914 var aw: std.io.AllocatingWriter = undefined;1922 var aw: std.io.AllocatingWriter = undefined;
1915 const bw = aw.init(std.testing.allocator);1923 aw.init(std.testing.allocator);
1924 const bw = &aw.buffered_writer;
1916 defer aw.deinit();1925 defer aw.deinit();
19171926
1918 const Recurse = struct { r: []const @This() };1927 const Recurse = struct { r: []const @This() };
...@@ -2173,7 +2182,8 @@ test "std.zon stringify primitives" {...@@ -2173,7 +2182,8 @@ test "std.zon stringify primitives" {
21732182
2174test "std.zon stringify ident" {2183test "std.zon stringify ident" {
2175 var aw: std.io.AllocatingWriter = undefined;2184 var aw: std.io.AllocatingWriter = undefined;
2176 var s: Serializer = .{ .writer = aw.init(std.testing.allocator) };2185 aw.init(std.testing.allocator);
2186 var s: Serializer = .{ .writer = &aw.buffered_writer };
2177 defer aw.deinit();2187 defer aw.deinit();
21782188
2179 try expectSerializeEqual(".{ .a = 0 }", .{ .a = 0 }, .{});2189 try expectSerializeEqual(".{ .a = 0 }", .{ .a = 0 }, .{});
...@@ -2220,7 +2230,8 @@ test "std.zon stringify ident" {...@@ -2220,7 +2230,8 @@ test "std.zon stringify ident" {
22202230
2221test "std.zon stringify as tuple" {2231test "std.zon stringify as tuple" {
2222 var aw: std.io.AllocatingWriter = undefined;2232 var aw: std.io.AllocatingWriter = undefined;
2223 var s: Serializer = .{ .writer = aw.init(std.testing.allocator) };2233 aw.init(std.testing.allocator);
2234 var s: Serializer = .{ .writer = &aw.buffered_writer };
2224 defer aw.deinit();2235 defer aw.deinit();
22252236
2226 // Tuples2237 // Tuples
...@@ -2241,7 +2252,8 @@ test "std.zon stringify as tuple" {...@@ -2241,7 +2252,8 @@ test "std.zon stringify as tuple" {
22412252
2242test "std.zon stringify as float" {2253test "std.zon stringify as float" {
2243 var aw: std.io.AllocatingWriter = undefined;2254 var aw: std.io.AllocatingWriter = undefined;
2244 var s: Serializer = .{ .writer = aw.init(std.testing.allocator) };2255 aw.init(std.testing.allocator);
2256 var s: Serializer = .{ .writer = &aw.buffered_writer };
2245 defer aw.deinit();2257 defer aw.deinit();
22462258
2247 // Comptime float2259 // Comptime float
...@@ -2345,7 +2357,8 @@ test "std.zon pointers" {...@@ -2345,7 +2357,8 @@ test "std.zon pointers" {
23452357
2346test "std.zon tuple/struct field" {2358test "std.zon tuple/struct field" {
2347 var aw: std.io.AllocatingWriter = undefined;2359 var aw: std.io.AllocatingWriter = undefined;
2348 var s: Serializer = .{ .writer = aw.init(std.testing.allocator) };2360 aw.init(std.testing.allocator);
2361 var s: Serializer = .{ .writer = &aw.buffered_writer };
2349 defer aw.deinit();2362 defer aw.deinit();
23502363
2351 // Test on structs2364 // Test on structs