archive-tar-minitar-0.5.2/0000775000175000017500000000000011624477521015647 5ustar formorerformorerarchive-tar-minitar-0.5.2/bin/0000775000175000017500000000000011624477521016417 5ustar formorerformorerarchive-tar-minitar-0.5.2/bin/minitar0000644000175000017500000000141111624477521020000 0ustar formorerformorer#!/usr/bin/env ruby #-- # Archive::Tar::Minitar 0.5.2 # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler # # This program is based on and incorporates parts of RPA::Package from # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been # adapted to be more generic by Austin. # # It is licensed under the GNU General Public Licence or Ruby's licence. # # $Id: minitar 213 2008-02-26 22:32:11Z austin $ #++ # 1) Try to load Archive::Tar::Minitar from the gem. # 2) Try to load Archive::Tar::Minitar from $LOAD_PATH. begin require 'rubygems' require_gem 'archive-tar-minitar', '= 0.5.2' rescue LoadError nil end require 'archive/tar/minitar' require 'archive/tar/minitar/command' exit Archive::Tar::Minitar::Command.run(ARGV) archive-tar-minitar-0.5.2/Install0000644000175000017500000000031011624477521017170 0ustar formorerformorerInstalling this package is as simple as: % ruby install.rb Alternatively, you can use the RubyGem version of Archive::Tar::Minitar available as archive-tar-minitar-0.5.2.gem from the usual sources. archive-tar-minitar-0.5.2/metadata.yml0000664000175000017500000000321611624477521020154 0ustar formorerformorer--- !ruby/object:Gem::Specification name: archive-tar-minitar version: !ruby/object:Gem::Version version: 0.5.2 platform: ruby authors: - Austin Ziegler, Mauricio Ferna'ndez autorequire: bindir: bin cert_chain: [] date: 2008-02-26 00:00:00 -05:00 default_executable: dependencies: [] description: Archive::Tar::Minitar is a pure-Ruby library and command-line utility that provides the ability to deal with POSIX tar(1) archive files. The implementation is based heavily on Mauricio Ferna'ndez's implementation in rpa-base, but has been reorganised to promote reuse in other projects. email: minitar@halostatue.ca executables: - minitar extensions: [] extra_rdoc_files: - README - ChangeLog - Install files: - bin - bin/minitar - ChangeLog - Install - lib - lib/archive - lib/archive/tar - lib/archive/tar/minitar - lib/archive/tar/minitar/command.rb - lib/archive/tar/minitar.rb - Rakefile - README - tests - tests/tc_tar.rb - tests/testall.rb has_rdoc: true homepage: http://rubyforge.org/projects/ruwiki/ post_install_message: rdoc_options: - --title - Archive::Tar::MiniTar -- A POSIX tarchive library - --main - README - --line-numbers require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.8.2 version: required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: "0" version: requirements: [] rubyforge_project: ruwiki rubygems_version: 1.0.1 signing_key: specification_version: 2 summary: Provides POSIX tarchive management from Ruby programs. test_files: - tests/testall.rb archive-tar-minitar-0.5.2/lib/0000775000175000017500000000000011624477521016415 5ustar formorerformorerarchive-tar-minitar-0.5.2/lib/archive/0000775000175000017500000000000011624477521020036 5ustar formorerformorerarchive-tar-minitar-0.5.2/lib/archive/tar/0000775000175000017500000000000011624477521020624 5ustar formorerformorerarchive-tar-minitar-0.5.2/lib/archive/tar/minitar/0000775000175000017500000000000011624477521022267 5ustar formorerformorerarchive-tar-minitar-0.5.2/lib/archive/tar/minitar/command.rb0000644000175000017500000005110511624477521024232 0ustar formorerformorer#!/usr/bin/env ruby #-- # Archive::Tar::Baby 0.5.2 # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler # This is free software with ABSOLUTELY NO WARRANTY. # # This program is based on and incorporates parts of RPA::Package from # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been # adapted to be more generic by Austin. # # This file contains an adaptation of Ruby/ProgressBar by Satoru # Takabayashi , copyright 2001 - 2004. # # It is licensed under the GNU General Public Licence or Ruby's licence. # # $Id: command.rb 213 2008-02-26 22:32:11Z austin $ #++ require 'zlib' # TODO: add # TODO: delete ??? require 'optparse' require 'ostruct' require 'fileutils' module Archive::Tar::Minitar::Command class ProgressBar VERSION = "0.8" attr_accessor :total attr_accessor :title def initialize (title, total, out = STDERR) @title = title @total = total @out = out @bar_width = 80 @bar_mark = "o" @current = 0 @previous = 0 @is_finished = false @start_time = Time.now @previous_time = @start_time @title_width = 14 @format = "%-#{@title_width}s %3d%% %s %s" @format_arguments = [:title, :percentage, :bar, :stat] show end private def convert_bytes (bytes) if bytes < 1024 sprintf("%6dB", bytes) elsif bytes < 1024 * 1000 # 1000kb sprintf("%5.1fKB", bytes.to_f / 1024) elsif bytes < 1024 * 1024 * 1000 # 1000mb sprintf("%5.1fMB", bytes.to_f / 1024 / 1024) else sprintf("%5.1fGB", bytes.to_f / 1024 / 1024 / 1024) end end def transfer_rate bytes_per_second = @current.to_f / (Time.now - @start_time) sprintf("%s/s", convert_bytes(bytes_per_second)) end def bytes convert_bytes(@current) end def format_time (t) t = t.to_i sec = t % 60 min = (t / 60) % 60 hour = t / 3600 sprintf("%02d:%02d:%02d", hour, min, sec); end # ETA stands for Estimated Time of Arrival. def eta if @current == 0 "ETA: --:--:--" else elapsed = Time.now - @start_time eta = elapsed * @total / @current - elapsed; sprintf("ETA: %s", format_time(eta)) end end def elapsed elapsed = Time.now - @start_time sprintf("Time: %s", format_time(elapsed)) end def stat if @is_finished then elapsed else eta end end def stat_for_file_transfer if @is_finished then sprintf("%s %s %s", bytes, transfer_rate, elapsed) else sprintf("%s %s %s", bytes, transfer_rate, eta) end end def eol if @is_finished then "\n" else "\r" end end def bar len = percentage * @bar_width / 100 sprintf("|%s%s|", @bar_mark * len, " " * (@bar_width - len)) end def percentage(value = nil) if @total.zero? 100 else (value || @current) * 100 / @total end end def title @title[0,(@title_width - 1)] + ":" end def get_width # FIXME: I don't know how portable it is. default_width = 80 # begin # tiocgwinsz = 0x5413 # data = [0, 0, 0, 0].pack("SSSS") # if @out.ioctl(tiocgwinsz, data) >= 0 then # rows, cols, xpixels, ypixels = data.unpack("SSSS") # if cols >= 0 then cols else default_width end # else # default_width # end # rescue Exception # default_width # end end def show arguments = @format_arguments.map {|method| send(method) } line = sprintf(@format, *arguments) width = get_width if line.length == width - 1 @out.print(line + eol) elsif line.length >= width @bar_width = [@bar_width - (line.length - width + 1), 0].max if @bar_width == 0 then @out.print(line + eol) else show end else # line.length < width - 1 @bar_width += width - line.length + 1 show end @previous_time = Time.now end def show_progress if @total.zero? cur_percentage = 100 prev_percentage = 0 else cur_percentage = (@current * 100 / @total).to_i prev_percentage = (@previous * 100 / @total).to_i end if cur_percentage > prev_percentage || Time.now - @previous_time >= 1 || @is_finished show end end public def file_transfer_mode @format_arguments = [:title, :percentage, :bar, :stat_for_file_transfer] end def format= (format) @format = format end def format_arguments= (arguments) @format_arguments = arguments end def finish @current = @total @is_finished = true show_progress end def halt @is_finished = true show_progress end def set (count) if count < 0 || count > @total raise "invalid count: #{count} (total: #{@total})" end @current = count show_progress @previous = @current end def inc (step = 1) @current += step @current = @total if @current > @total show_progress @previous = @current end def inspect "(ProgressBar: #{@current}/#{@total})" end end class CommandPattern class AbstractCommandError < Exception; end class UnknownCommandError < RuntimeError; end class CommandAlreadyExists < RuntimeError; end class << self def add(command) command = command.new if command.kind_of?(Class) @commands ||= {} if @commands.has_key?(command.name) raise CommandAlreadyExists else @commands[command.name] = command end if command.respond_to?(:altname) unless @commands.has_key?(command.altname) @commands[command.altname] = command end end end def <<(command) add(command) end attr_accessor :default def default=(command) #:nodoc: if command.kind_of?(CommandPattern) @default = command elsif command.kind_of?(Class) @default = command.new elsif @commands.has_key?(command) @default = @commands[command] else raise UnknownCommandError end end def command?(command) @commands.has_key?(command) end def command(command) if command?(command) @commands[command] else @default end end def [](cmd) self.command(cmd) end def default_ioe(ioe = {}) ioe[:input] ||= $stdin ioe[:output] ||= $stdout ioe[:error] ||= $stderr ioe end end def [](args, opts = {}, ioe = {}) call(args, opts, ioe) end def name raise AbstractCommandError end def call(args, opts = {}, ioe = {}) raise AbstractCommandError end def help raise AbstractCommandError end end class CommandHelp < CommandPattern def name "help" end def call(args, opts = {}, ioe = {}) ioe = CommandPattern.default_ioe(ioe) help_on = args.shift if CommandPattern.command?(help_on) ioe[:output] << CommandPattern[help_on].help elsif help_on == "commands" ioe[:output] << <<-EOH The commands known to minitar are: minitar create Creates a new tarfile. minitar extract Extracts files from a tarfile. minitar list Lists files in the tarfile. All commands accept the options --verbose and --progress, which are mutually exclusive. In "minitar list", --progress means the same as --verbose. --verbose, -V Performs the requested command verbosely. --progress, -P Shows a progress bar, if appropriate, for the action being performed. EOH else ioe[:output] << "Unknown command: #{help_on}\n" unless help_on.nil? or help_on.empty? ioe[:output] << self.help end 0 end def help help = <<-EOH This is a basic help message containing pointers to more information on how to use this command-line tool. Try: minitar help commands list all 'minitar' commands minitar help show help on (e.g., 'minitar help create') EOH end # minitar add Adds a file to an existing tarfile. # minitar delete Deletes a file from an existing tarfile. end class CommandCreate < CommandPattern def name "create" end def altname "cr" end def call(args, opts = {}, ioe = {}) argv = [] while (arg = args.shift) case arg when '--compress', '-z' opts[:compress] = true else argv << arg end end if argv.size < 2 ioe[:output] << "Not enough arguments.\n\n" CommandPattern["help"][["create"]] return 255 end output = argv.shift if '-' == output opts[:name] = "STDOUT" output = ioe[:output] opts[:output] = ioe[:error] else opts[:name] = output output = File.open(output, "wb") opts[:output] = ioe[:output] end if opts[:name] =~ /\.tar\.gz$|\.tgz$/ or opts[:compress] output = Zlib::GzipWriter.new(output) end files = [] if argv.include?("--") # Read stdin for the list of files. files = "" files << ioe[:input].read while not ioe[:input].eof? files = files.split(/\r\n|\n|\r/) args.delete("--") end files << argv.to_a files.flatten! if opts[:verbose] watcher = lambda do |action, name, stats| opts[:output] << "#{name}\n" if action == :dir or action == :file_done end finisher = lambda { opts[:output] << "\n" } elsif opts[:progress] progress = ProgressBar.new(opts[:name], 1) watcher = lambda do |action, name, stats| case action when :file_start, :dir progress.title = File.basename(name) if action == :dir progress.total += 1 progress.inc else progress.total += stats[:size] end when :file_progress progress.inc(stats[:currinc]) end end finisher = lambda do progress.title = opts[:name] progress.finish end else watcher = nil finisher = lambda { } end Archive::Tar::Minitar.pack(files, output, &watcher) finisher.call 0 ensure output.close if output and not output.closed? end def help help = <<-EOH minitar create [OPTIONS] + Creates a new tarfile. If the tarfile is named .tar.gz or .tgz, then it will be compressed automatically. If the tarfile is "-", then it will be output to standard output (stdout) so that minitar may be piped. The files or directories that will be packed into the tarfile are specified after the name of the tarfile itself. Directories will be processed recursively. If the token "--" is found in the list of files to be packed, additional filenames will be read from standard input (stdin). If any file is not found, the packaging will be halted. create Options: --compress, -z Compresses the tarfile with gzip. EOH end end class CommandExtract < CommandPattern def name "extract" end def altname "ex" end def call(args, opts = {}, ioe = {}) argv = [] output = nil dest = "." files = [] while (arg = args.shift) case arg when '--uncompress', '-z' opts[:uncompress] = true when '--pipe' opts[:output] = ioe[:error] output = ioe[:output] when '--output', '-o' dest = args.shift else argv << arg end end if argv.size < 1 ioe[:output] << "Not enough arguments.\n\n" CommandPattern["help"][["extract"]] return 255 end input = argv.shift if '-' == input opts[:name] = "STDIN" input = ioe[:input] else opts[:name] = input input = File.open(input, "rb") end if opts[:name] =~ /\.tar\.gz$|\.tgz$/ or opts[:uncompress] input = Zlib::GzipReader.new(input) end files << argv.to_a files.flatten! if opts[:verbose] watcher = lambda do |action, name, stats| opts[:output] << "#{name}\n" if action == :dir or action == :file_done end finisher = lambda { opts[:output] << "\n" } elsif opts[:progress] progress = ProgressBar.new(opts[:name], 1) watcher = lambda do |action, name, stats| case action when :file_start, :dir progress.title = File.basename(name) if action == :dir progress.total += 1 progress.inc else progress.total += stats[:entry].size end when :file_progress progress.inc(stats[:currinc]) end end finisher = lambda do progress.title = opts[:name] progress.finish end else watcher = nil finisher = lambda { } end if output.nil? Archive::Tar::Minitar.unpack(input, dest, files, &watcher) finisher.call else Archive::Tar::Minitar::Input.open(input) do |inp| inp.each do |entry| stats = { :mode => entry.mode, :mtime => entry.mtime, :size => entry.size, :gid => entry.gid, :uid => entry.uid, :current => 0, :currinc => 0, :entry => entry } if files.empty? or files.include?(entry.full_name) if entry.directory? puts "Directory: #{entry.full_name}" watcher[:dir, dest, stats] unless watcher.nil? else puts "File: #{entry.full_name}" watcher[:file_start, destfile, stats] unless watcher.nil? loop do data = entry.read(4096) break unless data stats[:currinc] = output.write(data) stats[:current] += stats[:currinc] watcher[:file_progress, name, stats] unless watcher.nil? end watcher[:file_done, name, stats] unless watcher.nil? end end end end end 0 end def help help = <<-EOH minitar extract [OPTIONS] [+] Extracts files from an existing tarfile. If the tarfile is named .tar.gz or .tgz, then it will be uncompressed automatically. If the tarfile is "-", then it will be read from standard input (stdin) so that minitar may be piped. The files or directories that will be extracted from the tarfile are specified after the name of the tarfile itself. Directories will be processed recursively. Files must be specified in full. A file "foo/bar/baz.txt" cannot simply be specified by specifying "baz.txt". Any file not found will simply be skipped and an error will be reported. extract Options: --uncompress, -z Uncompresses the tarfile with gzip. --pipe Emits the extracted files to STDOUT for piping. --output, -o Extracts the files to the specified directory. EOH end end class CommandList < CommandPattern def name "list" end def altname "ls" end def modestr(mode) s = "---" s[0] = ?r if (mode & 4) == 4 s[1] = ?w if (mode & 2) == 2 s[2] = ?x if (mode & 1) == 1 s end def call(args, opts = {}, ioe = {}) argv = [] output = nil dest = "." files = [] opts[:field] = "name" while (arg = args.shift) case arg when '--sort', '-S' opts[:sort] = true opts[:field] = args.shift when '--reverse', '-R' opts[:reverse] = true opts[:sort] = true when '--uncompress', '-z' opts[:uncompress] = true when '-l' opts[:verbose] = true else argv << arg end end if argv.size < 1 ioe[:output] << "Not enough arguments.\n\n" CommandPattern["help"][["list"]] return 255 end input = argv.shift if '-' == input opts[:name] = "STDIN" input = ioe[:input] else opts[:name] = input input = File.open(input, "rb") end if opts[:name] =~ /\.tar\.gz$|\.tgz$/ or opts[:uncompress] input = Zlib::GzipReader.new(input) end files << argv.to_a files.flatten! if opts[:verbose] or opts[:progress] format = "%10s %4d %8s %8s %8d %12s %s" datefmt = "%b %d %Y" timefmt = "%b %d %H:%M" fields = %w(permissions inodes user group size date fullname) else format = "%s" fields = %w(fullname) end opts[:field] = opts[:field].intern opts[:field] = :full_name if opts[:field] == :name output = [] Archive::Tar::Minitar::Input.open(input) do |inp| today = Time.now oneyear = Time.mktime(today.year - 1, today.month, today.day) inp.each do |entry| value = format % fields.map do |ff| case ff when "permissions" s = entry.directory? ? "d" : "-" s << modestr(entry.mode / 0100) s << modestr(entry.mode / 0010) s << modestr(entry.mode) when "inodes" entry.size / 512 when "user" entry.uname || entry.uid || 0 when "group" entry.gname || entry.gid || 0 when "size" entry.size when "date" if Time.at(entry.mtime) > (oneyear) Time.at(entry.mtime).strftime(timefmt) else Time.at(entry.mtime).strftime(datefmt) end when "fullname" entry.full_name end end if opts[:sort] output << [entry.send(opts[:field]), value] else ioe[:output] << value << "\n" end end end if opts[:sort] output = output.sort { |a, b| a[0] <=> b[0] } if opts[:reverse] output.reverse_each { |oo| ioe[:output] << oo[1] << "\n" } else output.each { |oo| ioe[:output] << oo[1] << "\n" } end end 0 end def help help = <<-EOH minitar list [OPTIONS] [+] Lists files in an existing tarfile. If the tarfile is named .tar.gz or .tgz, then it will be uncompressed automatically. If the tarfile is "-", then it will be read from standard input (stdin) so that minitar may be piped. If --verbose or --progress is specified, then the file list will be similar to that produced by the Unix command "ls -l". list Options: --uncompress, -z Uncompresses the tarfile with gzip. --sort [], -S Sorts the list of files by the specified field. The sort defaults to the filename. --reverse, -R Reverses the sort. -l Lists the files in detail. Sort Fields: name, mtime, size EOH end end CommandPattern << CommandHelp CommandPattern << CommandCreate CommandPattern << CommandExtract CommandPattern << CommandList # CommandPattern << CommandAdd # CommandPattern << CommandDelete def self.run(argv, input = $stdin, output = $stdout, error = $stderr) ioe = { :input => input, :output => output, :error => error, } opts = { } if argv.include?("--version") output << <<-EOB minitar #{Archive::Tar::Minitar::VERSION} Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler This is free software with ABSOLUTELY NO WARRANTY. see http://rubyforge.org/projects/ruwiki for more information EOB end if argv.include?("--verbose") or argv.include?("-V") opts[:verbose] = true argv.delete("--verbose") argv.delete("-V") end if argv.include?("--progress") or argv.include?("-P") opts[:progress] = true opts[:verbose] = false argv.delete("--progress") argv.delete("-P") end command = CommandPattern[(argv.shift or "").downcase] command ||= CommandPattern["help"] return command[argv, opts, ioe] end end archive-tar-minitar-0.5.2/lib/archive/tar/minitar.rb0000644000175000017500000007666511624477521022636 0ustar formorerformorer#!/usr/bin/env ruby #-- # Archive::Tar::Minitar 0.5.2 # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler # # This program is based on and incorporates parts of RPA::Package from # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been # adapted to be more generic by Austin. # # It is licensed under the GNU General Public Licence or Ruby's licence. # # $Id: minitar.rb 213 2008-02-26 22:32:11Z austin $ #++ module Archive; end module Archive::Tar; end # = Archive::Tar::PosixHeader # 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]; // ditto a8 /A8 # char gid[8]; // ditto 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+ may be one of the following known values: # # "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. # # POSIX indicates that "A POSIX-compliant implementation must treat any # unrecognized typeflag value as a regular file." class Archive::Tar::PosixHeader FIELDS = %w(name mode uid gid size mtime checksum typeflag linkname) + %w(magic version uname gname devmajor devminor prefix) FIELDS.each { |field| attr_reader field.intern } HEADER_PACK_FORMAT = "a100a8a8a8a12a12a7aaa100a6a2a32a32a8a8a155" HEADER_UNPACK_FORMAT = "Z100A8A8A8A12A12A8aZ100A6A2Z32Z32A8A8Z155" # Creates a new PosixHeader from a data stream. def self.new_from_stream(stream) data = stream.read(512) fields = data.unpack(HEADER_UNPACK_FORMAT) name = fields.shift mode = fields.shift.oct uid = fields.shift.oct gid = fields.shift.oct size = fields.shift.oct 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 == "\0" * 512) 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) end # Creates a new PosixHeader. A PosixHeader cannot be created unless the # #name, #size, #prefix, and #mode are provided. def initialize(vals) unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] raise ArgumentError end vals[:mtime] ||= 0 vals[:checksum] ||= "" vals[:typeflag] ||= "0" vals[:magic] ||= "ustar" vals[:version] ||= "00" FIELDS.each do |field| instance_variable_set("@#{field}", vals[field.intern]) end @empty = vals[:empty] end def empty? @empty end def to_s update_checksum header(@checksum) end # Update the checksum field. def update_checksum hh = header(" " * 8) @checksum = oct(calculate_checksum(hh), 6) end private 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 { |aa, bb| aa + bb } 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" * ((512 - str.size) % 512) end end require 'fileutils' require 'find' # = Archive::Tar::Minitar 0.5.2 # Archive::Tar::Minitar is a pure-Ruby library and command-line # utility that provides the ability to deal with POSIX tar(1) archive # files. The implementation is based heavily on Mauricio Ferna'ndez's # implementation in rpa-base, but has been reorganised to promote # reuse in other projects. # # This tar class performs a subset of all tar (POSIX tape archive) # operations. We can only deal with typeflags 0, 1, 2, and 5 (see # Archive::Tar::PosixHeader). All other typeflags will be treated as # normal files. # # NOTE::: support for typeflags 1 and 2 is not yet implemented in this # version. # # This release is version 0.5.2. The library can only handle files and # directories at this point. A future version will be expanded to # handle symbolic links and hard links in a portable manner. The # command line utility, minitar, can only create archives, extract # from archives, and list archive contents. # # == Synopsis # Using this library is easy. The simplest case is: # # require 'zlib' # require 'archive/tar/minitar' # include Archive::Tar # # # Packs everything that matches Find.find('tests') # File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) } # # Unpacks 'test.tar' to 'x', creating 'x' if necessary. # Minitar.unpack('test.tar', 'x') # # A gzipped tar can be written with: # # tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb')) # # Warning: tgz will be closed! # Minitar.pack('tests', tgz) # # tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb')) # # Warning: tgz will be closed! # Minitar.unpack(tgz, '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 = Output.new(sgz) # Find.find('tests') do |entry| # Minitar.pack_file(entry, tar) # end # ensure # # Closes both tar and sgz. # tar.close # end # # == Copyright # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler # # This program is based on and incorporates parts of RPA::Package from # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and # has been adapted to be more generic by Austin. # # 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru # Takabayashi , copyright 2001 - 2004. # # This program is free software. It may be redistributed and/or # modified under the terms of the GPL version 2 (or later) or Ruby's # licence. module Archive::Tar::Minitar VERSION = "0.5.2" # The exception raised when a wrapped data stream class is expected to # respond to #rewind or #pos but does not. class NonSeekableStream < StandardError; end # The exception raised when a block is required for proper operation of # the method. class BlockRequired < ArgumentError; end # The exception raised when operations are performed on a stream that has # previously been closed. class ClosedStream < StandardError; end # The exception raised when a filename exceeds 256 bytes in length, # the maximum supported by the standard Tar format. class FileNameTooLong < StandardError; end # The exception raised when a data stream ends before the amount of data # expected in the archive's PosixHeader. class UnexpectedEOF < StandardError; end # The class that writes a tar format archive to a data stream. class Writer # 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 RestrictedStream def initialize(anIO) @io = anIO end def write(data) @io.write(data) end end # A RestrictedStream that also has a size limit. class BoundedStream < Archive::Tar::Minitar::Writer::RestrictedStream # The exception raised when the user attempts to write more data to # a BoundedStream than has been allocated. class FileOverflow < RuntimeError; end # 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) raise FileOverflow if (data.size + @written) > @limit @io.write(data) @written += data.size data.size end end # 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. def self.open(anIO) writer = Writer.new(anIO) return writer unless block_given? begin res = yield writer ensure writer.close end res end # Creates and returns a new Writer object. def initialize(anIO) @io = anIO @closed = false end # Adds a file to the archive as +name+. +opts+ must contain the # following values: # # :mode:: The Unix file permissions mode value. # :size:: The size, in bytes. # # +opts+ may contain the following values: # # :uid: The Unix file owner user ID number. # :gid: The Unix file owner group ID number. # :mtime:: The *integer* modification time value. # # It will not be possible to add more than opts[:size] bytes # to the file. def add_file_simple(name, opts = {}) # :yields BoundedStream: raise Archive::Tar::Minitar::BlockRequired unless block_given? raise Archive::Tar::ClosedStream if @closed name, prefix = split_name(name) header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime], :size => opts[:size], :gid => opts[:gid], :uid => opts[:uid], :prefix => prefix } header = Archive::Tar::PosixHeader.new(header).to_s @io.write(header) os = BoundedStream.new(@io, opts[:size]) yield os # FIXME: what if an exception is raised in the block? min_padding = opts[:size] - os.written @io.write("\0" * min_padding) remainder = (512 - (opts[:size] % 512)) % 512 @io.write("\0" * remainder) end # Adds a file to the archive as +name+. +opts+ must contain the # following value: # # :mode:: The Unix file permissions mode value. # # +opts+ may contain the following values: # # :uid: The Unix file owner user ID number. # :gid: The Unix file owner group ID number. # :mtime:: The *integer* modification time value. # # The file's size will be determined from the amount of data written # to the stream. # # For #add_file to be used, the Archive::Tar::Minitar::Writer must be # wrapping a stream object that is seekable (e.g., it responds to # #pos=). Otherwise, #add_file_simple must be used. # # +opts+ may be modified during the writing to the stream. def add_file(name, opts = {}) # :yields RestrictedStream, +opts+: raise Archive::Tar::Minitar::BlockRequired unless block_given? raise Archive::Tar::Minitar::ClosedStream if @closed raise Archive::Tar::Minitar::NonSeekableStream unless @io.respond_to?(:pos=) name, prefix = split_name(name) init_pos = @io.pos @io.write("\0" * 512) # placeholder for the header yield RestrictedStream.new(@io), opts # FIXME: what if an exception is raised in the block? size = @io.pos - (init_pos + 512) remainder = (512 - (size % 512)) % 512 @io.write("\0" * remainder) final_pos = @io.pos @io.pos = init_pos header = { :name => name, :mode => opts[:mode], :mtime => opts[:mtime], :size => size, :gid => opts[:gid], :uid => opts[:uid], :prefix => prefix } header = Archive::Tar::PosixHeader.new(header).to_s @io.write(header) @io.pos = final_pos end # Creates a directory in the tar. def mkdir(name, opts = {}) raise ClosedStream if @closed name, prefix = split_name(name) header = { :name => name, :mode => opts[:mode], :typeflag => "5", :size => 0, :gid => opts[:gid], :uid => opts[:uid], :mtime => opts[:mtime], :prefix => prefix } header = Archive::Tar::PosixHeader.new(header).to_s @io.write(header) 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 # Closes the Writer. def close return if @closed @io.write("\0" * 1024) @closed = true end private def split_name(name) raise FileNameTooLong if name.size > 256 if name.size <= 100 prefix = "" else parts = name.split(/\//) newname = parts.pop nxt = "" loop do nxt = parts.pop break if newname.size + 1 + nxt.size > 100 newname = "#{nxt}/#{newname}" end prefix = (parts + [nxt]).join("/") name = newname raise FileNameTooLong if name.size > 100 || prefix.size > 155 end return name, prefix end end # 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 # This marks the EntryStream closed for reading without closing the # actual data stream. module InvalidEntryStream def read(len = nil); raise ClosedStream; end def getc; raise ClosedStream; end def rewind; raise ClosedStream; end end # EntryStreams are pseudo-streams on top of the main data stream. class EntryStream Archive::Tar::PosixHeader::FIELDS.each do |field| attr_reader field.intern end def initialize(header, anIO) @io = anIO @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 = @io.pos 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.size 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? @typeflag == "5" end alias_method :directory, :directory? # Returns +true+ if the entry represents a plain file. def file? @typeflag == "0" end alias_method :file, :file? # Returns +true+ if the current read pointer is at the end of the # EntryStream data. def eof? @read >= @size end # Returns the current read pointer in the EntryStream. def pos @read end # Sets the current read pointer to the beginning of the EntryStream. def rewind raise NonSeekableStream unless @io.respond_to?(:pos=) @io.pos = @orig_pos @read = 0 end def bytes_read @read end # Returns the full and proper name of the entry. def full_name if @prefix != "" File.join(@prefix, @name) else @name end end # Closes the entry. def close invalidate end 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(anIO) reader = Reader.new(anIO) return reader unless block_given? begin res = yield reader ensure reader.close end res end # Creates and returns a new Reader object. def initialize(anIO) @io = anIO @init_pos = anIO.pos end # Iterates through each entry in the data stream. def each(&block) each_entry(&block) 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 == 0 raise NonSeekableStream unless @io.respond_to?(:rewind) @io.rewind else raise NonSeekableStream unless @io.respond_to?(:pos=) @io.pos = @init_pos end end # Iterates through each entry in the data stream. def each_entry loop do return if @io.eof? header = Archive::Tar::PosixHeader.new_from_stream(@io) return if header.empty? entry = EntryStream.new(header, @io) size = entry.size yield entry skip = (512 - (size % 512)) % 512 if @io.respond_to?(:seek) # avoid reading... @io.seek(size - entry.bytes_read, IO::SEEK_CUR) else pending = size - entry.bytes_read while pending > 0 bread = @io.read([pending, 4096].min).size 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 def close end end # Wraps a Archive::Tar::Minitar::Reader with convenience methods and # wrapped stream management; Input only works with random access data # streams. See Input::new for details. class Input include Enumerable # With no associated block, +Input::open+ is a synonym for # +Input::new+. If the optional code block is given, it will be passed # the new _writer_ as an argument and the Input object will # automatically be closed when the block terminates. In this instance, # +Input::open+ returns the value of the block. def self.open(input) stream = Input.new(input) return stream unless block_given? begin res = yield stream ensure stream.close end res 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. def initialize(input) if input.respond_to?(:read) @io = input else @io = open(input, "rb") end @tarreader = Archive::Tar::Minitar::Reader.new(@io) end # Iterates through each entry and rewinds to the beginning of the stream # when finished. def each(&block) @tarreader.each { |entry| yield entry } ensure @tarreader.rewind 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. def extract_entry(destdir, entry) # :yields action, name, stats: stats = { :current => 0, :currinc => 0, :entry => entry } if entry.directory? dest = File.join(destdir, entry.full_name) yield :dir, entry.full_name, stats if block_given? if Archive::Tar::Minitar.dir?(dest) begin FileUtils.chmod(entry.mode, dest) rescue Exception nil end else FileUtils.mkdir_p(dest, :mode => entry.mode) FileUtils.chmod(entry.mode, dest) end fsync_dir(dest) fsync_dir(File.join(dest, "..")) return else # it's a file destdir = File.join(destdir, File.dirname(entry.full_name)) FileUtils.mkdir_p(destdir, :mode => 0755) destfile = File.join(destdir, File.basename(entry.full_name)) FileUtils.chmod(0600, destfile) rescue nil # Errno::ENOENT yield :file_start, entry.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, entry.full_name, stats if block_given? end os.fsync end FileUtils.chmod(entry.mode, destfile) fsync_dir(File.dirname(destfile)) fsync_dir(File.join(File.dirname(destfile), "..")) yield :file_done, entry.full_name, stats if block_given? end end # Returns the Reader object for direct access. def tar @tarreader end # Closes the Reader object and the wrapped data stream. def close @io.close @tarreader.close end private def fsync_dir(dirname) # make sure this hits the disc dir = open(dirname, 'rb') dir.fsync rescue # ignore IOError if it's an unpatched (old) Ruby nil ensure dir.close if dir rescue nil end end # Wraps a Archive::Tar::Minitar::Writer with convenience methods and # wrapped stream management; Output only works with random access data # streams. See Output::new for details. class Output # With no associated block, +Output::open+ is a synonym for # +Output::new+. If the optional code block is given, it will be passed # the new _writer_ as an argument and the Output object will # automatically be closed when the block terminates. In this instance, # +Output::open+ returns the value of the block. def self.open(output) stream = Output.new(output) return stream unless block_given? begin res = yield stream ensure stream.close end res end # Creates a new Output object. If +output+ 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 Output#close is called, # the stream object wrapped will be closed. def initialize(output) if output.respond_to?(:write) @io = output else @io = ::File.open(output, "wb") end @tarwriter = Archive::Tar::Minitar::Writer.new(@io) end # Returns the Writer object for direct access. def tar @tarwriter end # Closes the Writer object and the wrapped data stream. def close @tarwriter.close @io.close end end class << self # Tests if +path+ refers to a directory. Fixes an apparently # corrupted stat() call on Windows. def dir?(path) File.directory?((path[-1] == ?/) ? path : "#{path}/") end # A convenience method for wrapping Archive::Tar::Minitar::Input.open # (mode +r+) and Archive::Tar::Minitar::Output.open (mode +w+). No other # modes are currently supported. def open(dest, mode = "r", &block) case mode when "r" Input.open(dest, &block) when "w" Output.open(dest, &block) else raise "Unknown open mode for Archive::Tar::Minitar.open." end end # A convenience method to packs 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 tarchive. # *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+ # Symol, the full name of the file being packed, and a Hash of # statistical information, just as with # Archive::Tar::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.kind_of?(Archive::Tar::Minitar::Output) stats = {} if entry.kind_of?(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 RUBY_PLATFORM =~ /win32/ stats[:uid] = nil stats[:gid] = nil else stats[:uid] ||= stat.uid stats[:gid] ||= stat.gid end case when 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 when 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 Archive::Tar::Minitar::Output stream; if +recurse_dirs+ # is true, then directories will be recursed. # # If +src+ is an Array, it will be treated as the argument to Find.find; # all files matching will be packed. def pack(src, dest, recurse_dirs = true, &block) Output.open(dest) do |outp| if src.kind_of?(Array) src.each do |entry| pack_file(entry, outp, &block) if dir?(entry) and recurse_dirs Dir["#{entry}/**/**"].each do |ee| pack_file(ee, outp, &block) end 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 = [], &block) Input.open(src) do |inp| if File.exist?(dest) and (not dir?(dest)) raise "Can't unpack to a non-directory." elsif not File.exist?(dest) FileUtils.mkdir_p(dest) end inp.each do |entry| if files.empty? or files.include?(entry.full_name) inp.extract_entry(dest, entry, &block) end end end end end end archive-tar-minitar-0.5.2/README0000644000175000017500000000470611624477521016534 0ustar formorerformorerArchive::Tar::Minitar README ============================ Archive::Tar::Minitar is a pure-Ruby library and command-line utility that provides the ability to deal with POSIX tar(1) archive files. The implementation is based heavily on Mauricio Ferna'ndez's implementation in rpa-base, but has been reorganised to promote reuse in other projects. This release is version 0.5.2, offering a Ruby 1.9 compatibility bugfix over version 0.5.1. The library can only handle files and directories at this point. A future version will be expanded to handle symbolic links and hard links in a portable manner. The command line utility, minitar, can only create archives, extract from archives, and list archive contents. Using this library is easy. The simplest case is: require 'zlib' require 'archive/tar/minitar' include Archive::Tar # Packs everything that matches Find.find('tests') File.open('test.tar', 'wb') { |tar| Minitar.pack('tests', tar) } # Unpacks 'test.tar' to 'x', creating 'x' if necessary. Minitar.unpack('test.tar', 'x') A gzipped tar can be written with: tgz = Zlib::GzipWriter.new(File.open('test.tgz', 'wb')) # Warning: tgz will be closed! Minitar.pack('tests', tgz) tgz = Zlib::GzipReader.new(File.open('test.tgz', 'rb')) # Warning: tgz will be closed! Minitar.unpack(tgz, '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 = Output.new(sgz) Find.find('tests') do |entry| Minitar.pack_file(entry, tar) end ensure # Closes both tar and sgz. tar.close end Copyright ========= # Copyright 2004 Mauricio Julio Ferna'ndez Pradier and Austin Ziegler # # This program is based on and incorporates parts of RPA::Package from # rpa-base (lib/rpa/package.rb and lib/rpa/util.rb) by Mauricio and has been # adapted to be more generic by Austin. # # 'minitar' contains an adaptation of Ruby/ProgressBar by Satoru # Takabayashi , copyright 2001 - 2004. # # This program is free software. It may be redistributed and/or modified # under the terms of the GPL version 2 (or later) or Ruby's licence. # # $Id: README 213 2008-02-26 22:32:11Z austin $ archive-tar-minitar-0.5.2/ChangeLog0000644000175000017500000000056411624477521017424 0ustar formorerformorerRevision history for Ruby library Archive::Tar::Minitar. Unless explicitly noted otherwise, all changes are produced by Austin Ziegler . == 0.5.2 * Fixed a Ruby 1.9 compatibility error. == 0.5.1 * Fixed a variable name error. == Archive::Tar::Minitar 0.5.0 * Initial release. Does files and directories. Command does create, extract, * and list. archive-tar-minitar-0.5.2/Rakefile0000644000175000017500000000531111624477521017312 0ustar formorerformorer#! /usr/bin/env rake $LOAD_PATH.unshift('lib') require 'rubygems' require 'rake/gempackagetask' require 'rake/contrib/rubyforgepublisher' require 'archive/tar/minitar' require 'zlib' DISTDIR = "archive-tar-minitar-#{Archive::Tar::Minitar::VERSION}" TARDIST = "../#{DISTDIR}.tar.gz" DATE_RE = %r<(\d{4})[./-]?(\d{2})[./-]?(\d{2})(?:[\sT]?(\d{2})[:.]?(\d{2})[:.]?(\d{2})?)?> if ENV['RELEASE_DATE'] year, month, day, hour, minute, second = DATE_RE.match(ENV['RELEASE_DATE']).captures year ||= 0 month ||= 0 day ||= 0 hour ||= 0 minute ||= 0 second ||= 0 ReleaseDate = Time.mktime(year, month, day, hour, minute, second) else ReleaseDate = nil end task :test do |t| require 'test/unit/testsuite' require 'test/unit/ui/console/testrunner' runner = Test::Unit::UI::Console::TestRunner $LOAD_PATH.unshift('tests') Dir['tests/tc_*.rb'].each do |testcase| load testcase end suite = Test::Unit::TestSuite.new ObjectSpace.each_object(Class) do |testcase| suite << testcase.suite if testcase < Test::Unit::TestCase end runner.run(suite) end spec = eval(File.read("archive-tar-minitar.gemspec")) desc "Build the RubyGem for Archive::Tar::Minitar." task :gem => [ :test ] Rake::GemPackageTask.new(spec) do |g| g.need_tar = false g.need_zip = false g.package_dir = ".." end desc "Build an Archive::Tar::Minitar .tar.gz distribution." task :tar => [ TARDIST ] file TARDIST do |t| current = File.basename(Dir.pwd) Dir.chdir("..") do begin files = Dir["#{current}/**/*"].select { |dd| dd !~ %r{(?:/CVS/?|~$)} } files.map! do |dd| ddnew = dd.gsub(/^#{current}/, DISTDIR) mtime = ReleaseDate || File.stat(dd).mtime if File.directory?(dd) { :name => ddnew, :mode => 0755, :dir => true, :mtime => mtime } else if dd =~ %r{bin/} mode = 0755 else mode = 0644 end data = File.read(dd) { :name => ddnew, :mode => mode, :data => data, :size => data.size, :mtime => mtime } end end ff = File.open(t.name.gsub(%r{^\.\./}o, ''), "wb") gz = Zlib::GzipWriter.new(ff) tw = Archive::Tar::Minitar::Writer.new(gz) files.each do |entry| if entry[:dir] tw.mkdir(entry[:name], entry) else tw.add_file_simple(entry[:name], entry) { |os| os.write(entry[:data]) } end end ensure tw.close if tw gz.close if gz end end end task TARDIST => [ :test ] def sign(file) sh %(gpg -ba #{file}) end task :signtar => [ :tar ] do sign TARDIST end task :signgem => [ :gem ] do sign "../#{DISTDIR}.gem" end desc "Build everything." task :default => [ :signtar, :signgem ] do end archive-tar-minitar-0.5.2/tests/0000775000175000017500000000000011624477521017011 5ustar formorerformorerarchive-tar-minitar-0.5.2/tests/tc_tar.rb0000644000175000017500000004604611624477521020622 0ustar formorerformorer#!/usr/bin/env ruby $LOAD_PATH.unshift("#{File.dirname(__FILE__)}/../lib") if __FILE__ == $0 require 'archive/tar/minitar' require 'test/unit' require 'stringio' require 'yaml' require 'zlib' module TarTester private def assert_headers_equal(h1, h2) fields = %w(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 until fields.empty? name = fields.shift length = fields.shift.to_i if name == "checksum" chksum_off = offset offset += length next end assert_equal(h1[offset, length], h2[offset, length], "Field #{name} of the tar header differs.") offset += length end assert_equal(h1[chksum_off, 8], h2[chksum_off, 8], "Checksumes differ.") end def tar_file_header(fname, dname, mode, length) h = header("0", fname, dname, length, mode) checksum = calc_checksum(h) header("0", fname, dname, length, mode, checksum) end def tar_dir_header(name, prefix, mode) h = header("5", name, prefix, 0, mode) checksum = calc_checksum(h) header("5", name, prefix, 0, mode, checksum) end def header(type, fname, dname, length, mode, checksum = nil) checksum ||= " " * 8 arr = [ASCIIZ(fname, 100), Z(to_oct(mode, 7)), Z(to_oct(nil, 7)), Z(to_oct(nil, 7)), Z(to_oct(length, 11)), Z(to_oct(0, 11)), checksum, type, "\0" * 100, "ustar\0", "00", ASCIIZ("", 32), ASCIIZ("", 32), Z(to_oct(nil, 7)), Z(to_oct(nil, 7)), ASCIIZ(dname, 155) ] arr = arr.join("").split(//).map{ |x| x[0] } h = arr.pack("C100C8C8C8C12C12C8CC100C6C2C32C32C8C8C155") ret = h + "\0" * (512 - h.size) assert_equal(512, ret.size) ret end def calc_checksum(header) sum = header.unpack("C*").inject { |s, a| s + a } SP(Z(to_oct(sum, 6))) end def to_oct(n, pad_size) if n.nil? "\0" * pad_size else "%0#{pad_size}o" % n end end def ASCIIZ(str, length) str + "\0" * (length - str.length) end def SP(s) s + " " end def Z(s) s + "\0" end def SP_Z(s) s + " \0" end end class TC_Tar__Header < Test::Unit::TestCase include Archive::Tar::Minitar include TarTester def test_arguments_are_checked e = ArgumentError assert_raises(e) { Archive::Tar::PosixHeader.new(:name => "", :size => "", :mode => "") } assert_raises(e) { Archive::Tar::PosixHeader.new(:name => "", :size => "", :prefix => "") } assert_raises(e) { Archive::Tar::PosixHeader.new(:name => "", :prefix => "", :mode => "") } assert_raises(e) { Archive::Tar::PosixHeader.new(:prefix => "", :size => "", :mode => "") } end def test_basic_headers header = { :name => "bla", :mode => 012345, :size => 10, :prefix => "", :typeflag => "0" } assert_headers_equal(tar_file_header("bla", "", 012345, 10), Archive::Tar::PosixHeader.new(header).to_s) header = { :name => "bla", :mode => 012345, :size => 0, :prefix => "", :typeflag => "5" } assert_headers_equal(tar_dir_header("bla", "", 012345), Archive::Tar::PosixHeader.new(header).to_s) end def test_long_name_works header = { :name => "a" * 100, :mode => 012345, :size => 10, :prefix => "" } assert_headers_equal(tar_file_header("a" * 100, "", 012345, 10), Archive::Tar::PosixHeader.new(header).to_s) header = { :name => "a" * 100, :mode => 012345, :size => 10, :prefix => "bb" * 60 } assert_headers_equal(tar_file_header("a" * 100, "bb" * 60, 012345, 10), Archive::Tar::PosixHeader.new(header).to_s) end def test_new_from_stream header = tar_file_header("a" * 100, "", 012345, 10) h = nil header = StringIO.new(header) assert_nothing_raised { h = Archive::Tar::PosixHeader.new_from_stream(header) } assert_equal("a" * 100, h.name) assert_equal(012345, h.mode) assert_equal(10, h.size) assert_equal("", h.prefix) assert_equal("ustar", h.magic) end def test_new_from_stream_with_evil_name header = tar_file_header("a \0" + "\0" * 97, "", 012345, 10) h = nil header = StringIO.new(header) assert_nothing_raised{ h = Archive::Tar::PosixHeader.new_from_stream header } assert_equal("a ", h.name) end end class TC_Tar__Writer < Test::Unit::TestCase include Archive::Tar::Minitar include TarTester class DummyIO attr_reader :data def initialize @data = "" end def write(dat) data << dat dat.size end def reset @data = "" end end def setup @data = "a" * 10 @dummyos = DummyIO.new @os = Writer.new(@dummyos) end def teardown @os.close end def test_add_file_simple @dummyos.reset Writer.open(@dummyos) do |os| os.add_file_simple("lib/foo/bar", :mode => 0644, :size => 10) do |f| f.write "a" * 10 end os.add_file_simple("lib/bar/baz", :mode => 0644, :size => 100) do |f| f.write "fillme" end end assert_headers_equal(tar_file_header("lib/foo/bar", "", 0644, 10), @dummyos.data[0, 512]) assert_equal("a" * 10 + "\0" * 502, @dummyos.data[512, 512]) assert_headers_equal(tar_file_header("lib/bar/baz", "", 0644, 100), @dummyos.data[512 * 2, 512]) assert_equal("fillme" + "\0" * 506, @dummyos.data[512 * 3, 512]) assert_equal("\0" * 512, @dummyos.data[512 * 4, 512]) assert_equal("\0" * 512, @dummyos.data[512 * 5, 512]) end def test_write_operations_fail_after_closed @dummyos.reset @os.add_file_simple("sadd", :mode => 0644, :size => 20) { |f| } @os.close assert_raises(ClosedStream) { @os.flush } assert_raises(ClosedStream) { @os.add_file("dfdsf", :mode => 0644) {} } assert_raises(ClosedStream) { @os.mkdir "sdfdsf", :mode => 0644 } end def test_file_name_is_split_correctly # test insane file lengths, and: a{100}/b{155}, etc @dummyos.reset names = [ "#{'a' * 155}/#{'b' * 100}", "#{'a' * 151}/#{'qwer/' * 19}bla" ] o_names = [ "#{'b' * 100}", "#{'qwer/' * 19}bla" ] o_prefixes = [ "a" * 155, "a" * 151 ] names.each do |name| @os.add_file_simple(name, :mode => 0644, :size => 10) { } end o_names.each_with_index do |nam, i| assert_headers_equal(tar_file_header(nam, o_prefixes[i], 0644, 10), @dummyos.data[2 * i * 512, 512]) end assert_raises(FileNameTooLong) do @os.add_file_simple(File.join("a" * 152, "b" * 10, "a" * 92), :mode => 0644, :size => 10) { } end assert_raises(FileNameTooLong) do @os.add_file_simple(File.join("a" * 162, "b" * 10), :mode => 0644, :size => 10) { } end assert_raises(FileNameTooLong) do @os.add_file_simple(File.join("a" * 10, "b" * 110), :mode => 0644, :size => 10) { } end end def test_add_file dummyos = StringIO.new class << dummyos def method_missing(meth, *a) self.string.send(meth, *a) end end os = Writer.new dummyos content1 = ('a'..'z').to_a.join("") # 26 content2 = ('aa'..'zz').to_a.join("") # 1352 Writer.open(dummyos) do |os| os.add_file("lib/foo/bar", :mode => 0644) { |f, opts| f.write "a" * 10 } os.add_file("lib/bar/baz", :mode => 0644) { |f, opts| f.write content1 } os.add_file("lib/bar/baz", :mode => 0644) { |f, opts| f.write content2 } os.add_file("lib/bar/baz", :mode => 0644) { |f, opts| } end assert_headers_equal(tar_file_header("lib/foo/bar", "", 0644, 10), dummyos[0, 512]) assert_equal(%Q(#{"a" * 10}#{"\0" * 502}), dummyos[512, 512]) offset = 512 * 2 [content1, content2, ""].each do |data| assert_headers_equal(tar_file_header("lib/bar/baz", "", 0644, data.size), dummyos[offset, 512]) offset += 512 until !data || data == "" chunk = data[0, 512] data[0, 512] = "" assert_equal(chunk + "\0" * (512 - chunk.size), dummyos[offset, 512]) offset += 512 end end assert_equal("\0" * 1024, dummyos[offset, 1024]) end def test_add_file_tests_seekability assert_raises(Archive::Tar::Minitar::NonSeekableStream) do @os.add_file("libdfdsfd", :mode => 0644) { |f| } end end def test_write_header @dummyos.reset @os.add_file_simple("lib/foo/bar", :mode => 0644, :size => 0) { |f| } @os.flush assert_headers_equal(tar_file_header("lib/foo/bar", "", 0644, 0), @dummyos.data[0, 512]) @dummyos.reset @os.mkdir("lib/foo", :mode => 0644) assert_headers_equal(tar_dir_header("lib/foo", "", 0644), @dummyos.data[0, 512]) @os.mkdir("lib/bar", :mode => 0644) assert_headers_equal(tar_dir_header("lib/bar", "", 0644), @dummyos.data[512 * 1, 512]) end def test_write_data @dummyos.reset @os.add_file_simple("lib/foo/bar", :mode => 0644, :size => 10) do |f| f.write @data end @os.flush assert_equal(@data + ("\0" * (512-@data.size)), @dummyos.data[512, 512]) end def test_file_size_is_checked @dummyos.reset assert_raises(Archive::Tar::Minitar::Writer::BoundedStream::FileOverflow) do @os.add_file_simple("lib/foo/bar", :mode => 0644, :size => 10) do |f| f.write "1" * 100 end end assert_nothing_raised do @os.add_file_simple("lib/foo/bar", :mode => 0644, :size => 10) {|f| } end end end class TC_Tar__Reader < Test::Unit::TestCase include Archive::Tar::Minitar include TarTester def setup end def teardown end def test_multiple_entries str = tar_file_header("lib/foo", "", 010644, 10) + "\0" * 512 str += tar_file_header("bar", "baz", 0644, 0) str += tar_dir_header("foo", "bar", 012345) str += "\0" * 1024 names = %w[lib/foo bar foo] prefixes = ["", "baz", "bar"] modes = [010644, 0644, 012345] sizes = [10, 0, 0] isdir = [false, false, true] isfile = [true, true, false] Reader.new(StringIO.new(str)) do |is| i = 0 is.each_entry do |entry| assert_kind_of(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 = tar_file_header("lib/foo", "", 010644, content.size) + content + "\0" * (512 - content.size) str << "\0" * 1024 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.size, entry.pos) end end end end def test_rewind_works content = ('a'..'z').to_a.join(" ") str = tar_file_header("lib/foo", "", 010644, content.size) + content + "\0" * (512 - content.size) str << "\0" * 1024 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(""){|s, x| s << x * 100} str = tar_file_header("lib/foo", "", 010644, contents.size) + contents str += "\0" * (512 - (str.size % 512)) Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of(Reader::EntryStream, entry) data = entry.read(3000) # bigger than contents.size assert_equal(contents, data) assert_equal(true, entry.eof?) end end Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of(Reader::EntryStream, entry) data = entry.read(100) (entry.size - data.size).times {|i| data << entry.getc.chr } assert_equal(contents, data) assert_equal(nil, entry.read(10)) assert_equal(true, entry.eof?) end end Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of(Reader::EntryStream, entry) data = entry.read assert_equal(contents, data) assert_equal(nil, entry.read(10)) assert_equal(nil, entry.read) assert_equal(nil, entry.getc) assert_equal(true, entry.eof?) end end end def test_eof_works str = tar_file_header("bar", "baz", 0644, 0) Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of(Reader::EntryStream, entry) data = entry.read assert_equal(nil, data) assert_equal(nil, entry.read(10)) assert_equal(nil, entry.read) assert_equal(nil, entry.getc) assert_equal(true, entry.eof?) end end str = tar_dir_header("foo", "bar", 012345) Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of(Reader::EntryStream, entry) data = entry.read assert_equal(nil, data) assert_equal(nil, entry.read(10)) assert_equal(nil, entry.read) assert_equal(nil, entry.getc) assert_equal(true, entry.eof?) end end str = tar_dir_header("foo", "bar", 012345) str += tar_file_header("bar", "baz", 0644, 0) str += tar_file_header("bar", "baz", 0644, 0) Reader.new(StringIO.new(str)) do |is| is.each_entry do |entry| assert_kind_of(Reader::EntryStream, entry) data = entry.read assert_equal(nil, data) assert_equal(nil, entry.read(10)) assert_equal(nil, entry.read) assert_equal(nil, entry.getc) assert_equal(true, entry.eof?) end end end end class TC_Tar__Input < Test::Unit::TestCase include Archive::Tar::Minitar include TarTester require 'rbconfig' TEST_TGZ = "\037\213\010\000\001B1A\000\vKI,I\324+I,\322K\257b\240\0250\000\002sSS\254\342 `dj\306``nnnbndbjd\000\0247336`P0\240\231\213\220@i1\320\367@+\351a\327 \004\362\335\034\f\313\034\r\035\031\270\337Ns\344b2\344q\335\375M\304\266QM1W\357\321>\221U\021\005\246\306\367\356\367u3\262;\212\004\265\236\\\334}\351,\377\037;\217\223\301e\247\030\024\\\236\211\277\347\346sii\265\010\330\355\234\240\362\274\371[\202\361\366\302S\316\335o&~m\237r\355\377\303\230\365\352WNW\334\266_\373\273\237\347Q\315t?\263{\377?\006\271\337?\367\207\325\346]\371\376y\307_\234~d\3772\265\346\261}\323\317\373\315\352\377O\376\271/\305\377?X\253\324\303S\373\361\347\277\372^)\267\377\363\03460\331\311\\wW|\031\203\300@\207\325p\004i\2319\251\3064\266\203P\376702B\313\377\246\246\006&\243\371\237\036 $#\263X\001\210@\351@\301XO\201\227k\240]4\nF\301(\030\005\243\200\036\000\000\004\330t\023\000\f\000\000" FILETIMES = Time.mktime(2004).to_i TEST_CONTENTS = [ [ "data.tar.gz", 174, 0755 ], [ "file3", 18, 0755 ], ] TEST_DATA_CONTENTS = [ [ "data", 0, 040755 ], [ "data/file1", 16, 010644 ], [ "data/file2", 16, 010644 ], [ "data/__dir__", 0, 010644 ], ] def setup FileUtils.mkdir_p("data__") end def teardown FileUtils.rm_rf("data__") end def test_each_works gzr = Zlib::GzipReader.new(StringIO.new(TEST_TGZ)) Input.open(gzr) do |is| ii = 0 is.each_with_index do |entry, ii| assert_kind_of(Reader::EntryStream, entry) assert_equal(TEST_CONTENTS[ii][0], entry.name) assert_equal(TEST_CONTENTS[ii][1], entry.size) assert_equal(TEST_CONTENTS[ii][2], entry.mode) assert_equal(FILETIMES, entry.mtime) if 0 == ii gzr2 = Zlib::GzipReader.new(StringIO.new(entry.read)) Input.open(gzr2) do |is2| jj = 0 is2.each_with_index do |entry2, jj| assert_kind_of(Reader::EntryStream, entry2) assert_equal(TEST_DATA_CONTENTS[jj][0], entry2.name) assert_equal(TEST_DATA_CONTENTS[jj][1], entry2.size) assert_equal(TEST_DATA_CONTENTS[jj][2], entry2.mode) assert_equal(FILETIMES, entry2.mtime) end assert_equal(3, jj) end end end assert_equal(1, ii) end end def test_extract_entry_works gzr = Zlib::GzipReader.new(StringIO.new(TEST_TGZ)) Input.open(gzr) do |is| ii = 0 is.each_with_index do |entry, ii| is.extract_entry("data__", entry) name = File.join("data__", entry.name) if entry.directory? assert(File.directory?(name)) else assert(File.file?(name)) assert_equal(TEST_CONTENTS[ii][1], File.stat(name).size) end assert_equal(TEST_CONTENTS[ii][2], File.stat(name).mode & 0777) unless RUBY_PLATFORM =~ /win32/ if 0 == ii begin ff = File.open(name, "rb") gzr2 = Zlib::GzipReader.new(ff) Input.open(gzr2) do |is2| jj = 0 is2.each_with_index do |entry2, jj| is2.extract_entry("data__", entry2) name2 = File.join("data__", entry2.name) if entry2.directory? assert(File.directory?(name2)) else assert(File.file?(name2)) assert_equal(TEST_DATA_CONTENTS[jj][1], File.stat(name2).size, name2) end assert_equal(TEST_DATA_CONTENTS[jj][2], File.stat(name2).mode, name2) unless RUBY_PLATFORM =~ /win32/ end end ensure ff.close unless ff.closed? end end end assert_equal(1, ii) end end end class TC_Tar__Output < Test::Unit::TestCase include Archive::Tar::Minitar include TarTester def setup FileUtils.mkdir_p("data__") %w(a b c).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_file_looks_good Output.open(@tarfile) do |os| Dir.chdir("data__") do %w(a b c).each do |name| stat = File.stat(name) opts = { :size => stat.size, :mode => 0644 } os.tar.add_file_simple(name, opts) do |ss| File.open(name, "rb") { |ff| ss.write(ff.read(4096)) until ff.eof? } end end end end ff = File.open(@tarfile, "rb") Reader.open(ff) do |is| ii = 0 is.each do |entry| case ii when 0 assert_equal("a", entry.name) when 1 assert_equal("b", entry.name) when 2 assert_equal("c", entry.name) end ii += 1 end assert_equal(3, ii) end ensure ff.close if ff end end archive-tar-minitar-0.5.2/tests/testall.rb0000644000175000017500000000032711624477521021006 0ustar formorerformorer#!/usr/bin/env ruby $LOAD_PATH.unshift("#{File.dirname(__FILE__)}/../lib") if __FILE__ == $0 puts "Checking for test cases:" Dir['tc*.rb'].each do |testcase| puts "\t#{testcase}" require testcase end puts " "