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)