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
+ require "minitest_helper"
4
+
5
+ class TestIntegrationPackUnpackCycle < Minitest::Test
6
+ def test_comprehensive
7
+ files = MIXED_FILENAME_SCENARIOS.merge(
8
+ VERY_LONG_FILENAME_SCENARIOS,
9
+ BOUNDARY_SCENARIOS,
10
+ {
11
+ "empty_dir" => nil,
12
+ "nested/empty" => nil,
13
+ "long_dir_#{"i" * 120}" => nil,
14
+ "root_file.txt" => "root content",
15
+ "level1/file1.txt" => "level1 content",
16
+ "level1/level2/file2.txt" => "level2 content",
17
+ "level1/level2/level3/#{"deep" * 30}.txt" => "deep nested with long name",
18
+ "#{"long_dir" * 20}/file_in_long_dir.txt" => "file in long directory name",
19
+ "mixed/#{"long_subdir" * 15}/#{"long_file" * 25}.txt" => "long dir and file names"
20
+ }
21
+ )
22
+
23
+ workspace with_files: files do |ws|
24
+ minitar_pack_in_workspace
25
+
26
+ assert ws.tarball.file?, "Tarball does not exist"
27
+ assert ws.tarball.size > 0, "Tarball should not be empty"
28
+
29
+ minitar_unpack_in_workspace
30
+
31
+ assert_files_extracted_in_workspace
32
+ refute_file_path_duplication_in_workspace
33
+
34
+ assert_extracted_files_match_source_files_in_workspace
35
+ assert_file_modes_match_in_workspace
36
+ end
37
+ end
38
+ end
@@ -1,44 +1,26 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
- require "minitar"
4
3
  require "minitest_helper"
5
- require "base64"
6
- require "zlib"
7
4
 
8
5
  class TestIssue46 < Minitest::Test
9
- SUPERLONG_TGZ = Base64.decode64(<<~EOS).freeze
10
- H4sIAK1+smYAA+3WQQ6CMBAF0K49BScAprYd3XkALoECSiQlQYzXt0IkSKLGBdXE
11
- /zbtNF000PkQRmG0SWq7T0p7FPOIHaNUNzrTkWI5zPt1IiYtgmSm8zw4n9q0CQLR
12
- 1HX7at/lkOeVjwP5FZNcKm14tU63uyyPUP91/e3rCJ75uF/j/Gej+6yXw/fArbnM
13
- Z2ZlDKlb/ktNrEQQ+3gA9/xP3aS0z/e5bUXh40B+/Vj+oJ63Xkzff26zoqzmzf13
14
- /d/98437n0izQf8DAAAAAAAAAAAAAAAAAHziCqQuXDYAKAAA
15
- EOS
16
-
17
- FILETIMES = Time.utc(2004).to_i
18
-
19
- superlong_name = (["0123456789abcde"] * 33).join("/")
20
-
21
6
  SUPERLONG_CONTENTS = {
22
- superlong_name => {size: 496, mode: 0o644},
7
+ ["0123456789abcde"].then { _1 * 33 }.join("/") => {size: 496, mode: 0o644},
23
8
  "endfile" => {size: 0, mode: 0o644}
24
9
  }
25
10
 
26
11
  def test_each_works
27
- reader = Zlib::GzipReader.new(StringIO.new(SUPERLONG_TGZ))
28
-
29
- Minitar::Input.open(reader) do |stream|
12
+ Minitar::Input.open(open_fixture("issue_46")) do |stream|
30
13
  outer = 0
31
14
  stream.each.with_index do |entry, i|
32
15
  assert_kind_of Minitar::Reader::EntryStream, entry
33
16
  assert SUPERLONG_CONTENTS.key?(entry.name), "File #{entry.name} not defined"
34
17
 
35
18
  assert_equal SUPERLONG_CONTENTS[entry.name][:size],
36
- entry.size,
37
- "File sizes sizes do not match: #{entry.name}"
19
+ entry.size, "File sizes sizes do not match: #{entry.name}"
38
20
 
39
21
  assert_modes_equal(SUPERLONG_CONTENTS[entry.name][:mode],
40
22
  entry.mode, entry.name)
