Skip to content

Commit 6899bec

Browse files
committed
chore: Update contributing documentation
This also fixes a couple of misnamed test files and improves test coverage slightly. Signed-off-by: Austin Ziegler <austin@zieglers.ca>
1 parent 36c7402 commit 6899bec

File tree

11 files changed

+391
-64
lines changed

11 files changed

+391
-64
lines changed

CONTRIBUTING.md

Lines changed: 191 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ contributions. There are a few DOs and DON'Ts that should be followed:
1111
- Use thoughtfully-named topic branches for contributions. Rebase your commits
1212
into logical chunks as necessary.
1313

14-
- Use [quality commit messages][qcm].
14+
- Use [quality commit messages][qcm] for each commit (minitar uses a rebase
15+
merge strategy). Ensure that each commit includes the required Developer
16+
Certificate of Origin [sign-off][sign-off].
1517

1618
- Add your name or GitHub handle to `CONTRIBUTORS.md` and a record in the
1719
`CHANGELOG.md` as a separate commit from your main change. (Follow the style
@@ -49,7 +51,10 @@ without such declaration, the pull request **will be declined**.
4951
Any contribution (bug, feature request, or pull request) that uses unreviewed
5052
LLM output will be rejected.
5153

52-
## Test Dependencies
54+
For an example of how this should be done, see [#151][pr-151] and its
55+
[associated commits][pr-151-commits].
56+
57+
## Test
5358

5459
minitar uses Ryan Davis's [Hoe][Hoe] to manage the release process, and it adds
5560
a number of rake tasks. You will mostly be interested in `rake`, which runs
@@ -62,6 +67,186 @@ the development dependencies.
6267

6368
You can run tests with code coverage analysis by running `rake coverage`.
6469

70+
### Test Helpers
71+
72+
Minitar includes a number of custom test assertions, constants, and test utility
73+
methods that are useful for writing tests. These are maintained through modules
74+
defined in `test/support`.
75+
76+
#### Fixture Utilities
77+
78+
Minitar uses fixture tarballs in various tests, referenced by their base name
79+
(`test/fixtures/tar_input.tar.gz` becomes `tar_input`, etc.). There are two
80+
utility methods:
81+
82+
- `Fixture(name)`: This returns the `Pathname` object for the full path of the
83+
named fixture tarball or `nil` if the named fixture does not exist.
84+
85+
- `open_fixture(name)`: This retrieves the named fixture and opens it. If the
86+
fixture ends with `.gz` or `.tgz`, it will be opened with a
87+
`Zlib::GZipReader`. A block may be provided to ensure that the fixture is
88+
automatically closed.
89+
90+
#### Header Assertions and Utilities
91+
92+
Tar headers need to be built and compared in an exacting way, even for tests.
93+
94+
There are two assertions:
95+
96+
- `assert_headers_equal(expected, actual)`: This compares headers by field order
97+
verifying that each field in `actual` is supposed to match the corresponding
98+
field in `expected`.
99+
100+
`expected` must be a string representation of the expected header and this
101+
assertion calls `#to_s` on the `actual` value so that both `PosixHeader` and
102+
`PaxHeader` instances are converted to string representations for comparison.
103+
104+
- `assert_modes_equal(expected, actual, filename)`: This compares the expected
105+
octal mode string of `expected` against `actual` for a given `filename`. The
106+
modes must be integer values. This assertion is skipped on Windows.
107+
108+
There are several other helper methods available for working with headers:
109+
110+
- `build_tar_file_header(name, prefix, mode, length)`: This builds a header for
111+
a file `prefix/name` with `mode` and `length` bytes. `name` is limited to 100
112+
bytes and `prefix` is limited to 155 bytes.
113+
114+
- `build_tar_dir_header(name, prefix, mode)`: This builds a header for a
115+
directory `prefix/name` with `mode`. `name` is limited to 100 bytes and
116+
`prefix` is limited to 155 bytes.
117+
118+
- `build_tar_symlink_header(name, prefix, mode, target)`: This builds a header
119+
for a symbolic link of `prefix/name` to `target` where the symbolic link has
120+
`mode`. `name` is limited to 100 bytes and `prefix` is limited to 155 bytes.
121+
122+
- `build_tar_pax_header(name, prefix, bytes)`: This builds a header block for a
123+
PAX extension at `name/prefix` with `content_size` bytes.
124+
125+
- `build_header(type, name, prefix, size, mode, link = "")`: This builds an
126+
otherwise unspecified header type. If you find yourself using this, it is
127+
recommended to add a new `build_*_header` helper method.
128+
129+
#### Tarball Helpers
130+
131+
Minitar has several complex assertions and utilities to work with both in-memory
132+
and on-disk tarballs. These work using two concepts, file hashes (`file_hash`)
133+
and workspaces (`workspace`).
134+
135+
##### File Hashes (`file_hash`)
136+
137+
Many of these consume or produce a `file_hash`, which is a hash of
138+
`{filename => content}` where the tarball will be produced with such that each
139+
entry in the `file_hash` becomes a file named `filename` with the data
140+
`content`.
141+
142+
As an example, `Minitar::TestHelpers` has a `MIXED_FILENAME_SCENARIOS` constant
143+
that is a `file_hash`:
144+
145+
```ruby
146+
MIXED_FILENAME_SCENARIOS = {
147+
"short.txt" => "short content",
148+
"medium_length_filename_under_100_chars.txt" => "medium content",
149+
"dir1/medium_filename.js" => "medium nested content",
150+
"#{"x" * 120}.txt" => "long content",
151+
"nested/dir/#{"y" * 110}.css" => "long nested content"
152+
}.freeze
153+
```
154+
155+
This will produce a tarball that looks like:
156+
157+
```
158+
short.txt
159+
medium_length_filename_under_100_chars.txt
160+
dir1/medium_filename.js
161+
x[118 more 'x' characters...]x
162+
nested/dir/y[108 more y' characters...]y.css
163+
```
164+
165+
Each file will contain the text as the content.
166+
167+
If the `content` is `nil`, this will be ignored for in-memory tarballs, but will
168+
be created as empty directory entries for on-disk tarballs.
169+
170+
##### Workspace (`workspace`)
171+
172+
A workspace is a temporary directory used for on-disk tests. It is created with
173+
the `workspace` utility method (see below) and must be passed a block where all
174+
setup and tests will be run.
175+
176+
At most one `workspace` may be used per test method.
177+
178+
##### Assertions
179+
180+
There are five assertions:
181+
182+
- `assert_tar_structure_preserved(original_files, extracted_files)`: This is
183+
used primarily with string tarballs. Given two `file_hash`es representing
184+
tarball contents (the original files passed to `create_tar_string` and the
185+
extracted files returned from `extract_tar_string`), it ensures that all files
186+
from the original contents are present and that no additional files have been
187+
added in the process.
188+
189+
- `assert_files_extracted_in_workspace`: Can only be run in a `workspace` and
190+
the test tarball must have been both created and extracted. This ensures that
191+
all of the files and/or directories expected have been extracted and that the
192+
contents of files match. File modes are ignored for this assertion.
193+
194+
- `refute_file_path_duplication_in_workspace`: Can only be run in a `workspace`
195+
and the test tarball must have been both created and extracted. This is used
196+
to prevent regression of [#62][issue-62] with explicit file tests. This only
197+
needs to be called after unpacking with Minitar methods.
198+
199+
- `assert_extracted_files_match_source_files_in_workspace`: Can only be run in a
200+
`workspace` and the test tarball must have been both created and extracted.
201+
This ensures that there are no files missing or added in the `target`
202+
directory that should are not also be in the `source` directory. This does no
203+
contents comparison.
204+
205+
- `assert_file_modes_match_in_workspace`: Can only be run in a `workspace` and
206+
the test tarball must have been both created and extracted. This ensures that
207+
all files have the same modes between source and target. This is skipped on
208+
Windows.
209+
210+
##### In-Memory Tarball Utilities
211+
212+
- `create_tar_string`: Given a `file_hash`, this creates a string containing the
213+
output of `Minitar::Output.open` and `Minitar.pack_as_file`.
214+
215+
- `extract_tar_string`: Given the string output of `create_tar_string` (or any
216+
uncompressed tarball string), uses `Minitar::Input.open` to read the files
217+
into a hash of `{filename => content}`.
218+
219+
- `roundtrip_tar_string`: calls `create_tar_string` on a `file_hash` and
220+
immediately calls `extract_tar_string`, returning a processed `file_hash`.
221+
222+
##### On-Disk Workspace Tarball Utilities
223+
224+
- `workspace`: Prepares a temporary directory for working with tarballs on disk
225+
inside the block that must be provided. If given a hash of files, calls
226+
`prepare_files`. The workspace directory will be removed after the block
227+
finishes executing.
228+
229+
A workspace has a `source` directory, a `target` directory`, and the`tarball`
230+
which will be created from the prepared files.
231+
232+
All other utility methods _must_ be run inside of a `workspace` block.
233+
234+
- `prepare_workspace`: creates a file structure in the workspace source
235+
directory given the `{filename => content}` hash. For on-disk file structures,
236+
`{directory_name => nil}` can be used to create empty directories. Directory
237+
names will be created automatically for nested filenames.
238+
239+
- `gnu_tar_create_in_workspace`, `gnu_tar_extract_in_workspace`, and
240+
`gnu_tar_list_in_workspace` work with the workspace tarball using GNU tar
241+
(either `tar` or `gtar`). GNU tar tests will be skipped if GNU tar is not
242+
available.
243+
244+
- `minitar_pack_in_workspace`, `minitar_unpack_in_workspace` use `Minitar.pack`
245+
and `Minitar.unpack`, respectively, to work with the workspace tarball.
246+
247+
- `minitar_writer_create_in_workspace` uses `Minitar::Writer` to create the
248+
workspace tarball.
249+
65250
## Workflow
66251

67252
Here's the most direct way to get your work merged into the project:
@@ -79,6 +264,10 @@ Here's the most direct way to get your work merged into the project:
79264

80265
[dco]: licences/dco.txt
81266
[hoe]: https://github.com/seattlerb/hoe
267+
[issue-62]: https://github.com/halostatue/minitar/issues/62
82268
[minitest]: https://github.com/seattlerb/minitest
269+
[pr-151-commits]: https://github.com/halostatue/minitar/pull/151/commits
270+
[pr-151]: https://github.com/halostatue/minitar/pull/151
83271
[qcm]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
272+
[sign-off]: LICENCE.md#developer-certificate-of-origin
84273
[standardrb]: https://github.com/standardrb/standard

LICENCE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Licence
22

3-
- SPDX-License-Identifier: [Ruby][ruby-license] OR [BSD-2-Clause]
3+
- SPDX-License-Identifier: [Ruby][ruby-license] OR [BSD-2-Clause][bsd-2-clause]
44

55
minitar is free software that may be redistributed and/or modified under the
66
terms of Ruby’s licence or the Simplified BSD licence.

Manifest.txt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ docs/ruby.txt
1212
lib/minitar.rb
1313
lib/minitar/input.rb
1414
lib/minitar/output.rb
15+
lib/minitar/pax_header.rb
1516
lib/minitar/posix_header.rb
1617
lib/minitar/reader.rb
1718
lib/minitar/version.rb
@@ -20,11 +21,12 @@ licenses/bsdl.txt
2021
licenses/dco.txt
2122
licenses/ruby.txt
2223
test/fixtures/issue_46.tar.gz
23-
test/fixtures/issue_52.tar.gz
24+
test/fixtures/issue_62.tar.gz
2425
test/fixtures/tar_input.tgz
2526
test/fixtures/test_input_non_strict_octal.tgz
2627
test/fixtures/test_input_relative.tgz
2728
test/fixtures/test_input_space_octal.tgz
29+
test/fixtures/test_minitar.tar.gz
2830
test/minitest_helper.rb
2931
test/support/minitar_test_helpers.rb
3032
test/support/minitar_test_helpers/fixtures.rb
@@ -34,8 +36,10 @@ test/test_filename_boundary_conditions.rb
3436
test/test_gnu_tar_compatibility.rb
3537
test/test_integration_pack_unpack_cycle.rb
3638
test/test_issue_46.rb
37-
test/test_issue_52.rb
39+
test/test_issue_62.rb
3840
test/test_minitar.rb
41+
test/test_pax_header.rb
42+
test/test_pax_support.rb
3943
test/test_tar_header.rb
4044
test/test_tar_input.rb
4145
test/test_tar_output.rb

lib/minitar.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ def open(dest, mode = "r", &)
7777
when "r", "rb"
7878
Minitar::Input.open(dest, &)
7979
when "w", "wb"
80-
Minitar::Output.open(dest, &block)
80+
Minitar::Output.open(dest, &)
8181
else
82-
raise "Unknown open mode for Minitar.open."
82+
raise ArgumentError, "Unknown open mode for Minitar.open."
8383
end
8484
end
8585

minitar.gemspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ Gem::Specification.new do |s|
99
s.metadata = { "bug_tracker_uri" => "https://github.com/halostatue/minitar/issues", "changelog_uri" => "https://github.com/halostatue/minitar/blob/main/CHANGELOG.md", "rubygems_mfa_required" => "true", "source_code_uri" => "https://github.com/halostatue/minitar" } if s.respond_to? :metadata=
1010
s.require_paths = ["lib".freeze]
1111
s.authors = ["Austin Ziegler".freeze]
12-
s.date = "2025-09-07"
12+
s.date = "2025-09-08"
1313
s.description = "The minitar library is a pure-Ruby library that operates on POSIX tar(1) archive files. minitar (previously called Archive::Tar::Minitar) is based heavily on code originally written by Mauricio Julio Fern\u00E1ndez Pradier for the rpa-base project.".freeze
1414
s.email = ["halostatue@gmail.com".freeze]
1515
s.extra_rdoc_files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "SECURITY.md".freeze, "docs/bsdl.txt".freeze, "docs/ruby.txt".freeze, "licenses/bsdl.txt".freeze, "licenses/dco.txt".freeze, "licenses/ruby.txt".freeze]
16-
s.files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "Rakefile".freeze, "SECURITY.md".freeze, "docs/bsdl.txt".freeze, "docs/ruby.txt".freeze, "lib/minitar.rb".freeze, "lib/minitar/input.rb".freeze, "lib/minitar/output.rb".freeze, "lib/minitar/posix_header.rb".freeze, "lib/minitar/reader.rb".freeze, "lib/minitar/version.rb".freeze, "lib/minitar/writer.rb".freeze, "licenses/bsdl.txt".freeze, "licenses/dco.txt".freeze, "licenses/ruby.txt".freeze, "test/fixtures/issue_46.tar.gz".freeze, "test/fixtures/issue_52.tar.gz".freeze, "test/fixtures/tar_input.tgz".freeze, "test/fixtures/test_input_non_strict_octal.tgz".freeze, "test/fixtures/test_input_relative.tgz".freeze, "test/fixtures/test_input_space_octal.tgz".freeze, "test/minitest_helper.rb".freeze, "test/support/minitar_test_helpers.rb".freeze, "test/support/minitar_test_helpers/fixtures.rb".freeze, "test/support/minitar_test_helpers/header.rb".freeze, "test/support/minitar_test_helpers/tarball.rb".freeze, "test/test_filename_boundary_conditions.rb".freeze, "test/test_gnu_tar_compatibility.rb".freeze, "test/test_integration_pack_unpack_cycle.rb".freeze, "test/test_issue_46.rb".freeze, "test/test_issue_52.rb".freeze, "test/test_minitar.rb".freeze, "test/test_tar_header.rb".freeze, "test/test_tar_input.rb".freeze, "test/test_tar_output.rb".freeze, "test/test_tar_reader.rb".freeze, "test/test_tar_writer.rb".freeze]
16+
s.files = ["CHANGELOG.md".freeze, "CODE_OF_CONDUCT.md".freeze, "CONTRIBUTING.md".freeze, "CONTRIBUTORS.md".freeze, "LICENCE.md".freeze, "Manifest.txt".freeze, "README.md".freeze, "Rakefile".freeze, "SECURITY.md".freeze, "docs/bsdl.txt".freeze, "docs/ruby.txt".freeze, "lib/minitar.rb".freeze, "lib/minitar/input.rb".freeze, "lib/minitar/output.rb".freeze, "lib/minitar/pax_header.rb".freeze, "lib/minitar/posix_header.rb".freeze, "lib/minitar/reader.rb".freeze, "lib/minitar/version.rb".freeze, "lib/minitar/writer.rb".freeze, "licenses/bsdl.txt".freeze, "licenses/dco.txt".freeze, "licenses/ruby.txt".freeze, "test/fixtures/issue_46.tar.gz".freeze, "test/fixtures/issue_62.tar.gz".freeze, "test/fixtures/tar_input.tgz".freeze, "test/fixtures/test_input_non_strict_octal.tgz".freeze, "test/fixtures/test_input_relative.tgz".freeze, "test/fixtures/test_input_space_octal.tgz".freeze, "test/minitest_helper.rb".freeze, "test/support/minitar_test_helpers.rb".freeze, "test/support/minitar_test_helpers/fixtures.rb".freeze, "test/support/minitar_test_helpers/header.rb".freeze, "test/support/minitar_test_helpers/tarball.rb".freeze, "test/test_filename_boundary_conditions.rb".freeze, "test/test_gnu_tar_compatibility.rb".freeze, "test/test_integration_pack_unpack_cycle.rb".freeze, "test/test_issue_46.rb".freeze, "test/test_issue_62.rb".freeze, "test/test_minitar.rb".freeze, "test/test_pax_header.rb".freeze, "test/test_pax_support.rb".freeze, "test/test_tar_header.rb".freeze, "test/test_tar_input.rb".freeze, "test/test_tar_output.rb".freeze, "test/test_tar_reader.rb".freeze, "test/test_tar_writer.rb".freeze]
1717
s.homepage = "https://github.com/halostatue/minitar".freeze
1818
s.licenses = ["Ruby".freeze, "BSD-2-Clause".freeze]
1919
s.rdoc_options = ["--main".freeze, "README.md".freeze]
File renamed without changes.

test/fixtures/test_minitar.tar.gz

189 Bytes
Binary file not shown.

test/support/minitar_test_helpers/header.rb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,33 +22,33 @@ def assert_headers_equal(expected, actual)
2222
end
2323
end
2424

25-
def assert_modes_equal(expected, actual, name)
25+
def assert_modes_equal(expected, actual, filename)
2626
return if Minitar.windows?
2727

28-
assert_equal mode_string(expected), mode_string(actual), "Mode for #{name} does not match"
28+
assert_equal mode_string(expected), mode_string(actual), "Mode for #{filename} does not match"
2929
end
3030

31-
def build_raw_header(type, fname, dname, length, mode, link_name = "") =
31+
def build_raw_header(type, name, prefix, size, mode, link_name = "") =
3232
[
33-
fname, mode, z(octal(nil, 7)), z(octal(nil, 7)), length, z(octal(0, 11)),
33+
name, mode, z(octal(nil, 7)), z(octal(nil, 7)), size, z(octal(0, 11)),
3434
BLANK_CHECKSUM, type, asciiz(link_name, 100), USTAR, DOUBLE_ZERO, asciiz("", 32),
35-
asciiz("", 32), z(octal(nil, 7)), z(octal(nil, 7)), dname
35+
asciiz("", 32), z(octal(nil, 7)), z(octal(nil, 7)), prefix
3636
].join.bytes.to_a.pack("C100C8C8C8C12C12C8CC100C6C2C32C32C8C8C155").then {
3737
"#{_1}#{"\0" * (512 - _1.bytesize)}"
3838
}.tap { assert_equal 512, _1.bytesize }
3939

40-
def build_header(type, fname, dname, length, mode, link_name = "") =
40+
def build_header(type, name, prefix, size, mode, link_name = "") =
4141
build_raw_header(
4242
type,
43-
asciiz(fname, 100),
44-
asciiz(dname, 155),
45-
z(octal(length, 11)),
43+
asciiz(name, 100),
44+
asciiz(prefix, 155),
45+
z(octal(size, 11)),
4646
z(octal(mode, 7)),
4747
asciiz(link_name, 100)
4848
)
4949

50-
def build_tar_file_header(fname, dname, mode, length) =
51-
build_header("0", fname, dname, length, mode).then {
50+
def build_tar_file_header(name, prefix, mode, size) =
51+
build_header("0", name, prefix, size, mode).then {
5252
update_header_checksum(_1)
5353
}
5454

@@ -80,7 +80,7 @@ def update_header_checksum(header) =
8080

8181
def octal(n, pad_size) = n.nil? ? "\0" * pad_size : "%0#{pad_size}o" % n
8282

83-
def asciiz(str, length) = "#{str}#{"\0" * (length - str.bytesize)}"
83+
def asciiz(str, size) = "#{str}#{"\0" * (size - str.bytesize)}"
8484

8585
def sp(s) = "#{s} "
8686

test/support/minitar_test_helpers/tarball.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ module Minitar::TestHelpers::Tarball
2121

2222
# Given the +original_files+ file hash (input to +create_tar_string+) and the
2323
# +extracted_files+ file has (output from +extract_tar_string+), ensures that the tar
24-
# structure is preserved, including checking for possible regression of issue 52.
24+
# structure is preserved, including checking for possible regression of issue 62.
2525
#
2626
# Such a regression would result in a directory like <tt>>/b/c.txt</tt> looking like
2727
# <tt>a/b/a/b/c.txt</tt> (but only for long filenames).
@@ -46,7 +46,7 @@ def assert_tar_structure_preserved(original_files, extracted_files)
4646
duplicated_paths = extracted_paths.select { |path| path == bad_pattern }
4747

4848
refute duplicated_paths.any?,
49-
"Regression of #52, path duplication on extraction! " \
49+
"Regression of #62, path duplication on extraction! " \
5050
"Original: #{filename}, " \
5151
"Bad pattern found: #{bad_pattern}, " \
5252
"All extracted paths: #{extracted_paths}"
@@ -58,6 +58,7 @@ def create_tar_string(file_hash) =
5858
StringIO.new.tap { |io|
5959
Minitar::Output.open(io) do |output|
6060
file_hash.each do |filename, content|
61+
next if content.nil?
6162
Minitar.pack_as_file(filename, content.to_s.dup, output)
6263
end
6364
end

0 commit comments

Comments
 (0)