rubyzip/zip.rb

1215 lines
30 KiB
Ruby
Raw Normal View History

2002-01-03 01:48:31 +08:00
#!/usr/bin/env ruby
2002-03-18 03:36:02 +08:00
require 'delegate'
2002-01-03 01:48:31 +08:00
require 'singleton'
2002-03-18 00:22:19 +08:00
require 'tempfile'
require 'ftools'
2002-01-03 01:48:31 +08:00
require 'zlib'
2002-07-26 22:12:34 +08:00
require 'zipfilesystem'
2002-01-03 01:48:31 +08:00
unless Enumerable.instance_methods.include?("inject")
module Enumerable #:nodoc:all
def inject(n = 0)
each { |value| n = yield(n, value) }
n
end
2002-01-26 03:37:48 +08:00
end
end
class String
def startsWith(aString)
slice(0, aString.size) == aString
end
def endsWith(aString)
aStringSize = aString.size
slice(-aStringSize, aStringSize) == aString
end
def ensureEnd(aString)
endsWith(aString) ? self : self + aString
end
end
class Time
#MS-DOS File Date and Time format as used in Interrupt 21H Function 57H:
#
# Register CX, the Time:
# Bits 0-4 2 second increments (0-29)
# Bits 5-10 minutes (0-59)
# bits 11-15 hours (0-24)
#
# Register DX, the Date:
# Bits 0-4 day (1-31)
# bits 5-8 month (1-12)
# bits 9-15 year (four digit year minus 1980)
def toBinaryDosDate
(sec/2) +
(min << 5) +
(hour << 11)
end
def toBinaryDosTime
(day) +
(month << 5) +
((year - 1980) << 9)
end
# Dos time is only stored with two seconds accuracy
def dosEquals(other)
(year == other.year &&
month == other.month &&
day == other.day &&
hour == other.hour &&
min == other.min &&
sec/2 == other.sec/2)
end
def self.parseBinaryDosFormat(binaryDosDate, binaryDosTime)
second = 2 * ( 0b11111 & binaryDosTime)
minute = ( 0b11111100000 & binaryDosTime) >> 5
hour = (0b1111100000000000 & binaryDosTime) >> 11
day = ( 0b11111 & binaryDosDate)
month = ( 0b111100000 & binaryDosDate) >> 5
year = ((0b1111111000000000 & binaryDosDate) >> 9) + 1980
begin
return Time.local(year, month, day, hour, minute, second)
end
end
end
module Zip
RUBY_MINOR_VERSION = VERSION.split(".")[1].to_i
2002-03-18 00:22:19 +08:00
module FakeIO
def kind_of?(object)
object == IO || super
end
end
# Implements many of the convenience methods of IO
# such as gets, getc, readline and readlines
# depends on: inputFinished?, produceInput and read
module AbstractInputStream
include Enumerable
include FakeIO
def initialize
super
@lineno = 0
@outputBuffer = ""
end
attr_accessor :lineno
def readlines(aSepString = $/)
retVal = []
each_line(aSepString) { |line| retVal << line }
return retVal
end
def gets(aSepString=$/)
@lineno = @lineno.next
return read if aSepString == nil
aSepString="#{$/}#{$/}" if aSepString == ""
bufferIndex=0
while ((matchIndex = @outputBuffer.index(aSepString, bufferIndex)) == nil)
bufferIndex=@outputBuffer.length
if inputFinished?
return @outputBuffer.empty? ? nil : flush
end
@outputBuffer << produceInput
2002-01-03 01:48:31 +08:00
end
sepIndex=matchIndex + aSepString.length
return @outputBuffer.slice!(0...sepIndex)
2002-01-03 01:48:31 +08:00
end
def flush
retVal=@outputBuffer
@outputBuffer=""
return retVal
end
def readline(aSepString = $/)
retVal = gets(aSepString)
raise EOFError if retVal == nil
return retVal
2002-01-03 01:48:31 +08:00
end
def each_line(aSepString = $/)
while true
yield readline(aSepString)
end
2002-01-03 01:48:31 +08:00
rescue EOFError
end
alias_method :each, :each_line
2002-01-03 01:48:31 +08:00
end
2002-01-03 01:48:31 +08:00
#relies on <<
module AbstractOutputStream
include FakeIO
def write(data)
self << data
data.to_s.length
end
2002-01-03 01:48:31 +08:00
def print(*params)
self << params.to_s << $\.to_s
end
2002-01-03 01:48:31 +08:00
def printf(aFormatString, *params)
self << sprintf(aFormatString, *params)
end
2002-01-03 01:48:31 +08:00
def putc(anObject)
self << case anObject
when Fixnum then anObject.chr
when String then anObject
else raise TypeError, "putc: Only Fixnum and String supported"
end
anObject
end
def puts(*params)
params << "\n" if params.empty?
params.flatten.each {
|element|
val = element.to_s
self << val
self << "\n" unless val[-1,1] == "\n"
}
end
2002-01-03 01:48:31 +08:00
end
class ZipInputStream
include AbstractInputStream
def initialize(filename, offset = 0)
super()
@archiveIO = File.open(filename, "rb")
@archiveIO.seek(offset, IO::SEEK_SET)
2002-01-03 01:48:31 +08:00
@decompressor = NullDecompressor.instance
@currentEntry = nil
2002-01-03 01:48:31 +08:00
end
def close
@archiveIO.close
end
def ZipInputStream.open(filename)
return new(filename) unless block_given?
zio = new(filename)
yield zio
2002-07-20 00:48:41 +08:00
ensure
zio.close if zio
end
2002-07-20 00:48:41 +08:00
def getNextEntry
@archiveIO.seek(@currentEntry.nextHeaderOffset,
IO::SEEK_SET) if @currentEntry
openEntry
end
def rewind
return if @currentEntry.nil?
@lineno = 0
@archiveIO.seek(@currentEntry.localHeaderOffset,
IO::SEEK_SET)
openEntry
end
def openEntry
@currentEntry = ZipEntry.readLocalEntry(@archiveIO)
if (@currentEntry == nil)
@decompressor = NullDecompressor.instance
elsif @currentEntry.compressionMethod == ZipEntry::STORED
@decompressor = PassThruDecompressor.new(@archiveIO,
@currentEntry.size)
elsif @currentEntry.compressionMethod == ZipEntry::DEFLATED
@decompressor = Inflater.new(@archiveIO)
else
raise ZipCompressionMethodError,
"Unsupported compression method #{@currentEntry.compressionMethod}"
end
flush
return @currentEntry
end
def read(numberOfBytes = nil)
@decompressor.read(numberOfBytes)
end
protected
def produceInput
@decompressor.produceInput
end
def inputFinished?
@decompressor.inputFinished?
2002-01-03 01:48:31 +08:00
end
end
class Decompressor #:nodoc:all
CHUNK_SIZE=32768
def initialize(inputStream)
super()
@inputStream=inputStream
end
2002-01-05 08:13:58 +08:00
end
class Inflater < Decompressor #:nodoc:all
def initialize(inputStream)
super
@zlibInflater = Zlib::Inflate.new(-Zlib::Inflate::MAX_WBITS)
@outputBuffer=""
@hasReturnedEmptyString = (RUBY_MINOR_VERSION >= 7)
end
def read(numberOfBytes = nil)
readEverything = (numberOfBytes == nil)
while (readEverything || @outputBuffer.length < numberOfBytes)
break if internalInputFinished?
@outputBuffer << internalProduceInput
end
return valueWhenFinished if @outputBuffer.length==0 && inputFinished?
endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes
return @outputBuffer.slice!(0...endIndex)
end
def produceInput
if (@outputBuffer.empty?)
return internalProduceInput
else
return @outputBuffer.slice!(0...(@outputBuffer.length))
end
end
# to be used with produceInput, not read (as read may still have more data cached)
def inputFinished?
@outputBuffer.empty? && internalInputFinished?
end
private
def internalProduceInput
@zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE))
end
def internalInputFinished?
@zlibInflater.finished?
end
# TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
def valueWhenFinished # mimic behaviour of ruby File object.
return nil if @hasReturnedEmptyString
@hasReturnedEmptyString=true
return ""
end
2002-01-03 01:48:31 +08:00
end
class PassThruDecompressor < Decompressor #:nodoc:all
def initialize(inputStream, charsToRead)
super inputStream
@charsToRead = charsToRead
@readSoFar = 0
@hasReturnedEmptyString = (RUBY_MINOR_VERSION >= 7)
end
# TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
def read(numberOfBytes = nil)
if inputFinished?
hasReturnedEmptyStringVal=@hasReturnedEmptyString
@hasReturnedEmptyString=false
return "" unless hasReturnedEmptyStringVal
return nil
end
if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead)
numberOfBytes = @charsToRead-@readSoFar
end
@readSoFar += numberOfBytes
@inputStream.read(numberOfBytes)
end
def produceInput
read(Decompressor::CHUNK_SIZE)
end
def inputFinished?
(@readSoFar >= @charsToRead)
end
2002-01-03 01:48:31 +08:00
end
class NullDecompressor #:nodoc:all
include Singleton
def read(numberOfBytes = nil)
nil
end
def produceInput
nil
end
def inputFinished?
true
end
2002-01-03 01:48:31 +08:00
end
class NullInputStream < NullDecompressor #:nodoc:all
include AbstractInputStream
end
class ZipEntry
STORED = 0
DEFLATED = 8
attr_accessor :comment, :compressedSize, :crc, :extra, :compressionMethod,
:name, :size, :localHeaderOffset, :time
2002-07-27 04:58:51 +08:00
alias :mtime :time
def initialize(zipfile = "", name = "", comment = "", extra = "",
compressedSize = 0, crc = 0,
compressionMethod = ZipEntry::DEFLATED, size = 0,
time = Time.now)
super()
if name.startsWith("/")
raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
end
@localHeaderOffset = 0
@zipfile, @comment, @compressedSize, @crc, @extra, @compressionMethod,
@name, @size = zipfile, comment, compressedSize, crc,
extra, compressionMethod, name, size
@time = time
end
2002-07-26 22:12:34 +08:00
def directory?
return (%r{\/$} =~ @name) != nil
end
2002-07-26 22:12:34 +08:00
alias :isDirectory :directory?
2002-07-26 23:06:42 +08:00
def file?
! directory?
end
def localEntryOffset #:nodoc:all
localHeaderOffset + localHeaderSize
end
def localHeaderSize #:nodoc:all
2002-01-26 03:37:48 +08:00
LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.size : 0)
end
def cdirHeaderSize #:nodoc:all
2002-01-26 03:37:48 +08:00
CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) +
(@extra ? @extra.size : 0) + (@comment ? @comment.size : 0)
end
def nextHeaderOffset #:nodoc:all
localEntryOffset + self.compressedSize
end
def to_s
@name
end
protected
def ZipEntry.readZipShort(io)
io.read(2).unpack('v')[0]
end
def ZipEntry.readZipLong(io)
io.read(4).unpack('V')[0]
end
public
LOCAL_ENTRY_SIGNATURE = 0x04034b50
LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30
def readLocalEntry(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 ,
@version ,
@gpFlags ,
@compressionMethod,
lastModTime ,
lastModDate ,
@crc ,
@compressedSize ,
@size ,
nameLength ,
extraLength = staticSizedFieldsBuf.unpack('VvvvvvVVVvv')
unless (localHeader == LOCAL_ENTRY_SIGNATURE)
raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
end
2002-04-25 02:59:13 +08:00
setTime(lastModDate, lastModTime)
@name = io.read(nameLength)
@extra = io.read(extraLength)
unless (@extra && @extra.length == extraLength)
raise ZipError, "Truncated local zip entry header"
end
end
def ZipEntry.readLocalEntry(io)
entry = new(io.path)
entry.readLocalEntry(io)
return entry
rescue ZipError
return nil
end
def writeLocalEntry(io) #:nodoc:all
@localHeaderOffset = io.tell
io <<
[LOCAL_ENTRY_SIGNATURE ,
0 , # @version ,
0 , # @gpFlags ,
@compressionMethod ,
@time.toBinaryDosDate , # @lastModTime ,
@time.toBinaryDosTime , # @lastModDate ,
@crc ,
@compressedSize ,
@size ,
@name ? @name.length : 0,
@extra? @extra.length : 0 ].pack('VvvvvvVVVvv')
io << @name
io << @extra
end
CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
2002-01-26 03:37:48 +08:00
CDIR_ENTRY_STATIC_HEADER_LENGTH = 46
def readCDirEntry(io) #:nodoc:all
2002-01-26 03:37:48 +08:00
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 ,
@version ,
@versionNeededToExtract,
@gpFlags ,
@compressionMethod ,
lastModTime ,
lastModDate ,
@crc ,
@compressedSize ,
@size ,
nameLength ,
extraLength ,
commentLength ,
diskNumberStart ,
@internalFileAttributes,
@externalFileAttributes,
@localHeaderOffset ,
@name ,
@extra ,
@comment = staticSizedFieldsBuf.unpack('VvvvvvvVVVvvvvvVV')
unless (cdirSignature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE)
raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
end
2002-04-25 02:59:13 +08:00
setTime(lastModDate, lastModTime)
@name = io.read(nameLength)
@extra = io.read(extraLength)
@comment = io.read(commentLength)
unless (@comment && @comment.length == commentLength)
raise ZipError, "Truncated cdir zip entry header"
end
end
def ZipEntry.readCDirEntry(io) #:nodoc:all
entry = new(io.path)
entry.readCDirEntry(io)
return entry
rescue ZipError
return nil
end
2002-01-03 01:48:31 +08:00
def writeCDirEntry(io) #:nodoc:all
io <<
[CENTRAL_DIRECTORY_ENTRY_SIGNATURE,
0 , # @version ,
0 , # @versionNeededToExtract ,
0 , # @gpFlags ,
@compressionMethod ,
@time.toBinaryDosDate , # @lastModTime ,
@time.toBinaryDosTime , # @lastModDate ,
@crc ,
@compressedSize ,
@size ,
@name ? @name.length : 0 ,
@extra ? @extra.length : 0 ,
@comment ? comment.length : 0 ,
0 , # disk number start
0 , # @internalFileAttributes ,
0 , # @externalFileAttributes ,
@localHeaderOffset ,
@name ,
@extra ,
@comment ].pack('VvvvvvvVVVvvvvvVV')
io << @name
io << @extra
io << @comment
end
2002-01-26 03:37:48 +08:00
def == (other)
return false unless other.class == ZipEntry
2002-01-26 03:37:48 +08:00
# Compares contents of local entry and exposed fields
(@compressionMethod == other.compressionMethod &&
@crc == other.crc &&
@compressedSize == other.compressedSize &&
@size == other.size &&
@name == other.name &&
@extra == other.extra &&
@time.dosEquals(other.time))
2002-02-23 21:16:08 +08:00
end
2002-09-08 02:21:19 +08:00
def <=> (other)
return to_s <=> other.to_s
2002-09-08 02:21:19 +08:00
end
def getInputStream
zis = ZipInputStream.new(@zipfile, localHeaderOffset)
2002-03-18 03:36:02 +08:00
zis.getNextEntry
if block_given?
begin
return yield(zis)
2002-03-18 03:36:02 +08:00
ensure
zis.close
end
else
return zis
end
2002-02-23 21:16:08 +08:00
end
def writeToZipOutputStream(aZipOutputStream) #:nodoc:all
aZipOutputStream.putNextEntry(self.dup)
aZipOutputStream << getRawInputStream {
|is|
is.seek(localEntryOffset, IO::SEEK_SET)
is.read(compressedSize)
}
end
def parentAsString
val = name[/.*(?=[^\/](\/)?)/]
val == "" ? nil : val
end
private
def getRawInputStream(&aProc)
File.open(@zipfile, "rb", &aProc)
end
2002-04-25 02:59:13 +08:00
def setTime(binaryDosDate, binaryDosTime)
@time = Time.parseBinaryDosFormat(binaryDosDate, binaryDosTime)
rescue ArgumentError
puts "Invalid date/time in zip entry"
end
2002-01-03 01:48:31 +08:00
end
2002-01-05 04:51:24 +08:00
class ZipOutputStream
include AbstractOutputStream
attr_accessor :comment
def initialize(fileName)
super()
@fileName = fileName
@outputStream = File.new(@fileName, "wb")
@entrySet = ZipEntrySet.new
@compressor = NullCompressor.instance
@closed = false
@currentEntry = nil
@comment = nil
2002-01-05 04:51:24 +08:00
end
def ZipOutputStream.open(fileName)
return new(fileName) unless block_given?
zos = new(fileName)
2002-01-26 03:37:48 +08:00
yield zos
2002-02-23 22:30:22 +08:00
ensure
zos.close if zos
end
2002-01-05 04:51:24 +08:00
def close
return if @closed
finalizeCurrentEntry
updateLocalHeaders
writeCentralDirectory
@outputStream.close
@closed = true
end
2002-01-05 04:51:24 +08:00
2002-01-20 23:00:00 +08:00
def putNextEntry(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)
initNextEntry(newEntry)
2002-01-26 03:37:48 +08:00
@currentEntry=newEntry
end
2002-01-05 04:51:24 +08:00
private
def finalizeCurrentEntry
return unless @currentEntry
2002-01-26 03:37:48 +08:00
finish
@currentEntry.compressedSize = @outputStream.tell - @currentEntry.localHeaderOffset -
@currentEntry.localHeaderSize
2002-01-26 04:12:01 +08:00
@currentEntry.size = @compressor.size
@currentEntry.crc = @compressor.crc
@currentEntry = nil
@compressor = NullCompressor.instance
end
2002-01-20 23:00:00 +08:00
def initNextEntry(entry, level = Zlib::DEFAULT_COMPRESSION)
finalizeCurrentEntry
@entrySet << entry
2002-01-26 03:37:48 +08:00
entry.writeLocalEntry(@outputStream)
@compressor = getCompressor(entry, level)
end
2002-01-05 04:51:24 +08:00
2002-01-26 03:37:48 +08:00
def getCompressor(entry, level)
case entry.compressionMethod
2002-01-20 23:00:00 +08:00
when ZipEntry::DEFLATED then Deflater.new(@outputStream, level)
2002-01-26 03:37:48 +08:00
when ZipEntry::STORED then PassThruCompressor.new(@outputStream)
else raise ZipCompressionMethodError,
"Invalid compression method: '#{entry.compressionMethod}'"
end
end
2002-01-05 08:13:58 +08:00
def updateLocalHeaders
2002-01-26 03:37:48 +08:00
pos = @outputStream.tell
@entrySet.each {
2002-01-26 03:37:48 +08:00
|entry|
@outputStream.pos = entry.localHeaderOffset
entry.writeLocalEntry(@outputStream)
}
@outputStream.pos = pos
end
2002-01-05 08:13:58 +08:00
def writeCentralDirectory
cdir = ZipCentralDirectory.new(@entrySet, @comment)
2002-01-26 03:37:48 +08:00
cdir.writeToStream(@outputStream)
end
protected
def finish
@compressor.finish
end
public
def << (data)
@compressor << data
end
2002-01-05 08:13:58 +08:00
end
2002-03-18 00:22:19 +08:00
class Compressor #:nodoc:all
2002-01-20 23:00:00 +08:00
def finish
end
end
2002-03-18 00:22:19 +08:00
class PassThruCompressor < Compressor #:nodoc:all
def initialize(outputStream)
super()
@outputStream = outputStream
@crc = Zlib::crc32
2002-01-26 04:12:01 +08:00
@size = 0
end
2002-03-18 00:22:19 +08:00
def << (data)
2002-01-26 04:12:01 +08:00
val = data.to_s
@crc = Zlib::crc32(val, @crc)
2002-01-26 04:12:01 +08:00
@size += val.size
@outputStream << val
end
2002-01-26 04:12:01 +08:00
attr_reader :size, :crc
2002-01-05 08:13:58 +08:00
end
2002-01-05 04:51:24 +08:00
class NullCompressor < Compressor #:nodoc:all
include Singleton
2002-01-05 04:51:24 +08:00
def << (data)
raise IOError, "closed stream"
end
2002-01-26 04:12:01 +08:00
attr_reader :size, :compressedSize
2002-01-05 08:13:58 +08:00
end
2002-01-03 01:48:31 +08:00
class Deflater < Compressor #:nodoc:all
2002-01-20 23:00:00 +08:00
def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION)
super()
2002-01-20 23:00:00 +08:00
@outputStream = outputStream
@zlibDeflater = Zlib::Deflate.new(level, -Zlib::Deflate::MAX_WBITS)
2002-01-26 04:12:01 +08:00
@size = 0
@crc = Zlib::crc32
2002-01-20 23:00:00 +08:00
end
def << (data)
2002-01-26 04:12:01 +08:00
val = data.to_s
@crc = Zlib::crc32(val, @crc)
2002-01-26 04:12:01 +08:00
@size += val.size
2002-01-20 23:00:00 +08:00
@outputStream << @zlibDeflater.deflate(data)
end
def finish
until @zlibDeflater.finished?
@outputStream << @zlibDeflater.finish
end
end
2002-01-26 04:12:01 +08:00
attr_reader :size, :crc
2002-01-20 23:00:00 +08:00
end
class ZipEntrySet
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.parentAsString]
end
#TODO attr_accessor :autoCreateDirectories
protected
attr_accessor :entrySet
end
class ZipCentralDirectory #:nodoc:all
include Enumerable
END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18
2002-01-26 03:37:48 +08:00
STATIC_EOCD_SIZE = 22
attr_reader :size, :comment
def entries
@entrySet.entries
end
def initialize(entries = ZipEntrySet.new, comment = "")
super()
@entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries)
2002-01-26 03:37:48 +08:00
@comment = comment
end
def writeToStream(io)
offset = io.tell
@entrySet.each { |entry| entry.writeCDirEntry(io) }
2002-01-26 03:37:48 +08:00
writeEOCD(io, offset)
end
def writeEOCD(io, offset)
io <<
[END_OF_CENTRAL_DIRECTORY_SIGNATURE,
0 , # @numberOfThisDisk
0 , # @numberOfDiskWithStartOfCDir
@entrySet? @entrySet.size : 0 ,
@entrySet? @entrySet.size : 0 ,
2002-01-26 03:37:48 +08:00
cdirSize ,
offset ,
@comment ? @comment.length : 0 ].pack('VvvvvVVv')
io << @comment
end
private :writeEOCD
def cdirSize
2002-01-26 04:12:01 +08:00
# does not include eocd
@entrySet.inject(0) { |value, entry| entry.cdirHeaderSize + value }
2002-01-26 03:37:48 +08:00
end
private :cdirSize
def readEOCD(io)
buf = getEOCD(io)
@numberOfThisDisk = ZipEntry::readZipShort(buf)
@numberOfDiskWithStartOfCDir = ZipEntry::readZipShort(buf)
@totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::readZipShort(buf)
@size = ZipEntry::readZipShort(buf)
@sizeInBytes = ZipEntry::readZipLong(buf)
@cdirOffset = ZipEntry::readZipLong(buf)
commentLength = ZipEntry::readZipShort(buf)
@comment = buf.read(commentLength)
raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0
end
def readCentralDirectoryEntries(io)
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.readCDirEntry(io)
}
end
def readFromStream(io)
readEOCD(io)
readCentralDirectoryEntries(io)
end
def getEOCD(io)
begin
io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END)
rescue Errno::EINVAL
io.seek(0, IO::SEEK_SET)
end
buf = io.read
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
def each(&proc)
@entrySet.each(&proc)
end
2002-01-26 03:37:48 +08:00
def ZipCentralDirectory.readFromStream(io)
cdir = new
cdir.readFromStream(io)
return cdir
rescue ZipError
return nil
end
2002-01-26 03:37:48 +08:00
def == (other)
return false unless other.kind_of?(ZipCentralDirectory)
@entrySet.entries.sort == other.entries.sort && comment == other.comment
2002-01-26 03:37:48 +08:00
end
2002-01-03 01:48:31 +08:00
end
class ZipError < StandardError ; end
class ZipEntryExistsError < ZipError; end
class ZipDestinationFileExistsError < ZipError; end
class ZipCompressionMethodError < ZipError; end
class ZipEntryNameError < ZipError; end
class ZipFile < ZipCentralDirectory
include ZipFileSystem
2002-02-23 22:30:22 +08:00
CREATE = 1
2002-03-18 03:36:02 +08:00
attr_reader :name
2002-02-23 22:30:22 +08:00
def initialize(fileName, create = nil)
super()
2002-03-18 00:22:19 +08:00
@name = fileName
@comment = ""
2002-02-23 21:16:08 +08:00
if (File.exists?(fileName))
File.open(name, "rb") { |f| readFromStream(f) }
2002-02-23 22:30:22 +08:00
elsif (create == ZipFile::CREATE)
@entrySet = ZipEntrySet.new
2002-02-23 22:30:22 +08:00
else
raise ZipError, "File #{fileName} not found"
2002-02-23 21:16:08 +08:00
end
@create = create
@storedEntries = @entrySet.dup
2002-02-23 21:16:08 +08:00
end
2002-03-18 00:22:19 +08:00
def ZipFile.open(fileName, create = nil)
zf = ZipFile.new(fileName, create)
if block_given?
begin
yield zf
ensure
zf.close
end
else
zf
end
2002-02-23 22:30:22 +08:00
end
2002-03-18 03:36:02 +08:00
attr_accessor :comment
2002-03-18 03:36:02 +08:00
def ZipFile.foreach(aZipFileName, &block)
2002-07-20 00:48:41 +08:00
ZipFile.open(aZipFileName) {
|zipFile|
zipFile.each(&block)
}
end
def getInputStream(entry, &aProc)
getEntry(entry).getInputStream(&aProc)
end
def to_s
@name
end
2002-07-26 22:12:34 +08:00
def add(entry, srcPath, &continueOnExistsProc)
continueOnExistsProc ||= proc { false }
checkEntryExists(entry, continueOnExistsProc, "add")
newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
if isDirectory(newEntry, srcPath)
@entrySet << ZipStreamableDirectory.new(newEntry)
else
@entrySet << ZipStreamableFile.new(newEntry, srcPath)
end
2002-03-18 00:22:19 +08:00
end
def remove(entry)
@entrySet.delete(getEntry(entry))
2002-03-18 00:22:19 +08:00
end
def rename(entry, newName, &continueOnExistsProc)
2002-03-18 00:22:19 +08:00
foundEntry = getEntry(entry)
checkEntryExists(newName, continueOnExistsProc, "rename")
2002-03-18 00:22:19 +08:00
foundEntry.name=newName
end
2002-03-18 00:22:19 +08:00
def replace(entry, srcPath)
checkFile(srcPath)
add(remove(entry), srcPath)
end
def extract(entry, destPath, &onExistsProc)
onExistsProc ||= proc { false }
2002-03-18 00:22:19 +08:00
foundEntry = getEntry(entry)
if foundEntry.isDirectory
createDirectory(foundEntry, destPath, &onExistsProc)
else
writeFile(destPath, onExistsProc) {
|os|
foundEntry.getInputStream { |is| os << is.read }
}
end
2002-02-23 21:16:08 +08:00
end
2002-02-23 22:30:22 +08:00
def commit
return if ! commitRequired?
2002-03-18 00:22:19 +08:00
onSuccessReplace(name) {
|tmpFile|
ZipOutputStream.open(tmpFile) {
|zos|
@entrySet.each { |e| e.writeToZipOutputStream(zos) }
2002-03-18 04:04:03 +08:00
zos.comment = comment
2002-02-23 22:30:22 +08:00
}
2002-03-18 00:22:19 +08:00
true
2002-02-23 22:30:22 +08:00
}
initialize(name)
2002-02-23 22:30:22 +08:00
end
2002-03-18 00:22:19 +08:00
2002-02-23 22:30:22 +08:00
def close
commit
end
def commitRequired?
return @entrySet != @storedEntries || @create == ZipFile::CREATE
end
2002-07-26 22:12:34 +08:00
def findEntry(entry)
@entrySet.detect {
2002-07-26 22:12:34 +08:00
|e|
e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "")
}
end
2002-03-18 00:22:19 +08:00
def getEntry(entry)
selectedEntry = findEntry(entry)
unless selectedEntry
raise Errno::ENOENT,
"No matching entry found in zip file '#{@name}' for '#{entry}'"
end
return selectedEntry
end
2002-02-23 21:16:08 +08:00
private
def createDirectory(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 isDirectory(newEntry, srcPath)
srcPathIsDirectory = File.directory?(srcPath)
if newEntry.isDirectory && ! srcPathIsDirectory
raise ArgumentError,
"entry name '#{newEntry}' indicates directory entry, but "+
"'#{srcPath}' is not a directory"
elsif ! newEntry.isDirectory && srcPathIsDirectory
newEntry.name += "/"
end
return newEntry.isDirectory && srcPathIsDirectory
end
def checkEntryExists(entryName, continueOnExistsProc, procedureName)
continueOnExistsProc ||= proc { false }
if @entrySet.detect { |e| e.name == entryName }
if continueOnExistsProc.call
remove getEntry(entryName)
else
raise ZipEntryExistsError,
procedureName+" failed. Entry #{entryName} already exists"
end
end
end
2002-03-18 00:22:19 +08:00
def writeFile(destPath, continueOnExistsProc = proc { false }, &writeFileProc)
if File.exists?(destPath) && ! continueOnExistsProc.call
raise ZipDestinationFileExistsError,
2002-03-18 00:22:19 +08:00
"Destination '#{destPath}' already exists"
end
2002-03-18 03:36:02 +08:00
File.open(destPath, "wb", &writeFileProc)
2002-02-23 22:30:22 +08:00
end
2002-03-18 00:22:19 +08:00
def checkFile(path)
unless File.readable? path
raise Errno::ENOENT,
"'#{path}' does not exist or cannot be opened reading"
2002-03-18 00:22:19 +08:00
end
end
def onSuccessReplace(aFilename)
tmpfile = getTempfile
tmpFilename = tmpfile.path
tmpfile.close
if yield tmpFilename
File.move(tmpFilename, name)
end
end
def getTempfile
Tempfile.new(File.basename(name), File.dirname(name)).binmode
2002-02-23 21:16:08 +08:00
end
2002-03-18 03:36:02 +08:00
end
class ZipStreamableFile < DelegateClass(ZipEntry) #:nodoc:all
2002-03-18 03:36:02 +08:00
def initialize(entry, filepath)
super(entry)
@delegate = entry
2002-03-18 03:36:02 +08:00
@filepath = filepath
2002-03-18 00:22:19 +08:00
end
2002-03-18 03:36:02 +08:00
def getInputStream(&aProc)
File.open(@filepath, "rb", &aProc)
2002-02-23 21:16:08 +08:00
end
2002-03-18 03:36:02 +08:00
def writeToZipOutputStream(aZipOutputStream)
aZipOutputStream.putNextEntry(self)
aZipOutputStream << getInputStream { |is| is.read }
2002-03-18 00:22:19 +08:00
end
def == (other)
return false unless other.class == ZipStreamableFile
@filepath == other.filepath && super(other.delegate)
end
protected
attr_reader :filepath, :delegate
2002-02-23 21:16:08 +08:00
end
class ZipStreamableDirectory < DelegateClass(ZipEntry) #:nodoc:all
def initialize(entry)
super(entry)
end
def getInputStream(&aProc)
return yield(NullInputStream.instance) if block_given?
NullInputStream.instance
end
def writeToZipOutputStream(aZipOutputStream)
aZipOutputStream.putNextEntry(self)
end
end
2002-02-23 21:16:08 +08:00
end # Zip namespace module
2002-02-02 22:58:02 +08:00
# Copyright (C) 2002 Thomas Sondergaard
# rubyzip is free software; you can redistribute it and/or
# modify it under the terms of the ruby license.