41
- assert_equal FILETIMES, entry.mtime, "entry.mtime"
23
+ assert_equal TIME_2004, entry.mtime, "entry.mtime"
42
24
 
43
25
  outer += 1
44
26
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest_helper"
4
+
5
+ class TestIssue62 < Minitest::Test
6
+ # These are representative filenames from issue #62 which were extracted incorrectly.
7
+ FILENAMES = [
8
+ "hpg5lfg/1j/973e4t/hqc/djcrcb1l49ardcthyl5u80dcgmo03cp5mh938wr38dka7us1ja4i3dfrp3ahg4q2ooet6avyw45nqpzrcxfzdemvzj07oftcghtkl5bdc.gz",
9
+ "hpg5lfg/1j/973e4t/hqc/mioxx4h9tgcc9gqw0j8z2fj2covf6nsplrwggyjsg4swmh0glzy2jji4n2gspvb2vlki7zmu81046hvgt4fstlk6fldv0p1w3nf7o6.css",
10
+ "k6hly56/mh/ri2pa1/04/0afdks3r6k1mbf64xzuwh5efkuxurro63rbckjssmz9mdratf6ayfduqpb0r9qxx2mgnrs0thi0ohh4qtfylfd6cd506zawwic0u3ec0iluu4myn.map",
11
+ "k6hly56/mh/ri2pa1/04/5k8mnvwxe7hmvp1n932o4mn2b25gqrxfrbe4jfjbig6kzhphnsfkrtqruypfzl93u0ohlv9yyxcoxn6jg6iv5ml8e27jdqjiikyq3.js"
12
+ ].freeze
13
+
14
+ FILENAMES.each do |filename|
15
+ first, *, last = filename.split("/")
16
+ last = File.extname(last)
17
+
18
+ define_method :"test_issue_62_path_#{first}_#{last}" do
19
+ file_map = {filename => "Test content for #{File.basename(filename)}"}
20
+ files = roundtrip_tar_string(file_map)
21
+
22
+ assert_tar_structure_preserved file_map, files
23
+ end
24
+ end
25
+
26
+ def test_issue_62_full_regression
27
+ file_map = FILENAMES.each_with_object({}) { |name, map|
28
+ map[name] = "Test content for #{File.basename(name)}"
29
+ }
30
+ files = roundtrip_tar_string(file_map)
31
+
32
+ assert_tar_structure_preserved file_map, files
33
+ end
34
+
35
+ def test_issue_62_mixed_filename_lengths_no_regression
36
+ file_map = MIXED_FILENAME_SCENARIOS.dup
37
+ FILENAMES.each { file_map[_1] = "content for problematic filename #{_1}" }
38
+
39
+ files = roundtrip_tar_string(file_map)
40
+
41
+ assert_tar_structure_preserved file_map, files
42
+ end
43
+
44
+ def test_issue_62_very_long_filenames_no_regression
45
+ file_map = VERY_LONG_FILENAME_SCENARIOS.dup
46
+ files = roundtrip_tar_string(file_map)
47
+
48
+ assert_tar_structure_preserved file_map, files
49
+ end
50
+ end
data/test/test_minitar.rb CHANGED
@@ -1,60 +1,189 @@
1
- #!/usr/bin/env ruby
1
+ # frozen_string_literal: true
2
2
 
3
- require "minitar"
4
3
  require "minitest_helper"
5
- require "zlib"
6
4
 
7
5
  class TestMinitar < Minitest::Test
8
- FILE_2004 = Time.utc(2004).to_i
6
+ SCENARIO = {
7
+ "path" => nil,
8
+ "test" => "test",
9
+ "extra/test" => "extra/test",
10
+ "empty2004" => {mtime: TIME_2004, mode: 0o755, data: ""},
11
+ "notime" => {mtime: nil, data: "notime"}
12
+ }
9
13
 
10
- def test_pack_as_file
11
- input = [
12
- ["path", nil],
13
- ["test", "test"],
14
- ["extra/test", "extra/test"],
15
- [{name: "empty2004", mtime: FILE_2004, mode: 0o755}, ""]
16
- ]
14
+ def test_minitar_open_r
15
+ count = 0
17
16
 
