archive-zip 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CONTRIBUTORS +13 -0
- data/GPL +676 -0
- data/HACKING +122 -0
- data/LEGAL +8 -0
- data/LICENSE +57 -0
- data/MANIFEST +26 -0
- data/NEWS +22 -0
- data/README +130 -0
- data/lib/archive/support/io-like.rb +12 -0
- data/lib/archive/support/io.rb +14 -0
- data/lib/archive/support/iowindow.rb +123 -0
- data/lib/archive/support/stringio.rb +22 -0
- data/lib/archive/support/time.rb +85 -0
- data/lib/archive/support/zlib.rb +211 -0
- data/lib/archive/zip.rb +643 -0
- data/lib/archive/zip/codec.rb +30 -0
- data/lib/archive/zip/codec/deflate.rb +206 -0
- data/lib/archive/zip/codec/store.rb +241 -0
- data/lib/archive/zip/datadescriptor.rb +54 -0
- data/lib/archive/zip/entry.rb +991 -0
- data/lib/archive/zip/error.rb +22 -0
- data/lib/archive/zip/extrafield.rb +23 -0
- data/lib/archive/zip/extrafield/extendedtimestamp.rb +101 -0
- data/lib/archive/zip/extrafield/raw.rb +32 -0
- data/lib/archive/zip/extrafield/unix.rb +101 -0
- data/test/test_archive.rb +8 -0
- metadata +98 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
class StringIO
|
4
|
+
unless StringIO.method_defined?(:readbytes)
|
5
|
+
# Copied from IO#readbytes.
|
6
|
+
def readbytes(n)
|
7
|
+
str = read(n)
|
8
|
+
if str == nil
|
9
|
+
raise EOFError, "end of file reached"
|
10
|
+
end
|
11
|
+
if str.size < n
|
12
|
+
raise TruncatedDataError.new("data truncated", str)
|
13
|
+
end
|
14
|
+
str
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Always returns +true+. Added for compatibility with IO#seekable?.
|
19
|
+
def seekable?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Time
|
2
|
+
# Returns a DOSTime representing this time object as a DOS date-time
|
3
|
+
# structure. Times are bracketed by the limits of the ability of the DOS
|
4
|
+
# date-time structure to represent them. Accuracy is 2 seconds and years
|
5
|
+
# range from 1980 to 2099. The returned structure represents as closely as
|
6
|
+
# possible the time of this object.
|
7
|
+
#
|
8
|
+
# See DOSTime#new for a description of this structure.
|
9
|
+
def to_dos_time
|
10
|
+
dos_sec = sec/2
|
11
|
+
dos_year = year - 1980
|
12
|
+
dos_year = 0 if dos_year < 0
|
13
|
+
dos_year = 119 if dos_year > 119
|
14
|
+
|
15
|
+
DOSTime.new(
|
16
|
+
(dos_sec ) |
|
17
|
+
(min << 5) |
|
18
|
+
(hour << 11) |
|
19
|
+
(day << 16) |
|
20
|
+
(month << 21) |
|
21
|
+
(dos_year << 25)
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# A representation of the DOS time structure which can be converted into
|
27
|
+
# instances of Time.
|
28
|
+
class DOSTime
|
29
|
+
include Comparable
|
30
|
+
|
31
|
+
# Creates a new instance of DOSTime. _dos_time_ is a 4 byte String or
|
32
|
+
# unsigned number (Integer) representing an MS-DOS time structure where:
|
33
|
+
# Bits 0-4:: 2 second increments (0-29)
|
34
|
+
# Bits 5-10:: minutes (0-59)
|
35
|
+
# Bits 11-15:: hours (0-24)
|
36
|
+
# Bits 16-20:: day (1-31)
|
37
|
+
# Bits 21-24:: month (1-12)
|
38
|
+
# Bits 25-31:: four digit year minus 1980 (0-119)
|
39
|
+
#
|
40
|
+
# If _dos_time_ is ommitted or +nil+, a new instance is created based on the
|
41
|
+
# current time.
|
42
|
+
def initialize(dos_time = nil)
|
43
|
+
case dos_time
|
44
|
+
when nil
|
45
|
+
@dos_time = Time.now.to_dos_time.dos_time
|
46
|
+
when Integer
|
47
|
+
@dos_time = dos_time
|
48
|
+
else
|
49
|
+
unless dos_time.length == 4 then
|
50
|
+
raise ArgumentError, 'length of DOS time structure is not 4'
|
51
|
+
end
|
52
|
+
@dos_time = dos_time.unpack('V')[0]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns -1 if _other_ is a time earlier than this one, 0 if _other_ is the
|
57
|
+
# same time, and 1 if _other_ is a later time.
|
58
|
+
def cmp(other)
|
59
|
+
@dos_time <=> other.dos_time
|
60
|
+
end
|
61
|
+
alias :<=> :cmp
|
62
|
+
|
63
|
+
# Returns the time value of this object as an integer representing the DOS
|
64
|
+
# time structure.
|
65
|
+
def to_i
|
66
|
+
@dos_time
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns a Time instance which is equivalent to the time represented by this
|
70
|
+
# object.
|
71
|
+
def to_time
|
72
|
+
second = ((0b11111 & @dos_time) ) * 2
|
73
|
+
minute = ((0b111111 << 5 & @dos_time) >> 5)
|
74
|
+
hour = ((0b11111 << 11 & @dos_time) >> 11)
|
75
|
+
day = ((0b11111 << 16 & @dos_time) >> 16)
|
76
|
+
month = ((0b1111 << 21 & @dos_time) >> 21)
|
77
|
+
year = ((0b1111111 << 25 & @dos_time) >> 25) + 1980
|
78
|
+
return Time.local(year, month, day, hour, minute, second)
|
79
|
+
end
|
80
|
+
|
81
|
+
protected
|
82
|
+
|
83
|
+
# Used by _cmp_ to read another time stored in another DOSTime instance.
|
84
|
+
attr_reader :dos_time # :nodoc:
|
85
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
3
|
+
require 'archive/support/io-like'
|
4
|
+
|
5
|
+
module Zlib # :nodoc:
|
6
|
+
if ! const_defined?(:MAX_WBITS) then
|
7
|
+
MAX_WBITS = Deflate::MAX_WBITS
|
8
|
+
end
|
9
|
+
|
10
|
+
# Zlib::ZWriter is a writable, IO-like object (includes IO::Like) which wraps
|
11
|
+
# other writable, IO-like objects in order to facilitate writing data to those
|
12
|
+
# objects using the deflate method of compression.
|
13
|
+
class ZWriter
|
14
|
+
include IO::Like
|
15
|
+
|
16
|
+
# Creates a new instance of this class with the given arguments using #new
|
17
|
+
# and then passes the instance to the given block. The #close method is
|
18
|
+
# guaranteed to be called after the block completes.
|
19
|
+
#
|
20
|
+
# Equivalent to #new if no block is given.
|
21
|
+
def self.open(io, level = Zlib::DEFAULT_COMPRESSION, window_bits = nil, mem_level = nil, strategy = nil)
|
22
|
+
zw = new(io, level, window_bits, mem_level, strategy)
|
23
|
+
return zw unless block_given?
|
24
|
+
|
25
|
+
begin
|
26
|
+
yield(zw)
|
27
|
+
ensure
|
28
|
+
zw.close unless zw.closed?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Creates a new instance of this class. _io_ must respond to the _write_
|
33
|
+
# method as an instance of IO would. _level_, _window_bits_, _mem_level_,
|
34
|
+
# and _strategy_ are all passed directly to Zlib::Deflate.new(). See the
|
35
|
+
# documentation of that method for their meanings.
|
36
|
+
#
|
37
|
+
# NOTE: Due to limitations in Ruby's finalization capabilities, the #close
|
38
|
+
# method is _not_ automatically called when this object is garbage
|
39
|
+
# collected. Make sure to call #close when finished with this object.
|
40
|
+
def initialize(io, level = Zlib::DEFAULT_COMPRESSION, window_bits = nil, mem_level = nil, strategy = nil)
|
41
|
+
@delegate = io
|
42
|
+
@deflater = Zlib::Deflate.new(level, window_bits, mem_level, strategy)
|
43
|
+
@deflate_buffer = ''
|
44
|
+
@crc32 = 0
|
45
|
+
end
|
46
|
+
|
47
|
+
# The CRC32 checksum of the uncompressed data written using this object.
|
48
|
+
#
|
49
|
+
# NOTE: Anything still in the internal write buffer has not been processed,
|
50
|
+
# so calling #flush prior to examining this attribute may be necessary for
|
51
|
+
# an accurate computation.
|
52
|
+
attr_reader :crc32
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
# The delegate object to which compressed data is written.
|
57
|
+
attr_reader :delegate
|
58
|
+
|
59
|
+
public
|
60
|
+
|
61
|
+
# Closes the writer by finishing the compressed data and flushing it to the
|
62
|
+
# delegate.
|
63
|
+
#
|
64
|
+
# Raises IOError if called more than once.
|
65
|
+
def close
|
66
|
+
super()
|
67
|
+
delegate.write(@deflater.finish)
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the number of bytes of compressed data produced so far.
|
72
|
+
#
|
73
|
+
# NOTE: Anything still in the internal write buffer has not been processed,
|
74
|
+
# so calling #flush prior to calling this method may be necessary for an
|
75
|
+
# accurate count.
|
76
|
+
def compressed_size
|
77
|
+
@deflater.total_out
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the number of bytes sent to be compressed so far.
|
81
|
+
#
|
82
|
+
# NOTE: Anything still in the internal write buffer has not been processed,
|
83
|
+
# so calling #flush prior to calling this method may be necessary for an
|
84
|
+
# accurate count.
|
85
|
+
def uncompressed_size
|
86
|
+
@deflater.total_in
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def unbuffered_write(string)
|
92
|
+
until @deflate_buffer.empty? do
|
93
|
+
@deflate_buffer.slice!(0, delegate.write(@deflate_buffer))
|
94
|
+
end
|
95
|
+
@deflate_buffer = @deflater.deflate(string)
|
96
|
+
|
97
|
+
begin
|
98
|
+
@deflate_buffer.slice!(0, delegate.write(@deflate_buffer))
|
99
|
+
rescue Errno::EINTR, Errno::EAGAIN
|
100
|
+
# Ignore this because everything is in the deflate buffer and will be
|
101
|
+
# attempted again the next time this method is called.
|
102
|
+
end
|
103
|
+
@crc32 = Zlib.crc32(string, @crc32)
|
104
|
+
string.length
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Zlib::ZReader is a readable, IO-like object (includes IO::Like) which wraps
|
109
|
+
# other readable, IO-like objects in order to facilitate reading data from
|
110
|
+
# those objects using the inflate method of decompression.
|
111
|
+
class ZReader
|
112
|
+
include IO::Like
|
113
|
+
|
114
|
+
# Creates a new instance of this class with the given arguments using #new
|
115
|
+
# and then passes the instance to the given block. The #close method is
|
116
|
+
# guaranteed to be called after the block completes.
|
117
|
+
#
|
118
|
+
# Equivalent to #new if no block is given.
|
119
|
+
def self.open(io, window_bits = nil)
|
120
|
+
zr = new(io, window_bits)
|
121
|
+
return zr unless block_given?
|
122
|
+
|
123
|
+
begin
|
124
|
+
yield(zr)
|
125
|
+
ensure
|
126
|
+
zr.close unless zr.closed?
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Creates a new instance of this class. _io_ must respond to the _read_
|
131
|
+
# method as an IO instance would. _window_bits_ is passed directly to
|
132
|
+
# Zlib::Inflate.new(). See the documentation of that method for its
|
133
|
+
# meaning. If _io_ also responds to _rewind_, then the _rewind_ method of
|
134
|
+
# this class can be used to reset the whole stream back to the beginning.
|
135
|
+
#
|
136
|
+
# NOTE: Due to limitations in Ruby's finalization capabilities, the #close
|
137
|
+
# method is _not_ automatically called when this object is garbage
|
138
|
+
# collected. Make sure to call #close when finished with this object.
|
139
|
+
def initialize(io, window_bits = nil)
|
140
|
+
@delegate = io
|
141
|
+
@window_bits = window_bits
|
142
|
+
@inflater = Zlib::Inflate.new(@window_bits)
|
143
|
+
@crc32 = 0
|
144
|
+
@decompress_buffer = ''
|
145
|
+
end
|
146
|
+
|
147
|
+
# The CRC32 checksum of the uncompressed data read using this object.
|
148
|
+
#
|
149
|
+
# NOTE: The contents of the internal read buffer are immediately processed
|
150
|
+
# any time the buffer is filled, so this count is only accurate if all data
|
151
|
+
# has been read out of this object.
|
152
|
+
attr_reader :crc32
|
153
|
+
|
154
|
+
protected
|
155
|
+
|
156
|
+
# The delegate object from which compressed data is read.
|
157
|
+
attr_reader :delegate
|
158
|
+
|
159
|
+
public
|
160
|
+
|
161
|
+
# Closes the reader.
|
162
|
+
#
|
163
|
+
# Raises IOError if called more than once.
|
164
|
+
def close
|
165
|
+
super()
|
166
|
+
@inflater.close
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns the number of bytes sent to be decompressed so far.
|
171
|
+
def compressed_size
|
172
|
+
@inflater.total_in
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns the number of bytes of decompressed data produced so far.
|
176
|
+
def uncompressed_size
|
177
|
+
@inflater.total_out
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def unbuffered_read(length)
|
183
|
+
raise EOFError, 'end of file reached' if @inflater.finished?
|
184
|
+
|
185
|
+
begin
|
186
|
+
while @decompress_buffer.length < length && ! @inflater.finished? do
|
187
|
+
@decompress_buffer << @inflater.inflate(delegate.read(1))
|
188
|
+
end
|
189
|
+
rescue Errno::EINTR, Errno::EAGAIN
|
190
|
+
raise if @decompress_buffer.empty?
|
191
|
+
end
|
192
|
+
buffer = @decompress_buffer.slice!(0, length)
|
193
|
+
@crc32 = Zlib.crc32(buffer, @crc32)
|
194
|
+
buffer
|
195
|
+
end
|
196
|
+
|
197
|
+
def unbuffered_seek(offset, whence = IO::SEEK_SET)
|
198
|
+
unless offset == 0 && whence == IO::SEEK_SET then
|
199
|
+
raise Errno::EINVAL, 'Invalid argument'
|
200
|
+
end
|
201
|
+
unless delegate.respond_to?(:rewind) then
|
202
|
+
raise Errno::ESPIPE, 'Illegal seek'
|
203
|
+
end
|
204
|
+
delegate.rewind
|
205
|
+
@inflater.close
|
206
|
+
@inflater = Zlib::Inflate.new(@window_bits)
|
207
|
+
@crc32 = 0
|
208
|
+
@decompress_buffer = ''
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
data/lib/archive/zip.rb
ADDED
@@ -0,0 +1,643 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'set'
|
5
|
+
require 'tempfile'
|
6
|
+
|
7
|
+
require 'archive/support/io'
|
8
|
+
require 'archive/support/iowindow'
|
9
|
+
require 'archive/support/stringio'
|
10
|
+
require 'archive/support/time'
|
11
|
+
require 'archive/support/zlib'
|
12
|
+
require 'archive/zip/codec'
|
13
|
+
require 'archive/zip/entry'
|
14
|
+
require 'archive/zip/error'
|
15
|
+
|
16
|
+
module Archive # :nodoc:
|
17
|
+
# Archive::Zip represents a ZIP archive compatible with InfoZip tools and the
|
18
|
+
# archives they generate. It currently supports both stored and deflated ZIP
|
19
|
+
# entries, directory entries, file entries, and symlink entries. File and
|
20
|
+
# directory accessed and modified times, POSIX permissions, and ownerships can
|
21
|
+
# be archived and restored as well depending on platform support for such
|
22
|
+
# metadata.
|
23
|
+
#
|
24
|
+
# Zip64, digital signatures, and encryption are not supported. ZIP archives
|
25
|
+
# can only be read from seekable kinds of IO, such as files; reading archives
|
26
|
+
# from pipes or any other non-seekable kind of IO is not supported. However,
|
27
|
+
# writing to such IO objects <b><em>IS</em></b> supported.
|
28
|
+
class Zip
|
29
|
+
include Enumerable
|
30
|
+
|
31
|
+
# The lead-in marker for the end of central directory record.
|
32
|
+
EOCD_SIGNATURE = "PK\x5\x6" # 0x06054b50
|
33
|
+
# The lead-in marker for the digital signature record.
|
34
|
+
DS_SIGNATURE = "PK\x5\x5" # 0x05054b50
|
35
|
+
# The lead-in marker for the ZIP64 end of central directory record.
|
36
|
+
Z64EOCD_SIGNATURE = "PK\x6\x6" # 0x06064b50
|
37
|
+
# The lead-in marker for the ZIP64 end of central directory locator record.
|
38
|
+
Z64EOCDL_SIGNATURE = "PK\x6\x7" # 0x07064b50
|
39
|
+
# The lead-in marker for a central file record.
|
40
|
+
CFH_SIGNATURE = "PK\x1\x2" # 0x02014b50
|
41
|
+
# The lead-in marker for a local file record.
|
42
|
+
LFH_SIGNATURE = "PK\x3\x4" # 0x04034b50
|
43
|
+
# The lead-in marker for data descriptor record.
|
44
|
+
DD_SIGNATURE = "PK\x7\x8" # 0x08074b50
|
45
|
+
|
46
|
+
|
47
|
+
# A convenience method which opens a new or existing archive located in the
|
48
|
+
# path indicated by _archive_path_, adds and updates entries based on the
|
49
|
+
# paths given in _paths_, and then saves and closes the archive. See the
|
50
|
+
# instance method #archive for more information about _paths_ and _options_.
|
51
|
+
def self.archive(archive_path, paths, options = {})
|
52
|
+
open(archive_path) { |z| z.archive(paths, options) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# A convenience method which opens an archive located in the path indicated
|
56
|
+
# by _archive_path_, extracts the entries to the path indicated by
|
57
|
+
# _destination_, and then closes the archive. See the instance method
|
58
|
+
# #extract for more information about _destination_ and _options_.
|
59
|
+
def self.extract(archive_path, destination, options = {})
|
60
|
+
open(archive_path) { |z| z.extract(destination, options) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Calls #new with the given arguments and yields the resulting Zip instance
|
64
|
+
# to the given block. Returns the result of the block and ensures that the
|
65
|
+
# Zip instance is closed.
|
66
|
+
#
|
67
|
+
# This is a synonym for #new if no block is given.
|
68
|
+
def self.open(archive_path, archive_out = nil)
|
69
|
+
zf = new(archive_path, archive_out)
|
70
|
+
return zf unless block_given?
|
71
|
+
|
72
|
+
begin
|
73
|
+
yield(zf)
|
74
|
+
ensure
|
75
|
+
zf.close unless zf.closed?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Open and parse the file located at the path indicated by _archive_path_ if
|
80
|
+
# _archive_path_ is not +nil+ and the path exists. If _archive_out_ is
|
81
|
+
# unspecified or +nil+, any changes made will be saved in place, replacing
|
82
|
+
# the current archive with a new one having the same name. If _archive_out_
|
83
|
+
# is a String, it points to a file which will recieve the new archive's
|
84
|
+
# contents. Otherwise, _archive_out_ is assumed to be a writable, IO-like
|
85
|
+
# object operating in *binary* mode which will recieve the new archive's
|
86
|
+
# contents.
|
87
|
+
#
|
88
|
+
# At least one of _archive_path_ and _archive_out_ must be specified and
|
89
|
+
# non-nil; otherwise, an error will be raised.
|
90
|
+
def initialize(archive_path, archive_out = nil)
|
91
|
+
if (archive_path.nil? || archive_path.empty?) &&
|
92
|
+
(archive_out.nil? ||
|
93
|
+
archive_out.kind_of?(String) && archive_out.empty?) then
|
94
|
+
raise ArgumentError, 'No valid source or destination archive specified'
|
95
|
+
end
|
96
|
+
@archive_path = archive_path
|
97
|
+
@archive_out = archive_out
|
98
|
+
@entries = {}
|
99
|
+
@dirty = false
|
100
|
+
@comment = ''
|
101
|
+
@closed = false
|
102
|
+
if ! @archive_path.nil? && File.exist?(@archive_path) then
|
103
|
+
@archive_in = File.new(@archive_path, 'rb')
|
104
|
+
parse(@archive_in)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# A comment string for the ZIP archive.
|
109
|
+
attr_accessor :comment
|
110
|
+
|
111
|
+
# Close the archive. It is at this point that any changes made to the
|
112
|
+
# archive will be persisted to an output stream.
|
113
|
+
#
|
114
|
+
# Raises Archive::Zip::IOError if called more than once.
|
115
|
+
def close
|
116
|
+
raise IOError, 'closed archive' if closed?
|
117
|
+
|
118
|
+
if @dirty then
|
119
|
+
# There is something to write...
|
120
|
+
if @archive_out.nil? then
|
121
|
+
# Update the archive "in place".
|
122
|
+
tmp_archive_path = nil
|
123
|
+
Tempfile.open(*File.split(@archive_path).reverse) do |archive_out|
|
124
|
+
# Ensure the file is in binary mode for Windows.
|
125
|
+
archive_out.binmode
|
126
|
+
# Save off the path so that the temporary file can be renamed to the
|
127
|
+
# archive file later.
|
128
|
+
tmp_archive_path = archive_out.path
|
129
|
+
dump(archive_out)
|
130
|
+
end
|
131
|
+
File.chmod(0666 & ~File.umask, tmp_archive_path)
|
132
|
+
elsif @archive_out.kind_of?(String) then
|
133
|
+
# Open a new archive to receive the data.
|
134
|
+
File.open(@archive_out, 'wb') do |archive_out|
|
135
|
+
dump(archive_out)
|
136
|
+
end
|
137
|
+
else
|
138
|
+
# Assume the given object is an IO-like object and dump the archive
|
139
|
+
# contents to it.
|
140
|
+
dump(@archive_out)
|
141
|
+
end
|
142
|
+
@archive_in.close unless @archive_in.nil?
|
143
|
+
# The rename must happen after the original archive is closed when
|
144
|
+
# running on Windows since that platform does not allow a file which is
|
145
|
+
# in use to be replaced as is required when trying to update the archive
|
146
|
+
# "in place".
|
147
|
+
File.rename(tmp_archive_path, @archive_path) if @archive_out.nil?
|
148
|
+
end
|
149
|
+
|
150
|
+
closed = true
|
151
|
+
nil
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns +true+ if the ZIP archive is closed, false otherwise.
|
155
|
+
def closed?
|
156
|
+
@closed
|
157
|
+
end
|
158
|
+
|
159
|
+
# When the ZIP archive is open, this method iterates through each entry in
|
160
|
+
# turn yielding each one to the given block. Since Zip includes Enumerable,
|
161
|
+
# Zip instances are enumerables of Entry instances.
|
162
|
+
#
|
163
|
+
# Raises Archive::Zip::IOError if called after #close.
|
164
|
+
def each(&b)
|
165
|
+
raise IOError, 'closed archive' if @closed
|
166
|
+
|
167
|
+
@entries.each_value(&b)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Add _entry_ into the ZIP archive replacing any existing entry with the
|
171
|
+
# same zip path.
|
172
|
+
#
|
173
|
+
# Raises Archive::Zip::IOError if called after #close.
|
174
|
+
def add_entry(entry)
|
175
|
+
raise IOError, 'closed archive' if @closed
|
176
|
+
unless entry.kind_of?(Entry) then
|
177
|
+
raise ArgumentError, 'Archive::Zip::Entry instance required'
|
178
|
+
end
|
179
|
+
|
180
|
+
@entries[entry.zip_path] = entry
|
181
|
+
@dirty = true
|
182
|
+
self
|
183
|
+
end
|
184
|
+
alias :<< :add_entry
|
185
|
+
|
186
|
+
# Look up an entry based on the zip path located in _zip_path_. Returns
|
187
|
+
# +nil+ if no entry is found.
|
188
|
+
def get_entry(zip_path)
|
189
|
+
@entries[zip_path]
|
190
|
+
end
|
191
|
+
alias :[] :get_entry
|
192
|
+
|
193
|
+
# Removes an entry from the ZIP file and returns the entry or +nil+ if no
|
194
|
+
# entry was found to remove. If _entry_ is an instance of
|
195
|
+
# Archive::Zip::Entry, the zip_path attribute is used to find the entry to
|
196
|
+
# remove; otherwise, _entry_ is assumed to be a zip path matching an entry
|
197
|
+
# in the ZIP archive.
|
198
|
+
#
|
199
|
+
# Raises Archive::Zip::IOError if called after #close.
|
200
|
+
def remove_entry(entry)
|
201
|
+
raise IOError, 'closed archive' if @closed
|
202
|
+
|
203
|
+
zip_path = entry
|
204
|
+
zip_path = entry.zip_path if entry.kind_of?(Entry)
|
205
|
+
entry = @entries.delete(zip_path)
|
206
|
+
entry = entry[1] unless entry.nil?
|
207
|
+
@dirty ||= ! entry.nil?
|
208
|
+
entry
|
209
|
+
end
|
210
|
+
|
211
|
+
# Adds _paths_ to the archive. _paths_ may be either a single path or an
|
212
|
+
# Array of paths. The files and directories referenced by _paths_ are added
|
213
|
+
# using their respective basenames as their zip paths. The exception to
|
214
|
+
# this is when the basename for a path is either <tt>"."</tt> or
|
215
|
+
# <tt>".."</tt>. In this case, the path is replaced with the paths to the
|
216
|
+
# contents of the directory it references.
|
217
|
+
#
|
218
|
+
# _options_ is a Hash optionally containing the following:
|
219
|
+
# <b>:path_prefix</b>::
|
220
|
+
# Specifies a prefix to be added to the zip_path attribute of each entry
|
221
|
+
# where `/' is the file separator character. This defaults to the empty
|
222
|
+
# string. All values are passed through Archive::Zip::Entry.expand_path
|
223
|
+
# before use.
|
224
|
+
# <b>:recursion</b>::
|
225
|
+
# When set to +true+ (the default), the contents of directories are
|
226
|
+
# recursively added to the archive.
|
227
|
+
# <b>:directories</b>::
|
228
|
+
# When set to +true+ (the default), entries are added to the archive for
|
229
|
+
# directories. Otherwise, the entries for directories will not be added;
|
230
|
+
# however, the contents of the directories will still be considered if the
|
231
|
+
# <b>:recursion</b> option is +true+.
|
232
|
+
# <b>:symlinks</b>::
|
233
|
+
# When set to +false+ (the default), entries for symlinks are excluded
|
234
|
+
# from the archive. Otherwise, they are included. <b>NOTE:</b> Unless
|
235
|
+
# <b>:follow_symlinks</b> is explicitly set, it will be set to the logical
|
236
|
+
# NOT of this option in calls to Archive::Zip::Entry.from_file. If
|
237
|
+
# symlinks should be completely ignored, set both this option and
|
238
|
+
# <b>:follow_symlinks</b> to +false+. See Archive::Zip::Entry.from_file
|
239
|
+
# for details regarding <b>:follow_symlinks</b>.
|
240
|
+
# <b>:flatten</b>::
|
241
|
+
# When set to +false+ (the default), the directory paths containing
|
242
|
+
# archived files will be included in the zip paths of entries representing
|
243
|
+
# the files. When set to +true+ files are archived without any containing
|
244
|
+
# directory structure in the zip paths. Setting to +true+ implies that
|
245
|
+
# <b>:directories</b> is +false+ and <b>:path_prefix</b> is empty.
|
246
|
+
# <b>:exclude</b>::
|
247
|
+
# Specifies a proc or lambda which takes a single argument containing a
|
248
|
+
# prospective zip entry and returns +true+ if the entry should be excluded
|
249
|
+
# from the archive and +false+ if it should be included. <b>NOTE:</b> If
|
250
|
+
# a directory is excluded in this way, the <b>:recursion</b> option has no
|
251
|
+
# effect for it.
|
252
|
+
# <b>:ignore_error</b>::
|
253
|
+
# When set to +false+ (the default), an error generated while creating an
|
254
|
+
# archive entry for a file will be raised. Otherwise, the bad file is
|
255
|
+
# skipped.
|
256
|
+
# Any other options which are supported by Archive::Zip::Entry.from_file are
|
257
|
+
# also supported.
|
258
|
+
#
|
259
|
+
# Raises Archive::Zip::IOError if called after #close. Raises
|
260
|
+
# Archive::Zip::EntryError if the <b>:ignore_error</b> option is +false+ and
|
261
|
+
# Archive::Zip::Entry.from_file raises an error.
|
262
|
+
#
|
263
|
+
# == Example
|
264
|
+
#
|
265
|
+
# A directory contains:
|
266
|
+
# zip-test
|
267
|
+
# +- dir1
|
268
|
+
# | +- file2.txt
|
269
|
+
# +- dir2
|
270
|
+
# +- file1.txt
|
271
|
+
#
|
272
|
+
# Create some archives:
|
273
|
+
# Archive::Zip.open('zip-test1.zip') do |z|
|
274
|
+
# z.archive('zip-test')
|
275
|
+
# end
|
276
|
+
#
|
277
|
+
# Archive::Zip.open('zip-test2.zip') do |z|
|
278
|
+
# z.archive('zip-test/.', :path_prefix => 'a/b/c/d')
|
279
|
+
# end
|
280
|
+
#
|
281
|
+
# Archive::Zip.open('zip-test3.zip') do |z|
|
282
|
+
# z.archive('zip-test', :directories => false)
|
283
|
+
# end
|
284
|
+
#
|
285
|
+
# Archive::Zip.open('zip-test4.zip') do |z|
|
286
|
+
# z.archive('zip-test', :exclude => lambda { |e| e.file? })
|
287
|
+
# end
|
288
|
+
#
|
289
|
+
# The archives contain:
|
290
|
+
# zip-test1.zip -> zip-test/
|
291
|
+
# zip-test/dir1/
|
292
|
+
# zip-test/dir1/file2.txt
|
293
|
+
# zip-test/dir2/
|
294
|
+
# zip-test/file1.txt
|
295
|
+
#
|
296
|
+
# zip-test2.zip -> a/b/c/d/dir1/
|
297
|
+
# a/b/c/d/dir1/file2.txt
|
298
|
+
# a/b/c/d/dir2/
|
299
|
+
# a/b/c/d/file1.txt
|
300
|
+
#
|
301
|
+
# zip-test3.zip -> zip-test/dir1/file2.txt
|
302
|
+
# zip-test/file1.txt
|
303
|
+
#
|
304
|
+
# zip-test4.zip -> zip-test/
|
305
|
+
# zip-test/dir1/
|
306
|
+
# zip-test/dir2/
|
307
|
+
def archive(paths, options = {})
|
308
|
+
raise IOError, 'closed archive' if @closed
|
309
|
+
|
310
|
+
# Ensure that paths is an enumerable.
|
311
|
+
paths = [paths] unless paths.kind_of?(Enumerable)
|
312
|
+
# If the basename of a path is '.' or '..', replace the path with the
|
313
|
+
# paths of all the entries contained within the directory referenced by
|
314
|
+
# the original path.
|
315
|
+
paths = paths.collect do |path|
|
316
|
+
basename = File.basename(path)
|
317
|
+
if basename == '.' || basename == '..' then
|
318
|
+
Dir.entries(path).reject do |e|
|
319
|
+
e == '.' || e == '..'
|
320
|
+
end.collect do |e|
|
321
|
+
File.join(path, e)
|
322
|
+
end
|
323
|
+
else
|
324
|
+
path
|
325
|
+
end
|
326
|
+
end.flatten.uniq
|
327
|
+
|
328
|
+
# Ensure that unspecified options have default values.
|
329
|
+
options[:path_prefix] = '' unless options.has_key?(:path_prefix)
|
330
|
+
options[:recursion] = true unless options.has_key?(:recursion)
|
331
|
+
options[:directories] = true unless options.has_key?(:directories)
|
332
|
+
options[:symlinks] = false unless options.has_key?(:symlinks)
|
333
|
+
options[:flatten] = false unless options.has_key?(:flatten)
|
334
|
+
options[:ignore_error] = false unless options.has_key?(:ignore_error)
|
335
|
+
|
336
|
+
# Flattening the directory structure implies that directories are skipped
|
337
|
+
# and that the path prefix should be ignored.
|
338
|
+
if options[:flatten] then
|
339
|
+
options[:path_prefix] = ''
|
340
|
+
options[:directories] = false
|
341
|
+
end
|
342
|
+
|
343
|
+
# Clean up the path prefix.
|
344
|
+
options[:path_prefix] = Entry.expand_path(options[:path_prefix].to_s)
|
345
|
+
|
346
|
+
paths.each do |path|
|
347
|
+
# Generate the zip path.
|
348
|
+
zip_entry_path = File.basename(path)
|
349
|
+
zip_entry_path += '/' if File.directory?(path)
|
350
|
+
unless options[:path_prefix].empty? then
|
351
|
+
zip_entry_path = "#{options[:path_prefix]}/#{zip_entry_path}"
|
352
|
+
end
|
353
|
+
|
354
|
+
begin
|
355
|
+
# Create the entry, but do not add it to the archive yet.
|
356
|
+
zip_entry = Zip::Entry.from_file(
|
357
|
+
path,
|
358
|
+
options.merge(
|
359
|
+
:zip_path => zip_entry_path,
|
360
|
+
:follow_symlinks => options.has_key?(:follow_symlinks) ?
|
361
|
+
options[:follow_symlinks] :
|
362
|
+
! options[:symlinks]
|
363
|
+
)
|
364
|
+
)
|
365
|
+
rescue Zip::EntryError
|
366
|
+
# Ignore the error if requested.
|
367
|
+
if options[:ignore_error] then
|
368
|
+
next
|
369
|
+
else
|
370
|
+
raise
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Skip this entry if so directed.
|
375
|
+
if (zip_entry.symlink? && ! options[:symlinks]) ||
|
376
|
+
(! options[:exclude].nil? && options[:exclude].call(zip_entry)) then
|
377
|
+
next
|
378
|
+
end
|
379
|
+
|
380
|
+
# Add entries for directories (if requested) and files/symlinks.
|
381
|
+
if (! zip_entry.directory? || options[:directories]) then
|
382
|
+
add_entry(zip_entry)
|
383
|
+
end
|
384
|
+
|
385
|
+
|
386
|
+
# Recurse into subdirectories (if requested).
|
387
|
+
if zip_entry.directory? && options[:recursion] then
|
388
|
+
archive(
|
389
|
+
Dir.entries(path).reject do |e|
|
390
|
+
e == '.' || e == '..'
|
391
|
+
end.collect do |e|
|
392
|
+
File.join(path, e)
|
393
|
+
end,
|
394
|
+
options.merge(:path_prefix => zip_entry_path)
|
395
|
+
)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
nil
|
400
|
+
end
|
401
|
+
|
402
|
+
# Extracts the contents of the archive to _destination_, where _destination_
|
403
|
+
# is a path to a directory which will contain the contents of the archive.
|
404
|
+
# The destination path will be created if it does not already exist.
|
405
|
+
#
|
406
|
+
# _options_ is a Hash optionally containing the following:
|
407
|
+
# <b>:directories</b>::
|
408
|
+
# When set to +true+ (the default), entries representing directories in
|
409
|
+
# the archive are extracted. This happens after all non-directory entries
|
410
|
+
# are extracted so that directory metadata can be properly updated.
|
411
|
+
# <b>:symlinks</b>::
|
412
|
+
# When set to +false+ (the default), entries representing symlinks in the
|
413
|
+
# archive are skipped. When set to +true+, such entries are extracted.
|
414
|
+
# Exceptions may be raised on plaforms/file systems which do not support
|
415
|
+
# symlinks.
|
416
|
+
# <b>:overwrite</b>::
|
417
|
+
# When set to <tt>:all</tt> (the default), files which already exist will
|
418
|
+
# be replaced. When set to <tt>:older</tt>, such files will only be
|
419
|
+
# replaced if they are older according to their last modified times than
|
420
|
+
# the zip entry which would replace them. When set to <tt>:none</tt>,
|
421
|
+
# such files will never be replaced. Any other value is the same as
|
422
|
+
# <tt>:all</tt>.
|
423
|
+
# <b>:create</b>::
|
424
|
+
# When set to +true+ (the default), files and directories which do not
|
425
|
+
# already exist will be extracted. When set to +false+ only files and
|
426
|
+
# directories which already exist will be extracted (depending on the
|
427
|
+
# setting of <b>:overwrite</b>).
|
428
|
+
# <b>:flatten</b>::
|
429
|
+
# When set to +false+ (the default), the directory paths containing
|
430
|
+
# extracted files will be created within +destination+ in order to contain
|
431
|
+
# the files. When set to +true+ files are extracted directly to
|
432
|
+
# +destination+ and directory entries are skipped.
|
433
|
+
# <b>:exclude</b>::
|
434
|
+
# Specifies a proc or lambda which takes a single argument containing a
|
435
|
+
# zip entry and returns +true+ if the entry should be skipped during
|
436
|
+
# extraction and +false+ if it should be extracted.
|
437
|
+
# Any other options which are supported by Archive::Zip::Entry#extract are
|
438
|
+
# also supported.
|
439
|
+
#
|
440
|
+
# Raises Archive::Zip::IOError if called after #close.
|
441
|
+
#
|
442
|
+
# == Example
|
443
|
+
#
|
444
|
+
# An archive, <tt>archive.zip</tt>, contains:
|
445
|
+
# zip-test/
|
446
|
+
# zip-test/dir1/
|
447
|
+
# zip-test/dir1/file2.txt
|
448
|
+
# zip-test/dir2/
|
449
|
+
# zip-test/file1.txt
|
450
|
+
#
|
451
|
+
# A directory, <tt>extract4</tt>, contains:
|
452
|
+
# zip-test
|
453
|
+
# +- dir1
|
454
|
+
# +- file1.txt
|
455
|
+
#
|
456
|
+
# Extract the archive:
|
457
|
+
# Archive::Zip.open('archive.zip') do |z|
|
458
|
+
# z.extract('extract1')
|
459
|
+
# end
|
460
|
+
#
|
461
|
+
# Archive::Zip.open('archive.zip') do |z|
|
462
|
+
# z.extract('extract2', :flatten => true)
|
463
|
+
# end
|
464
|
+
#
|
465
|
+
# Archive::Zip.open('archive.zip') do |z|
|
466
|
+
# z.extract('extract3', :create => false)
|
467
|
+
# end
|
468
|
+
#
|
469
|
+
# Archive::Zip.open('archive.zip') do |z|
|
470
|
+
# z.extract('extract3', :create => true)
|
471
|
+
# end
|
472
|
+
#
|
473
|
+
# Archive::Zip.open('archive.zip') do |z|
|
474
|
+
# z.extract( 'extract5', :exclude => lambda { |e| e.file? })
|
475
|
+
# end
|
476
|
+
#
|
477
|
+
# The directories contain:
|
478
|
+
# extract1 -> zip-test
|
479
|
+
# +- dir1
|
480
|
+
# | +- file2.txt
|
481
|
+
# +- dir2
|
482
|
+
# +- file1.txt
|
483
|
+
#
|
484
|
+
# extract2 -> file2.txt
|
485
|
+
# file1.txt
|
486
|
+
#
|
487
|
+
# extract3 -> <empty>
|
488
|
+
#
|
489
|
+
# extract4 -> zip-test
|
490
|
+
# +- dir2
|
491
|
+
# +- file1.txt <- from archive contents
|
492
|
+
#
|
493
|
+
# extract5 -> zip-test
|
494
|
+
# +- dir1
|
495
|
+
# +- dir2
|
496
|
+
def extract(destination, options = {})
|
497
|
+
raise IOError, 'closed archive' if @closed
|
498
|
+
|
499
|
+
# Ensure that unspecified options have default values.
|
500
|
+
options[:directories] = true unless options.has_key?(:directories)
|
501
|
+
options[:symlinks] = false unless options.has_key?(:symlinks)
|
502
|
+
options[:overwrite] = :all unless options[:overwrite] == :older ||
|
503
|
+
options[:overwrite] == :never
|
504
|
+
options[:create] = true unless options.has_key?(:create)
|
505
|
+
options[:flatten] = false unless options.has_key?(:flatten)
|
506
|
+
|
507
|
+
# Flattening the archive structure implies that directory entries are
|
508
|
+
# skipped.
|
509
|
+
options[:directories] = false if options[:flatten]
|
510
|
+
|
511
|
+
# First extract all non-directory entries.
|
512
|
+
directories = []
|
513
|
+
each do |entry|
|
514
|
+
# Compute the target file path.
|
515
|
+
file_path = entry.zip_path
|
516
|
+
file_path = File.basename(file_path) if options[:flatten]
|
517
|
+
file_path = File.join(destination, file_path)
|
518
|
+
|
519
|
+
# Cache some information about the file path.
|
520
|
+
file_exists = File.exist?(file_path)
|
521
|
+
file_mtime = File.mtime(file_path) if file_exists
|
522
|
+
|
523
|
+
# Skip this entry if so directed.
|
524
|
+
if (! file_exists && ! options[:create]) ||
|
525
|
+
(file_exists &&
|
526
|
+
(options[:overwrite] == :never ||
|
527
|
+
options[:overwrite] == :older && entry.mtime <= file_mtime)) ||
|
528
|
+
(! options[:exclude].nil? && options[:exclude].call(entry)) then
|
529
|
+
next
|
530
|
+
end
|
531
|
+
|
532
|
+
if entry.directory? then
|
533
|
+
# Record the directories as they are encountered.
|
534
|
+
directories << entry
|
535
|
+
elsif entry.file? || (entry.symlink? && options[:symlinks]) then
|
536
|
+
# Extract files and symlinks.
|
537
|
+
entry.extract(
|
538
|
+
options.merge(:file_path => file_path)
|
539
|
+
)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
if options[:directories] then
|
544
|
+
# Then extract the directory entries in depth first order so that time
|
545
|
+
# stamps, ownerships, and permissions can be properly restored.
|
546
|
+
directories.sort { |a, b| b.zip_path <=> a.zip_path }.each do |entry|
|
547
|
+
entry.extract(
|
548
|
+
options.merge(:file_path => File.join(destination, entry.zip_path))
|
549
|
+
)
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
nil
|
554
|
+
end
|
555
|
+
|
556
|
+
private
|
557
|
+
|
558
|
+
# NOTE: For now _io_ MUST be seekable and report such by returning +true+
|
559
|
+
# from its seekable? method. See IO#seekable?.
|
560
|
+
#
|
561
|
+
# Raises Archive::Zip::IOError if _io_ is not seekable.
|
562
|
+
def parse(io)
|
563
|
+
# Error out if the IO object is not confirmed seekable.
|
564
|
+
raise Zip::IOError, 'non-seekable IO object given' unless io.respond_to?(:seekable?) and io.seekable?
|
565
|
+
|
566
|
+
socd_pos = find_central_directory(io)
|
567
|
+
io.seek(socd_pos)
|
568
|
+
# Parse each entry in the central directory.
|
569
|
+
loop do
|
570
|
+
signature = io.readbytes(4)
|
571
|
+
break unless signature == CFH_SIGNATURE
|
572
|
+
add_entry(Zip::Entry.parse(io))
|
573
|
+
end
|
574
|
+
@dirty = false
|
575
|
+
# Maybe add support for digital signatures and ZIP64 records... Later
|
576
|
+
|
577
|
+
nil
|
578
|
+
end
|
579
|
+
|
580
|
+
# Returns the file offset of the first record in the central directory.
|
581
|
+
# _io_ must be a seekable, readable, IO-like object.
|
582
|
+
#
|
583
|
+
# Raises Archive::Zip::UnzipError if the end of central directory signature
|
584
|
+
# is not found where expected or at all.
|
585
|
+
def find_central_directory(io)
|
586
|
+
# First find the offset to the end of central directory record.
|
587
|
+
# It is expected that the variable length comment field will usually be
|
588
|
+
# empty and as a result the initial value of eocd_offset is all that is
|
589
|
+
# necessary.
|
590
|
+
#
|
591
|
+
# NOTE: A cleverly crafted comment could throw this thing off if the
|
592
|
+
# comment itself looks like a valid end of central directory record.
|
593
|
+
eocd_offset = -22
|
594
|
+
loop do
|
595
|
+
io.seek(eocd_offset, IO::SEEK_END)
|
596
|
+
if io.readbytes(4) == EOCD_SIGNATURE then
|
597
|
+
io.seek(16, IO::SEEK_CUR)
|
598
|
+
break if io.readbytes(2).unpack('v')[0] == (eocd_offset + 22).abs
|
599
|
+
end
|
600
|
+
eocd_offset -= 1
|
601
|
+
end
|
602
|
+
# At this point, eocd_offset should point to the location of the end of
|
603
|
+
# central directory record relative to the end of the archive.
|
604
|
+
# Now, jump into the location in the record which contains a pointer to
|
605
|
+
# the start of the central directory record and return the value.
|
606
|
+
io.seek(eocd_offset + 16, IO::SEEK_END)
|
607
|
+
return io.readbytes(4).unpack('V')[0]
|
608
|
+
rescue Errno::EINVAL
|
609
|
+
raise Zip::UnzipError, 'unable to locate end-of-central-directory record'
|
610
|
+
end
|
611
|
+
|
612
|
+
# Writes all the entries of this archive to _io_. _io_ must be a writable,
|
613
|
+
# IO-like object providing a _write_ method. Returns the total number of
|
614
|
+
# bytes written.
|
615
|
+
def dump(io)
|
616
|
+
bytes_written = 0
|
617
|
+
entries = @entries.values
|
618
|
+
entries.each do |entry|
|
619
|
+
bytes_written += entry.dump_local_file_record(io, bytes_written)
|
620
|
+
end
|
621
|
+
central_directory_offset = bytes_written
|
622
|
+
entries.each do |entry|
|
623
|
+
bytes_written += entry.dump_central_file_record(io)
|
624
|
+
end
|
625
|
+
central_directory_length = bytes_written - central_directory_offset
|
626
|
+
bytes_written += io.write(EOCD_SIGNATURE)
|
627
|
+
bytes_written += io.write(
|
628
|
+
[
|
629
|
+
0,
|
630
|
+
0,
|
631
|
+
entries.length,
|
632
|
+
entries.length,
|
633
|
+
central_directory_length,
|
634
|
+
central_directory_offset,
|
635
|
+
comment.length
|
636
|
+
].pack('vvvvVVv')
|
637
|
+
)
|
638
|
+
bytes_written += io.write(comment)
|
639
|
+
|
640
|
+
bytes_written
|
641
|
+
end
|
642
|
+
end
|
643
|
+
end
|