Initial revision

This commit is contained in:
thomas 2002-01-02 17:48:31 +00:00
commit 609f3527d5
6 changed files with 779 additions and 0 deletions

29
README.txt Normal file
View File

@ -0,0 +1,29 @@
= rubyzip =
rubyzip is a ruby library for reading and writing zip (pkzip format)
files, with the restriction that only uncompressed and deflated zip
entries are supported. All this library does is handling of the zip
file format. the actual compression/decompression is handled by zlib
= Resources =
zlib http://www.gzip.org/zlib/
ruby-zlib: http://www.blue.sky.or.jp/atelier/#ruby-zlib
= Ruby/zlib issue =
There is a problem with ruby/zlib version 0.4.0 and earlier concerning
wbits, that prevents rubyzip from working. The ruby wrapper does some
parameters checks of its own, and restrict the wbits parameter passed
to inflateInit2 from being negative. Apply the patch 'zlib.c.diff' to
zlib.c from ruby/zlib, then rebuild and install ruby/zlib to fix the
issue.
= Missing tests =
zip.rb is only 280 lines. Go through it and check for each line
whether there is a test for it!
= todo for release 0.2.0 =
Write ZipFile or ZipDir that reads the zip central directory

14
example.rb Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env ruby
system("zip example.zip example.rb")
require 'zip'
ZipInputStream.open("example.zip") {
|zis|
entry = zis.getNextEntry
puts "Zip entry '#{entry.name}' contains:"
puts zis.read
}
# For other examples, look at zip.rb and ziptest.rb

46
file1.txt Normal file
View File

@ -0,0 +1,46 @@
AUTOMAKE_OPTIONS = gnu
EXTRA_DIST = test.zip
CXXFLAGS= -g
noinst_LIBRARIES = libzipios.a
bin_PROGRAMS = test_zip test_izipfilt test_izipstream
# test_flist
libzipios_a_SOURCES = backbuffer.h fcol.cpp fcol.h \
fcol_common.h fcolexceptions.cpp fcolexceptions.h \
fileentry.cpp fileentry.h flist.cpp \
flist.h flistentry.cpp flistentry.h \
flistscanner.h ifiltstreambuf.cpp ifiltstreambuf.h \
inflatefilt.cpp inflatefilt.h izipfilt.cpp \
izipfilt.h izipstream.cpp izipstream.h \
zipfile.cpp zipfile.h ziphead.cpp \
ziphead.h flistscanner.ll
# test_flist_SOURCES = test_flist.cpp
test_izipfilt_SOURCES = test_izipfilt.cpp
test_izipstream_SOURCES = test_izipstream.cpp
test_zip_SOURCES = test_zip.cpp
# Notice that libzipios.a is not specified as -L. -lzipios
# If it was, automake would not include it as a dependency.
# test_flist_LDADD = libzipios.a
test_izipfilt_LDADD = libzipios.a -lz
test_zip_LDADD = libzipios.a -lz
test_izipstream_LDADD = libzipios.a -lz
flistscanner.cc : flistscanner.ll
$(LEX) -+ -PFListScanner -o$@ $^

307
zip.rb Executable file
View File