18
- writer = StringIO.new
19
- Minitar::Output.open(writer) do |out_stream|
20
- input.each do |(name, data)|
21
- Minitar.pack_as_file(name, data, out_stream)
17
+ open_fixture("test_minitar") do |fixture|
18
+ Minitar.open(fixture, "r") do |stream|
19
+ stream.each do |entry|
20
+ assert_kind_of Minitar::Reader::EntryStream, entry
21
+
22
+ assert SCENARIO.has_key?(entry.name), "#{entry.name} not expected"
23
+
24
+ expected = SCENARIO[entry.name]
25
+
26
+ case expected
27
+ when nil
28
+ assert_equal 0, entry.size
29
+ assert_modes_equal 0o755, entry.mode, entry.name
30
+ assert entry.directory?, "#{entry.name} should be a directory"
31
+ when String
32
+ assert_equal expected.length, entry.size, entry.name
33
+ assert_modes_equal 0o644, entry.mode, entry.name
34
+ assert entry.file?, "#{entry.name} should be a file"
35
+
36
+ if entry.size.zero?
37
+ assert_nil entry.read
38
+ else
39
+ assert_equal expected, entry.read
40
+ end
41
+ when Hash
42
+ if expected[:data].nil?
43
+ assert_equal 0, entry.size
44
+ assert_modes_equal (expected[:mode] || 0o755), entry.mode, entry.name
45
+ assert entry.directory?, "#{entry.name} should be a directory"
46
+ else
47
+ assert_equal expected[:data].length, entry.size
48
+ assert_modes_equal (expected[:mode] || 0o644), entry.mode, entry.name
49
+ assert entry.file?, "#{entry.name} should be a file"
50
+ end
51
+
52
+ assert_equal expected[:mtime], entry.mtime if expected[:mtime]
53
+
54
+ if entry.size.zero?
55
+ assert_nil entry.read
56
+ else
57
+ assert_equal expected[:data], entry.read
58
+ end
59
+ end
60
+
61
+ count += 1
62
+ end
22
63
  end
23
64
  end
24
65
 
25
- expected = [
26
- {name: "path", size: 0, mode: 0o755},
27
- {name: "test", size: 4, mode: 0o644, data: "test"},
28
- {name: "extra/test", size: 10, mode: 0o0644, data: "extra/test"},
29
- {name: "empty2004", size: 0, mode: 0o755, mtime: FILE_2004, nil: true}
30
- ]
66
+ assert_equal SCENARIO.size, count
67
+ end
31
68
 
32
- count = 0
33
- reader = StringIO.new(writer.string)
34
- Minitar.open(reader) do |stream|
35
- stream.each.with_index do |entry, i|
36
- assert_kind_of Minitar::Reader::EntryStream, entry
69
+ def test_minitar_open_w
70
+ events = []
37
71
 
38
- assert_equal expected[i][:name], entry.name
39
- assert_equal expected[i][:size], entry.size
40
- assert_equal expected[i][:mode], entry.mode
72
+ writer = StringIO.new
73
+ Minitar.open(writer, "w") do |stream|
74
+ SCENARIO.each_pair do |name, data|
75
+ name, data =
76
+ if data.is_a?(Hash)
77
+ name = data.merge(name: name)
78
+ [name, name.delete(:data)]
79
+ else
80
+ [name, data]
81
+ end
41
82
 
42
- if expected[i].key?(:mtime)
43
- assert_equal expected[i][:mtime], entry.mtime
83
+ Minitar.pack_as_file(name, data, stream) do |op, entry_name, stats|
84
+ events << {
85
+ name: name,
86
+ data: data,
87
+ op: op,
88
+ entry_name: entry_name,
89
+ stats: stats
90
+ }
44
91
  end
92
+ end
93
+ end
45
94
 
46
- if expected[i].key?(:data)
47
- assert_equal expected[i][:data], entry.read
48
- end
95
+ assert_equal 5120, writer.string.length
49
96
 
50
- if expected[i].key?(:nil)
51
- assert_nil entry.read
52
- end
97
+ events.each do |event|
98
+ if event[:name].is_a?(Hash)
99
+ assert_equal event[:name][:name], event[:entry_name]
100
+ else
101
+ assert_equal event[:name], event[:entry_name]
102
+ end
53
103
 
