minitar-1.1.0/0000755000004100000410000000000015061026725013210 5ustar www-datawww-dataminitar-1.1.0/Manifest.txt0000644000004100000410000000222715061026725015522 0ustar www-datawww-dataCHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md CONTRIBUTORS.md LICENCE.md Manifest.txt README.md Rakefile SECURITY.md docs/bsdl.txt docs/ruby.txt lib/minitar.rb lib/minitar/input.rb lib/minitar/output.rb lib/minitar/pax_header.rb lib/minitar/posix_header.rb lib/minitar/reader.rb lib/minitar/version.rb lib/minitar/writer.rb licenses/bsdl.txt licenses/dco.txt licenses/ruby.txt test/fixtures/issue_46.tar.gz test/fixtures/issue_62.tar.gz test/fixtures/tar_input.tgz test/fixtures/test_input_non_strict_octal.tgz test/fixtures/test_input_relative.tgz test/fixtures/test_input_space_octal.tgz test/fixtures/test_minitar.tar.gz test/minitest_helper.rb test/support/minitar_test_helpers.rb test/support/minitar_test_helpers/fixtures.rb test/support/minitar_test_helpers/header.rb test/support/minitar_test_helpers/tarball.rb test/test_filename_boundary_conditions.rb test/test_gnu_tar_compatibility.rb test/test_integration_pack_unpack_cycle.rb test/test_issue_46.rb test/test_issue_62.rb test/test_minitar.rb test/test_pax_header.rb test/test_pax_support.rb test/test_tar_header.rb test/test_tar_input.rb test/test_tar_output.rb test/test_tar_reader.rb test/test_tar_writer.rb minitar-1.1.0/minitar.gemspec0000644000004100000410000001075115061026725016224 0ustar www-datawww-data######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: minitar 1.1.0 ruby lib Gem::Specification.new do |s| s.name = "minitar".freeze s.version = "1.1.0".freeze s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= 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= s.require_paths = ["lib".freeze] s.authors = ["Austin Ziegler".freeze] s.date = "1980-01-02" s.description = "The minitar library is a pure-Ruby library that operates on POSIX tar(1) archive\nfiles.\n\nminitar (previously called Archive::Tar::Minitar) is based heavily on code\noriginally written by Mauricio Julio Fern\u00E1ndez Pradier for the rpa-base project.".freeze s.email = ["halostatue@gmail.com".freeze] 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] 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/fixtures/test_minitar.tar.gz".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] s.homepage = "https://github.com/halostatue/minitar".freeze s.licenses = ["Ruby".freeze, "BSD-2-Clause".freeze] s.rdoc_options = ["--main".freeze, "README.md".freeze] s.required_ruby_version = Gem::Requirement.new(">= 3.1".freeze) s.rubygems_version = "3.6.9".freeze s.summary = "The minitar library is a pure-Ruby library that operates on POSIX tar(1) archive files".freeze s.specification_version = 4 s.add_development_dependency(%q.freeze, ["~> 4.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 2.1".freeze, ">= 2.1.1".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 5.16".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.1".freeze]) s.add_development_dependency(%q.freeze, [">= 10.0".freeze, "< 14".freeze]) s.add_development_dependency(%q.freeze, [">= 0.0".freeze, "< 7".freeze]) s.add_development_dependency(%q.freeze, ["~> 0.22".freeze]) s.add_development_dependency(%q.freeze, ["~> 0.8".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) s.add_development_dependency(%q.freeze, ["~> 1.0".freeze]) end minitar-1.1.0/licenses/0000755000004100000410000000000015061026725015015 5ustar www-datawww-dataminitar-1.1.0/licenses/ruby.txt0000644000004100000410000000425515061026725016545 0ustar www-datawww-data1. You may make and give away verbatim copies of the source form of the software without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may modify your copy of the software in any way, provided that you do at least ONE of the following: a. place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or by allowing the author to include your modifications in the software. b. use the modified software only within your corporation or organization. c. give non-standard binaries non-standard names, with instructions on where to get the original software distribution. d. make other distribution arrangements with the author. 3. You may distribute the software in object code or binary form, provided that you do at least ONE of the following: a. distribute the binaries and library files of the software, together with instructions (in the manual page or equivalent) on where to get the original distribution. b. accompany the distribution with the machine-readable source of the software. c. give non-standard binaries non-standard names, with instructions on where to get the original software distribution. d. make other distribution arrangements with the author. 4. You may modify and include the part of the software into any other software (possibly commercial). But some files in the distribution are not written by the author, so that they are not under these terms. For the list of those files and their copying conditions, see the file LEGAL. 5. The scripts and library files supplied as input to or produced as output from the software do not automatically fall under the copyright of the software, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this software. 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. minitar-1.1.0/licenses/dco.txt0000644000004100000410000000252615061026725016330 0ustar www-datawww-dataDeveloper Certificate of Origin Version 1.1 Copyright (C) 2004, 2006 The Linux Foundation and its contributors. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. minitar-1.1.0/licenses/bsdl.txt0000644000004100000410000000233515061026725016505 0ustar www-datawww-dataRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. minitar-1.1.0/CONTRIBUTING.md0000644000004100000410000002653715061026725015456 0ustar www-datawww-data# Contributing Contribution to minitar is encouraged: bug reports, feature requests, or code contributions. There are a few DOs and DON'Ts that should be followed: - DO: - Keep the coding style that already exists for any updated Ruby code (support or otherwise). I use [Standard Ruby][standardrb] for linting and formatting. - Use thoughtfully-named topic branches for contributions. Rebase your commits into logical chunks as necessary. - Use [quality commit messages][qcm] for each commit (minitar uses a rebase merge strategy). Ensure that each commit includes the required Developer Certificate of Origin [sign-off][sign-off]. - Add your name or GitHub handle to `CONTRIBUTORS.md` and a record in the `CHANGELOG.md` as a separate commit from your main change. (Follow the style in the `CHANGELOG.md` and provide a link to your PR.) - Add or update tests as appropriate for your change. The test suite is written with [minitest][minitest]. - Add or update documentation as appropriate for your change. The documentation is RDoc; mime-types does not use extensions that may be present in alternative documentation generators. - DO NOT: - Modify `VERSION` in `lib/minitar/version.rb`. When your patch is accepted and a release is made, the version will be updated at that point. - Modify `minitar.gemspec`; it is a generated file. (You _may_ use `rake gemspec` to regenerate it if your change involves metadata related to gem itself). - Modify the `Gemfile`. ## LLM-Generated Contribution Policy minitar-cli accepts only issues or pull requests that are well understood by the submitter and that, especially for pull requests, the developer can attest to the [Developer Certificate of Origin][dco] for each pull request (see [LICENCE](LICENCE.md)). If LLM assistance is used in writing pull requests, this must be documented in the commit message and pull request. If there is evidence of LLM assistance without such declaration, the pull request **will be declined**. Any contribution (bug, feature request, or pull request) that uses unreviewed LLM output will be rejected. For an example of how this should be done, see [#151][pr-151] and its [associated commits][pr-151-commits]. ## Test minitar uses Ryan Davis's [Hoe][Hoe] to manage the release process, and it adds a number of rake tasks. You will mostly be interested in `rake`, which runs tests in the same way that `rake test` does. To assist with the installation of the development dependencies for minitar, I have provided the simplest possible Gemfile pointing to the (generated) `minitar.gemspec` file. This will permit you to use `bundle install` to install the development dependencies. You can run tests with code coverage analysis by running `rake coverage`. ### Test Helpers Minitar includes a number of custom test assertions, constants, and test utility methods that are useful for writing tests. These are maintained through modules defined in `test/support`. #### Fixture Utilities Minitar uses fixture tarballs in various tests, referenced by their base name (`test/fixtures/tar_input.tar.gz` becomes `tar_input`, etc.). There are two utility methods: - `Fixture(name)`: This returns the `Pathname` object for the full path of the named fixture tarball or `nil` if the named fixture does not exist. - `open_fixture(name)`: This retrieves the named fixture and opens it. If the fixture ends with `.gz` or `.tgz`, it will be opened with a `Zlib::GZipReader`. A block may be provided to ensure that the fixture is automatically closed. #### Header Assertions and Utilities Tar headers need to be built and compared in an exacting way, even for tests. There are two assertions: - `assert_headers_equal(expected, actual)`: This compares headers by field order verifying that each field in `actual` is supposed to match the corresponding field in `expected`. `expected` must be a string representation of the expected header and this assertion calls `#to_s` on the `actual` value so that both `PosixHeader` and `PaxHeader` instances are converted to string representations for comparison. - `assert_modes_equal(expected, actual, filename)`: This compares the expected octal mode string of `expected` against `actual` for a given `filename`. The modes must be integer values. This assertion is skipped on Windows. There are several other helper methods available for working with headers: - `build_tar_file_header(name, prefix, mode, length)`: This builds a header for a file `prefix/name` with `mode` and `length` bytes. `name` is limited to 100 bytes and `prefix` is limited to 155 bytes. - `build_tar_dir_header(name, prefix, mode)`: This builds a header for a directory `prefix/name` with `mode`. `name` is limited to 100 bytes and `prefix` is limited to 155 bytes. - `build_tar_symlink_header(name, prefix, mode, target)`: This builds a header for a symbolic link of `prefix/name` to `target` where the symbolic link has `mode`. `name` is limited to 100 bytes and `prefix` is limited to 155 bytes. - `build_tar_pax_header(name, prefix, bytes)`: This builds a header block for a PAX extension at `name/prefix` with `content_size` bytes. - `build_header(type, name, prefix, size, mode, link = "")`: This builds an otherwise unspecified header type. If you find yourself using this, it is recommended to add a new `build_*_header` helper method. #### Tarball Helpers Minitar has several complex assertions and utilities to work with both in-memory and on-disk tarballs. These work using two concepts, file hashes (`file_hash`) and workspaces (`workspace`). ##### File Hashes (`file_hash`) Many of these consume or produce a `file_hash`, which is a hash of `{filename => content}` where the tarball will be produced with such that each entry in the `file_hash` becomes a file named `filename` with the data `content`. As an example, `Minitar::TestHelpers` has a `MIXED_FILENAME_SCENARIOS` constant that is a `file_hash`: ```ruby MIXED_FILENAME_SCENARIOS = { "short.txt" => "short content", "medium_length_filename_under_100_chars.txt" => "medium content", "dir1/medium_filename.js" => "medium nested content", "#{"x" * 120}.txt" => "long content", "nested/dir/#{"y" * 110}.css" => "long nested content" }.freeze ``` This will produce a tarball that looks like: ``` short.txt medium_length_filename_under_100_chars.txt dir1/medium_filename.js x[118 more 'x' characters...]x nested/dir/y[108 more y' characters...]y.css ``` Each file will contain the text as the content. If the `content` is `nil`, this will be ignored for in-memory tarballs, but will be created as empty directory entries for on-disk tarballs. ##### Workspace (`workspace`) A workspace is a temporary directory used for on-disk tests. It is created with the `workspace` utility method (see below) and must be passed a block where all setup and tests will be run. At most one `workspace` may be used per test method. ##### Assertions There are five assertions: - `assert_tar_structure_preserved(original_files, extracted_files)`: This is used primarily with string tarballs. Given two `file_hash`es representing tarball contents (the original files passed to `create_tar_string` and the extracted files returned from `extract_tar_string`), it ensures that all files from the original contents are present and that no additional files have been added in the process. - `assert_files_extracted_in_workspace`: Can only be run in a `workspace` and the test tarball must have been both created and extracted. This ensures that all of the files and/or directories expected have been extracted and that the contents of files match. File modes are ignored for this assertion. - `refute_file_path_duplication_in_workspace`: Can only be run in a `workspace` and the test tarball must have been both created and extracted. This is used to prevent regression of [#62][issue-62] with explicit file tests. This only needs to be called after unpacking with Minitar methods. - `assert_extracted_files_match_source_files_in_workspace`: Can only be run in a `workspace` and the test tarball must have been both created and extracted. This ensures that there are no files missing or added in the `target` directory that should are not also be in the `source` directory. This does no contents comparison. - `assert_file_modes_match_in_workspace`: Can only be run in a `workspace` and the test tarball must have been both created and extracted. This ensures that all files have the same modes between source and target. This is skipped on Windows. ##### In-Memory Tarball Utilities - `create_tar_string`: Given a `file_hash`, this creates a string containing the output of `Minitar::Output.open` and `Minitar.pack_as_file`. - `extract_tar_string`: Given the string output of `create_tar_string` (or any uncompressed tarball string), uses `Minitar::Input.open` to read the files into a hash of `{filename => content}`. - `roundtrip_tar_string`: calls `create_tar_string` on a `file_hash` and immediately calls `extract_tar_string`, returning a processed `file_hash`. ##### On-Disk Workspace Tarball Utilities - `workspace`: Prepares a temporary directory for working with tarballs on disk inside the block that must be provided. If given a hash of files, calls `prepare_files`. The workspace directory will be removed after the block finishes executing. A workspace has a `source` directory, a `target` directory`, and the`tarball` which will be created from the prepared files. All other utility methods _must_ be run inside of a `workspace` block. - `prepare_workspace`: creates a file structure in the workspace source directory given the `{filename => content}` hash. For on-disk file structures, `{directory_name => nil}` can be used to create empty directories. Directory names will be created automatically for nested filenames. - `gnu_tar_create_in_workspace`, `gnu_tar_extract_in_workspace`, and `gnu_tar_list_in_workspace` work with the workspace tarball using GNU tar (either `tar` or `gtar`). GNU tar tests will be skipped if GNU tar is not available. - `minitar_pack_in_workspace`, `minitar_unpack_in_workspace` use `Minitar.pack` and `Minitar.unpack`, respectively, to work with the workspace tarball. - `minitar_writer_create_in_workspace` uses `Minitar::Writer` to create the workspace tarball. ## Workflow Here's the most direct way to get your work merged into the project: - Fork the project. - Clone your fork (`git clone git://github.com//minitar.git`). - Create a topic branch to contain your change (`git checkout -b my_awesome_feature`). - Hack away, add tests. Not necessarily in that order. - Make sure everything still passes by running `rake`. - If necessary, rebase your commits into logical chunks, without errors. - Push the branch up (`git push origin my_awesome_feature`). - Create a pull request against halostatue/minitar and describe what your change does and the why you think it should be merged. [dco]: licences/dco.txt [hoe]: https://github.com/seattlerb/hoe [issue-62]: https://github.com/halostatue/minitar/issues/62 [minitest]: https://github.com/seattlerb/minitest [pr-151-commits]: https://github.com/halostatue/minitar/pull/151/commits [pr-151]: https://github.com/halostatue/minitar/pull/151 [qcm]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [sign-off]: LICENCE.md#developer-certificate-of-origin [standardrb]: https://github.com/standardrb/standard minitar-1.1.0/SECURITY.md0000644000004100000410000000506515061026725015007 0ustar www-datawww-data# Minitar Security Policy Minitar aims to be secure by default for the data _inside_ of a tar file. ## LLM-Generated Security Report Policy Absolutely no security reports will be accepted that have been generated by LLM agents. ## Supported Versions Security reports are accepted only for the most recent major release. As of December 2024, that is the 1.0 release series. Older releases are no longer supported. ## Reporting a Vulnerability By preference, use the [Tidelift security contact][tidelift]. Tidelift will coordinate the fix and disclosure. Alternatively, Send an email to [minitar@halostatue.ca][email] with the text `Minitar` in the subject. Emails sent to this address should be encrypted using [age][age] with the following public key: ``` age1fc6ngxmn02m62fej5cl30lrvwmxn4k3q2atqu53aatekmnqfwumqj4g93w ``` ## Exclusions There are several classes of potential security issues that will not be accepted for Minitar There are several classes of "security" issues which will not be accepted for Minitar, because any issues arising from these are a matter of the library being used incorrectly. - [CWE-073](https://cwe.mitre.org/data/definitions/73.html) - [CWE-078](https://cwe.mitre.org/data/definitions/78.html) - [CWE-088](https://cwe.mitre.org/data/definitions/88.html) Minitar does _not_ perform validation or sanitization of path names provided to the convenience classes `Minitar::Output` and `Minitar::Input`, which use `Kernel.open` for their underlying implementations when not given an IO-like object. Improper use of these convenience classes with arbitrary input filenames may leave your your software to the same class of vulnerability as reported for Net::FTP ([CVE-2017-17405][CVE-2017-17405]). If the input filename argument starts with the pipe character (`|`), the command following the pipe character is executed. Additionally, the use of the `open-uri` library (which extends `Kernel.open` with transparent implementations of `Net::HTTP`, `Net::HTTPS`, and `Net::FTP`), there are other possible vulnerabilities when accepting arbitrary input, as [detailed][openuri] by Egor Homakov. These security vulnerabilities may be avoided, even with the `Minitar::Output` and `Minitar::Input` convenience classes, by providing IO-like objects instead of pathname-like objects as the source or destination of these classes. [tidelift]: https://tidelift.com/security [email]: mailto:minitar@halostatue.ca [age]: https://github.com/FiloSottile/age [CVE-2017-17405]: https://nvd.nist.gov/vuln/detail/CVE-2017-17405 [openuri]: https://sakurity.com/blog/2015/02/28/openuri.html minitar-1.1.0/lib/0000755000004100000410000000000015061026725013756 5ustar www-datawww-dataminitar-1.1.0/lib/minitar.rb0000644000004100000410000002343115061026725015751 0ustar www-datawww-data# frozen_string_literal: true require "fileutils" require "rbconfig" require "rbconfig/sizeof" # == Synopsis # # Using minitar is easy. The simplest case is: # # require 'zlib' # require 'minitar' # # # Packs everything that matches Find.find('tests'). test.tar will automatically be # # closed by Minitar.pack. # Minitar.pack('tests', File.open('test.tar', 'wb')) # # # Unpacks 'test.tar' to 'x', creating 'x' if necessary. # Minitar.unpack('test.tar', 'x') # # A gzipped tar can be written with: # # # test.tgz will be closed automatically. # Minitar.pack('tests', Zlib::GzipWriter.new(File.open('test.tgz', 'wb')) # # # test.tgz will be closed automatically. # Minitar.unpack(Zlib::GzipReader.new(File.open('test.tgz', 'rb')), 'x') # # As the case above shows, one need not write to a file. However, it will sometimes # require that one dive a little deeper into the API, as in the case of StringIO objects. # Note that I'm not providing a block with Minitar::Output, as Minitar::Output#close # automatically closes both the Output object and the wrapped data stream object. # # begin # sgz = Zlib::GzipWriter.new(StringIO.new("")) # tar = Minitar::Output.new(sgz) # Find.find('tests') do |entry| # Minitar.pack_file(entry, tar) # end # ensure # # Closes both tar and sgz. # tar.close # end class Minitar # The base class for any minitar error. Error = Class.new(::StandardError) # Raised when a wrapped data stream class is not seekable. NonSeekableStream = Class.new(Error) # The exception raised when operations are performed on a stream that has previously # been closed. ClosedStream = Class.new(Error) # The exception raised when a filename exceeds 256 bytes in length, the maximum # supported by the standard Tar format. FileNameTooLong = Class.new(Error) # The exception raised when a data stream ends before the amount of data expected in the # archive's PosixHeader. UnexpectedEOF = Class.new(StandardError) # The exception raised when a file contains a relative path in secure mode (the default # for this version). SecureRelativePathError = Class.new(Error) # The exception raised when a file contains an invalid Posix header. InvalidTarStream = Class.new(Error) end class << Minitar # Tests if +path+ refers to a directory. Fixes an apparently corrupted stat() # call on Windows. def dir?(path) path = "#{path}/" unless path.to_s.end_with?("/") File.directory?(path) end # A convenience method for wrapping Minitar::Input.open (mode +r+ or +rb) and # Minitar::Output.open (mode +w+ or +wb+). No other modes are currently supported. def open(dest, mode = "r", &) case mode when "r", "rb" Minitar::Input.open(dest, &) when "w", "wb" Minitar::Output.open(dest, &) else raise ArgumentError, "Unknown open mode for Minitar.open." end end def windows? = # :nodoc: RUBY_PLATFORM =~ /^(?:cyginw|mingw|mswin)/i || RbConfig::CONFIG["host_os"].to_s =~ /^(cygwin|mingw|mswin|windows)/i || File::ALT_SEPARATOR == "\\" # A convenience method to pack the provided +data+ as a file named +entry+. +entry+ may # either be a name or a Hash with the fields described below. When only a name is # provided, or only some Hash fields are provided, the default values will apply. # # :name:: The filename to be packed into the archive. Required. # :mode:: The mode to be applied. Defaults to 0o644 for files and 0o755 for # directories. # :uid:: The user owner of the file. Default is +nil+. # :gid:: The group owner of the file. Default is +nil+. # :mtime:: The modification Time of the file. Default is +Time.now+. # # If +data+ is +nil+, a directory will be created. Use an empty String for a normal # empty file. def pack_as_file(entry, data, outputter) # :yields: action, name, stats outputter = outputter.tar if outputter.is_a?(Minitar::Output) stats = { gid: nil, uid: nil, mtime: Time.now, size: data&.size || 0, mode: data ? 0o644 : 0o755 } if entry.is_a?(Hash) name = entry[:name] entry.each_pair { next if _1 == :name stats[_1] = _2 unless _2.nil? } else name = entry end if data.nil? # Create a directory yield :dir, name, stats if block_given? outputter.mkdir(name, stats) else outputter.add_file_simple(name, stats) do |os| stats[:current] = 0 yield :file_start, name, stats if block_given? StringIO.open(data, "rb") do |ff| until ff.eof? stats[:currinc] = os.write(ff.read(4096)) stats[:current] += stats[:currinc] yield :file_progress, name, stats if block_given? end end yield :file_done, name, stats if block_given? end end end # A convenience method to pack the file provided. +entry+ may either be a filename (in # which case various values for the file (see below) will be obtained from # File#stat(entry) or a Hash with the fields: # # :name:: The filename to be packed into the archive. Required. # :mode:: The mode to be applied. # :uid:: The user owner of the file. (Ignored on Windows.) # :gid:: The group owner of the file. (Ignored on Windows.) # :mtime:: The modification Time of the file. # # During packing, if a block is provided, #pack_file yields an +action+ Symbol, the full # name of the file being packed, and a Hash of statistical information, just as with # Minitar::Input#extract_entry. # # The +action+ will be one of: # :dir:: The +entry+ is a directory. # :file_start:: The +entry+ is a file; the extract of the file is just # beginning. # :file_progress:: Yielded every 4096 bytes during the extract of the +entry+. # :file_done:: Yielded when the +entry+ is completed. # # The +stats+ hash contains the following keys: # :current:: The current total number of bytes read in the +entry+. # :currinc:: The current number of bytes read in this read cycle. # :name:: The filename to be packed into the tarchive. *REQUIRED*. # :mode:: The mode to be applied. # :uid:: The user owner of the file. (+nil+ on Windows.) # :gid:: The group owner of the file. (+nil+ on Windows.) # :mtime:: The modification Time of the file. def pack_file(entry, outputter) # :yields: action, name, stats outputter = outputter.tar if outputter.is_a?(Minitar::Output) stats = {} if entry.is_a?(Hash) name = entry[:name] entry.each { |kk, vv| stats[kk] = vv unless vv.nil? } else name = entry end name = name.sub(%r{\./}, "") stat = File.stat(name) stats[:mode] ||= stat.mode stats[:mtime] ||= stat.mtime stats[:size] = stat.size if windows? stats[:uid] = nil stats[:gid] = nil else stats[:uid] ||= stat.uid stats[:gid] ||= stat.gid end if File.file?(name) outputter.add_file_simple(name, stats) do |os| stats[:current] = 0 yield :file_start, name, stats if block_given? File.open(name, "rb") do |ff| until ff.eof? stats[:currinc] = os.write(ff.read(4096)) stats[:current] += stats[:currinc] yield :file_progress, name, stats if block_given? end end yield :file_done, name, stats if block_given? end elsif dir?(name) yield :dir, name, stats if block_given? outputter.mkdir(name, stats) else raise "Don't yet know how to pack this type of file." end end # A convenience method to pack files specified by +src+ into +dest+. If +src+ is an # Array, then each file detailed therein will be packed into the resulting # Minitar::Output stream; if +recurse_dirs+ is true, then directories will be recursed. # # If +src+ is not an Array, it will be treated as the result of Find.find; all files # matching will be packed. def pack(src, dest, recurse_dirs = true, &block) require "find" Minitar::Output.open(dest) do |outp| if src.is_a?(Array) src.each do |entry| if dir?(entry) && recurse_dirs Find.find(entry) do |ee| pack_file(ee, outp, &block) end else pack_file(entry, outp, &block) end end else Find.find(src) do |entry| pack_file(entry, outp, &block) end end end end # A convenience method to unpack files from +src+ into the directory specified by # +dest+. Only those files named explicitly in +files+ will be extracted. def unpack(src, dest, files = [], options = {}, &block) Minitar::Input.open(src) do |inp| if File.exist?(dest) && !dir?(dest) raise "Can't unpack to a non-directory." end FileUtils.mkdir_p(dest) unless File.exist?(dest) inp.each do |entry| if files.empty? || files.include?(entry.full_name) inp.extract_entry(dest, entry, options, &block) end end end end # Check whether +io+ can seek without errors. def seekable?(io, methods = nil) # The IO class throws an exception at runtime if we try to change position on # a non-regular file. if io.respond_to?(:stat) io.stat.file? else # Duck-type the rest of this. methods ||= [:pos, :pos=, :seek, :rewind] methods = [methods] unless methods.is_a?(Array) methods.all? { |m| io.respond_to?(m) } end end end require "minitar/posix_header" require "minitar/pax_header" require "minitar/input" require "minitar/output" require "minitar/version" minitar-1.1.0/lib/minitar/0000755000004100000410000000000015061026725015421 5ustar www-datawww-dataminitar-1.1.0/lib/minitar/output.rb0000644000004100000410000000577715061026725017326 0ustar www-datawww-data# frozen_string_literal: true require "minitar/writer" class Minitar # Wraps a Minitar::Writer with convenience methods and wrapped stream management. If the # stream provided to Output does not support random access, only Writer#add_file_simple # and Writer#mkdir are guaranteed to work. # # === Security Notice # # Constructing a Minitar::Output will use Kernel.open if the provided output is not # a readable stream object. Using an untrusted value for output may allow a malicious # user to execute arbitrary system commands. It is the caller's responsibility to ensure # that the output value is safe. # # * {CWE-073}[https://cwe.mitre.org/data/definitions/73.html] # * {CWE-078}[https://cwe.mitre.org/data/definitions/78.html] # * {CWE-088}[https://cwe.mitre.org/data/definitions/88.html] # # This notice applies to Minitar::Output.open, Minitar::Output.tar, and # Minitar::Output.new. class Output # With no associated block, +Output.open+ is a synonym for +Output.new+. # # If a block is given, the new Output will be yielded to the block as an argument and # the Output object will automatically be closed when the block terminates (this also # closes the wrapped stream object). The return value will be the value of the block. # # :call-seq: # Minitar::Output.open(io) -> output # Minitar::Output.open(io) { |output| block } -> obj def self.open(output) stream = new(output) return stream unless block_given? # This exception context must remain, otherwise the stream closes on open # even if a block is not given. begin yield stream ensure stream.close end end # Output.tar is a wrapper for Output.open that yields the owned tar object instead of # the Output object. If a block is not provided, an enumerator will be created with # the same behaviour. # # :call-seq: # Minitar::Output.tar(io) -> enumerator # Minitar::Output.tar(io) { |tar| block } -> obj def self.tar(output) return to_enum(__method__, output) unless block_given? Output.open(output) do |stream| yield stream.tar end end # Creates a new Output object. If +output+ is a stream object that responds to #write, # then it will simply be wrapped. Otherwise, one will be created and opened using # Kernel#open. When Output#close is called, the stream object wrapped will be closed. # # :call-seq: # Minitar::Output.new(io) -> output # Minitar::Output.new(path) -> output def initialize(output) @io = if output.respond_to?(:write) output else ::Kernel.open(output, "wb") end @tar = Minitar::Writer.new(@io) end # Returns the Writer object for direct access. attr_reader :tar # Returns false if the wrapped data stream is open. def closed? = @io.closed? # Closes the Writer object and the wrapped data stream. def close @tar.close @io.close end end end minitar-1.1.0/lib/minitar/input.rb0000644000004100000410000001732315061026725017113 0ustar www-datawww-data# frozen_string_literal: true require "minitar/reader" class Minitar # Wraps a Minitar::Reader with convenience methods and wrapped stream management; Input # only works with data streams that can be rewound. # # === Security Notice # # Constructing a Minitar::Input will use Kernel.open if the provided input is not # a readable stream object. Using an untrusted value for input may allow a malicious # user to execute arbitrary system commands. It is the caller's responsibility to ensure # that the input value is safe. # # * {CWE-073}[https://cwe.mitre.org/data/definitions/73.html] # * {CWE-078}[https://cwe.mitre.org/data/definitions/78.html] # * {CWE-088}[https://cwe.mitre.org/data/definitions/88.html] # # This notice applies to Minitar::Input.open, Minitar::Input.each_entry, and # Minitar::Input.new. class Input include Enumerable # With no associated block, +Input.open+ is a synonym for +Input.new+. # # If a block is given, the new Input will be yielded to the block as an argument and # the Input object will automatically be closed when the block terminates (this also # closes the wrapped stream object). The return value will be the value of the block. # # :call-seq: # Minitar::Input.open(io) -> input # Minitar::Input.open(io) { |input| block } -> obj def self.open(input) stream = new(input) if block_given? # This exception context must remain, otherwise the stream closes on open even if # a block is not given. begin yield stream ensure stream.close end else stream end end # Iterates over each entry in the provided input. This wraps the common pattern of: # # Minitar::Input.open(io) do |i| # inp.each do |entry| # # ... # end # end # # If a block is not provided, an enumerator will be created with the same behaviour. # # :call-seq: # Minitar::Input.each_entry(io) -> enumerator # Minitar::Input.each_entry(io) { |entry| block } -> obj def self.each_entry(input) return to_enum(__method__, input) unless block_given? Input.open(input) do |stream| stream.each do |entry| yield entry end end end # Creates a new Input object. If +input+ is a stream object that responds to #read, # then it will simply be wrapped. Otherwise, one will be created and opened using # Kernel#open. When Input#close is called, the stream object wrapped will be closed. # # An exception will be raised if the stream that is wrapped does not support # rewinding. # # :call-seq: # Minitar::Input.new(io) -> input # Minitar::Input.new(path) -> input def initialize(input) @io = if input.respond_to?(:read) input else ::Kernel.open(input, "rb") end raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :rewind) @tar = Reader.new(@io) end # When provided a block, iterates through each entry in the archive. When finished, # rewinds to the beginning of the stream. # # If not provided a block, creates an enumerator with the same semantics. def each_entry return to_enum unless block_given? @tar.each do |entry| yield entry end ensure @tar.rewind end alias_method :each, :each_entry # Extracts the current +entry+ to +destdir+. If a block is provided, it yields an # +action+ Symbol, the full name of the file being extracted (+name+), and a Hash of # statistical information (+stats+). # # The +action+ will be one of: # # :dir:: The +entry+ is a directory. # :file_start:: The +entry+ is a file; the extract of the file is just # beginning. # :file_progress:: Yielded every 4096 bytes during the extract of the # +entry+. # :file_done:: Yielded when the +entry+ is completed. # # The +stats+ hash contains the following keys: # # :current:: The current total number of bytes read in the +entry+. # :currinc:: The current number of bytes read in this read cycle. # :entry:: The entry being extracted; this is a Reader::EntryStream, with # all methods thereof. def extract_entry(destdir, entry, options = {}, &) # :yields: action, name, stats stats = { current: 0, currinc: 0, entry: entry } # extract_entry is not vulnerable to prefix '/' vulnerabilities, but it is # vulnerable to relative path directories. This code will break this vulnerability. # For this version, we are breaking relative paths HARD by throwing an exception. # # Future versions may permit relative paths as long as the file does not leave # +destdir+. # # However, squeeze consecutive '/' characters together. full_name = entry.full_name.squeeze("/") if /\.{2}(?:\/|\z)/.match?(full_name) raise SecureRelativePathError, "Path contains '..'" end if entry.directory? extract_directory(destdir, full_name, entry, stats, options, &) else # it's a file extract_file(destdir, full_name, entry, stats, options, &) end end # Returns false if the wrapped data stream is open. def closed? = @io.closed? # Returns the Reader object for direct access. attr_reader :tar # Closes both the Reader object and the wrapped data stream. def close @io.close @tar.close end private def fsync_dir(dirname) # make sure this hits the disc dir = IO.open(dirname, "rb") dir.fsync rescue # ignore IOError if it's an unpatched (old) Ruby nil ensure dir&.close rescue nil # standard:disable Style/RescueModifier end def extract_directory(destdir, full_name, entry, stats, options) dest = File.join(destdir, full_name) yield :dir, full_name, stats if block_given? if Minitar.dir?(dest) begin FileUtils.chmod(entry.mode, dest) rescue nil end else File.unlink(dest.chomp("/")) if File.symlink?(dest.chomp("/")) FileUtils.mkdir_p(dest, mode: entry.mode) FileUtils.chmod(entry.mode, dest) end if options.fetch(:fsync, true) fsync_dir(dest) fsync_dir(File.join(dest, "..")) end end def extract_file(destdir, full_name, entry, stats, options) destdir = File.join(destdir, File.dirname(full_name)) FileUtils.mkdir_p(destdir, mode: 0o755) destfile = File.join(destdir, File.basename(full_name)) File.unlink(destfile) if File.symlink?(destfile) # Errno::ENOENT begin FileUtils.chmod(0o600, destfile) rescue nil end yield :file_start, full_name, stats if block_given? File.open(destfile, "wb", entry.mode) do |os| loop do data = entry.read(4096) break unless data stats[:currinc] = os.write(data) stats[:current] += stats[:currinc] yield :file_progress, full_name, stats if block_given? end if options.fetch(:fsync, true) yield :file_fsync, full_name, stats if block_given? os.fsync end end FileUtils.chmod(entry.mode, destfile) if options.fetch(:fsync, true) yield :dir_fsync, full_name, stats if block_given? fsync_dir(File.dirname(destfile)) fsync_dir(File.join(File.dirname(destfile), "..")) end yield :file_done, full_name, stats if block_given? end end end minitar-1.1.0/lib/minitar/reader.rb0000644000004100000410000001710215061026725017211 0ustar www-datawww-data# frozen_string_literal: true class Minitar # The class that reads a tar format archive from a data stream. The data stream may be # sequential or random access, but certain features only work with random access data # streams. class Reader include Enumerable # This marks the EntryStream closed for reading without closing the actual data # stream. module InvalidEntryStream def read(*) = raise ClosedStream # :nodoc: def getc = raise ClosedStream # :nodoc: def rewind = raise ClosedStream # :nodoc: def closed? = true # :nodoc: end # EntryStreams are pseudo-streams on top of the main data stream. class EntryStream Minitar::PosixHeader::FIELDS.each do |field| attr_reader field.to_sym end def initialize(header, io) @io = io @name = header.name @mode = header.mode @uid = header.uid @gid = header.gid @size = header.size @mtime = header.mtime @checksum = header.checksum @typeflag = header.typeflag @linkname = header.linkname @magic = header.magic @version = header.version @uname = header.uname @gname = header.gname @devmajor = header.devmajor @devminor = header.devminor @prefix = header.prefix @read = 0 @orig_pos = if Minitar.seekable?(@io) @io.pos else 0 end end # Reads +len+ bytes (or all remaining data) from the entry. Returns +nil+ if there # is no more data to read. def read(len = nil) return nil if @read >= @size len ||= @size - @read max_read = [len, @size - @read].min ret = @io.read(max_read) @read += ret.bytesize ret end # Reads one byte from the entry. Returns +nil+ if there is no more data to read. def getc return nil if @read >= @size ret = @io.getc @read += 1 if ret ret end # Returns +true+ if the entry represents a directory. def directory? case @typeflag when "5" true when "0", "\0" # If the name ends with a slash, treat it as a directory. This is what other # major tar implementations do for interoperability and compatibility with older # tar variants and some new ones. @name.end_with?("/") else false end end alias_method :directory, :directory? # Returns +true+ if the entry represents a plain file. def file? (@typeflag == "0" || @typeflag == "\0") && !@name.end_with?("/") end alias_method :file, :file? # Returns +true+ if the current read pointer is at the end of the EntryStream data. def eof? = @read >= @size # Returns the current read pointer in the EntryStream. def pos = @read alias_method :bytes_read, :pos # Sets the current read pointer to the beginning of the EntryStream. def rewind unless Minitar.seekable?(@io, :pos=) raise Minitar::NonSeekableStream end @io.pos = @orig_pos @read = 0 end # Returns the full and proper name of the entry. def full_name if @prefix != "" File.join(@prefix, @name) else @name end end # Returns false if the entry stream is valid. def closed? = false # Closes the entry. def close = invalidate private def invalidate extend InvalidEntryStream end end # With no associated block, +Reader::open+ is a synonym for +Reader::new+. If the # optional code block is given, it will be passed the new _writer_ as an argument and # the Reader object will automatically be closed when the block terminates. In this # instance, +Reader::open+ returns the value of the block. def self.open(io) reader = new(io) return reader unless block_given? # This exception context must remain, otherwise the stream closes on open even if # a block is not given. begin yield reader ensure reader.close end end # Iterates over each entry in the provided input. This wraps the common pattern of: # # Minitar::Input.open(io) do |i| # inp.each do |entry| # # ... # end # end # # If a block is not provided, an enumerator will be created with the same behaviour. # # :call-seq: # Minitar::Reader.each_entry(io) -> enumerator # Minitar::Reader.each_entry(io) { |entry| block } -> obj def self.each_entry(io) return to_enum(__method__, io) unless block_given? Input.open(io) do |reader| reader.each_entry do |entry| yield entry end end end # Creates and returns a new Reader object. def initialize(io) @io = io @init_pos = begin io.pos rescue nil end end # Resets the read pointer to the beginning of data stream. Do not call this during # a #each or #each_entry iteration. This only works with random access data streams # that respond to #rewind and #pos. def rewind if @init_pos.zero? raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :rewind) @io.rewind else raise Minitar::NonSeekableStream unless Minitar.seekable?(@io, :pos=) @io.pos = @init_pos end end # Iterates through each entry in the data stream. def each_entry return to_enum unless block_given? loop do return if @io.eof? header = Minitar::PosixHeader.from_stream(@io) raise Minitar::InvalidTarStream unless header.valid? return if header.empty? raise Minitar::InvalidTarStream if header.size < 0 if header.long_name? name_block = (header.size / 512.0).ceil * 512 long_name = @io.read(name_block).rstrip header = PosixHeader.from_stream(@io) return if header.empty? header.long_name = long_name elsif header.pax_header? pax_header = PaxHeader.from_stream(@io, header) header = PosixHeader.from_stream(@io) return if header.empty? header.size = pax_header.size if pax_header.size end entry = EntryStream.new(header, @io) size = entry.size yield entry skip = (512 - (size % 512)) % 512 if Minitar.seekable?(@io, :seek) # avoid reading... try_seek(size - entry.bytes_read) else pending = size - entry.bytes_read while pending > 0 bread = @io.read([pending, 4096].min).bytesize raise UnexpectedEOF if @io.eof? pending -= bread end end @io.read(skip) # discard trailing zeros # make sure nobody can use #read, #getc or #rewind anymore entry.close end end alias_method :each, :each_entry # Returns false if the reader is open (it never closes). def closed? = false def close end private def try_seek(bytes) @io.seek(bytes, IO::SEEK_CUR) rescue RangeError # This happens when skipping the large entry and the skipping entry size exceeds # maximum allowed size (varies by platform and underlying IO object). max = RbConfig::LIMITS.fetch("INT_MAX", 2147483647) skipped = 0 while skipped < bytes to_skip = [bytes - skipped, max].min @io.seek(to_skip, IO::SEEK_CUR) skipped += to_skip end end end end minitar-1.1.0/lib/minitar/pax_header.rb0000644000004100000410000000712215061026725020050 0ustar www-datawww-data# frozen_string_literal: true class Minitar # Implements the PAX Extended Header as a Ruby class. The header consists of following # format: # # = # # Where: # # - +decimal-length+ is the total number of bytes for the PaxHeader record using ASCII # decimal values; this includes the terminal newline (0x10). # - +space+ is a single literal ASCII space (0x20). # - +ascii-keyword+ is a PAX Extended Header keyword, which may be any ASCII character # except newline (0x10) or equal sign (0x3D). # - +=+ is the literal ASCII equal sign (0x3D). # - +value+ is any series of bytes except newline (0x10). # - +newline+ is the literal ASCII newline (0x10). # # There are several keywords defined in the POSIX standard; some of them are supported # in this class, but may not be supported by Minitar as a whole. # # Primary support for PAX extended headers is for extracting size information for large # file support. Other features may be added in the future. class PaxHeader BLOCK_SIZE = 512 attr_reader :attributes class << self # Creates a new PaxHeader from a data stream and posix header. Reads the PAX content # based on the size specified in the posix header. def from_stream(stream, posix_header) raise ArgumentError, "Header must be a PAX header" unless posix_header.pax_header? pax_block = (posix_header.size / BLOCK_SIZE.to_f).ceil * BLOCK_SIZE pax_content = stream.read(pax_block) raise Minitar::InvalidTarStream if pax_content.nil? || pax_content.bytesize < posix_header.size actual_content = pax_content[0, posix_header.size] from_data(actual_content) end # Creates a new PaxHeader from PAX content data. def from_data(content) = new(parse_content(content)) private def parse_content(content) attributes = {} offset = 0 while offset < content.bytesize space_pos = content.index(" ", offset) break unless space_pos length_str = content[offset, space_pos - offset] unless length_str.match?(/\A\d+\z/) raise ArgumentError, "Invalid length format in PAX header: '#{length_str}'" end length = length_str.to_i if offset + length > content.bytesize raise ArgumentError, "Length beyond PAX header: '#{content[offset..]}'" end record = content[offset, length] keyword_value = record[(space_pos - offset + 1)..-2] if keyword_value.include?("=") keyword, value = keyword_value.split("=", 2) attributes[keyword] = value end offset += length end attributes end end # Creates a new PaxHeader from attributes hash. def initialize(attributes = {}) @attributes = attributes.transform_keys(&:to_s) end # The size value from PAX attributes def size = @attributes["size"]&.to_i # The path value from PAX attributes def path = @attributes["path"] # The mtime value from PAX attributes def mtime = @attributes["mtime"]&.to_f # Returns a string representation of the PAX header content. def to_s @attributes.map do |keyword, value| keyword_value = " #{keyword}=#{value}\n" record = keyword_value begin length = record.bytesize length_str = length.to_s record = "#{length_str}#{keyword_value}" end while record.size != length # standard:disable Lint/Loop record end.join end end end minitar-1.1.0/lib/minitar/writer.rb0000644000004100000410000002224615061026725017270 0ustar www-datawww-data# frozen_string_literal: true class Minitar # The class that writes a tar format archive to a data stream. class Writer # The exception raised when the user attempts to write more data to # a BoundedWriteStream than has been allocated. WriteBoundaryOverflow = Class.new(StandardError) # A stream wrapper that can only be written to. Any attempt to read from this # restricted stream will result in a NameError being thrown. class WriteOnlyStream def initialize(io) @io = io end def write(data) = @io.write(data) end private_constant :WriteOnlyStream # A WriteOnlyStream that also has a size limit. class BoundedWriteStream < WriteOnlyStream # The maximum number of bytes that may be written to this data stream. attr_reader :limit # The current total number of bytes written to this data stream. attr_reader :written def initialize(io, limit) @io = io @limit = limit @written = 0 end def write(data) size = data.bytesize raise WriteBoundaryOverflow if (size + @written) > @limit @io.write(data) @written += size size end end private_constant :BoundedWriteStream # With no associated block, +Writer::open+ is a synonym for +Writer::new+. If the # optional code block is given, it will be passed the new _writer_ as an argument and # the Writer object will automatically be closed when the block terminates. In this # instance, +Writer::open+ returns the value of the block. # # :call-seq: # w = Minitar::Writer.open(STDOUT) # w.add_file_simple('foo.txt', :size => 3) # w.close # # Minitar::Writer.open(STDOUT) do |w| # w.add_file_simple('foo.txt', :size => 3) # end def self.open(io) # :yields: Writer writer = new(io) return writer unless block_given? # This exception context must remain, otherwise the stream closes on open even if # a block is not given. begin yield writer ensure writer.close end end # Creates and returns a new Writer object. def initialize(io) @io = io @closed = false end # Adds a file to the archive as +name+. The data can be provided in the # opts[:data] or provided to a BoundedWriteStream that is yielded to the # provided block. # # If opts[:data] is provided, all other values to +opts+ are optional. If the # data is provided to the yielded BoundedWriteStream, opts[:size] must be # provided. # # Valid parameters to +opts+ are: # # :data:: Optional. The data to write to the archive. # :mode:: The Unix file permissions mode value. If not provided, defaults to # 0o644. # :size:: The size, in bytes. If :data is provided, this parameter # may be ignored (if it is less than the size of the data provided) # or used to add padding (if it is greater than the size of the data # provided). # :uid:: The Unix file owner user ID number. # :gid:: The Unix file owner group ID number. # :mtime:: File modification time, interpreted as an integer. # # An exception will be raised if the Writer is already closed, or if more data is # written to the BoundedWriteStream than expected. # # :call-seq: # writer.add_file_simple('foo.txt', :data => "bar") # writer.add_file_simple('foo.txt', :size => 3) do |w| # w.write("bar") # end def add_file_simple(name, opts = {}) # :yields: BoundedWriteStream raise ClosedStream if @closed header = { mode: opts.fetch(:mode, 0o644), mtime: opts.fetch(:mtime, nil), gid: opts.fetch(:gid, nil), uid: opts.fetch(:uid, nil) } data = opts.fetch(:data, nil) size = opts.fetch(:size, nil) if block_given? raise ArgumentError, "Too much data (opts[:data] and block_given?)." if data raise ArgumentError, "No size provided" unless size else raise ArgumentError, "No data provided" unless data bytes = data.bytesize size = bytes if size.nil? || size < bytes end header[:size] = size short_name, prefix, needs_long_name = split_name(name) write_header(header, name, short_name, prefix, needs_long_name) os = BoundedWriteStream.new(@io, size) if block_given? yield os else os.write(data) end min_padding = size - os.written @io.write("\0" * min_padding) remainder = (512 - (size % 512)) % 512 @io.write("\0" * remainder) end # Adds a file to the archive as +name+. The data can be provided in the # opts[:data] or provided to a yielded +WriteOnlyStream+. The size of the # file will be determined from the amount of data written to the stream. # # Valid parameters to +opts+ are: # # :mode:: The Unix file permissions mode value. If not provided, defaults to # 0o644. # :uid:: The Unix file owner user ID number. # :gid:: The Unix file owner group ID number. # :mtime:: File modification time, interpreted as an integer. # :data:: Optional. The data to write to the archive. # # If opts[:data] is provided, this acts the same as #add_file_simple. # Otherwise, the file's size will be determined from the amount of data written to the # stream. # # For #add_file to be used without opts[:data], the Writer must be wrapping # a stream object that is seekable. Otherwise, #add_file_simple must be used. # # +opts+ may be modified during the writing of the file to the stream. def add_file(name, opts = {}, &) # :yields: WriteOnlyStream, +opts+ raise ClosedStream if @closed return add_file_simple(name, opts, &) if opts[:data] raise Minitar::NonSeekableStream unless Minitar.seekable?(@io) short_name, prefix, needs_long_name = split_name(name) data_offset = needs_long_name ? 3 * 512 : 512 init_pos = @io.pos @io.write("\0" * data_offset) # placeholder for the header yield WriteOnlyStream.new(@io), opts size = @io.pos - (init_pos + data_offset) remainder = (512 - (size % 512)) % 512 @io.write("\0" * remainder) final_pos, @io.pos = @io.pos, init_pos header = { mode: opts[:mode], mtime: opts[:mtime], size: size, gid: opts[:gid], uid: opts[:uid] } write_header(header, name, short_name, prefix, needs_long_name) @io.pos = final_pos end # Creates a directory entry in the tar. def mkdir(name, opts = {}) raise ClosedStream if @closed header = { mode: opts[:mode], typeflag: "5", size: 0, gid: opts[:gid], uid: opts[:uid], mtime: opts[:mtime] } short_name, prefix, needs_long_name = split_name(name) write_header(header, name, short_name, prefix, needs_long_name) nil end # Creates a symbolic link entry in the tar. def symlink(name, link_target, opts = {}) raise ClosedStream if @closed raise FileNameTooLong if link_target.size > 100 name, prefix = split_name(name) header = { name: name, mode: opts[:mode], typeflag: "2", size: 0, linkname: link_target, gid: opts[:gid], uid: opts[:uid], mtime: opts[:mtime], prefix: prefix } @io.write(PosixHeader.new(header).to_s) nil end # Passes the #flush method to the wrapped stream, used for buffered streams. def flush raise ClosedStream if @closed @io.flush if @io.respond_to?(:flush) end # Returns false if the writer is open. def closed? = @closed # Closes the Writer. This does not close the underlying wrapped output stream. def close return if @closed @io.write("\0" * 1024) @closed = true end private def write_header(header, long_name, short_name, prefix, needs_long_name) if needs_long_name long_name_header = { prefix: "", name: PosixHeader::GNU_EXT_LONG_LINK, typeflag: "L", size: long_name.length + 1, mode: 0 } @io.write(PosixHeader.new(long_name_header).to_s) @io.write(long_name) @io.write("\0" * (512 - (long_name.length % 512))) end new_header = header.merge({name: short_name, prefix: prefix}) @io.write(PosixHeader.new(new_header).to_s) end def split_name(name) if name.bytesize <= 100 prefix = "" else parts = name.split("/") newname = parts.pop nxt = "" loop do nxt = parts.pop || "" break if newname.bytesize + 1 + nxt.bytesize >= 100 newname = "#{nxt}/#{newname}" end prefix = (parts + [nxt]).join("/") name = newname end [name, prefix, name.bytesize > 100 || prefix.bytesize > 155] end end end minitar-1.1.0/lib/minitar/version.rb0000644000004100000410000000010515061026725017427 0ustar www-datawww-data# frozen_string_literal: true class Minitar VERSION = "1.1.0" end minitar-1.1.0/lib/minitar/posix_header.rb0000644000004100000410000002232615061026725020425 0ustar www-datawww-data# frozen_string_literal: true class Minitar # Implements the POSIX tar header as a Ruby class. The structure of # the POSIX tar header is: # # struct tarfile_entry_posix # { // pack unpack # char name[100]; // ASCII (+ Z unless filled) a100 Z100 # char mode[8]; // 0 padded, octal, null a8 A8 # char uid[8]; // 0 padded, octal, null a8 A8 # char gid[8]; // 0 padded, octal, null a8 A8 # char size[12]; // 0 padded, octal, null a12 A12 # char mtime[12]; // 0 padded, octal, null a12 A12 # char checksum[8]; // 0 padded, octal, null, space a8 A8 # char typeflag[1]; // see below a a # char linkname[100]; // ASCII + (Z unless filled) a100 Z100 # char magic[6]; // "ustar\0" a6 A6 # char version[2]; // "00" a2 A2 # char uname[32]; // ASCIIZ a32 Z32 # char gname[32]; // ASCIIZ a32 Z32 # char devmajor[8]; // 0 padded, octal, null a8 A8 # char devminor[8]; // 0 padded, octal, null a8 A8 # char prefix[155]; // ASCII (+ Z unless filled) a155 Z155 # }; # # The #typeflag is one of several known values. POSIX indicates that "A POSIX-compliant # implementation must treat any unrecognized typeflag value as a regular file." class PosixHeader BLOCK_SIZE = 512 MAGIC_BYTES = "ustar" GNU_EXT_LONG_LINK = "././@LongLink" # Fields that must be set in a POSIX tar(1) header. REQUIRED_FIELDS = [:name, :size, :prefix, :mode].freeze # Fields that may be set in a POSIX tar(1) header. OPTIONAL_FIELDS = [ :uid, :gid, :mtime, :checksum, :typeflag, :linkname, :magic, :version, :uname, :gname, :devmajor, :devminor ].freeze # All fields available in a POSIX tar(1) header. FIELDS = (REQUIRED_FIELDS + OPTIONAL_FIELDS).freeze FIELDS.each do |f| attr_reader f.to_sym end ## def name=(value) valid_name!(value) @name = value end attr_writer :size # The pack format passed to Array#pack for encoding a header. HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155" # The unpack format passed to String#unpack for decoding a header. HEADER_UNPACK_FORMAT = "Z100A8A8A8a12A12A8aZ100A6A2Z32Z32A8A8Z155" class << self # Creates a new PosixHeader from a data stream. def from_stream(stream) = from_data(stream.read(BLOCK_SIZE)) # Creates a new PosixHeader from a BLOCK_SIZE-byte data buffer. def from_data(data) fields = data.unpack(HEADER_UNPACK_FORMAT) name = fields.shift mode = fields.shift.oct uid = fields.shift.oct gid = fields.shift.oct size = parse_numeric_field(fields.shift) mtime = fields.shift.oct checksum = fields.shift.oct typeflag = fields.shift linkname = fields.shift magic = fields.shift version = fields.shift.oct uname = fields.shift gname = fields.shift devmajor = fields.shift.oct devminor = fields.shift.oct prefix = fields.shift empty = !data.each_byte.any?(&:nonzero?) new( name: name, mode: mode, uid: uid, gid: gid, size: size, mtime: mtime, checksum: checksum, typeflag: typeflag, magic: magic, version: version, uname: uname, gname: gname, devmajor: devmajor, devminor: devminor, prefix: prefix, empty: empty, linkname: linkname ) end private def parse_numeric_field(string) return string.oct if /\A[0-7 \0]*\z/.match?(string) # \0 appears as a padding return parse_base256(string) if string.bytes.first == 0x80 || string.bytes.first == 0xff raise ArgumentError, "#{string.inspect} is not a valid numeric field" end def parse_base256(string) # https://www.gnu.org/software/tar/manual/html_node/Extensions.html bytes = string.bytes case bytes.first when 0x80 # Positive number: *non-leading* bytes, number in big-endian order bytes[1..].inject(0) { |r, byte| (r << 8) | byte } when 0xff # Negative number: *all* bytes, two's complement in big-endian order result = bytes.inject(0) { |r, byte| (r << 8) | byte } bit_length = bytes.size * 8 result - (1 << bit_length) else raise ArgumentError, "Invalid binary field format" end end end # Creates a new PosixHeader. A PosixHeader cannot be created unless +name+, +size+, # +prefix+, and +mode+ are provided. def initialize(v) REQUIRED_FIELDS.each do |f| raise ArgumentError, "Field #{f} is required." unless v.key?(f) end v[:mtime] = v[:mtime].to_i v[:checksum] ||= "" v[:typeflag] ||= "0" v[:magic] ||= MAGIC_BYTES v[:version] ||= "00" FIELDS.each do |f| instance_variable_set(:"@#{f}", v[f]) end @empty = v[:empty] valid_name!(v[:name]) unless v[:empty] end # Indicates if the header was an empty header. def empty? = @empty # Indicates if the header has a valid magic value. def valid? = empty? || @magic == MAGIC_BYTES # Returns +true+ if the header is a long name special header which indicates # that the next block of data is the filename. def long_name? = typeflag == "L" && name == GNU_EXT_LONG_LINK # Returns +true+ if the header is a PAX extended header which contains # metadata for the next file entry. def pax_header? = typeflag == "x" # Sets the +name+ to the +value+ provided and clears +prefix+. # # Used by Minitar::Reader#each_entry to set the long name when processing GNU long # filename extensions. # # The +value+ must be the complete name, including leading directory components. def long_name=(value) valid_name!(value) @prefix = "" @name = value end # A string representation of the header. def to_s update_checksum header(@checksum) end alias_method :to_str, :to_s # TODO: In Minitar 2, PosixHeader#to_str will be removed. # Update the checksum field. def update_checksum hh = header(" " * 8) @checksum = oct(calculate_checksum(hh), 6) end private def valid_name!(value) return if value.is_a?(String) && !value.empty? raise ArgumentError, "Field name must be a non-empty string" end def oct(num, len) if num.nil? "\0" * (len + 1) else "%0#{len}o" % num end end def calculate_checksum(hdr) hdr.unpack("C*").inject(:+) end def header(chksum) arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11), oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version, uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix] str = arr.pack(HEADER_PACK_FORMAT) str + "\0" * ((BLOCK_SIZE - str.bytesize) % BLOCK_SIZE) end ## # :attr_accessor: name # The name of the file. Required. # # By default, limited to 100 bytes, but may be up to BLOCK_SIZE bytes if using the # GNU long name tar extension. ## # :attr_accessor: size # The size of the file. Required. ## # :attr_reader: prefix # The prefix of the file; the path before #name. Limited to 155 bytes. # Required. ## # :attr_reader: mode # The Unix file mode of the file. Stored as an octal integer. Required. ## # :attr_reader: uid # The Unix owner user ID of the file. Stored as an octal integer. ## # :attr_reader: uname # The user name of the Unix owner of the file. ## # :attr_reader: gid # The Unix owner group ID of the file. Stored as an octal integer. ## # :attr_reader: gname # The group name of the Unix owner of the file. ## # :attr_reader: mtime # The modification time of the file in epoch seconds. Stored as an # octal integer. ## # :attr_reader: checksum # The checksum of the file. Stored as an octal integer. Calculated # before encoding the header as a string. ## # :attr_reader: typeflag # The type of record in the file. # # +0+:: Regular file. NULL should be treated as a synonym, for compatibility # purposes. # +1+:: Hard link. # +2+:: Symbolic link. # +3+:: Character device node. # +4+:: Block device node. # +5+:: Directory. # +6+:: FIFO node. # +7+:: Reserved. # +L+:: GNU extension for long filenames when #name is ././@LongLink. ## # :attr_reader: linkname # The target of the symbolic link. ## # :attr_reader: magic # Always "ustar\0". ## # :attr_reader: version # Always "00" ## # :attr_reader: devmajor # The major device ID. Not currently used. ## # :attr_reader: devminor # The minor device ID. Not currently used. end end minitar-1.1.0/test/0000755000004100000410000000000015061026725014167 5ustar www-datawww-dataminitar-1.1.0/test/test_pax_support.rb0000644000004100000410000000453115061026725020142 0ustar www-datawww-datarequire "minitest_helper" class TestPaxSupport < Minitest::Test def test_pax_header_size_extraction_in_reader pax_content = "16 size=1048576\n28 mtime=1749098832.3200000\n" tar_data = create_pax_with_file_headers(pax_content, "./PaxHeaders.X/large_file.mov", "large_file.mov", 1048576, 0) entries = read_tar_entries(tar_data) assert_equal 1, entries.size entry = entries.first assert_equal "large_file.mov", entry.name assert_equal 1048576, entry.size # Size from PAX header end def test_pax_header_without_size_uses_header_size pax_content = "28 mtime=1749098832.3200000\n" tar_data = create_pax_with_file_headers(pax_content, "./PaxHeaders.X/normal_file.txt", "normal_file.txt", 12345, 12345) entries = read_tar_entries(tar_data) assert_equal 1, entries.size entry = entries.first assert_equal "normal_file.txt", entry.name assert_equal 12345, entry.size # Original header size preserved end def test_pax_header_takes_precedence_over_posix_header_size pax_content = "16 size=1048576\n28 mtime=1749098832.3200000\n" tar_data = create_pax_with_file_headers(pax_content, "./PaxHeaders.X/precedence_file.txt", "precedence_file.txt", 12345, 12345) entries = read_tar_entries(tar_data) assert_equal 1, entries.size entry = entries.first assert_equal "precedence_file.txt", entry.name assert_equal 1048576, entry.size # PAX size takes precedence over POSIX size (12345) end def test_pax_size_extraction_logic pax_header_with_size = Minitar::PaxHeader.new(size: "1048576", mtime: "1749098832.3200000") assert_equal 1048576, pax_header_with_size.size pax_header_without_size = Minitar::PaxHeader.new(mtime: "1749098832.3200000") assert_nil pax_header_without_size.size end private def read_tar_entries(tar_data) io = StringIO.new(tar_data) Minitar::Reader.open(io, &:to_a) end def create_pax_with_file_headers(pax_content, pax_name, file_name, file_size, posix_header_file_size) file_content = "x" * file_size padded_file_content = file_content.ljust((file_size / 512.0).ceil * 512, "\0") [ build_tar_pax_header(pax_name, "", pax_content.bytesize), pax_content.ljust((pax_content.bytesize / 512.0).ceil * 512, "\0"), build_tar_file_header(file_name, "", 0o644, file_size), padded_file_content ].join end end minitar-1.1.0/test/test_tar_output.rb0000644000004100000410000000227515061026725017767 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestTarOutput < Minitest::Test NAMES = ["a", "b", "c", "d" * 200] def setup FileUtils.mkdir_p("data__") NAMES.each do |filename| name = File.join("data__", filename) File.open(name, "wb") { |f| f.puts "#{name}: 123456789012345678901234567890" } end @tarfile = "data__/bla2.tar" end def teardown FileUtils.rm_rf("data__") end def test_open_no_block output = Minitar::Output.open(@tarfile) refute output.closed? ensure output.close assert output.closed? end def test_file_looks_good Minitar::Output.open(@tarfile) do |os| NAMES.each do |name| filename = File.join("data__", name) stat = File.stat(filename) opts = {size: stat.size, mode: 0o644} os.tar.add_file_simple(name, opts) do |ss| File.open(filename, "rb") { |ff| ss.write(ff.read(4096)) until ff.eof? } end end end ff = File.open(@tarfile, "rb") Minitar::Reader.open(ff) do |is| names_from_tar = is.map do |entry| entry.name end assert_equal NAMES, names_from_tar end ensure ff&.close end end minitar-1.1.0/test/minitest_helper.rb0000644000004100000410000000062315061026725017710 0ustar www-datawww-data# frozen_string_literal: true require "fileutils" require "minitar" require "pathname" require "stringio" require "zlib" gem "minitest" require "minitest/autorun" require "minitest/focus" if ENV["STRICT"] != "false" $VERBOSE = true Warning[:deprecated] = true require "minitest/error_on_warning" end Dir.glob(File.join(__dir__, "support/**/*.rb")).sort.each do |support| require support end minitar-1.1.0/test/test_filename_boundary_conditions.rb0000644000004100000410000000430315061026725023467 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestFilenameBoundaryConditions < Minitest::Test SCENARIOS = [99, 100, 101, 102, 154, 155, 156].each_with_object({}) { |len, map| name = "a" * len content = "#{len} chars content" map[name] = content define_method :"test_single_file_#{len}_chars" do file_map = {name => content} files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end name = "dir/#{"a" * (len - 4)}" content = "dir/ #{len - 4} chars content: #{len} total chars" map[name] = content define_method :"test_nested_file_total_#{len}_chars" do file_map = {name => content} files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end } posix_scenarios = [155, 156].each_with_object({}) { |len, map| name = "a" * len content = "#{len} chars content" map[name] = content define_method :"test_posix_boundary_#{len}_chars" do file_map = {name => content} files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end } posix_total_scenarios = {155 => 100, 165 => 110}.each_with_object({}) { |(k, v), map| name = "prefix_#{"a" * (k - 7)}/name_#{"a" * (v - 5)}" content = "prefix #{k} name #{v} chars" map[name] = content define_method :"test_posix_total_boundary_#{k + v + 1}_chars" do file_map = {name => content} files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end } name = "very_long_component_name_with_many_characters" .then { _1 * 3 } .then { [_1] } .then { _1 * 8 } .then { _1.join("/") } .then { "#{_1}/final_file_with_long_name.txt" } content = "Content for very long path" define_method :test_long_near_system_limits do file_map = {name => content} files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end SCENARIOS[name] = content SCENARIOS.merge!(posix_scenarios, posix_total_scenarios) def test_full_scenario_in_archive files = roundtrip_tar_string(SCENARIOS) assert_tar_structure_preserved(SCENARIOS, files) end end minitar-1.1.0/test/test_tar_reader.rb0000644000004100000410000001245615061026725017673 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestTarReader < Minitest::Test def test_open_no_block str = build_tar_file_header("lib/foo", "", 0o10644, 10) + "\0" * 512 str += build_tar_file_header("bar", "baz", 0o644, 0) str += build_tar_dir_header("foo", "bar", 0o12345) str += "\0" * 1024 reader = Minitar::Reader.open(StringIO.new(str)) refute reader.closed? ensure reader.close refute reader.closed? # Reader doesn't actually close anything end def test_multiple_entries str = build_tar_file_header("lib/foo", "", 0o10644, 10) + "\0" * 512 str += build_tar_file_header("bar", "baz", 0o644, 0) str += build_tar_dir_header("foo", "bar", 0o12345) str += build_tar_file_header("src/", "", 0o755, 0) # "file" with a trailing slash str += "\0" * 1024 names = %w[lib/foo bar foo src/] prefixes = ["", "baz", "bar", ""] modes = [0o10644, 0o644, 0o12345, 0o755] sizes = [10, 0, 0, 0] isdir = [false, false, true, true] isfile = [true, true, false, false] Minitar::Reader.new(StringIO.new(str)) do |is| i = 0 is.each_entry do |entry| assert_kind_of Minitar::Reader::EntryStream, entry assert_equal names[i], entry.name assert_equal prefixes[i], entry.prefix assert_equal sizes[i], entry.size assert_equal modes[i], entry.mode assert_equal isdir[i], entry.directory? assert_equal isfile[i], entry.file? if prefixes[i] != "" assert_equal File.join(prefixes[i], names[i]), entry.full_name else assert_equal names[i], entry.name end i += 1 end assert_equal names.size, i end end def test_rewind_entry_works content = ("a".."z").to_a.join(" ") str = build_tar_file_header("lib/foo", "", 0o10644, content.bytesize) + content + "\0" * (512 - content.bytesize) str << "\0" * 1024 Minitar::Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| 3.times do entry.rewind assert_equal content, entry.read assert_equal content.bytesize, entry.pos end end end end def test_rewind_works content = ("a".."z").to_a.join(" ") str = build_tar_file_header("lib/foo", "", 0o10644, content.bytesize) + content + "\0" * (512 - content.bytesize) str << "\0" * 1024 Minitar::Reader.new(StringIO.new(str)) do |is| 3.times do is.rewind i = 0 is.each_entry do |entry| assert_equal content, entry.read i += 1 end assert_equal 1, i end end end def test_read_works contents = ("a".."z").inject(+"") { |a, e| a << e * 100 } str = build_tar_file_header("lib/foo", "", 0o10644, contents.bytesize) + contents str += "\0" * (512 - (str.bytesize % 512)) Minitar::Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of Minitar::Reader::EntryStream, entry data = entry.read(3000) # bigger than contents.bytesize assert_equal contents, data assert entry.eof? end end Minitar::Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of Minitar::Reader::EntryStream, entry data = entry.read(100) (entry.size - data.bytesize).times { data << entry.getc.chr } assert_equal contents, data assert_nil entry.read(10) assert entry.eof? end end Minitar::Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of Minitar::Reader::EntryStream, entry data = entry.read assert_equal contents, data assert_nil entry.read(10) assert_nil entry.read assert_nil entry.getc assert entry.eof? end end end def test_eof_works str = build_tar_file_header("bar", "baz", 0o644, 0) Minitar::Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of Minitar::Reader::EntryStream, entry data = entry.read assert_nil data assert_nil entry.read(10) assert_nil entry.read assert_nil entry.getc assert entry.eof? end end str = build_tar_dir_header("foo", "bar", 0o12345) Minitar::Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of Minitar::Reader::EntryStream, entry data = entry.read assert_nil data assert_nil entry.read(10) assert_nil entry.read assert_nil entry.getc assert entry.eof? end end str = build_tar_dir_header("foo", "bar", 0o12345) str += build_tar_file_header("bar", "baz", 0o644, 0) str += build_tar_file_header("bar", "baz", 0o644, 0) Minitar::Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of Minitar::Reader::EntryStream, entry data = entry.read assert_nil data assert_nil entry.read(10) assert_nil entry.read assert_nil entry.getc assert entry.eof? end end end def test_read_invalid_tar_file assert_raises Minitar::InvalidTarStream do Minitar::Reader.open(StringIO.new("testing")) do |r| r.each_entry do |entry| fail "invalid tar file should not read files" end end end end end minitar-1.1.0/test/test_integration_pack_unpack_cycle.rb0000644000004100000410000000227315061026725023620 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestIntegrationPackUnpackCycle < Minitest::Test def test_comprehensive files = MIXED_FILENAME_SCENARIOS.merge( VERY_LONG_FILENAME_SCENARIOS, BOUNDARY_SCENARIOS, { "empty_dir" => nil, "nested/empty" => nil, "long_dir_#{"i" * 120}" => nil, "root_file.txt" => "root content", "level1/file1.txt" => "level1 content", "level1/level2/file2.txt" => "level2 content", "level1/level2/level3/#{"deep" * 30}.txt" => "deep nested with long name", "#{"long_dir" * 20}/file_in_long_dir.txt" => "file in long directory name", "mixed/#{"long_subdir" * 15}/#{"long_file" * 25}.txt" => "long dir and file names" } ) workspace with_files: files do |ws| minitar_pack_in_workspace assert ws.tarball.file?, "Tarball does not exist" assert ws.tarball.size > 0, "Tarball should not be empty" minitar_unpack_in_workspace assert_files_extracted_in_workspace refute_file_path_duplication_in_workspace assert_extracted_files_match_source_files_in_workspace assert_file_modes_match_in_workspace end end end minitar-1.1.0/test/fixtures/0000755000004100000410000000000015061026725016040 5ustar www-datawww-dataminitar-1.1.0/test/fixtures/test_input_non_strict_octal.tgz0000644000004100000410000000066115061026725024413 0ustar www-datawww-dataI9[KI,I+I,Kb000031Qf`33S mbffhb```hljnƠ`@3!bNJ22p*KKc(!9fgN a`~{>W]gw_+1äCo˽I"*"g&o1s񾟬^bե%:F\fK{ZN޹V(L{=S}W^Wjݻu6sM/gvՔ<βUk4[\txgZt'.4ӳOm?9U1½6oϞ ! 2sRil(ߛb02BF&fƣ $#X@@XOk]4 F(`Q0 F(`Q0 F(`Q0 F(`Q0 H,(minitar-1.1.0/test/fixtures/test_minitar.tar.gz0000644000004100000410000000027515061026725021675 0ustar www-datawww-data Dh10 4 v| 7.2W {size: 210, mode: 0o644}, "file3" => {size: 18, mode: 0o755} }.freeze TEST_DATA_CONTENTS = { "data/" => {size: 0, mode: 0o755}, "data/__dir__/" => {size: 0, mode: 0o755}, "data/file1" => {size: 16, mode: 0o644}, "data/file2" => {size: 16, mode: 0o644} }.freeze def setup @reader = open_fixture("tar_input") FileUtils.mkdir_p("data__") end def teardown @reader&.close unless @reader&.closed? FileUtils.rm_rf("data__") end def test_open_no_block input = Minitar::Input.open(@reader) refute input.closed? ensure input.close assert input.closed? end def test_each_works Minitar::Input.open(@reader) do |stream| outer = 0 stream.each.with_index do |entry, i| assert_kind_of Minitar::Reader::EntryStream, entry assert TEST_CONTENTS.key?(entry.name) assert_equal TEST_CONTENTS[entry.name][:size], entry.size, entry.name assert_modes_equal(TEST_CONTENTS[entry.name][:mode], entry.mode, entry.name) assert_equal TIME_2004, entry.mtime, "entry.mtime" if i.zero? data_reader = Zlib::GzipReader.new(StringIO.new(entry.read)) Minitar::Input.open(data_reader) do |is2| inner = 0 is2.each_with_index do |entry2, _j| assert_kind_of Minitar::Reader::EntryStream, entry2 assert TEST_DATA_CONTENTS.key?(entry2.name) assert_equal(TEST_DATA_CONTENTS[entry2.name][:size], entry2.size, entry2.name) assert_modes_equal(TEST_DATA_CONTENTS[entry2.name][:mode], entry2.mode, entry2.name) assert_equal TIME_2004, entry2.mtime, entry2.name inner += 1 end assert_equal 4, inner end end outer += 1 end assert_equal 2, outer end end def test_extract_entry_works Minitar::Input.open(@reader) do |stream| outer_count = 0 stream.each_with_index do |entry, i| stream.extract_entry("data__", entry) name = File.join("data__", entry.name) assert TEST_CONTENTS.key?(entry.name) if entry.directory? assert(File.directory?(name)) else assert(File.file?(name)) assert_equal TEST_CONTENTS[entry.name][:size], File.stat(name).size end assert_modes_equal(TEST_CONTENTS[entry.name][:mode], File.stat(name).mode, entry.name) if i.zero? begin ff = File.open(name, "rb") data_reader = Zlib::GzipReader.new(ff) Minitar::Input.open(data_reader) do |is2| is2.each_with_index do |entry2, _j| is2.extract_entry("data__", entry2) name2 = File.join("data__", entry2.name) assert TEST_DATA_CONTENTS.key?(entry2.name) if entry2.directory? assert(File.directory?(name2)) else assert(File.file?(name2)) assert_equal(TEST_DATA_CONTENTS[entry2.name][:size], File.stat(name2).size) end assert_modes_equal(TEST_DATA_CONTENTS[entry2.name][:mode], File.stat(name2).mode, name2) end end ensure ff.close unless ff.closed? end end outer_count += 1 end assert_equal 2, outer_count end end def test_extract_entry_breaks_symlinks return if Minitar.windows? IO.respond_to?(:write) && IO.write("data__/file4", "") || File.write("data__/file4", "") File.symlink("data__/file4", "data__/file3") File.symlink("data__/file4", "data__/data") Minitar.unpack(@reader, "data__") Minitar.unpack(Zlib::GzipReader.new(File.open("data__/data.tar.gz", "rb")), "data__") refute File.symlink?("data__/file3") refute File.symlink?("data__/data") end def test_extract_entry_fails_with_relative_directory reader = open_fixture("test_input_relative") Minitar::Input.open(reader) do |stream| stream.each do |entry| assert_raises Minitar::SecureRelativePathError do stream.extract_entry("data__", entry) end end end end def test_extract_with_non_strict_octal reader = open_fixture("test_input_non_strict_octal") assert_raises(ArgumentError) do Minitar.unpack(reader, "data__") end end def test_extract_octal_wrapped_by_space reader = open_fixture("test_input_space_octal") header = Minitar::PosixHeader.from_stream(reader) assert_equal 210, header.size reader = open_fixture("test_input_space_octal") Minitar.unpack(reader, "data__", []) end def test_fsync_false outer = 0 Minitar.unpack(@reader, "data__", [], fsync: false) do |_label, _path, _stats| outer += 1 end assert_equal 6, outer end end minitar-1.1.0/test/test_gnu_tar_compatibility.rb0000644000004100000410000000555615061026725022156 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestGnuTarCompatibility < Minitest::Test def setup skip "GNU tar not available" unless has_gnu_tar? end def test_roundtrip_gnu_tar_cf_minitar_unpack # Use mixed filename scenarios from shared test utilities for comprehensive testing files = MIXED_FILENAME_SCENARIOS.dup # Add one very long filename to test GNU extension compatibility files[VERY_LONG_FILENAME_SCENARIOS.keys.first] = VERY_LONG_FILENAME_SCENARIOS.values.first workspace with_files: files do gnu_tar_create_in_workspace minitar_unpack_in_workspace assert_files_extracted_in_workspace refute_file_path_duplication_in_workspace end end def test_roundtrip_minitar_pack_gnu_tar_xf # Use different mixed scenarios for the reverse roundtrip test files = MIXED_FILENAME_SCENARIOS.dup # Add a different very long filename to test GNU extension compatibility files[VERY_LONG_FILENAME_SCENARIOS.keys.last] = VERY_LONG_FILENAME_SCENARIOS.values.last workspace with_files: files do gnu_tar_create_in_workspace minitar_unpack_in_workspace assert_files_extracted_in_workspace end end def test_roundtrip_gnu_tar_cf_minitar_unpack_mixed_filenames # Test GNU tar create → Minitar extract with mixed filename lengths files = { "short.txt" => "short content", "medium_length_filename.js" => "medium content", "#{"f" * 120}.css" => "long content", "dir/#{"g" * 130}.html" => "nested long content" } workspace with_files: files do gnu_tar_create_in_workspace minitar_unpack_in_workspace assert_files_extracted_in_workspace refute_file_path_duplication_in_workspace end end def test_minitar_writer_gnu_tar_xf_with_long_filenames # Test Minitar create → GNU tar extract with long filenames files = { "#{"j" * 120}.txt" => "content for 120 char filename", "nested/path/#{"k" * 110}.js" => "content for nested long filename", "#{"m" * 200}.html" => "content for very long filename", "regular_file.md" => "regular file content" } workspace with_files: files do minitar_writer_create_in_workspace gnu_tar_extract_in_workspace assert_files_extracted_in_workspace end end def test_gnu_tar_list_compatibility_with_long_filenames # Test that GNU tar can list files created by Minitar with long filenames files = { "#{"f" * 180}.data" => "gnu extension test content" } workspace with_files: files do minitar_writer_create_in_workspace list_output = gnu_tar_list_in_workspace files.each_key do |name| assert list_output.find { _1 == name }, "#{name} not present in GNU tar list output: #{list_output}" end gnu_tar_extract_in_workspace assert_files_extracted_in_workspace end end end minitar-1.1.0/test/test_tar_writer.rb0000644000004100000410000001665515061026725017752 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestTarWriter < Minitest::Test class DummyIO attr_reader :data def initialize reset end def write(dat) data << dat dat.size end def reset @data = +"" @data.force_encoding("ascii-8bit") end end def setup @data = "a" * 10 @unicode = [0xc3.chr, 0xa5.chr].join * 10 @unicode.force_encoding("utf-8") @dummy_writer = DummyIO.new @writer = Minitar::Writer.new(@dummy_writer) end def teardown @writer.close end def test_open_no_block writer = Minitar::Writer.open(@dummy_writer) refute writer.closed? ensure writer.close assert writer.closed? end def test_add_file_simple Minitar::Writer.open(@dummy_writer) do |os| os.add_file_simple("lib/foo/bar", mode: 0o644, size: 10) { _1.write "a" * 10 } os.add_file_simple("lib/bar/baz", mode: 0o644, size: 100) { _1.write "fillme" } end assert_headers_equal build_tar_file_header("lib/foo/bar", "", 0o644, 10), @dummy_writer.data[0, 512] assert_equal "a" * 10 + "\0" * 502, @dummy_writer.data[512, 512] assert_headers_equal build_tar_file_header("lib/bar/baz", "", 0o644, 100), @dummy_writer.data[512 * 2, 512] assert_equal "fillme" + "\0" * 506, @dummy_writer.data[512 * 3, 512] assert_equal "\0" * 512, @dummy_writer.data[512 * 4, 512] assert_equal "\0" * 512, @dummy_writer.data[512 * 5, 512] end def test_write_operations_fail_after_closed @writer.add_file_simple("sadd", mode: 0o644, size: 20) {} @writer.close assert_raises(Minitar::ClosedStream) { @writer.flush } assert_raises(Minitar::ClosedStream) { @writer.add_file("dfdsf", mode: 0o644) {} } assert_raises(Minitar::ClosedStream) { @writer.mkdir "sdfdsf", mode: 0o644 } assert_raises(Minitar::ClosedStream) { @writer.symlink "a", "b", mode: 0o644 } end def test_file_name_is_split_correctly # test extreme file lengths, and: a{100}/b{155}, etc names = { "#{"a" * 155}/#{"b" * 100}" => {name: "b" * 100, prefix: "a" * 155}, "#{"a" * 151}/#{"qwer/" * 19}bla" => {name: "#{"qwer/" * 19}bla", prefix: "a" * 151}, "/#{"a" * 49}/#{"b" * 50}" => {name: "b" * 50, prefix: "/#{"a" * 49}"}, "#{"a" * 49}/#{"b" * 50}x" => {name: "#{"b" * 50}x", prefix: "a" * 49}, "#{"a" * 49}x/#{"b" * 50}" => {name: "b" * 50, prefix: "#{"a" * 49}x"} } names.each_key do |name| @writer.add_file_simple(name, mode: 0o644, size: 10) {} end names.each_key.with_index do |key, index| name, prefix = names[key][:name], names[key][:prefix] assert_headers_equal build_tar_file_header(name, prefix, 0o644, 10), @dummy_writer.data[2 * index * 512, 512] end end def test_file_name_is_long @writer.add_file_simple(File.join("a" * 152, "b" * 10, "c" * 92), mode: 0o644, size: 10) {} @writer.add_file_simple(File.join("d" * 162, "e" * 10), mode: 0o644, size: 10) {} @writer.add_file_simple(File.join("f" * 10, "g" * 110), mode: 0o644, size: 10) {} # Issue #6. @writer.add_file_simple("a" * 114, mode: 0o644, size: 10) {} # "././@LongLink", a file name, its actual header, its data, ... 4.times do |i| assert_equal Minitar::PosixHeader::GNU_EXT_LONG_LINK, @dummy_writer.data[4 * i * 512, 32].rstrip end end def test_add_file_simple_content_with_long_name long_name_file_content = "where_is_all_the_content_gone" @writer.add_file_simple("a" * 114, mode: 0o0644, data: long_name_file_content) assert_equal long_name_file_content, @dummy_writer.data[3 * 512, long_name_file_content.bytesize] end def test_add_file_content_with_long_name dummyos = StringIO.new def dummyos.method_missing(meth, *a) string.send(meth, *a) end def dummyos.respond_to_missing?(meth, all) string.respond_to?(meth, all) end long_name_file_content = "where_is_all_the_content_gone" Minitar::Writer.open(dummyos) do |os| os.add_file("a" * 114, mode: 0o0644) do |f| f.write(long_name_file_content) end end assert_equal long_name_file_content, dummyos[3 * 512, long_name_file_content.bytesize] end def test_add_file dummyos = StringIO.new def dummyos.method_missing(meth, *a) string.send(meth, *a) end def dummyos.respond_to_missing?(meth, all) string.respond_to?(meth, all) end content1 = ("a".."z").to_a.join("") # 26 content2 = ("aa".."zz").to_a.join("") # 1352 Minitar::Writer.open(dummyos) do |os| os.add_file("lib/foo/bar", mode: 0o644) { |f, _opts| f.write "a" * 10 } os.add_file("lib/bar/baz", mode: 0o644) { |f, _opts| f.write content1 } os.add_file("lib/bar/baz", mode: 0o644) { |f, _opts| f.write content2 } os.add_file("lib/bar/baz", mode: 0o644) { |_f, _opts| } end assert_headers_equal build_tar_file_header("lib/foo/bar", "", 0o644, 10), dummyos[0, 512] assert_equal %(#{"a" * 10}#{"\0" * 502}), dummyos[512, 512] offset = 512 * 2 [content1, content2, ""].each do |data| assert_headers_equal build_tar_file_header("lib/bar/baz", "", 0o644, data.bytesize), dummyos[offset, 512] offset += 512 until !data || data == "" chunk = data[0, 512] data[0, 512] = "" assert_equal chunk + "\0" * (512 - chunk.bytesize), dummyos[offset, 512] offset += 512 end end assert_equal "\0" * 1024, dummyos[offset, 1024] end def test_add_file_tests_seekability assert_raises(Minitar::NonSeekableStream) do @writer.add_file("libdfdsfd", mode: 0o644) { |f| } end end def test_write_header @writer.add_file_simple("lib/foo/bar", mode: 0o644, size: 0) {} @writer.flush assert_headers_equal build_tar_file_header("lib/foo/bar", "", 0o644, 0), @dummy_writer.data[0, 512] @dummy_writer.reset @writer.mkdir("lib/foo", mode: 0o644) assert_headers_equal build_tar_dir_header("lib/foo", "", 0o644), @dummy_writer.data[0, 512] @writer.mkdir("lib/bar", mode: 0o644) assert_headers_equal build_tar_dir_header("lib/bar", "", 0o644), @dummy_writer.data[512 * 1, 512] end def test_write_data @writer.add_file_simple("lib/foo/bar", mode: 0o644, size: 10) do |f| f.write @data end @writer.flush assert_equal @data + ("\0" * (512 - @data.bytesize)), @dummy_writer.data[512, 512] end def test_write_unicode_data assert_equal 10, @unicode.size assert_equal 20, @unicode.bytesize @unicode.force_encoding("ascii-8bit") file = ["lib/foo/b", 0xc3.chr, 0xa5.chr, "r"].join @writer.add_file_simple(file, mode: 0o644, size: 20) do |f| f.write @unicode end @writer.flush assert_equal @unicode + ("\0" * (512 - @unicode.bytesize)), @dummy_writer.data[512, 512] end def test_file_size_is_checked assert_raises(Minitar::Writer::WriteBoundaryOverflow) do @writer.add_file_simple("lib/foo/bar", mode: 0o644, size: 10) do |f| f.write "1" * 100 end end @writer.add_file_simple("lib/foo/bar", mode: 0o644, size: 10) { |f| } end def test_symlink @writer.symlink("lib/foo/bar", "lib/foo/baz", mode: 0o644) @writer.flush assert_headers_equal build_tar_symlink_header("lib/foo/bar", "", 0o644, "lib/foo/baz"), @dummy_writer.data[0, 512] end def test_symlink_target_size_is_checked assert_raises(Minitar::FileNameTooLong) do @writer.symlink("lib/foo/bar", "x" * 101) end end end minitar-1.1.0/test/test_tar_header.rb0000644000004100000410000002037615061026725017661 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestTarHeader < Minitest::Test def test_arguments_are_checked assert_raises(ArgumentError) { new(name: "", size: "", mode: "") } assert_raises(ArgumentError) { new(name: "", size: "", prefix: "") } assert_raises(ArgumentError) { new(name: "", prefix: "", mode: "") } assert_raises(ArgumentError) { new(prefix: "", size: "", mode: "") } end def test_basic_headers assert_headers_equal build_tar_file_header("bla", "", 0o12345, 10), new( name: "bla", mode: 0o12345, size: 10, prefix: "", typeflag: "0" ) assert_headers_equal build_tar_dir_header("bla", "", 0o12345), new( name: "bla", mode: 0o12345, size: 0, prefix: "", typeflag: "5" ) end def test_long_name_works assert_headers_equal build_tar_file_header("a" * 100, "", 0o12345, 10), new(name: "a" * 100, mode: 0o12345, size: 10, prefix: "") assert_headers_equal build_tar_file_header("a" * 100, "bb" * 60, 0o12345, 10), new(name: "a" * 100, mode: 0o12345, size: 10, prefix: "bb" * 60) end def test_from_stream header = from_stream( build_tar_file_header("a" * 100, "", 0o12345, 10) ) assert_equal "a" * 100, header.name assert_equal 0o12345, header.mode assert_equal 10, header.size assert_equal "", header.prefix assert_equal "ustar", header.magic end def test_from_stream_with_evil_name assert_equal "a ", from_stream( build_tar_file_header("a \0" + "\0" * 97, "", 0o12345, 10) ).name end def test_valid_with_valid_header assert from_stream( build_tar_file_header("a" * 100, "", 0o12345, 10) ).valid? end def test_from_stream_with_no_strict_octal assert_raises(ArgumentError) do from_stream( build_tar_file_header("a" * 100, "", 0o12345, -1213) ) end end def test_from_stream_with_octal_wrapped_by_spaces assert_equal 651, from_stream( update_header_checksum( build_raw_header( 0, asciiz("a" * 100, 100), asciiz("", 155), " 1213\0", z(octal(0o12345, 7)) ) ) ).size end def test_valid_with_invalid_header refute from_stream("invalid").valid? end def test_setting_long_name_assigns_complete_path header = new( name: "short.txt", mode: 0o644, size: 100, prefix: "some/directory/path" ) complete_path = "some/directory/path/very_long_filename_that_exceeds_one_hundred_characters_and_needs_gnu_extension.txt" header.long_name = complete_path assert_equal complete_path, header.name end def test_setting_long_name_clears_prefix_field header = new( name: "short.txt", mode: 0o644, size: 100, prefix: "some/directory/path" ) complete_path = "some/directory/path/very_long_filename_that_exceeds_one_hundred_characters_and_needs_gnu_extension.txt" header.long_name = complete_path assert_equal "", header.prefix end def test_setting_long_name_with_nested_directories header = new( name: "file.txt", mode: 0o644, size: 50, prefix: "old/prefix" ) complete_path = "deep/nested/directory/structure/with/very_long_filename_that_needs_gnu_long_name_extension_support.txt" header.long_name = complete_path assert_equal complete_path, header.name assert_equal "", header.prefix end def test_setting_long_name_with_empty_prefix header = new( name: "file.txt", mode: 0o644, size: 50, prefix: "" ) complete_path = "very_long_filename_that_exceeds_one_hundred_characters_and_definitely_needs_gnu_extension_support.txt" header.long_name = complete_path assert_equal complete_path, header.name assert_equal "", header.prefix end def test_gnu_long_names_extension_is_detected assert new( name: Minitar::PosixHeader::GNU_EXT_LONG_LINK, mode: 0o644, size: 150, prefix: "", typeflag: "L" ).long_name? end def test_setting_long_name_with_very_long_path header = new( name: "file.txt", mode: 0o644, size: 100, prefix: "old/prefix" ) long_dir = "very_long_directory_name_that_exceeds_normal_limits" * 5 complete_path = "#{long_dir}/extremely_long_filename_with_many_characters.txt" header.long_name = complete_path assert_equal complete_path, header.name assert_equal "", header.prefix end def test_setting_long_name_with_single_filename_no_directories header = new( name: "short.txt", mode: 0o644, size: 100, prefix: "some/existing/prefix" ) complete_path = "extremely_long_single_filename_without_any_directory_structure_that_exceeds_one_hundred_characters.txt" header.long_name = complete_path assert_equal complete_path, header.name assert_equal "", header.prefix end def test_setting_long_name_preserves_other_header_fields header = new( name: "short.txt", mode: 0o755, size: 12345, prefix: "old/prefix", uid: 1000, gid: 1000, mtime: 1234567890, typeflag: "0", linkname: "some_link" ) complete_path = "new/very_long_path/that_exceeds_one_hundred_characters_and_needs_gnu_extension_support.txt" header.long_name = complete_path assert_equal complete_path, header.name assert_equal "", header.prefix assert_equal 0o755, header.mode assert_equal 12345, header.size assert_equal 1000, header.uid assert_equal 1000, header.gid assert_equal 1234567890, header.mtime assert_equal "0", header.typeflag assert_equal "some_link", header.linkname end def test_setting_name_with_empty_string header = new( name: "short.txt", mode: 0o644, size: 100, prefix: "some/prefix" ) assert_raises(ArgumentError) do header.name = "" end end def test_setting_long_name_with_empty_string header = new( name: "short.txt", mode: 0o644, size: 100, prefix: "some/prefix" ) assert_raises(ArgumentError) do header.long_name = "" end end def test_new_with_empty_string assert_raises(ArgumentError) do new( name: "", mode: 0o644, size: 100, prefix: "some/prefix" ) end end def test_parse_numeric_field_octal ph = Minitar::PosixHeader assert_equal 123, ph.send(:parse_numeric_field, "173") assert_equal 0, ph.send(:parse_numeric_field, "0") assert_equal 511, ph.send(:parse_numeric_field, "777") end def test_parse_numeric_field_octal_with_spaces ph = Minitar::PosixHeader assert_equal 123, ph.send(:parse_numeric_field, " 173 ") assert_equal 0, ph.send(:parse_numeric_field, " ") end def test_parse_numeric_field_binary_positive_number binary_data = [0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9A].pack("C*") header = build_raw_header( 0, asciiz("large_file.bin", 100), asciiz("", 155), binary_data, z(octal(0o12345, 7)) ) header = update_header_checksum(header) io = StringIO.new(header) h = Minitar::PosixHeader.from_stream(io) expected_size = 0x123456789A assert_equal expected_size, h.size end def test_parse_numeric_field_binary_negative_number binary_data = [0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF].pack("C*") header = build_raw_header( 0, asciiz("negative_size.bin", 100), asciiz("", 155), binary_data, z(octal(0o12345, 7)) ) header = update_header_checksum(header) io = StringIO.new(header) h = Minitar::PosixHeader.from_stream(io) expected_size = -1 assert_equal expected_size, h.size end def test_parse_numeric_field_invalid ph = Minitar::PosixHeader assert_raises(ArgumentError) do ph.send(:parse_numeric_field, "invalid") end assert_raises(ArgumentError) do ph.send(:parse_numeric_field, "\x01\x02\x03") # Invalid binary format end end private def from_stream(stream) = stream .then { _1.is_a?(String) ? StringIO.new(_1) : _1 } .then { Minitar::PosixHeader.from_stream(_1) } def new(header_hash) = Minitar::PosixHeader.new(header_hash) end minitar-1.1.0/test/test_issue_46.rb0000755000004100000410000000160115061026725017215 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestIssue46 < Minitest::Test SUPERLONG_CONTENTS = { ["0123456789abcde"].then { _1 * 33 }.join("/") => {size: 496, mode: 0o644}, "endfile" => {size: 0, mode: 0o644} } def test_each_works Minitar::Input.open(open_fixture("issue_46")) do |stream| outer = 0 stream.each.with_index do |entry, i| assert_kind_of Minitar::Reader::EntryStream, entry assert SUPERLONG_CONTENTS.key?(entry.name), "File #{entry.name} not defined" assert_equal SUPERLONG_CONTENTS[entry.name][:size], entry.size, "File sizes sizes do not match: #{entry.name}" assert_modes_equal(SUPERLONG_CONTENTS[entry.name][:mode], entry.mode, entry.name) assert_equal TIME_2004, entry.mtime, "entry.mtime" outer += 1 end assert_equal 2, outer end end end minitar-1.1.0/test/support/0000755000004100000410000000000015061026725015703 5ustar www-datawww-dataminitar-1.1.0/test/support/minitar_test_helpers/0000755000004100000410000000000015061026725022127 5ustar www-datawww-dataminitar-1.1.0/test/support/minitar_test_helpers/tarball.rb0000644000004100000410000002224615061026725024103 0ustar www-datawww-data# frozen_string_literal: true require "open3" # Test assertions and helpers for working with tarballs. # # Includes Minitar in-memory and on-disk operations and GNU tar helpers. module Minitar::TestHelpers::Tarball private GNU_TAR = %w[tar gtar].find { |program| path = `bash -c "command -v '#{program}'"`.chomp if path.empty? false else version = `#{program} --version`.chomp version =~ /\(GNU tar\)|Free Software Foundation/ end }.freeze # Given the +original_files+ file hash (input to +create_tar_string+) and the # +extracted_files+ file has (output from +extract_tar_string+), ensures that the tar # structure is preserved, including checking for possible regression of issue 62. # # Such a regression would result in a directory like >/b/c.txt looking like # a/b/a/b/c.txt (but only for long filenames). def assert_tar_structure_preserved(original_files, extracted_files) assert_equal original_files.length, extracted_files.length, "File counts do not match" original_paths = original_files.keys.sort extracted_paths = extracted_files.keys.sort assert_equal original_paths, extracted_paths, "Complete file paths should match exactly" original_files.each do |filename, content| assert extracted_files.key?(filename), "File #{filename} should be extracted" assert_equal content, extracted_files[filename], "Content should be preserved for #{filename}" next unless filename.include?("/") dirname, basename = File.split(filename) bad_pattern = File.join(dirname, dirname, basename) duplicated_paths = extracted_paths.select { |path| path == bad_pattern } refute duplicated_paths.any?, "Regression of #62, path duplication on extraction! " \ "Original: #{filename}, " \ "Bad pattern found: #{bad_pattern}, " \ "All extracted paths: #{extracted_paths}" end end # Create a tarball string from the +file_hash+ ({filename => content}). def create_tar_string(file_hash) = StringIO.new.tap { |io| Minitar::Output.open(io) do |output| file_hash.each do |filename, content| next if content.nil? Minitar.pack_as_file(filename, content.to_s.dup, output) end end }.string # Extract a hash of {filename => content} from the +tar_data+. Directories are # skipped. def extract_tar_string(tar_data) = {}.tap { |files| Minitar::Input.open(StringIO.new(tar_data)) do |input| input.each do |entry| next if entry.directory? files[entry.full_name] = entry.read end end } # Create a tarball string from the +file_hash+ ({filename => content}) provided # and immediately extracts a hash of {filename => content} from the tarball # string. Directories are skipped. def roundtrip_tar_string(file_hash) = create_tar_string(file_hash).then { extract_tar_string(_1) } def has_gnu_tar? = !GNU_TAR&.empty? Workspace = Struct.new(:tmpdir, :source, :target, :tarball, :files, keyword_init: true) private_constant :Workspace # Prepare a workspace for a file-based test. def workspace(with_files: nil) raise "Workspace requires a block" unless block_given? raise "No nested workspace permitted" if @workspace tmpdir = if Pathname.respond_to?(:mktmpdir) Pathname.mktmpdir else Pathname(Dir.mktmpdir) end source = tmpdir.join("source") target = tmpdir.join("target") tarball = tmpdir.join("test.tar") @workspace = Workspace.new( tmpdir: tmpdir, source: source, target: target, tarball: tarball ) source.mkpath target.mkpath prepare_workspace(with_files:) if with_files yield @workspace ensure tmpdir&.rmtree @workspace = nil end def prepare_workspace(with_files:) missing_workspace! raise "Missing workspace" unless @workspace raise "Files already prepared" if @workspace.files @workspace.files = with_files.each_pair do full_path = @workspace.source.join(_1) if _2.nil? full_path.mkpath assert full_path.directory?, "#{full_path} should be created as a directory" else full_path.dirname.mkpath full_path.write(_2) assert full_path.file?, "#{full_path} should be created as a file" end end end def gnu_tar_create_in_workspace missing_workspace! __gnu_tar(:create) assert @workspace.tarball.file?, "Workspace tarball not created by GNU tar" assert @workspace.tarball.size > 0, "Workspace tarball should not be empty" end def gnu_tar_extract_in_workspace missing_workspace! assert @workspace.tarball.file?, "Workspace tarball not present for extraction" assert @workspace.tarball.size > 0, "Workspace tarball should not be empty" __gnu_tar(:extract) end def gnu_tar_list_in_workspace missing_workspace! assert @workspace.tarball.file?, "Workspace tarball not present for extraction" assert @workspace.tarball.size > 0, "Workspace tarball should not be empty" __gnu_tar(:list).strip.split($/) end def minitar_pack_in_workspace missing_workspace! @workspace.tarball.open("wb") do |tar_io| Dir.chdir(@workspace.source) do Minitar.pack(".", tar_io) end end assert @workspace.tarball.file?, "Workspace tarball not created by Minitar.pack" assert @workspace.tarball.size > 0, "Workspace tarball should not be empty" end def minitar_unpack_in_workspace missing_workspace! assert @workspace.tarball.file?, "Workspace tarball not present for extraction" assert @workspace.tarball.size > 0, "Workspace tarball should not be empty" @workspace.tarball.open("rb") do Minitar.unpack(_1, @workspace.target) end end def minitar_writer_create_in_workspace missing_workspace! @workspace.tarball.open("wb") do |tar_io| Minitar::Writer.open(tar_io) do |writer| @workspace.files.each_pair do |name, content| full_path = @workspace.source.join(name) stat = full_path.stat writer.add_file_simple(name, mode: stat.mode, size: stat.size) do _1.write(content) end end end end assert @workspace.tarball.file?, "Workspace tarball not created by Minitar::Writer" assert @workspace.tarball.size > 0, "Workspace tarball should not be empty" end def assert_files_extracted_in_workspace missing_workspace! @workspace.files.each_pair do target = @workspace.target.join(_1) assert target.exist?, "#{_1.inspect} does not exist" if _2.nil? assert target.directory?, "#{_1} is not a directory" else assert_equal _2, @workspace.target.join(_1).read, "#{_1} content does not match" end end end def refute_file_path_duplication_in_workspace missing_workspace! @workspace.files.each_key do next unless _1.include?("/") dir, filename = Pathname(_1).split dup_path = dir.join(dir, filename) refute @workspace.target.join(dup_path).exist?, "No path duplication should occur: #{dup_path}" end end def assert_extracted_files_match_source_files_in_workspace missing_workspace! source_files = __collect_relative_paths(@workspace.source) target_files = __collect_relative_paths(@workspace.target) assert_equal source_files, target_files, "Complete directory structure should match exactly" end def __collect_relative_paths(dir) return [] unless dir.directory? dir.glob("**/*").map { _1.relative_path_from(dir).to_s }.sort end def assert_file_modes_match_in_workspace missing_workspace! return if Minitar.windows? @workspace.files.each_key do |file_path| source = @workspace.source.join(file_path) target = @workspace.target.join(file_path) source_mode = source.stat.mode & 0o777 target_mode = target.stat.mode & 0o777 assert_modes_equal source_mode, target_mode, file_path end end def missing_workspace! raise "Missing workspace" unless defined?(@workspace) end def __gnu_tar(action) missing_workspace! cmd = [GNU_TAR] cmd.push("--force-local") if Minitar.windows? case action when :create cmd.push("-cf", @workspace.tarball.to_s, "-C", @workspace.source.to_s, ".") when :extract cmd.push("-xf", @workspace.tarball.to_s, "-C", @workspace.target.to_s) when :list cmd.push("-tf", @workspace.tarball.to_s) end stdout_str = "" stderr_str = "" status = nil Open3.popen3(*cmd) do |stdin, stdout, stderr, wait_thread| stdin.close out_t = Thread.new { stdout.read } err_t = Thread.new { stderr.read } stdout_str = out_t.value.to_s stderr_str = err_t.value.to_s status = wait_thread.value end unless status.success? warn stdout_str unless stdout_str.empty? warn stderr_str unless stderr_str.empty? if status.exited? raise "command #{cmd.join(" ")} failed (exit status: #{status.exitstatus})" else raise "command #{cmd.join(" ")} failed (status: #{status.inspect})" end end stdout_str end Minitest::Test.send(:include, self) end minitar-1.1.0/test/support/minitar_test_helpers/header.rb0000644000004100000410000000705515061026725023713 0ustar www-datawww-data# frozen_string_literal: true # Test assertions and helpers for working with header objects. module Minitar::TestHelpers::Header private # Assert that the +actual+ header is equal to +expected+. def assert_headers_equal(expected, actual) actual = actual.to_s __field_order.each do |field| message = if field == "checksum" "Header checksums are expected to match." else "Header field #{field} is expected to match." end offset = __fields[field].offset length = __fields[field].length assert_equal expected[offset, length], actual[offset, length], message end end def assert_modes_equal(expected, actual, filename) return if Minitar.windows? assert_equal mode_string(expected), mode_string(actual), "Mode for #{filename} does not match" end def build_raw_header(type, name, prefix, size, mode, link_name = "") = [ name, mode, z(octal(nil, 7)), z(octal(nil, 7)), size, z(octal(0, 11)), BLANK_CHECKSUM, type, asciiz(link_name, 100), USTAR, DOUBLE_ZERO, asciiz("", 32), asciiz("", 32), z(octal(nil, 7)), z(octal(nil, 7)), prefix ].join.bytes.to_a.pack("C100C8C8C8C12C12C8CC100C6C2C32C32C8C8C155").then { "#{_1}#{"\0" * (512 - _1.bytesize)}" }.tap { assert_equal 512, _1.bytesize } def build_header(type, name, prefix, size, mode, link_name = "") = build_raw_header( type, asciiz(name, 100), asciiz(prefix, 155), z(octal(size, 11)), z(octal(mode, 7)), asciiz(link_name, 100) ) def build_tar_file_header(name, prefix, mode, size) = build_header("0", name, prefix, size, mode).then { update_header_checksum(_1) } def build_tar_dir_header(name, prefix, mode) = build_header("5", name, prefix, 0, mode).then { update_header_checksum(_1) } def build_tar_symlink_header(name, prefix, mode, target) = build_header("2", name, prefix, 0, mode, target).then { update_header_checksum(_1) } def build_tar_pax_header(name, prefix, content_size) = build_header("x", name, prefix, content_size, 0o644).then { update_header_checksum(_1) } def update_header_checksum(header) = header.tap { |h| checksum = __fields["checksum"] h[checksum.offset, checksum.length] = h.unpack("C*") .inject(:+) .then { octal(_1, 6) } .then { z(_1) } .then { sp(_1) } } def octal(n, pad_size) = n.nil? ? "\0" * pad_size : "%0#{pad_size}o" % n def asciiz(str, size) = "#{str}#{"\0" * (size - str.bytesize)}" def sp(s) = "#{s} " def z(s) = "#{s}\0" def mode_string(value) = "%04o" % (value & 0o777) def __field_order = FIELD_ORDER def __fields = FIELDS FIELD_ORDER = [] private_constant :FIELD_ORDER FIELDS = {} private_constant :FIELDS Field = Struct.new(:name, :offset, :length) private_constant :Field BLANK_CHECKSUM = (" " * 8).freeze private_constant :BLANK_CHECKSUM DOUBLE_ZERO = "00" private_constant :DOUBLE_ZERO NULL_100 = ("\0" * 100).freeze private_constant :NULL_100 USTAR = "ustar\0" private_constant :USTAR fields = [ ["name", 100], ["mode", 8], ["uid", 8], ["gid", 8], ["size", 12], ["mtime", 12], ["checksum", 8], ["typeflag", 1], ["linkname", 100], ["magic", 6], ["version", 2], ["uname", 32], ["gname", 32], ["devmajor", 8], ["devminor", 8], ["prefix", 155] ] offset = 0 fields.each do |(name, length)| FIELDS[name] = Field.new(name, offset, length) FIELD_ORDER << name offset += length end Minitest::Test.send(:include, self) end minitar-1.1.0/test/support/minitar_test_helpers/fixtures.rb0000644000004100000410000000141415061026725024325 0ustar www-datawww-data# frozen_string_literal: true module Minitar::TestHelpers::Fixtures def Fixture(name) = FIXTURES.fetch(name) def open_fixture(name) fixture = Fixture(name) io = fixture.open("rb").then { if %w[.gz .tgz].include?(fixture.extname.to_s) Zlib::GzipReader.new(_1) else _1 end }.tap { yield _1 if block_given? } ensure io&.close if block_given? end FIXTURES = Pathname(__dir__) .join("../../fixtures") .expand_path .glob("**") .each_with_object({}) { next if _1.directory? name = _1.dup name = name.basename(name.extname) until name.nil? || name.extname.empty? _2[name.to_s] = _1 } .freeze private_constant :FIXTURES Minitest::Test.send(:include, self) end minitar-1.1.0/test/support/minitar_test_helpers.rb0000644000004100000410000000235615061026725022462 0ustar www-datawww-data# frozen_string_literal: true module Minitar::TestHelpers TIME_2004 = Time.utc(2004).to_i BOUNDARY_SCENARIOS = { ("a" * 99) => "99 chars content", ("a" * 100) => "100 chars content", ("a" * 101) => "101 chars content", ("a" * 102) => "102 chars content", "dir/#{"a" * 96}" => "nested path 100 total content", "dir/#{"a" * 97}" => "nested path 101 total content", "nested/#{"d" * 93}" => "nested 100 total content", "nested/#{"e" * 94}" => "nested 101 total content" }.freeze MIXED_FILENAME_SCENARIOS = { "short.txt" => "short content", "medium_length_filename_under_100_chars.txt" => "medium content", "dir1/medium_filename.js" => "medium nested content", "#{"x" * 120}.txt" => "long content", "nested/dir/#{"y" * 110}.css" => "long nested content" }.freeze VERY_LONG_FILENAME_SCENARIOS = { "#{"f" * 180}.data" => "180 char filename content", "#{"g" * 200}.json" => "200 char filename content", "nested/path/#{"h" * 150}.css" => "nested long filename content", "deep/nested/structure/#{"i" * 170}.html" => "deeply nested long content", "project/src/main/#{"j" * 160}.java" => "project structure long content" }.freeze private Minitest::Test.send(:include, self) end minitar-1.1.0/test/test_pax_header.rb0000644000004100000410000000731315061026725017657 0ustar www-datawww-datarequire "minitest_helper" class TestPaxHeader < Minitest::Test def test_from_stream_with_size_attribute pax_content = "19 size=8614356715\n28 mtime=1749098832.3200000\n" pax_header = create_pax_header_from_stream(pax_content) assert_equal 8614356715, pax_header.size assert_equal "1749098832.3200000", pax_header.attributes["mtime"] end def test_from_stream_without_size_attribute pax_content = "28 mtime=1749098832.3200000\n27 path=some/long/path.txt\n" pax_header = create_pax_header_from_stream(pax_content) assert_nil pax_header.size assert_equal "some/long/path.txt", pax_header.path assert_equal 1749098832.32, pax_header.mtime end def test_parse_multiline_values pax_content = "22 foo=one\ntwo\nthree\n\n12 bar=four\n" pax_header = Minitar::PaxHeader.from_data(pax_content) assert_equal "one\ntwo\nthree\n", pax_header.attributes["foo"] assert_equal "four", pax_header.attributes["bar"] end def test_from_stream_with_invalid_header header_data = build_tar_file_header("regular_file.txt", "", 0o644, 100) io = StringIO.new(header_data) posix_header = Minitar::PosixHeader.from_stream(io) refute posix_header.pax_header? assert_raises(ArgumentError, "Header must be a PAX header") do Minitar::PaxHeader.from_stream(io, posix_header) end end def test_parse_content_with_multiple_attributes pax_content = "19 size=8614356715\n28 mtime=1749098832.3200000\n27 path=some/long/path.txt\n" pax_header = Minitar::PaxHeader.from_data(pax_content) assert_equal 8614356715, pax_header.size assert_equal "some/long/path.txt", pax_header.path assert_equal 1749098832.32, pax_header.mtime # Check raw attributes assert_equal "8614356715", pax_header.attributes["size"] assert_equal "1749098832.3200000", pax_header.attributes["mtime"] assert_equal "some/long/path.txt", pax_header.attributes["path"] end def test_parse_content_with_invalid_length_format assert_raises(ArgumentError) do Minitar::PaxHeader.from_data("19 size=8614356715\ninvalid line\n23 path=valid/path.txt\n") end end def test_parse_content_with_oversized_record assert_raises(ArgumentError) do Minitar::PaxHeader.from_data("19 size=8614356715\n999 toolong=value\n") end end def test_from_stream_strips_padding pax_content = "19 size=8614356715\n" pax_header = create_pax_header_from_stream(pax_content) # Should parse only the actual content, ignoring padding assert_equal 8614356715, pax_header.size assert_equal 1, pax_header.attributes.size # Only one attribute parsed # Should have parsed content correctly assert_equal 1, pax_header.attributes.size assert_equal "8614356715", pax_header.attributes["size"] end def test_attributes_accessor pax_content = "19 size=8614356715\n23 custom=custom_value\n" pax_header = Minitar::PaxHeader.from_data(pax_content) assert_equal "8614356715", pax_header.attributes["size"] assert_equal "custom_value", pax_header.attributes["custom"] assert_nil pax_header.attributes["nonexistent"] end def test_pax_header_to_s pax_header = Minitar::PaxHeader.new(size: "8614356715", mtime: "1749098832.3200000") assert_equal "19 size=8614356715\n28 mtime=1749098832.3200000\n", pax_header.to_s end private def create_pax_header_from_stream(pax_content, name = "./PaxHeaders.X/test_file") pax_header_data = build_tar_pax_header(name, "", pax_content.bytesize) padded_content = pax_content.ljust((pax_content.bytesize / 512.0).ceil * 512, "\0") io = StringIO.new(pax_header_data + padded_content) posix_header = Minitar::PosixHeader.from_stream(io) Minitar::PaxHeader.from_stream(io, posix_header) end end minitar-1.1.0/test/test_issue_62.rb0000644000004100000410000000351415061026725017215 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestIssue62 < Minitest::Test # These are representative filenames from issue #62 which were extracted incorrectly. FILENAMES = [ "hpg5lfg/1j/973e4t/hqc/djcrcb1l49ardcthyl5u80dcgmo03cp5mh938wr38dka7us1ja4i3dfrp3ahg4q2ooet6avyw45nqpzrcxfzdemvzj07oftcghtkl5bdc.gz", "hpg5lfg/1j/973e4t/hqc/mioxx4h9tgcc9gqw0j8z2fj2covf6nsplrwggyjsg4swmh0glzy2jji4n2gspvb2vlki7zmu81046hvgt4fstlk6fldv0p1w3nf7o6.css", "k6hly56/mh/ri2pa1/04/0afdks3r6k1mbf64xzuwh5efkuxurro63rbckjssmz9mdratf6ayfduqpb0r9qxx2mgnrs0thi0ohh4qtfylfd6cd506zawwic0u3ec0iluu4myn.map", "k6hly56/mh/ri2pa1/04/5k8mnvwxe7hmvp1n932o4mn2b25gqrxfrbe4jfjbig6kzhphnsfkrtqruypfzl93u0ohlv9yyxcoxn6jg6iv5ml8e27jdqjiikyq3.js" ].freeze FILENAMES.each do |filename| first, *, last = filename.split("/") last = File.extname(last) define_method :"test_issue_62_path_#{first}_#{last}" do file_map = {filename => "Test content for #{File.basename(filename)}"} files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end end def test_issue_62_full_regression file_map = FILENAMES.each_with_object({}) { |name, map| map[name] = "Test content for #{File.basename(name)}" } files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end def test_issue_62_mixed_filename_lengths_no_regression file_map = MIXED_FILENAME_SCENARIOS.dup FILENAMES.each { file_map[_1] = "content for problematic filename #{_1}" } files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end def test_issue_62_very_long_filenames_no_regression file_map = VERY_LONG_FILENAME_SCENARIOS.dup files = roundtrip_tar_string(file_map) assert_tar_structure_preserved file_map, files end end minitar-1.1.0/test/test_minitar.rb0000755000004100000410000001417415061026725017230 0ustar www-datawww-data# frozen_string_literal: true require "minitest_helper" class TestMinitar < Minitest::Test SCENARIO = { "path" => nil, "test" => "test", "extra/test" => "extra/test", "empty2004" => {mtime: TIME_2004, mode: 0o755, data: ""}, "notime" => {mtime: nil, data: "notime"} } def test_minitar_open_r count = 0 open_fixture("test_minitar") do |fixture| Minitar.open(fixture, "r") do |stream| stream.each do |entry| assert_kind_of Minitar::Reader::EntryStream, entry assert SCENARIO.has_key?(entry.name), "#{entry.name} not expected" expected = SCENARIO[entry.name] case expected when nil assert_equal 0, entry.size assert_modes_equal 0o755, entry.mode, entry.name assert entry.directory?, "#{entry.name} should be a directory" when String assert_equal expected.length, entry.size, entry.name assert_modes_equal 0o644, entry.mode, entry.name assert entry.file?, "#{entry.name} should be a file" if entry.size.zero? assert_nil entry.read else assert_equal expected, entry.read end when Hash if expected[:data].nil? assert_equal 0, entry.size assert_modes_equal (expected[:mode] || 0o755), entry.mode, entry.name assert entry.directory?, "#{entry.name} should be a directory" else assert_equal expected[:data].length, entry.size assert_modes_equal (expected[:mode] || 0o644), entry.mode, entry.name assert entry.file?, "#{entry.name} should be a file" end assert_equal expected[:mtime], entry.mtime if expected[:mtime] if entry.size.zero? assert_nil entry.read else assert_equal expected[:data], entry.read end end count += 1 end end end assert_equal SCENARIO.size, count end def test_minitar_open_w events = [] writer = StringIO.new Minitar.open(writer, "w") do |stream| SCENARIO.each_pair do |name, data| name, data = if data.is_a?(Hash) name = data.merge(name: name) [name, name.delete(:data)] else [name, data] end Minitar.pack_as_file(name, data, stream) do |op, entry_name, stats| events << { name: name, data: data, op: op, entry_name: entry_name, stats: stats } end end end assert_equal 5120, writer.string.length events.each do |event| if event[:name].is_a?(Hash) assert_equal event[:name][:name], event[:entry_name] else assert_equal event[:name], event[:entry_name] end case [event[:op], event[:entry_name]] in [:dir, "path"] assert_equal 0, event[:stats][:size] assert_equal 493, event[:stats][:mode] in [:file_start, "test"] assert_equal 4, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 4, event[:stats][:current] assert_equal 4, event[:stats][:currinc] assert_equal "test", event[:data] in [:file_progress, "test"] assert_equal 4, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 4, event[:stats][:current] assert_equal 4, event[:stats][:currinc] assert_equal "test", event[:data] in [:file_done, "test"] assert_equal 4, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 4, event[:stats][:current] assert_equal 4, event[:stats][:currinc] assert_equal "test", event[:data] in [:file_start, "extra/test"] assert_equal 10, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 10, event[:stats][:current] assert_equal 10, event[:stats][:currinc] assert_equal "extra/test", event[:data] in [:file_progress, "extra/test"] assert_equal 10, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 10, event[:stats][:current] assert_equal 10, event[:stats][:currinc] assert_equal "extra/test", event[:data] in [:file_done, "extra/test"] assert_equal 10, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 10, event[:stats][:current] assert_equal 10, event[:stats][:currinc] assert_equal "extra/test", event[:data] in [:file_start, "empty2004"] assert_equal 0, event[:stats][:size] assert_equal 493, event[:stats][:mode] assert_equal 0, event[:stats][:current] assert_equal 1072915200, event[:stats][:mtime] assert_equal "", event[:data] in [:file_done, "empty2004"] assert_equal 0, event[:stats][:size] assert_equal 493, event[:stats][:mode] assert_equal 0, event[:stats][:current] assert_equal 1072915200, event[:stats][:mtime] assert_equal "", event[:data] in [:file_start, "notime"] assert_equal 6, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 6, event[:stats][:current] assert_equal 6, event[:stats][:currinc] assert_equal "notime", event[:data] in [:file_progress, "notime"] assert_equal 6, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 6, event[:stats][:current] assert_equal 6, event[:stats][:currinc] assert_equal "notime", event[:data] in [:file_done, "notime"] assert_equal 6, event[:stats][:size] assert_equal 420, event[:stats][:mode] assert_equal 6, event[:stats][:current] assert_equal 6, event[:stats][:currinc] assert_equal "notime", event[:data] else raise "Unknown operation #{event[:op].inspect} for #{event[:entry_name].inspect}" end end end def test_minitar_x assert_raises(ArgumentError) do Minitar.open("foo", "x") end end end minitar-1.1.0/Rakefile0000644000004100000410000000476215061026725014666 0ustar www-datawww-data# frozen_string_literal: true require "rubygems" require "hoe" require "rake/clean" require "rdoc/task" require "minitest" require "minitest/test_task" Hoe.plugin :halostatue Hoe.plugin :rubygems Hoe.plugins.delete :debug Hoe.plugins.delete :newb Hoe.plugins.delete :publish Hoe.plugins.delete :signing Hoe.plugins.delete :test hoe = Hoe.spec "minitar" do developer("Austin Ziegler", "halostatue@gmail.com") self.trusted_release = ENV["rubygems_release_gem"] == "true" require_ruby_version ">= 3.1" self.licenses = ["Ruby", "BSD-2-Clause"] spec_extras[:metadata] = ->(val) { val["rubygems_mfa_required"] = "true" } extra_dev_deps << ["hoe", "~> 4.0"] extra_dev_deps << ["hoe-halostatue", "~> 2.1", ">= 2.1.1"] extra_dev_deps << ["irb", "~> 1.0"] extra_dev_deps << ["minitest", "~> 5.16"] extra_dev_deps << ["minitest-autotest", "~> 1.0"] extra_dev_deps << ["minitest-focus", "~> 1.1"] extra_dev_deps << ["rake", ">= 10.0", "< 14"] extra_dev_deps << ["rdoc", ">= 0.0", "< 7"] extra_dev_deps << ["simplecov", "~> 0.22"] extra_dev_deps << ["simplecov-lcov", "~> 0.8"] extra_dev_deps << ["standard", "~> 1.0"] extra_dev_deps << ["standard-minitest", "~> 1.0"] extra_dev_deps << ["standard-thread_safety", "~> 1.0"] end Minitest::TestTask.create :test Minitest::TestTask.create :coverage do |t| formatters = <<-RUBY.split($/).join(" ") SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, SimpleCov::Formatter::LcovFormatter, SimpleCov::Formatter::SimpleFormatter ]) RUBY t.test_prelude = <<-RUBY.split($/).join("; ") require "simplecov" require "simplecov-lcov" SimpleCov::Formatter::LcovFormatter.config do |config| config.report_with_single_file = true config.lcov_file_name = "lcov.info" end SimpleCov.start "test_frameworks" do enable_coverage :branch primary_coverage :branch formatter #{formatters} end RUBY end task default: :test task :version do require "color/version" puts Color::VERSION end RDoc::Task.new do _1.title = "minitar" _1.main = "lib/minitar.rb" _1.rdoc_dir = "doc" _1.rdoc_files = hoe.spec.require_paths - ["Manifest.txt"] + hoe.spec.extra_rdoc_files _1.markup = "markdown" end task docs: :rerdoc task :console do arguments = %w[irb] arguments.push(*hoe.spec.require_paths.map { |dir| "-I#{dir}" }) arguments.push("-r#{hoe.spec.name.gsub("-", File::SEPARATOR)}") unless system(*arguments) error "Command failed: #{show_command}" abort end end minitar-1.1.0/CODE_OF_CONDUCT.md0000644000004100000410000001222615061026725016012 0ustar www-datawww-data# Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at . Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at . Translations are available at . [homepage]: https://www.contributor-covenant.org [Mozilla CoC]: https://github.com/mozilla/diversity minitar-1.1.0/docs/0000755000004100000410000000000015061026725014140 5ustar www-datawww-dataminitar-1.1.0/docs/ruby.txt0000644000004100000410000000470615061026725015671 0ustar www-datawww-dataRuby is copyrighted free software by Yukihiro Matsumoto . You can redistribute it and/or modify it under either the terms of the 2-clause BSDL (see the file BSDL), or the conditions below: 1. You may make and give away verbatim copies of the source form of the software without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may modify your copy of the software in any way, provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or by allowing the author to include your modifications in the software. b) use the modified software only within your corporation or organization. c) give non-standard binaries non-standard names, with instructions on where to get the original software distribution. d) make other distribution arrangements with the author. 3. You may distribute the software in object code or binary form, provided that you do at least ONE of the following: a) distribute the binaries and library files of the software, together with instructions (in the manual page or equivalent) on where to get the original distribution. b) accompany the distribution with the machine-readable source of the software. c) give non-standard binaries non-standard names, with instructions on where to get the original software distribution. d) make other distribution arrangements with the author. 4. You may modify and include the part of the software into any other software (possibly commercial). But some files in the distribution are not written by the author, so that they are not under these terms. For the list of those files and their copying conditions, see the file LEGAL. 5. The scripts and library files supplied as input to or produced as output from the software do not automatically fall under the copyright of the software, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this software. 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. minitar-1.1.0/docs/bsdl.txt0000644000004100000410000000231215061026725015623 0ustar www-datawww-dataRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. minitar-1.1.0/CONTRIBUTORS.md0000644000004100000410000000117615061026725015474 0ustar www-datawww-data# Contributors - Austin Ziegler created minitar, based on work originally written by Mauricio Fernández for rpa-base. Thanks to everyone who has contributed to minitar: - Akinori MUSHA (knu) - Antoine Toulme - Curtis Sampson - Daniel J. Berger - [@dearblue](https://github.com/dearblue) - [@inkstak](https://github.com/inkstak) - John Prince - Jorie Tappa - Kazuyoshi Kato - Kevin McDermott - Matthew Kent - Merten Falk - Michal Suchanek - Mike Furr - [@ooooooo-q](https://github.com/ooooooo-q) - Pete Fritchman - [@sorah](https://github.com/sorah) - Vijay ([@bv-vijay](https://github.com/bv-vijay)) - Yamamoto Kōhei - Zach Dennis minitar-1.1.0/README.md0000644000004100000410000000361215061026725014471 0ustar www-datawww-data# minitar - code :: - issues :: - changelog :: ## 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ández Pradier for the rpa-base project. ## Synopsis Using minitar is easy. The simplest case is: ```ruby require 'minitar' # Packs everything that matches Find.find('tests'). # test.tar will automatically be closed by Minitar.pack. Minitar.pack('tests', File.open('test.tar', 'wb')) # Unpacks 'test.tar' to 'x', creating 'x' if necessary. Minitar.unpack('test.tar', 'x') ``` A gzipped tar can be written with: ```ruby require 'zlib' # test.tgz will be closed automatically. Minitar.pack('tests', Zlib::GzipWriter.new(File.open('test.tgz', 'wb')) # test.tgz will be closed automatically. Minitar.unpack(Zlib::GzipReader.new(File.open('test.tgz', 'rb')), 'x') ``` As the case above shows, one need not write to a file. However, it will sometimes require that one dive a little deeper into the API, as in the case of StringIO objects. Note that I'm not providing a block with Minitar::Output, as Minitar::Output#close automatically closes both the Output object and the wrapped data stream object. ```ruby begin sgz = Zlib::GzipWriter.new(StringIO.new(String.new)) tar = Output.new(sgz) Find.find('tests') do |entry| Minitar.pack_file(entry, tar) end ensure # Closes both tar and sgz. tar.close end ``` ## Minitar and Security See [SECURITY](./SECURITY.md) ## minitar Semantic Versioning The minitar library uses a [Semantic Versioning][semver] scheme with one change: - When PATCH is zero (`0`), it will be omitted from version references. [semver]: http://semver.org/ minitar-1.1.0/CHANGELOG.md0000644000004100000410000002742215061026725015030 0ustar www-datawww-data# Changelog ## 1.1.0 / 2025-09-07 - Enhancements: - Support large file size encoded in base-256 encoding which is a GNU tar extension [#121][pull-121]. - Support large file size encoded in PAX extension header. [#121][pull-121]. - Bug fix: - Resolved [#62][issue-62]. The initial solution was developed with the assistance of Claude Sonnet 4 via Kiro, but nearly every line of the solution and tests were rewritten as part of a comprehensive review of all tests. - Breaking Change: - Removed `Minitar::PosixHeader.new_from_stream` which should have been removed with 1.0.0 and has been deprecated for a decade or so. - Governance: Changes described below are effective 2024-12-31. - Update gem management details to use markdown files for everything, enabled in part by [flavorjones/hoe-markdown][hoe-markdown]. Several files were renamed to be more consistent with standard practices. - Updated security notes with an [age][age] public key rather than pointing to Keybase.io and a PGP public key which I no longer use. The use of the [Tidelift security contact][tidelift] is recommended over direct disclosure. Changes described below are effective 2025-08-04. - Contributions to minitar now require a DCO certification. ## 1.0.2 / 2024-08-23 - Bug fix: - Minitar 1.0.1 was released with an unchanged gemspec. Reported by Debashish Biswas in [#65][issue-65]. ## 1.0.1 / 2024-08-08 - Bug fix: - Resolve a constant lookup issue. The accepted fix has been provided by Aram Price in [#58][issue-58]. ## 1.0.0 / 2024-08-07 - Breaking Changes: - Minimum Ruby version is 3.1. - The `Archive::Tar::Minitar` namespace has been completely removed and `Minitar` is a class instead of a module. - Enhancements: - Added `Minitar.pack_as_file`, originally proposed by John Prince back in 2011 [#7][issue-07]. ## 0.12.1 / 2024-08-21 - Reverted adbbb9b596 to restore compatibility with Ruby < 2.0. Resolves [#63][issue-63] reported by Robert Schulze. ## 0.12 / 2024-08-06 - Properly handle very long GNU filenames, resolving [#46][issue-46]. - Handle very long GNU filenames that are 512 or more bytes, resolving [#45][issue-45]. Originally implemented in [#47][pull-47] by Vijay, but accidentally closed. ## 0.11 / 2022-12-31 - symlink support is complete. Merged as PR [#42][pull-42], rebased and built on top of PR [#12][pull-12] by fetep. - kymmt90 fixed a documentation error on `Minitar.pack` in PR [#43][pull-43]. - This version is a soft-deprecation of all versions before Ruby 2.7, as they will no longer be tested in CI. ## 0.10 / 2022-03-26 - nevesenin fixed an issue with long filename handling. Merged as PR [#40][pull-40]. ## 0.9 / 2019-09-04 - jtappa added the ability to skip fsync with a new option to `Minitar.unpack` and `Minitar::Input#extract_entry`. Provide `:fsync => false` as the last parameter to enable. Merged from a modified version of PR [#37][pull-37]. ## 0.8 / 2019-01-05 - [@inkstak](https://github.com/inkstak) resolved an issue introduced in the fix for [#31][issue-31] by allowing spaces to be considered valid characters in strict octal handling. Octal conversion ignores leading spaces. Merged from a slightly modified version of PR [#35][pull-35]. - [@dearblue](https://github.com/dearblue) contributed PR [#32][pull-32] providing an explicit call to #bytesize for strings that include multibyte characters. The PR has been modified to be compatible with older versions of Ruby and extend tests. - Akinori MUSHA (knu) contributed PR [#36][pull-36] that treats certain badly encoded regular files (with names ending in `/`) as if they were directories on decode. ## 0.7 / 2018-02-19 - Fixed issue [#28][issue-28] with a modified version of PR [#29][pull-29] covering the security policy and position for `Minitar`. Thanks so much to [@ooooooo-q](https://github.com/ooooooo-q) for the report and an initial patch. Additional information was added as [#30][issue-30]. - [@dearblue](https://github.com/dearblue) contributed PR [#33][pull-33] providing a fix for `Minitar::Reader` when the IO-like object does not have a `#pos` method. - Kevin McDermott contributed PR [#34][pull-34] so that an InvalidTarStream is raised if the tar header is not valid, preventing incorrect streaming of files from a non-tarfile. This is a minor breaking change, so the version has been bumped accordingly. - Kazuyoshi Kato contributed PR [#26][pull-26] providing support for the GNU tar long filename extension. - Addressed a potential DOS with negative size fields in tar headers ([#31][issue-31]). This has been handled in two ways: the size field in a tar header is interpreted as a strict octal value and the `Minitar` reader will raise an InvalidTarStream if the size ends up being negative anyway. ## 0.6.1 / 2017-02-07 - Fixed issue [#24][issue-24] where streams were being improperly closed immediately on open unless there was a block provided. - Hopefully fixes issue [#23][issue-23] by releasing archive-tar-minitar after minitar-cli is available. ## 0.6 / 2017-02-07 - Breaking Changes: - Extracted `bin/minitar` into a new gem, `minitar-cli`. No, I am _not_ going to bump the major version for this. As far as I can tell, few people use the command-line utility anyway. (Installing `archive-tar-minitar` will install both `minitar` and `minitar-cli`, at least until version 1.0.) - `Minitar` extraction before 0.6 traverses directories if the tarball includes a relative directory reference, as reported in [#16][issue-16] by [@ecneladis](https://github.com/ecneladis). This has been disallowed entirely and will throw a `SecureRelativePathError` when found. Additionally, if the final destination of an entry is an already-existing symbolic link, the existing symbolic link will be removed and the file will be written correctly (on platforms that support symbolic links). - Enhancements: - Licence change. After speaking with Mauricio Fernández, we have changed the licensing of this library to Ruby and Simplified BSD and have dropped the GNU GPL license. This takes effect from the 0.6 release. - Printing a deprecation warning for including Archive::Tar to put `Minitar` in the top-level namespace. - Printing a deprecation warning for including `Archive::Tar::Minitar` into a class (`Minitar` will be a class for version 1.0). - Moved `Archive::Tar::PosixHeader` to `Archive::Tar::Minitar::PosixHeader` with a deprecation warning. Do not depend on `Archive::Tar::Minitar::PosixHeader`, as it will be moving to `::Minitar::PosixHeader` in a future release. - Added an alias, `::Minitar`, for `Archive::Tar::Minitar`, opted in with `require 'minitar'`. In future releases, this alias will be enabled by default, and the `Archive::Tar` namespace will be removed entirely for version 1.0. - Modified the handling of `mtime` in `PosixHeader` to do an integer conversion (`#to_i`) so that a Time object can be used instead of the integer value of the time object. - `Writer::RestrictedStream` was renamed to `Writer::WriteOnlyStream` for clarity. No alias or deprecation warning was provided for this as it is an internal implementation detail. - `Writer::BoundedStream` was renamed to `Writer::BoundedWriteStream` for clarity. A deprecation warning is provided on first use because a BoundedWriteStream may raise a `BoundedWriteStream::FileOverflow` exception. - `Writer::BoundedWriteStream::FileOverflow` has been renamed to `Writer::WriteBoundaryOverflow` and inherits from `StandardError` instead of `RuntimeError`. Note that for Ruby 2.0 or higher, an error will be raised when specifying `Writer::BoundedWriteStream::FileOverflow` because `Writer::BoundedWriteStream` has been declared a private constant. - Modified `Writer#add_file_simple` to accept the data for a file in `opts[:data]`. When `opts[:data]` is provided, a stream block must not be provided. Improved the documentation for this method. - Modified `Writer#add_file` to accept `opts[:data]` and transparently call `Writer#add_file_simple` in this case. - Methods that require blocks are no longer required, so the `Archive::Tar::Minitar::BlockRequired` exception has been removed with a warning (this may not work on Ruby 1.8). - Dramatically reduced the number of strings created when creating a POSIX tarball header. - Added a helper, `Input.each_entry` that iterates over each entry in an opened entry object. - Bugs: - Fix [#2][issue-02] to handle IO streams that are not seekable, such as pipes, `STDIN`, or `STDOUT`. - Fix [#3][issue-03] to make the test timezone resilient. - Fix [#4][issue-04] for supporting the reading of tar files with filenames in the GNU long filename extension format. Ported from [@atoulme](https://github.com/atoulme)’s fork, originally provided by Curtis Sampson. - Fix [#6][issue-06] by making it raise the correct error for a long filename with no path components. - Fix [#13][issue-13] provided by [@fetep](https://github.com/fetep) fixes an off-by-one error on filename splitting. - Fix [#14][issue-14] provided by [@kzys](https://github.com/kzys) should fix Windows detection issues. - Fix [#16][issue-16] as specified above. - Fix an issue where `Minitar.pack` would not include Unix hidden files when creating a tarball. - Development: - Modernized minitar tooling around Hoe. - Added travis and coveralls. ## 0.5.2 / 2008-02-26 - Bugs: - Fixed a Ruby 1.9 compatibility error. ## 0.5.1 / 2004-09-27 - Bugs: - Fixed a variable name error. ## 0.5.0 - Initial release. Does files and directories. Command does create, extract, and list. [age]: https://github.com/FiloSottile/age [hoe-halostatue]: https://github.com/halostatue/hoe-halostatue [hoe-markdown]: https://github.com/flavorjones/hoe-markdown [issue-02]: https://github.com/halostatue/minitar/issues/2 [issue-03]: https://github.com/halostatue/minitar/issues/3 [issue-04]: https://github.com/halostatue/minitar/issues/4 [issue-06]: https://github.com/halostatue/minitar/issues/6 [issue-07]: https://github.com/halostatue/minitar/issues/7 [issue-13]: https://github.com/halostatue/minitar/issues/13 [issue-14]: https://github.com/halostatue/minitar/issues/14 [issue-16]: https://github.com/halostatue/minitar/issues/16 [issue-23]: https://github.com/halostatue/minitar/issues/23 [issue-24]: https://github.com/halostatue/minitar/issues/24 [issue-28]: https://github.com/halostatue/minitar/issues/28 [issue-30]: https://github.com/halostatue/minitar/issues/30 [issue-31]: https://github.com/halostatue/minitar/issues/31 [issue-45]: https://github.com/halostatue/minitar/issues/45 [issue-46]: https://github.com/halostatue/minitar/issues/46 [issue-58]: https://github.com/halostatue/minitar/issues/58 [issue-62]: https://github.com/halostatue/minitar/issues/62 [issue-63]: https://github.com/halostatue/minitar/issues/63 [issue-65]: https://github.com/halostatue/minitar/issues/65 [pull-12]: https://github.com/halostatue/minitar/pull/12 [pull-26]: https://github.com/halostatue/minitar/pull/26 [pull-29]: https://github.com/halostatue/minitar/pull/29 [pull-32]: https://github.com/halostatue/minitar/pull/32 [pull-33]: https://github.com/halostatue/minitar/pull/33 [pull-34]: https://github.com/halostatue/minitar/pull/34 [pull-35]: https://github.com/halostatue/minitar/pull/35 [pull-36]: https://github.com/halostatue/minitar/pull/36 [pull-37]: https://github.com/halostatue/minitar/pull/37 [pull-40]: https://github.com/halostatue/minitar/pull/40 [pull-42]: https://github.com/halostatue/minitar/pull/42 [pull-43]: https://github.com/halostatue/minitar/pull/43 [pull-47]: https://github.com/halostatue/minitar/pull/47 [pull-121]: https://github.com/halostatue/minitar/pull/121 [tidelift]: https://tidelift.com/security minitar-1.1.0/LICENCE.md0000644000004100000410000000242315061026725014575 0ustar www-datawww-data# Licence - SPDX-License-Identifier: [Ruby][ruby-license] OR [BSD-2-Clause][bsd-2-clause] minitar is free software that may be redistributed and/or modified under the terms of Ruby’s licence or the Simplified BSD licence. - Copyright 2004–2025 Austin Ziegler and other contributors. - Portions copyright 2004 Mauricio Julio Fernández Pradier. ### Simplified BSD Licence See the file [bsdl.txt](docs/bsdl.txt) in the main distribution. ### Ruby's Licence See the file [ruby.txt](docs/ruby.txt) in the main distribution. ## Developer Certificate of Origin All contributors **must** certify they are willing and able to provide their contributions under the terms of this project's licences with the certification of the [Developer Certificate of Origin (Version 1.1)](licences/dco.txt). Such certification is provided by ensuring that a `Signed-off-by` [commit trailer][trailer] is present on every commit: Signed-off-by: FirstName LastName The `Signed-off-by` trailer can be automatically added by git with the `-s` or `--signoff` option on `git commit`: ```sh git commit --signoff ``` [bsd-2-clause]: https://spdx.org/licenses/BSD-2-Clause.html [ruby-license]: https://spdx.org/licenses/Ruby.html [trailer]: https://git-scm.com/docs/git-interpret-trailers