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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +285 -0
- data/CONTRIBUTING.md +273 -0
- data/CONTRIBUTORS.md +27 -0
- data/LICENCE.md +39 -0
- data/Manifest.txt +29 -6
- data/README.md +70 -0
- data/Rakefile +74 -19
- data/SECURITY.md +64 -0
- data/docs/ruby.txt +3 -3
- data/lib/minitar/input.rb +69 -56
- data/lib/minitar/output.rb +34 -22
- data/lib/minitar/pax_header.rb +111 -0
- data/lib/minitar/posix_header.rb +96 -57
- data/lib/minitar/reader.rb +65 -70
- data/lib/minitar/version.rb +5 -0
- data/lib/minitar/writer.rb +50 -88
- data/lib/minitar.rb +60 -64
- data/licenses/bsdl.txt +20 -0
- data/licenses/dco.txt +34 -0
- data/licenses/ruby.txt +52 -0
- data/test/fixtures/issue_46.tar.gz +0 -0
- data/test/fixtures/issue_62.tar.gz +0 -0
- data/test/fixtures/tar_input.tgz +0 -0
- data/test/fixtures/test_input_non_strict_octal.tgz +0 -0
- data/test/fixtures/test_input_relative.tgz +0 -0
- data/test/fixtures/test_input_space_octal.tgz +0 -0
- data/test/fixtures/test_minitar.tar.gz +0 -0
- data/test/minitest_helper.rb +12 -1
- data/test/support/minitar_test_helpers/fixtures.rb +38 -0
- data/test/support/minitar_test_helpers/header.rb +130 -0
- data/test/support/minitar_test_helpers/tarball.rb +324 -0
- data/test/support/minitar_test_helpers.rb +36 -0
- data/test/test_filename_boundary_conditions.rb +74 -0
- data/test/test_gnu_tar_compatibility.rb +92 -0
- data/test/test_integration_pack_unpack_cycle.rb +38 -0
- data/test/test_issue_46.rb +5 -23
- data/test/test_issue_62.rb +50 -0
- data/test/test_minitar.rb +168 -39
- data/test/test_pax_header.rb +104 -0
- data/test/test_pax_support.rb +66 -0
- data/test/test_tar_header.rb +289 -75
- data/test/test_tar_input.rb +14 -61
- data/test/test_tar_output.rb +7 -9
- data/test/test_tar_reader.rb +17 -18
- data/test/test_tar_writer.rb +105 -126
- metadata +95 -89
- data/Contributing.md +0 -94
- data/History.md +0 -236
- data/Licence.md +0 -15
- data/README.rdoc +0 -92
- data/test/support/tar_test_helpers.rb +0 -134
- /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
|