54
- count += 1
104
+ case [event[:op], event[:entry_name]]
105
+ in [:dir, "path"]
106
+ assert_equal 0, event[:stats][:size]
107
+ assert_equal 493, event[:stats][:mode]
108
+
109
+ in [:file_start, "test"]
110
+ assert_equal 4, event[:stats][:size]
111
+ assert_equal 420, event[:stats][:mode]
112
+ assert_equal 4, event[:stats][:current]
113
+ assert_equal 4, event[:stats][:currinc]
114
+ assert_equal "test", event[:data]
115
+ in [:file_progress, "test"]
116
+ assert_equal 4, event[:stats][:size]
117
+ assert_equal 420, event[:stats][:mode]
118
+ assert_equal 4, event[:stats][:current]
119
+ assert_equal 4, event[:stats][:currinc]
120
+ assert_equal "test", event[:data]
121
+ in [:file_done, "test"]
122
+ assert_equal 4, event[:stats][:size]
123
+ assert_equal 420, event[:stats][:mode]
124
+ assert_equal 4, event[:stats][:current]
125
+ assert_equal 4, event[:stats][:currinc]
126
+ assert_equal "test", event[:data]
127
+
128
+ in [:file_start, "extra/test"]
129
+ assert_equal 10, event[:stats][:size]
130
+ assert_equal 420, event[:stats][:mode]
131
+ assert_equal 10, event[:stats][:current]
132
+ assert_equal 10, event[:stats][:currinc]
133
+ assert_equal "extra/test", event[:data]
134
+ in [:file_progress, "extra/test"]
135
+ assert_equal 10, event[:stats][:size]
136
+ assert_equal 420, event[:stats][:mode]
137
+ assert_equal 10, event[:stats][:current]
138
+ assert_equal 10, event[:stats][:currinc]
139
+ assert_equal "extra/test", event[:data]
140
+ in [:file_done, "extra/test"]
141
+ assert_equal 10, event[:stats][:size]
142
+ assert_equal 420, event[:stats][:mode]
143
+ assert_equal 10, event[:stats][:current]
144
+ assert_equal 10, event[:stats][:currinc]
145
+ assert_equal "extra/test", event[:data]
146
+
147
+ in [:file_start, "empty2004"]
148
+ assert_equal 0, event[:stats][:size]
149
+ assert_equal 493, event[:stats][:mode]
150
+ assert_equal 0, event[:stats][:current]
151
+ assert_equal 1072915200, event[:stats][:mtime]
152
+ assert_equal "", event[:data]
153
+ in [:file_done, "empty2004"]
154
+ assert_equal 0, event[:stats][:size]
155
+ assert_equal 493, event[:stats][:mode]
156
+ assert_equal 0, event[:stats][:current]
157
+ assert_equal 1072915200, event[:stats][:mtime]
158
+ assert_equal "", event[:data]
159
+
160
+ in [:file_start, "notime"]
161
+ assert_equal 6, event[:stats][:size]
162
+ assert_equal 420, event[:stats][:mode]
163
+ assert_equal 6, event[:stats][:current]
164
+ assert_equal 6, event[:stats][:currinc]
165
+ assert_equal "notime", event[:data]
166
+ in [:file_progress, "notime"]
167
+ assert_equal 6, event[:stats][:size]
168
+ assert_equal 420, event[:stats][:mode]
169
+ assert_equal 6, event[:stats][:current]
170
+ assert_equal 6, event[:stats][:currinc]
171
+ assert_equal "notime", event[:data]
172
+ in [:file_done, "notime"]
173
+ assert_equal 6, event[:stats][:size]
174
+ assert_equal 420, event[:stats][:mode]
175
+ assert_equal 6, event[:stats][:current]
176
+ assert_equal 6, event[:stats][:currinc]
177
+ assert_equal "notime", event[:data]
178
+ else
179
+ raise "Unknown operation #{event[:op].inspect} for #{event[:entry_name].inspect}"
55
180
  end
56
181
  end
182
+ end
57
183
 
58
- assert_equal expected.size, count
184
+ def test_minitar_x
185
+ assert_raises(ArgumentError) do
186
+ Minitar.open("foo", "x")
187
+ end
59
188
  end
