master
  1const std = @import("std");
  2const tar = @import("../tar.zig");
  3const testing = std.testing;
  4
  5const 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, useful for huge files
 13    };
 14
 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
 21const gnu_case: Case = .{
 22    .data = @embedFile("testdata/gnu.tar"),
 23    .files = &[_]Case.File{
 24        .{
 25            .name = "small.txt",
 26            .size = 5,
 27            .mode = 0o640,
 28        },
 29        .{
 30            .name = "small2.txt",
 31            .size = 11,
 32            .mode = 0o640,
 33        },
 34    },
 35    .chksums = &[_][]const u8{
 36        "e38b27eaccb4391bdec553a7f3ae6b2f",
 37        "c65bd2e50a56a2138bf1716f2fd56fe9",
 38    },
 39};
 40
 41const 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,
 48        },
 49    },
 50};
 51
 52const 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
 62const 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
 75const fuzz1_case: Case = .{
 76    .data = @embedFile("testdata/fuzz1.tar"),
 77    .err = error.TarInsufficientBuffer,
 78};
 79
 80test "run test cases" {
 81    try testCase(gnu_case);
 82    try testCase(.{
 83        .data = @embedFile("testdata/sparse-formats.tar"),
 84        .err = error.TarUnsupportedHeader,
 85    });
 86    try testCase(.{
 87        .data = @embedFile("testdata/star.tar"),
 88        .files = &[_]Case.File{
 89            .{
 90                .name = "small.txt",
 91                .size = 5,
 92                .mode = 0o640,
 93            },
 94            .{
 95                .name = "small2.txt",
 96                .size = 11,
 97                .mode = 0o640,
 98            },
 99        },
100        .chksums = &[_][]const u8{
101            "e38b27eaccb4391bdec553a7f3ae6b2f",
102            "c65bd2e50a56a2138bf1716f2fd56fe9",
103        },
104    });
105    try testCase(.{
106        .data = @embedFile("testdata/v7.tar"),
107        .files = &[_]Case.File{
108            .{
109                .name = "small.txt",
110                .size = 5,
111                .mode = 0o444,
112            },
113            .{
114                .name = "small2.txt",
115                .size = 11,
116                .mode = 0o444,
117            },
118        },
119        .chksums = &[_][]const u8{
120            "e38b27eaccb4391bdec553a7f3ae6b2f",
121            "c65bd2e50a56a2138bf1716f2fd56fe9",
122        },
123    });
124    try testCase(.{
125        .data = @embedFile("testdata/pax.tar"),
126        .files = &[_]Case.File{
127            .{
128                .name = "a/123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
129                .size = 7,
130                .mode = 0o664,
131            },
132            .{
133                .name = "a/b",
134                .size = 0,
135                .kind = .sym_link,
136                .mode = 0o777,
137                .link_name = "123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100",
138            },
139        },
140        .chksums = &[_][]const u8{
141            "3c382e8f5b6631aa2db52643912ffd4a",
142        },
143    });
144    try testCase(.{
145        // pax attribute don't end with \n
146        .data = @embedFile("testdata/pax-bad-hdr-file.tar"),
147        .err = error.PaxInvalidAttributeEnd,
148    });
149    try testCase(.{
150        // size is in pax attribute
151        .data = @embedFile("testdata/pax-pos-size-file.tar"),
152        .files = &[_]Case.File{
153            .{
154                .name = "foo",
155                .size = 999,
156                .kind = .file,
157                .mode = 0o640,
158            },
159        },
160        .chksums = &[_][]const u8{
161            "0afb597b283fe61b5d4879669a350556",
162        },
163    });
164    try testCase(.{
165        // has pax records which we are not interested in
166        .data = @embedFile("testdata/pax-records.tar"),
167        .files = &[_]Case.File{
168            .{
169                .name = "file",
170            },
171        },
172    });
173    try testCase(.{
174        // has global records which we are ignoring
175        .data = @embedFile("testdata/pax-global-records.tar"),
176        .files = &[_]Case.File{
177            .{
178                .name = "file1",
179            },
180            .{
181                .name = "file2",
182            },
183            .{
184                .name = "file3",
185            },
186            .{
187                .name = "file4",
188            },
189        },
190    });
191    try testCase(.{
192        .data = @embedFile("testdata/nil-uid.tar"),
193        .files = &[_]Case.File{
194            .{
195                .name = "P1050238.JPG.log",
196                .size = 14,
197                .kind = .file,
198                .mode = 0o664,
199            },
200        },
201        .chksums = &[_][]const u8{
202            "08d504674115e77a67244beac19668f5",
203        },
204    });
205    try testCase(.{
206        // has xattrs and pax records which we are ignoring
207        .data = @embedFile("testdata/xattrs.tar"),
208        .files = &[_]Case.File{
209            .{
210                .name = "small.txt",
211                .size = 5,
212                .kind = .file,
213                .mode = 0o644,
214            },
215            .{
216                .name = "small2.txt",
217                .size = 11,
218                .kind = .file,
219                .mode = 0o644,
220            },
221        },
222        .chksums = &[_][]const u8{
223            "e38b27eaccb4391bdec553a7f3ae6b2f",
224            "c65bd2e50a56a2138bf1716f2fd56fe9",
225        },
226    });
227    try testCase(gnu_multi_headers_case);
228    try testCase(.{
229        // has gnu type D (directory) and S (sparse) blocks
230        .data = @embedFile("testdata/gnu-incremental.tar"),
231        .err = error.TarUnsupportedHeader,
232    });
233    try testCase(.{
234        // should use values only from last pax header
235        .data = @embedFile("testdata/pax-multi-hdrs.tar"),
236        .files = &[_]Case.File{
237            .{
238                .name = "bar",
239                .link_name = "PAX4/PAX4/long-linkpath-name",
240                .kind = .sym_link,
241            },
242        },
243    });
244    try testCase(.{
245        .data = @embedFile("testdata/gnu-long-nul.tar"),
246        .files = &[_]Case.File{
247            .{
248                .name = "0123456789",
249                .mode = 0o644,
250            },
251        },
252    });
253    try testCase(.{
254        .data = @embedFile("testdata/gnu-utf8.tar"),
255        .files = &[_]Case.File{
256            .{
257                .name = "☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹☺☻☹",
258                .mode = 0o644,
259            },
260        },
261    });
262    try testCase(.{
263        .data = @embedFile("testdata/gnu-not-utf8.tar"),
264        .files = &[_]Case.File{
265            .{
266                .name = "hi\x80\x81\x82\x83bye",
267                .mode = 0o644,
268            },
269        },
270    });
271    try testCase(.{
272        // null in pax key
273        .data = @embedFile("testdata/pax-nul-xattrs.tar"),
274        .err = error.PaxNullInKeyword,
275    });
276    try testCase(.{
277        .data = @embedFile("testdata/pax-nul-path.tar"),
278        .err = error.PaxNullInValue,
279    });
280    try testCase(.{
281        .data = @embedFile("testdata/neg-size.tar"),
282        .err = error.TarHeader,
283    });
284    try testCase(.{
285        .data = @embedFile("testdata/issue10968.tar"),
286        .err = error.TarHeader,
287    });
288    try testCase(.{
289        .data = @embedFile("testdata/issue11169.tar"),
290        .err = error.TarHeader,
291    });
292    try testCase(.{
293        .data = @embedFile("testdata/issue12435.tar"),
294        .err = error.TarHeaderChksum,
295    });
296    try testCase(.{
297        // has magic with space at end instead of null
298        .data = @embedFile("testdata/invalid-go17.tar"),
299        .files = &[_]Case.File{
300            .{
301                .name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/foo",
302            },
303        },
304    });
305    try testCase(.{
306        .data = @embedFile("testdata/ustar-file-devs.tar"),
307        .files = &[_]Case.File{
308            .{
309                .name = "file",
310                .mode = 0o644,
311            },
312        },
313    });
314    try testCase(trailing_slash_case);
315    try testCase(.{
316        // Has size in gnu extended format. To represent size bigger than 8 GB.
317        .data = @embedFile("testdata/writer-big.tar"),
318        .files = &[_]Case.File{
319            .{
320                .name = "tmp/16gig.txt",
321                .size = 16 * 1024 * 1024 * 1024,
322                .truncated = true,
323                .mode = 0o640,
324            },
325        },
326    });
327    try testCase(writer_big_long_case);
328    try testCase(fuzz1_case);
329    try testCase(.{
330        .data = @embedFile("testdata/fuzz2.tar"),
331        .err = error.PaxSizeAttrOverflow,
332    });
333}
334
335fn testCase(case: Case) !void {
336    var file_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
337    var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined;
338
339    var br: std.Io.Reader = .fixed(case.data);
340    var it: tar.Iterator = .init(&br, .{
341        .file_name_buffer = &file_name_buffer,
342        .link_name_buffer = &link_name_buffer,
343    });
344    var i: usize = 0;
345    while (it.next() catch |err| {
346        if (case.err) |e| {
347            try testing.expectEqual(e, err);
348            return;
349        } else {
350            return err;
351        }
352    }) |actual| : (i += 1) {
353        const expected = case.files[i];
354        try testing.expectEqualStrings(expected.name, actual.name);
355        try testing.expectEqual(expected.size, actual.size);
356        try testing.expectEqual(expected.kind, actual.kind);
357        try testing.expectEqual(expected.mode, actual.mode);
358        try testing.expectEqualStrings(expected.link_name, actual.link_name);
359
360        if (case.chksums.len > i) {
361            var aw: std.Io.Writer.Allocating = .init(std.testing.allocator);
362            defer aw.deinit();
363            try it.streamRemaining(actual, &aw.writer);
364            const chksum = std.fmt.bytesToHex(std.crypto.hash.Md5.hashResult(aw.written()), .lower);
365            try testing.expectEqualStrings(case.chksums[i], &chksum);
366        } else {
367            if (expected.truncated) {
368                it.unread_file_bytes = 0;
369            }
370        }
371    }
372    try testing.expectEqual(case.files.len, i);
373}
374
375test "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
384fn testLongNameCase(case: Case) !void {
385    // should fail with insufficient buffer error
386
387    var min_file_name_buffer: [256]u8 = undefined;
388    var min_link_name_buffer: [100]u8 = undefined;
389
390    var br: std.Io.Reader = .fixed(case.data);
391    var iter: tar.Iterator = .init(&br, .{
392        .file_name_buffer = &min_file_name_buffer,
393        .link_name_buffer = &min_link_name_buffer,
394    });
395
396    var iter_err: ?anyerror = null;
397    while (iter.next() catch |err| brk: {
398        iter_err = err;
399        break :brk null;
400    }) |_| {}
401
402    try testing.expect(iter_err != null);
403    try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?);
404}
405
406test "insufficient buffer in Header name filed" {
407    var min_file_name_buffer: [9]u8 = undefined;
408    var min_link_name_buffer: [100]u8 = undefined;
409
410    var br: std.Io.Reader = .fixed(gnu_case.data);
411    var iter: tar.Iterator = .init(&br, .{
412        .file_name_buffer = &min_file_name_buffer,
413        .link_name_buffer = &min_link_name_buffer,
414    });
415
416    var iter_err: ?anyerror = null;
417    while (iter.next() catch |err| brk: {
418        iter_err = err;
419        break :brk null;
420    }) |_| {}
421
422    try testing.expect(iter_err != null);
423    try testing.expectEqual(error.TarInsufficientBuffer, iter_err.?);
424}
425
426test "should not overwrite existing file" {
427    // Starting from this folder structure:
428    // $ tree root
429    //    root
430    //    ├── a
431    //    │   └── b
432    //    │       └── c
433    //    │           └── file.txt
434    //    └── d
435    //        └── b
436    //            └── c
437    //                └── file.txt
438    //
439    // Packed with command:
440    // $ cd root; tar cf overwrite_file.tar *
441    // Resulting tar has following structure:
442    // $ tar tvf overwrite_file.tar
443    //  size path
444    //  0    a/
445    //  0    a/b/
446    //  0    a/b/c/
447    //  2    a/b/c/file.txt
448    //  0    d/
449    //  0    d/b/
450    //  0    d/b/c/
451    //  2    d/b/c/file.txt
452    //
453    // Note that there is no root folder in archive.
454    //
455    // With strip_components = 1 resulting unpacked folder was:
456    //  root
457    //     └── b
458    //         └── c
459    //             └── file.txt
460    //
461    // a/b/c/file.txt is overwritten with d/b/c/file.txt !!!
462    // This ensures that file is not overwritten.
463    //
464    const data = @embedFile("testdata/overwrite_file.tar");
465    var r: std.Io.Reader = .fixed(data);
466
467    // Unpack with strip_components = 1 should fail
468    var root = std.testing.tmpDir(.{});
469    defer root.cleanup();
470    try testing.expectError(
471        error.PathAlreadyExists,
472        tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }),
473    );
474
475    // Unpack with strip_components = 0 should pass
476    r = .fixed(data);
477    var root2 = std.testing.tmpDir(.{});
478    defer root2.cleanup();
479    try tar.pipeToFileSystem(root2.dir, &r, .{ .mode_mode = .ignore, .strip_components = 0 });
480}
481
482test "case sensitivity" {
483    // Mimicking issue #18089, this tar contains, same file name in two case
484    // sensitive name version. Should fail on case insensitive file systems.
485    //
486    // $ tar tvf 18089.tar
487    //     18089/
488    //     18089/alacritty/
489    //     18089/alacritty/darkermatrix.yml
490    //     18089/alacritty/Darkermatrix.yml
491    //
492    const data = @embedFile("testdata/18089.tar");
493    var r: std.Io.Reader = .fixed(data);
494
495    var root = std.testing.tmpDir(.{});
496    defer root.cleanup();
497
498    tar.pipeToFileSystem(root.dir, &r, .{ .mode_mode = .ignore, .strip_components = 1 }) catch |err| {
499        // on case insensitive fs we fail on overwrite existing file
500        try testing.expectEqual(error.PathAlreadyExists, err);
501        return;
502    };
503
504    // on case sensitive os both files are created
505    try testing.expect((try root.dir.statFile("alacritty/darkermatrix.yml")).kind == .file);
506    try testing.expect((try root.dir.statFile("alacritty/Darkermatrix.yml")).kind == .file);
507}