| author | |
| committer | |
| log | f60c24c73cc5c5894fbfb7060a70bc683c4a4ba5 |
| tree | 981b2b5fa0a8ab5ed5db4be972467367de2ec1f9 |
| parent | 26e895e3dc4ff1b7ac235414a356840bccb4fb1e |
| parent | c974e198164e13fea491e3897a04ec82d132fbf6 |
| signature |
std.tar: error on insufficient buffers provided to iterator6 files changed, 633 insertions(+), 449 deletions(-)
lib/docs/wasm/main.zig+1-2| ... | ... | @@ -767,7 +767,7 @@ fn unpack_inner(tar_bytes: []u8) !void { |
| 767 | 767 | }); |
| 768 | 768 | while (try it.next()) |tar_file| { |
| 769 | 769 | switch (tar_file.kind) { |
| 770 | .normal => { | |
| 770 | .file => { | |
| 771 | 771 | if (tar_file.size == 0 and tar_file.name.len == 0) break; |
| 772 | 772 | if (std.mem.endsWith(u8, tar_file.name, ".zig")) { |
| 773 | 773 | log.debug("found file: '{s}'", .{tar_file.name}); |
| ... | ... | @@ -790,7 +790,6 @@ fn unpack_inner(tar_bytes: []u8) !void { |
| 790 | 790 | tar_file.name, |
| 791 | 791 | }); |
| 792 | 792 | } |
| 793 | try tar_file.skip(); | |
| 794 | 793 | }, |
| 795 | 794 | else => continue, |
| 796 | 795 | } |
lib/std/tar.zig+293-123| ... | ... | @@ -15,22 +15,65 @@ |
| 15 | 15 | //! GNU tar reference: https://www.gnu.org/software/tar/manual/html_node/Standard.html |
| 16 | 16 | //! pax reference: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13 |
| 17 | 17 | |
| 18 | const std = @import("std.zig"); | |
| 18 | const std = @import("std"); | |
| 19 | 19 | const assert = std.debug.assert; |
| 20 | const testing = std.testing; | |
| 20 | 21 | |
| 21 | 22 | pub const output = @import("tar/output.zig"); |
| 22 | 23 | |
| 23 | pub const Options = struct { | |
| 24 | /// Provide this to receive detailed error messages. | |
| 25 | /// When this is provided, some errors which would otherwise be returned | |
| 26 | /// immediately will instead be added to this structure. The API user must check | |
| 27 | /// the errors in diagnostics to know whether the operation succeeded or failed. | |
| 28 | pub const Diagnostics = struct { | |
| 29 | allocator: std.mem.Allocator, | |
| 30 | errors: std.ArrayListUnmanaged(Error) = .{}, | |
| 31 | ||
| 32 | pub const Error = union(enum) { | |
| 33 | unable_to_create_sym_link: struct { | |
| 34 | code: anyerror, | |
| 35 | file_name: []const u8, | |
| 36 | link_name: []const u8, | |
| 37 | }, | |
| 38 | unable_to_create_file: struct { | |
| 39 | code: anyerror, | |
| 40 | file_name: []const u8, | |
| 41 | }, | |
| 42 | unsupported_file_type: struct { | |
| 43 | file_name: []const u8, | |
| 44 | file_type: Header.Kind, | |
| 45 | }, | |
| 46 | }; | |
| 47 | ||
| 48 | pub fn deinit(d: *Diagnostics) void { | |
| 49 | for (d.errors.items) |item| { | |
| 50 | switch (item) { | |
| 51 | .unable_to_create_sym_link => |info| { | |
| 52 | d.allocator.free(info.file_name); | |
| 53 | d.allocator.free(info.link_name); | |
| 54 | }, | |
| 55 | .unable_to_create_file => |info| { | |
| 56 | d.allocator.free(info.file_name); | |
| 57 | }, | |
| 58 | .unsupported_file_type => |info| { | |
| 59 | d.allocator.free(info.file_name); | |
| 60 | }, | |
| 61 | } | |
| 62 | } | |
| 63 | d.errors.deinit(d.allocator); | |
| 64 | d.* = undefined; | |
| 65 | } | |
| 66 | }; | |
| 67 | ||
| 68 | /// pipeToFileSystem options | |
| 69 | pub const PipeOptions = struct { | |
| 24 | 70 | /// Number of directory levels to skip when extracting files. |
| 25 | 71 | strip_components: u32 = 0, |
| 26 | 72 | /// How to handle the "mode" property of files from within the tar file. |
| 27 | 73 | mode_mode: ModeMode = .executable_bit_only, |
| 28 | 74 | /// Prevents creation of empty directories. |
| 29 | 75 | exclude_empty_directories: bool = false, |
| 30 | /// Provide this to receive detailed error messages. | |
| 31 | /// When this is provided, some errors which would otherwise be returned immediately | |
| 32 | /// will instead be added to this structure. The API user must check the errors | |
| 33 | /// in diagnostics to know whether the operation succeeded or failed. | |
| 76 | /// Collects error messages during unpacking | |
| 34 | 77 | diagnostics: ?*Diagnostics = null, |
| 35 | 78 | |
| 36 | 79 | pub const ModeMode = enum { |
| ... | ... | @@ -42,56 +85,16 @@ pub const Options = struct { |
| 42 | 85 | /// Other bits of the mode are left as the default when creating files. |
| 43 | 86 | executable_bit_only, |
| 44 | 87 | }; |
| 45 | ||
| 46 | pub const Diagnostics = struct { | |
| 47 | allocator: std.mem.Allocator, | |
| 48 | errors: std.ArrayListUnmanaged(Error) = .{}, | |
| 49 | ||
| 50 | pub const Error = union(enum) { | |
| 51 | unable_to_create_sym_link: struct { | |
| 52 | code: anyerror, | |
| 53 | file_name: []const u8, | |
| 54 | link_name: []const u8, | |
| 55 | }, | |
| 56 | unable_to_create_file: struct { | |
| 57 | code: anyerror, | |
| 58 | file_name: []const u8, | |
| 59 | }, | |
| 60 | unsupported_file_type: struct { | |
| 61 | file_name: []const u8, | |
| 62 | file_type: Header.Kind, | |
| 63 | }, | |
| 64 | }; | |
| 65 | ||
| 66 | pub fn deinit(d: *Diagnostics) void { | |
| 67 | for (d.errors.items) |item| { | |
| 68 | switch (item) { | |
| 69 | .unable_to_create_sym_link => |info| { | |
| 70 | d.allocator.free(info.file_name); | |
| 71 | d.allocator.free(info.link_name); | |
| 72 | }, | |
| 73 | .unable_to_create_file => |info| { | |
| 74 | d.allocator.free(info.file_name); | |
| 75 | }, | |
| 76 | .unsupported_file_type => |info| { | |
| 77 | d.allocator.free(info.file_name); | |
| 78 | }, | |
| 79 | } | |
| 80 | } | |
| 81 | d.errors.deinit(d.allocator); | |
| 82 | d.* = undefined; | |
| 83 | } | |
| 84 | }; | |
| 85 | 88 | }; |
| 86 | 89 | |
| 87 | pub const Header = struct { | |
| 90 | const Header = struct { | |
| 88 | 91 | const SIZE = 512; |
| 89 | 92 | const MAX_NAME_SIZE = 100 + 1 + 155; // name(100) + separator(1) + prefix(155) |
| 90 | 93 | const LINK_NAME_SIZE = 100; |
| 91 | 94 | |
| 92 | 95 | bytes: *const [SIZE]u8, |
| 93 | 96 | |
| 94 | pub const Kind = enum(u8) { | |
| 97 | const Kind = enum(u8) { | |
| 95 | 98 | normal_alias = 0, |
| 96 | 99 | normal = '0', |
| 97 | 100 | hard_link = '1', |
| ... | ... | @@ -114,9 +117,10 @@ pub const Header = struct { |
| 114 | 117 | |
| 115 | 118 | /// Includes prefix concatenated, if any. |
| 116 | 119 | /// TODO: check against "../" and other nefarious things |
| 117 | pub fn fullName(header: Header, buffer: *[MAX_NAME_SIZE]u8) ![]const u8 { | |
| 120 | pub fn fullName(header: Header, buffer: []u8) ![]const u8 { | |
| 118 | 121 | const n = name(header); |
| 119 | 122 | const p = prefix(header); |
| 123 | if (buffer.len < n.len + p.len + 1) return error.TarInsufficientBuffer; | |
| 120 | 124 | if (!is_ustar(header) or p.len == 0) { |
| 121 | 125 | @memcpy(buffer[0..n.len], n); |
| 122 | 126 | return buffer[0..n.len]; |
| ... | ... | @@ -127,11 +131,14 @@ pub const Header = struct { |
| 127 | 131 | return buffer[0 .. p.len + 1 + n.len]; |
| 128 | 132 | } |
| 129 | 133 | |
| 130 | pub fn linkName(header: Header, buffer: *[LINK_NAME_SIZE]u8) []const u8 { | |
| 134 | /// When kind is symbolic_link linked-to name (target_path) is specified in | |
| 135 | /// the linkname field. | |
| 136 | pub fn linkName(header: Header, buffer: []u8) ![]const u8 { | |
| 131 | 137 | const link_name = header.str(157, 100); |
| 132 | 138 | if (link_name.len == 0) { |
| 133 | 139 | return buffer[0..0]; |
| 134 | 140 | } |
| 141 | if (buffer.len < link_name.len) return error.TarInsufficientBuffer; | |
| 135 | 142 | const buf = buffer[0..link_name.len]; |
| 136 | 143 | @memcpy(buf, link_name); |
| 137 | 144 | return buf; |
| ... | ... | @@ -233,71 +240,85 @@ fn nullStr(str: []const u8) []const u8 { |
| 233 | 240 | return str; |
| 234 | 241 | } |
| 235 | 242 | |
| 243 | /// Options for iterator. | |
| 244 | /// Buffers should be provided by the caller. | |
| 236 | 245 | pub const IteratorOptions = struct { |
| 237 | 246 | /// Use a buffer with length `std.fs.MAX_PATH_BYTES` to match file system capabilities. |
| 238 | 247 | file_name_buffer: []u8, |
| 239 | 248 | /// Use a buffer with length `std.fs.MAX_PATH_BYTES` to match file system capabilities. |
| 240 | 249 | link_name_buffer: []u8, |
| 250 | /// Collects error messages during unpacking | |
| 241 | 251 | diagnostics: ?*Diagnostics = null, |
| 242 | ||
| 243 | pub const Diagnostics = Options.Diagnostics; | |
| 244 | 252 | }; |
| 245 | 253 | |
| 246 | 254 | /// Iterates over files in tar archive. |
| 247 | /// `next` returns each file in `reader` tar archive. | |
| 255 | /// `next` returns each file in tar archive. | |
| 248 | 256 | pub fn iterator(reader: anytype, options: IteratorOptions) Iterator(@TypeOf(reader)) { |
| 249 | 257 | return .{ |
| 250 | 258 | .reader = reader, |
| 251 | 259 | .diagnostics = options.diagnostics, |
| 252 | .header_buffer = undefined, | |
| 253 | 260 | .file_name_buffer = options.file_name_buffer, |
| 254 | 261 | .link_name_buffer = options.link_name_buffer, |
| 255 | .padding = 0, | |
| 256 | .file = undefined, | |
| 257 | 262 | }; |
| 258 | 263 | } |
| 259 | 264 | |
| 260 | fn Iterator(comptime ReaderType: type) type { | |
| 265 | /// Type of the file returned by iterator `next` method. | |
| 266 | pub const FileKind = enum { | |
| 267 | directory, | |
| 268 | sym_link, | |
| 269 | file, | |
| 270 | }; | |
| 271 | ||
| 272 | /// Iteartor over entries in the tar file represented by reader. | |
| 273 | pub fn Iterator(comptime ReaderType: type) type { | |
| 261 | 274 | return struct { |
| 262 | 275 | reader: ReaderType, |
| 263 | diagnostics: ?*Options.Diagnostics, | |
| 276 | diagnostics: ?*Diagnostics = null, | |
| 264 | 277 | |
| 265 | 278 | // buffers for heeader and file attributes |
| 266 | header_buffer: [Header.SIZE]u8, | |
| 279 | header_buffer: [Header.SIZE]u8 = undefined, | |
| 267 | 280 | file_name_buffer: []u8, |
| 268 | 281 | link_name_buffer: []u8, |
| 269 | 282 | |
| 270 | 283 | // bytes of padding to the end of the block |
| 271 | padding: usize, | |
| 272 | // current tar file | |
| 273 | file: File, | |
| 284 | padding: usize = 0, | |
| 285 | // not consumed bytes of file from last next iteration | |
| 286 | unread_file_bytes: u64 = 0, | |
| 274 | 287 | |
| 275 | 288 | pub const File = struct { |
| 276 | 289 | name: []const u8, // name of file, symlink or directory |
| 277 | 290 | link_name: []const u8, // target name of symlink |
| 278 | size: u64, // size of the file in bytes | |
| 279 | mode: u32, | |
| 280 | kind: Header.Kind, | |
| 291 | size: u64 = 0, // size of the file in bytes | |
| 292 | mode: u32 = 0, | |
| 293 | kind: FileKind = .file, | |
| 294 | ||
| 295 | unread_bytes: *u64, | |
| 296 | parent_reader: ReaderType, | |
| 297 | ||
| 298 | pub const Reader = std.io.Reader(File, ReaderType.Error, File.read); | |
| 281 | 299 | |
| 282 | reader: ReaderType, | |
| 300 | pub fn reader(self: File) Reader { | |
| 301 | return .{ .context = self }; | |
| 302 | } | |
| 303 | ||
| 304 | pub fn read(self: File, dest: []u8) ReaderType.Error!usize { | |
| 305 | const buf = dest[0..@min(dest.len, self.unread_bytes.*)]; | |
| 306 | const n = try self.parent_reader.read(buf); | |
| 307 | self.unread_bytes.* -= n; | |
| 308 | return n; | |
| 309 | } | |
| 283 | 310 | |
| 284 | 311 | // Writes file content to writer. |
| 285 | pub fn write(self: File, writer: anytype) !void { | |
| 312 | pub fn writeAll(self: File, writer: anytype) !void { | |
| 286 | 313 | var buffer: [4096]u8 = undefined; |
| 287 | 314 | |
| 288 | var n: u64 = 0; | |
| 289 | while (n < self.size) { | |
| 290 | const buf = buffer[0..@min(buffer.len, self.size - n)]; | |
| 291 | try self.reader.readNoEof(buf); | |
| 315 | while (self.unread_bytes.* > 0) { | |
| 316 | const buf = buffer[0..@min(buffer.len, self.unread_bytes.*)]; | |
| 317 | try self.parent_reader.readNoEof(buf); | |
| 292 | 318 | try writer.writeAll(buf); |
| 293 | n += buf.len; | |
| 319 | self.unread_bytes.* -= buf.len; | |
| 294 | 320 | } |
| 295 | 321 | } |
| 296 | ||
| 297 | // Skips file content. Advances reader. | |
| 298 | pub fn skip(self: File) !void { | |
| 299 | try self.reader.skipBytes(self.size, .{}); | |
| 300 | } | |
| 301 | 322 | }; |
| 302 | 323 | |
| 303 | 324 | const Self = @This(); |
| ... | ... | @@ -315,20 +336,18 @@ fn Iterator(comptime ReaderType: type) type { |
| 315 | 336 | } |
| 316 | 337 | |
| 317 | 338 | fn readString(self: *Self, size: usize, buffer: []u8) ![]const u8 { |
| 318 | if (size > buffer.len) return error.TarCorruptInput; | |
| 339 | if (size > buffer.len) return error.TarInsufficientBuffer; | |
| 319 | 340 | const buf = buffer[0..size]; |
| 320 | 341 | try self.reader.readNoEof(buf); |
| 321 | 342 | return nullStr(buf); |
| 322 | 343 | } |
| 323 | 344 | |
| 324 | fn initFile(self: *Self) void { | |
| 325 | self.file = .{ | |
| 345 | fn newFile(self: *Self) File { | |
| 346 | return .{ | |
| 326 | 347 | .name = self.file_name_buffer[0..0], |
| 327 | 348 | .link_name = self.link_name_buffer[0..0], |
| 328 | .size = 0, | |
| 329 | .kind = .normal, | |
| 330 | .mode = 0, | |
| 331 | .reader = self.reader, | |
| 349 | .parent_reader = self.reader, | |
| 350 | .unread_bytes = &self.unread_file_bytes, | |
| 332 | 351 | }; |
| 333 | 352 | } |
| 334 | 353 | |
| ... | ... | @@ -345,7 +364,12 @@ fn Iterator(comptime ReaderType: type) type { |
| 345 | 364 | /// loop iterates through one or more entries until it collects a all |
| 346 | 365 | /// file attributes. |
| 347 | 366 | pub fn next(self: *Self) !?File { |
| 348 | self.initFile(); | |
| 367 | if (self.unread_file_bytes > 0) { | |
| 368 | // If file content was not consumed by caller | |
| 369 | try self.reader.skipBytes(self.unread_file_bytes, .{}); | |
| 370 | self.unread_file_bytes = 0; | |
| 371 | } | |
| 372 | var file: File = self.newFile(); | |
| 349 | 373 | |
| 350 | 374 | while (try self.readHeader()) |header| { |
| 351 | 375 | const kind = header.kind(); |
| ... | ... | @@ -355,46 +379,52 @@ fn Iterator(comptime ReaderType: type) type { |
| 355 | 379 | switch (kind) { |
| 356 | 380 | // File types to retrun upstream |
| 357 | 381 | .directory, .normal, .symbolic_link => { |
| 358 | self.file.kind = kind; | |
| 359 | self.file.mode = try header.mode(); | |
| 382 | file.kind = switch (kind) { | |
| 383 | .directory => .directory, | |
| 384 | .normal => .file, | |
| 385 | .symbolic_link => .sym_link, | |
| 386 | else => unreachable, | |
| 387 | }; | |
| 388 | file.mode = try header.mode(); | |
| 360 | 389 | |
| 361 | 390 | // set file attributes if not already set by prefix/extended headers |
| 362 | if (self.file.size == 0) { | |
| 363 | self.file.size = size; | |
| 391 | if (file.size == 0) { | |
| 392 | file.size = size; | |
| 364 | 393 | } |
| 365 | if (self.file.link_name.len == 0) { | |
| 366 | self.file.link_name = header.linkName(self.link_name_buffer[0..Header.LINK_NAME_SIZE]); | |
| 394 | if (file.link_name.len == 0) { | |
| 395 | file.link_name = try header.linkName(self.link_name_buffer); | |
| 367 | 396 | } |
| 368 | if (self.file.name.len == 0) { | |
| 369 | self.file.name = try header.fullName(self.file_name_buffer[0..Header.MAX_NAME_SIZE]); | |
| 397 | if (file.name.len == 0) { | |
| 398 | file.name = try header.fullName(self.file_name_buffer); | |
| 370 | 399 | } |
| 371 | 400 | |
| 372 | self.padding = blockPadding(self.file.size); | |
| 373 | return self.file; | |
| 401 | self.padding = blockPadding(file.size); | |
| 402 | self.unread_file_bytes = file.size; | |
| 403 | return file; | |
| 374 | 404 | }, |
| 375 | 405 | // Prefix header types |
| 376 | 406 | .gnu_long_name => { |
| 377 | self.file.name = try self.readString(@intCast(size), self.file_name_buffer); | |
| 407 | file.name = try self.readString(@intCast(size), self.file_name_buffer); | |
| 378 | 408 | }, |
| 379 | 409 | .gnu_long_link => { |
| 380 | self.file.link_name = try self.readString(@intCast(size), self.link_name_buffer); | |
| 410 | file.link_name = try self.readString(@intCast(size), self.link_name_buffer); | |
| 381 | 411 | }, |
| 382 | 412 | .extended_header => { |
| 383 | 413 | // Use just attributes from last extended header. |
| 384 | self.initFile(); | |
| 414 | file = self.newFile(); | |
| 385 | 415 | |
| 386 | 416 | var rdr = paxIterator(self.reader, @intCast(size)); |
| 387 | 417 | while (try rdr.next()) |attr| { |
| 388 | 418 | switch (attr.kind) { |
| 389 | 419 | .path => { |
| 390 | self.file.name = try attr.value(self.file_name_buffer); | |
| 420 | file.name = try attr.value(self.file_name_buffer); | |
| 391 | 421 | }, |
| 392 | 422 | .linkpath => { |
| 393 | self.file.link_name = try attr.value(self.link_name_buffer); | |
| 423 | file.link_name = try attr.value(self.link_name_buffer); | |
| 394 | 424 | }, |
| 395 | 425 | .size => { |
| 396 | 426 | var buf: [pax_max_size_attr_len]u8 = undefined; |
| 397 | self.file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10); | |
| 427 | file.size = try std.fmt.parseInt(u64, try attr.value(&buf), 10); | |
| 398 | 428 | }, |
| 399 | 429 | } |
| 400 | 430 | } |
| ... | ... | @@ -467,7 +497,8 @@ fn PaxIterator(comptime ReaderType: type) type { |
| 467 | 497 | // Copies pax attribute value into destination buffer. |
| 468 | 498 | // Must be called with destination buffer of size at least Attribute.len. |
| 469 | 499 | pub fn value(self: Attribute, dst: []u8) ![]const u8 { |
| 470 | assert(self.len <= dst.len); | |
| 500 | if (self.len > dst.len) return error.TarInsufficientBuffer; | |
| 501 | // assert(self.len <= dst.len); | |
| 471 | 502 | const buf = dst[0..self.len]; |
| 472 | 503 | const n = try self.reader.readAll(buf); |
| 473 | 504 | if (n < self.len) return error.UnexpectedEndOfStream; |
| ... | ... | @@ -540,7 +571,8 @@ fn PaxIterator(comptime ReaderType: type) type { |
| 540 | 571 | }; |
| 541 | 572 | } |
| 542 | 573 | |
| 543 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !void { | |
| 574 | /// Saves tar file content to the file systems. | |
| 575 | pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: PipeOptions) !void { | |
| 544 | 576 | switch (options.mode_mode) { |
| 545 | 577 | .ignore => {}, |
| 546 | 578 | .executable_bit_only => { |
| ... | ... | @@ -568,24 +600,23 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi |
| 568 | 600 | try dir.makePath(file_name); |
| 569 | 601 | } |
| 570 | 602 | }, |
| 571 | .normal => { | |
| 603 | .file => { | |
| 572 | 604 | if (file.size == 0 and file.name.len == 0) return; |
| 573 | 605 | const file_name = stripComponents(file.name, options.strip_components); |
| 574 | 606 | if (file_name.len == 0) return error.BadFileName; |
| 575 | 607 | |
| 576 | 608 | if (createDirAndFile(dir, file_name)) |fs_file| { |
| 577 | 609 | defer fs_file.close(); |
| 578 | try file.write(fs_file); | |
| 610 | try file.writeAll(fs_file); | |
| 579 | 611 | } else |err| { |
| 580 | 612 | const d = options.diagnostics orelse return err; |
| 581 | 613 | try d.errors.append(d.allocator, .{ .unable_to_create_file = .{ |
| 582 | 614 | .code = err, |
| 583 | 615 | .file_name = try d.allocator.dupe(u8, file_name), |
| 584 | 616 | } }); |
| 585 | try file.skip(); | |
| 586 | 617 | } |
| 587 | 618 | }, |
| 588 | .symbolic_link => { | |
| 619 | .sym_link => { | |
| 589 | 620 | // The file system path of the symbolic link. |
| 590 | 621 | const file_name = stripComponents(file.name, options.strip_components); |
| 591 | 622 | if (file_name.len == 0) return error.BadFileName; |
| ... | ... | @@ -601,7 +632,6 @@ pub fn pipeToFileSystem(dir: std.fs.Dir, reader: anytype, options: Options) !voi |
| 601 | 632 | } }); |
| 602 | 633 | }; |
| 603 | 634 | }, |
| 604 | else => unreachable, | |
| 605 | 635 | } |
| 606 | 636 | } |
| 607 | 637 | } |
| ... | ... | @@ -619,6 +649,7 @@ fn createDirAndFile(dir: std.fs.Dir, file_name: []const u8) !std.fs.File { |
| 619 | 649 | return fs_file; |
| 620 | 650 | } |
| 621 | 651 | |
| 652 | // Creates a symbolic link at path `file_name` which points to `link_name`. | |
| 622 | 653 | fn createDirAndSymlink(dir: std.fs.Dir, link_name: []const u8, file_name: []const u8) !void { |
| 623 | 654 | dir.symLink(link_name, file_name, .{}) catch |err| { |
| 624 | 655 | if (err == error.FileNotFound) { |
| ... | ... | @@ -645,8 +676,8 @@ fn stripComponents(path: []const u8, count: u32) []const u8 { |
| 645 | 676 | return path[i..]; |
| 646 | 677 | } |
| 647 | 678 | |
| 648 | test "tar stripComponents" { | |
| 649 | const expectEqualStrings = std.testing.expectEqualStrings; | |
| 679 | test "stripComponents" { | |
| 680 | const expectEqualStrings = testing.expectEqualStrings; | |
| 650 | 681 | try expectEqualStrings("a/b/c", stripComponents("a/b/c", 0)); |
| 651 | 682 | try expectEqualStrings("b/c", stripComponents("a/b/c", 1)); |
| 652 | 683 | try expectEqualStrings("c", stripComponents("a/b/c", 2)); |
| ... | ... | @@ -654,7 +685,7 @@ test "tar stripComponents" { |
| 654 | 685 | try expectEqualStrings("", stripComponents("a/b/c", 4)); |
| 655 | 686 | } |
| 656 | 687 | |
| 657 | test "tar PaxIterator" { | |
| 688 | test "PaxIterator" { | |
| 658 | 689 | const Attr = struct { |
| 659 | 690 | kind: PaxAttributeKind, |
| 660 | 691 | value: []const u8 = undefined, |
| ... | ... | @@ -757,24 +788,24 @@ test "tar PaxIterator" { |
| 757 | 788 | var i: usize = 0; |
| 758 | 789 | while (iter.next() catch |err| { |
| 759 | 790 | if (case.err) |e| { |
| 760 | try std.testing.expectEqual(e, err); | |
| 791 | try testing.expectEqual(e, err); | |
| 761 | 792 | continue; |
| 762 | 793 | } |
| 763 | 794 | return err; |
| 764 | 795 | }) |attr| : (i += 1) { |
| 765 | 796 | const exp = case.attrs[i]; |
| 766 | try std.testing.expectEqual(exp.kind, attr.kind); | |
| 797 | try testing.expectEqual(exp.kind, attr.kind); | |
| 767 | 798 | const value = attr.value(&buffer) catch |err| { |
| 768 | 799 | if (exp.err) |e| { |
| 769 | try std.testing.expectEqual(e, err); | |
| 800 | try testing.expectEqual(e, err); | |
| 770 | 801 | break :outer; |
| 771 | 802 | } |
| 772 | 803 | return err; |
| 773 | 804 | }; |
| 774 | try std.testing.expectEqualStrings(exp.value, value); | |
| 805 | try testing.expectEqualStrings(exp.value, value); | |
| 775 | 806 | } |
| 776 | try std.testing.expectEqual(case.attrs.len, i); | |
| 777 | try std.testing.expect(case.err == null); | |
| 807 | try testing.expectEqual(case.attrs.len, i); | |
| 808 | try testing.expect(case.err == null); | |
| 778 | 809 | } |
| 779 | 810 | } |
| 780 | 811 | |
| ... | ... | @@ -782,7 +813,7 @@ test { |
| 782 | 813 | _ = @import("tar/test.zig"); |
| 783 | 814 | } |
| 784 | 815 | |
| 785 | test "tar header parse size" { | |
| 816 | test "header parse size" { | |
| 786 | 817 | const cases = [_]struct { |
| 787 | 818 | in: []const u8, |
| 788 | 819 | want: u64 = 0, |
| ... | ... | @@ -810,14 +841,14 @@ test "tar header parse size" { |
| 810 | 841 | @memcpy(bytes[124 .. 124 + case.in.len], case.in); |
| 811 | 842 | var header = Header{ .bytes = &bytes }; |
| 812 | 843 | if (case.err) |err| { |
| 813 | try std.testing.expectError(err, header.size()); | |
| 844 | try testing.expectError(err, header.size()); | |
| 814 | 845 | } else { |
| 815 | try std.testing.expectEqual(case.want, try header.size()); | |
| 846 | try testing.expectEqual(case.want, try header.size()); | |
| 816 | 847 | } |
| 817 | 848 | } |
| 818 | 849 | } |
| 819 | 850 | |
| 820 | test "tar header parse mode" { | |
| 851 | test "header parse mode" { | |
| 821 | 852 | const cases = [_]struct { |
| 822 | 853 | in: []const u8, |
| 823 | 854 | want: u64 = 0, |
| ... | ... | @@ -835,9 +866,148 @@ test "tar header parse mode" { |
| 835 | 866 | @memcpy(bytes[100 .. 100 + case.in.len], case.in); |
| 836 | 867 | var header = Header{ .bytes = &bytes }; |
| 837 | 868 | if (case.err) |err| { |
| 838 | try std.testing.expectError(err, header.mode()); | |
| 869 | try testing.expectError(err, header.mode()); | |
| 839 | 870 | } else { |
| 840 | try std.testing.expectEqual(case.want, try header.mode()); | |
| 871 | try testing.expectEqual(case.want, try header.mode()); | |
| 872 | } | |
| 873 | } | |
| 874 | } | |
| 875 | ||
| 876 | test "create file and symlink" { | |
| 877 | var root = testing.tmpDir(.{}); | |
| 878 | defer root.cleanup(); | |
| 879 | ||
| 880 | var file = try createDirAndFile(root.dir, "file1"); | |
| 881 | file.close(); | |
| 882 | file = try createDirAndFile(root.dir, "a/b/c/file2"); | |
| 883 | file.close(); | |
| 884 | ||
| 885 | createDirAndSymlink(root.dir, "a/b/c/file2", "symlink1") catch |err| { | |
| 886 | // On Windows when developer mode is not enabled | |
| 887 | if (err == error.AccessDenied) return error.SkipZigTest; | |
| 888 | return err; | |
| 889 | }; | |
| 890 | try createDirAndSymlink(root.dir, "../../../file1", "d/e/f/symlink2"); | |
| 891 | ||
| 892 | // Danglink symlnik, file created later | |
| 893 | try createDirAndSymlink(root.dir, "../../../g/h/i/file4", "j/k/l/symlink3"); | |
| 894 | file = try createDirAndFile(root.dir, "g/h/i/file4"); | |
| 895 | file.close(); | |
| 896 | } | |
| 897 | ||
| 898 | test iterator { | |
| 899 | // Example tar file is created from this tree structure: | |
| 900 | // $ tree example | |
| 901 | // example | |
| 902 | // ├── a | |
| 903 | // │   └── file | |
| 904 | // ├── b | |
| 905 | // │   └── symlink -> ../a/file | |
| 906 | // └── empty | |
| 907 | // $ cat example/a/file | |
| 908 | // content | |
| 909 | // $ tar -cf example.tar example | |
| 910 | // $ tar -tvf example.tar | |
| 911 | // example/ | |
| 912 | // example/b/ | |
| 913 | // example/b/symlink -> ../a/file | |
| 914 | // example/a/ | |
| 915 | // example/a/file | |
| 916 | // example/empty/ | |
| 917 | ||
| 918 | const data = @embedFile("tar/testdata/example.tar"); | |
| 919 | var fbs = std.io.fixedBufferStream(data); | |
| 920 | ||
| 921 | // User provided buffers to the iterator | |
| 922 | var file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; | |
| 923 | var link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; | |
| 924 | // Create iterator | |
| 925 | var iter = iterator(fbs.reader(), .{ | |
| 926 | .file_name_buffer = &file_name_buffer, | |
| 927 | .link_name_buffer = &link_name_buffer, | |
| 928 | }); | |
| 929 | // Iterate over files in example.tar | |
| 930 | var file_no: usize = 0; | |
| 931 | while (try iter.next()) |file| : (file_no += 1) { | |
| 932 | switch (file.kind) { | |
| 933 | .directory => { | |
| 934 | switch (file_no) { | |
| 935 | 0 => try testing.expectEqualStrings("example/", file.name), | |
| 936 | 1 => try testing.expectEqualStrings("example/b/", file.name), | |
| 937 | 3 => try testing.expectEqualStrings("example/a/", file.name), | |
| 938 | 5 => try testing.expectEqualStrings("example/empty/", file.name), | |
| 939 | else => unreachable, | |
| 940 | } | |
| 941 | }, | |
| 942 | .file => { | |
| 943 | try testing.expectEqualStrings("example/a/file", file.name); | |
| 944 | // Read file content | |
| 945 | var buf: [16]u8 = undefined; | |
| 946 | const n = try file.reader().readAll(&buf); | |
| 947 | try testing.expectEqualStrings("content\n", buf[0..n]); | |
| 948 | }, | |
| 949 | .sym_link => { | |
| 950 | try testing.expectEqualStrings("example/b/symlink", file.name); | |
| 951 | try testing.expectEqualStrings("../a/file", file.link_name); | |
| 952 | }, | |
| 841 | 953 | } |
| 842 | 954 | } |
| 843 | 955 | } |
| 956 | ||
| 957 | test pipeToFileSystem { | |
| 958 | // Example tar file is created from this tree structure: | |
| 959 | // $ tree example | |
| 960 | // example | |
| 961 | // ├── a | |
| 962 | // │   └── file | |
| 963 | // ├── b | |
| 964 | // │   └── symlink -> ../a/file | |
| 965 | // └── empty | |
| 966 | // $ cat example/a/file | |
| 967 | // content | |
| 968 | // $ tar -cf example.tar example | |
| 969 | // $ tar -tvf example.tar | |
| 970 | // example/ | |
| 971 | // example/b/ | |
| 972 | // example/b/symlink -> ../a/file | |
| 973 | // example/a/ | |
| 974 | // example/a/file | |
| 975 | // example/empty/ | |
| 976 | ||
| 977 | const data = @embedFile("tar/testdata/example.tar"); | |
| 978 | var fbs = std.io.fixedBufferStream(data); | |
| 979 | const reader = fbs.reader(); | |
| 980 | ||
| 981 | var tmp = testing.tmpDir(.{ .no_follow = true }); | |
| 982 | defer tmp.cleanup(); | |
| 983 | const dir = tmp.dir; | |
| 984 | ||
| 985 | // Save tar from `reader` to the file system `dir` | |
| 986 | pipeToFileSystem(dir, reader, .{ | |
| 987 | .mode_mode = .ignore, | |
| 988 | .strip_components = 1, | |
| 989 | .exclude_empty_directories = true, | |
| 990 | }) catch |err| { | |
| 991 | // Skip on platform which don't support symlinks | |
| 992 | if (err == error.UnableToCreateSymLink) return error.SkipZigTest; | |
| 993 | return err; | |
| 994 | }; | |
| 995 | ||
| 996 | try testing.expectError(error.FileNotFound, dir.statFile("empty")); | |
| 997 | try testing.expect((try dir.statFile("a/file")).kind == .file); | |
| 998 | try testing.expect((try dir.statFile("b/symlink")).kind == .file); // statFile follows symlink | |
| 999 | ||
| 1000 | var buf: [32]u8 = undefined; | |
| 1001 | try testing.expectEqualSlices( | |
| 1002 | u8, | |
| 1003 | "../a/file", | |
| 1004 | normalizePath(try dir.readLink("b/symlink", &buf)), | |
| 1005 | ); | |
| 1006 | } | |
| 1007 | ||
| 1008 | fn normalizePath(bytes: []u8) []u8 { | |
| 1009 | const canonical_sep = std.fs.path.sep_posix; | |
| 1010 | if (std.fs.path.sep == canonical_sep) return bytes; | |
| 1011 | std.mem.replaceScalar(u8, bytes, std.fs.path.sep, canonical_sep); | |
| 1012 | return bytes; | |
| 1013 | } |
lib/std/tar/test.zig+338-323| ... | ... | @@ -1,328 +1,347 @@ |
| 1 | const std = @import("../std.zig"); | |
| 2 | const tar = std.tar; | |
| 1 | const std = @import("std"); | |
| 2 | const tar = @import("../tar.zig"); | |
| 3 | 3 | const testing = std.testing; |
| 4 | 4 | |
| 5 | test "tar run Go test cases" { | |
| 6 | const Case = struct { | |
| 7 | const File = struct { | |
| 8 | name: []const u8, | |
| 9 | size: u64 = 0, | |
| 10 | mode: u32 = 0, | |
| 11 | link_name: []const u8 = &[0]u8{}, | |
| 12 | kind: tar.Header.Kind = .normal, | |
| 13 | truncated: bool = false, // when there is no file body, just header, usefull for huge files | |
| 14 | }; | |
| 15 | ||
| 16 | data: []const u8, // testdata file content | |
| 17 | files: []const File = &[_]@This().File{}, // expected files to found in archive | |
| 18 | chksums: []const []const u8 = &[_][]const u8{}, // chksums of each file content | |
| 19 | err: ?anyerror = null, // parsing should fail with this error | |
| 5 | const Case = struct { | |
| 6 | const File = struct { | |
| 7 | name: []const u8, | |
| 8 | size: u64 = 0, | |
| 9 | mode: u32 = 0, | |
| 10 | link_name: []const u8 = &[0]u8{}, | |
| 11 | kind: tar.FileKind = .file, | |
| 12 | truncated: bool = false, // when there is no file body, just header, usefull for huge files | |
| 20 | 13 | }; |
| 21 | 14 | |
| 22 | const cases = [_]Case{ | |
| 23 | .{ | |
| 24 | .data = @embedFile("testdata/gnu.tar"), | |
| 25 | .files = &[_]Case.File{ | |
| 26 | .{ | |
| 27 | .name = "small.txt", | |
| 28 | .size = 5, | |
| 29 | .mode = 0o640, | |
| 30 | }, | |
| 31 | .{ | |
| 32 | .name = "small2.txt", | |
| 33 | .size = 11, | |
| 34 | .mode = 0o640, | |
| 35 | }, | |
| 15 | data: []const u8, // testdata file content | |
| 16 | files: []const File = &[_]@This().File{}, // expected files to found in archive | |
| 17 | chksums: []const []const u8 = &[_][]const u8{}, // chksums of each file content | |
| 18 | err: ?anyerror = null, // parsing should fail with this error | |
| 19 | }; | |
| 20 | ||
| 21 | const cases = [_]Case{ | |
| 22 | .{ | |
| 23 | .data = @embedFile("testdata/gnu.tar"), | |
| 24 | .files = &[_]Case.File{ | |
| 25 | .{ | |
| 26 | .name = "small.txt", | |
| 27 | .size = 5, | |
| 28 | .mode = 0o640, | |
| 36 | 29 | }, |
| 37 | .chksums = &[_][]const u8{ | |
| 38 | "e38b27eaccb4391bdec553a7f3ae6b2f", | |
| 39 | "c65bd2e50a56a2138bf1716f2fd56fe9", | |
| 30 | .{ | |
| 31 | .name = "small2.txt", | |
| 32 | .size = 11, | |
| 33 | .mode = 0o640, | |
| 40 | 34 | }, |
| 41 | 35 | }, |
| 42 | .{ | |
| 43 | .data = @embedFile("testdata/sparse-formats.tar"), | |
| 44 | .err = error.TarUnsupportedHeader, | |
| 36 | .chksums = &[_][]const u8{ | |
| 37 | "e38b27eaccb4391bdec553a7f3ae6b2f", | |
| 38 | "c65bd2e50a56a2138bf1716f2fd56fe9", | |
| 45 | 39 | }, |
| 46 | .{ | |
| 47 | .data = @embedFile("testdata/star.tar"), | |
| 48 | .files = &[_]Case.File{ | |
| 49 | .{ | |
| 50 | .name = "small.txt", | |
| 51 | .size = 5, | |
| 52 | .mode = 0o640, | |
| 53 | }, | |
| 54 | .{ | |
| 55 | .name = "small2.txt", | |
| 56 | .size = 11, | |
| 57 | .mode = 0o640, | |
| 58 | }, | |
| 40 | }, | |
| 41 | .{ | |
| 42 | .data = @embedFile("testdata/sparse-formats.tar"), | |
| 43 | .err = error.TarUnsupportedHeader, | |
| 44 | }, | |
| 45 | .{ | |
| 46 | .data = @embedFile("testdata/star.tar"), | |
| 47 | .files = &[_]Case.File{ | |
| 48 | .{ | |
| 49 | .name = "small.txt", | |
| 50 | .size = 5, | |
| 51 | .mode = 0o640, | |
| 59 | 52 | }, |
| 60 | .chksums = &[_][]const u8{ | |
| 61 | "e38b27eaccb4391bdec553a7f3ae6b2f", | |
| 62 | "c65bd2e50a56a2138bf1716f2fd56fe9", | |
| 53 | .{ | |
| 54 | .name = "small2.txt", | |
| 55 | .size = 11, | |
| 56 | .mode = 0o640, | |
| 63 | 57 | }, |
| 64 | 58 | }, |
| 65 | .{ | |
| 66 | .data = @embedFile("testdata/v7.tar"), | |
| 67 | .files = &[_]Case.File{ | |
| 68 | .{ | |
| 69 | .name = "small.txt", | |
| 70 | .size = 5, | |
| 71 | .mode = 0o444, | |
| 72 | }, | |
| 73 | .{ | |
| 74 | .name = "small2.txt", | |
| 75 | .size = 11, | |
| 76 | .mode = 0o444, | |
| 77 | }, | |
| 78 | }, | |
| 79 | .chksums = &[_][]const u8{ | |
| 80 | "e38b27eaccb4391bdec553a7f3ae6b2f", | |
| 81 | "c65bd2e50a56a2138bf1716f2fd56fe9", | |
| 82 | }, | |
| 59 | .chksums = &[_][]const u8{ | |
| 60 | "e38b27eaccb4391bdec553a7f3ae6b2f", | |
| 61 | "c65bd2e50a56a2138bf1716f2fd56fe9", | |
| 83 | 62 | }, |
| 84 | .{ | |
| 85 | .data = @embedFile("testdata/pax.tar"), | |
| 86 | .files = &[_]Case.File{ | |
| 87 | .{ | |
| 88 | .name = "a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100", | |
| 89 | .size = 7, | |
| 90 | .mode = 0o664, | |
| 91 | }, | |
| 92 | .{ | |
| 93 | .name = "a/b", | |
| 94 | .size = 0, | |
| 95 | .kind = .symbolic_link, | |
| 96 | .mode = 0o777, | |
| 97 | .link_name = "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100", | |
| 98 | }, | |
| 63 | }, | |
| 64 | .{ | |
| 65 | .data = @embedFile("testdata/v7.tar"), | |
| 66 | .files = &[_]Case.File{ | |
| 67 | .{ | |
| 68 | .name = "small.txt", | |
| 69 | .size = 5, | |
| 70 | .mode = 0o444, | |
| 99 | 71 | }, |
| 100 | .chksums = &[_][]const u8{ | |
| 101 | "3c382e8f5b6631aa2db52643912ffd4a", | |
| 72 | .{ | |
| 73 | .name = "small2.txt", | |
| 74 | .size = 11, | |
| 75 | .mode = 0o444, | |
| 102 | 76 | }, |
| 103 | 77 | }, |
| 104 | .{ | |
| 105 | // pax attribute don't end with \n | |
| 106 | .data = @embedFile("testdata/pax-bad-hdr-file.tar"), | |
| 107 | .err = error.PaxInvalidAttributeEnd, | |
| 78 | .chksums = &[_][]const u8{ | |
| 79 | "e38b27eaccb4391bdec553a7f3ae6b2f", | |
| 80 | "c65bd2e50a56a2138bf1716f2fd56fe9", | |
| 108 | 81 | }, |
| 109 | .{ | |
| 110 | // size is in pax attribute | |
| 111 | .data = @embedFile("testdata/pax-pos-size-file.tar"), | |
| 112 | .files = &[_]Case.File{ | |
| 113 | .{ | |
| 114 | .name = "foo", | |
| 115 | .size = 999, | |
| 116 | .kind = .normal, | |
| 117 | .mode = 0o640, | |
| 118 | }, | |
| 82 | }, | |
| 83 | .{ | |
| 84 | .data = @embedFile("testdata/pax.tar"), | |
| 85 | .files = &[_]Case.File{ | |
| 86 | .{ | |
| 87 | .name = "a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100", | |
| 88 | .size = 7, | |
| 89 | .mode = 0o664, | |
| 119 | 90 | }, |
| 120 | .chksums = &[_][]const u8{ | |
| 121 | "0afb597b283fe61b5d4879669a350556", | |
| 91 | .{ | |
| 92 | .name = "a/b", | |
| 93 | .size = 0, | |
| 94 | .kind = .sym_link, | |
| 95 | .mode = 0o777, | |
| 96 | .link_name = "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100", | |
| 122 | 97 | }, |
| 123 | 98 | }, |
| 124 | .{ | |
| 125 | // has pax records which we are not interested in | |
| 126 | .data = @embedFile("testdata/pax-records.tar"), | |
| 127 | .files = &[_]Case.File{ | |
| 128 | .{ | |
| 129 | .name = "file", | |
| 130 | }, | |
| 99 | .chksums = &[_][]const u8{ | |
| 100 | "3c382e8f5b6631aa2db52643912ffd4a", | |
| 101 | }, | |
| 102 | }, | |
| 103 | .{ | |
| 104 | // pax attribute don't end with \n | |
| 105 | .data = @embedFile("testdata/pax-bad-hdr-file.tar"), | |
| 106 | .err = error.PaxInvalidAttributeEnd, | |
| 107 | }, | |
| 108 | .{ | |
| 109 | // size is in pax attribute | |
| 110 | .data = @embedFile("testdata/pax-pos-size-file.tar"), | |
| 111 | .files = &[_]Case.File{ | |
| 112 | .{ | |
| 113 | .name = "foo", | |
| 114 | .size = 999, | |
| 115 | .kind = .file, | |
| 116 | .mode = 0o640, | |
| 131 | 117 | }, |
| 132 | 118 | }, |
| 133 | .{ | |
| 134 | // has global records which we are ignoring | |
| 135 | .data = @embedFile("testdata/pax-global-records.tar"), | |
| 136 | .files = &[_]Case.File{ | |
| 137 | .{ | |
| 138 | .name = "file1", | |
| 139 | }, | |
| 140 | .{ | |
| 141 | .name = "file2", | |
| 142 | }, | |
| 143 | .{ | |
| 144 | .name = "file3", | |
| 145 | }, | |
| 146 | .{ | |
| 147 | .name = "file4", | |
| 148 | }, | |
| 119 | .chksums = &[_][]const u8{ | |
| 120 | "0afb597b283fe61b5d4879669a350556", | |
| 121 | }, | |
| 122 | }, | |
| 123 | .{ | |
| 124 | // has pax records which we are not interested in | |
| 125 | .data = @embedFile("testdata/pax-records.tar"), | |
| 126 | .files = &[_]Case.File{ | |
| 127 | .{ | |
| 128 | .name = "file", | |
| 149 | 129 | }, |
| 150 | 130 | }, |
| 151 | .{ | |
| 152 | .data = @embedFile("testdata/nil-uid.tar"), | |
| 153 | .files = &[_]Case.File{ | |
| 154 | .{ | |
| 155 | .name = "P1050238.JPG.log", | |
| 156 | .size = 14, | |
| 157 | .kind = .normal, | |
| 158 | .mode = 0o664, | |
| 159 | }, | |
| 131 | }, | |
| 132 | .{ | |
| 133 | // has global records which we are ignoring | |
| 134 | .data = @embedFile("testdata/pax-global-records.tar"), | |
| 135 | .files = &[_]Case.File{ | |
| 136 | .{ | |
| 137 | .name = "file1", | |
| 160 | 138 | }, |
| 161 | .chksums = &[_][]const u8{ | |
| 162 | "08d504674115e77a67244beac19668f5", | |
| 139 | .{ | |
| 140 | .name = "file2", | |
| 163 | 141 | }, |
| 164 | }, | |
| 165 | .{ | |
| 166 | // has xattrs and pax records which we are ignoring | |
| 167 | .data = @embedFile("testdata/xattrs.tar"), | |
| 168 | .files = &[_]Case.File{ | |
| 169 | .{ | |
| 170 | .name = "small.txt", | |
| 171 | .size = 5, | |
| 172 | .kind = .normal, | |
| 173 | .mode = 0o644, | |
| 174 | }, | |
| 175 | .{ | |
| 176 | .name = "small2.txt", | |
| 177 | .size = 11, | |
| 178 | .kind = .normal, | |
| 179 | .mode = 0o644, | |
| 180 | }, | |
| 142 | .{ | |
| 143 | .name = "file3", | |
| 181 | 144 | }, |
| 182 | .chksums = &[_][]const u8{ | |
| 183 | "e38b27eaccb4391bdec553a7f3ae6b2f", | |
| 184 | "c65bd2e50a56a2138bf1716f2fd56fe9", | |
| 145 | .{ | |
| 146 | .name = "file4", | |
| 185 | 147 | }, |
| 186 | 148 | }, |
| 187 | .{ | |
| 188 | .data = @embedFile("testdata/gnu-multi-hdrs.tar"), | |
| 189 | .files = &[_]Case.File{ | |
| 190 | .{ | |
| 191 | .name = "GNU2/GNU2/long-path-name", | |
| 192 | .link_name = "GNU4/GNU4/long-linkpath-name", | |
| 193 | .kind = .symbolic_link, | |
| 194 | }, | |
| 149 | }, | |
| 150 | .{ | |
| 151 | .data = @embedFile("testdata/nil-uid.tar"), | |
| 152 | .files = &[_]Case.File{ | |
| 153 | .{ | |
| 154 | .name = "P1050238.JPG.log", | |
| 155 | .size = 14, | |
| 156 | .kind = .file, | |
| 157 | .mode = 0o664, | |
| 195 | 158 | }, |
| 196 | 159 | }, |
| 197 | .{ | |
| 198 | // has gnu type D (directory) and S (sparse) blocks | |
| 199 | .data = @embedFile("testdata/gnu-incremental.tar"), | |
| 200 | .err = error.TarUnsupportedHeader, | |
| 160 | .chksums = &[_][]const u8{ | |
| 161 | "08d504674115e77a67244beac19668f5", | |
| 201 | 162 | }, |
| 202 | .{ | |
| 203 | // should use values only from last pax header | |
| 204 | .data = @embedFile("testdata/pax-multi-hdrs.tar"), | |
| 205 | .files = &[_]Case.File{ | |
| 206 | .{ | |
| 207 | .name = "bar", | |
| 208 | .link_name = "PAX4/PAX4/long-linkpath-name", | |
| 209 | .kind = .symbolic_link, | |
| 210 | }, | |
| 163 | }, | |
| 164 | .{ | |
| 165 | // has xattrs and pax records which we are ignoring | |
| 166 | .data = @embedFile("testdata/xattrs.tar"), | |
| 167 | .files = &[_]Case.File{ | |
| 168 | .{ | |
| 169 | .name = "small.txt", | |
| 170 | .size = 5, | |
| 171 | .kind = .file, | |
| 172 | .mode = 0o644, | |
| 211 | 173 | }, |
| 212 | }, | |
| 213 | .{ | |
| 214 | .data = @embedFile("testdata/gnu-long-nul.tar"), | |
| 215 | .files = &[_]Case.File{ | |
| 216 | .{ | |
| 217 | .name = "0123456789", | |
| 218 | .mode = 0o644, | |
| 219 | }, | |
| 174 | .{ | |
| 175 | .name = "small2.txt", | |
| 176 | .size = 11, | |
| 177 | .kind = .file, | |
| 178 | .mode = 0o644, | |
| 220 | 179 | }, |
| 221 | 180 | }, |
| 222 | .{ | |
| 223 | .data = @embedFile("testdata/gnu-utf8.tar"), | |
| 224 | .files = &[_]Case.File{ | |
| 225 | .{ | |
| 226 | .name = "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹", | |
| 227 | .mode = 0o644, | |
| 228 | }, | |
| 229 | }, | |
| 181 | .chksums = &[_][]const u8{ | |
| 182 | "e38b27eaccb4391bdec553a7f3ae6b2f", | |
| 183 | "c65bd2e50a56a2138bf1716f2fd56fe9", | |
| 230 | 184 | }, |
| 231 | .{ | |
| 232 | .data = @embedFile("testdata/gnu-not-utf8.tar"), | |
| 233 | .files = &[_]Case.File{ | |
| 234 | .{ | |
| 235 | .name = "hi\x80\x81\x82\x83bye", | |
| 236 | .mode = 0o644, | |
| 237 | }, | |
| 185 | }, | |
| 186 | .{ | |
| 187 | .data = @embedFile("testdata/gnu-multi-hdrs.tar"), | |
| 188 | .files = &[_]Case.File{ | |
| 189 | .{ | |
| 190 | .name = "GNU2/GNU2/long-path-name", | |
| 191 | .link_name = "GNU4/GNU4/long-linkpath-name", | |
| 192 | .kind = .sym_link, | |
| 238 | 193 | }, |
| 239 | 194 | }, |
| 240 | .{ | |
| 241 | // null in pax key | |
| 242 | .data = @embedFile("testdata/pax-nul-xattrs.tar"), | |
| 243 | .err = error.PaxNullInKeyword, | |
| 244 | }, | |
| 245 | .{ | |
| 246 | .data = @embedFile("testdata/pax-nul-path.tar"), | |
| 247 | .err = error.PaxNullInValue, | |
| 248 | }, | |
| 249 | .{ | |
| 250 | .data = @embedFile("testdata/neg-size.tar"), | |
| 251 | .err = error.TarHeader, | |
| 252 | }, | |
| 253 | .{ | |
| 254 | .data = @embedFile("testdata/issue10968.tar"), | |
| 255 | .err = error.TarHeader, | |
| 256 | }, | |
| 257 | .{ | |
| 258 | .data = @embedFile("testdata/issue11169.tar"), | |
| 259 | .err = error.TarHeader, | |
| 195 | }, | |
| 196 | .{ | |
| 197 | // has gnu type D (directory) and S (sparse) blocks | |
| 198 | .data = @embedFile("testdata/gnu-incremental.tar"), | |
| 199 | .err = error.TarUnsupportedHeader, | |
| 200 | }, | |
| 201 | .{ | |
| 202 | // should use values only from last pax header | |
| 203 | .data = @embedFile("testdata/pax-multi-hdrs.tar"), | |
| 204 | .files = &[_]Case.File{ | |
| 205 | .{ | |
| 206 | .name = "bar", | |
| 207 | .link_name = "PAX4/PAX4/long-linkpath-name", | |
| 208 | .kind = .sym_link, | |
| 209 | }, | |
| 260 | 210 | }, |
| 261 | .{ | |
| 262 | .data = @embedFile("testdata/issue12435.tar"), | |
| 263 | .err = error.TarHeaderChksum, | |
| 211 | }, | |
| 212 | .{ | |
| 213 | .data = @embedFile("testdata/gnu-long-nul.tar"), | |
| 214 | .files = &[_]Case.File{ | |
| 215 | .{ | |
| 216 | .name = "0123456789", | |
| 217 | .mode = 0o644, | |
| 218 | }, | |
| 264 | 219 | }, |
| 265 | .{ | |
| 266 | // has magic with space at end instead of null | |
| 267 | .data = @embedFile("testdata/invalid-go17.tar"), | |
| 268 | .files = &[_]Case.File{ | |
| 269 | .{ | |
| 270 | .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo", | |
| 271 | }, | |
| 220 | }, | |
| 221 | .{ | |
| 222 | .data = @embedFile("testdata/gnu-utf8.tar"), | |
| 223 | .files = &[_]Case.File{ | |
| 224 | .{ | |
| 225 | .name = "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹", | |
| 226 | .mode = 0o644, | |
| 272 | 227 | }, |
| 273 | 228 | }, |
| 274 | .{ | |
| 275 | .data = @embedFile("testdata/ustar-file-devs.tar"), | |
| 276 | .files = &[_]Case.File{ | |
| 277 | .{ | |
| 278 | .name = "file", | |
| 279 | .mode = 0o644, | |
| 280 | }, | |
| 229 | }, | |
| 230 | .{ | |
| 231 | .data = @embedFile("testdata/gnu-not-utf8.tar"), | |
| 232 | .files = &[_]Case.File{ | |
| 233 | .{ | |
| 234 | .name = "hi\x80\x81\x82\x83bye", | |
| 235 | .mode = 0o644, | |
| 281 | 236 | }, |
| 282 | 237 | }, |
| 283 | .{ | |
| 284 | .data = @embedFile("testdata/trailing-slash.tar"), | |
| 285 | .files = &[_]Case.File{ | |
| 286 | .{ | |
| 287 | .name = "123456789/" ** 30, | |
| 288 | .kind = .directory, | |
| 289 | }, | |
| 238 | }, | |
| 239 | .{ | |
| 240 | // null in pax key | |
| 241 | .data = @embedFile("testdata/pax-nul-xattrs.tar"), | |
| 242 | .err = error.PaxNullInKeyword, | |
| 243 | }, | |
| 244 | .{ | |
| 245 | .data = @embedFile("testdata/pax-nul-path.tar"), | |
| 246 | .err = error.PaxNullInValue, | |
| 247 | }, | |
| 248 | .{ | |
| 249 | .data = @embedFile("testdata/neg-size.tar"), | |
| 250 | .err = error.TarHeader, | |
| 251 | }, | |
| 252 | .{ | |
| 253 | .data = @embedFile("testdata/issue10968.tar"), | |
| 254 | .err = error.TarHeader, | |
| 255 | }, | |
| 256 | .{ | |
| 257 | .data = @embedFile("testdata/issue11169.tar"), | |
| 258 | .err = error.TarHeader, | |
| 259 | }, | |
| 260 | .{ | |
| 261 | .data = @embedFile("testdata/issue12435.tar"), | |
| 262 | .err = error.TarHeaderChksum, | |
| 263 | }, | |
| 264 | .{ | |
| 265 | // has magic with space at end instead of null | |
| 266 | .data = @embedFile("testdata/invalid-go17.tar"), | |
| 267 | .files = &[_]Case.File{ | |
| 268 | .{ | |
| 269 | .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo", | |
| 290 | 270 | }, |
| 291 | 271 | }, |
| 292 | .{ | |
| 293 | // Has size in gnu extended format. To represent size bigger than 8 GB. | |
| 294 | .data = @embedFile("testdata/writer-big.tar"), | |
| 295 | .files = &[_]Case.File{ | |
| 296 | .{ | |
| 297 | .name = "tmp/16gig.txt", | |
| 298 | .size = 16 * 1024 * 1024 * 1024, | |
| 299 | .truncated = true, | |
| 300 | .mode = 0o640, | |
| 301 | }, | |
| 272 | }, | |
| 273 | .{ | |
| 274 | .data = @embedFile("testdata/ustar-file-devs.tar"), | |
| 275 | .files = &[_]Case.File{ | |
| 276 | .{ | |
| 277 | .name = "file", | |
| 278 | .mode = 0o644, | |
| 302 | 279 | }, |
| 303 | 280 | }, |
| 304 | .{ | |
| 305 | // Size in gnu extended format, and name in pax attribute. | |
| 306 | .data = @embedFile("testdata/writer-big-long.tar"), | |
| 307 | .files = &[_]Case.File{ | |
| 308 | .{ | |
| 309 | .name = "longname/" ** 15 ++ "16gig.txt", | |
| 310 | .size = 16 * 1024 * 1024 * 1024, | |
| 311 | .mode = 0o644, | |
| 312 | .truncated = true, | |
| 313 | }, | |
| 281 | }, | |
| 282 | .{ | |
| 283 | .data = @embedFile("testdata/trailing-slash.tar"), | |
| 284 | .files = &[_]Case.File{ | |
| 285 | .{ | |
| 286 | .name = "123456789/" ** 30, | |
| 287 | .kind = .directory, | |
| 314 | 288 | }, |
| 315 | 289 | }, |
| 316 | .{ | |
| 317 | .data = @embedFile("testdata/fuzz1.tar"), | |
| 318 | .err = error.TarCorruptInput, | |
| 290 | }, | |
| 291 | .{ | |
| 292 | // Has size in gnu extended format. To represent size bigger than 8 GB. | |
| 293 | .data = @embedFile("testdata/writer-big.tar"), | |
| 294 | .files = &[_]Case.File{ | |
| 295 | .{ | |
| 296 | .name = "tmp/16gig.txt", | |
| 297 | .size = 16 * 1024 * 1024 * 1024, | |
| 298 | .truncated = true, | |
| 299 | .mode = 0o640, | |
| 300 | }, | |
| 319 | 301 | }, |
| 320 | .{ | |
| 321 | .data = @embedFile("testdata/fuzz2.tar"), | |
| 322 | .err = error.PaxSizeAttrOverflow, | |
| 302 | }, | |
| 303 | .{ | |
| 304 | // Size in gnu extended format, and name in pax attribute. | |
| 305 | .data = @embedFile("testdata/writer-big-long.tar"), | |
| 306 | .files = &[_]Case.File{ | |
| 307 | .{ | |
| 308 | .name = "longname/" ** 15 ++ "16gig.txt", | |
| 309 | .size = 16 * 1024 * 1024 * 1024, | |
| 310 | .mode = 0o644, | |
| 311 | .truncated = true, | |
| 312 | }, | |
| 323 | 313 | }, |
| 324 | }; | |
| 314 | }, | |
| 315 | .{ | |
| 316 | .data = @embedFile("testdata/fuzz1.tar"), | |
| 317 | .err = error.TarInsufficientBuffer, | |
| 318 | }, | |
| 319 | .{ | |
| 320 | .data = @embedFile("testdata/fuzz2.tar"), | |
| 321 | .err = error.PaxSizeAttrOverflow, | |
| 322 | }, | |
| 323 | }; | |
| 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 | } | |
| 325 | 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 | ||
| 344 | test "run test cases" { | |
| 326 | 345 | var file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; |
| 327 | 346 | var link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined; |
| 328 | 347 | |
| ... | ... | @@ -350,37 +369,65 @@ test "tar run Go test cases" { |
| 350 | 369 | |
| 351 | 370 | if (case.chksums.len > i) { |
| 352 | 371 | var md5writer = Md5Writer{}; |
| 353 | try actual.write(&md5writer); | |
| 372 | try actual.writeAll(&md5writer); | |
| 354 | 373 | const chksum = md5writer.chksum(); |
| 355 | 374 | try testing.expectEqualStrings(case.chksums[i], &chksum); |
| 356 | 375 | } else { |
| 357 | if (!expected.truncated) try actual.skip(); // skip file content | |
| 376 | if (expected.truncated) { | |
| 377 | iter.unread_file_bytes = 0; | |
| 378 | } | |
| 358 | 379 | } |
| 359 | 380 | } |
| 360 | 381 | try testing.expectEqual(case.files.len, i); |
| 361 | 382 | } |
| 362 | 383 | } |
| 363 | 384 | |
| 364 | // used in test to calculate file chksum | |
| 365 | const Md5Writer = struct { | |
| 366 | h: std.crypto.hash.Md5 = std.crypto.hash.Md5.init(.{}), | |
| 385 | test "pax/gnu long names with small buffer" { | |
| 386 | // should fail with insufficient buffer error | |
| 367 | 387 | |
| 368 | pub fn writeAll(self: *Md5Writer, buf: []const u8) !void { | |
| 369 | self.h.update(buf); | |
| 370 | } | |
| 388 | var min_file_name_buffer: [256]u8 = undefined; | |
| 389 | var min_link_name_buffer: [100]u8 = undefined; | |
| 390 | const long_name_cases = [_]Case{ cases[11], cases[25], cases[28] }; | |
| 371 | 391 | |
| 372 | pub fn writeByte(self: *Md5Writer, byte: u8) !void { | |
| 373 | self.h.update(&[_]u8{byte}); | |
| 374 | } | |
| 392 | for (long_name_cases) |case| { | |
| 393 | var fsb = std.io.fixedBufferStream(case.data); | |
| 394 | var iter = tar.iterator(fsb.reader(), .{ | |
| 395 | .file_name_buffer = &min_file_name_buffer, | |
| 396 | .link_name_buffer = &min_link_name_buffer, | |
| 397 | }); | |
| 375 | 398 | |
| 376 | pub fn chksum(self: *Md5Writer) [32]u8 { | |
| 377 | var s = [_]u8{0} ** 16; | |
| 378 | self.h.final(&s); | |
| 379 | return std.fmt.bytesToHex(s, .lower); | |
| 399 | var iter_err: ?anyerror = null; | |
| 400 | while (iter.next() catch |err| brk: { | |
| 401 | iter_err = err; | |
| 402 | break :brk null; | |
| 403 | }) |_| {} | |
| 404 | ||
| 405 | try testing.expect(iter_err != null); | |
| 406 | try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?); | |
| 380 | 407 | } |
| 381 | }; | |
| 408 | } | |
| 409 | ||
| 410 | test "insufficient buffer in Header name filed" { | |
| 411 | var min_file_name_buffer: [9]u8 = undefined; | |
| 412 | var min_link_name_buffer: [100]u8 = undefined; | |
| 413 | ||
| 414 | var fsb = std.io.fixedBufferStream(cases[0].data); | |
| 415 | var iter = tar.iterator(fsb.reader(), .{ | |
| 416 | .file_name_buffer = &min_file_name_buffer, | |
| 417 | .link_name_buffer = &min_link_name_buffer, | |
| 418 | }); | |
| 419 | ||
| 420 | var iter_err: ?anyerror = null; | |
| 421 | while (iter.next() catch |err| brk: { | |
| 422 | iter_err = err; | |
| 423 | break :brk null; | |
| 424 | }) |_| {} | |
| 425 | ||
| 426 | try testing.expect(iter_err != null); | |
| 427 | try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?); | |
| 428 | } | |
| 382 | 429 | |
| 383 | test "tar should not overwrite existing file" { | |
| 430 | test "should not overwrite existing file" { | |
| 384 | 431 | // Starting from this folder structure: |
| 385 | 432 | // $ tree root |
| 386 | 433 | // root |
| ... | ... | @@ -436,7 +483,7 @@ test "tar should not overwrite existing file" { |
| 436 | 483 | try tar.pipeToFileSystem(root2.dir, fsb.reader(), .{ .mode_mode = .ignore, .strip_components = 0 }); |
| 437 | 484 | } |
| 438 | 485 | |
| 439 | test "tar case sensitivity" { | |
| 486 | test "case sensitivity" { | |
| 440 | 487 | // Mimicking issue #18089, this tar contains, same file name in two case |
| 441 | 488 | // sensitive name version. Should fail on case insensitive file systems. |
| 442 | 489 | // |
| ... | ... | @@ -462,35 +509,3 @@ test "tar case sensitivity" { |
| 462 | 509 | try testing.expect((try root.dir.statFile("alacritty/darkermatrix.yml")).kind == .file); |
| 463 | 510 | try testing.expect((try root.dir.statFile("alacritty/Darkermatrix.yml")).kind == .file); |
| 464 | 511 | } |
| 465 | ||
| 466 | test "tar pipeToFileSystem" { | |
| 467 | // $ tar tvf | |
| 468 | // pipe_to_file_system_test/ | |
| 469 | // pipe_to_file_system_test/b/ | |
| 470 | // pipe_to_file_system_test/b/symlink -> ../a/file | |
| 471 | // pipe_to_file_system_test/a/ | |
| 472 | // pipe_to_file_system_test/a/file | |
| 473 | // pipe_to_file_system_test/empty/ | |
| 474 | const data = @embedFile("testdata/pipe_to_file_system_test.tar"); | |
| 475 | var fsb = std.io.fixedBufferStream(data); | |
| 476 | ||
| 477 | var root = std.testing.tmpDir(.{ .no_follow = true }); | |
| 478 | defer root.cleanup(); | |
| 479 | ||
| 480 | tar.pipeToFileSystem(root.dir, fsb.reader(), .{ | |
| 481 | .mode_mode = .ignore, | |
| 482 | .strip_components = 1, | |
| 483 | .exclude_empty_directories = true, | |
| 484 | }) catch |err| { | |
| 485 | // Skip on platform which don't support symlinks | |
| 486 | if (err == error.UnableToCreateSymLink) return error.SkipZigTest; | |
| 487 | return err; | |
| 488 | }; | |
| 489 | ||
| 490 | try testing.expectError(error.FileNotFound, root.dir.statFile("empty")); | |
| 491 | try testing.expect((try root.dir.statFile("a/file")).kind == .file); | |
| 492 | // TODO is there better way to test symlink | |
| 493 | try testing.expect((try root.dir.statFile("b/symlink")).kind == .file); // statFile follows symlink | |
| 494 | var buf: [32]u8 = undefined; | |
| 495 | _ = try root.dir.readLink("b/symlink", &buf); | |
| 496 | } |
lib/std/tar/testdata/example.tar created| Binary files /dev/null and b/lib/std/tar/testdata/example.tar differ |
lib/std/tar/testdata/pipe_to_file_system_test.tar deleted| Binary files a/lib/std/tar/testdata/pipe_to_file_system_test.tar and /dev/null differ |
src/Package/Fetch.zig+1-1| ... | ... | @@ -1147,7 +1147,7 @@ fn unpackTarball(f: *Fetch, out_dir: fs.Dir, reader: anytype) RunError!void { |
| 1147 | 1147 | const eb = &f.error_bundle; |
| 1148 | 1148 | const gpa = f.arena.child_allocator; |
| 1149 | 1149 | |
| 1150 | var diagnostics: std.tar.Options.Diagnostics = .{ .allocator = gpa }; | |
| 1150 | var diagnostics: std.tar.Diagnostics = .{ .allocator = gpa }; | |
| 1151 | 1151 | defer diagnostics.deinit(); |
| 1152 | 1152 | |
| 1153 | 1153 | std.tar.pipeToFileSystem(out_dir, reader, .{ |