class Minitar::Input
This 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.
This notice applies to Minitar::Input.open, Minitar::Input.each_entry, and Minitar::Input.new.
Public Class Methods
Source
# File lib/minitar/input.rb, line 62 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
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.
Source
# File lib/minitar/input.rb, line 82 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 = Minitar::Reader.new(@io) 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.
Source
# File lib/minitar/input.rb, line 31 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
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.
Public Instance Methods
Source
# File lib/minitar/input.rb, line 207 def check_relative_path!(name) if %r{(?:\A|/)\.\.(?:/|\z)}.match?(name) raise Minitar::SecureRelativePathError, "Path contains '..'" end end
Raises Minitar::SecureRelativePathError if any path component of the (normalized) name is ...
Source
# File lib/minitar/input.rb, line 154 def close @io.close @tar.close end
Closes both the Minitar::Reader object and the wrapped data stream.
Source
# File lib/minitar/input.rb, line 148 def closed? = @io.closed? # Returns the Minitar::Reader object for direct access. attr_reader :tar # Closes both the Minitar::Reader object and the wrapped data stream. def close @io.close @tar.close end private # Normalizes an entry name as the filesystem will interpret it. # # On Unix-like systems (`File::ALT_SEPARATOR.nil?`), consecutive `/` are always # squeezed to a single `/`. # # On Windows, the normalization _and check_ are more extensive: # # - All `\` are normalized to `/` and squeezed. # - Drive designators `[A-Z]:` are rejected with UnsafeFileNameError. # - Files with `:` are rejected with UnsafeFileNameError (NTFS alternate data streams # are removed with `File.basename`, causing file aliasing). # - Each path component is examined to see if, aside from `..` and `.`, it contains # a trailing dot or space. These are rejected with UnsafeFileNameError due to file # aliasing. def normalize_entry_name(name, alt_separator = File::ALT_SEPARATOR) return name.squeeze("/") unless alt_separator name = name.tr(alt_separator, "/").squeeze("/") # A drive designator looks like a directory (`C:` is a drive, not a stream name), so # it is called out separately from the general `:` check below. if name.match?(/\A[A-Za-z]:/) raise Minitar::UnsafeFileNameError, "Path begins with a drive designator" end if name.include?(":") raise Minitar::UnsafeFileNameError, "Path contains ':'" end # Win32 trims trailing dots and spaces from path components, so `...`, `.. `, and # `dirname..` alias a different name (often the containing directory) than the # literal name in the archive. `.` and `..` are left alone: `.` is harmless and `..` # is handled by #check_relative_path!. name.split("/").each do |part| next if part == "." || part == ".." if part.match?(/[ .]\z/) raise Minitar::UnsafeFileNameError, "Path component ends with '.' or ' '" end end name end # Raises Minitar::SecureRelativePathError if any path component of the (normalized) # `name` is `..`. def check_relative_path!(name) if %r{(?:\A|/)\.\.(?:/|\z)}.match?(name) raise Minitar::SecureRelativePathError, "Path contains '..'" end end 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
Returns false if the wrapped data stream is open.
Source
# File lib/minitar/input.rb, line 99 def each_entry return to_enum unless block_given? @tar.each do |entry| yield entry end ensure @tar.rewind 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.
Source
# File lib/minitar/input.rb, line 223 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
Source
# File lib/minitar/input.rb, line 126 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; check and reject # relative path directories on both Windows and Unix-like systems. This is a hard # rejection by throwing exceptions. Future versions may permit relative paths as long # as the file does not leave `destdir`. full_name = normalize_entry_name(entry.full_name) check_relative_path!(full_name) 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
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: Theentryis a directory. -
:file_start: Theentryis a file; the extract of the file is just beginning. -
:file_progress: Yielded every 4096 bytes during the extract of theentry. -
:file_done: Yielded when theentryis completed.
The stats hash may contain the following keys:
-
:current: The current total number of bytes read in theentry. -
:currinc: The current number of bytes read in this read cycle. -
:entry: The entry being extracted; this is a Minitar::Reader::EntryStream.
Source
# File lib/minitar/input.rb, line 247 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
Source
# File lib/minitar/input.rb, line 213 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
Source
# File lib/minitar/input.rb, line 175 def normalize_entry_name(name, alt_separator = File::ALT_SEPARATOR) return name.squeeze("/") unless alt_separator name = name.tr(alt_separator, "/").squeeze("/") # A drive designator looks like a directory (`C:` is a drive, not a stream name), so # it is called out separately from the general `:` check below. if name.match?(/\A[A-Za-z]:/) raise Minitar::UnsafeFileNameError, "Path begins with a drive designator" end if name.include?(":") raise Minitar::UnsafeFileNameError, "Path contains ':'" end # Win32 trims trailing dots and spaces from path components, so `...`, `.. `, and # `dirname..` alias a different name (often the containing directory) than the # literal name in the archive. `.` and `..` are left alone: `.` is harmless and `..` # is handled by #check_relative_path!. name.split("/").each do |part| next if part == "." || part == ".." if part.match?(/[ .]\z/) raise Minitar::UnsafeFileNameError, "Path component ends with '.' or ' '" end end name end
Normalizes an entry name as the filesystem will interpret it.
On Unix-like systems (File::ALT_SEPARATOR.nil?), consecutive / are always squeezed to a single /.
On Windows, the normalization and check are more extensive:
-
All
\are normalized to/and squeezed. -
Drive designators
[A-Z]:are rejected with UnsafeFileNameError. -
Files with
:are rejected with UnsafeFileNameError (NTFS alternate data streams are removed withFile.basename, causing file aliasing). -
Each path component is examined to see if, aside from
..and., it contains a trailing dot or space. These are rejected with UnsafeFileNameError due to file aliasing.