Applied extra-field patch

This commit is contained in:
thomas 2004-01-30 15:07:56 +00:00
parent d024c87c47
commit c3949e3e73
6 changed files with 602 additions and 188 deletions

2
README
View File

@ -45,3 +45,5 @@ http://rubyzip.sourceforge.net
= Author
Thomas Sondergaard (thomas at thomassondergaard.com)
extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org)

View File

@ -173,9 +173,12 @@ class ZipFsFileNonmutatingTest < RUNIT::TestCase
end
def test_utime
assert_exception(StandardError, "utime not supported") {
@zipFile.file.utime(100, "file1", "dir1")
}
t_now = Time.now
t_bak = @zipFile.file.mtime("file1")
@zipFile.file.utime(t_now, "file1")
assert_equals(t_now, @zipFile.file.mtime("file1"))
@zipFile.file.utime(t_bak, "file1")
assert_equals(t_bak, @zipFile.file.mtime("file1"))
end
@ -256,7 +259,10 @@ class ZipFsFileNonmutatingTest < RUNIT::TestCase
end
def test_chown
assert_equals(2, @zipFile.file.chown(1,2, "noSuchFile", "file1"))
assert_equals(2, @zipFile.file.chown(1,2, "dir1", "file1"))
assert_equals(1, @zipFile.file.stat("dir1").uid)
assert_equals(2, @zipFile.file.stat("dir1").gid)
assert_equals(2, @zipFile.file.chown(nil, nil, "dir1", "file1"))
end
def test_zero?
@ -295,17 +301,17 @@ class ZipFsFileNonmutatingTest < RUNIT::TestCase
end
def test_mtime
assert_equals(Time.local(2002, "Jul", 26, 16, 38, 26),
assert_equals(Time.at(1027694306),
@zipFile.file.mtime("dir2/file21"))
assert_equals(Time.local(2002, "Jul", 26, 15, 41, 04),
assert_equals(Time.at(1027690863),
@zipFile.file.mtime("dir2/dir21"))
assert_exception(Errno::ENOENT) {
@zipFile.file.mtime("noSuchEntry")
}
assert_equals(Time.local(2002, "Jul", 26, 16, 38, 26),
assert_equals(Time.at(1027694306),
@zipFile.file.stat("dir2/file21").mtime)
assert_equals(Time.local(2002, "Jul", 26, 15, 41, 04),
assert_equals(Time.at(1027690863),
@zipFile.file.stat("dir2/dir21").mtime)
end
@ -320,35 +326,59 @@ class ZipFsFileNonmutatingTest < RUNIT::TestCase
end
def test_readable?
assert_true_if_entry_exists(:readable?)
assert(! @zipFile.file.readable?("noSuchFile"))
assert(@zipFile.file.readable?("file1"))
assert(@zipFile.file.readable?("dir1"))
assert(@zipFile.file.stat("file1").readable?)
assert(@zipFile.file.stat("dir1").readable?)
end
def test_readable_real?
assert_true_if_entry_exists(:readable_real?)
assert(! @zipFile.file.readable_real?("noSuchFile"))
assert(@zipFile.file.readable_real?("file1"))
assert(@zipFile.file.readable_real?("dir1"))
assert(@zipFile.file.stat("file1").readable_real?)
assert(@zipFile.file.stat("dir1").readable_real?)
end
def test_writable?
assert_true_if_entry_exists(:writable?)
assert(! @zipFile.file.writable?("noSuchFile"))
assert(@zipFile.file.writable?("file1"))
assert(@zipFile.file.writable?("dir1"))
assert(@zipFile.file.stat("file1").writable?)
assert(@zipFile.file.stat("dir1").writable?)
end
def test_writable_real?
assert_true_if_entry_exists(:writable_real?)
assert(! @zipFile.file.writable_real?("noSuchFile"))
assert(@zipFile.file.writable_real?("file1"))
assert(@zipFile.file.writable_real?("dir1"))
assert(@zipFile.file.stat("file1").writable_real?)
assert(@zipFile.file.stat("dir1").writable_real?)
end
def test_executable?
assert_true_if_entry_exists(:executable?)
assert(! @zipFile.file.executable?("noSuchFile"))
assert(! @zipFile.file.executable?("file1"))
assert(@zipFile.file.executable?("dir1"))
assert(! @zipFile.file.stat("file1").executable?)
assert(@zipFile.file.stat("dir1").executable?)
end
def test_executable_real?
assert_true_if_entry_exists(:executable_real?)
assert(! @zipFile.file.executable_real?("noSuchFile"))
assert(! @zipFile.file.executable_real?("file1"))
assert(@zipFile.file.executable_real?("dir1"))
assert(! @zipFile.file.stat("file1").executable_real?)
assert(@zipFile.file.stat("dir1").executable_real?)
end
def test_owned?
assert_true_if_entry_exists(:executable_real?)
assert_true_if_entry_exists(:owned?)
end
def test_grpowned?
assert_true_if_entry_exists(:executable_real?)
assert_true_if_entry_exists(:grpowned?)
end
def test_setgid?
@ -485,7 +515,10 @@ class ZipFsFileStatTest < RUNIT::TestCase
end
def test_mode
assert_equals(33206, @zipFile.file.stat("file1").mode)
assert_equals(0600, @zipFile.file.stat("file1").mode & 0777)
assert_equals(0600, @zipFile.file.stat("file1").mode & 0777)
assert_equals(0755, @zipFile.file.stat("dir1").mode & 0777)
assert_equals(0755, @zipFile.file.stat("dir1").mode & 0777)
end
def test_dev

View File

@ -156,7 +156,7 @@ class ZipEntryTest < RUNIT::TestCase
assert_equals(TEST_COMMENT, entry.comment)
assert_equals(TEST_COMPRESSED_SIZE, entry.compressed_size)
assert_equals(TEST_CRC, entry.crc)
assert_equals(TEST_EXTRA, entry.extra)
assert_instance_of(Zip::ZipExtraField, entry.extra)
assert_equals(TEST_COMPRESSIONMETHOD, entry.compression_method)
assert_equals(TEST_NAME, entry.name)
assert_equals(TEST_SIZE, entry.size)
@ -323,7 +323,7 @@ class ZipLocalEntryTest < RUNIT::TestCase
|file|
entry = ZipEntry.read_local_entry(file)
assert_equals("zippedruby1.rb", entry.name)
assert_equals(Time.local(2002, "Apr", 20, 02, 13, 58), entry.time)
assert_equals(Time.at(1019261638), entry.time)
}
end
@ -1838,6 +1838,73 @@ END {
exit if ARGV.index("recreateonly") != nil
}
class ZipExtraFieldTest < RUNIT::TestCase
def test_new
extra_pure = ZipExtraField.new("")
extra_withstr = ZipExtraField.new("foo")
assert_instance_of(ZipExtraField, extra_pure)
assert_instance_of(ZipExtraField, extra_withstr)
end
def test_unknownfield
extra = ZipExtraField.new("foo")
assert_equals(extra["Unknown"], "foo")
extra.merge("a")
assert_equals(extra["Unknown"], "fooa")
extra.merge("barbaz")
assert_equals(extra.to_s, "fooabarbaz")
end
def test_merge
str = "UT\x5\0\x3\250$\r@Ux\0\0"
extra1 = ZipExtraField.new("")
extra2 = ZipExtraField.new(str)
assert(! extra1.member?("UniversalTime"))
assert(extra2.member?("UniversalTime"))
extra1.merge(str)
assert_equals(extra1["UniversalTime"].mtime, extra2["UniversalTime"].mtime)
end
def test_length
str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit"
extra = ZipExtraField.new(str)
assert_equals(extra.local_length, extra.to_local_bin.length)
assert_equals(extra.c_dir_length, extra.to_c_dir_bin.length)
extra.merge("foo")
assert_equals(extra.local_length, extra.to_local_bin.length)
assert_equals(extra.c_dir_length, extra.to_c_dir_bin.length)
end
def test_to_s
str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit"
extra = ZipExtraField.new(str)
assert_instance_of(String, extra.to_s)
s = extra.to_s
extra.merge("foo")
assert_equals(s.length + 3, extra.to_s.length)
end
def test_equality
str = "UT\x5\0\x3\250$\r@"
extra1 = ZipExtraField.new(str)
extra2 = ZipExtraField.new(str)
extra3 = ZipExtraField.new(str)
assert_equals(extra1, extra2)
extra2["UniversalTime"].mtime = Time.now
assert(extra1 != extra2)
extra3.create("IUnix")
assert(extra1 != extra3)
extra1.create("IUnix")
assert_equals(extra1, extra3)
end
end
# Copyright (C) 2002, 2003 Thomas Sondergaard
# rubyzip is free software; you can redistribute it and/or

View File

@ -80,12 +80,7 @@ class Time
# Dos time is only stored with two seconds accuracy
def dos_equals(other)
(year == other.year &&
month == other.month &&
day == other.day &&
hour == other.hour &&
min == other.min &&
sec/2 == other.sec/2)
to_i/2 == other.to_i/2
end
def self.parse_binary_dos_format(binaryDosDate, binaryDosTime)

View File

@ -316,12 +316,10 @@ module Zip
DEFLATED = 8
attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
:name, :size, :localHeaderOffset, :time, :zipfile
alias :mtime :time
:name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes
def initialize(zipfile = "", name = "", comment = "", extra = "",
compressed_size = 0, crc = 0,
compressed_size = 0, crc = 0,
compression_method = ZipEntry::DEFLATED, size = 0,
time = Time.now)
super()
@ -329,10 +327,36 @@ module Zip
raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
end
@localHeaderOffset = 0
@internalFileAttributes = 1
@externalFileAttributes = 0
@version = 52 # this library's version
@fstype = 0 # default is fat
@zipfile, @comment, @compressed_size, @crc, @extra, @compression_method,
@name, @size = zipfile, comment, compressed_size, crc,
extra, compression_method, name, size
@time = time
unless ZipExtraField === @extra
@extra = ZipExtraField.new(@extra.to_s)
end
end
def time
if @extra["UniversalTime"]
@extra["UniversalTime"].mtime
else
# Atandard time field in central directory has local time
# under archive creator. Then, we can't get timezone.
@time
end
end
alias :mtime :time
def time=(aTime)
unless @extra.member?("UniversalTime")
@extra.create("UniversalTime")
end
@extra["UniversalTime"].mtime = aTime
@time = aTime
end
def directory?
@ -349,12 +373,12 @@ module Zip
end
def local_header_size #:nodoc:all
LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.size : 0)
LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0)
end
def cdir_header_size #:nodoc:all
CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) +
(@extra ? @extra.size : 0) + (@comment ? @comment.size : 0)
(@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0)
end
def next_header_offset #:nodoc:all
@ -387,7 +411,8 @@ module Zip
end
localHeader ,
@version ,
@version ,
@fstype ,
@gpFlags ,
@compression_method,
lastModTime ,
@ -396,7 +421,7 @@ module Zip
@compressed_size ,
@size ,
nameLength ,
extraLength = staticSizedFieldsBuf.unpack('VvvvvvVVVvv')
extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv')
unless (localHeader == LOCAL_ENTRY_SIGNATURE)
raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
@ -404,9 +429,16 @@ module Zip
set_time(lastModDate, lastModTime)
@name = io.read(nameLength)
@extra = io.read(extraLength)
unless (@extra && @extra.length == extraLength)
extra = io.read(extraLength)
if (extra && extra.length != extraLength)
raise ZipError, "Truncated local zip entry header"
else
if ZipExtraField === @extra
@extra.merge(extra)
else
@extra = ZipExtraField.new(extra)
end
end
end
@ -423,7 +455,8 @@ module Zip
io <<
[LOCAL_ENTRY_SIGNATURE ,
0 , # @version ,
@version ,
@fstype ,
0 , # @gpFlags ,
@compression_method ,
@time.to_binary_dos_time , # @lastModTime ,
@ -432,9 +465,9 @@ module Zip
@compressed_size ,
@size ,
@name ? @name.length : 0,
@extra? @extra.length : 0 ].pack('VvvvvvVVVvv')
@extra? @extra.local_length : 0 ].pack('VCCvvvvVVVvv')
io << @name
io << @extra
io << (@extra ? @extra.to_local_bin : "")
end
CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
@ -447,7 +480,8 @@ module Zip
end
cdirSignature ,
@version ,
@version , # version of encoding software
@fstype , # filesystem tye
@versionNeededToExtract,
@gpFlags ,
@compression_method ,
@ -465,7 +499,7 @@ module Zip
@localHeaderOffset ,
@name ,
@extra ,
@comment = staticSizedFieldsBuf.unpack('VvvvvvvVVVvvvvvVV')
@comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV')
unless (cdirSignature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE)
raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
@ -473,7 +507,11 @@ module Zip
set_time(lastModDate, lastModTime)
@name = io.read(nameLength)
@extra = io.read(extraLength)
if ZipExtraField === @extra
@extra.merge(io.read(extraLength))
else
@extra = ZipExtraField.new(io.read(extraLength))
end
@comment = io.read(commentLength)
unless (@comment && @comment.length == commentLength)
raise ZipError, "Truncated cdir zip entry header"
@ -492,7 +530,8 @@ module Zip
def write_c_dir_entry(io) #:nodoc:all
io <<
[CENTRAL_DIRECTORY_ENTRY_SIGNATURE,
0 , # @version ,
@version , # version of encoding software
@fstype , # filesystem type
0 , # @versionNeededToExtract ,
0 , # @gpFlags ,
@compression_method ,
@ -502,18 +541,18 @@ module Zip
@compressed_size ,
@size ,
@name ? @name.length : 0 ,
@extra ? @extra.length : 0 ,
@extra ? @extra.c_dir_length : 0 ,
@comment ? comment.length : 0 ,
0 , # disk number start
0 , # @internalFileAttributes ,
0 , # @externalFileAttributes ,
@internalFileAttributes , # file type (binary=0, text=1)
@externalFileAttributes , # native filesystem attributes
@localHeaderOffset ,
@name ,
@extra ,
@comment ].pack('VvvvvvvVVVvvvvvVV')
@comment ].pack('VCCvvvvvVVVvvvvvVV')
io << @name
io << @extra
io << (@extra ? @extra.to_c_dir_bin : "")
io << @comment
end
@ -526,7 +565,7 @@ module Zip
@size == other.size &&
@name == other.name &&
@extra == other.extra &&
@time.dos_equals(other.time))
self.time.dos_equals(other.time))
end
def <=> (other)
@ -1212,6 +1251,214 @@ module Zip
end
end
class ZipExtraField < Hash
ID_MAP = {}
# Meta class for extra fields
class Generic
def self.register_map
if self.const_defined?(:HEADER_ID)
ID_MAP[self.const_get(:HEADER_ID)] = self
end
end
def self.name
self.to_s.split("::")[-1]
end
# return field [size, content] or false
def initial_parse(binstr)
if ! binstr
# If nil, start with empty.
return false
elsif binstr[0,2] != self.class.const_get(:HEADER_ID)
$stderr.puts "Warning: weired extra feild header ID. skip parsing"
return false
end
[binstr[2,2].unpack("v")[0], binstr[4..-1]]
end
def ==(other)
self.class != other.class and return false
each { |k, v|
v != other[k] and return false
}
true
end
def to_local_bin
s = pack_for_local
self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
end
def to_c_dir_bin
s = pack_for_c_dir
self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
end
end
# Info-ZIP Additional timestamp field
class UniversalTime < Generic
HEADER_ID = "UT"
register_map
def initialize(binstr = nil)
@ctime = nil
@mtime = nil
@atime = nil
@flag = nil
binstr and merge(binstr)
end
attr_accessor :atime, :ctime, :mtime, :flag
def merge(binstr)
binstr == "" and return
size, content = initial_parse(binstr)
size or return
@flag, mtime, atime, ctime = content.unpack("CVVV")
mtime and @mtime ||= Time.at(mtime)
atime and @atime ||= Time.at(atime)
ctime and @ctime ||= Time.at(ctime)
end
def ==(other)
@mtime == other.mtime &&
@atime == other.atime &&
@ctime == other.ctime
end
def pack_for_local
s = [@flag].pack("C")
@flag & 1 != 0 and s << [@mtime.to_i].pack("V")
@flag & 2 != 0 and s << [@atime.to_i].pack("V")
@flag & 4 != 0 and s << [@ctime.to_i].pack("V")
s
end
def pack_for_c_dir
s = [@flag].pack("C")
@flag & 1 == 1 and s << [@mtime.to_i].pack("V")
s
end
end
# Info-ZIP Extra for UNIX uid/gid
class IUnix < Generic
HEADER_ID = "Ux"
register_map
def initialize(binstr = nil)
@uid = nil
@gid = nil
binstr and merge(binstr)
end
attr_accessor :uid, :gid
def merge(binstr)
binstr == "" and return
size, content = initial_parse(binstr)
# size: 0 for central direcotry. 4 for local header
return if(! size || size == 0)
uid, gid = content.unpack("vv")
@uid ||= uid
@gid ||= gid
end
def ==(other)
@uid == other.uid &&
@gid == other.gid
end
def pack_for_local
[@uid, @gid].pack("vv")
end
def pack_for_c_dir
""
end
end
## start main of ZipExtraField < Hash
def initialize(binstr = nil)
binstr and merge(binstr)
end
def merge(binstr)
binstr == "" and return
i = 0
while i < binstr.length
id = binstr[i,2]
len = binstr[i+2,2].to_s.unpack("v")[0]
if id && ID_MAP.member?(id)
field_name = ID_MAP[id].name
if self.member?(field_name)
self[field_name].mergea(binstr[i, len+4])
else
field_obj = ID_MAP[id].new(binstr[i, len+4])
self[field_name] = field_obj
end
elsif id
unless self["Unknown"]
s = ""
class << s
alias_method :to_c_dir_bin, :to_s
alias_method :to_local_bin, :to_s
end
self["Unknown"] = s
end
if ! len || len+4 > binstr[i..-1].length
self["Unknown"] << binstr[i..-1]
break;
end
self["Unknown"] << binstr[i, len+4]
end
i += len+4
end
end
def create(name)
field_class = nil
ID_MAP.each { |id, klass|
if klass.name == name
field_class = klass
break
end
}
if ! field_class
raise ZipError, "Unknown extra field '#{name}'"
end
self[name] = field_class.new()
end
def to_local_bin
s = ""
each { |k, v|
s << v.to_local_bin
}
s
end
alias :to_s :to_local_bin
def to_c_dir_bin
s = ""
each { |k, v|
s << v.to_c_dir_bin
}
s
end
def c_dir_length
to_c_dir_bin.length
end
def local_length
to_local_bin.length
end
alias :c_dir_size :c_dir_length
alias :local_size :local_length
alias :length :local_length
alias :size :local_length
end # end ZipExtraField
end # Zip namespace module

