class Minitar::Input
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 63 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 83 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
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 34 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 165 def close @io.close @tar.close end
Closes both the Reader object and the wrapped data stream.
Source
# File lib/minitar/input.rb, line 159 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
Returns false if the wrapped data stream is open.
Source
# File lib/minitar/input.rb, line 100 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 182 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 130 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
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.
Source
# File lib/minitar/input.rb, line 206 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 172 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