1870 lines
52 KiB
Ruby
Executable file
1870 lines
52 KiB
Ruby
Executable file
require 'delegate'
|
|
require 'iconv'
|
|
require 'singleton'
|
|
require 'tempfile'
|
|
require 'fileutils'
|
|
require 'stringio'
|
|
require 'zlib'
|
|
require 'zip/stdrubyext'
|
|
require 'zip/ioextras'
|
|
|
|
if Tempfile.superclass == SimpleDelegator
|
|
require 'zip/tempfile_bugfixed'
|
|
Tempfile = BugFix::Tempfile
|
|
end
|
|
|
|
module Zlib #:nodoc:all
|
|
if ! const_defined? :MAX_WBITS
|
|
MAX_WBITS = Zlib::Deflate.MAX_WBITS
|
|
end
|
|
end
|
|
|
|
module Zip
|
|
|
|
VERSION = '0.9.3'
|
|
|
|
RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i
|
|
|
|
RUNNING_ON_WINDOWS = /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
|
|
|
|
# Ruby 1.7.x compatibility
|
|
# In ruby 1.6.x and 1.8.0 reading from an empty stream returns
|
|
# an empty string the first time and then nil.
|
|
# not so in 1.7.x
|
|
EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7
|
|
|
|
# ZipInputStream is the basic class for reading zip entries in a
|
|
# zip file. It is possible to create a ZipInputStream object directly,
|
|
# passing the zip file name to the constructor, but more often than not
|
|
# the ZipInputStream will be obtained from a ZipFile (perhaps using the
|
|
# ZipFileSystem interface) object for a particular entry in the zip
|
|
# archive.
|
|
#
|
|
# A ZipInputStream inherits IOExtras::AbstractInputStream in order
|
|
# to provide an IO-like interface for reading from a single zip
|
|
# entry. Beyond methods for mimicking an IO-object it contains
|
|
# the method get_next_entry for iterating through the entries of
|
|
# an archive. get_next_entry returns a ZipEntry object that describes
|
|
# the zip entry the ZipInputStream is currently reading from.
|
|
#
|
|
# Example that creates a zip archive with ZipOutputStream and reads it
|
|
# back again with a ZipInputStream.
|
|
#
|
|
# require 'zip/zip'
|
|
#
|
|
# Zip::ZipOutputStream::open("my.zip") {
|
|
# |io|
|
|
#
|
|
# io.put_next_entry("first_entry.txt")
|
|
# io.write "Hello world!"
|
|
#
|
|
# io.put_next_entry("adir/first_entry.txt")
|
|
# io.write "Hello again!"
|
|
# }
|
|
#
|
|
#
|
|
# Zip::ZipInputStream::open("my.zip") {
|
|
# |io|
|
|
#
|
|
# while (entry = io.get_next_entry)
|
|
# puts "Contents of #{entry.name}: '#{io.read}'"
|
|
# end
|
|
# }
|
|
#
|
|
# java.util.zip.ZipInputStream is the original inspiration for this
|
|
# class.
|
|
|
|
class ZipInputStream
|
|
include IOExtras::AbstractInputStream
|
|
|
|
# Opens the indicated zip file. An exception is thrown
|
|
# if the specified offset in the specified filename is
|
|
# not a local zip entry header.
|
|
def initialize(filename, offset = 0)
|
|
super()
|
|
@archiveIO = File.open(filename, "rb")
|
|
@archiveIO.seek(offset, IO::SEEK_SET)
|
|
@decompressor = NullDecompressor.instance
|
|
@currentEntry = nil
|
|
end
|
|
|
|
def close
|
|
@archiveIO.close
|
|
end
|
|
|
|
# Same as #initialize but if a block is passed the opened
|
|
# stream is passed to the block and closed when the block
|
|
# returns.
|
|
def ZipInputStream.open(filename)
|
|
return new(filename) unless block_given?
|
|
|
|
zio = new(filename)
|
|
yield zio
|
|
ensure
|
|
zio.close if zio
|
|
end
|
|
|
|
# Returns a ZipEntry object. It is necessary to call this
|
|
# method on a newly created ZipInputStream before reading from
|
|
# the first entry in the archive. Returns nil when there are
|
|
# no more entries.
|
|
|
|
def get_next_entry
|
|
@archiveIO.seek(@currentEntry.next_header_offset,
|
|
IO::SEEK_SET) if @currentEntry
|
|
open_entry
|
|
end
|
|
|
|
# Rewinds the stream to the beginning of the current entry
|
|
def rewind
|
|
return if @currentEntry.nil?
|
|
@lineno = 0
|
|
@archiveIO.seek(@currentEntry.localHeaderOffset,
|
|
IO::SEEK_SET)
|
|
open_entry
|
|
end
|
|
|
|
# Modeled after IO.sysread
|
|
def sysread(numberOfBytes = nil, buf = nil)
|
|
@decompressor.sysread(numberOfBytes, buf)
|
|
end
|
|
|
|
def eof
|
|
@outputBuffer.empty? && @decompressor.eof
|
|
end
|
|
alias :eof? :eof
|
|
|
|
protected
|
|
|
|
def open_entry
|
|
@currentEntry = ZipEntry.read_local_entry(@archiveIO)
|
|
if (@currentEntry == nil)
|
|
@decompressor = NullDecompressor.instance
|
|
elsif @currentEntry.compression_method == ZipEntry::STORED
|
|
@decompressor = PassThruDecompressor.new(@archiveIO,
|
|
@currentEntry.size)
|
|
elsif @currentEntry.compression_method == ZipEntry::DEFLATED
|
|
@decompressor = Inflater.new(@archiveIO)
|
|
else
|
|
raise ZipCompressionMethodError,
|
|
"Unsupported compression method #{@currentEntry.compression_method}"
|
|
end
|
|
flush
|
|
return @currentEntry
|
|
end
|
|
|
|
def produce_input
|
|
@decompressor.produce_input
|
|
end
|
|
|
|
def input_finished?
|
|
@decompressor.input_finished?
|
|
end
|
|
end
|
|
|
|
|
|
|
|
class Decompressor #:nodoc:all
|
|
CHUNK_SIZE=32768
|
|
def initialize(inputStream)
|
|
super()
|
|
@inputStream=inputStream
|
|
end
|
|
end
|
|
|
|
class Inflater < Decompressor #:nodoc:all
|
|
def initialize(inputStream)
|
|
super
|
|
@zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
|
@outputBuffer=""
|
|
@hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
|
|
end
|
|
|
|
def sysread(numberOfBytes = nil, buf = nil)
|
|
readEverything = (numberOfBytes == nil)
|
|
while (readEverything || @outputBuffer.length < numberOfBytes)
|
|
break if internal_input_finished?
|
|
@outputBuffer << internal_produce_input(buf)
|
|
end
|
|
return value_when_finished if @outputBuffer.length==0 && input_finished?
|
|
endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes
|
|
return @outputBuffer.slice!(0...endIndex)
|
|
end
|
|
|
|
def produce_input
|
|
if (@outputBuffer.empty?)
|
|
return internal_produce_input
|
|
else
|
|
return @outputBuffer.slice!(0...(@outputBuffer.length))
|
|
end
|
|
end
|
|
|
|
# to be used with produce_input, not read (as read may still have more data cached)
|
|
# is data cached anywhere other than @outputBuffer? the comment above may be wrong
|
|
def input_finished?
|
|
@outputBuffer.empty? && internal_input_finished?
|
|
end
|
|
alias :eof :input_finished?
|
|
alias :eof? :input_finished?
|
|
|
|
private
|
|
|
|
def internal_produce_input(buf = nil)
|
|
retried = 0
|
|
begin
|
|
@zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE, buf))
|
|
rescue Zlib::BufError
|
|
raise if (retried >= 5) # how many times should we retry?
|
|
retried += 1
|
|
retry
|
|
end
|
|
end
|
|
|
|
def internal_input_finished?
|
|
@zlibInflater.finished?
|
|
end
|
|
|
|
# TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
|
|
def value_when_finished # mimic behaviour of ruby File object.
|
|
return nil if @hasReturnedEmptyString
|
|
@hasReturnedEmptyString=true
|
|
return ""
|
|
end
|
|
end
|
|
|
|
class PassThruDecompressor < Decompressor #:nodoc:all
|
|
def initialize(inputStream, charsToRead)
|
|
super inputStream
|
|
@charsToRead = charsToRead
|
|
@readSoFar = 0
|
|
@hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
|
|
end
|
|
|
|
# TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
|
|
def sysread(numberOfBytes = nil, buf = nil)
|
|
if input_finished?
|
|
hasReturnedEmptyStringVal=@hasReturnedEmptyString
|
|
@hasReturnedEmptyString=true
|
|
return "" unless hasReturnedEmptyStringVal
|
|
return nil
|
|
end
|
|
|
|
if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead)
|
|
numberOfBytes = @charsToRead-@readSoFar
|
|
end
|
|
@readSoFar += numberOfBytes
|
|
@inputStream.read(numberOfBytes, buf)
|
|
end
|
|
|
|
def produce_input
|
|
sysread(Decompressor::CHUNK_SIZE)
|
|
end
|
|
|
|
def input_finished?
|
|
(@readSoFar >= @charsToRead)
|
|
end
|
|
alias :eof :input_finished?
|
|
alias :eof? :input_finished?
|
|
end
|
|
|
|
class NullDecompressor #:nodoc:all
|
|
include Singleton
|
|
def sysread(numberOfBytes = nil, buf = nil)
|
|
nil
|
|
end
|
|
|
|
def produce_input
|
|
nil
|
|
end
|
|
|
|
def input_finished?
|
|
true
|
|
end
|
|
|
|
def eof
|
|
true
|
|
end
|
|
alias :eof? :eof
|
|
end
|
|
|
|
class NullInputStream < NullDecompressor #:nodoc:all
|
|
include IOExtras::AbstractInputStream
|
|
end
|
|
|
|
class ZipEntry
|
|
STORED = 0
|
|
DEFLATED = 8
|
|
|
|
FSTYPE_FAT = 0
|
|
FSTYPE_AMIGA = 1
|
|
FSTYPE_VMS = 2
|
|
FSTYPE_UNIX = 3
|
|
FSTYPE_VM_CMS = 4
|
|
FSTYPE_ATARI = 5
|
|
FSTYPE_HPFS = 6
|
|
FSTYPE_MAC = 7
|
|
FSTYPE_Z_SYSTEM = 8
|
|
FSTYPE_CPM = 9
|
|
FSTYPE_TOPS20 = 10
|
|
FSTYPE_NTFS = 11
|
|
FSTYPE_QDOS = 12
|
|
FSTYPE_ACORN = 13
|
|
FSTYPE_VFAT = 14
|
|
FSTYPE_MVS = 15
|
|
FSTYPE_BEOS = 16
|
|
FSTYPE_TANDEM = 17
|
|
FSTYPE_THEOS = 18
|
|
FSTYPE_MAC_OSX = 19
|
|
FSTYPE_ATHEOS = 30
|
|
|
|
FSTYPES = {
|
|
FSTYPE_FAT => 'FAT'.freeze,
|
|
FSTYPE_AMIGA => 'Amiga'.freeze,
|
|
FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze,
|
|
FSTYPE_UNIX => 'Unix'.freeze,
|
|
FSTYPE_VM_CMS => 'VM/CMS'.freeze,
|
|
FSTYPE_ATARI => 'Atari ST'.freeze,
|
|
FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze,
|
|
FSTYPE_MAC => 'Macintosh'.freeze,
|
|
FSTYPE_Z_SYSTEM => 'Z-System'.freeze,
|
|
FSTYPE_CPM => 'CP/M'.freeze,
|
|
FSTYPE_TOPS20 => 'TOPS-20'.freeze,
|
|
FSTYPE_NTFS => 'NTFS'.freeze,
|
|
FSTYPE_QDOS => 'SMS/QDOS'.freeze,
|
|
FSTYPE_ACORN => 'Acorn RISC OS'.freeze,
|
|
FSTYPE_VFAT => 'Win32 VFAT'.freeze,
|
|
FSTYPE_MVS => 'MVS'.freeze,
|
|
FSTYPE_BEOS => 'BeOS'.freeze,
|
|
FSTYPE_TANDEM => 'Tandem NSK'.freeze,
|
|
FSTYPE_THEOS => 'Theos'.freeze,
|
|
FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze,
|
|
FSTYPE_ATHEOS => 'AtheOS'.freeze,
|
|
}.freeze
|
|
|
|
attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
|
|
:name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes, :gp_flags, :header_signature
|
|
|
|
attr_accessor :follow_symlinks
|
|
attr_accessor :restore_times, :restore_permissions, :restore_ownership
|
|
attr_accessor :unix_uid, :unix_gid, :unix_perms
|
|
|
|
attr_reader :ftype, :filepath # :nodoc:
|
|
|
|
# Returns the character encoding used for name and comment
|
|
def name_encoding
|
|
(@gp_flags & 0b100000000000) != 0 ? "utf8" : "CP437//"
|
|
end
|
|
|
|
# Returns the name in the encoding specified by enc
|
|
def name_in(enc)
|
|
Iconv.conv(enc, name_encoding, @name)
|
|
end
|
|
|
|
# Returns the name in the encoding specified by enc
|
|
def comment_in(enc)
|
|
Iconv.conv(enc, name_encoding, @name)
|
|
end
|
|
|
|
def initialize(zipfile = "", name = "", comment = "", extra = "",
|
|
compressed_size = 0, crc = 0,
|
|
compression_method = ZipEntry::DEFLATED, size = 0,
|
|
time = Time.now)
|
|
super()
|
|
if name.starts_with("/")
|
|
raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
|
|
end
|
|
@localHeaderOffset = 0
|
|
@local_header_size = 0
|
|
@internalFileAttributes = 1
|
|
@externalFileAttributes = 0
|
|
@version = 52 # this library's version
|
|
@ftype = nil # unspecified or unknown
|
|
@filepath = nil
|
|
if Zip::RUNNING_ON_WINDOWS
|
|
@fstype = FSTYPE_FAT
|
|
else
|
|
@fstype = FSTYPE_UNIX
|
|
end
|
|
@zipfile, @comment, @compressed_size, @crc, @extra, @compression_method,
|
|
@name, @size = zipfile, comment, compressed_size, crc,
|
|
extra, compression_method, name, size
|
|
@time = time
|
|
|
|
@follow_symlinks = false
|
|
|
|
@restore_times = true
|
|
@restore_permissions = false
|
|
@restore_ownership = false
|
|
|
|
# BUG: need an extra field to support uid/gid's
|
|
@unix_uid = nil
|
|
@unix_gid = nil
|
|
@unix_perms = nil
|
|
# @posix_acl = nil
|
|
# @ntfs_acl = nil
|
|
|
|
if name_is_directory?
|
|
@ftype = :directory
|
|
else
|
|
@ftype = :file
|
|
end
|
|
|
|
unless ZipExtraField === @extra
|
|
@extra = ZipExtraField.new(@extra.to_s)
|
|
end
|
|
end
|
|
|
|
def time
|
|
if @extra["UniversalTime"]
|
|
@extra["UniversalTime"].mtime
|
|
else
|
|
# Atandard time field in central directory has local time
|
|
# under archive creator. Then, we can't get timezone.
|
|
@time
|
|
end
|
|
end
|
|
alias :mtime :time
|
|
|
|
def time=(aTime)
|
|
unless @extra.member?("UniversalTime")
|
|
@extra.create("UniversalTime")
|
|
end
|
|
@extra["UniversalTime"].mtime = aTime
|
|
@time = aTime
|
|
end
|
|
|
|
# Returns +true+ if the entry is a directory.
|
|
def directory?
|
|
raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
|
|
@ftype == :directory
|
|
end
|
|
alias :is_directory :directory?
|
|
|
|
# Returns +true+ if the entry is a file.
|
|
def file?
|
|
raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
|
|
@ftype == :file
|
|
end
|
|
|
|
# Returns +true+ if the entry is a symlink.
|
|
def symlink?
|
|
raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
|
|
@ftype == :symlink
|
|
end
|
|
|
|
def name_is_directory? #:nodoc:all
|
|
(%r{\/$} =~ @name) != nil
|
|
end
|
|
|
|
def local_entry_offset #:nodoc:all
|
|
localHeaderOffset + @local_header_size
|
|
end
|
|
|
|
def calculate_local_header_size #:nodoc:all
|
|
LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0)
|
|
end
|
|
|
|
def cdir_header_size #:nodoc:all
|
|
CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) +
|
|
(@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0)
|
|
end
|
|
|
|
def next_header_offset #:nodoc:all
|
|
local_entry_offset + self.compressed_size
|
|
end
|
|
|
|
# Extracts entry to file destPath (defaults to @name).
|
|
def extract(destPath = @name, &onExistsProc)
|
|
onExistsProc ||= proc { false }
|
|
|
|
if directory?
|
|
create_directory(destPath, &onExistsProc)
|
|
elsif file?
|
|
write_file(destPath, &onExistsProc)
|
|
elsif symlink?
|
|
create_symlink(destPath, &onExistsProc)
|
|
else
|
|
raise RuntimeError, "unknown file type #{self.inspect}"
|
|
end
|
|
|
|
self
|
|
end
|
|
|
|
def to_s
|
|
@name
|
|
end
|
|
|
|
protected
|
|
|
|
def ZipEntry.read_zip_short(io) # :nodoc:
|
|
io.read(2).unpack('v')[0]
|
|
end
|
|
|
|
def ZipEntry.read_zip_long(io) # :nodoc:
|
|
io.read(4).unpack('V')[0]
|
|
end
|
|
public
|
|
|
|
LOCAL_ENTRY_SIGNATURE = 0x04034b50
|
|
LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30
|
|
LOCAL_ENTRY_TRAILING_DESCRIPTOR_LENGTH = 4+4+4
|
|
VERSION_NEEDED_TO_EXTRACT = 10
|
|
|
|
def read_local_entry(io) #:nodoc:all
|
|
@localHeaderOffset = io.tell
|
|
staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH)
|
|
unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH)
|
|
raise ZipError, "Premature end of file. Not enough data for zip entry local header"
|
|
end
|
|
|
|
@header_signature ,
|
|
@version ,
|
|
@fstype ,
|
|
@gp_flags ,
|
|
@compression_method,
|
|
lastModTime ,
|
|
lastModDate ,
|
|
@crc ,
|
|
@compressed_size ,
|
|
@size ,
|
|
nameLength ,
|
|
extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv')
|
|
|
|
unless (@header_signature == LOCAL_ENTRY_SIGNATURE)
|
|
raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
|
|
end
|
|
set_time(lastModDate, lastModTime)
|
|
|
|
|
|
@name = io.read(nameLength)
|
|
extra = io.read(extraLength)
|
|
|
|
if (extra && extra.length != extraLength)
|
|
raise ZipError, "Truncated local zip entry header"
|
|
else
|
|
if ZipExtraField === @extra
|
|
@extra.merge(extra)
|
|
else
|
|
@extra = ZipExtraField.new(extra)
|
|
end
|
|
end
|
|
@local_header_size = calculate_local_header_size
|
|
end
|
|
|
|
def ZipEntry.read_local_entry(io)
|
|
entry = new(io.path)
|
|
entry.read_local_entry(io)
|
|
return entry
|
|
rescue ZipError
|
|
return nil
|
|
end
|
|
|
|
def write_local_entry(io) #:nodoc:all
|
|
@localHeaderOffset = io.tell
|
|
|
|
io <<
|
|
[LOCAL_ENTRY_SIGNATURE ,
|
|
VERSION_NEEDED_TO_EXTRACT , # version needed to extract
|
|
0 , # @gp_flags ,
|
|
@compression_method ,
|
|
@time.to_binary_dos_time , # @lastModTime ,
|
|
@time.to_binary_dos_date , # @lastModDate ,
|
|
@crc ,
|
|
@compressed_size ,
|
|
@size ,
|
|
@name ? @name.length : 0,
|
|
@extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv')
|
|
io << @name
|
|
io << (@extra ? @extra.to_local_bin : "")
|
|
end
|
|
|
|
CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
|
|
CDIR_ENTRY_STATIC_HEADER_LENGTH = 46
|
|
|
|
def read_c_dir_entry(io) #:nodoc:all
|
|
staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH)
|
|
unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH)
|
|
raise ZipError, "Premature end of file. Not enough data for zip cdir entry header"
|
|
end
|
|
|
|
@header_signature ,
|
|
@version , # version of encoding software
|
|
@fstype , # filesystem type
|
|
@versionNeededToExtract,
|
|
@gp_flags ,
|
|
@compression_method ,
|
|
lastModTime ,
|
|
lastModDate ,
|
|
@crc ,
|
|
@compressed_size ,
|
|
@size ,
|
|
nameLength ,
|
|
extraLength ,
|
|
commentLength ,
|
|
diskNumberStart ,
|
|
@internalFileAttributes,
|
|
@externalFileAttributes,
|
|
@localHeaderOffset ,
|
|
@name ,
|
|
@extra ,
|
|
@comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV')
|
|
|
|
unless (@header_signature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE)
|
|
raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
|
|
end
|
|
set_time(lastModDate, lastModTime)
|
|
|
|
@name = io.read(nameLength)
|
|
if ZipExtraField === @extra
|
|
@extra.merge(io.read(extraLength))
|
|
else
|
|
@extra = ZipExtraField.new(io.read(extraLength))
|
|
end
|
|
@comment = io.read(commentLength)
|
|
unless (@comment && @comment.length == commentLength)
|
|
raise ZipError, "Truncated cdir zip entry header"
|
|
end
|
|
|
|
case @fstype
|
|
when FSTYPE_UNIX
|
|
@unix_perms = (@externalFileAttributes >> 16) & 07777
|
|
|
|
case (@externalFileAttributes >> 28)
|
|
when 04
|
|
@ftype = :directory
|
|
when 010
|
|
@ftype = :file
|
|
when 012
|
|
@ftype = :symlink
|
|
else
|
|
raise ZipInternalError, "unknown file type #{'0%o' % (@externalFileAttributes >> 28)}"
|
|
end
|
|
else
|
|
if name_is_directory?
|
|
@ftype = :directory
|
|
else
|
|
@ftype = :file
|
|
end
|
|
end
|
|
@local_header_size = calculate_local_header_size
|
|
end
|
|
|
|
def ZipEntry.read_c_dir_entry(io) #:nodoc:all
|
|
entry = new(io.path)
|
|
entry.read_c_dir_entry(io)
|
|
return entry
|
|
rescue ZipError
|
|
return nil
|
|
end
|
|
|
|
def file_stat(path) # :nodoc:
|
|
if @follow_symlinks
|
|
return File::stat(path)
|
|
else
|
|
return File::lstat(path)
|
|
end
|
|
end
|
|
|
|
def get_extra_attributes_from_path(path) # :nodoc:
|
|
unless Zip::RUNNING_ON_WINDOWS
|
|
stat = file_stat(path)
|
|
@unix_uid = stat.uid
|
|
@unix_gid = stat.gid
|
|
@unix_perms = stat.mode & 07777
|
|
end
|
|
end
|
|
|
|
def set_extra_attributes_on_path(destPath) # :nodoc:
|
|
return unless (file? or directory?)
|
|
|
|
case @fstype
|
|
when FSTYPE_UNIX
|
|
# BUG: does not update timestamps into account
|
|
# ignore setuid/setgid bits by default. honor if @restore_ownership
|
|
unix_perms_mask = 01777
|
|
unix_perms_mask = 07777 if (@restore_ownership)
|
|
FileUtils::chmod(@unix_perms & unix_perms_mask, destPath) if (@restore_permissions && @unix_perms)
|
|
FileUtils::chown(@unix_uid, @unix_gid, destPath) if (@restore_ownership && @unix_uid && @unix_gid && Process::egid == 0)
|
|
# File::utimes()
|
|
end
|
|
end
|
|
|
|
def write_c_dir_entry(io) #:nodoc:all
|
|
case @fstype
|
|
when FSTYPE_UNIX
|
|
ft = nil
|
|
case @ftype
|
|
when :file
|
|
ft = 010
|
|
@unix_perms ||= 0644
|
|
when :directory
|
|
ft = 004
|
|
@unix_perms ||= 0755
|
|
when :symlink
|
|
ft = 012
|
|
@unix_perms ||= 0755
|
|
else
|
|
raise ZipInternalError, "unknown file type #{self.inspect}"
|
|
end
|
|
|
|
@externalFileAttributes = (ft << 12 | (@unix_perms & 07777)) << 16
|
|
end
|
|
|
|
io <<
|
|
[CENTRAL_DIRECTORY_ENTRY_SIGNATURE,
|
|
@version , # version of encoding software
|
|
@fstype , # filesystem type
|
|
VERSION_NEEDED_TO_EXTRACT , # @versionNeededToExtract ,
|
|
0 , # @gp_flags ,
|
|
@compression_method ,
|
|
@time.to_binary_dos_time , # @lastModTime ,
|
|
@time.to_binary_dos_date , # @lastModDate ,
|
|
@crc ,
|
|
@compressed_size ,
|
|
@size ,
|
|
@name ? @name.length : 0 ,
|
|
@extra ? @extra.c_dir_length : 0 ,
|
|
@comment ? comment.length : 0 ,
|
|
0 , # disk number start
|
|
@internalFileAttributes , # file type (binary=0, text=1)
|
|
@externalFileAttributes , # native filesystem attributes
|
|
@localHeaderOffset ,
|
|
@name ,
|
|
@extra ,
|
|
@comment ].pack('VCCvvvvvVVVvvvvvVV')
|
|
|
|
io << @name
|
|
io << (@extra ? @extra.to_c_dir_bin : "")
|
|
io << @comment
|
|
end
|
|
|
|
def == (other)
|
|
return false unless other.class == self.class
|
|
# Compares contents of local entry and exposed fields
|
|
(@compression_method == other.compression_method &&
|
|
@crc == other.crc &&
|
|
@compressed_size == other.compressed_size &&
|
|
@size == other.size &&
|
|
@name == other.name &&
|
|
@extra == other.extra &&
|
|
@filepath == other.filepath &&
|
|
self.time.dos_equals(other.time))
|
|
end
|
|
|
|
def <=> (other)
|
|
return to_s <=> other.to_s
|
|
end
|
|
|
|
# Returns an IO like object for the given ZipEntry.
|
|
# Warning: may behave weird with symlinks.
|
|
def get_input_stream(&aProc)
|
|
if @ftype == :directory
|
|
return yield(NullInputStream.instance) if block_given?
|
|
return NullInputStream.instance
|
|
elsif @filepath
|
|
case @ftype
|
|
when :file
|
|
return File.open(@filepath, "rb", &aProc)
|
|
|
|
when :symlink
|
|
linkpath = File::readlink(@filepath)
|
|
stringio = StringIO.new(linkpath)
|
|
return yield(stringio) if block_given?
|
|
return stringio
|
|
else
|
|
raise "unknown @ftype #{@ftype}"
|
|
end
|
|
else
|
|
zis = ZipInputStream.new(@zipfile, localHeaderOffset)
|
|
zis.get_next_entry
|
|
if block_given?
|
|
begin
|
|
return yield(zis)
|
|
ensure
|
|
zis.close
|
|
end
|
|
else
|
|
return zis
|
|
end
|
|
end
|
|
end
|
|
|
|
def gather_fileinfo_from_srcpath(srcPath) # :nodoc:
|
|
stat = file_stat(srcPath)
|
|
case stat.ftype
|
|
when 'file'
|
|
if name_is_directory?
|
|
raise ArgumentError,
|
|
"entry name '#{newEntry}' indicates directory entry, but "+
|
|
"'#{srcPath}' is not a directory"
|
|
end
|
|
@ftype = :file
|
|
when 'directory'
|
|
if ! name_is_directory?
|
|
@name += "/"
|
|
end
|
|
@ftype = :directory
|
|
when 'link'
|
|
if name_is_directory?
|
|
raise ArgumentError,
|
|
"entry name '#{newEntry}' indicates directory entry, but "+
|
|
"'#{srcPath}' is not a directory"
|
|
end
|
|
@ftype = :symlink
|
|
else
|
|
raise RuntimeError, "unknown file type: #{srcPath.inspect} #{stat.inspect}"
|
|
end
|
|
|
|
@filepath = srcPath
|
|
get_extra_attributes_from_path(@filepath)
|
|
end
|
|
|
|
def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all
|
|
if @ftype == :directory
|
|
aZipOutputStream.put_next_entry(self)
|
|
elsif @filepath
|
|
aZipOutputStream.put_next_entry(self)
|
|
get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) }
|
|
else
|
|
aZipOutputStream.copy_raw_entry(self)
|
|
end
|
|
end
|
|
|
|
def parent_as_string
|
|
entry_name = name.chomp("/")
|
|
slash_index = entry_name.rindex("/")
|
|
slash_index ? entry_name.slice(0, slash_index+1) : nil
|
|
end
|
|
|
|
def get_raw_input_stream(&aProc)
|
|
File.open(@zipfile, "rb", &aProc)
|
|
end
|
|
|
|
private
|
|
|
|
def set_time(binaryDosDate, binaryDosTime)
|
|
@time = Time.parse_binary_dos_format(binaryDosDate, binaryDosTime)
|
|
rescue ArgumentError
|
|
puts "Invalid date/time in zip entry"
|
|
end
|
|
|
|
def write_file(destPath, continueOnExistsProc = proc { false })
|
|
if File.exists?(destPath) && ! yield(self, destPath)
|
|
raise ZipDestinationFileExistsError,
|
|
"Destination '#{destPath}' already exists"
|
|
end
|
|
File.open(destPath, "wb") do |os|
|
|
get_input_stream do |is|
|
|
set_extra_attributes_on_path(destPath)
|
|
|
|
buf = ''
|
|
while buf = is.sysread(Decompressor::CHUNK_SIZE, buf)
|
|
os << buf
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def create_directory(destPath)
|
|
if File.directory? destPath
|
|
return
|
|
elsif File.exists? destPath
|
|
if block_given? && yield(self, destPath)
|
|
FileUtils::rm_f destPath
|
|
else
|
|
raise ZipDestinationFileExistsError,
|
|
"Cannot create directory '#{destPath}'. "+
|
|
"A file already exists with that name"
|
|
end
|
|
end
|
|
Dir.mkdir destPath
|
|
set_extra_attributes_on_path(destPath)
|
|
end
|
|
|
|
# BUG: create_symlink() does not use &onExistsProc
|
|
def create_symlink(destPath)
|
|
stat = nil
|
|
begin
|
|
stat = File::lstat(destPath)
|
|
rescue Errno::ENOENT
|
|
end
|
|
|
|
io = get_input_stream
|
|
linkto = io.read
|
|
|
|
if stat
|
|
if stat.symlink?
|
|
if File::readlink(destPath) == linkto
|
|
return
|
|
else
|
|
raise ZipDestinationFileExistsError,
|
|
"Cannot create symlink '#{destPath}'. "+
|
|
"A symlink already exists with that name"
|
|
end
|
|
else
|
|
raise ZipDestinationFileExistsError,
|
|
"Cannot create symlink '#{destPath}'. "+
|
|
"A file already exists with that name"
|
|
end
|
|
end
|
|
|
|
File::symlink(linkto, destPath)
|
|
end
|
|
end
|
|
|
|
|
|
# ZipOutputStream is the basic class for writing zip files. It is
|
|
# possible to create a ZipOutputStream object directly, passing
|
|
# the zip file name to the constructor, but more often than not
|
|
# the ZipOutputStream will be obtained from a ZipFile (perhaps using the
|
|
# ZipFileSystem interface) object for a particular entry in the zip
|
|
# archive.
|
|
#
|
|
# A ZipOutputStream inherits IOExtras::AbstractOutputStream in order
|
|
# to provide an IO-like interface for writing to a single zip
|
|
# entry. Beyond methods for mimicking an IO-object it contains
|
|
# the method put_next_entry that closes the current entry
|
|
# and creates a new.
|
|
#
|
|
# Please refer to ZipInputStream for example code.
|
|
#
|
|
# java.util.zip.ZipOutputStream is the original inspiration for this
|
|
# class.
|
|
|
|
class ZipOutputStream
|
|
include IOExtras::AbstractOutputStream
|
|
|
|
attr_accessor :comment
|
|
|
|
# Opens the indicated zip file. If a file with that name already
|
|
# exists it will be overwritten.
|
|
def initialize(fileName)
|
|
super()
|
|
@fileName = fileName
|
|
@outputStream = File.new(@fileName, "wb")
|
|
@entrySet = ZipEntrySet.new
|
|
@compressor = NullCompressor.instance
|
|
@closed = false
|
|
@currentEntry = nil
|
|
@comment = nil
|
|
end
|
|
|
|
# Same as #initialize but if a block is passed the opened
|
|
# stream is passed to the block and closed when the block
|
|
# returns.
|
|
def ZipOutputStream.open(fileName)
|
|
return new(fileName) unless block_given?
|
|
zos = new(fileName)
|
|
yield zos
|
|
ensure
|
|
zos.close if zos
|
|
end
|
|
|
|
# Closes the stream and writes the central directory to the zip file
|
|
def close
|
|
return if @closed
|
|
finalize_current_entry
|
|
update_local_headers
|
|
write_central_directory
|
|
@outputStream.close
|
|
@closed = true
|
|
end
|
|
|
|
# Closes the current entry and opens a new for writing.
|
|
# +entry+ can be a ZipEntry object or a string.
|
|
def put_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
|
|
raise ZipError, "zip stream is closed" if @closed
|
|
newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@fileName, entry.to_s)
|
|
init_next_entry(newEntry, level)
|
|
@currentEntry=newEntry
|
|
end
|
|
|
|
def copy_raw_entry(entry)
|
|
entry = entry.dup
|
|
raise ZipError, "zip stream is closed" if @closed
|
|
raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(ZipEntry)
|
|
finalize_current_entry
|
|
@entrySet << entry
|
|
src_pos = entry.local_entry_offset
|
|
entry.write_local_entry(@outputStream)
|
|
@compressor = NullCompressor.instance
|
|
entry.get_raw_input_stream {
|
|
|is|
|
|
is.seek(src_pos, IO::SEEK_SET)
|
|
IOExtras.copy_stream_n(@outputStream, is, entry.compressed_size)
|
|
}
|
|
@compressor = NullCompressor.instance
|
|
@currentEntry = nil
|
|
end
|
|
|
|
private
|
|
def finalize_current_entry
|
|
return unless @currentEntry
|
|
finish
|
|
@currentEntry.compressed_size = @outputStream.tell - @currentEntry.localHeaderOffset -
|
|
@currentEntry.calculate_local_header_size
|
|
@currentEntry.size = @compressor.size
|
|
@currentEntry.crc = @compressor.crc
|
|
@currentEntry = nil
|
|
@compressor = NullCompressor.instance
|
|
end
|
|
|
|
def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
|
|
finalize_current_entry
|
|
@entrySet << entry
|
|
entry.write_local_entry(@outputStream)
|
|
@compressor = get_compressor(entry, level)
|
|
end
|
|
|
|
def get_compressor(entry, level)
|
|
case entry.compression_method
|
|
when ZipEntry::DEFLATED then Deflater.new(@outputStream, level)
|
|
when ZipEntry::STORED then PassThruCompressor.new(@outputStream)
|
|
else raise ZipCompressionMethodError,
|
|
"Invalid compression method: '#{entry.compression_method}'"
|
|
end
|
|
end
|
|
|
|
def update_local_headers
|
|
pos = @outputStream.tell
|
|
@entrySet.each {
|
|
|entry|
|
|
@outputStream.pos = entry.localHeaderOffset
|
|
entry.write_local_entry(@outputStream)
|
|
}
|
|
@outputStream.pos = pos
|
|
end
|
|
|
|
def write_central_directory
|
|
cdir = ZipCentralDirectory.new(@entrySet, @comment)
|
|
cdir.write_to_stream(@outputStream)
|
|
end
|
|
|
|
protected
|
|
|
|
def finish
|
|
@compressor.finish
|
|
end
|
|
|
|
public
|
|
# Modeled after IO.<<
|
|
def << (data)
|
|
@compressor << data
|
|
end
|
|
end
|
|
|
|
|
|
class Compressor #:nodoc:all
|
|
def finish
|
|
end
|
|
end
|
|
|
|
class PassThruCompressor < Compressor #:nodoc:all
|
|
def initialize(outputStream)
|
|
super()
|
|
@outputStream = outputStream
|
|
@crc = Zlib::crc32
|
|
@size = 0
|
|
end
|
|
|
|
def << (data)
|
|
val = data.to_s
|
|
@crc = Zlib::crc32(val, @crc)
|
|
@size += val.size
|
|
@outputStream << val
|
|
end
|
|
|
|
attr_reader :size, :crc
|
|
end
|
|
|
|
class NullCompressor < Compressor #:nodoc:all
|
|
include Singleton
|
|
|
|
def << (data)
|
|
raise IOError, "closed stream"
|
|
end
|
|
|
|
attr_reader :size, :compressed_size
|
|
end
|
|
|
|
class Deflater < Compressor #:nodoc:all
|
|
def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION)
|
|
super()
|
|
@outputStream = outputStream
|
|
@zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS)
|
|
@size = 0
|
|
@crc = Zlib::crc32
|
|
end
|
|
|
|
def << (data)
|
|
val = data.to_s
|
|
@crc = Zlib::crc32(val, @crc)
|
|
@size += val.size
|
|
@outputStream << @zlibDeflater.deflate(data)
|
|
end
|
|
|
|
def finish
|
|
until @zlibDeflater.finished?
|
|
@outputStream << @zlibDeflater.finish
|
|
end
|
|
end
|
|
|
|
attr_reader :size, :crc
|
|
end
|
|
|
|
|
|
class ZipEntrySet #:nodoc:all
|
|
include Enumerable
|
|
|
|
def initialize(anEnumerable = [])
|
|
super()
|
|
@entrySet = {}
|
|
anEnumerable.each { |o| push(o) }
|
|
end
|
|
|
|
def include?(entry)
|
|
@entrySet.include?(entry.to_s)
|
|
end
|
|
|
|
def <<(entry)
|
|
@entrySet[entry.to_s] = entry
|
|
end
|
|
alias :push :<<
|
|
|
|
def size
|
|
@entrySet.size
|
|
end
|
|
alias :length :size
|
|
|
|
def delete(entry)
|
|
@entrySet.delete(entry.to_s) ? entry : nil
|
|
end
|
|
|
|
def each(&aProc)
|
|
@entrySet.values.each(&aProc)
|
|
end
|
|
|
|
def entries
|
|
@entrySet.values
|
|
end
|
|
|
|
# deep clone
|
|
def dup
|
|
newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup })
|
|
end
|
|
|
|
def == (other)
|
|
return false unless other.kind_of?(ZipEntrySet)
|
|
return @entrySet == other.entrySet
|
|
end
|
|
|
|
def parent(entry)
|
|
@entrySet[entry.parent_as_string]
|
|
end
|
|
|
|
def glob(pattern, flags = File::FNM_PATHNAME|File::FNM_DOTMATCH)
|
|
entries.select {
|
|
|entry|
|
|
File.fnmatch(pattern, entry.name.chomp('/'), flags)
|
|
}
|
|
end
|
|
|
|
#TODO attr_accessor :auto_create_directories
|
|
protected
|
|
attr_accessor :entrySet
|
|
end
|
|
|
|
|
|
class ZipCentralDirectory
|
|
include Enumerable
|
|
|
|
END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
|
|
MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18
|
|
STATIC_EOCD_SIZE = 22
|
|
|
|
attr_reader :comment
|
|
|
|
# Returns an Enumerable containing the entries.
|
|
def entries
|
|
@entrySet.entries
|
|
end
|
|
|
|
def initialize(entries = ZipEntrySet.new, comment = "") #:nodoc:
|
|
super()
|
|
@entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries)
|
|
@comment = comment
|
|
end
|
|
|
|
def write_to_stream(io) #:nodoc:
|
|
offset = io.tell
|
|
@entrySet.each { |entry| entry.write_c_dir_entry(io) }
|
|
write_e_o_c_d(io, offset)
|
|
end
|
|
|
|
def write_e_o_c_d(io, offset) #:nodoc:
|
|
io <<
|
|
[END_OF_CENTRAL_DIRECTORY_SIGNATURE,
|
|
0 , # @numberOfThisDisk
|
|
0 , # @numberOfDiskWithStartOfCDir
|
|
@entrySet? @entrySet.size : 0 ,
|
|
@entrySet? @entrySet.size : 0 ,
|
|
cdir_size ,
|
|
offset ,
|
|
@comment ? @comment.length : 0 ].pack('VvvvvVVv')
|
|
io << @comment
|
|
end
|
|
private :write_e_o_c_d
|
|
|
|
def cdir_size #:nodoc:
|
|
# does not include eocd
|
|
@entrySet.inject(0) { |value, entry| entry.cdir_header_size + value }
|
|
end
|
|
private :cdir_size
|
|
|
|
def read_e_o_c_d(io) #:nodoc:
|
|
buf = get_e_o_c_d(io)
|
|
@numberOfThisDisk = ZipEntry::read_zip_short(buf)
|
|
@numberOfDiskWithStartOfCDir = ZipEntry::read_zip_short(buf)
|
|
@totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::read_zip_short(buf)
|
|
@size = ZipEntry::read_zip_short(buf)
|
|
@sizeInBytes = ZipEntry::read_zip_long(buf)
|
|
@cdirOffset = ZipEntry::read_zip_long(buf)
|
|
commentLength = ZipEntry::read_zip_short(buf)
|
|
@comment = buf.read(commentLength)
|
|
raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0
|
|
end
|
|
|
|
def read_central_directory_entries(io) #:nodoc:
|
|
begin
|
|
io.seek(@cdirOffset, IO::SEEK_SET)
|
|
rescue Errno::EINVAL
|
|
raise ZipError, "Zip consistency problem while reading central directory entry"
|
|
end
|
|
@entrySet = ZipEntrySet.new
|
|
@size.times {
|
|
@entrySet << ZipEntry.read_c_dir_entry(io)
|
|
}
|
|
end
|
|
|
|
def read_from_stream(io) #:nodoc:
|
|
read_e_o_c_d(io)
|
|
read_central_directory_entries(io)
|
|
end
|
|
|
|
def get_e_o_c_d(io) #:nodoc:
|
|
begin
|
|
io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END)
|
|
rescue Errno::EINVAL
|
|
io.seek(0, IO::SEEK_SET)
|
|
rescue Errno::EFBIG # FreeBSD 4.9 raise Errno::EFBIG instead of Errno::EINVAL
|
|
io.seek(0, IO::SEEK_SET)
|
|
end
|
|
|
|
# 'buf = io.read' substituted with lump of code to work around FreeBSD 4.5 issue
|
|
retried = false
|
|
buf = nil
|
|
begin
|
|
buf = io.read
|
|
rescue Errno::EFBIG # FreeBSD 4.5 may raise Errno::EFBIG
|
|
raise if (retried)
|
|
retried = true
|
|
|
|
io.seek(0, IO::SEEK_SET)
|
|
retry
|
|
end
|
|
|
|
sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V'))
|
|
raise ZipError, "Zip end of central directory signature not found" unless sigIndex
|
|
buf=buf.slice!((sigIndex+4)...(buf.size))
|
|
def buf.read(count)
|
|
slice!(0, count)
|
|
end
|
|
return buf
|
|
end
|
|
|
|
# For iterating over the entries.
|
|
def each(&proc)
|
|
@entrySet.each(&proc)
|
|
end
|
|
|
|
# Returns the number of entries in the central directory (and
|
|
# consequently in the zip archive).
|
|
def size
|
|
@entrySet.size
|
|
end
|
|
|
|
def ZipCentralDirectory.read_from_stream(io) #:nodoc:
|
|
cdir = new
|
|
cdir.read_from_stream(io)
|
|
return cdir
|
|
rescue ZipError
|
|
return nil
|
|
end
|
|
|
|
def == (other) #:nodoc:
|
|
return false unless other.kind_of?(ZipCentralDirectory)
|
|
@entrySet.entries.sort == other.entries.sort && comment == other.comment
|
|
end
|
|
end
|
|
|
|
|
|
class ZipError < StandardError ; end
|
|
|
|
class ZipEntryExistsError < ZipError; end
|
|
class ZipDestinationFileExistsError < ZipError; end
|
|
class ZipCompressionMethodError < ZipError; end
|
|
class ZipEntryNameError < ZipError; end
|
|
class ZipInternalError < ZipError; end
|
|
|
|
# ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
|
|
# The most important methods are those inherited from
|
|
# ZipCentralDirectory for accessing information about the entries in
|
|
# the archive and methods such as get_input_stream and
|
|
# get_output_stream for reading from and writing entries to the
|
|
# archive. The class includes a few convenience methods such as
|
|
# #extract for extracting entries to the filesystem, and #remove,
|
|
# #replace, #rename and #mkdir for making simple modifications to
|
|
# the archive.
|
|
#
|
|
# Modifications to a zip archive are not committed until #commit or
|
|
# #close is called. The method #open accepts a block following
|
|
# the pattern from File.open offering a simple way to
|
|
# automatically close the archive when the block returns.
|
|
#
|
|
# The following example opens zip archive <code>my.zip</code>
|
|
# (creating it if it doesn't exist) and adds an entry
|
|
# <code>first.txt</code> and a directory entry <code>a_dir</code>
|
|
# to it.
|
|
#
|
|
# require 'zip/zip'
|
|
#
|
|
# Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
|
|
# |zipfile|
|
|
# zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
|
|
# zipfile.mkdir("a_dir")
|
|
# }
|
|
#
|
|
# The next example reopens <code>my.zip</code> writes the contents of
|
|
# <code>first.txt</code> to standard out and deletes the entry from
|
|
# the archive.
|
|
#
|
|
# require 'zip/zip'
|
|
#
|
|
# Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
|
|
# |zipfile|
|
|
# puts zipfile.read("first.txt")
|
|
# zipfile.remove("first.txt")
|
|
# }
|
|
#
|
|
# ZipFileSystem offers an alternative API that emulates ruby's
|
|
# interface for accessing the filesystem, ie. the File and Dir classes.
|
|
|
|
class ZipFile < ZipCentralDirectory
|
|
|
|
CREATE = 1
|
|
|
|
attr_reader :name
|
|
|
|
# default -> false
|
|
attr_accessor :restore_ownership
|
|
# default -> false
|
|
attr_accessor :restore_permissions
|
|
# default -> true
|
|
attr_accessor :restore_times
|
|
|
|
# Opens a zip archive. Pass true as the second parameter to create
|
|
# a new archive if it doesn't exist already.
|
|
def initialize(fileName, create = nil)
|
|
super()
|
|
@name = fileName
|
|
@comment = ""
|
|
if (File.exists?(fileName))
|
|
File.open(name, "rb") { |f| read_from_stream(f) }
|
|
elsif (create)
|
|
@entrySet = ZipEntrySet.new
|
|
else
|
|
raise ZipError, "File #{fileName} not found"
|
|
end
|
|
@create = create
|
|
@storedEntries = @entrySet.dup
|
|
|
|
@restore_ownership = false
|
|
@restore_permissions = false
|
|
@restore_times = true
|
|
end
|
|
|
|
# Same as #new. If a block is passed the ZipFile object is passed
|
|
# to the block and is automatically closed afterwards just as with
|
|
# ruby's builtin File.open method.
|
|
def ZipFile.open(fileName, create = nil)
|
|
zf = ZipFile.new(fileName, create)
|
|
if block_given?
|
|
begin
|
|
yield zf
|
|
ensure
|
|
zf.close
|
|
end
|
|
else
|
|
zf
|
|
end
|
|
end
|
|
|
|
# Returns the zip files comment, if it has one
|
|
attr_accessor :comment
|
|
|
|
# Iterates over the contents of the ZipFile. This is more efficient
|
|
# than using a ZipInputStream since this methods simply iterates
|
|
# through the entries in the central directory structure in the archive
|
|
# whereas ZipInputStream jumps through the entire archive accessing the
|
|
# local entry headers (which contain the same information as the
|
|
# central directory).
|
|
def ZipFile.foreach(aZipFileName, &block)
|
|
ZipFile.open(aZipFileName) {
|
|
|zipFile|
|
|
zipFile.each(&block)
|
|
}
|
|
end
|
|
|
|
# Returns an input stream to the specified entry. If a block is passed
|
|
# the stream object is passed to the block and the stream is automatically
|
|
# closed afterwards just as with ruby's builtin File.open method.
|
|
def get_input_stream(entry, &aProc)
|
|
get_entry(entry).get_input_stream(&aProc)
|
|
end
|
|
|
|
# Returns an output stream to the specified entry. If a block is passed
|
|
# the stream object is passed to the block and the stream is automatically
|
|
# closed afterwards just as with ruby's builtin File.open method.
|
|
def get_output_stream(entry, &aProc)
|
|
newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
|
|
if newEntry.directory?
|
|
raise ArgumentError,
|
|
"cannot open stream to directory entry - '#{newEntry}'"
|
|
end
|
|
zipStreamableEntry = ZipStreamableStream.new(newEntry)
|
|
@entrySet << zipStreamableEntry
|
|
zipStreamableEntry.get_output_stream(&aProc)
|
|
end
|
|
|
|
# Returns the name of the zip archive
|
|
def to_s
|
|
@name
|
|
end
|
|
|
|
# Returns a string containing the contents of the specified entry
|
|
def read(entry)
|
|
get_input_stream(entry) { |is| is.read }
|
|
end
|
|
|
|
# Convenience method for adding the contents of a file to the archive
|
|
def add(entry, srcPath, &continueOnExistsProc)
|
|
continueOnExistsProc ||= proc { false }
|
|
check_entry_exists(entry, continueOnExistsProc, "add")
|
|
newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
|
|
newEntry.gather_fileinfo_from_srcpath(srcPath)
|
|
@entrySet << newEntry
|
|
end
|
|
|
|
# Removes the specified entry.
|
|
def remove(entry)
|
|
@entrySet.delete(get_entry(entry))
|
|
end
|
|
|
|
# Renames the specified entry.
|
|
def rename(entry, newName, &continueOnExistsProc)
|
|
foundEntry = get_entry(entry)
|
|
check_entry_exists(newName, continueOnExistsProc, "rename")
|
|
@entrySet.delete(foundEntry)
|
|
foundEntry.name = newName
|
|
@entrySet << foundEntry
|
|
end
|
|
|
|
# Replaces the specified entry with the contents of srcPath (from
|
|
# the file system).
|
|
def replace(entry, srcPath)
|
|
check_file(srcPath)
|
|
add(remove(entry), srcPath)
|
|
end
|
|
|
|
# Extracts entry to file destPath.
|
|
def extract(entry, destPath, &onExistsProc)
|
|
onExistsProc ||= proc { false }
|
|
foundEntry = get_entry(entry)
|
|
foundEntry.extract(destPath, &onExistsProc)
|
|
end
|
|
|
|
# Commits changes that has been made since the previous commit to
|
|
# the zip archive.
|
|
def commit
|
|
return if ! commit_required?
|
|
on_success_replace(name) {
|
|
|tmpFile|
|
|
ZipOutputStream.open(tmpFile) {
|
|
|zos|
|
|
|
|
@entrySet.each { |e| e.write_to_zip_output_stream(zos) }
|
|
zos.comment = comment
|
|
}
|
|
true
|
|
}
|
|
initialize(name)
|
|
end
|
|
|
|
# Closes the zip file committing any changes that has been made.
|
|
def close
|
|
commit
|
|
end
|
|
|
|
# Returns true if any changes has been made to this archive since
|
|
# the previous commit
|
|
def commit_required?
|
|
return @entrySet != @storedEntries || @create == ZipFile::CREATE
|
|
end
|
|
|
|
# Searches for entry with the specified name. Returns nil if
|
|
# no entry is found. See also get_entry
|
|
def find_entry(entry)
|
|
@entrySet.detect {
|
|
|e|
|
|
e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "")
|
|
}
|
|
end
|
|
|
|
# Searches for an entry just as find_entry, but throws Errno::ENOENT
|
|
# if no entry is found.
|
|
def get_entry(entry)
|
|
selectedEntry = find_entry(entry)
|
|
unless selectedEntry
|
|
raise Errno::ENOENT, entry
|
|
end
|
|
selectedEntry.restore_ownership = @restore_ownership
|
|
selectedEntry.restore_permissions = @restore_permissions
|
|
selectedEntry.restore_times = @restore_times
|
|
|
|
return selectedEntry
|
|
end
|
|
|
|
# Creates a directory
|
|
def mkdir(entryName, permissionInt = 0755)
|
|
if find_entry(entryName)
|
|
raise Errno::EEXIST, "File exists - #{entryName}"
|
|
end
|
|
@entrySet << ZipStreamableDirectory.new(@name, entryName.to_s.ensure_end("/"), nil, permissionInt)
|
|
end
|
|
|
|
private
|
|
|
|
def is_directory(newEntry, srcPath)
|
|
srcPathIsDirectory = File.directory?(srcPath)
|
|
if newEntry.is_directory && ! srcPathIsDirectory
|
|
raise ArgumentError,
|
|
"entry name '#{newEntry}' indicates directory entry, but "+
|
|
"'#{srcPath}' is not a directory"
|
|
elsif ! newEntry.is_directory && srcPathIsDirectory
|
|
newEntry.name += "/"
|
|
end
|
|
return newEntry.is_directory && srcPathIsDirectory
|
|
end
|
|
|
|
def check_entry_exists(entryName, continueOnExistsProc, procedureName)
|
|
continueOnExistsProc ||= proc { false }
|
|
if @entrySet.detect { |e| e.name == entryName }
|
|
if continueOnExistsProc.call
|
|
remove get_entry(entryName)
|
|
else
|
|
raise ZipEntryExistsError,
|
|
procedureName+" failed. Entry #{entryName} already exists"
|
|
end
|
|
end
|
|
end
|
|
|
|
def check_file(path)
|
|
unless File.readable? path
|
|
raise Errno::ENOENT, path
|
|
end
|
|
end
|
|
|
|
def on_success_replace(aFilename)
|
|
tmpfile = get_tempfile
|
|
tmpFilename = tmpfile.path
|
|
tmpfile.close
|
|
if yield tmpFilename
|
|
File.rename(tmpFilename, name)
|
|
end
|
|
end
|
|
|
|
def get_tempfile
|
|
tempFile = Tempfile.new(File.basename(name), File.dirname(name))
|
|
tempFile.binmode
|
|
tempFile
|
|
end
|
|
|
|
end
|
|
|
|
class ZipStreamableDirectory < ZipEntry
|
|
def initialize(zipfile, entry, srcPath = nil, permissionInt = nil)
|
|
super(zipfile, entry)
|
|
|
|
@ftype = :directory
|
|
entry.get_extra_attributes_from_path(srcPath) if (srcPath)
|
|
@unix_perms = permissionInt if (permissionInt)
|
|
end
|
|
end
|
|
|
|
class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all
|
|
def initialize(entry)
|
|
super(entry)
|
|
@tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile))
|
|
@tempFile.binmode
|
|
end
|
|
|
|
def get_output_stream
|
|
if block_given?
|
|
begin
|
|
yield(@tempFile)
|
|
ensure
|
|
@tempFile.close
|
|
end
|
|
else
|
|
@tempFile
|
|
end
|
|
end
|
|
|
|
def get_input_stream
|
|
if ! @tempFile.closed?
|
|
raise StandardError, "cannot open entry for reading while its open for writing - #{name}"
|
|
end
|
|
@tempFile.open # reopens tempfile from top
|
|
@tempFile.binmode
|
|
if block_given?
|
|
begin
|
|
yield(@tempFile)
|
|
ensure
|
|
@tempFile.close
|
|
end
|
|
else
|
|
@tempFile
|
|
end
|
|
end
|
|
|
|
def write_to_zip_output_stream(aZipOutputStream)
|
|
aZipOutputStream.put_next_entry(self)
|
|
get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) }
|
|
end
|
|
end
|
|
|
|
class ZipExtraField < Hash
|
|
ID_MAP = {}
|
|
|
|
# Meta class for extra fields
|
|
class Generic
|
|
def self.register_map
|
|
if self.const_defined?(:HEADER_ID)
|
|
ID_MAP[self.const_get(:HEADER_ID)] = self
|
|
end
|
|
end
|
|
|
|
def self.name
|
|
self.to_s.split("::")[-1]
|
|
end
|
|
|
|
# return field [size, content] or false
|
|
def initial_parse(binstr)
|
|
if ! binstr
|
|
# If nil, start with empty.
|
|
return false
|
|
elsif binstr[0,2] != self.class.const_get(:HEADER_ID)
|
|
$stderr.puts "Warning: weired extra feild header ID. skip parsing"
|
|
return false
|
|
end
|
|
[binstr[2,2].unpack("v")[0], binstr[4..-1]]
|
|
end
|
|
|
|
def ==(other)
|
|
self.class != other.class and return false
|
|
each { |k, v|
|
|
v != other[k] and return false
|
|
}
|
|
true
|
|
end
|
|
|
|
def to_local_bin
|
|
s = pack_for_local
|
|
self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
|
|
end
|
|
|
|
def to_c_dir_bin
|
|
s = pack_for_c_dir
|
|
self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
|
|
end
|
|
end
|
|
|
|
# Info-ZIP Additional timestamp field
|
|
class UniversalTime < Generic
|
|
HEADER_ID = "UT"
|
|
register_map
|
|
|
|
def initialize(binstr = nil)
|
|
@ctime = nil
|
|
@mtime = nil
|
|
@atime = nil
|
|
@flag = nil
|
|
binstr and merge(binstr)
|
|
end
|
|
attr_accessor :atime, :ctime, :mtime, :flag
|
|
|
|
def merge(binstr)
|
|
binstr == "" and return
|
|
size, content = initial_parse(binstr)
|
|
size or return
|
|
@flag, mtime, atime, ctime = content.unpack("CVVV")
|
|
mtime and @mtime ||= Time.at(mtime)
|
|
atime and @atime ||= Time.at(atime)
|
|
ctime and @ctime ||= Time.at(ctime)
|
|
end
|
|
|
|
def ==(other)
|
|
@mtime == other.mtime &&
|
|
@atime == other.atime &&
|
|
@ctime == other.ctime
|
|
end
|
|
|
|
def pack_for_local
|
|
s = [@flag].pack("C")
|
|
@flag & 1 != 0 and s << [@mtime.to_i].pack("V")
|
|
@flag & 2 != 0 and s << [@atime.to_i].pack("V")
|
|
@flag & 4 != 0 and s << [@ctime.to_i].pack("V")
|
|
s
|
|
end
|
|
|
|
def pack_for_c_dir
|
|
s = [@flag].pack("C")
|
|
@flag & 1 == 1 and s << [@mtime.to_i].pack("V")
|
|
s
|
|
end
|
|
end
|
|
|
|
# Info-ZIP Extra for UNIX uid/gid
|
|
class IUnix < Generic
|
|
HEADER_ID = "Ux"
|
|
register_map
|
|
|
|
def initialize(binstr = nil)
|
|
@uid = 0
|
|
@gid = 0
|
|
binstr and merge(binstr)
|
|
end
|
|
attr_accessor :uid, :gid
|
|
|
|
def merge(binstr)
|
|
binstr == "" and return
|
|
size, content = initial_parse(binstr)
|
|
# size: 0 for central direcotry. 4 for local header
|
|
return if(! size || size == 0)
|
|
uid, gid = content.unpack("vv")
|
|
@uid ||= uid
|
|
@gid ||= gid
|
|
end
|
|
|
|
def ==(other)
|
|
@uid == other.uid &&
|
|
@gid == other.gid
|
|
end
|
|
|
|
def pack_for_local
|
|
[@uid, @gid].pack("vv")
|
|
end
|
|
|
|
def pack_for_c_dir
|
|
""
|
|
end
|
|
end
|
|
|
|
## start main of ZipExtraField < Hash
|
|
def initialize(binstr = nil)
|
|
binstr and merge(binstr)
|
|
end
|
|
|
|
def merge(binstr)
|
|
binstr == "" and return
|
|
i = 0
|
|
while i < binstr.length
|
|
id = binstr[i,2]
|
|
len = binstr[i+2,2].to_s.unpack("v")[0]
|
|
if id && ID_MAP.member?(id)
|
|
field_name = ID_MAP[id].name
|
|
if self.member?(field_name)
|
|
self[field_name].mergea(binstr[i, len+4])
|
|
else
|
|
field_obj = ID_MAP[id].new(binstr[i, len+4])
|
|
self[field_name] = field_obj
|
|
end
|
|
elsif id
|
|
unless self["Unknown"]
|
|
s = ""
|
|
class << s
|
|
alias_method :to_c_dir_bin, :to_s
|
|
alias_method :to_local_bin, :to_s
|
|
end
|
|
self["Unknown"] = s
|
|
end
|
|
if ! len || len+4 > binstr[i..-1].length
|
|
self["Unknown"] << binstr[i..-1]
|
|
break;
|
|
end
|
|
self["Unknown"] << binstr[i, len+4]
|
|
end
|
|
i += len+4
|
|
end
|
|
end
|
|
|
|
def create(name)
|
|
field_class = nil
|
|
ID_MAP.each { |id, klass|
|
|
if klass.name == name
|
|
field_class = klass
|
|
break
|
|
end
|
|
}
|
|
if ! field_class
|
|
raise ZipError, "Unknown extra field '#{name}'"
|
|
end
|
|
self[name] = field_class.new()
|
|
end
|
|
|
|
def to_local_bin
|
|
s = ""
|
|
each { |k, v|
|
|
s << v.to_local_bin
|
|
}
|
|
s
|
|
end
|
|
alias :to_s :to_local_bin
|
|
|
|
def to_c_dir_bin
|
|
s = ""
|
|
each { |k, v|
|
|
s << v.to_c_dir_bin
|
|
}
|
|
s
|
|
end
|
|
|
|
def c_dir_length
|
|
to_c_dir_bin.length
|
|
end
|
|
def local_length
|
|
to_local_bin.length
|
|
end
|
|
alias :c_dir_size :c_dir_length
|
|
alias :local_size :local_length
|
|
alias :length :local_length
|
|
alias :size :local_length
|
|
end # end ZipExtraField
|
|
|
|
end # Zip namespace module
|
|
|
|
|
|
|
|
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
|
# rubyzip is free software; you can redistribute it and/or
|
|
# modify it under the terms of the ruby license.
|