17
17
//!
18
18
const std = @import ("std" );
19
19
const assert = std .debug .assert ;
20
+ const testing = std .testing ;
20
21
21
- pub const Options = struct {
22
+ /// pipeToFileSystem options
23
+ pub const PipeOptions = struct {
22
24
/// Number of directory levels to skip when extracting files.
23
25
strip_components : u32 = 0 ,
24
26
/// How to handle the "mode" property of files from within the tar file.
@@ -82,14 +84,14 @@ pub const Options = struct {
82
84
};
83
85
};
84
86
85
- pub const Header = struct {
87
+ const Header = struct {
86
88
const SIZE = 512 ;
87
- pub const MAX_NAME_SIZE = 100 + 1 + 155 ; // name(100) + separator(1) + prefix(155)
88
- pub const LINK_NAME_SIZE = 100 ;
89
+ const MAX_NAME_SIZE = 100 + 1 + 155 ; // name(100) + separator(1) + prefix(155)
90
+ const LINK_NAME_SIZE = 100 ;
89
91
90
92
bytes : * const [SIZE ]u8 ,
91
93
92
- pub const Kind = enum (u8 ) {
94
+ const Kind = enum (u8 ) {
93
95
normal_alias = 0 ,
94
96
normal = '0' ,
95
97
hard_link = '1' ,
@@ -235,74 +237,53 @@ fn nullStr(str: []const u8) []const u8 {
235
237
return str ;
236
238
}
237
239
240
+ /// Options for iterator.
241
+ /// Buffers should be provided by the caller.
238
242
pub const IteratorOptions = struct {
239
243
/// Use a buffer with length `std.fs.MAX_PATH_BYTES` to match file system capabilities.
240
244
file_name_buffer : []u8 ,
241
245
/// Use a buffer with length `std.fs.MAX_PATH_BYTES` to match file system capabilities.
242
246
link_name_buffer : []u8 ,
247
+ /// Provide this to receive detailed error messages.
248
+ /// When this is provided, some errors which would otherwise be returned immediately
249
+ /// will instead be added to this structure. The API user must check the errors
250
+ /// in diagnostics to know whether the operation succeeded or failed.
243
251
diagnostics : ? * Diagnostics = null ,
244
252
245
- pub const Diagnostics = Options .Diagnostics ;
253
+ pub const Diagnostics = PipeOptions .Diagnostics ;
246
254
};
247
255
248
256
/// Iterates over files in tar archive.
249
- /// `next` returns each file in `reader` tar archive.
250
- ///
251
- /// Init iterator with tar archive reader and provided buffers:
252
- ///
253
- /// var file_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
254
- /// var link_name_buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
255
- ///
256
- /// var iter = std.tar.iterator(archive.reader(), .{
257
- /// .file_name_buffer = &file_name_buffer,
258
- /// .link_name_buffer = &link_name_buffer,
259
- /// });
260
- ///
261
- /// Iterate on each tar archive file:
262
- ///
263
- /// while (try iter.next()) |file| {
264
- /// switch (file.kind) {
265
- /// .directory => {
266
- /// // try dir.makePath(file.name);
267
- /// },
268
- /// .file => {
269
- /// // try file.writeAll(writer);
270
- /// },
271
- /// .sym_link => {
272
- /// // try dir.symLink(file.link_name, file.name, .{});
273
- /// },
274
- /// }
275
- /// }
276
- ///
257
+ /// `next` returns each file in tar archive.
277
258
pub fn iterator (reader : anytype , options : IteratorOptions ) Iterator (@TypeOf (reader )) {
278
259
return .{
279
260
.reader = reader ,
280
261
.diagnostics = options .diagnostics ,
281
- .header_buffer = undefined ,
282
262
.file_name_buffer = options .file_name_buffer ,
283
263
.link_name_buffer = options .link_name_buffer ,
284
- .padding = 0 ,
285
264
};
286
265
}
287
266
267
+ /// Type of the file returned by iterator `next` method.
288
268
pub const FileKind = enum {
289
269
directory ,
290
270
sym_link ,
291
271
file ,
292
272
};
293
273
294
- fn Iterator (comptime ReaderType : type ) type {
274
+ /// Iteartor over entries in the tar file represented by reader.
275
+ pub fn Iterator (comptime ReaderType : type ) type {
295
276
return struct {
296
277
reader : ReaderType ,
297
- diagnostics : ? * Options .Diagnostics ,
278
+ diagnostics : ? * PipeOptions .Diagnostics = null ,
298
279
299
280
// buffers for heeader and file attributes
300
- header_buffer : [Header .SIZE ]u8 ,
281
+ header_buffer : [Header .SIZE ]u8 = undefined ,
301
282
file_name_buffer : []u8 ,
302
283
link_name_buffer : []u8 ,
303
284
304
285
// bytes of padding to the end of the block
305
- padding : usize ,
286
+ padding : usize = 0 ,
306
287
// not consumed bytes of file from last next iteration
307
288
unread_file_bytes : u64 = 0 ,
308
289
@@ -314,18 +295,18 @@ fn Iterator(comptime ReaderType: type) type {
314
295
kind : FileKind = .file ,
315
296
316
297
unread_bytes : * u64 ,
317
- reader : ReaderType ,
298
+ parent_reader : ReaderType ,
318
299
319
- pub const Reader = std .io .Reader (* Self , ReaderType .Error , read );
300
+ pub const Reader = std .io .Reader (File , ReaderType .Error , File . read );
320
301
321
- pub fn reader (self : * Self ) Reader {
302
+ pub fn reader (self : File ) Reader {
322
303
return .{ .context = self };
323
304
}
324
305
325
- pub fn read (self : * Self , dest : []u8 ) ReaderType.Error ! usize {
326
- const buf = dest [0.. @min (dest .len , self .unread_size .* )];
327
- const n = try self .reader .read (buf );
328
- self .unread_size .* -= n ;
306
+ pub fn read (self : File , dest : []u8 ) ReaderType.Error ! usize {
307
+ const buf = dest [0.. @min (dest .len , self .unread_bytes .* )];
308
+ const n = try self .parent_reader .read (buf );
309
+ self .unread_bytes .* -= n ;
329
310
return n ;
330
311
}
331
312
@@ -335,7 +316,7 @@ fn Iterator(comptime ReaderType: type) type {
335
316
336
317
while (self .unread_bytes .* > 0 ) {
337
318
const buf = buffer [0.. @min (buffer .len , self .unread_bytes .* )];
338
- try self .reader .readNoEof (buf );
319
+ try self .parent_reader .readNoEof (buf );
339
320
try writer .writeAll (buf );
340
321
self .unread_bytes .* -= buf .len ;
341
322
}
@@ -367,7 +348,7 @@ fn Iterator(comptime ReaderType: type) type {
367
348
return .{
368
349
.name = self .file_name_buffer [0.. 0],
369
350
.link_name = self .link_name_buffer [0.. 0],
370
- .reader = self .reader ,
351
+ .parent_reader = self .reader ,
371
352
.unread_bytes = & self .unread_file_bytes ,
372
353
};
373
354
}
@@ -592,7 +573,8 @@ fn PaxIterator(comptime ReaderType: type) type {
592
573
};
593
574
}
594
575
595
- pub fn pipeToFileSystem (dir : std.fs.Dir , reader : anytype , options : Options ) ! void {
576
+ /// Saves tar file content to the file systems.
577
+ pub fn pipeToFileSystem (dir : std.fs.Dir , reader : anytype , options : PipeOptions ) ! void {
596
578
switch (options .mode_mode ) {
597
579
.ignore = > {},
598
580
.executable_bit_only = > {
@@ -697,7 +679,7 @@ fn stripComponents(path: []const u8, count: u32) []const u8 {
697
679
}
698
680
699
681
test "stripComponents" {
700
- const expectEqualStrings = std . testing .expectEqualStrings ;
682
+ const expectEqualStrings = testing .expectEqualStrings ;
701
683
try expectEqualStrings ("a/b/c" , stripComponents ("a/b/c" , 0 ));
702
684
try expectEqualStrings ("b/c" , stripComponents ("a/b/c" , 1 ));
703
685
try expectEqualStrings ("c" , stripComponents ("a/b/c" , 2 ));
@@ -808,24 +790,24 @@ test "PaxIterator" {
808
790
var i : usize = 0 ;
809
791
while (iter .next () catch | err | {
810
792
if (case .err ) | e | {
811
- try std . testing .expectEqual (e , err );
793
+ try testing .expectEqual (e , err );
812
794
continue ;
813
795
}
814
796
return err ;
815
797
}) | attr | : (i += 1 ) {
816
798
const exp = case .attrs [i ];
817
- try std . testing .expectEqual (exp .kind , attr .kind );
799
+ try testing .expectEqual (exp .kind , attr .kind );
818
800
const value = attr .value (& buffer ) catch | err | {
819
801
if (exp .err ) | e | {
820
- try std . testing .expectEqual (e , err );
802
+ try testing .expectEqual (e , err );
821
803
break :outer ;
822
804
}
823
805
return err ;
824
806
};
825
- try std . testing .expectEqualStrings (exp .value , value );
807
+ try testing .expectEqualStrings (exp .value , value );
826
808
}
827
- try std . testing .expectEqual (case .attrs .len , i );
828
- try std . testing .expect (case .err == null );
809
+ try testing .expectEqual (case .attrs .len , i );
810
+ try testing .expect (case .err == null );
829
811
}
830
812
}
831
813
@@ -861,9 +843,9 @@ test "header parse size" {
861
843
@memcpy (bytes [124 .. 124 + case .in .len ], case .in );
862
844
var header = Header { .bytes = & bytes };
863
845
if (case .err ) | err | {
864
- try std . testing .expectError (err , header .size ());
846
+ try testing .expectError (err , header .size ());
865
847
} else {
866
- try std . testing .expectEqual (case .want , try header .size ());
848
+ try testing .expectEqual (case .want , try header .size ());
867
849
}
868
850
}
869
851
}
@@ -886,15 +868,15 @@ test "header parse mode" {
886
868
@memcpy (bytes [100 .. 100 + case .in .len ], case .in );
887
869
var header = Header { .bytes = & bytes };
888
870
if (case .err ) | err | {
889
- try std . testing .expectError (err , header .mode ());
871
+ try testing .expectError (err , header .mode ());
890
872
} else {
891
- try std . testing .expectEqual (case .want , try header .mode ());
873
+ try testing .expectEqual (case .want , try header .mode ());
892
874
}
893
875
}
894
876
}
895
877
896
878
test "create file and symlink" {
897
- var root = std . testing .tmpDir (.{});
879
+ var root = testing .tmpDir (.{});
898
880
defer root .cleanup ();
899
881
900
882
var file = try createDirAndFile (root .dir , "file1" );
@@ -914,3 +896,120 @@ test "create file and symlink" {
914
896
file = try createDirAndFile (root .dir , "g/h/i/file4" );
915
897
file .close ();
916
898
}
899
+
900
+ test iterator {
901
+ // Example tar file is created from this tree structure:
902
+ // $ tree example
903
+ // example
904
+ // ├── a
905
+ // │ └── file
906
+ // ├── b
907
+ // │ └── symlink -> ../a/file
908
+ // └── empty
909
+ // $ cat example/a/file
910
+ // content
911
+ // $ tar -cf example.tar example
912
+ // $ tar -tvf example.tar
913
+ // example/
914
+ // example/b/
915
+ // example/b/symlink -> ../a/file
916
+ // example/a/
917
+ // example/a/file
918
+ // example/empty/
919
+
920
+ const data = @embedFile ("tar/testdata/example.tar" );
921
+ var fbs = std .io .fixedBufferStream (data );
922
+
923
+ // User provided buffers to the iterator
924
+ var file_name_buffer : [std .fs .MAX_PATH_BYTES ]u8 = undefined ;
925
+ var link_name_buffer : [std .fs .MAX_PATH_BYTES ]u8 = undefined ;
926
+ // Create iterator
927
+ var iter = iterator (fbs .reader (), .{
928
+ .file_name_buffer = & file_name_buffer ,
929
+ .link_name_buffer = & link_name_buffer ,
930
+ });
931
+ // Iterate over files in example.tar
932
+ var file_no : usize = 0 ;
933
+ while (try iter .next ()) | file | : (file_no += 1 ) {
934
+ switch (file .kind ) {
935
+ .directory = > {
936
+ switch (file_no ) {
937
+ 0 = > try testing .expectEqualStrings ("example/" , file .name ),
938
+ 1 = > try testing .expectEqualStrings ("example/b/" , file .name ),
939
+ 3 = > try testing .expectEqualStrings ("example/a/" , file .name ),
940
+ 5 = > try testing .expectEqualStrings ("example/empty/" , file .name ),
941
+ else = > unreachable ,
942
+ }
943
+ },
944
+ .file = > {
945
+ try testing .expectEqualStrings ("example/a/file" , file .name );
946
+ // Read file content
947
+ var buf : [16 ]u8 = undefined ;
948
+ const n = try file .reader ().readAll (& buf );
949
+ try testing .expectEqualStrings ("content\n " , buf [0.. n ]);
950
+ },
951
+ .sym_link = > {
952
+ try testing .expectEqualStrings ("example/b/symlink" , file .name );
953
+ try testing .expectEqualStrings ("../a/file" , file .link_name );
954
+ },
955
+ }
956
+ }
957
+ }
958
+
959
+ test pipeToFileSystem {
960
+ // Example tar file is created from this tree structure:
961
+ // $ tree example
962
+ // example
963
+ // ├── a
964
+ // │ └── file
965
+ // ├── b
966
+ // │ └── symlink -> ../a/file
967
+ // └── empty
968
+ // $ cat example/a/file
969
+ // content
970
+ // $ tar -cf example.tar example
971
+ // $ tar -tvf example.tar
972
+ // example/
973
+ // example/b/
974
+ // example/b/symlink -> ../a/file
975
+ // example/a/
976
+ // example/a/file
977
+ // example/empty/
978
+
979
+ const data = @embedFile ("tar/testdata/example.tar" );
980
+ var fbs = std .io .fixedBufferStream (data );
981
+ const reader = fbs .reader ();
982
+
983
+ var tmp = testing .tmpDir (.{ .no_follow = true });
984
+ defer tmp .cleanup ();
985
+ const dir = tmp .dir ;
986
+
987
+ // Save tar from `reader` to the file system `dir`
988
+ pipeToFileSystem (dir , reader , .{
989
+ .mode_mode = .ignore ,
990
+ .strip_components = 1 ,
991
+ .exclude_empty_directories = true ,
992
+ }) catch | err | {
993
+ // Skip on platform which don't support symlinks
994
+ if (err == error .UnableToCreateSymLink ) return error .SkipZigTest ;
995
+ return err ;
996
+ };
997
+
998
+ try testing .expectError (error .FileNotFound , dir .statFile ("empty" ));
999
+ try testing .expect ((try dir .statFile ("a/file" )).kind == .file );
1000
+ try testing .expect ((try dir .statFile ("b/symlink" )).kind == .file ); // statFile follows symlink
1001
+
1002
+ var buf : [32 ]u8 = undefined ;
1003
+ try testing .expectEqualSlices (
1004
+ u8 ,
1005
+ "../a/file" ,
1006
+ normalizePath (try dir .readLink ("b/symlink" , & buf )),
1007
+ );
1008
+ }
1009
+
1010
+ fn normalizePath (bytes : []u8 ) []u8 {
1011
+ const canonical_sep = std .fs .path .sep_posix ;
1012
+ if (std .fs .path .sep == canonical_sep ) return bytes ;
1013
+ std .mem .replaceScalar (u8 , bytes , std .fs .path .sep , canonical_sep );
1014
+ return bytes ;
1015
+ }
0 commit comments