| author | |
| committer | |
| log | 1dcea220a4daf537c1a75835de45076fac429895 |
| tree | e2fe0cd20e756c7ce67e3dd209b9f2e951b0d0db |
| parent | 2ac81c76e3d176d4ac0c568d195d5b979078a938 |
9 files changed, 1016 insertions(+), 1033 deletions(-)
lib/std/Build/Fuzz/WebServer.zig+8-5| ... | @@ -522,7 +522,9 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void { | ... | @@ -522,7 +522,9 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void { |
| 522 | 522 | ||
| 523 | var cwd_cache: ?[]const u8 = null; | 523 | var cwd_cache: ?[]const u8 = null; |
| 524 | 524 | ||
| 525 | var archiver = std.tar.writer(response.writer()); | 525 | var adapter = response.writer().adaptToNewApi(); |
| 526 | var archiver: std.tar.Writer = .{ .underlying_writer = &adapter.new_interface }; | ||
| 527 | var read_buffer: [1024]u8 = undefined; | ||
| 526 | 528 | ||
| 527 | for (deduped_paths) |joined_path| { | 529 | for (deduped_paths) |joined_path| { |
| 528 | var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| { | 530 | var file = joined_path.root_dir.handle.openFile(joined_path.sub_path, .{}) catch |err| { |
| ... | @@ -530,13 +532,14 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void { | ... | @@ -530,13 +532,14 @@ fn serveSourcesTar(ws: *WebServer, request: *std.http.Server.Request) !void { |
| 530 | continue; | 532 | continue; |
| 531 | }; | 533 | }; |
| 532 | defer file.close(); | 534 | defer file.close(); |
| 533 | 535 | const stat = try file.stat(); | |
| 536 | var file_reader: std.fs.File.Reader = .initSize(file, &read_buffer, stat.size); | ||
| 534 | archiver.prefix = joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache); | 537 | archiver.prefix = joined_path.root_dir.path orelse try memoizedCwd(arena, &cwd_cache); |
| 535 | try archiver.writeFile(joined_path.sub_path, file); | 538 | try archiver.writeFile(joined_path.sub_path, &file_reader, stat.mtime); |
| 536 | } | 539 | } |
| 537 | 540 | ||
| 538 | // intentionally omitting the pointless trailer | 541 | // intentionally not calling `archiver.finishPedantically` |
| 539 | //try archiver.finish(); | 542 | try adapter.new_interface.flush(); |
| 540 | try response.end(); | 543 | try response.end(); |
| 541 | } | 544 | } |
| 542 | 545 |
lib/std/Io/Reader.zig+6| ... | @@ -179,6 +179,12 @@ pub fn streamExact(r: *Reader, w: *Writer, n: usize) StreamError!void { | ... | @@ -179,6 +179,12 @@ pub fn streamExact(r: *Reader, w: *Writer, n: usize) StreamError!void { |
| 179 | while (remaining != 0) remaining -= try r.stream(w, .limited(remaining)); | 179 | while (remaining != 0) remaining -= try r.stream(w, .limited(remaining)); |
| 180 | } | 180 | } |
| 181 | 181 | ||
| 182 | /// "Pump" exactly `n` bytes from the reader to the writer. | ||
| 183 | pub fn streamExact64(r: *Reader, w: *Writer, n: u64) StreamError!void { | ||
| 184 | var remaining = n; | ||
| 185 | while (remaining != 0) remaining -= try r.stream(w, .limited64(remaining)); | ||
| 186 | } | ||
| 187 | |||
| 182 | /// "Pump" data from the reader to the writer, handling `error.EndOfStream` as | 188 | /// "Pump" data from the reader to the writer, handling `error.EndOfStream` as |
| 183 | /// a success case. | 189 | /// a success case. |
| 184 | /// | 190 | /// |
lib/std/crypto/md5.zig+10-2| ... | @@ -54,12 +54,20 @@ pub const Md5 = struct { | ... | @@ -54,12 +54,20 @@ pub const Md5 = struct { |
| 54 | }; | 54 | }; |
| 55 | } | 55 | } |
| 56 | 56 | ||
| 57 | pub fn hash(b: []const u8, out: *[digest_length]u8, options: Options) void { | 57 | pub fn hash(data: []const u8, out: *[digest_length]u8, options: Options) void { |
| 58 | var d = Md5.init(options); | 58 | var d = Md5.init(options); |
| 59 | d.update(b); | 59 | d.update(data); |
| 60 | d.final(out); | 60 | d.final(out); |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | pub fn hashResult(data: []const u8) [digest_length]u8 { | ||
| 64 | var out: [digest_length]u8 = undefined; | ||
| 65 | var d = Md5.init(.{}); | ||
| 66 | d.update(data); | ||
| 67 | d.final(&out); | ||
| 68 | return out; | ||
| 69 | } | ||
| 70 | |||
| 63 | pub fn update(d: *Self, b: []const u8) void { | 71 | pub fn update(d: *Self, b: []const u8) void { |
| 64 | var off: usize = 0; | 72 | var off: usize = 0; |
| 65 | 73 |
lib/std/tar.zig+283-329| ... | @@ -19,7 +19,7 @@ const std = @import("std"); | ... | @@ -19,7 +19,7 @@ const std = @import("std"); |
| 19 | const assert = std.debug.assert; | 19 | const assert = std.debug.assert; |
| 20 | const testing = std.testing; | 20 | const testing = std.testing; |
| 21 | 21 | ||
| 22 | pub const writer = @import("tar/writer.zig").writer; | 22 | pub const Writer = @import("tar/Writer.zig"); |
| 23 | 23 | ||
| 24 | /// Provide this to receive detailed error messages. | 24 | /// Provide this to receive detailed error messages. |
| 25 | /// When this is provided, some errors which would otherwise be returned | 25 | /// When this is provided, some errors which would otherwise be returned |
| ... | @@ -293,28 +293,6 @@ fn nullStr(str: []const u8) []const u8 { | ... | @@ -293,28 +293,6 @@ fn nullStr(str: []const u8) []const u8 { |
| 293 | return str; | 293 | return str; |
| 294 | } | 294 | } |
| 295 | 295 | ||
| 296 | /// Options for iterator. | ||
| 297 | /// Buffers should be provided by the caller. | ||
| 298 | pub const IteratorOptions = struct { | ||
| 299 | /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities. | ||
| 300 | file_name_buffer: []u8, | ||
| 301 | /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities. | ||
| 302 | link_name_buffer: []u8, | ||
| 303 | /// Collects error messages during unpacking | ||
| 304 | diagnostics: ?*Diagnostics = null, | ||
| 305 | }; | ||
| 306 | |||
| 307 | /// Iterates over files in tar archive. | ||
| 308 | /// `next` returns each file in tar archive. | ||
| 309 | pub fn iterator(reader: anytype, options: IteratorOptions) Iterator(@TypeOf(reader)) { | ||
| 310 | return .{ | ||
| 311 | .reader = reader, | ||
| 312 | .diagnostics = options.diagnostics, | ||
| 313 | .file_name_buffer = options.file_name_buffer, | ||
| 314 | .link_name_buffer = options.link_name_buffer, | ||
| 315 | }; | ||
| 316 | } | ||
| 317 | |||
| 318 | /// Type of the file returned by iterator `next` method. | 296 | /// Type of the file returned by iterator `next` method. |
| 319 | pub const FileKind = enum { | 297 | pub const FileKind = enum { |
| 320 | directory, | 298 | directory, |
| ... | @@ -323,206 +301,192 @@ pub const FileKind = enum { | ... | @@ -323,206 +301,192 @@ pub const FileKind = enum { |
| 323 | }; | 301 | }; |
| 324 | 302 | ||
| 325 | /// Iterator over entries in the tar file represented by reader. | 303 | /// Iterator over entries in the tar file represented by reader. |
| 326 | pub fn Iterator(comptime ReaderType: type) type { | 304 | pub const Iterator = struct { |
| 327 | return struct { | 305 | reader: *std.Io.Reader, |
| 328 | reader: ReaderType, | 306 | diagnostics: ?*Diagnostics = null, |
| 329 | diagnostics: ?*Diagnostics = null, | ||
| 330 | |||
| 331 | // buffers for heeader and file attributes | ||
| 332 | header_buffer: [Header.SIZE]u8 = undefined, | ||
| 333 | file_name_buffer: []u8, | ||
| 334 | link_name_buffer: []u8, | ||
| 335 | |||
| 336 | // bytes of padding to the end of the block | ||
| 337 | padding: usize = 0, | ||
| 338 | // not consumed bytes of file from last next iteration | ||
| 339 | unread_file_bytes: u64 = 0, | ||
| 340 | |||
| 341 | pub const File = struct { | ||
| 342 | name: []const u8, // name of file, symlink or directory | ||
| 343 | link_name: []const u8, // target name of symlink | ||
| 344 | size: u64 = 0, // size of the file in bytes | ||
| 345 | mode: u32 = 0, | ||
| 346 | kind: FileKind = .file, | ||
| 347 | |||
| 348 | unread_bytes: *u64, | ||
| 349 | parent_reader: ReaderType, | ||
| 350 | |||
| 351 | pub const Reader = std.io.GenericReader(File, ReaderType.Error, File.read); | ||
| 352 | 307 | ||
| 353 | pub fn reader(self: File) Reader { | 308 | // buffers for heeader and file attributes |
| 354 | return .{ .context = self }; | 309 | header_buffer: [Header.SIZE]u8 = undefined, |
| 355 | } | 310 | file_name_buffer: []u8, |
| 311 | link_name_buffer: []u8, | ||
| 356 | 312 | ||
| 357 | pub fn read(self: File, dest: []u8) ReaderType.Error!usize { | 313 | // bytes of padding to the end of the block |
| 358 | const buf = dest[0..@min(dest.len, self.unread_bytes.*)]; | 314 | padding: usize = 0, |
| 359 | const n = try self.parent_reader.read(buf); | 315 | // not consumed bytes of file from last next iteration |
| 360 | self.unread_bytes.* -= n; | 316 | unread_file_bytes: u64 = 0, |
| 361 | return n; | ||
| 362 | } | ||
| 363 | 317 | ||
| 364 | // Writes file content to writer. | 318 | /// Options for iterator. |
| 365 | pub fn writeAll(self: File, out_writer: anytype) !void { | 319 | /// Buffers should be provided by the caller. |
| 366 | var buffer: [4096]u8 = undefined; | 320 | pub const Options = struct { |
| 321 | /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities. | ||
| 322 | file_name_buffer: []u8, | ||
| 323 | /// Use a buffer with length `std.fs.max_path_bytes` to match file system capabilities. | ||
| 324 | link_name_buffer: []u8, | ||
| 325 | /// Collects error messages during unpacking | ||
| 326 | diagnostics: ?*Diagnostics = null, | ||
| 327 | }; | ||
| 367 | 328 | ||
| 368 | while (self.unread_bytes.* > 0) { | 329 | /// Iterates over files in tar archive. |
| 369 | const buf = buffer[0..@min(buffer.len, self.unread_bytes.*)]; | 330 | /// `next` returns each file in tar archive. |
| 370 | try self.parent_reader.readNoEof(buf); | 331 | pub fn init(reader: *std.Io.Reader, options: Options) Iterator { |
| 371 | try out_writer.writeAll(buf); | 332 | return .{ |
| 372 | self.unread_bytes.* -= buf.len; | 333 | .reader = reader, |
| 373 | } | 334 | .diagnostics = options.diagnostics, |
| 374 | } | 335 | .file_name_buffer = options.file_name_buffer, |
| 336 | .link_name_buffer = options.link_name_buffer, | ||
| 375 | }; | 337 | }; |
| 338 | } | ||
| 376 | 339 | ||
| 377 | const Self = @This(); | 340 | pub const File = struct { |
| 378 | 341 | name: []const u8, // name of file, symlink or directory | |
| 379 | fn readHeader(self: *Self) !?Header { | 342 | link_name: []const u8, // target name of symlink |
| 380 | if (self.padding > 0) { | 343 | size: u64 = 0, // size of the file in bytes |
| 381 | try self.reader.skipBytes(self.padding, .{}); | 344 | mode: u32 = 0, |
| 382 | } | 345 | kind: FileKind = .file, |
| 383 | const n = try self.reader.readAll(&self.header_buffer); | 346 | }; |
| 384 | if (n == 0) return null; | ||
| 385 | if (n < Header.SIZE) return error.UnexpectedEndOfStream; | ||
| 386 | const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] }; | ||
| 387 | if (try header.checkChksum() == 0) return null; | ||
| 388 | return header; | ||
| 389 | } | ||
| 390 | 347 | ||
| 391 | fn readString(self: *Self, size: usize, buffer: []u8) ![]const u8 { | 348 | fn readHeader(self: *Iterator) !?Header { |
| 392 | if (size > buffer.len) return error.TarInsufficientBuffer; | 349 | if (self.padding > 0) { |
| 393 | const buf = buffer[0..size]; | 350 | try self.reader.discardAll(self.padding); |
| 394 | try self.reader.readNoEof(buf); | ||
| 395 | return nullStr(buf); | ||
| 396 | } | 351 | } |
| 352 | const n = try self.reader.readSliceShort(&self.header_buffer); | ||
| 353 | if (n == 0) return null; | ||
| 354 | if (n < Header.SIZE) return error.UnexpectedEndOfStream; | ||
| 355 | const header = Header{ .bytes = self.header_buffer[0..Header.SIZE] }; | ||
| 356 | if (try header.checkChksum() == 0) return null; | ||
| 357 | return header; | ||
| 358 | } | ||
| 397 | 359 | ||
| 398 | fn newFile(self: *Self) File { | 360 | fn readString(self: *Iterator, size: usize, buffer: []u8) ![]const u8 { |
| 399 | return .{ | 361 | if (size > buffer.len) return error.TarInsufficientBuffer; |
| 400 | .name = self.file_name_buffer[0..0], | 362 | const buf = buffer[0..size]; |
| 401 | .link_name = self.link_name_buffer[0..0], | 363 | try self.reader.readSliceAll(buf); |
| 402 | .parent_reader = self.reader, | 364 | return nullStr(buf); |
| 403 | .unread_bytes = &self.unread_file_bytes, | 365 | } |
| 404 | }; | ||
| 405 | } | ||
| 406 | 366 | ||
| 407 | // Number of padding bytes in the last file block. | 367 | fn newFile(self: *Iterator) File { |
| 408 | fn blockPadding(size: u64) usize { | 368 | return .{ |
| 409 | const block_rounded = std.mem.alignForward(u64, size, Header.SIZE); // size rounded to te block boundary | 369 | .name = self.file_name_buffer[0..0], |
| 410 | return @intCast(block_rounded - size); | 370 | .link_name = self.link_name_buffer[0..0], |
| 411 | } | 371 | }; |
| 372 | } | ||
| 412 | 373 | ||
| 413 | /// Iterates through the tar archive as if it is a series of files. | 374 | // Number of padding bytes in the last file block. |
| 414 | /// Internally, the tar format often uses entries (header with optional | 375 | fn blockPadding(size: u64) usize { |
| 415 | /// content) to add meta data that describes the next file. These | 376 | const block_rounded = std.mem.alignForward(u64, size, Header.SIZE); // size rounded to te block boundary |
| 416 | /// entries should not normally be visible to the outside. As such, this | 377 | return @intCast(block_rounded - size); |
| 417 | /// loop iterates through one or more entries until it collects a all | 378 | } |
| 418 | /// file attributes. | ||
| 419 | pub fn next(self: *Self) !?File { | ||
| 420 | if (self.unread_file_bytes > 0) { | ||
| 421 | // If file content was not consumed by caller | ||
| 422 | try self.reader.skipBytes(self.unread_file_bytes, .{}); | ||
| 423 | self.unread_file_bytes = 0; | ||
| 424 | } | ||
| 425 | var file: File = self.newFile(); | ||
| 426 | |||
| 427 | while (try self.readHeader()) |header| { | ||
| 428 | const kind = header.kind(); | ||
| 429 | const size: u64 = try header.size(); | ||
| 430 | self.padding = blockPadding(size); | ||
| 431 | |||
| 432 | switch (kind) { | ||
| 433 | // File types to return upstream | ||
| 434 | .directory, .normal, .symbolic_link => { | ||
| 435 | file.kind = switch (kind) { | ||
| 436 | .directory => .directory, | ||
| 437 | .normal => .file, | ||
| 438 | .symbolic_link => .sym_link, | ||
| 439 | else => unreachable, | ||
| 440 | }; | ||
| 441 | file.mode = try header.mode(); | ||
| 442 | |||
| 443 | // set file attributes if not already set by prefix/extended headers | ||
| 444 | if (file.size == 0) { | ||
| 445 | file.size = size; | ||
| 446 | } | ||
| 447 | if (file.link_name.len == 0) { | ||
| 448 | file.link_name = try header.linkName(self.link_name_buffer); | ||
| 449 | } | ||
| 450 | if (file.name.len == 0) { | ||
| 451 | file.name = try header.fullName(self.file_name_buffer); | ||
| 452 | } | ||
| 453 | 379 | ||
| 454 | self.padding = blockPadding(file.size); | 380 | /// Iterates through the tar archive as if it is a series of files. |
| 455 | self.unread_file_bytes = file.size; | 381 | /// Internally, the tar format often uses entries (header with optional |
| 456 | return file; | 382 | /// content) to add meta data that describes the next file. These |
| 457 | }, | 383 | /// entries should not normally be visible to the outside. As such, this |
| 458 | // Prefix header types | 384 | /// loop iterates through one or more entries until it collects a all |
| 459 | .gnu_long_name => { | 385 | /// file attributes. |
| 460 | file.name = try self.readString(@intCast(size), self.file_name_buffer); | 386 | pub fn next(self: *Iterator) !?File { |
| 461 | }, | 387 | if (self.unread_file_bytes > 0) { |
| 462 | .gnu_long_link => { | 388 | // If file content was not consumed by caller |
| 463 | file.link_name = try self.readString(@intCast(size), self.link_name_buffer); | 389 | try self.reader.discardAll64(self.unread_file_bytes); |
| 464 | }, | 390 | self.unread_file_bytes = 0; |
| 465 | .extended_header => { | 391 | } |
| 466 | // Use just attributes from last extended header. | 392 | var file: File = self.newFile(); |
| 467 | file = self.newFile(); | 393 | |
| 468 | 394 | while (try self.readHeader()) |header| { | |
| 469 | var rdr = paxIterator(self.reader, @intCast(size)); | 395 | const kind = header.kind(); |
| 470 | while (try rdr.next()) |attr| { | 396 | const size: u64 = try header.size(); |
| 471 | switch (attr.kind) { | 397 | self.padding = blockPadding(size); |
| 472 | .path => { | 398 | |
| 473 | file.name = try attr.value(self.file_name_buffer); | 399 | switch (kind) { |
| 474 | }, | 400 | // File types to return upstream |
| 475 | .linkpath => { | 401 | .directory, .normal, .symbolic_link => { |
| 476 | file.link_name = try attr.value(self.link_name_buffer); | 402 | file.kind = switch (kind) { |
| 477 | }, | 403 | .directory => .directory, |
| 478 | .size => { | 404 | .normal => .file, |
| 479 | var buf: [pax_max_size_attr_len]u8 = undefined; | 405 | .symbolic_link => .sym_link, |
| 480 | file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10); | 406 | else => unreachable, |
| 481 | }, | 407 | }; |
| 482 | } | 408 | file.mode = try header.mode(); |
| 483 | } | 409 | |
| 484 | }, | 410 | // set file attributes if not already set by prefix/extended headers |
| 485 | // Ignored header type | 411 | if (file.size == 0) { |
| 486 | .global_extended_header => { | 412 | file.size = size; |
| 487 | self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig; | 413 | } |
| 488 | }, | 414 | if (file.link_name.len == 0) { |
| 489 | // All other are unsupported header types | 415 | file.link_name = try header.linkName(self.link_name_buffer); |
| 490 | else => { | 416 | } |
| 491 | const d = self.diagnostics orelse return error.TarUnsupportedHeader; | 417 | if (file.name.len == 0) { |
| 492 | try d.errors.append(d.allocator, .{ .unsupported_file_type = .{ | 418 | file.name = try header.fullName(self.file_name_buffer); |
| 493 | .file_name = try d.allocator.dupe(u8, header.name()), | 419 | } |
| 494 | .file_type = kind, | 420 | |
| 495 | } }); | 421 | self.padding = blockPadding(file.size); |
| 496 | if (kind == .gnu_sparse) { | 422 | self.unread_file_bytes = file.size; |
| 497 | try self.skipGnuSparseExtendedHeaders(header); | 423 | return file; |
| 424 | }, | ||
| 425 | // Prefix header types | ||
| 426 | .gnu_long_name => { | ||
| 427 | file.name = try self.readString(@intCast(size), self.file_name_buffer); | ||
| 428 | }, | ||
| 429 | .gnu_long_link => { | ||
| 430 | file.link_name = try self.readString(@intCast(size), self.link_name_buffer); | ||
| 431 | }, | ||
| 432 | .extended_header => { | ||
| 433 | // Use just attributes from last extended header. | ||
| 434 | file = self.newFile(); | ||
| 435 | |||
| 436 | var rdr: PaxIterator = .{ | ||
| 437 | .reader = self.reader, | ||
| 438 | .size = @intCast(size), | ||
| 439 | }; | ||
| 440 | while (try rdr.next()) |attr| { | ||
| 441 | switch (attr.kind) { | ||
| 442 | .path => { | ||
| 443 | file.name = try attr.value(self.file_name_buffer); | ||
| 444 | }, | ||
| 445 | .linkpath => { | ||
| 446 | file.link_name = try attr.value(self.link_name_buffer); | ||
| 447 | }, | ||
| 448 | .size => { | ||
| 449 | var buf: [pax_max_size_attr_len]u8 = undefined; | ||
| 450 | file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10); | ||
| 451 | }, | ||
| 498 | } | 452 | } |
| 499 | self.reader.skipBytes(size, .{}) catch return error.TarHeadersTooBig; | 453 | } |
| 500 | }, | 454 | }, |
| 501 | } | 455 | // Ignored header type |
| 456 | .global_extended_header => { | ||
| 457 | self.reader.discardAll64(size) catch return error.TarHeadersTooBig; | ||
| 458 | }, | ||
| 459 | // All other are unsupported header types | ||
| 460 | else => { | ||
| 461 | const d = self.diagnostics orelse return error.TarUnsupportedHeader; | ||
| 462 | try d.errors.append(d.allocator, .{ .unsupported_file_type = .{ | ||
| 463 | .file_name = try d.allocator.dupe(u8, header.name()), | ||
| 464 | .file_type = kind, | ||
| 465 | } }); | ||
| 466 | if (kind == .gnu_sparse) { | ||
| 467 | try self.skipGnuSparseExtendedHeaders(header); | ||
| 468 | } | ||
| 469 | self.reader.discardAll64(size) catch return error.TarHeadersTooBig; | ||
| 470 | }, | ||
| 502 | } | 471 | } |
| 503 | return null; | ||
| 504 | } | 472 | } |
| 473 | return null; | ||
| 474 | } | ||
| 505 | 475 | ||
| 506 | fn skipGnuSparseExtendedHeaders(self: *Self, header: Header) !void { | 476 | pub fn streamRemaining(it: *Iterator, file: File, w: *std.Io.Writer) std.Io.Reader.StreamError!void { |
| 507 | var is_extended = header.bytes[482] > 0; | 477 | try it.reader.streamExact64(w, file.size); |
| 508 | while (is_extended) { | 478 | it.unread_file_bytes = 0; |
| 509 | var buf: [Header.SIZE]u8 = undefined; | 479 | } |
| 510 | const n = try self.reader.readAll(&buf); | ||
| 511 | if (n < Header.SIZE) return error.UnexpectedEndOfStream; | ||
| 512 | is_extended = buf[504] > 0; | ||
| 513 | } | ||
| 514 | } | ||
| 515 | }; | ||
| 516 | } | ||
| 517 | 480 | ||
| 518 | /// Pax attributes iterator. | 481 | fn skipGnuSparseExtendedHeaders(self: *Iterator, header: Header) !void { |
| 519 | /// Size is length of pax extended header in reader. | 482 | var is_extended = header.bytes[482] > 0; |
| 520 | fn paxIterator(reader: anytype, size: usize) PaxIterator(@TypeOf(reader)) { | 483 | while (is_extended) { |
| 521 | return PaxIterator(@TypeOf(reader)){ | 484 | var buf: [Header.SIZE]u8 = undefined; |
| 522 | .reader = reader, | 485 | try self.reader.readSliceAll(&buf); |
| 523 | .size = size, | 486 | is_extended = buf[504] > 0; |
| 524 | }; | 487 | } |
| 525 | } | 488 | } |
| 489 | }; | ||
| 526 | 490 | ||
| 527 | const PaxAttributeKind = enum { | 491 | const PaxAttributeKind = enum { |
| 528 | path, | 492 | path, |
| ... | @@ -533,108 +497,99 @@ const PaxAttributeKind = enum { | ... | @@ -533,108 +497,99 @@ const PaxAttributeKind = enum { |
| 533 | // maxInt(u64) has 20 chars, base 10 in practice we got 24 chars | 497 | // maxInt(u64) has 20 chars, base 10 in practice we got 24 chars |
| 534 | const pax_max_size_attr_len = 64; | 498 | const pax_max_size_attr_len = 64; |
| 535 | 499 | ||
| 536 | fn PaxIterator(comptime ReaderType: type) type { | 500 | pub const PaxIterator = struct { |
| 537 | return struct { | 501 | size: usize, // cumulative size of all pax attributes |
| 538 | size: usize, // cumulative size of all pax attributes | 502 | reader: *std.Io.Reader, |
| 539 | reader: ReaderType, | ||
| 540 | // scratch buffer used for reading attribute length and keyword | ||
| 541 | scratch: [128]u8 = undefined, | ||
| 542 | |||
| 543 | const Self = @This(); | ||
| 544 | |||
| 545 | const Attribute = struct { | ||
| 546 | kind: PaxAttributeKind, | ||
| 547 | len: usize, // length of the attribute value | ||
| 548 | reader: ReaderType, // reader positioned at value start | ||
| 549 | |||
| 550 | // Copies pax attribute value into destination buffer. | ||
| 551 | // Must be called with destination buffer of size at least Attribute.len. | ||
| 552 | pub fn value(self: Attribute, dst: []u8) ![]const u8 { | ||
| 553 | if (self.len > dst.len) return error.TarInsufficientBuffer; | ||
| 554 | // assert(self.len <= dst.len); | ||
| 555 | const buf = dst[0..self.len]; | ||
| 556 | const n = try self.reader.readAll(buf); | ||
| 557 | if (n < self.len) return error.UnexpectedEndOfStream; | ||
| 558 | try validateAttributeEnding(self.reader); | ||
| 559 | if (hasNull(buf)) return error.PaxNullInValue; | ||
| 560 | return buf; | ||
| 561 | } | ||
| 562 | }; | ||
| 563 | 503 | ||
| 564 | // Iterates over pax attributes. Returns known only known attributes. | 504 | const Self = @This(); |
| 565 | // Caller has to call value in Attribute, to advance reader across value. | ||
| 566 | pub fn next(self: *Self) !?Attribute { | ||
| 567 | // Pax extended header consists of one or more attributes, each constructed as follows: | ||
| 568 | // "%d %s=%s\n", <length>, <keyword>, <value> | ||
| 569 | while (self.size > 0) { | ||
| 570 | const length_buf = try self.readUntil(' '); | ||
| 571 | const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes | ||
| 572 | |||
| 573 | const keyword = try self.readUntil('='); | ||
| 574 | if (hasNull(keyword)) return error.PaxNullInKeyword; | ||
| 575 | |||
| 576 | // calculate value_len | ||
| 577 | const value_start = length_buf.len + keyword.len + 2; // 2 separators | ||
| 578 | if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream; | ||
| 579 | const value_len = length - value_start - 1; // \n separator at end | ||
| 580 | self.size -= length; | ||
| 581 | |||
| 582 | const kind: PaxAttributeKind = if (eql(keyword, "path")) | ||
| 583 | .path | ||
| 584 | else if (eql(keyword, "linkpath")) | ||
| 585 | .linkpath | ||
| 586 | else if (eql(keyword, "size")) | ||
| 587 | .size | ||
| 588 | else { | ||
| 589 | try self.reader.skipBytes(value_len, .{}); | ||
| 590 | try validateAttributeEnding(self.reader); | ||
| 591 | continue; | ||
| 592 | }; | ||
| 593 | if (kind == .size and value_len > pax_max_size_attr_len) { | ||
| 594 | return error.PaxSizeAttrOverflow; | ||
| 595 | } | ||
| 596 | return Attribute{ | ||
| 597 | .kind = kind, | ||
| 598 | .len = value_len, | ||
| 599 | .reader = self.reader, | ||
| 600 | }; | ||
| 601 | } | ||
| 602 | 505 | ||
| 603 | return null; | 506 | const Attribute = struct { |
| 507 | kind: PaxAttributeKind, | ||
| 508 | len: usize, // length of the attribute value | ||
| 509 | reader: *std.Io.Reader, // reader positioned at value start | ||
| 510 | |||
| 511 | // Copies pax attribute value into destination buffer. | ||
| 512 | // Must be called with destination buffer of size at least Attribute.len. | ||
| 513 | pub fn value(self: Attribute, dst: []u8) ![]const u8 { | ||
| 514 | if (self.len > dst.len) return error.TarInsufficientBuffer; | ||
| 515 | // assert(self.len <= dst.len); | ||
| 516 | const buf = dst[0..self.len]; | ||
| 517 | const n = try self.reader.readSliceShort(buf); | ||
| 518 | if (n < self.len) return error.UnexpectedEndOfStream; | ||
| 519 | try validateAttributeEnding(self.reader); | ||
| 520 | if (hasNull(buf)) return error.PaxNullInValue; | ||
| 521 | return buf; | ||
| 604 | } | 522 | } |
| 523 | }; | ||
| 605 | 524 | ||
| 606 | fn readUntil(self: *Self, delimiter: u8) ![]const u8 { | 525 | // Iterates over pax attributes. Returns known only known attributes. |
| 607 | var fbs = std.io.fixedBufferStream(&self.scratch); | 526 | // Caller has to call value in Attribute, to advance reader across value. |
| 608 | try self.reader.streamUntilDelimiter(fbs.writer(), delimiter, null); | 527 | pub fn next(self: *Self) !?Attribute { |
| 609 | return fbs.getWritten(); | 528 | // Pax extended header consists of one or more attributes, each constructed as follows: |
| 529 | // "%d %s=%s\n", <length>, <keyword>, <value> | ||
| 530 | while (self.size > 0) { | ||
| 531 | const length_buf = try self.reader.takeSentinel(' '); | ||
| 532 | const length = try std.fmt.parseInt(usize, length_buf, 10); // record length in bytes | ||
| 533 | |||
| 534 | const keyword = try self.reader.takeSentinel('='); | ||
| 535 | if (hasNull(keyword)) return error.PaxNullInKeyword; | ||
| 536 | |||
| 537 | // calculate value_len | ||
| 538 | const value_start = length_buf.len + keyword.len + 2; // 2 separators | ||
| 539 | if (length < value_start + 1 or self.size < length) return error.UnexpectedEndOfStream; | ||
| 540 | const value_len = length - value_start - 1; // \n separator at end | ||
| 541 | self.size -= length; | ||
| 542 | |||
| 543 | const kind: PaxAttributeKind = if (eql(keyword, "path")) | ||
| 544 | .path | ||
| 545 | else if (eql(keyword, "linkpath")) | ||
| 546 | .linkpath | ||
| 547 | else if (eql(keyword, "size")) | ||
| 548 | .size | ||
| 549 | else { | ||
| 550 | try self.reader.discardAll(value_len); | ||
| 551 | try validateAttributeEnding(self.reader); | ||
| 552 | continue; | ||
| 553 | }; | ||
| 554 | if (kind == .size and value_len > pax_max_size_attr_len) { | ||
| 555 | return error.PaxSizeAttrOverflow; | ||
| 556 | } | ||
| 557 | return .{ | ||
| 558 | .kind = kind, | ||
| 559 | .len = value_len, | ||
| 560 | .reader = self.reader, | ||
| 561 | }; | ||
| 610 | } | 562 | } |
| 611 | 563 | ||
| 612 | fn eql(a: []const u8, b: []const u8) bool { | 564 | return null; |
| 613 | return std.mem.eql(u8, a, b); | 565 | } |
| 614 | } | ||
| 615 | 566 | ||
| 616 | fn hasNull(str: []const u8) bool { | 567 | fn eql(a: []const u8, b: []const u8) bool { |
| 617 | return (std.mem.indexOfScalar(u8, str, 0)) != null; | 568 | return std.mem.eql(u8, a, b); |
| 618 | } | 569 | } |
| 619 | 570 | ||
| 620 | // Checks that each record ends with new line. | 571 | fn hasNull(str: []const u8) bool { |
| 621 | fn validateAttributeEnding(reader: ReaderType) !void { | 572 | return (std.mem.indexOfScalar(u8, str, 0)) != null; |
| 622 | if (try reader.readByte() != '\n') return error.PaxInvalidAttributeEnd; | 573 | } |
| 623 | } | 574 | |
| 624 | }; | 575 | // Checks that each record ends with new line. |
| 625 | } | 576 | fn validateAttributeEnding(reader: *std.Io.Reader) !void { |
| 577 | if (try reader.takeByte() != '\n') return error.PaxInvalidAttributeEnd; | ||
| 578 | } | ||
| 579 | }; | ||
| 626 | 580 | ||
| 627 | /// Saves tar file content to the file systems. | 581 | /// Saves tar file content to the file systems. |
| 628 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions) !void { | 582 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: *std.Io.Reader, options: PipeOptions) !void { |
| 629 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | 583 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; |
| 630 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | 584 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; |
| 631 | var iter = iterator(reader, .{ | 585 | var file_contents_buffer: [1024]u8 = undefined; |
| 586 | var it: Iterator = .init(reader, .{ | ||
| 632 | .file_name_buffer = &file_name_buffer, | 587 | .file_name_buffer = &file_name_buffer, |
| 633 | .link_name_buffer = &link_name_buffer, | 588 | .link_name_buffer = &link_name_buffer, |
| 634 | .diagnostics = options.diagnostics, | 589 | .diagnostics = options.diagnostics, |
| 635 | }); | 590 | }); |
| 636 | 591 | ||
| 637 | while (try iter.next()) |file| { | 592 | while (try it.next()) |file| { |
| 638 | const file_name = stripComponents(file.name, options.strip_components); | 593 | const file_name = stripComponents(file.name, options.strip_components); |
| 639 | if (file_name.len == 0 and file.kind != .directory) { | 594 | if (file_name.len == 0 and file.kind != .directory) { |
| 640 | const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix; | 595 | const d = options.diagnostics orelse return error.TarComponentsOutsideStrippedPrefix; |
| ... | @@ -656,7 +611,9 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions) | ... | @@ -656,7 +611,9 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions) |
| 656 | .file => { | 611 | .file => { |
| 657 | if (createDirAndFile(dir, file_name, fileMode(file.mode, options))) |fs_file| { | 612 | if (createDirAndFile(dir, file_name, fileMode(file.mode, options))) |fs_file| { |
| 658 | defer fs_file.close(); | 613 | defer fs_file.close(); |
| 659 | try file.writeAll(fs_file); | 614 | var file_writer = fs_file.writer(&file_contents_buffer); |
| 615 | try it.streamRemaining(file, &file_writer.interface); | ||
| 616 | try file_writer.interface.flush(); | ||
| 660 | } else |err| { | 617 | } else |err| { |
| 661 | const d = options.diagnostics orelse return err; | 618 | const d = options.diagnostics orelse return err; |
| 662 | try d.errors.append(d.allocator, .{ .unable_to_create_file = .{ | 619 | try d.errors.append(d.allocator, .{ .unable_to_create_file = .{ |
| ... | @@ -826,11 +783,14 @@ test PaxIterator { | ... | @@ -826,11 +783,14 @@ test PaxIterator { |
| 826 | var buffer: [1024]u8 = undefined; | 783 | var buffer: [1024]u8 = undefined; |
| 827 | 784 | ||
| 828 | outer: for (cases) |case| { | 785 | outer: for (cases) |case| { |
| 829 | var stream = std.io.fixedBufferStream(case.data); | 786 | var reader: std.Io.Reader = .fixed(case.data); |
| 830 | var iter = paxIterator(stream.reader(), case.data.len); | 787 | var it: PaxIterator = .{ |
| 788 | .size = case.data.len, | ||
| 789 | .reader = &reader, | ||
| 790 | }; | ||
| 831 | 791 | ||
| 832 | var i: usize = 0; | 792 | var i: usize = 0; |
| 833 | while (iter.next() catch |err| { | 793 | while (it.next() catch |err| { |
| 834 | if (case.err) |e| { | 794 | if (case.err) |e| { |
| 835 | try testing.expectEqual(e, err); | 795 | try testing.expectEqual(e, err); |
| 836 | continue; | 796 | continue; |
| ... | @@ -853,12 +813,6 @@ test PaxIterator { | ... | @@ -853,12 +813,6 @@ test PaxIterator { |
| 853 | } | 813 | } |
| 854 | } | 814 | } |
| 855 | 815 | ||
| 856 | test { | ||
| 857 | _ = @import("tar/test.zig"); | ||
| 858 | _ = @import("tar/writer.zig"); | ||
| 859 | _ = Diagnostics; | ||
| 860 | } | ||
| 861 | |||
| 862 | test "header parse size" { | 816 | test "header parse size" { |
| 863 | const cases = [_]struct { | 817 | const cases = [_]struct { |
| 864 | in: []const u8, | 818 | in: []const u8, |
| ... | @@ -941,7 +895,7 @@ test "create file and symlink" { | ... | @@ -941,7 +895,7 @@ test "create file and symlink" { |
| 941 | file.close(); | 895 | file.close(); |
| 942 | } | 896 | } |
| 943 | 897 | ||
| 944 | test iterator { | 898 | test Iterator { |
| 945 | // Example tar file is created from this tree structure: | 899 | // Example tar file is created from this tree structure: |
| 946 | // $ tree example | 900 | // $ tree example |
| 947 | // example | 901 | // example |
| ... | @@ -962,19 +916,19 @@ test iterator { | ... | @@ -962,19 +916,19 @@ test iterator { |
| 962 | // example/empty/ | 916 | // example/empty/ |
| 963 | 917 | ||
| 964 | const data = @embedFile("tar/testdata/example.tar"); | 918 | const data = @embedFile("tar/testdata/example.tar"); |
| 965 | var fbs = std.io.fixedBufferStream(data); | 919 | var reader: std.Io.Reader = .fixed(data); |
| 966 | 920 | ||
| 967 | // User provided buffers to the iterator | 921 | // User provided buffers to the iterator |
| 968 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | 922 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; |
| 969 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | 923 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; |
| 970 | // Create iterator | 924 | // Create iterator |
| 971 | var iter = iterator(fbs.reader(), .{ | 925 | var it: Iterator = .init(&reader, .{ |
| 972 | .file_name_buffer = &file_name_buffer, | 926 | .file_name_buffer = &file_name_buffer, |
| 973 | .link_name_buffer = &link_name_buffer, | 927 | .link_name_buffer = &link_name_buffer, |
| 974 | }); | 928 | }); |
| 975 | // Iterate over files in example.tar | 929 | // Iterate over files in example.tar |
| 976 | var file_no: usize = 0; | 930 | var file_no: usize = 0; |
| 977 | while (try iter.next()) |file| : (file_no += 1) { | 931 | while (try it.next()) |file| : (file_no += 1) { |
| 978 | switch (file.kind) { | 932 | switch (file.kind) { |
| 979 | .directory => { | 933 | .directory => { |
| 980 | switch (file_no) { | 934 | switch (file_no) { |
| ... | @@ -987,10 +941,10 @@ test iterator { | ... | @@ -987,10 +941,10 @@ test iterator { |
| 987 | }, | 941 | }, |
| 988 | .file => { | 942 | .file => { |
| 989 | try testing.expectEqualStrings("example/a/file", file.name); | 943 | try testing.expectEqualStrings("example/a/file", file.name); |
| 990 | // Read file content | ||
| 991 | var buf: [16]u8 = undefined; | 944 | var buf: [16]u8 = undefined; |
| 992 | const n = try file.reader().readAll(&buf); | 945 | var w: std.Io.Writer = .fixed(&buf); |
| 993 | try testing.expectEqualStrings("content\n", buf[0..n]); | 946 | try it.streamRemaining(file, &w); |
| 947 | try testing.expectEqualStrings("content\n", w.buffered()); | ||
| 994 | }, | 948 | }, |
| 995 | .sym_link => { | 949 | .sym_link => { |
| 996 | try testing.expectEqualStrings("example/b/symlink", file.name); | 950 | try testing.expectEqualStrings("example/b/symlink", file.name); |
| ... | @@ -1021,15 +975,14 @@ test pipeToFileSystem { | ... | @@ -1021,15 +975,14 @@ test pipeToFileSystem { |
| 1021 | // example/empty/ | 975 | // example/empty/ |
| 1022 | 976 | ||
| 1023 | const data = @embedFile("tar/testdata/example.tar"); | 977 | const data = @embedFile("tar/testdata/example.tar"); |
| 1024 | var fbs = std.io.fixedBufferStream(data); | 978 | var reader: std.Io.Reader = .fixed(data); |
| 1025 | const reader = fbs.reader(); | ||
| 1026 | 979 | ||
| 1027 | var tmp = testing.tmpDir(.{ .no_follow = true }); | 980 | var tmp = testing.tmpDir(.{ .no_follow = true }); |
| 1028 | defer tmp.cleanup(); | 981 | defer tmp.cleanup(); |
| 1029 | const dir = tmp.dir; | 982 | const dir = tmp.dir; |
| 1030 | 983 | ||
| 1031 | // Save tar from `reader` to the file system `dir` | 984 | // Save tar from reader to the file system `dir` |
| 1032 | pipeToFileSystem(dir, reader, .{ | 985 | pipeToFileSystem(dir, &reader, .{ |
| 1033 | .mode_mode = .ignore, | 986 | .mode_mode = .ignore, |
| 1034 | .strip_components = 1, | 987 | .strip_components = 1, |
| 1035 | .exclude_empty_directories = true, | 988 | .exclude_empty_directories = true, |
| ... | @@ -1053,8 +1006,7 @@ test pipeToFileSystem { | ... | @@ -1053,8 +1006,7 @@ test pipeToFileSystem { |
| 1053 | 1006 | ||
| 1054 | test "pipeToFileSystem root_dir" { | 1007 | test "pipeToFileSystem root_dir" { |
| 1055 | const data = @embedFile("tar/testdata/example.tar"); | 1008 | const data = @embedFile("tar/testdata/example.tar"); |
| 1056 | var fbs = std.io.fixedBufferStream(data); | 1009 | var reader: std.Io.Reader = .fixed(data); |
| 1057 | const reader = fbs.reader(); | ||
| 1058 | 1010 | ||
| 1059 | // with strip_components = 1 | 1011 | // with strip_components = 1 |
| 1060 | { | 1012 | { |
| ... | @@ -1063,7 +1015,7 @@ test "pipeToFileSystem root_dir" { | ... | @@ -1063,7 +1015,7 @@ test "pipeToFileSystem root_dir" { |
| 1063 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1015 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1064 | defer diagnostics.deinit(); | 1016 | defer diagnostics.deinit(); |
| 1065 | 1017 | ||
| 1066 | pipeToFileSystem(tmp.dir, reader, .{ | 1018 | pipeToFileSystem(tmp.dir, &reader, .{ |
| 1067 | .strip_components = 1, | 1019 | .strip_components = 1, |
| 1068 | .diagnostics = &diagnostics, | 1020 | .diagnostics = &diagnostics, |
| 1069 | }) catch |err| { | 1021 | }) catch |err| { |
| ... | @@ -1079,13 +1031,13 @@ test "pipeToFileSystem root_dir" { | ... | @@ -1079,13 +1031,13 @@ test "pipeToFileSystem root_dir" { |
| 1079 | 1031 | ||
| 1080 | // with strip_components = 0 | 1032 | // with strip_components = 0 |
| 1081 | { | 1033 | { |
| 1082 | fbs.reset(); | 1034 | reader = .fixed(data); |
| 1083 | var tmp = testing.tmpDir(.{ .no_follow = true }); | 1035 | var tmp = testing.tmpDir(.{ .no_follow = true }); |
| 1084 | defer tmp.cleanup(); | 1036 | defer tmp.cleanup(); |
| 1085 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1037 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1086 | defer diagnostics.deinit(); | 1038 | defer diagnostics.deinit(); |
| 1087 | 1039 | ||
| 1088 | pipeToFileSystem(tmp.dir, reader, .{ | 1040 | pipeToFileSystem(tmp.dir, &reader, .{ |
| 1089 | .strip_components = 0, | 1041 | .strip_components = 0, |
| 1090 | .diagnostics = &diagnostics, | 1042 | .diagnostics = &diagnostics, |
| 1091 | }) catch |err| { | 1043 | }) catch |err| { |
| ... | @@ -1102,45 +1054,42 @@ test "pipeToFileSystem root_dir" { | ... | @@ -1102,45 +1054,42 @@ test "pipeToFileSystem root_dir" { |
| 1102 | 1054 | ||
| 1103 | test "findRoot with single file archive" { | 1055 | test "findRoot with single file archive" { |
| 1104 | const data = @embedFile("tar/testdata/22752.tar"); | 1056 | const data = @embedFile("tar/testdata/22752.tar"); |
| 1105 | var fbs = std.io.fixedBufferStream(data); | 1057 | var reader: std.Io.Reader = .fixed(data); |
| 1106 | const reader = fbs.reader(); | ||
| 1107 | 1058 | ||
| 1108 | var tmp = testing.tmpDir(.{}); | 1059 | var tmp = testing.tmpDir(.{}); |
| 1109 | defer tmp.cleanup(); | 1060 | defer tmp.cleanup(); |
| 1110 | 1061 | ||
| 1111 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1062 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1112 | defer diagnostics.deinit(); | 1063 | defer diagnostics.deinit(); |
| 1113 | try pipeToFileSystem(tmp.dir, reader, .{ .diagnostics = &diagnostics }); | 1064 | try pipeToFileSystem(tmp.dir, &reader, .{ .diagnostics = &diagnostics }); |
| 1114 | 1065 | ||
| 1115 | try testing.expectEqualStrings("", diagnostics.root_dir); | 1066 | try testing.expectEqualStrings("", diagnostics.root_dir); |
| 1116 | } | 1067 | } |
| 1117 | 1068 | ||
| 1118 | test "findRoot without explicit root dir" { | 1069 | test "findRoot without explicit root dir" { |
| 1119 | const data = @embedFile("tar/testdata/19820.tar"); | 1070 | const data = @embedFile("tar/testdata/19820.tar"); |
| 1120 | var fbs = std.io.fixedBufferStream(data); | 1071 | var reader: std.Io.Reader = .fixed(data); |
| 1121 | const reader = fbs.reader(); | ||
| 1122 | 1072 | ||
| 1123 | var tmp = testing.tmpDir(.{}); | 1073 | var tmp = testing.tmpDir(.{}); |
| 1124 | defer tmp.cleanup(); | 1074 | defer tmp.cleanup(); |
| 1125 | 1075 | ||
| 1126 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1076 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1127 | defer diagnostics.deinit(); | 1077 | defer diagnostics.deinit(); |
| 1128 | try pipeToFileSystem(tmp.dir, reader, .{ .diagnostics = &diagnostics }); | 1078 | try pipeToFileSystem(tmp.dir, &reader, .{ .diagnostics = &diagnostics }); |
| 1129 | 1079 | ||
| 1130 | try testing.expectEqualStrings("root", diagnostics.root_dir); | 1080 | try testing.expectEqualStrings("root", diagnostics.root_dir); |
| 1131 | } | 1081 | } |
| 1132 | 1082 | ||
| 1133 | test "pipeToFileSystem strip_components" { | 1083 | test "pipeToFileSystem strip_components" { |
| 1134 | const data = @embedFile("tar/testdata/example.tar"); | 1084 | const data = @embedFile("tar/testdata/example.tar"); |
| 1135 | var fbs = std.io.fixedBufferStream(data); | 1085 | var reader: std.Io.Reader = .fixed(data); |
| 1136 | const reader = fbs.reader(); | ||
| 1137 | 1086 | ||
| 1138 | var tmp = testing.tmpDir(.{ .no_follow = true }); | 1087 | var tmp = testing.tmpDir(.{ .no_follow = true }); |
| 1139 | defer tmp.cleanup(); | 1088 | defer tmp.cleanup(); |
| 1140 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; | 1089 | var diagnostics: Diagnostics = .{ .allocator = testing.allocator }; |
| 1141 | defer diagnostics.deinit(); | 1090 | defer diagnostics.deinit(); |
| 1142 | 1091 | ||
| 1143 | pipeToFileSystem(tmp.dir, reader, .{ | 1092 | pipeToFileSystem(tmp.dir, &reader, .{ |
| 1144 | .strip_components = 3, | 1093 | .strip_components = 3, |
| 1145 | .diagnostics = &diagnostics, | 1094 | .diagnostics = &diagnostics, |
| 1146 | }) catch |err| { | 1095 | }) catch |err| { |
| ... | @@ -1194,13 +1143,12 @@ test "executable bit" { | ... | @@ -1194,13 +1143,12 @@ test "executable bit" { |
| 1194 | const data = @embedFile("tar/testdata/example.tar"); | 1143 | const data = @embedFile("tar/testdata/example.tar"); |
| 1195 | 1144 | ||
| 1196 | for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| { | 1145 | for ([_]PipeOptions.ModeMode{ .ignore, .executable_bit_only }) |opt| { |
| 1197 | var fbs = std.io.fixedBufferStream(data); | 1146 | var reader: std.Io.Reader = .fixed(data); |
| 1198 | const reader = fbs.reader(); | ||
| 1199 | 1147 | ||
| 1200 | var tmp = testing.tmpDir(.{ .no_follow = true }); | 1148 | var tmp = testing.tmpDir(.{ .no_follow = true }); |
| 1201 | //defer tmp.cleanup(); | 1149 | //defer tmp.cleanup(); |
| 1202 | 1150 | ||
| 1203 | pipeToFileSystem(tmp.dir, reader, .{ | 1151 | pipeToFileSystem(tmp.dir, &reader, .{ |
| 1204 | .strip_components = 1, | 1152 | .strip_components = 1, |
| 1205 | .exclude_empty_directories = true, | 1153 | .exclude_empty_directories = true, |
| 1206 | .mode_mode = opt, | 1154 | .mode_mode = opt, |
| ... | @@ -1226,3 +1174,9 @@ test "executable bit" { | ... | @@ -1226,3 +1174,9 @@ test "executable bit" { |
| 1226 | } | 1174 | } |
| 1227 | } | 1175 | } |
| 1228 | } | 1176 | } |
| 1177 | |||
| 1178 | test { | ||
| 1179 | _ = @import("tar/test.zig"); | ||
| 1180 | _ = Writer; | ||
| 1181 | _ = Diagnostics; | ||
| 1182 | } |
lib/std/tar/Writer.zig created+498| ... | @@ -0,0 +1,498 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const assert = std.debug.assert; | ||
| 3 | const testing = std.testing; | ||
| 4 | const Writer = @This(); | ||
| 5 | |||
| 6 | const block_size = @sizeOf(Header); | ||
| 7 | |||
| 8 | /// Options for writing file/dir/link. If left empty 0o664 is used for | ||
| 9 | /// file mode and current time for mtime. | ||
| 10 | pub const Options = struct { | ||
| 11 | /// File system permission mode. | ||
| 12 | mode: u32 = 0, | ||
| 13 | /// File system modification time. | ||
| 14 | mtime: u64 = 0, | ||
| 15 | }; | ||
| 16 | |||
| 17 | underlying_writer: *std.Io.Writer, | ||
| 18 | prefix: []const u8 = "", | ||
| 19 | mtime_now: u64 = 0, | ||
| 20 | |||
| 21 | const Error = error{ | ||
| 22 | WriteFailed, | ||
| 23 | OctalOverflow, | ||
| 24 | NameTooLong, | ||
| 25 | }; | ||
| 26 | |||
| 27 | /// Sets prefix for all other write* method paths. | ||
| 28 | pub fn setRoot(w: *Writer, root: []const u8) Error!void { | ||
| 29 | if (root.len > 0) | ||
| 30 | try w.writeDir(root, .{}); | ||
| 31 | |||
| 32 | w.prefix = root; | ||
| 33 | } | ||
| 34 | |||
| 35 | pub fn writeDir(w: *Writer, sub_path: []const u8, options: Options) Error!void { | ||
| 36 | try w.writeHeader(.directory, sub_path, "", 0, options); | ||
| 37 | } | ||
| 38 | |||
| 39 | pub const WriteFileError = std.Io.Writer.FileError || Error || std.fs.File.GetEndPosError; | ||
| 40 | |||
| 41 | pub fn writeFile( | ||
| 42 | w: *Writer, | ||
| 43 | sub_path: []const u8, | ||
| 44 | file_reader: *std.fs.File.Reader, | ||
| 45 | stat_mtime: i128, | ||
| 46 | ) WriteFileError!void { | ||
| 47 | const size = try file_reader.getSize(); | ||
| 48 | const mtime: u64 = @intCast(@divFloor(stat_mtime, std.time.ns_per_s)); | ||
| 49 | |||
| 50 | var header: Header = .{}; | ||
| 51 | try w.setPath(&header, sub_path); | ||
| 52 | try header.setSize(size); | ||
| 53 | try header.setMtime(mtime); | ||
| 54 | try header.updateChecksum(); | ||
| 55 | |||
| 56 | try w.underlying_writer.writeAll(@ptrCast((&header)[0..1])); | ||
| 57 | _ = try w.underlying_writer.sendFileAll(file_reader, .unlimited); | ||
| 58 | try w.writePadding(size); | ||
| 59 | } | ||
| 60 | |||
| 61 | pub const WriteFileStreamError = Error || std.Io.Reader.StreamError; | ||
| 62 | |||
| 63 | /// Writes file reading file content from `reader`. Reads exactly `size` bytes | ||
| 64 | /// from `reader`, or returns `error.EndOfStream`. | ||
| 65 | pub fn writeFileStream( | ||
| 66 | w: *Writer, | ||
| 67 | sub_path: []const u8, | ||
| 68 | size: u64, | ||
| 69 | reader: *std.Io.Reader, | ||
| 70 | options: Options, | ||
| 71 | ) WriteFileStreamError!void { | ||
| 72 | try w.writeHeader(.regular, sub_path, "", size, options); | ||
| 73 | try reader.streamExact64(w.underlying_writer, size); | ||
| 74 | try w.writePadding(size); | ||
| 75 | } | ||
| 76 | |||
| 77 | /// Writes file using bytes buffer `content` for size and file content. | ||
| 78 | pub fn writeFileBytes(w: *Writer, sub_path: []const u8, content: []const u8, options: Options) Error!void { | ||
| 79 | try w.writeHeader(.regular, sub_path, "", content.len, options); | ||
| 80 | try w.underlying_writer.writeAll(content); | ||
| 81 | try w.writePadding(content.len); | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn writeLink(w: *Writer, sub_path: []const u8, link_name: []const u8, options: Options) Error!void { | ||
| 85 | try w.writeHeader(.symbolic_link, sub_path, link_name, 0, options); | ||
| 86 | } | ||
| 87 | |||
| 88 | /// Writes fs.Dir.WalkerEntry. Uses `mtime` from file system entry and | ||
| 89 | /// default for entry mode . | ||
| 90 | pub fn writeEntry(w: *Writer, entry: std.fs.Dir.Walker.Entry) Error!void { | ||
| 91 | switch (entry.kind) { | ||
| 92 | .directory => { | ||
| 93 | try w.writeDir(entry.path, .{ .mtime = try entryMtime(entry) }); | ||
| 94 | }, | ||
| 95 | .file => { | ||
| 96 | var file = try entry.dir.openFile(entry.basename, .{}); | ||
| 97 | defer file.close(); | ||
| 98 | const stat = try file.stat(); | ||
| 99 | try w.writeFile(entry.path, file, stat); | ||
| 100 | }, | ||
| 101 | .sym_link => { | ||
| 102 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | ||
| 103 | const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer); | ||
| 104 | try w.writeLink(entry.path, link_name, .{ .mtime = try entryMtime(entry) }); | ||
| 105 | }, | ||
| 106 | else => { | ||
| 107 | return error.UnsupportedWalkerEntryKind; | ||
| 108 | }, | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | fn writeHeader( | ||
| 113 | w: *Writer, | ||
| 114 | typeflag: Header.FileType, | ||
| 115 | sub_path: []const u8, | ||
| 116 | link_name: []const u8, | ||
| 117 | size: u64, | ||
| 118 | options: Options, | ||
| 119 | ) Error!void { | ||
| 120 | var header = Header.init(typeflag); | ||
| 121 | try w.setPath(&header, sub_path); | ||
| 122 | try header.setSize(size); | ||
| 123 | try header.setMtime(if (options.mtime != 0) options.mtime else w.mtimeNow()); | ||
| 124 | if (options.mode != 0) | ||
| 125 | try header.setMode(options.mode); | ||
| 126 | if (typeflag == .symbolic_link) | ||
| 127 | header.setLinkname(link_name) catch |err| switch (err) { | ||
| 128 | error.NameTooLong => try w.writeExtendedHeader(.gnu_long_link, &.{link_name}), | ||
| 129 | else => return err, | ||
| 130 | }; | ||
| 131 | try header.write(w.underlying_writer); | ||
| 132 | } | ||
| 133 | |||
| 134 | fn mtimeNow(w: *Writer) u64 { | ||
| 135 | if (w.mtime_now == 0) | ||
| 136 | w.mtime_now = @intCast(std.time.timestamp()); | ||
| 137 | return w.mtime_now; | ||
| 138 | } | ||
| 139 | |||
| 140 | fn entryMtime(entry: std.fs.Dir.Walker.Entry) !u64 { | ||
| 141 | const stat = try entry.dir.statFile(entry.basename); | ||
| 142 | return @intCast(@divFloor(stat.mtime, std.time.ns_per_s)); | ||
| 143 | } | ||
| 144 | |||
| 145 | /// Writes path in posix header, if don't fit (in name+prefix; 100+155 | ||
| 146 | /// bytes) writes it in gnu extended header. | ||
| 147 | fn setPath(w: *Writer, header: *Header, sub_path: []const u8) Error!void { | ||
| 148 | header.setPath(w.prefix, sub_path) catch |err| switch (err) { | ||
| 149 | error.NameTooLong => { | ||
| 150 | // write extended header | ||
| 151 | const buffers: []const []const u8 = if (w.prefix.len == 0) | ||
| 152 | &.{sub_path} | ||
| 153 | else | ||
| 154 | &.{ w.prefix, "/", sub_path }; | ||
| 155 | try w.writeExtendedHeader(.gnu_long_name, buffers); | ||
| 156 | }, | ||
| 157 | else => return err, | ||
| 158 | }; | ||
| 159 | } | ||
| 160 | |||
| 161 | /// Writes gnu extended header: gnu_long_name or gnu_long_link. | ||
| 162 | fn writeExtendedHeader(w: *Writer, typeflag: Header.FileType, buffers: []const []const u8) Error!void { | ||
| 163 | var len: usize = 0; | ||
| 164 | for (buffers) |buf| len += buf.len; | ||
| 165 | |||
| 166 | var header: Header = .init(typeflag); | ||
| 167 | try header.setSize(len); | ||
| 168 | try header.write(w.underlying_writer); | ||
| 169 | for (buffers) |buf| | ||
| 170 | try w.underlying_writer.writeAll(buf); | ||
| 171 | try w.writePadding(len); | ||
| 172 | } | ||
| 173 | |||
| 174 | fn writePadding(w: *Writer, bytes: usize) std.Io.Writer.Error!void { | ||
| 175 | const pos = bytes % block_size; | ||
| 176 | if (pos == 0) return; | ||
| 177 | try w.underlying_writer.splatByteAll(0, block_size - pos); | ||
| 178 | } | ||
| 179 | |||
| 180 | /// According to the specification, tar should finish with two zero blocks, but | ||
| 181 | /// "reasonable system must not assume that such a block exists when reading an | ||
| 182 | /// archive". Therefore, the Zig standard library recommends to not call this | ||
| 183 | /// function. | ||
| 184 | pub fn finishPedantically(w: *Writer) std.Io.Writer.Error!void { | ||
| 185 | try w.underlying_writer.splatByteAll(0, block_size * 2); | ||
| 186 | } | ||
| 187 | |||
| 188 | /// A struct that is exactly 512 bytes and matches tar file format. This is | ||
| 189 | /// intended to be used for outputting tar files; for parsing there is | ||
| 190 | /// `std.tar.Header`. | ||
| 191 | pub const Header = extern struct { | ||
| 192 | // This struct was originally copied from | ||
| 193 | // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT | ||
| 194 | // licensed. | ||
| 195 | // | ||
| 196 | // The name, linkname, magic, uname, and gname are null-terminated character | ||
| 197 | // strings. All other fields are zero-filled octal numbers in ASCII. Each | ||
| 198 | // numeric field of width w contains w minus 1 digits, and a null. | ||
| 199 | // Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html | ||
| 200 | // POSIX header: byte offset | ||
| 201 | name: [100]u8 = [_]u8{0} ** 100, // 0 | ||
| 202 | mode: [7:0]u8 = default_mode.file, // 100 | ||
| 203 | uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108 | ||
| 204 | gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116 | ||
| 205 | size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124 | ||
| 206 | mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136 | ||
| 207 | checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148 | ||
| 208 | typeflag: FileType = .regular, // 156 | ||
| 209 | linkname: [100]u8 = [_]u8{0} ** 100, // 157 | ||
| 210 | magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257 | ||
| 211 | version: [2]u8 = [_]u8{ '0', '0' }, // 263 | ||
| 212 | uname: [32]u8 = [_]u8{0} ** 32, // unused 265 | ||
| 213 | gname: [32]u8 = [_]u8{0} ** 32, // unused 297 | ||
| 214 | devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329 | ||
| 215 | devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337 | ||
| 216 | prefix: [155]u8 = [_]u8{0} ** 155, // 345 | ||
| 217 | pad: [12]u8 = [_]u8{0} ** 12, // unused 500 | ||
| 218 | |||
| 219 | pub const FileType = enum(u8) { | ||
| 220 | regular = '0', | ||
| 221 | symbolic_link = '2', | ||
| 222 | directory = '5', | ||
| 223 | gnu_long_name = 'L', | ||
| 224 | gnu_long_link = 'K', | ||
| 225 | }; | ||
| 226 | |||
| 227 | const default_mode = struct { | ||
| 228 | const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664 | ||
| 229 | const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775 | ||
| 230 | const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777 | ||
| 231 | const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000 | ||
| 232 | }; | ||
| 233 | |||
| 234 | pub fn init(typeflag: FileType) Header { | ||
| 235 | return .{ | ||
| 236 | .typeflag = typeflag, | ||
| 237 | .mode = switch (typeflag) { | ||
| 238 | .directory => default_mode.dir, | ||
| 239 | .symbolic_link => default_mode.sym_link, | ||
| 240 | .regular => default_mode.file, | ||
| 241 | else => default_mode.other, | ||
| 242 | }, | ||
| 243 | }; | ||
| 244 | } | ||
| 245 | |||
| 246 | pub fn setSize(w: *Header, size: u64) error{OctalOverflow}!void { | ||
| 247 | try octal(&w.size, size); | ||
| 248 | } | ||
| 249 | |||
| 250 | fn octal(buf: []u8, value: u64) error{OctalOverflow}!void { | ||
| 251 | var remainder: u64 = value; | ||
| 252 | var pos: usize = buf.len; | ||
| 253 | while (remainder > 0 and pos > 0) { | ||
| 254 | pos -= 1; | ||
| 255 | const c: u8 = @as(u8, @intCast(remainder % 8)) + '0'; | ||
| 256 | buf[pos] = c; | ||
| 257 | remainder /= 8; | ||
| 258 | if (pos == 0 and remainder > 0) return error.OctalOverflow; | ||
| 259 | } | ||
| 260 | } | ||
| 261 | |||
| 262 | pub fn setMode(w: *Header, mode: u32) error{OctalOverflow}!void { | ||
| 263 | try octal(&w.mode, mode); | ||
| 264 | } | ||
| 265 | |||
| 266 | // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time. | ||
| 267 | // mtime == 0 will use current time | ||
| 268 | pub fn setMtime(w: *Header, mtime: u64) error{OctalOverflow}!void { | ||
| 269 | try octal(&w.mtime, mtime); | ||
| 270 | } | ||
| 271 | |||
| 272 | pub fn updateChecksum(w: *Header) !void { | ||
| 273 | var checksum: usize = ' '; // other 7 w.checksum bytes are initialized to ' ' | ||
| 274 | for (std.mem.asBytes(w)) |val| | ||
| 275 | checksum += val; | ||
| 276 | try octal(&w.checksum, checksum); | ||
| 277 | } | ||
| 278 | |||
| 279 | pub fn write(h: *Header, bw: *std.Io.Writer) error{ OctalOverflow, WriteFailed }!void { | ||
| 280 | try h.updateChecksum(); | ||
| 281 | try bw.writeAll(std.mem.asBytes(h)); | ||
| 282 | } | ||
| 283 | |||
| 284 | pub fn setLinkname(w: *Header, link: []const u8) !void { | ||
| 285 | if (link.len > w.linkname.len) return error.NameTooLong; | ||
| 286 | @memcpy(w.linkname[0..link.len], link); | ||
| 287 | } | ||
| 288 | |||
| 289 | pub fn setPath(w: *Header, prefix: []const u8, sub_path: []const u8) !void { | ||
| 290 | const max_prefix = w.prefix.len; | ||
| 291 | const max_name = w.name.len; | ||
| 292 | const sep = std.fs.path.sep_posix; | ||
| 293 | |||
| 294 | if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix) | ||
| 295 | return error.NameTooLong; | ||
| 296 | |||
| 297 | // both fit into name | ||
| 298 | if (prefix.len > 0 and prefix.len + sub_path.len < max_name) { | ||
| 299 | @memcpy(w.name[0..prefix.len], prefix); | ||
| 300 | w.name[prefix.len] = sep; | ||
| 301 | @memcpy(w.name[prefix.len + 1 ..][0..sub_path.len], sub_path); | ||
| 302 | return; | ||
| 303 | } | ||
| 304 | |||
| 305 | // sub_path fits into name | ||
| 306 | // there is no prefix or prefix fits into prefix | ||
| 307 | if (sub_path.len <= max_name) { | ||
| 308 | @memcpy(w.name[0..sub_path.len], sub_path); | ||
| 309 | @memcpy(w.prefix[0..prefix.len], prefix); | ||
| 310 | return; | ||
| 311 | } | ||
| 312 | |||
| 313 | if (prefix.len > 0) { | ||
| 314 | @memcpy(w.prefix[0..prefix.len], prefix); | ||
| 315 | w.prefix[prefix.len] = sep; | ||
| 316 | } | ||
| 317 | const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0; | ||
| 318 | |||
| 319 | // add as much to prefix as you can, must split at / | ||
| 320 | const prefix_remaining = max_prefix - prefix_pos; | ||
| 321 | if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| { | ||
| 322 | @memcpy(w.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]); | ||
| 323 | if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong; | ||
| 324 | @memcpy(w.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]); | ||
| 325 | return; | ||
| 326 | } | ||
| 327 | |||
| 328 | return error.NameTooLong; | ||
| 329 | } | ||
| 330 | |||
| 331 | comptime { | ||
| 332 | assert(@sizeOf(Header) == 512); | ||
| 333 | } | ||
| 334 | |||
| 335 | test "setPath" { | ||
| 336 | const cases = [_]struct { | ||
| 337 | in: []const []const u8, | ||
| 338 | out: []const []const u8, | ||
| 339 | }{ | ||
| 340 | .{ | ||
| 341 | .in = &.{ "", "123456789" }, | ||
| 342 | .out = &.{ "", "123456789" }, | ||
| 343 | }, | ||
| 344 | // can fit into name | ||
| 345 | .{ | ||
| 346 | .in = &.{ "prefix", "sub_path" }, | ||
| 347 | .out = &.{ "", "prefix/sub_path" }, | ||
| 348 | }, | ||
| 349 | // no more both fits into name | ||
| 350 | .{ | ||
| 351 | .in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" }, | ||
| 352 | .out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" }, | ||
| 353 | }, | ||
| 354 | // put as much as you can into prefix the rest goes into name | ||
| 355 | .{ | ||
| 356 | .in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" }, | ||
| 357 | .out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" }, | ||
| 358 | }, | ||
| 359 | |||
| 360 | .{ | ||
| 361 | .in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" }, | ||
| 362 | .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" }, | ||
| 363 | }, | ||
| 364 | .{ | ||
| 365 | .in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" }, | ||
| 366 | .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" }, | ||
| 367 | }, | ||
| 368 | .{ | ||
| 369 | .in = &.{ "", "012345678/" ** 10 ++ "foo" }, | ||
| 370 | .out = &.{ "012345678/" ** 9 ++ "012345678", "foo" }, | ||
| 371 | }, | ||
| 372 | }; | ||
| 373 | |||
| 374 | for (cases) |case| { | ||
| 375 | var header = Header.init(.regular); | ||
| 376 | try header.setPath(case.in[0], case.in[1]); | ||
| 377 | try testing.expectEqualStrings(case.out[0], str(&header.prefix)); | ||
| 378 | try testing.expectEqualStrings(case.out[1], str(&header.name)); | ||
| 379 | } | ||
| 380 | |||
| 381 | const error_cases = [_]struct { | ||
| 382 | in: []const []const u8, | ||
| 383 | }{ | ||
| 384 | // basename can't fit into name (106 characters) | ||
| 385 | .{ .in = &.{ "zig", "test/cases/compile_errors/regression_test_2980_base_type_u32_is_not_type_checked_properly_when_assigning_a_value_within_a_struct.zig" } }, | ||
| 386 | // cant fit into 255 + sep | ||
| 387 | .{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } }, | ||
| 388 | // can fit but sub_path can't be split (there is no separator) | ||
| 389 | .{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } }, | ||
| 390 | .{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } }, | ||
| 391 | }; | ||
| 392 | |||
| 393 | for (error_cases) |case| { | ||
| 394 | var header = Header.init(.regular); | ||
| 395 | try testing.expectError( | ||
| 396 | error.NameTooLong, | ||
| 397 | header.setPath(case.in[0], case.in[1]), | ||
| 398 | ); | ||
| 399 | } | ||
| 400 | } | ||
| 401 | |||
| 402 | // Breaks string on first null character. | ||
| 403 | fn str(s: []const u8) []const u8 { | ||
| 404 | for (s, 0..) |c, i| { | ||
| 405 | if (c == 0) return s[0..i]; | ||
| 406 | } | ||
| 407 | return s; | ||
| 408 | } | ||
| 409 | }; | ||
| 410 | |||
| 411 | test { | ||
| 412 | _ = Header; | ||
| 413 | } | ||
| 414 | |||
| 415 | test "write files" { | ||
| 416 | const files = [_]struct { | ||
| 417 | path: []const u8, | ||
| 418 | content: []const u8, | ||
| 419 | }{ | ||
| 420 | .{ .path = "foo", .content = "bar" }, | ||
| 421 | .{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 }, | ||
| 422 | .{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 }, | ||
| 423 | .{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 }, | ||
| 424 | .{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 }, | ||
| 425 | .{ .path = "e123456789" ** 11, .content = "e" }, | ||
| 426 | }; | ||
| 427 | |||
| 428 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | ||
| 429 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | ||
| 430 | |||
| 431 | // with root | ||
| 432 | { | ||
| 433 | const root = "root"; | ||
| 434 | |||
| 435 | var output: std.Io.Writer.Allocating = .init(testing.allocator); | ||
| 436 | var w: Writer = .{ .underlying_writer = &output.writer }; | ||
| 437 | defer output.deinit(); | ||
| 438 | try w.setRoot(root); | ||
| 439 | for (files) |file| | ||
| 440 | try w.writeFileBytes(file.path, file.content, .{}); | ||
| 441 | |||
| 442 | var input: std.Io.Reader = .fixed(output.getWritten()); | ||
| 443 | var it: std.tar.Iterator = .init(&input, .{ | ||
| 444 | .file_name_buffer = &file_name_buffer, | ||
| 445 | .link_name_buffer = &link_name_buffer, | ||
| 446 | }); | ||
| 447 | |||
| 448 | // first entry is directory with prefix | ||
| 449 | { | ||
| 450 | const actual = (try it.next()).?; | ||
| 451 | try testing.expectEqualStrings(root, actual.name); | ||
| 452 | try testing.expectEqual(std.tar.FileKind.directory, actual.kind); | ||
| 453 | } | ||
| 454 | |||
| 455 | var i: usize = 0; | ||
| 456 | while (try it.next()) |actual| { | ||
| 457 | defer i += 1; | ||
| 458 | const expected = files[i]; | ||
| 459 | try testing.expectEqualStrings(root, actual.name[0..root.len]); | ||
| 460 | try testing.expectEqual('/', actual.name[root.len..][0]); | ||
| 461 | try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]); | ||
| 462 | |||
| 463 | var content: std.Io.Writer.Allocating = .init(testing.allocator); | ||
| 464 | defer content.deinit(); | ||
| 465 | try it.streamRemaining(actual, &content.writer); | ||
| 466 | try testing.expectEqualSlices(u8, expected.content, content.getWritten()); | ||
| 467 | } | ||
| 468 | } | ||
| 469 | // without root | ||
| 470 | { | ||
| 471 | var output: std.Io.Writer.Allocating = .init(testing.allocator); | ||
| 472 | var w: Writer = .{ .underlying_writer = &output.writer }; | ||
| 473 | defer output.deinit(); | ||
| 474 | for (files) |file| { | ||
| 475 | var content: std.Io.Reader = .fixed(file.content); | ||
| 476 | try w.writeFileStream(file.path, file.content.len, &content, .{}); | ||
| 477 | } | ||
| 478 | |||
| 479 | var input: std.Io.Reader = .fixed(output.getWritten()); | ||
| 480 | var it: std.tar.Iterator = .init(&input, .{ | ||
| 481 | .file_name_buffer = &file_name_buffer, | ||
| 482 | .link_name_buffer = &link_name_buffer, | ||
| 483 | }); | ||
| 484 | |||
| 485 | var i: usize = 0; | ||
| 486 | while (try it.next()) |actual| { | ||
| 487 | defer i += 1; | ||
| 488 | const expected = files[i]; | ||
| 489 | try testing.expectEqualStrings(expected.path, actual.name); | ||
| 490 | |||
| 491 | var content: std.Io.Writer.Allocating = .init(testing.allocator); | ||
| 492 | defer content.deinit(); | ||
| 493 | try it.streamRemaining(actual, &content.writer); | ||
| 494 | try testing.expectEqualSlices(u8, expected.content, content.getWritten()); | ||
| 495 | } | ||
| 496 | try w.finishPedantically(); | ||
| 497 | } | ||
| 498 | } | ||
lib/std/tar/test.zig+173-177| ... | @@ -18,31 +18,72 @@ const Case = struct { | ... | @@ -18,31 +18,72 @@ const Case = struct { |
| 18 | err: ?anyerror = null, // parsing should fail with this error | 18 | err: ?anyerror = null, // parsing should fail with this error |
| 19 | }; | 19 | }; |
| 20 | 20 | ||
| 21 | const cases = [_]Case{ | 21 | const gnu_case: Case = .{ |
| 22 | .{ | 22 | .data = @embedFile("testdata/gnu.tar"), |
| 23 | .data = @embedFile("testdata/gnu.tar"), | 23 | .files = &[_]Case.File{ |
| 24 | .files = &[_]Case.File{ | 24 | .{ |
| 25 | .{ | 25 | .name = "small.txt", |
| 26 | .name = "small.txt", | 26 | .size = 5, |
| 27 | .size = 5, | 27 | .mode = 0o640, |
| 28 | .mode = 0o640, | ||
| 29 | }, | ||
| 30 | .{ | ||
| 31 | .name = "small2.txt", | ||
| 32 | .size = 11, | ||
| 33 | .mode = 0o640, | ||
| 34 | }, | ||
| 35 | }, | 28 | }, |
| 36 | .chksums = &[_][]const u8{ | 29 | .{ |
| 37 | "e38b27eaccb4391bdec553a7f3ae6b2f", | 30 | .name = "small2.txt", |
| 38 | "c65bd2e50a56a2138bf1716f2fd56fe9", | 31 | .size = 11, |
| 32 | .mode = 0o640, | ||
| 33 | }, | ||
| 34 | }, | ||
| 35 | .chksums = &[_][]const u8{ | ||
| 36 | "e38b27eaccb4391bdec553a7f3ae6b2f", | ||
| 37 | "c65bd2e50a56a2138bf1716f2fd56fe9", | ||
| 38 | }, | ||
| 39 | }; | ||
| 40 | |||
| 41 | const gnu_multi_headers_case: Case = .{ | ||
| 42 | .data = @embedFile("testdata/gnu-multi-hdrs.tar"), | ||
| 43 | .files = &[_]Case.File{ | ||
| 44 | .{ | ||
| 45 | .name = "GNU2/GNU2/long-path-name", | ||
| 46 | .link_name = "GNU4/GNU4/long-linkpath-name", | ||
| 47 | .kind = .sym_link, | ||
| 39 | }, | 48 | }, |
| 40 | }, | 49 | }, |
| 41 | .{ | 50 | }; |
| 51 | |||
| 52 | const trailing_slash_case: Case = .{ | ||
| 53 | .data = @embedFile("testdata/trailing-slash.tar"), | ||
| 54 | .files = &[_]Case.File{ | ||
| 55 | .{ | ||
| 56 | .name = "123456789/" ** 30, | ||
| 57 | .kind = .directory, | ||
| 58 | }, | ||
| 59 | }, | ||
| 60 | }; | ||
| 61 | |||
| 62 | const writer_big_long_case: Case = .{ | ||
| 63 | // Size in gnu extended format, and name in pax attribute. | ||
| 64 | .data = @embedFile("testdata/writer-big-long.tar"), | ||
| 65 | .files = &[_]Case.File{ | ||
| 66 | .{ | ||
| 67 | .name = "longname/" ** 15 ++ "16gig.txt", | ||
| 68 | .size = 16 * 1024 * 1024 * 1024, | ||
| 69 | .mode = 0o644, | ||
| 70 | .truncated = true, | ||
| 71 | }, | ||
| 72 | }, | ||
| 73 | }; | ||
| 74 | |||
| 75 | const fuzz1_case: Case = .{ | ||
| 76 | .data = @embedFile("testdata/fuzz1.tar"), | ||
| 77 | .err = error.TarInsufficientBuffer, | ||
| 78 | }; | ||
| 79 | |||
| 80 | test "run test cases" { | ||
| 81 | try testCase(gnu_case); | ||
| 82 | try testCase(.{ | ||
| 42 | .data = @embedFile("testdata/sparse-formats.tar"), | 83 | .data = @embedFile("testdata/sparse-formats.tar"), |
| 43 | .err = error.TarUnsupportedHeader, | 84 | .err = error.TarUnsupportedHeader, |
| 44 | }, | 85 | }); |
| 45 | .{ | 86 | try testCase(.{ |
| 46 | .data = @embedFile("testdata/star.tar"), | 87 | .data = @embedFile("testdata/star.tar"), |
| 47 | .files = &[_]Case.File{ | 88 | .files = &[_]Case.File{ |
| 48 | .{ | 89 | .{ |
| ... | @@ -60,8 +101,8 @@ const cases = [_]Case{ | ... | @@ -60,8 +101,8 @@ const cases = [_]Case{ |
| 60 | "e38b27eaccb4391bdec553a7f3ae6b2f", | 101 | "e38b27eaccb4391bdec553a7f3ae6b2f", |
| 61 | "c65bd2e50a56a2138bf1716f2fd56fe9", | 102 | "c65bd2e50a56a2138bf1716f2fd56fe9", |
| 62 | }, | 103 | }, |
| 63 | }, | 104 | }); |
| 64 | .{ | 105 | try testCase(.{ |
| 65 | .data = @embedFile("testdata/v7.tar"), | 106 | .data = @embedFile("testdata/v7.tar"), |
| 66 | .files = &[_]Case.File{ | 107 | .files = &[_]Case.File{ |
| 67 | .{ | 108 | .{ |
| ... | @@ -79,8 +120,8 @@ const cases = [_]Case{ | ... | @@ -79,8 +120,8 @@ const cases = [_]Case{ |
| 79 | "e38b27eaccb4391bdec553a7f3ae6b2f", | 120 | "e38b27eaccb4391bdec553a7f3ae6b2f", |
| 80 | "c65bd2e50a56a2138bf1716f2fd56fe9", | 121 | "c65bd2e50a56a2138bf1716f2fd56fe9", |
| 81 | }, | 122 | }, |
| 82 | }, | 123 | }); |
| 83 | .{ | 124 | try testCase(.{ |
| 84 | .data = @embedFile("testdata/pax.tar"), | 125 | .data = @embedFile("testdata/pax.tar"), |
| 85 | .files = &[_]Case.File{ | 126 | .files = &[_]Case.File{ |
| 86 | .{ | 127 | .{ |
| ... | @@ -99,13 +140,13 @@ const cases = [_]Case{ | ... | @@ -99,13 +140,13 @@ const cases = [_]Case{ |
| 99 | .chksums = &[_][]const u8{ | 140 | .chksums = &[_][]const u8{ |
| 100 | "3c382e8f5b6631aa2db52643912ffd4a", | 141 | "3c382e8f5b6631aa2db52643912ffd4a", |
| 101 | }, | 142 | }, |
| 102 | }, | 143 | }); |
| 103 | .{ | 144 | try testCase(.{ |
| 104 | // pax attribute don't end with \n | 145 | // pax attribute don't end with \n |
| 105 | .data = @embedFile("testdata/pax-bad-hdr-file.tar"), | 146 | .data = @embedFile("testdata/pax-bad-hdr-file.tar"), |
| 106 | .err = error.PaxInvalidAttributeEnd, | 147 | .err = error.PaxInvalidAttributeEnd, |
| 107 | }, | 148 | }); |
| 108 | .{ | 149 | try testCase(.{ |
| 109 | // size is in pax attribute | 150 | // size is in pax attribute |
| 110 | .data = @embedFile("testdata/pax-pos-size-file.tar"), | 151 | .data = @embedFile("testdata/pax-pos-size-file.tar"), |
| 111 | .files = &[_]Case.File{ | 152 | .files = &[_]Case.File{ |
| ... | @@ -119,8 +160,8 @@ const cases = [_]Case{ | ... | @@ -119,8 +160,8 @@ const cases = [_]Case{ |
| 119 | .chksums = &[_][]const u8{ | 160 | .chksums = &[_][]const u8{ |
| 120 | "0afb597b283fe61b5d4879669a350556", | 161 | "0afb597b283fe61b5d4879669a350556", |
| 121 | }, | 162 | }, |
| 122 | }, | 163 | }); |
| 123 | .{ | 164 | try testCase(.{ |
| 124 | // has pax records which we are not interested in | 165 | // has pax records which we are not interested in |
| 125 | .data = @embedFile("testdata/pax-records.tar"), | 166 | .data = @embedFile("testdata/pax-records.tar"), |
| 126 | .files = &[_]Case.File{ | 167 | .files = &[_]Case.File{ |
| ... | @@ -128,8 +169,8 @@ const cases = [_]Case{ | ... | @@ -128,8 +169,8 @@ const cases = [_]Case{ |
| 128 | .name = "file", | 169 | .name = "file", |
| 129 | }, | 170 | }, |
| 130 | }, | 171 | }, |
| 131 | }, | 172 | }); |
| 132 | .{ | 173 | try testCase(.{ |
| 133 | // has global records which we are ignoring | 174 | // has global records which we are ignoring |
| 134 | .data = @embedFile("testdata/pax-global-records.tar"), | 175 | .data = @embedFile("testdata/pax-global-records.tar"), |
| 135 | .files = &[_]Case.File{ | 176 | .files = &[_]Case.File{ |
| ... | @@ -146,8 +187,8 @@ const cases = [_]Case{ | ... | @@ -146,8 +187,8 @@ const cases = [_]Case{ |
| 146 | .name = "file4", | 187 | .name = "file4", |
| 147 | }, | 188 | }, |
| 148 | }, | 189 | }, |
| 149 | }, | 190 | }); |
| 150 | .{ | 191 | try testCase(.{ |
| 151 | .data = @embedFile("testdata/nil-uid.tar"), | 192 | .data = @embedFile("testdata/nil-uid.tar"), |
| 152 | .files = &[_]Case.File{ | 193 | .files = &[_]Case.File{ |
| 153 | .{ | 194 | .{ |
| ... | @@ -160,8 +201,8 @@ const cases = [_]Case{ | ... | @@ -160,8 +201,8 @@ const cases = [_]Case{ |
| 160 | .chksums = &[_][]const u8{ | 201 | .chksums = &[_][]const u8{ |
| 161 | "08d504674115e77a67244beac19668f5", | 202 | "08d504674115e77a67244beac19668f5", |
| 162 | }, | 203 | }, |
| 163 | }, | 204 | }); |
| 164 | .{ | 205 | try testCase(.{ |
| 165 | // has xattrs and pax records which we are ignoring | 206 | // has xattrs and pax records which we are ignoring |
| 166 | .data = @embedFile("testdata/xattrs.tar"), | 207 | .data = @embedFile("testdata/xattrs.tar"), |
| 167 | .files = &[_]Case.File{ | 208 | .files = &[_]Case.File{ |
| ... | @@ -182,23 +223,14 @@ const cases = [_]Case{ | ... | @@ -182,23 +223,14 @@ const cases = [_]Case{ |
| 182 | "e38b27eaccb4391bdec553a7f3ae6b2f", | 223 | "e38b27eaccb4391bdec553a7f3ae6b2f", |
| 183 | "c65bd2e50a56a2138bf1716f2fd56fe9", | 224 | "c65bd2e50a56a2138bf1716f2fd56fe9", |
| 184 | }, | 225 | }, |
| 185 | }, | 226 | }); |
| 186 | .{ | 227 | try testCase(gnu_multi_headers_case); |
| 187 | .data = @embedFile("testdata/gnu-multi-hdrs.tar"), | 228 | try testCase(.{ |
| 188 | .files = &[_]Case.File{ | ||
| 189 | .{ | ||
| 190 | .name = "GNU2/GNU2/long-path-name", | ||
| 191 | .link_name = "GNU4/GNU4/long-linkpath-name", | ||
| 192 | .kind = .sym_link, | ||
| 193 | }, | ||
| 194 | }, | ||
| 195 | }, | ||
| 196 | .{ | ||
| 197 | // has gnu type D (directory) and S (sparse) blocks | 229 | // has gnu type D (directory) and S (sparse) blocks |
| 198 | .data = @embedFile("testdata/gnu-incremental.tar"), | 230 | .data = @embedFile("testdata/gnu-incremental.tar"), |
| 199 | .err = error.TarUnsupportedHeader, | 231 | .err = error.TarUnsupportedHeader, |
| 200 | }, | 232 | }); |
| 201 | .{ | 233 | try testCase(.{ |
| 202 | // should use values only from last pax header | 234 | // should use values only from last pax header |
| 203 | .data = @embedFile("testdata/pax-multi-hdrs.tar"), | 235 | .data = @embedFile("testdata/pax-multi-hdrs.tar"), |
| 204 | .files = &[_]Case.File{ | 236 | .files = &[_]Case.File{ |
| ... | @@ -208,8 +240,8 @@ const cases = [_]Case{ | ... | @@ -208,8 +240,8 @@ const cases = [_]Case{ |
| 208 | .kind = .sym_link, | 240 | .kind = .sym_link, |
| 209 | }, | 241 | }, |
| 210 | }, | 242 | }, |
| 211 | }, | 243 | }); |
| 212 | .{ | 244 | try testCase(.{ |
| 213 | .data = @embedFile("testdata/gnu-long-nul.tar"), | 245 | .data = @embedFile("testdata/gnu-long-nul.tar"), |
| 214 | .files = &[_]Case.File{ | 246 | .files = &[_]Case.File{ |
| 215 | .{ | 247 | .{ |
| ... | @@ -217,8 +249,8 @@ const cases = [_]Case{ | ... | @@ -217,8 +249,8 @@ const cases = [_]Case{ |
| 217 | .mode = 0o644, | 249 | .mode = 0o644, |
| 218 | }, | 250 | }, |
| 219 | }, | 251 | }, |
| 220 | }, | 252 | }); |
| 221 | .{ | 253 | try testCase(.{ |
| 222 | .data = @embedFile("testdata/gnu-utf8.tar"), | 254 | .data = @embedFile("testdata/gnu-utf8.tar"), |
| 223 | .files = &[_]Case.File{ | 255 | .files = &[_]Case.File{ |
| 224 | .{ | 256 | .{ |
| ... | @@ -226,8 +258,8 @@ const cases = [_]Case{ | ... | @@ -226,8 +258,8 @@ const cases = [_]Case{ |
| 226 | .mode = 0o644, | 258 | .mode = 0o644, |
| 227 | }, | 259 | }, |
| 228 | }, | 260 | }, |
| 229 | }, | 261 | }); |
| 230 | .{ | 262 | try testCase(.{ |
| 231 | .data = @embedFile("testdata/gnu-not-utf8.tar"), | 263 | .data = @embedFile("testdata/gnu-not-utf8.tar"), |
| 232 | .files = &[_]Case.File{ | 264 | .files = &[_]Case.File{ |
| 233 | .{ | 265 | .{ |
| ... | @@ -235,33 +267,33 @@ const cases = [_]Case{ | ... | @@ -235,33 +267,33 @@ const cases = [_]Case{ |
| 235 | .mode = 0o644, | 267 | .mode = 0o644, |
| 236 | }, | 268 | }, |
| 237 | }, | 269 | }, |
| 238 | }, | 270 | }); |
| 239 | .{ | 271 | try testCase(.{ |
| 240 | // null in pax key | 272 | // null in pax key |
| 241 | .data = @embedFile("testdata/pax-nul-xattrs.tar"), | 273 | .data = @embedFile("testdata/pax-nul-xattrs.tar"), |
| 242 | .err = error.PaxNullInKeyword, | 274 | .err = error.PaxNullInKeyword, |
| 243 | }, | 275 | }); |
| 244 | .{ | 276 | try testCase(.{ |
| 245 | .data = @embedFile("testdata/pax-nul-path.tar"), | 277 | .data = @embedFile("testdata/pax-nul-path.tar"), |
| 246 | .err = error.PaxNullInValue, | 278 | .err = error.PaxNullInValue, |
| 247 | }, | 279 | }); |
| 248 | .{ | 280 | try testCase(.{ |
| 249 | .data = @embedFile("testdata/neg-size.tar"), | 281 | .data = @embedFile("testdata/neg-size.tar"), |
| 250 | .err = error.TarHeader, | 282 | .err = error.TarHeader, |
| 251 | }, | 283 | }); |
| 252 | .{ | 284 | try testCase(.{ |
| 253 | .data = @embedFile("testdata/issue10968.tar"), | 285 | .data = @embedFile("testdata/issue10968.tar"), |
| 254 | .err = error.TarHeader, | 286 | .err = error.TarHeader, |
| 255 | }, | 287 | }); |
| 256 | .{ | 288 | try testCase(.{ |
| 257 | .data = @embedFile("testdata/issue11169.tar"), | 289 | .data = @embedFile("testdata/issue11169.tar"), |
| 258 | .err = error.TarHeader, | 290 | .err = error.TarHeader, |
| 259 | }, | 291 | }); |
| 260 | .{ | 292 | try testCase(.{ |
| 261 | .data = @embedFile("testdata/issue12435.tar"), | 293 | .data = @embedFile("testdata/issue12435.tar"), |
| 262 | .err = error.TarHeaderChksum, | 294 | .err = error.TarHeaderChksum, |
| 263 | }, | 295 | }); |
| 264 | .{ | 296 | try testCase(.{ |
| 265 | // has magic with space at end instead of null | 297 | // has magic with space at end instead of null |
| 266 | .data = @embedFile("testdata/invalid-go17.tar"), | 298 | .data = @embedFile("testdata/invalid-go17.tar"), |
| 267 | .files = &[_]Case.File{ | 299 | .files = &[_]Case.File{ |
| ... | @@ -269,8 +301,8 @@ const cases = [_]Case{ | ... | @@ -269,8 +301,8 @@ const cases = [_]Case{ |
| 269 | .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo", | 301 | .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo", |
| 270 | }, | 302 | }, |
| 271 | }, | 303 | }, |
| 272 | }, | 304 | }); |
| 273 | .{ | 305 | try testCase(.{ |
| 274 | .data = @embedFile("testdata/ustar-file-devs.tar"), | 306 | .data = @embedFile("testdata/ustar-file-devs.tar"), |
| 275 | .files = &[_]Case.File{ | 307 | .files = &[_]Case.File{ |
| 276 | .{ | 308 | .{ |
| ... | @@ -278,17 +310,9 @@ const cases = [_]Case{ | ... | @@ -278,17 +310,9 @@ const cases = [_]Case{ |
| 278 | .mode = 0o644, | 310 | .mode = 0o644, |
| 279 | }, | 311 | }, |
| 280 | }, | 312 | }, |
| 281 | }, | 313 | }); |
| 282 | .{ | 314 | try testCase(trailing_slash_case); |
| 283 | .data = @embedFile("testdata/trailing-slash.tar"), | 315 | try testCase(.{ |
| 284 | .files = &[_]Case.File{ | ||
| 285 | .{ | ||
| 286 | .name = "123456789/" ** 30, | ||
| 287 | .kind = .directory, | ||
| 288 | }, | ||
| 289 | }, | ||
| 290 | }, | ||
| 291 | .{ | ||
| 292 | // Has size in gnu extended format. To represent size bigger than 8 GB. | 316 | // Has size in gnu extended format. To represent size bigger than 8 GB. |
| 293 | .data = @embedFile("testdata/writer-big.tar"), | 317 | .data = @embedFile("testdata/writer-big.tar"), |
| 294 | .files = &[_]Case.File{ | 318 | .files = &[_]Case.File{ |
| ... | @@ -299,120 +323,92 @@ const cases = [_]Case{ | ... | @@ -299,120 +323,92 @@ const cases = [_]Case{ |
| 299 | .mode = 0o640, | 323 | .mode = 0o640, |
| 300 | }, | 324 | }, |
| 301 | }, | 325 | }, |
| 302 | }, | 326 | }); |
| 303 | .{ | 327 | try testCase(writer_big_long_case); |
| 304 | // Size in gnu extended format, and name in pax attribute. | 328 | try testCase(fuzz1_case); |
| 305 | .data = @embedFile("testdata/writer-big-long.tar"), | 329 | try testCase(.{ |
| 306 | .files = &[_]Case.File{ | ||
| 307 | .{ | ||
| 308 | .name = "longname/" ** 15 ++ "16gig.txt", | ||
| 309 | .size = 16 * 1024 * 1024 * 1024, | ||
| 310 | .mode = 0o644, | ||
| 311 | .truncated = true, | ||
| 312 | }, | ||
| 313 | }, | ||
| 314 | }, | ||
| 315 | .{ | ||
| 316 | .data = @embedFile("testdata/fuzz1.tar"), | ||
| 317 | .err = error.TarInsufficientBuffer, | ||
| 318 | }, | ||
| 319 | .{ | ||
| 320 | .data = @embedFile("testdata/fuzz2.tar"), | 330 | .data = @embedFile("testdata/fuzz2.tar"), |
| 321 | .err = error.PaxSizeAttrOverflow, | 331 | .err = error.PaxSizeAttrOverflow, |
| 322 | }, | 332 | }); |
| 323 | }; | 333 | } |
| 324 | |||
| 325 | // used in test to calculate file chksum | ||
| 326 | const Md5Writer = struct { | ||
| 327 | h: std.crypto.hash.Md5 = std.crypto.hash.Md5.init(.{}), | ||
| 328 | |||
| 329 | pub fn writeAll(self: *Md5Writer, buf: []const u8) !void { | ||
| 330 | self.h.update(buf); | ||
| 331 | } | ||
| 332 | |||
| 333 | pub fn writeByte(self: *Md5Writer, byte: u8) !void { | ||
| 334 | self.h.update(&[_]u8{byte}); | ||
| 335 | } | ||
| 336 | |||
| 337 | pub fn chksum(self: *Md5Writer) [32]u8 { | ||
| 338 | var s = [_]u8{0} ** 16; | ||
| 339 | self.h.final(&s); | ||
| 340 | return std.fmt.bytesToHex(s, .lower); | ||
| 341 | } | ||
| 342 | }; | ||
| 343 | 334 | ||
| 344 | test "run test cases" { | 335 | fn testCase(case: Case) !void { |
| 345 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | 336 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; |
| 346 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | 337 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; |
| 347 | 338 | ||
| 348 | for (cases) |case| { | 339 | var br: std.io.Reader = .fixed(case.data); |
| 349 | var fsb = std.io.fixedBufferStream(case.data); | 340 | var it: tar.Iterator = .init(&br, .{ |
| 350 | var iter = tar.iterator(fsb.reader(), .{ | 341 | .file_name_buffer = &file_name_buffer, |
| 351 | .file_name_buffer = &file_name_buffer, | 342 | .link_name_buffer = &link_name_buffer, |
| 352 | .link_name_buffer = &link_name_buffer, | 343 | }); |
| 353 | }); | 344 | var i: usize = 0; |
| 354 | var i: usize = 0; | 345 | while (it.next() catch |err| { |
| 355 | while (iter.next() catch |err| { | 346 | if (case.err) |e| { |
| 356 | if (case.err) |e| { | 347 | try testing.expectEqual(e, err); |
| 357 | try testing.expectEqual(e, err); | 348 | return; |
| 358 | continue; | 349 | } else { |
| 359 | } else { | 350 | return err; |
| 360 | return err; | 351 | } |
| 361 | } | 352 | }) |actual| : (i += 1) { |
| 362 | }) |actual| : (i += 1) { | 353 | const expected = case.files[i]; |
| 363 | const expected = case.files[i]; | 354 | try testing.expectEqualStrings(expected.name, actual.name); |
| 364 | try testing.expectEqualStrings(expected.name, actual.name); | 355 | try testing.expectEqual(expected.size, actual.size); |
| 365 | try testing.expectEqual(expected.size, actual.size); | 356 | try testing.expectEqual(expected.kind, actual.kind); |
| 366 | try testing.expectEqual(expected.kind, actual.kind); | 357 | try testing.expectEqual(expected.mode, actual.mode); |
| 367 | try testing.expectEqual(expected.mode, actual.mode); | 358 | try testing.expectEqualStrings(expected.link_name, actual.link_name); |
| 368 | try testing.expectEqualStrings(expected.link_name, actual.link_name); | ||
| 369 | 359 | ||
| 370 | if (case.chksums.len > i) { | 360 | if (case.chksums.len > i) { |
| 371 | var md5writer = Md5Writer{}; | 361 | var aw: std.Io.Writer.Allocating = .init(std.testing.allocator); |
| 372 | try actual.writeAll(&md5writer); | 362 | defer aw.deinit(); |
| 373 | const chksum = md5writer.chksum(); | 363 | try it.streamRemaining(actual, &aw.writer); |
| 374 | try testing.expectEqualStrings(case.chksums[i], &chksum); | 364 | const chksum = std.fmt.bytesToHex(std.crypto.hash.Md5.hashResult(aw.getWritten()), .lower); |
| 375 | } else { | 365 | try testing.expectEqualStrings(case.chksums[i], &chksum); |
| 376 | if (expected.truncated) { | 366 | } else { |
| 377 | iter.unread_file_bytes = 0; | 367 | if (expected.truncated) { |
| 378 | } | 368 | it.unread_file_bytes = 0; |
| 379 | } | 369 | } |
| 380 | } | 370 | } |
| 381 | try testing.expectEqual(case.files.len, i); | ||
| 382 | } | 371 | } |
| 372 | try testing.expectEqual(case.files.len, i); | ||
| 383 | } | 373 | } |
| 384 | 374 | ||
| 385 | test "pax/gnu long names with small buffer" { | 375 | test "pax/gnu long names with small buffer" { |
| 376 | try testLongNameCase(gnu_multi_headers_case); | ||
| 377 | try testLongNameCase(trailing_slash_case); | ||
| 378 | try testLongNameCase(.{ | ||
| 379 | .data = @embedFile("testdata/fuzz1.tar"), | ||
| 380 | .err = error.TarInsufficientBuffer, | ||
| 381 | }); | ||
| 382 | } | ||
| 383 | |||
| 384 | fn testLongNameCase(case: Case) !void { | ||
| 386 | // should fail with insufficient buffer error | 385 | // should fail with insufficient buffer error |
| 387 | 386 | ||
| 388 | var min_file_name_buffer: [256]u8 = undefined; | 387 | var min_file_name_buffer: [256]u8 = undefined; |
| 389 | var min_link_name_buffer: [100]u8 = undefined; | 388 | var min_link_name_buffer: [100]u8 = undefined; |
| 390 | const long_name_cases = [_]Case{ cases[11], cases[25], cases[28] }; | ||
| 391 | 389 | ||
| 392 | for (long_name_cases) |case| { | 390 | var br: std.io.Reader = .fixed(case.data); |
| 393 | var fsb = std.io.fixedBufferStream(case.data); | 391 | var iter: tar.Iterator = .init(&br, .{ |
| 394 | var iter = tar.iterator(fsb.reader(), .{ | 392 | .file_name_buffer = &min_file_name_buffer, |
| 395 | .file_name_buffer = &min_file_name_buffer, | 393 | .link_name_buffer = &min_link_name_buffer, |
| 396 | .link_name_buffer = &min_link_name_buffer, | 394 | }); |
| 397 | }); | ||
| 398 | 395 | ||
| 399 | var iter_err: ?anyerror = null; | 396 | var iter_err: ?anyerror = null; |
| 400 | while (iter.next() catch |err| brk: { | 397 | while (iter.next() catch |err| brk: { |
| 401 | iter_err = err; | 398 | iter_err = err; |
| 402 | break :brk null; | 399 | break :brk null; |
| 403 | }) |_| {} | 400 | }) |_| {} |
| 404 | 401 | ||
| 405 | try testing.expect(iter_err != null); | 402 | try testing.expect(iter_err != null); |
| 406 | try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?); | 403 | try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?); |
| 407 | } | ||
| 408 | } | 404 | } |
| 409 | 405 | ||
| 410 | test "insufficient buffer in Header name filed" { | 406 | test "insufficient buffer in Header name filed" { |
| 411 | var min_file_name_buffer: [9]u8 = undefined; | 407 | var min_file_name_buffer: [9]u8 = undefined; |
| 412 | var min_link_name_buffer: [100]u8 = undefined; | 408 | var min_link_name_buffer: [100]u8 = undefined; |
| 413 | 409 | ||
| 414 | var fsb = std.io.fixedBufferStream(cases[0].data); | 410 | var br: std.io.Reader = .fixed(gnu_case.data); |
| 415 | var iter = tar.iterator(fsb.reader(), .{ | 411 | var iter: tar.Iterator = .init(&br, .{ |
| 416 | .file_name_buffer = &min_file_name_buffer, | 412 | .file_name_buffer = &min_file_name_buffer, |
| 417 | .link_name_buffer = &min_link_name_buffer, | 413 | .link_name_buffer = &min_link_name_buffer, |
| 418 | }); | 414 | }); |
| ... | @@ -466,21 +462,21 @@ test "should not overwrite existing file" { | ... | @@ -466,21 +462,21 @@ test "should not overwrite existing file" { |
| 466 | // This ensures that file is not overwritten. | 462 | // This ensures that file is not overwritten. |
| 467 | // | 463 | // |
| 468 | const data = @embedFile("testdata/overwrite_file.tar"); | 464 | const data = @embedFile("testdata/overwrite_file.tar"); |
| 469 | var fsb = std.io.fixedBufferStream(data); | 465 | var r: std.io.Reader = .fixed(data); |
| 470 | 466 | ||
| 471 | // Unpack with strip_components = 1 should fail | 467 | // Unpack with strip_components = 1 should fail |
| 472 | var root = std.testing.tmpDir(.{}); | 468 | var root = std.testing.tmpDir(.{}); |
| 473 | defer root.cleanup(); | 469 | defer root.cleanup(); |
| 474 | try testing.expectError( | 470 | try testing.expectError( |
| 475 | error.PathAlreadyExists, | 471 | error.PathAlreadyExists, |
| 476 | tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }), | 472 | tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }), |
| 477 | ); | 473 | ); |
| 478 | 474 | ||
| 479 | // Unpack with strip_components = 0 should pass | 475 | // Unpack with strip_components = 0 should pass |
| 480 | fsb.reset(); | 476 | r = .fixed(data); |
| 481 | var root2 = std.testing.tmpDir(.{}); | 477 | var root2 = std.testing.tmpDir(.{}); |
| 482 | defer root2.cleanup(); | 478 | defer root2.cleanup(); |
| 483 | try tar.pipeToFileSystem(root2.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 0 }); | 479 | try tar.pipeToFileSystem(root2.dir, &r, .{ .mode_mode = .ignore, .strip_components = 0 }); |
| 484 | } | 480 | } |
| 485 | 481 | ||
| 486 | test "case sensitivity" { | 482 | test "case sensitivity" { |
| ... | @@ -494,12 +490,12 @@ test "case sensitivity" { | ... | @@ -494,12 +490,12 @@ test "case sensitivity" { |
| 494 | // 18089/alacritty/Darkermatrix.yml | 490 | // 18089/alacritty/Darkermatrix.yml |
| 495 | // | 491 | // |
| 496 | const data = @embedFile("testdata/18089.tar"); | 492 | const data = @embedFile("testdata/18089.tar"); |
| 497 | var fsb = std.io.fixedBufferStream(data); | 493 | var r: std.io.Reader = .fixed(data); |
| 498 | 494 | ||
| 499 | var root = std.testing.tmpDir(.{}); | 495 | var root = std.testing.tmpDir(.{}); |
| 500 | defer root.cleanup(); | 496 | defer root.cleanup(); |
| 501 | 497 | ||
| 502 | tar.pipeToFileSystem(root.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| { | 498 | tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| { |
| 503 | // on case insensitive fs we fail on overwrite existing file | 499 | // on case insensitive fs we fail on overwrite existing file |
| 504 | try testing.expectEqual(error.PathAlreadyExists, err); | 500 | try testing.expectEqual(error.PathAlreadyExists, err); |
| 505 | return; | 501 | return; |
lib/std/tar/writer.zig deleted-497| ... | @@ -1,497 +0,0 @@ | ||
| 1 | const std = @import("std"); | ||
| 2 | const assert = std.debug.assert; | ||
| 3 | const testing = std.testing; | ||
| 4 | |||
| 5 | /// Creates tar Writer which will write tar content to the `underlying_writer`. | ||
| 6 | /// Use setRoot to nest all following entries under single root. If file don't | ||
| 7 | /// fit into posix header (name+prefix: 100+155 bytes) gnu extented header will | ||
| 8 | /// be used for long names. Options enables setting file premission mode and | ||
| 9 | /// mtime. Default is to use current time for mtime and 0o664 for file mode. | ||
| 10 | pub fn writer(underlying_writer: anytype) Writer(@TypeOf(underlying_writer)) { | ||
| 11 | return .{ .underlying_writer = underlying_writer }; | ||
| 12 | } | ||
| 13 | |||
| 14 | pub fn Writer(comptime WriterType: type) type { | ||
| 15 | return struct { | ||
| 16 | const block_size = @sizeOf(Header); | ||
| 17 | const empty_block: [block_size]u8 = [_]u8{0} ** block_size; | ||
| 18 | |||
| 19 | /// Options for writing file/dir/link. If left empty 0o664 is used for | ||
| 20 | /// file mode and current time for mtime. | ||
| 21 | pub const Options = struct { | ||
| 22 | /// File system permission mode. | ||
| 23 | mode: u32 = 0, | ||
| 24 | /// File system modification time. | ||
| 25 | mtime: u64 = 0, | ||
| 26 | }; | ||
| 27 | const Self = @This(); | ||
| 28 | |||
| 29 | underlying_writer: WriterType, | ||
| 30 | prefix: []const u8 = "", | ||
| 31 | mtime_now: u64 = 0, | ||
| 32 | |||
| 33 | /// Sets prefix for all other write* method paths. | ||
| 34 | pub fn setRoot(self: *Self, root: []const u8) !void { | ||
| 35 | if (root.len > 0) | ||
| 36 | try self.writeDir(root, .{}); | ||
| 37 | |||
| 38 | self.prefix = root; | ||
| 39 | } | ||
| 40 | |||
| 41 | /// Writes directory. | ||
| 42 | pub fn writeDir(self: *Self, sub_path: []const u8, opt: Options) !void { | ||
| 43 | try self.writeHeader(.directory, sub_path, "", 0, opt); | ||
| 44 | } | ||
| 45 | |||
| 46 | /// Writes file system file. | ||
| 47 | pub fn writeFile(self: *Self, sub_path: []const u8, file: std.fs.File) !void { | ||
| 48 | const stat = try file.stat(); | ||
| 49 | const mtime: u64 = @intCast(@divFloor(stat.mtime, std.time.ns_per_s)); | ||
| 50 | |||
| 51 | var header = Header{}; | ||
| 52 | try self.setPath(&header, sub_path); | ||
| 53 | try header.setSize(stat.size); | ||
| 54 | try header.setMtime(mtime); | ||
| 55 | try header.write(self.underlying_writer); | ||
| 56 | |||
| 57 | try self.underlying_writer.writeFile(file); | ||
| 58 | try self.writePadding(stat.size); | ||
| 59 | } | ||
| 60 | |||
| 61 | /// Writes file reading file content from `reader`. Number of bytes in | ||
| 62 | /// reader must be equal to `size`. | ||
| 63 | pub fn writeFileStream(self: *Self, sub_path: []const u8, size: usize, reader: anytype, opt: Options) !void { | ||
| 64 | try self.writeHeader(.regular, sub_path, "", @intCast(size), opt); | ||
| 65 | |||
| 66 | var counting_reader = std.io.countingReader(reader); | ||
| 67 | var fifo = std.fifo.LinearFifo(u8, .{ .Static = 4096 }).init(); | ||
| 68 | try fifo.pump(counting_reader.reader(), self.underlying_writer); | ||
| 69 | if (counting_reader.bytes_read != size) return error.WrongReaderSize; | ||
| 70 | try self.writePadding(size); | ||
| 71 | } | ||
| 72 | |||
| 73 | /// Writes file using bytes buffer `content` for size and file content. | ||
| 74 | pub fn writeFileBytes(self: *Self, sub_path: []const u8, content: []const u8, opt: Options) !void { | ||
| 75 | try self.writeHeader(.regular, sub_path, "", @intCast(content.len), opt); | ||
| 76 | try self.underlying_writer.writeAll(content); | ||
| 77 | try self.writePadding(content.len); | ||
| 78 | } | ||
| 79 | |||
| 80 | /// Writes symlink. | ||
| 81 | pub fn writeLink(self: *Self, sub_path: []const u8, link_name: []const u8, opt: Options) !void { | ||
| 82 | try self.writeHeader(.symbolic_link, sub_path, link_name, 0, opt); | ||
| 83 | } | ||
| 84 | |||
| 85 | /// Writes fs.Dir.WalkerEntry. Uses `mtime` from file system entry and | ||
| 86 | /// default for entry mode . | ||
| 87 | pub fn writeEntry(self: *Self, entry: std.fs.Dir.Walker.Entry) !void { | ||
| 88 | switch (entry.kind) { | ||
| 89 | .directory => { | ||
| 90 | try self.writeDir(entry.path, .{ .mtime = try entryMtime(entry) }); | ||
| 91 | }, | ||
| 92 | .file => { | ||
| 93 | var file = try entry.dir.openFile(entry.basename, .{}); | ||
| 94 | defer file.close(); | ||
| 95 | try self.writeFile(entry.path, file); | ||
| 96 | }, | ||
| 97 | .sym_link => { | ||
| 98 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | ||
| 99 | const link_name = try entry.dir.readLink(entry.basename, &link_name_buffer); | ||
| 100 | try self.writeLink(entry.path, link_name, .{ .mtime = try entryMtime(entry) }); | ||
| 101 | }, | ||
| 102 | else => { | ||
| 103 | return error.UnsupportedWalkerEntryKind; | ||
| 104 | }, | ||
| 105 | } | ||
| 106 | } | ||
| 107 | |||
| 108 | fn writeHeader( | ||
| 109 | self: *Self, | ||
| 110 | typeflag: Header.FileType, | ||
| 111 | sub_path: []const u8, | ||
| 112 | link_name: []const u8, | ||
| 113 | size: u64, | ||
| 114 | opt: Options, | ||
| 115 | ) !void { | ||
| 116 | var header = Header.init(typeflag); | ||
| 117 | try self.setPath(&header, sub_path); | ||
| 118 | try header.setSize(size); | ||
| 119 | try header.setMtime(if (opt.mtime != 0) opt.mtime else self.mtimeNow()); | ||
| 120 | if (opt.mode != 0) | ||
| 121 | try header.setMode(opt.mode); | ||
| 122 | if (typeflag == .symbolic_link) | ||
| 123 | header.setLinkname(link_name) catch |err| switch (err) { | ||
| 124 | error.NameTooLong => try self.writeExtendedHeader(.gnu_long_link, &.{link_name}), | ||
| 125 | else => return err, | ||
| 126 | }; | ||
| 127 | try header.write(self.underlying_writer); | ||
| 128 | } | ||
| 129 | |||
| 130 | fn mtimeNow(self: *Self) u64 { | ||
| 131 | if (self.mtime_now == 0) | ||
| 132 | self.mtime_now = @intCast(std.time.timestamp()); | ||
| 133 | return self.mtime_now; | ||
| 134 | } | ||
| 135 | |||
| 136 | fn entryMtime(entry: std.fs.Dir.Walker.Entry) !u64 { | ||
| 137 | const stat = try entry.dir.statFile(entry.basename); | ||
| 138 | return @intCast(@divFloor(stat.mtime, std.time.ns_per_s)); | ||
| 139 | } | ||
| 140 | |||
| 141 | /// Writes path in posix header, if don't fit (in name+prefix; 100+155 | ||
| 142 | /// bytes) writes it in gnu extended header. | ||
| 143 | fn setPath(self: *Self, header: *Header, sub_path: []const u8) !void { | ||
| 144 | header.setPath(self.prefix, sub_path) catch |err| switch (err) { | ||
| 145 | error.NameTooLong => { | ||
| 146 | // write extended header | ||
| 147 | const buffers: []const []const u8 = if (self.prefix.len == 0) | ||
| 148 | &.{sub_path} | ||
| 149 | else | ||
| 150 | &.{ self.prefix, "/", sub_path }; | ||
| 151 | try self.writeExtendedHeader(.gnu_long_name, buffers); | ||
| 152 | }, | ||
| 153 | else => return err, | ||
| 154 | }; | ||
| 155 | } | ||
| 156 | |||
| 157 | /// Writes gnu extended header: gnu_long_name or gnu_long_link. | ||
| 158 | fn writeExtendedHeader(self: *Self, typeflag: Header.FileType, buffers: []const []const u8) !void { | ||
| 159 | var len: usize = 0; | ||
| 160 | for (buffers) |buf| | ||
| 161 | len += buf.len; | ||
| 162 | |||
| 163 | var header = Header.init(typeflag); | ||
| 164 | try header.setSize(len); | ||
| 165 | try header.write(self.underlying_writer); | ||
| 166 | for (buffers) |buf| | ||
| 167 | try self.underlying_writer.writeAll(buf); | ||
| 168 | try self.writePadding(len); | ||
| 169 | } | ||
| 170 | |||
| 171 | fn writePadding(self: *Self, bytes: u64) !void { | ||
| 172 | const pos: usize = @intCast(bytes % block_size); | ||
| 173 | if (pos == 0) return; | ||
| 174 | try self.underlying_writer.writeAll(empty_block[pos..]); | ||
| 175 | } | ||
| 176 | |||
| 177 | /// Tar should finish with two zero blocks, but 'reasonable system must | ||
| 178 | /// not assume that such a block exists when reading an archive' (from | ||
| 179 | /// reference). In practice it is safe to skip this finish. | ||
| 180 | pub fn finish(self: *Self) !void { | ||
| 181 | try self.underlying_writer.writeAll(&empty_block); | ||
| 182 | try self.underlying_writer.writeAll(&empty_block); | ||
| 183 | } | ||
| 184 | }; | ||
| 185 | } | ||
| 186 | |||
| 187 | /// A struct that is exactly 512 bytes and matches tar file format. This is | ||
| 188 | /// intended to be used for outputting tar files; for parsing there is | ||
| 189 | /// `std.tar.Header`. | ||
| 190 | const Header = extern struct { | ||
| 191 | // This struct was originally copied from | ||
| 192 | // https://github.com/mattnite/tar/blob/main/src/main.zig which is MIT | ||
| 193 | // licensed. | ||
| 194 | // | ||
| 195 | // The name, linkname, magic, uname, and gname are null-terminated character | ||
| 196 | // strings. All other fields are zero-filled octal numbers in ASCII. Each | ||
| 197 | // numeric field of width w contains w minus 1 digits, and a null. | ||
| 198 | // Reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html | ||
| 199 | // POSIX header: byte offset | ||
| 200 | name: [100]u8 = [_]u8{0} ** 100, // 0 | ||
| 201 | mode: [7:0]u8 = default_mode.file, // 100 | ||
| 202 | uid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 108 | ||
| 203 | gid: [7:0]u8 = [_:0]u8{0} ** 7, // unused 116 | ||
| 204 | size: [11:0]u8 = [_:0]u8{'0'} ** 11, // 124 | ||
| 205 | mtime: [11:0]u8 = [_:0]u8{'0'} ** 11, // 136 | ||
| 206 | checksum: [7:0]u8 = [_:0]u8{' '} ** 7, // 148 | ||
| 207 | typeflag: FileType = .regular, // 156 | ||
| 208 | linkname: [100]u8 = [_]u8{0} ** 100, // 157 | ||
| 209 | magic: [6]u8 = [_]u8{ 'u', 's', 't', 'a', 'r', 0 }, // 257 | ||
| 210 | version: [2]u8 = [_]u8{ '0', '0' }, // 263 | ||
| 211 | uname: [32]u8 = [_]u8{0} ** 32, // unused 265 | ||
| 212 | gname: [32]u8 = [_]u8{0} ** 32, // unused 297 | ||
| 213 | devmajor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 329 | ||
| 214 | devminor: [7:0]u8 = [_:0]u8{0} ** 7, // unused 337 | ||
| 215 | prefix: [155]u8 = [_]u8{0} ** 155, // 345 | ||
| 216 | pad: [12]u8 = [_]u8{0} ** 12, // unused 500 | ||
| 217 | |||
| 218 | pub const FileType = enum(u8) { | ||
| 219 | regular = '0', | ||
| 220 | symbolic_link = '2', | ||
| 221 | directory = '5', | ||
| 222 | gnu_long_name = 'L', | ||
| 223 | gnu_long_link = 'K', | ||
| 224 | }; | ||
| 225 | |||
| 226 | const default_mode = struct { | ||
| 227 | const file = [_:0]u8{ '0', '0', '0', '0', '6', '6', '4' }; // 0o664 | ||
| 228 | const dir = [_:0]u8{ '0', '0', '0', '0', '7', '7', '5' }; // 0o775 | ||
| 229 | const sym_link = [_:0]u8{ '0', '0', '0', '0', '7', '7', '7' }; // 0o777 | ||
| 230 | const other = [_:0]u8{ '0', '0', '0', '0', '0', '0', '0' }; // 0o000 | ||
| 231 | }; | ||
| 232 | |||
| 233 | pub fn init(typeflag: FileType) Header { | ||
| 234 | return .{ | ||
| 235 | .typeflag = typeflag, | ||
| 236 | .mode = switch (typeflag) { | ||
| 237 | .directory => default_mode.dir, | ||
| 238 | .symbolic_link => default_mode.sym_link, | ||
| 239 | .regular => default_mode.file, | ||
| 240 | else => default_mode.other, | ||
| 241 | }, | ||
| 242 | }; | ||
| 243 | } | ||
| 244 | |||
| 245 | pub fn setSize(self: *Header, size: u64) !void { | ||
| 246 | try octal(&self.size, size); | ||
| 247 | } | ||
| 248 | |||
| 249 | fn octal(buf: []u8, value: u64) !void { | ||
| 250 | var remainder: u64 = value; | ||
| 251 | var pos: usize = buf.len; | ||
| 252 | while (remainder > 0 and pos > 0) { | ||
| 253 | pos -= 1; | ||
| 254 | const c: u8 = @as(u8, @intCast(remainder % 8)) + '0'; | ||
| 255 | buf[pos] = c; | ||
| 256 | remainder /= 8; | ||
| 257 | if (pos == 0 and remainder > 0) return error.OctalOverflow; | ||
| 258 | } | ||
| 259 | } | ||
| 260 | |||
| 261 | pub fn setMode(self: *Header, mode: u32) !void { | ||
| 262 | try octal(&self.mode, mode); | ||
| 263 | } | ||
| 264 | |||
| 265 | // Integer number of seconds since January 1, 1970, 00:00 Coordinated Universal Time. | ||
| 266 | // mtime == 0 will use current time | ||
| 267 | pub fn setMtime(self: *Header, mtime: u64) !void { | ||
| 268 | try octal(&self.mtime, mtime); | ||
| 269 | } | ||
| 270 | |||
| 271 | pub fn updateChecksum(self: *Header) !void { | ||
| 272 | var checksum: usize = ' '; // other 7 self.checksum bytes are initialized to ' ' | ||
| 273 | for (std.mem.asBytes(self)) |val| | ||
| 274 | checksum += val; | ||
| 275 | try octal(&self.checksum, checksum); | ||
| 276 | } | ||
| 277 | |||
| 278 | pub fn write(self: *Header, output_writer: anytype) !void { | ||
| 279 | try self.updateChecksum(); | ||
| 280 | try output_writer.writeAll(std.mem.asBytes(self)); | ||
| 281 | } | ||
| 282 | |||
| 283 | pub fn setLinkname(self: *Header, link: []const u8) !void { | ||
| 284 | if (link.len > self.linkname.len) return error.NameTooLong; | ||
| 285 | @memcpy(self.linkname[0..link.len], link); | ||
| 286 | } | ||
| 287 | |||
| 288 | pub fn setPath(self: *Header, prefix: []const u8, sub_path: []const u8) !void { | ||
| 289 | const max_prefix = self.prefix.len; | ||
| 290 | const max_name = self.name.len; | ||
| 291 | const sep = std.fs.path.sep_posix; | ||
| 292 | |||
| 293 | if (prefix.len + sub_path.len > max_name + max_prefix or prefix.len > max_prefix) | ||
| 294 | return error.NameTooLong; | ||
| 295 | |||
| 296 | // both fit into name | ||
| 297 | if (prefix.len > 0 and prefix.len + sub_path.len < max_name) { | ||
| 298 | @memcpy(self.name[0..prefix.len], prefix); | ||
| 299 | self.name[prefix.len] = sep; | ||
| 300 | @memcpy(self.name[prefix.len + 1 ..][0..sub_path.len], sub_path); | ||
| 301 | return; | ||
| 302 | } | ||
| 303 | |||
| 304 | // sub_path fits into name | ||
| 305 | // there is no prefix or prefix fits into prefix | ||
| 306 | if (sub_path.len <= max_name) { | ||
| 307 | @memcpy(self.name[0..sub_path.len], sub_path); | ||
| 308 | @memcpy(self.prefix[0..prefix.len], prefix); | ||
| 309 | return; | ||
| 310 | } | ||
| 311 | |||
| 312 | if (prefix.len > 0) { | ||
| 313 | @memcpy(self.prefix[0..prefix.len], prefix); | ||
| 314 | self.prefix[prefix.len] = sep; | ||
| 315 | } | ||
| 316 | const prefix_pos = if (prefix.len > 0) prefix.len + 1 else 0; | ||
| 317 | |||
| 318 | // add as much to prefix as you can, must split at / | ||
| 319 | const prefix_remaining = max_prefix - prefix_pos; | ||
| 320 | if (std.mem.lastIndexOf(u8, sub_path[0..@min(prefix_remaining, sub_path.len)], &.{'/'})) |sep_pos| { | ||
| 321 | @memcpy(self.prefix[prefix_pos..][0..sep_pos], sub_path[0..sep_pos]); | ||
| 322 | if ((sub_path.len - sep_pos - 1) > max_name) return error.NameTooLong; | ||
| 323 | @memcpy(self.name[0..][0 .. sub_path.len - sep_pos - 1], sub_path[sep_pos + 1 ..]); | ||
| 324 | return; | ||
| 325 | } | ||
| 326 | |||
| 327 | return error.NameTooLong; | ||
| 328 | } | ||
| 329 | |||
| 330 | comptime { | ||
| 331 | assert(@sizeOf(Header) == 512); | ||
| 332 | } | ||
| 333 | |||
| 334 | test setPath { | ||
| 335 | const cases = [_]struct { | ||
| 336 | in: []const []const u8, | ||
| 337 | out: []const []const u8, | ||
| 338 | }{ | ||
| 339 | .{ | ||
| 340 | .in = &.{ "", "123456789" }, | ||
| 341 | .out = &.{ "", "123456789" }, | ||
| 342 | }, | ||
| 343 | // can fit into name | ||
| 344 | .{ | ||
| 345 | .in = &.{ "prefix", "sub_path" }, | ||
| 346 | .out = &.{ "", "prefix/sub_path" }, | ||
| 347 | }, | ||
| 348 | // no more both fits into name | ||
| 349 | .{ | ||
| 350 | .in = &.{ "prefix", "0123456789/" ** 8 ++ "basename" }, | ||
| 351 | .out = &.{ "prefix", "0123456789/" ** 8 ++ "basename" }, | ||
| 352 | }, | ||
| 353 | // put as much as you can into prefix the rest goes into name | ||
| 354 | .{ | ||
| 355 | .in = &.{ "prefix", "0123456789/" ** 10 ++ "basename" }, | ||
| 356 | .out = &.{ "prefix/" ++ "0123456789/" ** 9 ++ "0123456789", "basename" }, | ||
| 357 | }, | ||
| 358 | |||
| 359 | .{ | ||
| 360 | .in = &.{ "prefix", "0123456789/" ** 15 ++ "basename" }, | ||
| 361 | .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/0123456789/basename" }, | ||
| 362 | }, | ||
| 363 | .{ | ||
| 364 | .in = &.{ "prefix", "0123456789/" ** 21 ++ "basename" }, | ||
| 365 | .out = &.{ "prefix/" ++ "0123456789/" ** 12 ++ "0123456789", "0123456789/" ** 8 ++ "basename" }, | ||
| 366 | }, | ||
| 367 | .{ | ||
| 368 | .in = &.{ "", "012345678/" ** 10 ++ "foo" }, | ||
| 369 | .out = &.{ "012345678/" ** 9 ++ "012345678", "foo" }, | ||
| 370 | }, | ||
| 371 | }; | ||
| 372 | |||
| 373 | for (cases) |case| { | ||
| 374 | var header = Header.init(.regular); | ||
| 375 | try header.setPath(case.in[0], case.in[1]); | ||
| 376 | try testing.expectEqualStrings(case.out[0], str(&header.prefix)); | ||
| 377 | try testing.expectEqualStrings(case.out[1], str(&header.name)); | ||
| 378 | } | ||
| 379 | |||
| 380 | const error_cases = [_]struct { | ||
| 381 | in: []const []const u8, | ||
| 382 | }{ | ||
| 383 | // basename can't fit into name (106 characters) | ||
| 384 | .{ .in = &.{ "zig", "test/cases/compile_errors/regression_test_2980_base_type_u32_is_not_type_checked_properly_when_assigning_a_value_within_a_struct.zig" } }, | ||
| 385 | // cant fit into 255 + sep | ||
| 386 | .{ .in = &.{ "prefix", "0123456789/" ** 22 ++ "basename" } }, | ||
| 387 | // can fit but sub_path can't be split (there is no separator) | ||
| 388 | .{ .in = &.{ "prefix", "0123456789" ** 10 ++ "a" } }, | ||
| 389 | .{ .in = &.{ "prefix", "0123456789" ** 14 ++ "basename" } }, | ||
| 390 | }; | ||
| 391 | |||
| 392 | for (error_cases) |case| { | ||
| 393 | var header = Header.init(.regular); | ||
| 394 | try testing.expectError( | ||
| 395 | error.NameTooLong, | ||
| 396 | header.setPath(case.in[0], case.in[1]), | ||
| 397 | ); | ||
| 398 | } | ||
| 399 | } | ||
| 400 | |||
| 401 | // Breaks string on first null character. | ||
| 402 | fn str(s: []const u8) []const u8 { | ||
| 403 | for (s, 0..) |c, i| { | ||
| 404 | if (c == 0) return s[0..i]; | ||
| 405 | } | ||
| 406 | return s; | ||
| 407 | } | ||
| 408 | }; | ||
| 409 | |||
| 410 | test { | ||
| 411 | _ = Header; | ||
| 412 | } | ||
| 413 | |||
| 414 | test "write files" { | ||
| 415 | const files = [_]struct { | ||
| 416 | path: []const u8, | ||
| 417 | content: []const u8, | ||
| 418 | }{ | ||
| 419 | .{ .path = "foo", .content = "bar" }, | ||
| 420 | .{ .path = "a12345678/" ** 10 ++ "foo", .content = "a" ** 511 }, | ||
| 421 | .{ .path = "b12345678/" ** 24 ++ "foo", .content = "b" ** 512 }, | ||
| 422 | .{ .path = "c12345678/" ** 25 ++ "foo", .content = "c" ** 513 }, | ||
| 423 | .{ .path = "d12345678/" ** 51 ++ "foo", .content = "d" ** 1025 }, | ||
| 424 | .{ .path = "e123456789" ** 11, .content = "e" }, | ||
| 425 | }; | ||
| 426 | |||
| 427 | var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | ||
| 428 | var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; | ||
| 429 | |||
| 430 | // with root | ||
| 431 | { | ||
| 432 | const root = "root"; | ||
| 433 | |||
| 434 | var output = std.ArrayList(u8).init(testing.allocator); | ||
| 435 | defer output.deinit(); | ||
| 436 | var wrt = writer(output.writer()); | ||
| 437 | try wrt.setRoot(root); | ||
| 438 | for (files) |file| | ||
| 439 | try wrt.writeFileBytes(file.path, file.content, .{}); | ||
| 440 | |||
| 441 | var input = std.io.fixedBufferStream(output.items); | ||
| 442 | var iter = std.tar.iterator( | ||
| 443 | input.reader(), | ||
| 444 | .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer }, | ||
| 445 | ); | ||
| 446 | |||
| 447 | // first entry is directory with prefix | ||
| 448 | { | ||
| 449 | const actual = (try iter.next()).?; | ||
| 450 | try testing.expectEqualStrings(root, actual.name); | ||
| 451 | try testing.expectEqual(std.tar.FileKind.directory, actual.kind); | ||
| 452 | } | ||
| 453 | |||
| 454 | var i: usize = 0; | ||
| 455 | while (try iter.next()) |actual| { | ||
| 456 | defer i += 1; | ||
| 457 | const expected = files[i]; | ||
| 458 | try testing.expectEqualStrings(root, actual.name[0..root.len]); | ||
| 459 | try testing.expectEqual('/', actual.name[root.len..][0]); | ||
| 460 | try testing.expectEqualStrings(expected.path, actual.name[root.len + 1 ..]); | ||
| 461 | |||
| 462 | var content = std.ArrayList(u8).init(testing.allocator); | ||
| 463 | defer content.deinit(); | ||
| 464 | try actual.writeAll(content.writer()); | ||
| 465 | try testing.expectEqualSlices(u8, expected.content, content.items); | ||
| 466 | } | ||
| 467 | } | ||
| 468 | // without root | ||
| 469 | { | ||
| 470 | var output = std.ArrayList(u8).init(testing.allocator); | ||
| 471 | defer output.deinit(); | ||
| 472 | var wrt = writer(output.writer()); | ||
| 473 | for (files) |file| { | ||
| 474 | var content = std.io.fixedBufferStream(file.content); | ||
| 475 | try wrt.writeFileStream(file.path, file.content.len, content.reader(), .{}); | ||
| 476 | } | ||
| 477 | |||
| 478 | var input = std.io.fixedBufferStream(output.items); | ||
| 479 | var iter = std.tar.iterator( | ||
| 480 | input.reader(), | ||
| 481 | .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer }, | ||
| 482 | ); | ||
| 483 | |||
| 484 | var i: usize = 0; | ||
| 485 | while (try iter.next()) |actual| { | ||
| 486 | defer i += 1; | ||
| 487 | const expected = files[i]; | ||
| 488 | try testing.expectEqualStrings(expected.path, actual.name); | ||
| 489 | |||
| 490 | var content = std.ArrayList(u8).init(testing.allocator); | ||
| 491 | defer content.deinit(); | ||
| 492 | try actual.writeAll(content.writer()); | ||
| 493 | try testing.expectEqualSlices(u8, expected.content, content.items); | ||
| 494 | } | ||
| 495 | try wrt.finish(); | ||
| 496 | } | ||
| 497 | } | ||
src/Compilation.zig+23-14| ... | @@ -4862,6 +4862,9 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void { | ... | @@ -4862,6 +4862,9 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void { |
| 4862 | }; | 4862 | }; |
| 4863 | defer tar_file.close(); | 4863 | defer tar_file.close(); |
| 4864 | 4864 | ||
| 4865 | var buffer: [1024]u8 = undefined; | ||
| 4866 | var tar_file_writer = tar_file.writer(&buffer); | ||
| 4867 | |||
| 4865 | var seen_table: std.AutoArrayHashMapUnmanaged(*Package.Module, []const u8) = .empty; | 4868 | var seen_table: std.AutoArrayHashMapUnmanaged(*Package.Module, []const u8) = .empty; |
| 4866 | defer seen_table.deinit(comp.gpa); | 4869 | defer seen_table.deinit(comp.gpa); |
| 4867 | 4870 | ||
| ... | @@ -4871,7 +4874,7 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void { | ... | @@ -4871,7 +4874,7 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void { |
| 4871 | var i: usize = 0; | 4874 | var i: usize = 0; |
| 4872 | while (i < seen_table.count()) : (i += 1) { | 4875 | while (i < seen_table.count()) : (i += 1) { |
| 4873 | const mod = seen_table.keys()[i]; | 4876 | const mod = seen_table.keys()[i]; |
| 4874 | try comp.docsCopyModule(mod, seen_table.values()[i], tar_file); | 4877 | try comp.docsCopyModule(mod, seen_table.values()[i], &tar_file_writer); |
| 4875 | 4878 | ||
| 4876 | const deps = mod.deps.values(); | 4879 | const deps = mod.deps.values(); |
| 4877 | try seen_table.ensureUnusedCapacity(comp.gpa, deps.len); | 4880 | try seen_table.ensureUnusedCapacity(comp.gpa, deps.len); |
| ... | @@ -4879,24 +4882,29 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void { | ... | @@ -4879,24 +4882,29 @@ fn docsCopyFallible(comp: *Compilation) anyerror!void { |
| 4879 | } | 4882 | } |
| 4880 | } | 4883 | } |
| 4881 | 4884 | ||
| 4882 | fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8, tar_file: fs.File) !void { | 4885 | fn docsCopyModule( |
| 4886 | comp: *Compilation, | ||
| 4887 | module: *Package.Module, | ||
| 4888 | name: []const u8, | ||
| 4889 | tar_file_writer: *fs.File.Writer, | ||
| 4890 | ) !void { | ||
| 4883 | const root = module.root; | 4891 | const root = module.root; |
| 4884 | var mod_dir = d: { | 4892 | var mod_dir = d: { |
| 4885 | const root_dir, const sub_path = root.openInfo(comp.dirs); | 4893 | const root_dir, const sub_path = root.openInfo(comp.dirs); |
| 4886 | break :d root_dir.openDir(sub_path, .{ .iterate = true }); | 4894 | break :d root_dir.openDir(sub_path, .{ .iterate = true }); |
| 4887 | } catch |err| { | 4895 | } catch |err| { |
| 4888 | return comp.lockAndSetMiscFailure(.docs_copy, "unable to open directory '{f}': {s}", .{ | 4896 | return comp.lockAndSetMiscFailure(.docs_copy, "unable to open directory '{f}': {t}", .{ root.fmt(comp), err }); |
| 4889 | root.fmt(comp), @errorName(err), | ||
| 4890 | }); | ||
| 4891 | }; | 4897 | }; |
| 4892 | defer mod_dir.close(); | 4898 | defer mod_dir.close(); |
| 4893 | 4899 | ||
| 4894 | var walker = try mod_dir.walk(comp.gpa); | 4900 | var walker = try mod_dir.walk(comp.gpa); |
| 4895 | defer walker.deinit(); | 4901 | defer walker.deinit(); |
| 4896 | 4902 | ||
| 4897 | var archiver = std.tar.writer(tar_file.deprecatedWriter().any()); | 4903 | var archiver: std.tar.Writer = .{ .underlying_writer = &tar_file_writer.interface }; |
| 4898 | archiver.prefix = name; | 4904 | archiver.prefix = name; |
| 4899 | 4905 | ||
| 4906 | var buffer: [1024]u8 = undefined; | ||
| 4907 | |||
| 4900 | while (try walker.next()) |entry| { | 4908 | while (try walker.next()) |entry| { |
| 4901 | switch (entry.kind) { | 4909 | switch (entry.kind) { |
| 4902 | .file => { | 4910 | .file => { |
| ... | @@ -4907,14 +4915,17 @@ fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8, | ... | @@ -4907,14 +4915,17 @@ fn docsCopyModule(comp: *Compilation, module: *Package.Module, name: []const u8, |
| 4907 | else => continue, | 4915 | else => continue, |
| 4908 | } | 4916 | } |
| 4909 | var file = mod_dir.openFile(entry.path, .{}) catch |err| { | 4917 | var file = mod_dir.openFile(entry.path, .{}) catch |err| { |
| 4910 | return comp.lockAndSetMiscFailure(.docs_copy, "unable to open '{f}{s}': {s}", .{ | 4918 | return comp.lockAndSetMiscFailure(.docs_copy, "unable to open {f}{s}: {t}", .{ |
| 4911 | root.fmt(comp), entry.path, @errorName(err), | 4919 | root.fmt(comp), entry.path, err, |
| 4912 | }); | 4920 | }); |
| 4913 | }; | 4921 | }; |
| 4914 | defer file.close(); | 4922 | defer file.close(); |
| 4915 | archiver.writeFile(entry.path, file) catch |err| { | 4923 | const stat = try file.stat(); |
| 4916 | return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive '{f}{s}': {s}", .{ | 4924 | var file_reader: fs.File.Reader = .initSize(file, &buffer, stat.size); |
| 4917 | root.fmt(comp), entry.path, @errorName(err), | 4925 | |
| 4926 | archiver.writeFile(entry.path, &file_reader, stat.mtime) catch |err| { | ||
| 4927 | return comp.lockAndSetMiscFailure(.docs_copy, "unable to archive {f}{s}: {t}", .{ | ||
| 4928 | root.fmt(comp), entry.path, err, | ||
| 4918 | }); | 4929 | }); |
| 4919 | }; | 4930 | }; |
| 4920 | } | 4931 | } |
| ... | @@ -4926,9 +4937,7 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void | ... | @@ -4926,9 +4937,7 @@ fn workerDocsWasm(comp: *Compilation, parent_prog_node: std.Progress.Node) void |
| 4926 | 4937 | ||
| 4927 | workerDocsWasmFallible(comp, prog_node) catch |err| switch (err) { | 4938 | workerDocsWasmFallible(comp, prog_node) catch |err| switch (err) { |
| 4928 | error.SubCompilationFailed => return, // error reported already | 4939 | error.SubCompilationFailed => return, // error reported already |
| 4929 | else => comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {s}", .{ | 4940 | else => comp.lockAndSetMiscFailure(.docs_wasm, "unable to build autodocs: {t}", .{err}), |
| 4930 | @errorName(err), | ||
| 4931 | }), | ||
| 4932 | }; | 4941 | }; |
| 4933 | } | 4942 | } |
| 4934 | 4943 |
src/Package/Fetch.zig+15-9| ... | @@ -1197,12 +1197,16 @@ fn unpackResource( | ... | @@ -1197,12 +1197,16 @@ fn unpackResource( |
| 1197 | }; | 1197 | }; |
| 1198 | 1198 | ||
| 1199 | switch (file_type) { | 1199 | switch (file_type) { |
| 1200 | .tar => return try unpackTarball(f, tmp_directory.handle, resource.reader()), | 1200 | .tar => { |
| 1201 | var adapter = resource.reader().adaptToNewApi(); | ||
| 1202 | return unpackTarball(f, tmp_directory.handle, &adapter.new_interface); | ||
| 1203 | }, | ||
| 1201 | .@"tar.gz" => { | 1204 | .@"tar.gz" => { |
| 1202 | const reader = resource.reader(); | 1205 | const reader = resource.reader(); |
| 1203 | var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader); | 1206 | var br = std.io.bufferedReaderSize(std.crypto.tls.max_ciphertext_record_len, reader); |
| 1204 | var dcp = std.compress.gzip.decompressor(br.reader()); | 1207 | var dcp = std.compress.gzip.decompressor(br.reader()); |
| 1205 | return try unpackTarball(f, tmp_directory.handle, dcp.reader()); | 1208 | var adapter = dcp.reader().adaptToNewApi(); |
| 1209 | return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface); | ||
| 1206 | }, | 1210 | }, |
| 1207 | .@"tar.xz" => { | 1211 | .@"tar.xz" => { |
| 1208 | const gpa = f.arena.child_allocator; | 1212 | const gpa = f.arena.child_allocator; |
| ... | @@ -1215,7 +1219,8 @@ fn unpackResource( | ... | @@ -1215,7 +1219,8 @@ fn unpackResource( |
| 1215 | )); | 1219 | )); |
| 1216 | }; | 1220 | }; |
| 1217 | defer dcp.deinit(); | 1221 | defer dcp.deinit(); |
| 1218 | return try unpackTarball(f, tmp_directory.handle, dcp.reader()); | 1222 | var adapter = dcp.reader().adaptToNewApi(); |
| 1223 | return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface); | ||
| 1219 | }, | 1224 | }, |
| 1220 | .@"tar.zst" => { | 1225 | .@"tar.zst" => { |
| 1221 | const window_size = std.compress.zstd.DecompressorOptions.default_window_buffer_len; | 1226 | const window_size = std.compress.zstd.DecompressorOptions.default_window_buffer_len; |
| ... | @@ -1225,7 +1230,8 @@ fn unpackResource( | ... | @@ -1225,7 +1230,8 @@ fn unpackResource( |
| 1225 | var dcp = std.compress.zstd.decompressor(br.reader(), .{ | 1230 | var dcp = std.compress.zstd.decompressor(br.reader(), .{ |
| 1226 | .window_buffer = window_buffer, | 1231 | .window_buffer = window_buffer, |
| 1227 | }); | 1232 | }); |
| 1228 | return try unpackTarball(f, tmp_directory.handle, dcp.reader()); | 1233 | var adapter = dcp.reader().adaptToNewApi(); |
| 1234 | return try unpackTarball(f, tmp_directory.handle, &adapter.new_interface); | ||
| 1229 | }, | 1235 | }, |
| 1230 | .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) { | 1236 | .git_pack => return unpackGitPack(f, tmp_directory.handle, &resource.git) catch |err| switch (err) { |
| 1231 | error.FetchFailed => return error.FetchFailed, | 1237 | error.FetchFailed => return error.FetchFailed, |
| ... | @@ -1239,7 +1245,7 @@ fn unpackResource( | ... | @@ -1239,7 +1245,7 @@ fn unpackResource( |
| 1239 | } | 1245 | } |
| 1240 | } | 1246 | } |
| 1241 | 1247 | ||
| 1242 | fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackResult { | 1248 | fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: *std.Io.Reader) RunError!UnpackResult { |
| 1243 | const eb = &f.error_bundle; | 1249 | const eb = &f.error_bundle; |
| 1244 | const arena = f.arena.allocator(); | 1250 | const arena = f.arena.allocator(); |
| 1245 | 1251 | ||
| ... | @@ -1250,10 +1256,10 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackRes | ... | @@ -1250,10 +1256,10 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!UnpackRes |
| 1250 | .strip_components = 0, | 1256 | .strip_components = 0, |
| 1251 | .mode_mode = .ignore, | 1257 | .mode_mode = .ignore, |
| 1252 | .exclude_empty_directories = true, | 1258 | .exclude_empty_directories = true, |
| 1253 | }) catch |err| return f.fail(f.location_tok, try eb.printString( | 1259 | }) catch |err| return f.fail( |
| 1254 | "unable to unpack tarball to temporary directory: {s}", | 1260 | f.location_tok, |
| 1255 | .{@errorName(err)}, | 1261 | try eb.printString("unable to unpack tarball to temporary directory: {t}", .{err}), |
| 1256 | )); | 1262 | ); |
| 1257 | 1263 | ||
| 1258 | var res: UnpackResult = .{ .root_dir = diagnostics.root_dir }; | 1264 | var res: UnpackResult = .{ .root_dir = diagnostics.root_dir }; |
| 1259 | if (diagnostics.errors.items.len > 0) { | 1265 | if (diagnostics.errors.items.len > 0) { |