minitar 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +285 -0
  3. data/CONTRIBUTING.md +273 -0
  4. data/CONTRIBUTORS.md +27 -0
  5. data/LICENCE.md +39 -0
  6. data/Manifest.txt +29 -6
  7. data/README.md +70 -0
  8. data/Rakefile +74 -19
  9. data/SECURITY.md +64 -0
  10. data/docs/ruby.txt +3 -3
  11. data/lib/minitar/input.rb +69 -56
  12. data/lib/minitar/output.rb +34 -22
  13. data/lib/minitar/pax_header.rb +111 -0
  14. data/lib/minitar/posix_header.rb +96 -57
  15. data/lib/minitar/reader.rb +65 -70
  16. data/lib/minitar/version.rb +5 -0
  17. data/lib/minitar/writer.rb +50 -88
  18. data/lib/minitar.rb +60 -64
  19. data/licenses/bsdl.txt +20 -0
  20. data/licenses/dco.txt +34 -0
  21. data/licenses/ruby.txt +52 -0
  22. data/test/fixtures/issue_46.tar.gz +0 -0
  23. data/test/fixtures/issue_62.tar.gz +0 -0
  24. data/test/fixtures/tar_input.tgz +0 -0
  25. data/test/fixtures/test_input_non_strict_octal.tgz +0 -0
  26. data/test/fixtures/test_input_relative.tgz +0 -0
  27. data/test/fixtures/test_input_space_octal.tgz +0 -0
  28. data/test/fixtures/test_minitar.tar.gz +0 -0
  29. data/test/minitest_helper.rb +12 -1
  30. data/test/support/minitar_test_helpers/fixtures.rb +38 -0
  31. data/test/support/minitar_test_helpers/header.rb +130 -0
  32. data/test/support/minitar_test_helpers/tarball.rb +324 -0
  33. data/test/support/minitar_test_helpers.rb +36 -0
  34. data/test/test_filename_boundary_conditions.rb +74 -0
  35. data/test/test_gnu_tar_compatibility.rb +92 -0
  36. data/test/test_integration_pack_unpack_cycle.rb +38 -0
  37. data/test/test_issue_46.rb +5 -23
  38. data/test/test_issue_62.rb +50 -0
  39. data/test/test_minitar.rb +168 -39
  40. data/test/test_pax_header.rb +104 -0
  41. data/test/test_pax_support.rb +66 -0
  42. data/test/test_tar_header.rb +289 -75
  43. data/test/test_tar_input.rb +14 -61
  44. data/test/test_tar_output.rb +7 -9
  45. data/test/test_tar_reader.rb +17 -18
  46. data/test/test_tar_writer.rb +105 -126
  47. metadata +95 -89
  48. data/Contributing.md +0 -94
  49. data/History.md +0 -236
  50. data/Licence.md +0 -15
  51. data/README.rdoc +0 -92
  52. data/test/support/tar_test_helpers.rb +0 -134
  53. /data/{Code-of-Conduct.md → CODE_OF_CONDUCT.md} +0 -0
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitar::TestHelpers::Fixtures
4
+ def Fixture(name) = FIXTURES.fetch(name)
5
+
6
+ def open_fixture(name)
7
+ fixture = Fixture(name)
8
+
9
+ io = fixture.open("rb").then {
10
+ if %w[.gz .tgz].include?(fixture.extname.to_s)
11
+ Zlib::GzipReader.new(_1)
12
+ else
13
+ _1
14
+ end
15
+ }.tap {
16
+ yield _1 if block_given?
17
+ }
18
+ ensure
19
+ io&.close if block_given?
20
+ end
21
+
22
+ FIXTURES = Pathname(__dir__)
23
+ .join("../../fixtures")
24
+ .expand_path
25
+ .glob("**")
26
+ .each_with_object({}) {
27
+ next if _1.directory?
28
+
29
+ name = _1.dup
30
+ name = name.basename(name.extname) until name.nil? || name.extname.empty?
31
+
32
+ _2[name.to_s] = _1
33
+ }
34
+ .freeze
35
+ private_constant :FIXTURES
36
+
37
+ Minitest::Test.send(:include, self)
38
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Test assertions and helpers for working with header objects.
4
+ module Minitar::TestHelpers::Header
5
+ private
6
+
7
+ # Assert that the +actual+ header is equal to +expected+.
8
+ def assert_headers_equal(expected, actual)
9
+ actual = actual.to_s
10
+ __field_order.each do |field|
11
+ message =
12
+ if field == "checksum"
13
+ "Header checksums are expected to match."
14
+ else
15
+ "Header field #{field} is expected to match."
16
+ end
17
+
18
+ offset = __fields[field].offset
19
+ length = __fields[field].length
20
+
21
+ assert_equal expected[offset, length], actual[offset, length], message
22
+ end
23
+ end
24
+
25
+ def assert_modes_equal(expected, actual, filename)
26
+ return if Minitar.windows?
27
+
28
+ assert_equal mode_string(expected), mode_string(actual), "Mode for #{filename} does not match"
29
+ end
30
+
31
+ def build_raw_header(type, name, prefix, size, mode, link_name = "") =
32
+ [
33
+ name, mode, z(octal(nil, 7)), z(octal(nil, 7)), size, z(octal(0, 11)),
34
+ BLANK_CHECKSUM, type, asciiz(link_name, 100), USTAR, DOUBLE_ZERO, asciiz("", 32),
35
+ asciiz("", 32), z(octal(nil, 7)), z(octal(nil, 7)), prefix
36
+ ].join.bytes.to_a.pack("C100C8C8C8C12C12C8CC100C6C2C32C32C8C8C155").then {
37
+ "#{_1}#{"\0" * (512 - _1.bytesize)}"
38
+ }.tap { assert_equal 512, _1.bytesize }
39
+
40
+ def build_header(type, name, prefix, size, mode, link_name = "") =
41
+ build_raw_header(
42
+ type,
43
+ asciiz(name, 100),
44
+ asciiz(prefix, 155),
45
+ z(octal(size, 11)),
46
+ z(octal(mode, 7)),
47
+ asciiz(link_name, 100)
48
+ )
49
+
50
+ def build_tar_file_header(name, prefix, mode, size) =
51
+ build_header("0", name, prefix, size, mode).then {
52
+ update_header_checksum(_1)
53
+ }
54
+
55
+ def build_tar_dir_header(name, prefix, mode) =
56
+ build_header("5", name, prefix, 0, mode).then {
57
+ update_header_checksum(_1)
58
+ }
59
+
60
+ def build_tar_symlink_header(name, prefix, mode, target) =
61
+ build_header("2", name, prefix, 0, mode, target).then {
62
+ update_header_checksum(_1)
63
+ }
64
+
65
+ def build_tar_pax_header(name, prefix, content_size) =
66
+ build_header("x", name, prefix, content_size, 0o644).then {
67
+ update_header_checksum(_1)
68
+ }
69
+
70
+ def update_header_checksum(header) =
71
+ header.tap { |h|
72
+ checksum = __fields["checksum"]
73
+ h[checksum.offset, checksum.length] =
74
+ h.unpack("C*")
75
+ .inject(:+)
76
+ .then { octal(_1, 6) }
77
+ .then { z(_1) }
78
+ .then { sp(_1) }
79
+ }
80
+
81
+ def octal(n, pad_size) = n.nil? ? "\0" * pad_size : "%0#{pad_size}o" % n
82
+
83
+ def asciiz(str, size) = "#{str}#{"\0" * (size - str.bytesize)}"
84
+
85
+ def sp(s) = "#{s} "
86
+
87
+ def z(s) = "#{s}\0"
88
+
89
+ def mode_string(value) = "%04o" % (value & 0o777)
90
+
91
+ def __field_order = FIELD_ORDER
92
+
93
+ def __fields = FIELDS
94
+
95
+ FIELD_ORDER = []
96
+ private_constant :FIELD_ORDER
97
+
98
+ FIELDS = {}
99
+ private_constant :FIELDS
100
+
101
+ Field = Struct.new(:name, :offset, :length)
102
+ private_constant :Field
103
+
104
+ BLANK_CHECKSUM = (" " * 8).freeze
105
+ private_constant :BLANK_CHECKSUM
106
+
107
+ DOUBLE_ZERO = "00"
108
+ private_constant :DOUBLE_ZERO
109
+
110
+ NULL_100 = ("\0" * 100).freeze
111
+ private_constant :NULL_100
112
+
113
+ USTAR = "ustar\0"
114
+ private_constant :USTAR
115
+
116
+ fields = [
117
+ ["name", 100], ["mode", 8], ["uid", 8], ["gid", 8], ["size", 12], ["mtime", 12],
118
+ ["checksum", 8], ["typeflag", 1], ["linkname", 100], ["magic", 6], ["version", 2],
119
+ ["uname", 32], ["gname", 32], ["devmajor", 8], ["devminor", 8], ["prefix", 155]
120
+ ]
121
+ offset = 0
122
+
123
+ fields.each do |(name, length)|
124
+ FIELDS[name] = Field.new(name, offset, length)
125
+ FIELD_ORDER << name
126
+ offset += length
127
+ end
128
+
129
+ Minitest::Test.send(:include, self)
130
+ end
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ # Test assertions and helpers for working with tarballs.
6
+ #
7
+ # Includes Minitar in-memory and on-disk operations and GNU tar helpers.
8
+ module Minitar::TestHelpers::Tarball
9
+ private
10
+
11
+ GNU_TAR = %w[tar gtar].find { |program|
12
+ path = `bash -c "command -v '#{program}'"`.chomp
13
+
14
+ if path.empty?
15
+ false
16
+ else
17
+ version = `#{program} --version`.chomp
18
+ version =~ /\(GNU tar\)|Free Software Foundation/
19
+ end
20
+ }.freeze
21
+
22
+ # Given the +original_files+ file hash (input to +create_tar_string+) and the
23
+ # +extracted_files+ file has (output from +extract_tar_string+), ensures that the tar
24
+ # structure is preserved, including checking for possible regression of issue 62.
25
+ #
26
+ # Such a regression would result in a directory like <tt>>/b/c.txt</tt> looking like
27
+ # <tt>a/b/a/b/c.txt</tt> (but only for long filenames).
28
+ def assert_tar_structure_preserved(original_files, extracted_files)
29
+ assert_equal original_files.length, extracted_files.length, "File counts do not match"
30
+
31
+ original_paths = original_files.keys.sort
32
+ extracted_paths = extracted_files.keys.sort
33
+
34
+ assert_equal original_paths, extracted_paths, "Complete file paths should match exactly"
35
+
36
+ original_files.each do |filename, content|
37
+ assert extracted_files.key?(filename), "File #{filename} should be extracted"
38
+
39
+ assert_equal content, extracted_files[filename], "Content should be preserved for #{filename}"
40
+
41
+ next unless filename.include?("/")
42
+
43
+ dirname, basename = File.split(filename)
44
+ bad_pattern = File.join(dirname, dirname, basename)
45
+
46
+ duplicated_paths = extracted_paths.select { |path| path == bad_pattern }
47
+
48
+ refute duplicated_paths.any?,
49
+ "Regression of #62, path duplication on extraction! " \
50
+ "Original: #{filename}, " \
51
+ "Bad pattern found: #{bad_pattern}, " \
52
+ "All extracted paths: #{extracted_paths}"
53
+ end
54
+ end
55
+
56
+ # Create a tarball string from the +file_hash+ (<tt>{filename => content}</tt>).
57
+ def create_tar_string(file_hash) =
58
+ StringIO.new.tap { |io|
59
+ Minitar::Output.open(io) do |output|
60
+ file_hash.each do |filename, content|
61
+ next if content.nil?
62
+ Minitar.pack_as_file(filename, content.to_s.dup, output)
63
+ end
64
+ end
65
+ }.string
66
+
67
+ # Extract a hash of <tt>{filename => content}</tt> from the +tar_data+. Directories are
68
+ # skipped.
69
+ def extract_tar_string(tar_data) =
70
+ {}.tap { |files|
71
+ Minitar::Input.open(StringIO.new(tar_data)) do |input|
72
+ input.each do |entry|
73
+ next if entry.directory?
74
+ files[entry.full_name] = entry.read
75
+ end
76
+ end
77
+ }
78
+
79
+ # Create a tarball string from the +file_hash+ (<tt>{filename => content}</tt>) provided
80
+ # and immediately extracts a hash of <tt>{filename => content}</tt> from the tarball
81
+ # string. Directories are skipped.
82
+ def roundtrip_tar_string(file_hash) =
83
+ create_tar_string(file_hash).then { extract_tar_string(_1) }
84
+
85
+ def has_gnu_tar? = !GNU_TAR&.empty?
86
+
87
+ Workspace = Struct.new(:tmpdir, :source, :target, :tarball, :files, keyword_init: true)
88
+ private_constant :Workspace
89
+
90
+ # Prepare a workspace for a file-based test.
91
+ def workspace(with_files: nil)
92
+ raise "Workspace requires a block" unless block_given?
93
+ raise "No nested workspace permitted" if @workspace
94
+
95
+ tmpdir =
96
+ if Pathname.respond_to?(:mktmpdir)
97
+ Pathname.mktmpdir
98
+ else
99
+ Pathname(Dir.mktmpdir)
100
+ end
101
+
102
+ source = tmpdir.join("source")
103
+ target = tmpdir.join("target")
104
+ tarball = tmpdir.join("test.tar")
105
+
106
+ @workspace = Workspace.new(
107
+ tmpdir: tmpdir,
108
+ source: source,
109
+ target: target,
110
+ tarball: tarball
111
+ )
112
+
113
+ source.mkpath
114
+ target.mkpath
115
+
116
+ prepare_workspace(with_files:) if with_files
117
+
118
+ yield @workspace
119
+ ensure
120
+ tmpdir&.rmtree
121
+ @workspace = nil
122
+ end
123
+
124
+ def prepare_workspace(with_files:)
125
+ missing_workspace!
126
+
127
+ raise "Missing workspace" unless @workspace
128
+ raise "Files already prepared" if @workspace.files
129
+
130
+ @workspace.files = with_files.each_pair do
131
+ full_path = @workspace.source.join(_1)
132
+
133
+ if _2.nil?
134
+ full_path.mkpath
135
+
136
+ assert full_path.directory?, "#{full_path} should be created as a directory"
137
+ else
138
+ full_path.dirname.mkpath
139
+ full_path.write(_2)
140
+
141
+ assert full_path.file?, "#{full_path} should be created as a file"
142
+ end
143
+ end
144
+ end
145
+
146
+ def gnu_tar_create_in_workspace
147
+ missing_workspace!
148
+
149
+ __gnu_tar(:create)
150
+
151
+ assert @workspace.tarball.file?, "Workspace tarball not created by GNU tar"
152
+ assert @workspace.tarball.size > 0, "Workspace tarball should not be empty"
153
+ end
154
+
155
+ def gnu_tar_extract_in_workspace
156
+ missing_workspace!
157
+
158
+ assert @workspace.tarball.file?, "Workspace tarball not present for extraction"
159
+ assert @workspace.tarball.size > 0, "Workspace tarball should not be empty"
160
+
161
+ __gnu_tar(:extract)
162
+ end
163
+
164
+ def gnu_tar_list_in_workspace
165
+ missing_workspace!
166
+
167
+ assert @workspace.tarball.file?, "Workspace tarball not present for extraction"
168
+ assert @workspace.tarball.size > 0, "Workspace tarball should not be empty"
169
+
170
+ __gnu_tar(:list).strip.split($/)
171
+ end
172
+
173
+ def minitar_pack_in_workspace
174
+ missing_workspace!
175
+
176
+ @workspace.tarball.open("wb") do |tar_io|
177
+ Dir.chdir(@workspace.source) do
178
+ Minitar.pack(".", tar_io)
179
+ end
180
+ end
181
+
182
+ assert @workspace.tarball.file?, "Workspace tarball not created by Minitar.pack"
183
+ assert @workspace.tarball.size > 0, "Workspace tarball should not be empty"
184
+ end
185
+
186
+ def minitar_unpack_in_workspace
187
+ missing_workspace!
188
+
189
+ assert @workspace.tarball.file?, "Workspace tarball not present for extraction"
190
+ assert @workspace.tarball.size > 0, "Workspace tarball should not be empty"
191
+
192
+ @workspace.tarball.open("rb") do
193
+ Minitar.unpack(_1, @workspace.target)
194
+ end
195
+ end
196
+
197
+ def minitar_writer_create_in_workspace
198
+ missing_workspace!
199
+
200
+ @workspace.tarball.open("wb") do |tar_io|
201
+ Minitar::Writer.open(tar_io) do |writer|
202
+ @workspace.files.each_pair do |name, content|
203
+ full_path = @workspace.source.join(name)
204
+ stat = full_path.stat
205
+
206
+ writer.add_file_simple(name, mode: stat.mode, size: stat.size) do
207
+ _1.write(content)
208
+ end
209
+ end
210
+ end
211
+ end
212
+
213
+ assert @workspace.tarball.file?, "Workspace tarball not created by Minitar::Writer"
214
+ assert @workspace.tarball.size > 0, "Workspace tarball should not be empty"
215
+ end
216
+
217
+ def assert_files_extracted_in_workspace
218
+ missing_workspace!
219
+
220
+ @workspace.files.each_pair do
221
+ target = @workspace.target.join(_1)
222
+ assert target.exist?, "#{_1.inspect} does not exist"
223
+
224
+ if _2.nil?
225
+ assert target.directory?, "#{_1} is not a directory"
226
+ else
227
+ assert_equal _2, @workspace.target.join(_1).read, "#{_1} content does not match"
228
+ end
229
+ end
230
+ end
231
+
232
+ def refute_file_path_duplication_in_workspace
233
+ missing_workspace!
234
+
235
+ @workspace.files.each_key do
236
+ next unless _1.include?("/")
237
+
238
+ dir, filename = Pathname(_1).split
239
+ dup_path = dir.join(dir, filename)
240
+ refute @workspace.target.join(dup_path).exist?,
241
+ "No path duplication should occur: #{dup_path}"
242
+ end
243
+ end
244
+
245
+ def assert_extracted_files_match_source_files_in_workspace
246
+ missing_workspace!
247
+
248
+ source_files = __collect_relative_paths(@workspace.source)
249
+ target_files = __collect_relative_paths(@workspace.target)
250
+
251
+ assert_equal source_files, target_files,
252
+ "Complete directory structure should match exactly"
253
+ end
254
+
255
+ def __collect_relative_paths(dir)
256
+ return [] unless dir.directory?
257
+
258
+ dir.glob("**/*").map { _1.relative_path_from(dir).to_s }.sort
259
+ end
260
+
261
+ def assert_file_modes_match_in_workspace
262
+ missing_workspace!
263
+
264
+ return if Minitar.windows?
265
+
266
+ @workspace.files.each_key do |file_path|
267
+ source = @workspace.source.join(file_path)
268
+ target = @workspace.target.join(file_path)
269
+
270
+ source_mode = source.stat.mode & 0o777
271
+ target_mode = target.stat.mode & 0o777
272
+
273
+ assert_modes_equal source_mode, target_mode, file_path
274
+ end
275
+ end
276
+
277
+ def missing_workspace!
278
+ raise "Missing workspace" unless defined?(@workspace)
279
+ end
280
+
281
+ def __gnu_tar(action)
282
+ missing_workspace!
283
+
284
+ cmd = [GNU_TAR]
285
+ cmd.push("--force-local") if Minitar.windows?
286
+
287
+ case action
288
+ when :create
289
+ cmd.push("-cf", @workspace.tarball.to_s, "-C", @workspace.source.to_s, ".")
290
+ when :extract
291
+ cmd.push("-xf", @workspace.tarball.to_s, "-C", @workspace.target.to_s)
292
+ when :list
293
+ cmd.push("-tf", @workspace.tarball.to_s)
294
+ end
295
+
296
+ stdout_str = ""
297
+ stderr_str = ""
298
+ status = nil
299
+
300
+ Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thread|
301
+ stdin.close
302
+ out_t = Thread.new { stdout.read }
303
+ err_t = Thread.new { stderr.read }
304
+ stdout_str = out_t.value.to_s
305
+ stderr_str = err_t.value.to_s
306
+ status = wait_thread.value
307
+ end
308
+
309
+ unless status.success?
310
+ warn stdout_str unless stdout_str.empty?
311
+ warn stderr_str unless stderr_str.empty?
312
+
313
+ if status.exited?
314
+ raise "command #{cmd.join(" ")} failed (exit status: #{status.exitstatus})"
315
+ else
316
+ raise "command #{cmd.join(" ")} failed (status: #{status.inspect})"
317
+ end
318
+ end
319
+
320
+ stdout_str
321
+ end
322
+
323
+ Minitest::Test.send(:include, self)
324
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitar::TestHelpers
4
+ TIME_2004 = Time.utc(2004).to_i
5
+
6
+ BOUNDARY_SCENARIOS = {
7
+ ("a" * 99) => "99 chars content",
8
+ ("a" * 100) => "100 chars content",
9
+ ("a" * 101) => "101 chars content",
10
+ ("a" * 102) => "102 chars content",
11
+ "dir/#{"a" * 96}" => "nested path 100 total content",
12
+ "dir/#{"a" * 97}" => "nested path 101 total content",
13
+ "nested/#{"d" * 93}" => "nested 100 total content",
14
+ "nested/#{"e" * 94}" => "nested 101 total content"
15
+ }.freeze
16
+
17
+ MIXED_FILENAME_SCENARIOS = {
18
+ "short.txt" => "short content",
19
+ "medium_length_filename_under_100_chars.txt" => "medium content",
20
+ "dir1/medium_filename.js" => "medium nested content",
21
+ "#{"x" * 120}.txt" => "long content",
22
+ "nested/dir/#{"y" * 110}.css" => "long nested content"
23
+ }.freeze
24
+
25
+ VERY_LONG_FILENAME_SCENARIOS = {
26
+ "#{"f" * 180}.data" => "180 char filename content",
27
+ "#{"g" * 200}.json" => "200 char filename content",
28
+ "nested/path/#{"h" * 150}.css" => "nested long filename content",
29
+ "deep/nested/structure/#{"i" * 170}.html" => "deeply nested long content",
30
+ "project/src/main/#{"j" * 160}.java" => "project structure long content"
31
+ }.freeze
32
+
33
+ private
34
+
35
+ Minitest::Test.send(:include, self)
36
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest_helper"
4
+
5
+ class TestFilenameBoundaryConditions < Minitest::Test
6
+ SCENARIOS = [99, 100, 101, 102, 154, 155, 156].each_with_object({}) { |len, map|
7
+ name = "a" * len
8
+ content = "#{len} chars content"
9
+ map[name] = content
10
+
11
+ define_method :"test_single_file_#{len}_chars" do
12
+ file_map = {name => content}
13
+ files = roundtrip_tar_string(file_map)
14
+ assert_tar_structure_preserved file_map, files
15
+ end
16
+
17
+ name = "dir/#{"a" * (len - 4)}"
18
+ content = "dir/ #{len - 4} chars content: #{len} total chars"
19
+ map[name] = content
20
+
21
+ define_method :"test_nested_file_total_#{len}_chars" do
22
+ file_map = {name => content}
23
+ files = roundtrip_tar_string(file_map)
24
+ assert_tar_structure_preserved file_map, files
25
+ end
26
+ }
27
+
28
+ posix_scenarios = [155, 156].each_with_object({}) { |len, map|
29
+ name = "a" * len
30
+ content = "#{len} chars content"
31
+ map[name] = content
32
+
33
+ define_method :"test_posix_boundary_#{len}_chars" do
34
+ file_map = {name => content}
35
+ files = roundtrip_tar_string(file_map)
36
+ assert_tar_structure_preserved file_map, files
37
+ end
38
+ }
39
+
40
+ posix_total_scenarios = {155 => 100, 165 => 110}.each_with_object({}) { |(k, v), map|
41
+ name = "prefix_#{"a" * (k - 7)}/name_#{"a" * (v - 5)}"
42
+ content = "prefix #{k} name #{v} chars"
43
+ map[name] = content
44
+
45
+ define_method :"test_posix_total_boundary_#{k + v + 1}_chars" do
46
+ file_map = {name => content}
47
+ files = roundtrip_tar_string(file_map)
48
+ assert_tar_structure_preserved file_map, files
49
+ end
50
+ }
51
+
52
+ name = "very_long_component_name_with_many_characters"
53
+ .then { _1 * 3 }
54
+ .then { [_1] }
55
+ .then { _1 * 8 }
56
+ .then { _1.join("/") }
57
+ .then { "#{_1}/final_file_with_long_name.txt" }
58
+ content = "Content for very long path"
59
+
60
+ define_method :test_long_near_system_limits do
61
+ file_map = {name => content}
62
+ files = roundtrip_tar_string(file_map)
63
+ assert_tar_structure_preserved file_map, files
64
+ end
65
+
66
+ SCENARIOS[name] = content
67
+
68
+ SCENARIOS.merge!(posix_scenarios, posix_total_scenarios)
69
+
70
+ def test_full_scenario_in_archive
71
+ files = roundtrip_tar_string(SCENARIOS)
72
+ assert_tar_structure_preserved(SCENARIOS, files)
73
+ end
74
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest_helper"
4
+
5
+ class TestGnuTarCompatibility < Minitest::Test
6
+ def setup
7
+ skip "GNU tar not available" unless has_gnu_tar?
8
+ end
9
+
10
+ def test_roundtrip_gnu_tar_cf_minitar_unpack
11
+ # Use mixed filename scenarios from shared test utilities for comprehensive testing
12
+ files = MIXED_FILENAME_SCENARIOS.dup
13
+ # Add one very long filename to test GNU extension compatibility
14
+ files[VERY_LONG_FILENAME_SCENARIOS.keys.first] = VERY_LONG_FILENAME_SCENARIOS.values.first
15
+
16
+ workspace with_files: files do
17
+ gnu_tar_create_in_workspace
18
+ minitar_unpack_in_workspace
19
+
20
+ assert_files_extracted_in_workspace
21
+ refute_file_path_duplication_in_workspace
22
+ end
23
+ end
24
+
25
+ def test_roundtrip_minitar_pack_gnu_tar_xf
26
+ # Use different mixed scenarios for the reverse roundtrip test
27
+ files = MIXED_FILENAME_SCENARIOS.dup
28
+ # Add a different very long filename to test GNU extension compatibility
29
+ files[VERY_LONG_FILENAME_SCENARIOS.keys.last] = VERY_LONG_FILENAME_SCENARIOS.values.last
30
+
31
+ workspace with_files: files do
32
+ gnu_tar_create_in_workspace
33
+ minitar_unpack_in_workspace
34
+
35
+ assert_files_extracted_in_workspace
36
+ end
37
+ end
38
+
39
+ def test_roundtrip_gnu_tar_cf_minitar_unpack_mixed_filenames
40
+ # Test GNU tar create → Minitar extract with mixed filename lengths
41
+ files = {
42
+ "short.txt" => "short content",
43
+ "medium_length_filename.js" => "medium content",
44
+ "#{"f" * 120}.css" => "long content",
45
+ "dir/#{"g" * 130}.html" => "nested long content"
46
+ }
47
+
48
+ workspace with_files: files do
49
+ gnu_tar_create_in_workspace
50
+ minitar_unpack_in_workspace
51
+
52
+ assert_files_extracted_in_workspace
53
+ refute_file_path_duplication_in_workspace
54
+ end
55
+ end
56
+
57
+ def test_minitar_writer_gnu_tar_xf_with_long_filenames
58
+ # Test Minitar create → GNU tar extract with long filenames
59
+ files = {
60
+ "#{"j" * 120}.txt" => "content for 120 char filename",
61
+ "nested/path/#{"k" * 110}.js" => "content for nested long filename",
62
+ "#{"m" * 200}.html" => "content for very long filename",
63
+ "regular_file.md" => "regular file content"
64
+ }
65
+
66
+ workspace with_files: files do
67
+ minitar_writer_create_in_workspace
68
+ gnu_tar_extract_in_workspace
69
+ assert_files_extracted_in_workspace
70
+ end
71
+ end
72
+
73
+ def test_gnu_tar_list_compatibility_with_long_filenames
74
+ # Test that GNU tar can list files created by Minitar with long filenames
75
+ files = {
76
+ "#{"f" * 180}.data" => "gnu extension test content"
77
+ }
78
+
79
+ workspace with_files: files do
80
+ minitar_writer_create_in_workspace
81
+
82
+ list_output = gnu_tar_list_in_workspace
83
+ files.each_key do |name|
84
+ assert list_output.find { _1 == name },
85
+ "#{name} not present in GNU tar list output: #{list_output}"
86
+ end
87
+
88
+ gnu_tar_extract_in_workspace
89
+ assert_files_extracted_in_workspace
90
+ end
91
+ end
92
+ end