@ -0,0 +1,307 @@
#!/usr/bin/env ruby
require 'singleton'
require 'zlib'
# Implements many of the convenience methods of IO
# such as gets, getc, readline and readlines
module PseudoIO
def readlines(aSepString = $/)
retVal = []
each_line(aSepString) { |line| retVal << line }
return retVal
end
def gets(aSepString=$/)
@outputBuffer="" unless @outputBuffer
return read if aSepString == nil
aSepString="#{$/}#{$/}" if aSepString == ""
bufferIndex=0
while ((matchIndex = @outputBuffer.index(aSepString, bufferIndex)) == nil)
bufferIndex=@outputBuffer.length
if inputFinished?
return @outputBuffer.length==0 ? nil : flush
end
@outputBuffer << produceInput
end
sepIndex=matchIndex + aSepString.length
return @outputBuffer.slice!(0...sepIndex)
end
def flush
retVal=@outputBuffer
@outputBuffer=""
return retVal
end
def readline(aSepString = $/)
retVal = gets(aSepString)
raise EOFError if retVal == nil
return retVal
end
def each_line(aSepString = $/)
while true
yield readline
end
rescue EOFError
end
alias_method :each, :each_line
end
class ZipInputStream
include PseudoIO
def initialize(filename)
@archiveIO = File.open(filename, "rb")
@decompressor = NullDecompressor.instance
end
def close
puts "IMPLEMENT ME: ZipInputStream::close"
end
def ZipInputStream.open(filename)
return new(filename) unless block_given?
zio = new(filename)
yield zio
zio.close
end
def getNextEntry
@archiveIO.seek(@currentEntry.nextHeaderOffset,
IO::SEEK_SET) if @currentEntry
@currentEntry = ZipLocalEntry.readFromStream(@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 "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?
end
end
class Decompressor
CHUNK_SIZE=8192
def initialize(inputStream)
@inputStream=inputStream
end
end
class Inflater < Decompressor
def initialize(inputStream)
super
@zlibInflater = Inflate.new(-Inflate::MAX_WBITS)
@outputBuffer=""
end
def read(numberOfBytes = nil)
readEverything = (numberOfBytes == nil)
while (readEverything || @outputBuffer.length < numberOfBytes)
break if inputFinished?
@outputBuffer << produceInput
end
return nil if @outputBuffer.length==0 && inputFinished?
endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes
return @outputBuffer.slice!(0...endIndex)
end
def produceInput
@zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE))
end
def inputFinished?
@zlibInflater.finished?
end
end
class PassThruDecompressor < Decompressor
def initialize(inputStream, charsToRead)
super inputStream
@charsToRead = charsToRead
@readSoFar = 0
@isFirst=true
end
def read(numberOfBytes = nil)
if inputFinished?
isFirstVal=@isFirst
@isFirst=false
return "" if isFirstVal
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
end
class NullDecompressor
include Singleton
def read(numberOfBytes = nil)
nil
end
def produceInput
nil
end
def inputFinished?
true
end
end
class ZipEntry
STORED = 0
DEFLATED = 8
attr_reader :comment, :compressedSize, :crc, :extra, :compressionMethod,
:name, :size, :localHeaderOffset
def initialize(comment = nil, compressedSize = nil, crc = nil, extra = nil,
compressionMethod = nil, name = nil, size = nil)
@comment, @compressedSize, @crc, @extra, @compressionMethod,
@name, @size, @isDirectory = comment, compressedSize, crc,
extra, compressionMethod, name, size
end
def isDirectory
return (/\/$/ =~ @name) != nil
end
def localEntryOffset
localHeaderOffset + localHeaderSize
end
def localHeaderSize
30 + name.size + extra.size
end
def nextHeaderOffset
localEntryOffset + self.compressedSize
end
protected
def ZipEntry.readZipShort(io)
io.read(2).unpack('v')[0]
end
def ZipEntry.readZipLong(io)
io.read(4).unpack('V')[0]
end
end
class ZipLocalEntry < ZipEntry
LOCAL_ENTRY_SIGNATURE = 0x04034b50
def readFromStream(io)
unless (ZipEntry::readZipLong(io) == LOCAL_ENTRY_SIGNATURE)
raise ZipError,
"Zip Local Header magic '#{LOCAL_ENTRY_SIGNATURE} not found"
end
@localHeaderOffset = io. tell - 4
@version = ZipEntry::readZipShort(io)
@gpFlags = ZipEntry::readZipShort(io)
@compressionMethod = ZipEntry::readZipShort(io)
@lastModTime = ZipEntry::readZipShort(io)
@lastModDate = ZipEntry::readZipShort(io)
@crc = ZipEntry::readZipLong(io)
@compressedSize = ZipEntry::readZipLong(io)
@size = ZipEntry::readZipLong(io)
nameLength = ZipEntry::readZipShort(io)
extraLength = ZipEntry::readZipShort(io)
@name = io.read(nameLength)
@extra = io.read(extraLength)
end
def ZipLocalEntry.readFromStream(io)
entry = new()
entry.readFromStream(io)
return entry
rescue ZipError
return nil
end
end
# class ZipCentralDirectoryEntry < ZipEntry
# ZIP_CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
# def readFromStream(io)
# end
# def ZipCentralDirectoryEntry.readFromStream(io)
# entry = new()
# entry.readFromStream(io)
# return entry
# rescue ZipError
# return nil
# end
# end
class ZipError < RuntimeError
end
class ZipFile
include Enumerable
attr_reader :name
def initialize(name)
@name=name
end
def ZipFile.foreach(aZipFileName, &block)
zipFile = ZipFile.new(aZipFileName)
zipFile.each &block
end
def each
end
def getInputStream(entry)
end
end