View File

@ -53,9 +53,28 @@ module Zip
def blocks; nil; end
def gid; 0; end
def get_entry
@zipFsFile.__send__(:get_entry, @entryName)
end
private :get_entry
def uid; 0; end
def gid
e = get_entry
if e.extra.member? "IUnix"
e.extra["IUnix"].gid || 0
else
0
end
end
def uid
e = get_entry
if e.extra.member? "IUnix"
e.extra["IUnix"].uid || 0
else
0
end
end
def ino; 0; end
@ -81,38 +100,72 @@ module Zip
def blksize; nil; end
def mode; 33206; end # 33206 is equivalent to -rw-rw-rw-
def mode
e = get_entry
if e.fstype == 3
e.externalFileAttributes >> 16
else
33206 # 33206 is equivalent to -rw-rw-rw-
end
end
end
def initialize(mappedZip)
@mappedZip = mappedZip
end
def get_entry(fileName)
if ! exists?(fileName)
raise Errno::ENOENT, "No such file or directory - #{fileName}"
end
@mappedZip.find_entry(fileName)
end
private :get_entry
def unix_mode_cmp(fileName, mode)
begin
e = get_entry(fileName)
e.fstype == 3 && ((e.externalFileAttributes >> 16) & mode ) != 0
rescue Errno::ENOENT
false
end
end
private :unix_mode_cmp
def exists?(fileName)
expand_path(fileName) == "/" || @mappedZip.find_entry(fileName) != nil
end
alias :exist? :exists?
# Permissions not implemented, so if the file exists it is accessible
alias readable? exists?
alias readable_real? exists?
alias writable? exists?
alias writable_real? exists?
alias executable? exists?
alias executable_real? exists?
alias owned? exists?
alias grpowned? exists?
def readable?(fileName)
unix_mode_cmp(fileName, 0444)
end
alias readable_real? readable?
def writable?(fileName)
unix_mode_cmp(fileName, 0222)
end
alias writable_real? writable?
def executable?(fileName)
unix_mode_cmp(fileName, 0111)
end
alias executable_real? executable?
def setuid?(fileName)
false
unix_mode_cmp(fileName, 04000)
end
def setgid?(fileName)
false
unix_mode_cmp(fileName, 02000)
end
def sticky?(fileName)
false
unix_mode_cmp(fileName, 01000)
end
def umask(*args)
@ -154,15 +207,22 @@ module Zip
end
def chown(ownerInt, groupInt, *filenames)
filenames.each { |fileName|
e = get_entry(fileName)
unless e.extra.member?("IUnix")
e.extra.create("IUnix")
end
e.extra["IUnix"].uid = ownerInt
e.extra["IUnix"].gid = groupInt
}
filenames.size
end
def chmod (modeInt, *filenames)
filenames.each {
|elem|
if ! exists?(elem)
raise Errno::ENOENT, "No such file or directory - #{elem}"
end
filenames.each { |fileName|
e = get_entry(fileName)
e.fstype = 3 # force convertion filesystem type to unix
e.externalFileAttributes = modeInt << 16
}
filenames.size
end
@ -195,8 +255,10 @@ module Zip
::File.join(*fragments)
end
def utime(accessTime, *fileNames)
raise StandardError, "utime not supported"
def utime(modifiedTime, *fileNames)
fileNames.each { |fileName|
get_entry(fileName).time = modifiedTime
}
end
def mtime(fileName)
@ -204,13 +266,21 @@ module Zip
end
def atime(fileName)
@mappedZip.get_entry(fileName)
nil
e = get_entry(fileName)
if e.extra.member? "UniversalTime"
e.extra["UniversalTime"].atime
else
nil
end
end
def ctime(fileName)
@mappedZip.get_entry(fileName)
nil
e = get_entry(fileName)
if e.extra.member? "UniversalTime"
e.extra["UniversalTime"].ctime
else
nil
end
end
def pipe?(filename)