60
189
  end
@@ -0,0 +1,104 @@
1
+ require "minitest_helper"
2
+
3
+ class TestPaxHeader < Minitest::Test
4
+ def test_from_stream_with_size_attribute
5
+ pax_content = "19 size=8614356715\n28 mtime=1749098832.3200000\n"
6
+ pax_header = create_pax_header_from_stream(pax_content)
7
+
8
+ assert_equal 8614356715, pax_header.size
9
+ assert_equal "1749098832.3200000", pax_header.attributes["mtime"]
10
+ end
11
+
12
+ def test_from_stream_without_size_attribute
13
+ pax_content = "28 mtime=1749098832.3200000\n27 path=some/long/path.txt\n"
14
+ pax_header = create_pax_header_from_stream(pax_content)
15
+
16
+ assert_nil pax_header.size
17
+ assert_equal "some/long/path.txt", pax_header.path
18
+ assert_equal 1749098832.32, pax_header.mtime
19
+ end
20
+
21
+ def test_parse_multiline_values
22
+ pax_content = "22 foo=one\ntwo\nthree\n\n12 bar=four\n"
23
+ pax_header = Minitar::PaxHeader.from_data(pax_content)
24
+ assert_equal "one\ntwo\nthree\n", pax_header.attributes["foo"]
25
+ assert_equal "four", pax_header.attributes["bar"]
26
+ end
27
+
28
+ def test_from_stream_with_invalid_header
29
+ header_data = build_tar_file_header("regular_file.txt", "", 0o644, 100)
30
+ io = StringIO.new(header_data)
31
+
32
+ posix_header = Minitar::PosixHeader.from_stream(io)
33
+ refute posix_header.pax_header?
34
+
35
+ assert_raises(ArgumentError, "Header must be a PAX header") do
36
+ Minitar::PaxHeader.from_stream(io, posix_header)
37
+ end
38
+ end
39
+
40
+ def test_parse_content_with_multiple_attributes
41
+ pax_content = "19 size=8614356715\n28 mtime=1749098832.3200000\n27 path=some/long/path.txt\n"
42
+
43
+ pax_header = Minitar::PaxHeader.from_data(pax_content)
44
+
45
+ assert_equal 8614356715, pax_header.size
46
+ assert_equal "some/long/path.txt", pax_header.path
47
+ assert_equal 1749098832.32, pax_header.mtime
48
+
49
+ # Check raw attributes
50
+ assert_equal "8614356715", pax_header.attributes["size"]
51
+ assert_equal "1749098832.3200000", pax_header.attributes["mtime"]
52
+ assert_equal "some/long/path.txt", pax_header.attributes["path"]
53
+ end
54
+
55
+ def test_parse_content_with_invalid_length_format
56
+ assert_raises(ArgumentError) do
57
+ Minitar::PaxHeader.from_data("19 size=8614356715\ninvalid line\n23 path=valid/path.txt\n")
58
+ end
59
+ end
60
+
61
+ def test_parse_content_with_oversized_record
62
+ assert_raises(ArgumentError) do
63
+ Minitar::PaxHeader.from_data("19 size=8614356715\n999 toolong=value\n")
64
+ end
65
+ end
66
+
67
+ def test_from_stream_strips_padding
68
+ pax_content = "19 size=8614356715\n"
69
+ pax_header = create_pax_header_from_stream(pax_content)
70
+
71
+ # Should parse only the actual content, ignoring padding
72
+ assert_equal 8614356715, pax_header.size
73
+ assert_equal 1, pax_header.attributes.size # Only one attribute parsed
74
+
75
+ # Should have parsed content correctly
76
+ assert_equal 1, pax_header.attributes.size
77
+ assert_equal "8614356715", pax_header.attributes["size"]
78
+ end
79
+
80
+ def test_attributes_accessor
81
+ pax_content = "19 size=8614356715\n23 custom=custom_value\n"
82
+ pax_header = Minitar::PaxHeader.from_data(pax_content)
83
+
84
+ assert_equal "8614356715", pax_header.attributes["size"]
85
+ assert_equal "custom_value", pax_header.attributes["custom"]
86
+ assert_nil pax_header.attributes["nonexistent"]
87
+ end
88
+
89
+ def test_pax_header_to_s
90
+ pax_header = Minitar::PaxHeader.new(size: "8614356715", mtime: "1749098832.3200000")
91
+ assert_equal "19 size=8614356715\n28 mtime=1749098832.3200000\n", pax_header.to_s
92
+ end
93
+
94
+ private
95
+
96
+ def create_pax_header_from_stream(pax_content, name = "./PaxHeaders.X/test_file")
97
+ pax_header_data = build_tar_pax_header(name, "", pax_content.bytesize)
98
+ padded_content = pax_content.ljust((pax_content.bytesize / 512.0).ceil * 512, "\0")
99
+ io = StringIO.new(pax_header_data + padded_content)
100
+
101
+ posix_header = Minitar::PosixHeader.from_stream(io)
102
+ Minitar::PaxHeader.from_stream(io, posix_header)
103
+ end
104
+ end
@@ -0,0 +1,66 @@
1
+ require "minitest_helper"
2
+
3
+ class TestPaxSupport < Minitest::Test
4
+ def test_pax_header_size_extraction_in_reader
5
+ pax_content = "16 size=1048576\n28 mtime=1749098832.3200000\n"
6
+ tar_data = create_pax_with_file_headers(pax_content, "./PaxHeaders.X/large_file.mov", "large_file.mov", 1048576, 0)
7
+
8
+ entries = read_tar_entries(tar_data)
9
+ assert_equal 1, entries.size
10
+
11
+ entry = entries.first
12
+ assert_equal "large_file.mov", entry.name
13
+ assert_equal 1048576, entry.size # Size from PAX header
14
+ end
15
+
16
+ def test_pax_header_without_size_uses_header_size
17
+ pax_content = "28 mtime=1749098832.3200000\n"
18
+ tar_data = create_pax_with_file_headers(pax_content, "./PaxHeaders.X/normal_file.txt", "normal_file.txt", 12345, 12345)
19
+
20
+ entries = read_tar_entries(tar_data)
21
+ assert_equal 1, entries.size
22
+
23
+ entry = entries.first
24
+ assert_equal "normal_file.txt", entry.name
25
+ assert_equal 12345, entry.size # Original header size preserved
26
+ end
27
+
28
+ def test_pax_header_takes_precedence_over_posix_header_size
29
+ pax_content = "16 size=1048576\n28 mtime=1749098832.3200000\n"
30
+ tar_data = create_pax_with_file_headers(pax_content, "./PaxHeaders.X/precedence_file.txt", "precedence_file.txt", 12345, 12345)
31
+
32
+ entries = read_tar_entries(tar_data)
33
+ assert_equal 1, entries.size
34
+
35
+ entry = entries.first
36
+ assert_equal "precedence_file.txt", entry.name
37
+ assert_equal 1048576, entry.size # PAX size takes precedence over POSIX size (12345)
38
+ end
39
+
40
+ def test_pax_size_extraction_logic
41
+ pax_header_with_size = Minitar::PaxHeader.new(size: "1048576", mtime: "1749098832.3200000")
42
+ assert_equal 1048576, pax_header_with_size.size
43
+
44
+ pax_header_without_size = Minitar::PaxHeader.new(mtime: "1749098832.3200000")
45
+ assert_nil pax_header_without_size.size
46
+ end
47
+
48
+ private
49
+
50
+ def read_tar_entries(tar_data)
51
+ io = StringIO.new(tar_data)
52
+ Minitar::Reader.open(io, &:to_a)
53
+ end
54
+
55
+ def create_pax_with_file_headers(pax_content, pax_name, file_name, file_size, posix_header_file_size)
56
+ file_content = "x" * file_size
57
+ padded_file_content = file_content.ljust((file_size / 512.0).ceil * 512, "\0")
58
+
59
+ [
60
+ build_tar_pax_header(pax_name, "", pax_content.bytesize),
61
+ pax_content.ljust((pax_content.bytesize / 512.0).ceil * 512, "\0"),
62
+ build_tar_file_header(file_name, "", 0o644, file_size),
63
+ padded_file_content
64
+ ].join
65
+ end
66
+ end