372
ziptest.rb Executable file
View File

@ -0,0 +1,372 @@
#!/usr/bin/env ruby
require 'runit/testcase'
require 'runit/cui/testrunner'
require 'zip'
class PseudoIOTest < RUNIT::TestCase
# PseudoIO subclass that provides a read method
TEST_LINES = [ "Hello world#{$/}",
"this is the second line#{$/}",
"this is the last line"]
TEST_STRING = TEST_LINES.join
class TestPseudoIO
include PseudoIO
def initialize(aString)
@contents = aString
@readPointer = 0
end
def read(charsToRead)
retVal=@contents[@readPointer, charsToRead]
@readPointer+=charsToRead
return retVal
end
def produceInput
read(100)
end
def inputFinished?
@contents[@readPointer] == nil
end
end
def setup
@io = TestPseudoIO.new(TEST_STRING)
end
def test_gets
assert_equals(TEST_LINES[0], @io.gets)
assert_equals(TEST_LINES[1], @io.gets)
assert_equals(TEST_LINES[2], @io.gets)
assert_equals(nil, @io.gets)
end
def test_getsMultiCharSeperator
assert_equals("Hell", @io.gets("ll"))
assert_equals("o world#{$/}this is the second l", @io.gets("d l"))
end
def test_each_line
lineNumber=0
@io.each_line {
|line|
assert_equals(TEST_LINES[lineNumber], line)
lineNumber+=1
}
end
def test_readlines
assert_equals(TEST_LINES, @io.readlines)
end
def test_readline
test_gets
begin
@io.readline
fail "EOFError expected"
rescue EOFError
end
end
end
class ZipEntryTest < RUNIT::TestCase
TEST_COMMENT = "a comment"
TEST_COMPRESSED_SIZE = 1234
TEST_CRC = 325324
TEST_EXTRA = "Some data here"
TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED
TEST_NAME = "entry name"
TEST_SIZE = 8432
TEST_ISDIRECTORY = false
def test_constructorAndGetters
entry = ZipEntry.new(TEST_COMMENT,
TEST_COMPRESSED_SIZE,
TEST_CRC,
TEST_EXTRA,
TEST_COMPRESSIONMETHOD,
TEST_NAME,
TEST_SIZE)
assert_equals(TEST_COMMENT, entry.comment)
assert_equals(TEST_COMPRESSED_SIZE, entry.compressedSize)
assert_equals(TEST_CRC, entry.crc)
assert_equals(TEST_EXTRA, entry.extra)
assert_equals(TEST_COMPRESSIONMETHOD, entry.compressionMethod)
assert_equals(TEST_NAME, entry.name)
assert_equals(TEST_SIZE, entry.size)
assert_equals(TEST_ISDIRECTORY, entry.isDirectory)
end
end
class ZipLocalEntryTest < RUNIT::TestCase
def test_ReadLocalEntryHeaderOfFirstTestZipEntry
File.open(TestZipFile::TEST_ZIP3.zipName) {
|file|
entry = ZipLocalEntry.readFromStream(file)
assert_equal(nil, entry.comment)
assert_equal(480, entry.compressedSize)
assert_equal(0x2a27930f, entry.crc)
# extra field is 21 bytes long
# probably contains some unix attrutes or something
# disabled: assert_equal(nil, entry.extra)
assert_equal(ZipEntry::DEFLATED, entry.compressionMethod)
assert_equal(TestZipFile::TEST_ZIP3.entryNames[0], entry.name)
assert_equal(1373, entry.size)
assert_equal(false, entry.isDirectory)
}
end
def test_ReadLocalEntryFromNonZipFile
File.open("ziptest.rb") {
|file|
ZipLocalEntry.readFromStream(file)
}
fail "Excepted exception"
rescue
end
end
module AssertEntry
def assertNextEntry(filename, zis)
assertEntry(filename, zis, zis.getNextEntry.name)
end
def assertEntry(filename, zis, entryName)
assert_equals(filename, entryName)
File.open(filename, "rb") {
|file|
expected = file.read
actual = zis.read
if (expected != actual)
if (expected.length > 400 || actual.length > 400)
zipEntryFilename=entryName+".zipEntry"
File.open(zipEntryFilename, "wb") { |file| file << actual }
fail("File '#{filename}' is different from '#{zipEntryFilename}'")
else
assert_equals(expected, actual)
end
end
}
end
end
class ZipInputStreamTest < RUNIT::TestCase
include AssertEntry
def test_new
zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zipName)
assertTestfileContents(zis)
zis.close
end
def test_openWithBlock
ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) {
|zis|
assertTestfileContents(zis)
}
end
def test_openWithoutBlock
zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName)
assertTestfileContents(zis)
end
def test_incompleteReads
ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) {
|zis|
entry = zis.getNextEntry
assert_equals(TestZipFile::TEST_ZIP2.entryNames[0], entry.name)
assert zis.gets.length > 0
entry = zis.getNextEntry
assert_equals(TestZipFile::TEST_ZIP2.entryNames[1], entry.name)
assert_equals(0, entry.size)
assert_equals(nil, zis.gets)
entry = zis.getNextEntry
assert_equals(TestZipFile::TEST_ZIP2.entryNames[2], entry.name)
assert zis.gets.length > 0
entry = zis.getNextEntry
assert_equals(TestZipFile::TEST_ZIP2.entryNames[3], entry.name)
assert zis.gets.length > 0
}
end
private
def assertTestfileContents(zis)
assert(zis)
assertNextEntry(TestZipFile::TEST_ZIP2.entryNames[0], zis)
assertNextEntry(TestZipFile::TEST_ZIP2.entryNames[1], zis)
assertNextEntry(TestZipFile::TEST_ZIP2.entryNames[2], zis)
assertNextEntry(TestZipFile::TEST_ZIP2.entryNames[3], zis)
assert_equals(nil, zis.getNextEntry)
end
end
# For representation and creation of
# test data
class TestZipFile
attr_reader :zipName, :entryNames
def initialize(zipName, entryNames)
@zipName=zipName
@entryNames=entryNames
@@testZips << self
end
def TestZipFile.testZips
@@testZips
end
@@testZips = []
def TestZipFile.createTestZips(recreate)
files = Dir.entries(".")
if (recreate ||
! (files.index(TEST_ZIP1.zipName) &&
files.index(TEST_ZIP2.zipName) &&
files.index(TEST_ZIP3.zipName) &&
files.index("empty.txt") &&
files.index("short.txt") &&
files.index("longAscii.txt") &&
files.index("longBinary.bin") ))
raise "failed to create test zip '#{TEST_ZIP1.zipName}'" unless
system("zip #{TEST_ZIP1.zipName} ziptest.rb")
raise "failed to remove entry from '#{TEST_ZIP1.zipName}'" unless
system("zip #{TEST_ZIP1.zipName} -d ziptest.rb")
File.open("empty.txt", "w") {}
File.open("short.txt", "w") { |file| file << "ABCDEF" }
ziptestTxt=""
File.open("ziptest.rb") { |file| ziptestTxt=file.read }
File.open("longAscii.txt", "w") {
|file|
while (file.tell < 1E5)
file << ziptestTxt
end
}
testBinaryPattern=""
File.open("empty.zip") { |file| testBinaryPattern=file.read }
testBinaryPattern *= 4
File.open("longBinary.bin", "wb") {
|file|
while (file.tell < 1E6)
file << testBinaryPattern << rand
end
}
raise "failed to create test zip '#{TEST_ZIP2.zipName}'" unless
system("zip #{TEST_ZIP2.zipName} #{TEST_ZIP2.entryNames.join(' ')}")
raise "failed to create test zip '#{TEST_ZIP3.zipName}'" unless
system("zip #{TEST_ZIP3.zipName} #{TEST_ZIP3.entryNames.join(' ')}")
end
end
TEST_ZIP1 = TestZipFile.new("empty.zip", [])
TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin})
TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt })
end
class ZipCentralDirectoryTest < RUNIT::TestCase
def test_readFromStream
File.open(TestZipFile::TEST_ZIP2.zipName, "rb") {
|zipFile|
cdir = ZipCentralDirectory.new(zipFile)
######################################################################
# IN PROGRESS: write this test, then ZipCentralDirectory,
# then ZipCentralDirectoryEntryTest, then ZipCentralDirectory,
# then check ZipFileTest, then write ZipFile
######################################################################
# end of central dir signature 4 bytes (0x06054b50)
# number of this disk 2 bytes
# number of the disk with the
# start of the central directory 2 bytes
# total number of entries in the
# central directory on this disk 2 bytes
# total number of entries in
# the central directory 2 bytes
# size of the central directory 4 bytes
# offset of start of central
# directory with respect to
# the starting disk number 4 bytes
# .ZIP file comment length 2 bytes
# .ZIP file comment (variable size)
}
end
def test_readFromInvalidStream
File.open("ziptest.rb", "rb") {
|zipFile|
cdir = ZipCentralDirectory.new(zipFile)
}
fail "ZipError expected!"
rescue ZipError
end
end
# TODO: Tests yet to write
# ZipFile
class ZipFileTest < RUNIT::TestCase
include AssertEntry
def setup
@zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zipName)
@testEntryNameIndex=0
end
def nextTestEntryName
retVal=TestZipFile::TEST_ZIP2.entryNames[@testEntryNameIndex]
@testEntryNameIndex+=1
return retVal
end
def test_entries
entries = @zipFile.entries
assert_equals(4, entries.size)
assert_equals(nextTestEntryName, entries[0])
assert_equals(nextTestEntryName, entries[1])
assert_equals(nextTestEntryName, entries[2])
assert_equals(nextTestEntryName, entries[3])
end
def test_each
@zipFile.each {
|entry|
assert_equals(nextTestEntryName, entry.name)
}
assert_equals(4, @testEntryNameIndex)
end
def test_foreach
ZipFile.foreach(TestZipFile::TEST_ZIP2.zipName) {
|entry|
assert_equals(nextTestEntryName, entry.name)
}
assert_equals(4, @testEntryNameIndex)
end
def test_getInputStream
@zipFile.each {
|entry|
assertEntry(nextTestEntryName, @zipFile.getInputStream(entry),
entry.name)
}
assert_equals(4, @testEntryNameIndex)
end
end
TestZipFile::createTestZips(ARGV.index("recreate") != nil)
RUNIT::CUI::TestRunner.run(RUNIT::TestCase.all_suite)

11
zlib.c.diff Normal file
View File

@ -0,0 +1,11 @@
--- zlib.c.org Tue Jan 1 21:31:08 2002
+++ zlib.c Tue Jan 1 21:32:41 2002
@@ -514,7 +514,7 @@
Check_Type(val, T_FIXNUM);
wbits = FIX2INT(val);
- if (wbits < 8 || MAX_WBITS < wbits)
+ if (abs(wbits) < 8 || MAX_WBITS < abs(wbits))
rb_raise(rb_eArgError, "window bits out of range");
return wbits;
}