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}