lib/zip/zip.rb in rwdgutenberg-0.12 vs lib/zip/zip.rb in rwdgutenberg-0.13
- old
+ new
@@ -1,38 +1,86 @@
-
require 'delegate'
require 'singleton'
require 'tempfile'
require 'ftools'
+require 'stringio'
require 'zlib'
require 'lib/zip/stdrubyext'
require 'lib/zip/ioextras'
if Tempfile.superclass == SimpleDelegator
require 'zip/tempfile_bugfixed'
Tempfile = BugFix::Tempfile
end
-module Zlib
+module Zlib #:nodoc:all
if ! const_defined? :MAX_WBITS
MAX_WBITS = Zlib::Deflate.MAX_WBITS
end
end
module Zip
+ VERSION = '0.9.1'
+
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
@@ -40,34 +88,55 @@
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
+ 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
@@ -81,18 +150,14 @@
end
flush
return @currentEntry
end
- def read(numberOfBytes = nil)
- @decompressor.read(numberOfBytes)
- end
- protected
def produce_input
@decompressor.produce_input
end
-
+
def input_finished?
@decompressor.input_finished?
end
end
@@ -112,15 +177,15 @@
@zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
@outputBuffer=""
@hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
end
- def read(numberOfBytes = nil)
+ def sysread(numberOfBytes = nil, buf = nil)
readEverything = (numberOfBytes == nil)
while (readEverything || @outputBuffer.length < numberOfBytes)
break if internal_input_finished?
- @outputBuffer << internal_produce_input
+ @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
@@ -132,18 +197,28 @@
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
- @zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE))
+ 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
@@ -163,11 +238,11 @@
@readSoFar = 0
@hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
end
# TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
- def read(numberOfBytes = nil)
+ def sysread(numberOfBytes = nil, buf = nil)
if input_finished?
hasReturnedEmptyStringVal=@hasReturnedEmptyString
@hasReturnedEmptyString=true
return "" unless hasReturnedEmptyStringVal
return nil
@@ -175,47 +250,106 @@
if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead)
numberOfBytes = @charsToRead-@readSoFar
end
@readSoFar += numberOfBytes
- @inputStream.read(numberOfBytes)
+ @inputStream.read(numberOfBytes, buf)
end
def produce_input
- read(Decompressor::CHUNK_SIZE)
+ 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 read(numberOfBytes = nil)
+ 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
+ :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:
def initialize(zipfile = "", name = "", comment = "", extra = "",
compressed_size = 0, crc = 0,
compression_method = ZipEntry::DEFLATED, size = 0,
time = Time.now)
@@ -225,15 +359,41 @@
end
@localHeaderOffset = 0
@internalFileAttributes = 1
@externalFileAttributes = 0
@version = 52 # this library's version
- @fstype = 0 # default is fat
+ @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
@@ -254,19 +414,33 @@
end
@extra["UniversalTime"].mtime = aTime
@time = aTime
end
+ # Returns +true+ if the entry is a directory.
def directory?
- return (%r{\/$} =~ @name) != nil
+ 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?
- ! directory?
+ 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 == :link
+ end
+
+ def name_is_directory? #:nodoc:all
+ (%r{\/$} =~ @name) != nil
+ end
+
def local_entry_offset #:nodoc:all
localHeaderOffset + local_header_size
end
def local_header_size #:nodoc:all
@@ -279,50 +453,68 @@
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)
+ def ZipEntry.read_zip_short(io) # :nodoc:
io.read(2).unpack('v')[0]
end
- def ZipEntry.read_zip_long(io)
+ 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
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
- localHeader ,
+ @header_signature ,
@version ,
@fstype ,
- @gpFlags ,
+ @gp_flags ,
@compression_method,
lastModTime ,
lastModDate ,
@crc ,
@compressed_size ,
@size ,
nameLength ,
extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv')
- unless (localHeader == LOCAL_ENTRY_SIGNATURE)
+ 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)
@@ -351,11 +543,11 @@
@localHeaderOffset = io.tell
io <<
[LOCAL_ENTRY_SIGNATURE ,
0 ,
- 0 , # @gpFlags ,
+ 0 , # @gp_flags ,
@compression_method ,
@time.to_binary_dos_time , # @lastModTime ,
@time.to_binary_dos_date , # @lastModDate ,
@crc ,
@compressed_size ,
@@ -372,16 +564,16 @@
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
-
- cdirSignature ,
+
+ @header_signature ,
@version , # version of encoding software
@fstype , # filesystem type
@versionNeededToExtract,
- @gpFlags ,
+ @gp_flags ,
@compression_method ,
lastModTime ,
lastModDate ,
@crc ,
@compressed_size ,
@@ -395,11 +587,11 @@
@localHeaderOffset ,
@name ,
@extra ,
@comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV')
- unless (cdirSignature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE)
+ 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)
@@ -410,28 +602,101 @@
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 = :link
+ else
+ raise ZipInternalError, "unknown file type #{'0%o' % (@externalFileAttributes >> 28)}"
+ end
+ else
+ if name_is_directory?
+ @ftype = :directory
+ else
+ @ftype = :file
+ end
+ end
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)
+ File::chmod(@unix_perms & unix_perms_mask, destPath) if (@restore_permissions && @unix_perms)
+ File::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
0 , # @versionNeededToExtract ,
- 0 , # @gpFlags ,
+ 0 , # @gp_flags ,
@compression_method ,
@time.to_binary_dos_time , # @lastModTime ,
@time.to_binary_dos_date , # @lastModDate ,
@crc ,
@compressed_size ,
@@ -451,42 +716,99 @@
io << (@extra ? @extra.to_c_dir_bin : "")
io << @comment
end
def == (other)
- return false unless other.class == ZipEntry
+ 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 &&
+ @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
- def get_input_stream
- zis = ZipInputStream.new(@zipfile, localHeaderOffset)
- zis.get_next_entry
- if block_given?
- begin
- return yield(zis)
- ensure
- zis.close
- 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
- return zis
+ 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
- aZipOutputStream.copy_raw_entry(self)
+ 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("/")
@@ -496,23 +818,107 @@
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)
+ File.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
@@ -520,31 +926,37 @@
@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)
+ init_next_entry(newEntry, level)
@currentEntry=newEntry
end
def copy_raw_entry(entry)
entry = entry.dup
@@ -612,10 +1024,11 @@
def finish
@compressor.finish
end
public
+ # Modeled after IO.<<
def << (data)
@compressor << data
end
end
@@ -677,11 +1090,11 @@
attr_reader :size, :crc
end
- class ZipEntrySet
+ class ZipEntrySet #:nodoc:all
include Enumerable
def initialize(anEnumerable = [])
super()
@entrySet = {}
@@ -726,42 +1139,50 @@
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 #:nodoc:all
+ 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 = "")
+ 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)
+ 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)
+ def write_e_o_c_d(io, offset) #:nodoc:
io <<
[END_OF_CENTRAL_DIRECTORY_SIGNATURE,
0 , # @numberOfThisDisk
0 , # @numberOfDiskWithStartOfCDir
@entrySet? @entrySet.size : 0 ,
@@ -771,17 +1192,17 @@
@comment ? @comment.length : 0 ].pack('VvvvvVVv')
io << @comment
end
private :write_e_o_c_d
- def cdir_size
+ 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)
+ 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)
@@ -790,11 +1211,11 @@
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)
+ 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
@@ -802,50 +1223,66 @@
@size.times {
@entrySet << ZipEntry.read_c_dir_entry(io)
}
end
- def read_from_stream(io)
+ 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)
+ 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 returns Errno::EFBIG instead of Errno::EINVAL
+ rescue Errno::EFBIG # FreeBSD 4.9 raise Errno::EFBIG instead of Errno::EINVAL
io.seek(0, IO::SEEK_SET)
end
- buf = io.read
+
+ # '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)
+ def ZipCentralDirectory.read_from_stream(io) #:nodoc:
cdir = new
cdir.read_from_stream(io)
return cdir
rescue ZipError
return nil
end
- def == (other)
+ def == (other) #:nodoc:
return false unless other.kind_of?(ZipCentralDirectory)
@entrySet.entries.sort == other.entries.sort && comment == other.comment
end
end
@@ -854,32 +1291,92 @@
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 == ZipFile::CREATE)
+ 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
@@ -889,23 +1386,36 @@
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}'"
@@ -913,54 +1423,57 @@
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)
- if is_directory(newEntry, srcPath)
- @entrySet << ZipStreamableDirectory.new(newEntry)
- else
- @entrySet << ZipStreamableFile.new(newEntry, srcPath)
- end
+ 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")
foundEntry.name=newName
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)
- if foundEntry.is_directory
- create_directory(foundEntry, destPath, &onExistsProc)
- else
- write_file(foundEntry, destPath, &onExistsProc)
- end
+ 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) {
@@ -971,58 +1484,55 @@
}
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
- def mkdir(entryName, permissionInt = 0) #permissionInt ignored
+ # Creates a directory
+ def mkdir(entryName, permissionInt = 0755)
if find_entry(entryName)
raise Errno::EEXIST, "File exists - #{entryName}"
end
- @entrySet << ZipStreamableDirectory.new(ZipEntry.new(name, entryName.to_s.ensure_end("/")))
+ @entrySet << ZipStreamableDirectory.new(@name, entryName.to_s.ensure_end("/"), nil, permissionInt)
end
private
- def create_directory(entry, destPath)
- if File.directory? destPath
- return
- elsif File.exists? destPath
- if block_given? && yield(entry, destPath)
- File.rm_f destPath
- else
- raise ZipDestinationFileExistsError,
- "Cannot create directory '#{destPath}'. "+
- "A file already exists with that name"
- end
- end
- Dir.mkdir destPath
- end
-
def is_directory(newEntry, srcPath)
srcPathIsDirectory = File.directory?(srcPath)
if newEntry.is_directory && ! srcPathIsDirectory
raise ArgumentError,
"entry name '#{newEntry}' indicates directory entry, but "+
@@ -1043,22 +1553,10 @@
procedureName+" failed. Entry #{entryName} already exists"
end
end
end
- def write_file(entry, destPath, continueOnExistsProc = proc { false })
- if File.exists?(destPath) && ! yield(entry, destPath)
-
-# raise ZipDestinationFileExistsError,
- $stderr.puts "Destination '#{destPath}' already exists"
- end
- File.open(destPath, "wb") {
- |os|
- entry.get_input_stream { |is| os << is.read }
- }
- end
-
def check_file(path)
unless File.readable? path
raise Errno::ENOENT, path
end
end
@@ -1078,50 +1576,20 @@
tempFile
end
end
- class ZipStreamableFile < DelegateClass(ZipEntry) #:nodoc:all
- def initialize(entry, filepath)
- super(entry)
- @delegate = entry
- @filepath = filepath
- end
+ class ZipStreamableDirectory < ZipEntry
+ def initialize(zipfile, entry, srcPath = nil, permissionInt = nil)
+ super(zipfile, entry)
- def get_input_stream(&aProc)
- File.open(@filepath, "rb", &aProc)
+ @ftype = :directory
+ entry.get_extra_attributes_from_path(srcPath) if (srcPath)
+ @unix_perms = permissionInt if (permissionInt)
end
-
- def write_to_zip_output_stream(aZipOutputStream)
- aZipOutputStream.put_next_entry(self)
- aZipOutputStream << get_input_stream { |is| is.read }
- end
-
- def == (other)
- return false unless other.class == ZipStreamableFile
- @filepath == other.filepath && super(other.delegate)
- end
-
- protected
- attr_reader :filepath, :delegate
end
- class ZipStreamableDirectory < DelegateClass(ZipEntry) #:nodoc:all
- def initialize(entry)
- super(entry)
- end
-
- def get_input_stream(&aProc)
- return yield(NullInputStream.instance) if block_given?
- NullInputStream.instance
- end
-
- def write_to_zip_output_stream(aZipOutputStream)
- aZipOutputStream.put_next_entry(self)
- end
- end
-
class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all
def initialize(entry)
super(entry)
@tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile))
@tempFile.binmode
@@ -1142,10 +1610,11 @@
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
@@ -1155,11 +1624,11 @@
end
end
def write_to_zip_output_stream(aZipOutputStream)
aZipOutputStream.put_next_entry(self)
- aZipOutputStream << get_input_stream { |is| is.read }
+ get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) }
end
end
class ZipExtraField < Hash
ID_MAP = {}
@@ -1256,11 +1725,11 @@
class IUnix < Generic
HEADER_ID = "Ux"
register_map
def initialize(binstr = nil)
- @uid = nil
- @gid = nil
+ @uid = 0
+ @gid = 0
binstr and merge(binstr)
end
attr_accessor :uid, :gid
def merge(binstr)