zippo 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +17 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +83 -0
- data/Rakefile +11 -0
- data/lib/zippo/binary_structure/base.rb +119 -0
- data/lib/zippo/binary_structure/binary_packer.rb +17 -0
- data/lib/zippo/binary_structure/binary_unpacker.rb +32 -0
- data/lib/zippo/binary_structure/meta.rb +146 -0
- data/lib/zippo/binary_structure/structure.rb +24 -0
- data/lib/zippo/binary_structure/structure_member.rb +31 -0
- data/lib/zippo/binary_structure.rb +6 -0
- data/lib/zippo/cd_file_header.rb +36 -0
- data/lib/zippo/central_directory_entries_unpacker.rb +23 -0
- data/lib/zippo/central_directory_reader.rb +44 -0
- data/lib/zippo/end_cd_record.rb +21 -0
- data/lib/zippo/filter/base.rb +29 -0
- data/lib/zippo/filter/compressor/deflate.rb +23 -0
- data/lib/zippo/filter/compressor/store.rb +12 -0
- data/lib/zippo/filter/compressor.rb +42 -0
- data/lib/zippo/filter/compressors.rb +3 -0
- data/lib/zippo/filter/null_filters.rb +15 -0
- data/lib/zippo/filter/uncompressor/deflate.rb +25 -0
- data/lib/zippo/filter/uncompressor/store.rb +12 -0
- data/lib/zippo/filter/uncompressor.rb +59 -0
- data/lib/zippo/filter/uncompressors.rb +3 -0
- data/lib/zippo/io_zip_member.rb +24 -0
- data/lib/zippo/local_file_header.rb +28 -0
- data/lib/zippo/version.rb +3 -0
- data/lib/zippo/zip_directory.rb +80 -0
- data/lib/zippo/zip_file.rb +121 -0
- data/lib/zippo/zip_file_writer.rb +57 -0
- data/lib/zippo/zip_member.rb +85 -0
- data/lib/zippo.rb +18 -0
- data/spec/binary_structure_spec.rb +132 -0
- data/spec/central_directory_entries_unpacker_spec.rb +29 -0
- data/spec/central_directory_parser_spec.rb +50 -0
- data/spec/central_directory_unpacker_spec.rb +31 -0
- data/spec/compressor_spec.rb +14 -0
- data/spec/data/comment.zip +0 -0
- data/spec/data/deflate.zip +0 -0
- data/spec/data/multi.zip +0 -0
- data/spec/data/not_a.zip +1 -0
- data/spec/data/test.zip +0 -0
- data/spec/deflate_compressor_spec.rb +21 -0
- data/spec/deflate_uncompressor_spec.rb +23 -0
- data/spec/integration/compressors_spec.rb +21 -0
- data/spec/integration/zippo_spec.rb +55 -0
- data/spec/io_zip_member_spec.rb +32 -0
- data/spec/local_file_header_spec.rb +18 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/store_compressor_spec.rb +19 -0
- data/spec/store_uncompressor_spec.rb +19 -0
- data/spec/uncompressor_spec.rb +14 -0
- data/spec/zip_directory_spec.rb +63 -0
- data/spec/zip_file_spec.rb +50 -0
- data/spec/zip_file_writer_spec.rb +42 -0
- data/spec/zip_member_spec.rb +42 -0
- data/yard_extensions.rb +10 -0
- data/zippo.gemspec +23 -0
- metadata +163 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
module Zippo
|
2
|
+
# Filters used to compress and decompress archive data.
|
3
|
+
module Filter
|
4
|
+
# Generic filter mixin.
|
5
|
+
module Base
|
6
|
+
# Use same block size as rubyzip for better comparison
|
7
|
+
BLOCK_SIZE = 131072
|
8
|
+
module ClassMethods
|
9
|
+
def filters
|
10
|
+
@filters_hash ||= Hash[@filters.map{|u| [u::METHOD, u]}]
|
11
|
+
end
|
12
|
+
def for(method)
|
13
|
+
filters[method] or raise "unknown compression method #{method}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def inherited(klass)
|
17
|
+
@filters_hash = nil
|
18
|
+
@filters << klass
|
19
|
+
end
|
20
|
+
end
|
21
|
+
def self.included(base)
|
22
|
+
base.class_eval do
|
23
|
+
@filters = []
|
24
|
+
extend ClassMethods
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'zippo/filter/compressor'
|
2
|
+
|
3
|
+
module Zippo::Filter
|
4
|
+
# Compresses the input data using zlib.
|
5
|
+
class DeflateCompressor < Compressor
|
6
|
+
METHOD = 8
|
7
|
+
DEFAULT_COMPRESSION = Zlib::DEFAULT_COMPRESSION
|
8
|
+
|
9
|
+
def initialize(io, compression_mode = DEFAULT_COMPRESSION)
|
10
|
+
super(io)
|
11
|
+
@zlib = Zlib::Deflate.new(compression_mode, -Zlib::MAX_WBITS)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
def filter(buf)
|
16
|
+
@zlib.deflate(buf)
|
17
|
+
end
|
18
|
+
|
19
|
+
def tail_filter
|
20
|
+
@zlib.finish unless @zlib.finished?
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'zippo/filter/compressor'
|
2
|
+
require 'zippo/filter/null_filters'
|
3
|
+
|
4
|
+
module Zippo::Filter
|
5
|
+
# StoreCompressor stores the original member data directly in the zip
|
6
|
+
# file. No compression is performed.
|
7
|
+
class StoreCompressor < Compressor
|
8
|
+
METHOD = 0
|
9
|
+
|
10
|
+
include NullFilters
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'zippo/filter/base'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module Zippo::Filter
|
5
|
+
# A compressor is responsible for writing (and likely
|
6
|
+
# compressing) data into a zip file.
|
7
|
+
#
|
8
|
+
# @example Obtaining a compressor
|
9
|
+
# compression_method = 8
|
10
|
+
# compressor = Compressor.for(compression_method).new(io)
|
11
|
+
# compressor.compress_to out
|
12
|
+
#
|
13
|
+
# Subclasse should include a METHOD constant to indicate
|
14
|
+
# which zip compression method they handle. The actual
|
15
|
+
# compression should be implemented in the filter and
|
16
|
+
# tail_filter methods.
|
17
|
+
class Compressor
|
18
|
+
include Zippo::Filter::Base
|
19
|
+
|
20
|
+
def initialize(io)
|
21
|
+
@io = io
|
22
|
+
end
|
23
|
+
|
24
|
+
def read count = nil, buf = nil
|
25
|
+
@io.read count, buf
|
26
|
+
end
|
27
|
+
|
28
|
+
def compress_to io
|
29
|
+
data_size = 0
|
30
|
+
compressed_size = 0
|
31
|
+
crc32 = 0
|
32
|
+
buf = ""
|
33
|
+
while (read BLOCK_SIZE, buf)
|
34
|
+
data_size += buf.bytesize
|
35
|
+
compressed_size += io.write filter(buf)
|
36
|
+
crc32 = Zlib.crc32(buf, crc32)
|
37
|
+
end
|
38
|
+
compressed_size += io.write tail_filter
|
39
|
+
return compressed_size, data_size, crc32
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'zippo/filter/uncompressor'
|
2
|
+
|
3
|
+
require 'zlib'
|
4
|
+
require 'stringio'
|
5
|
+
|
6
|
+
module Zippo::Filter
|
7
|
+
# Uncompresses the data using Zlib.
|
8
|
+
class DeflateUncompressor < Uncompressor
|
9
|
+
METHOD = 8
|
10
|
+
|
11
|
+
def initialize(io, compressed_size, zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)) # no header
|
12
|
+
super(io, compressed_size)
|
13
|
+
@zlib = zlib
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def filter(buf)
|
18
|
+
@zlib.inflate(buf)
|
19
|
+
end
|
20
|
+
|
21
|
+
def tail_filter
|
22
|
+
@zlib.finish
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'zippo/filter/uncompressor'
|
2
|
+
require 'zippo/filter/null_filters'
|
3
|
+
|
4
|
+
module Zippo::Filter
|
5
|
+
# StoreUncompressor returns the member data as stored in the zip file.
|
6
|
+
# No uncompression is performed.
|
7
|
+
class StoreUncompressor < Uncompressor
|
8
|
+
METHOD = 0
|
9
|
+
|
10
|
+
include NullFilters
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'zippo/filter/base'
|
2
|
+
|
3
|
+
module Zippo::Filter
|
4
|
+
# An Uncompressor is responsible for reading (and likely
|
5
|
+
# uncompressing) member data from a Zip file.
|
6
|
+
#
|
7
|
+
# @example Obtaining an uncompressor
|
8
|
+
# compression_method = 8
|
9
|
+
# uncompressor = Uncompressor.for(compression_method).new(io)
|
10
|
+
# uncompressor.uncompress_to out
|
11
|
+
#
|
12
|
+
# Subclasse should include a METHOD constant to indicate
|
13
|
+
# which zip compression method they handle. The actual
|
14
|
+
# uncompression should be implemented in the filter and
|
15
|
+
# tail_filter methods.
|
16
|
+
class Uncompressor
|
17
|
+
include Zippo::Filter::Base
|
18
|
+
|
19
|
+
# @param [IO] io the IO the member data is located in
|
20
|
+
# @param [Integer] compressed_size the compressed size of the data
|
21
|
+
def initialize(io, compressed_size)
|
22
|
+
# XXX should probably capture the offset here
|
23
|
+
# current usage assumes the IO has already been positioned at
|
24
|
+
# the appropriate location
|
25
|
+
@io = io
|
26
|
+
@compressed_size = compressed_size
|
27
|
+
@remaining = @compressed_size
|
28
|
+
end
|
29
|
+
|
30
|
+
def read n, buf = nil
|
31
|
+
if @remaining >= n
|
32
|
+
@remaining -= n
|
33
|
+
elsif (n = @remaining) > 0
|
34
|
+
@remaining = 0
|
35
|
+
else
|
36
|
+
return nil
|
37
|
+
end
|
38
|
+
|
39
|
+
@io.read n, buf
|
40
|
+
buf.replace filter(buf)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Uncompresses the data to the specified IO
|
44
|
+
#
|
45
|
+
# @param [IO] io the object to uncompress to, must respond to #<<
|
46
|
+
def uncompress_to io
|
47
|
+
buf = ""
|
48
|
+
while (read BLOCK_SIZE, buf)
|
49
|
+
io << buf
|
50
|
+
end
|
51
|
+
io << tail_filter
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [String] the uncompressed data
|
55
|
+
def uncompress
|
56
|
+
uncompress_to ""
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'zippo/filter/compressor'
|
2
|
+
require 'zippo/filter/compressor/deflate' # XXX only used for the default method
|
3
|
+
|
4
|
+
module Zippo
|
5
|
+
# A zip member sourced from an IO object
|
6
|
+
class IOZipMember
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
def initialize(name, source)
|
10
|
+
@name = name
|
11
|
+
@source = source
|
12
|
+
end
|
13
|
+
|
14
|
+
def read
|
15
|
+
@source.read
|
16
|
+
ensure
|
17
|
+
@source.rewind
|
18
|
+
end
|
19
|
+
|
20
|
+
def write_to out, preferred_compression_method = Filter::DeflateCompressor::METHOD, recompress = nil
|
21
|
+
Filter::Compressor.for(preferred_compression_method).new(@source).compress_to(out)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'zippo/binary_structure'
|
2
|
+
|
3
|
+
module Zippo
|
4
|
+
# Represents a local file header as documented in APPNOTE.TXT
|
5
|
+
# @see http://www.pkware.com/documents/casestudies/APPNOTE.TXT
|
6
|
+
class LocalFileHeader
|
7
|
+
SIGNATURE = 0x04034b50
|
8
|
+
binary_structure do
|
9
|
+
# @!macro [attach] bs.field
|
10
|
+
# @!attribute [rw] $1
|
11
|
+
field :signature, 'L', :signature => SIGNATURE
|
12
|
+
field :version_extractable_by, 'S', :default => 20
|
13
|
+
field :bit_flags, 'S', :default => 0
|
14
|
+
field :compression_method, 'S'
|
15
|
+
field :last_modified_time, 'S', :default => 0 # XXX
|
16
|
+
field :last_modified_date, 'S', :default => 0 # XXX
|
17
|
+
field :crc32, 'L'
|
18
|
+
field :compressed_size, 'L'
|
19
|
+
field :uncompressed_size, 'L'
|
20
|
+
# set when name is set
|
21
|
+
field :file_name_length, 'S'
|
22
|
+
# set when extra_field is set
|
23
|
+
field :extra_field_length, 'S', :default => 0
|
24
|
+
field :name, 'a*', :size => :file_name_length
|
25
|
+
field :extra_field, 'a*', :default => '', :size => :extra_field_length
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'zippo/zip_member'
|
2
|
+
require 'zippo/io_zip_member'
|
3
|
+
require 'zippo/central_directory_reader'
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module Zippo
|
7
|
+
# The ZipDirectory is responsible for managing the set of ZipMembers
|
8
|
+
# belonging to a ZipFile.
|
9
|
+
class ZipDirectory
|
10
|
+
extend Forwardable
|
11
|
+
include Enumerable
|
12
|
+
# should delegate to entries_hash instead of entries whenever we can
|
13
|
+
def_delegators :entries_hash, :empty?
|
14
|
+
def_delegators :entries, :each, :map
|
15
|
+
|
16
|
+
def initialize io = nil
|
17
|
+
@io = io
|
18
|
+
end
|
19
|
+
|
20
|
+
# @param [String] name the name of the ZipMember
|
21
|
+
# @return [ZipMember] the ZipMember with the specified name
|
22
|
+
def [](name)
|
23
|
+
entries_hash[name]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Inserts data into the ZipFile
|
27
|
+
#
|
28
|
+
# @param [String] name the name of the member to insert
|
29
|
+
# @param [String] data the data to insert
|
30
|
+
def []=(name, data)
|
31
|
+
insert(name, StringIO.new(data))
|
32
|
+
end
|
33
|
+
|
34
|
+
# Inserts data into the ZipFile
|
35
|
+
#
|
36
|
+
# - when the source is a ZipMember, the already-compressed data may
|
37
|
+
# be re-used when writing out
|
38
|
+
# - when the source is a String, it is interpreted as a filename,
|
39
|
+
# and will be used as the source of the data
|
40
|
+
# - otherwise, source is assumed to an already opened IO object
|
41
|
+
#
|
42
|
+
# @param [String] name the name of the member to insert
|
43
|
+
# @param source where to read the data from
|
44
|
+
#
|
45
|
+
# Source can be any of
|
46
|
+
# - an IO object
|
47
|
+
# - a string (path to file)
|
48
|
+
# - another ZipMember (allowing direct stream copy)
|
49
|
+
def insert(name, source)
|
50
|
+
set name,
|
51
|
+
case source
|
52
|
+
when ZipMember then source.with_name name
|
53
|
+
when String then IOZipMember.new name, File.open(source, 'r:BINARY')
|
54
|
+
else IOZipMember.new name, source
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# @return [Hash] the hash of ZipMembers, the hash key is the
|
59
|
+
# member's name
|
60
|
+
def entries_hash
|
61
|
+
@entries_hash ||= if @io
|
62
|
+
CentralDirectoryReader.new(@io).cd_file_headers.each_with_object({}) do |header, hash|
|
63
|
+
hash[header.name] = ZipMember.new @io, header
|
64
|
+
end
|
65
|
+
else
|
66
|
+
{}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Array] the members of the ZipFile
|
71
|
+
def entries
|
72
|
+
entries_hash.values
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
def set(name, member)
|
77
|
+
entries_hash[name] = member
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
require 'zippo/zip_directory'
|
2
|
+
require 'zippo/zip_file_writer'
|
3
|
+
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module Zippo
|
7
|
+
# ZipFile represents a Zip archive.
|
8
|
+
#
|
9
|
+
# It can be called in block form like this:
|
10
|
+
#
|
11
|
+
# Zippo.open("file.zip") do |zip|
|
12
|
+
# str = zip["file.txt"]
|
13
|
+
# other = zip["other/file.txt"]
|
14
|
+
# puts str
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Or without a block:
|
18
|
+
#
|
19
|
+
# zip = Zippo.open("file.zip")
|
20
|
+
# puts zip["file.txt"]
|
21
|
+
# zip.close
|
22
|
+
#
|
23
|
+
# == Inserting archive members
|
24
|
+
#
|
25
|
+
# New archive members can be inserted using the insert method:
|
26
|
+
#
|
27
|
+
# zip = Zippo.open("out.zip", "w")
|
28
|
+
# zip.insert "out.txt", "something.txt"
|
29
|
+
#
|
30
|
+
# Additionally Zipfile#[]= has been overridden to support inserting
|
31
|
+
# strings directly:
|
32
|
+
#
|
33
|
+
# zip["other.txt"] = "now is the time"
|
34
|
+
#
|
35
|
+
# If you already have an IO object, you can just insert it:
|
36
|
+
#
|
37
|
+
# io = File.open("foo.bin")
|
38
|
+
# zip.insert "rename.bin", io
|
39
|
+
#
|
40
|
+
# Finally, you can insert a Zippo::ZipMember from another Zip file,
|
41
|
+
# which can allow the compressed data to be copied directly (avoiding
|
42
|
+
# having to uncompress and then recompress it):
|
43
|
+
#
|
44
|
+
# other = Zippo.open("other.zip")
|
45
|
+
# zip.insert "final.bin", other["final.bin"]
|
46
|
+
#
|
47
|
+
# No data will actually be written until the ZipFile is closed:
|
48
|
+
#
|
49
|
+
# zip.close
|
50
|
+
class ZipFile
|
51
|
+
# Opens a zip archive file
|
52
|
+
#
|
53
|
+
# @param [String] filename path to the archive
|
54
|
+
# @param [String] mode the mode to open in, 'r' or 'w' or 'rw'
|
55
|
+
#
|
56
|
+
# @return [Zippo::ZipFile] the opened zip file if no block is given
|
57
|
+
def self.open(filename, mode = 'r')
|
58
|
+
if block_given?
|
59
|
+
zippo = new(filename, mode)
|
60
|
+
a = yield zippo
|
61
|
+
zippo.close
|
62
|
+
return a
|
63
|
+
else
|
64
|
+
new filename, mode
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def initialize(filename, mode)
|
69
|
+
@filename = filename
|
70
|
+
@mode = mode
|
71
|
+
end
|
72
|
+
|
73
|
+
extend Forwardable
|
74
|
+
def_delegators :directory, :map, :[], :[]=, :each, :insert
|
75
|
+
|
76
|
+
# Closes the ZipFile, writing it to disk if it was opened in write
|
77
|
+
# mode
|
78
|
+
def close
|
79
|
+
# XXX should optimize to not write anything to unchanged files
|
80
|
+
# In update mode, we first write the zip to a temporary zip file,
|
81
|
+
# then move it on top of the original file
|
82
|
+
out_zip = update? ? tmp_zipfile : @filename
|
83
|
+
ZipFileWriter.new(directory, out_zip).write if write?
|
84
|
+
@io.close if @io
|
85
|
+
File.rename out_zip, @filename if update?
|
86
|
+
end
|
87
|
+
|
88
|
+
# @return [ZipDirectory] the ZipDirectory
|
89
|
+
def directory
|
90
|
+
@directory ||= if read?
|
91
|
+
ZipDirectory.new io
|
92
|
+
else
|
93
|
+
ZipDirectory.new
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def read?
|
99
|
+
File.exist? @filename and @mode.include? 'r'
|
100
|
+
end
|
101
|
+
|
102
|
+
def write?
|
103
|
+
@mode.include? 'w'
|
104
|
+
end
|
105
|
+
|
106
|
+
def update?
|
107
|
+
read? and write?
|
108
|
+
end
|
109
|
+
|
110
|
+
def io
|
111
|
+
@io ||= File.open(@filename, 'r:ASCII-8BIT')
|
112
|
+
end
|
113
|
+
|
114
|
+
def tmp_zipfile
|
115
|
+
# Not using Tempfile for performance
|
116
|
+
# Should probably throw a timestamp in there, in case multiple
|
117
|
+
# temps are being written at once from the one process
|
118
|
+
File.join File.dirname(@filename), ".zippo-tmp-#{Process.pid}-#{File.basename(@filename)}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Zippo
|
2
|
+
# ZipFileWriter writes the contents of a ZipDirectory to a Zip file.
|
3
|
+
class ZipFileWriter
|
4
|
+
# @param [ZipDirectory] directory the ZipDirectory to write
|
5
|
+
# @param [String] filename the filename to write to
|
6
|
+
def initialize(directory, filename)
|
7
|
+
@directory = directory
|
8
|
+
@filename = filename
|
9
|
+
end
|
10
|
+
|
11
|
+
# Writes the directory to the file.
|
12
|
+
def write
|
13
|
+
File.open(@filename,'wb:ASCII-8BIT') do |io|
|
14
|
+
packer = LocalFileHeader::Packer.new io
|
15
|
+
headers = []
|
16
|
+
for member in @directory
|
17
|
+
header = CdFileHeader.default
|
18
|
+
header.compression_method = 8 # XXX configurable
|
19
|
+
# XXX hack fix for maintaining method in zipped data copies
|
20
|
+
if member.is_a? ZipMember
|
21
|
+
header.compression_method = member.compression_method
|
22
|
+
end
|
23
|
+
header.name = member.name
|
24
|
+
header.extra_field = "" # XXX extra field unimplemented
|
25
|
+
header_size = header.file_name_length + header.extra_field_length + 30
|
26
|
+
|
27
|
+
# record header position so we can write it later
|
28
|
+
header.local_file_header_offset = io.pos
|
29
|
+
|
30
|
+
# move to after the header
|
31
|
+
io.seek header_size, IO::SEEK_CUR
|
32
|
+
|
33
|
+
# write the compressed data
|
34
|
+
header.compressed_size, header.uncompressed_size, header.crc32 =
|
35
|
+
member.write_to io, header.compression_method
|
36
|
+
|
37
|
+
# write the completed header, returning to the current position
|
38
|
+
io.seek header.local_file_header_offset
|
39
|
+
#packer.pack LocalFileHeader.from header.convert_to LocalHileHeader
|
40
|
+
packer.pack header.convert_to LocalFileHeader
|
41
|
+
io.seek header.compressed_size, IO::SEEK_CUR
|
42
|
+
headers << header
|
43
|
+
end
|
44
|
+
|
45
|
+
eocdr = EndCdRecord.default
|
46
|
+
eocdr.cd_offset = io.pos
|
47
|
+
packer = CdFileHeader::Packer.new io
|
48
|
+
for header in headers
|
49
|
+
packer.pack header
|
50
|
+
end
|
51
|
+
eocdr.cd_size = io.pos - eocdr.cd_offset
|
52
|
+
eocdr.records = eocdr.total_records = headers.size
|
53
|
+
EndCdRecord::Packer.new(io).pack eocdr
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'zippo/local_file_header'
|
2
|
+
require 'zippo/filter/uncompressors'
|
3
|
+
require 'zippo/filter/compressors'
|
4
|
+
|
5
|
+
require 'forwardable'
|
6
|
+
|
7
|
+
module Zippo
|
8
|
+
# A member of a Zip archive file.
|
9
|
+
class ZipMember
|
10
|
+
def initialize io, header
|
11
|
+
@io = io
|
12
|
+
@header = header
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [String] the name of the member
|
16
|
+
def name
|
17
|
+
@name ||= @header.name
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Boolean] True if the member is a directory, False
|
21
|
+
# otherwise
|
22
|
+
def directory?
|
23
|
+
name.end_with? '/'
|
24
|
+
end
|
25
|
+
|
26
|
+
extend Forwardable
|
27
|
+
def_delegators :@header, :crc32, :compressed_size, :uncompressed_size, :compression_method
|
28
|
+
|
29
|
+
# Reads (and possibly uncompresses) the member's data
|
30
|
+
#
|
31
|
+
# @return [String] the uncompressed member data
|
32
|
+
def read
|
33
|
+
seek_to_compressed_data
|
34
|
+
uncompressor.uncompress
|
35
|
+
end
|
36
|
+
|
37
|
+
# Duplicates this zip member and overrides the name.
|
38
|
+
#
|
39
|
+
# @param [String] name the name to use
|
40
|
+
# @return [ZipMember] the new ZipMember
|
41
|
+
def with_name name
|
42
|
+
dup.tap do |obj|
|
43
|
+
obj.instance_variable_set :@name, name
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Writes the member data to the specified IO using the specified
|
48
|
+
# compression method.
|
49
|
+
#
|
50
|
+
# @param [IO] out the IO to write to
|
51
|
+
# @param [Integer] preferred_method the compression method to use
|
52
|
+
# @param [Boolean] recompress whether or not to recompress the data
|
53
|
+
#
|
54
|
+
# @return [Integer, Integer, Integer] the amount written, the
|
55
|
+
# original size of the data, the crc32 of the data
|
56
|
+
def write_to out, preferred_method = Filter::DeflateCompressor::METHOD, recompress = false
|
57
|
+
seek_to_compressed_data
|
58
|
+
if recompress
|
59
|
+
Filter::Compressor.for(preferred_method).new(uncompressor).compress_to(out)
|
60
|
+
else
|
61
|
+
IO.copy_stream @io, out, @header.compressed_size
|
62
|
+
return @header.compressed_size, @header.uncompressed_size, @header.crc32
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def local_file_header
|
68
|
+
@io.seek @header.local_file_header_offset
|
69
|
+
LocalFileHeader::Unpacker.new(@io).unpack
|
70
|
+
end
|
71
|
+
|
72
|
+
def seek_to_compressed_data
|
73
|
+
@io.seek @header.local_file_header_offset + local_file_header.size
|
74
|
+
end
|
75
|
+
|
76
|
+
def uncompressor
|
77
|
+
Filter::Uncompressor.for(@header.compression_method).new(@io, @header.compressed_size)
|
78
|
+
end
|
79
|
+
|
80
|
+
def compressed_member_data
|
81
|
+
seek_to_compressed_data
|
82
|
+
@io.read @header.compressed_size
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/lib/zippo.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Ensure binary structure optimisations are active before any structures are defined
|
2
|
+
require 'zippo/binary_structure/meta'
|
3
|
+
|
4
|
+
require "zippo/version"
|
5
|
+
require 'zippo/zip_file'
|
6
|
+
require 'zippo/filter/uncompressors'
|
7
|
+
|
8
|
+
# Zippo is a Zip library.
|
9
|
+
module Zippo
|
10
|
+
class << self
|
11
|
+
# Calls Zippo::ZipFile.open
|
12
|
+
# @see ZipFile.open
|
13
|
+
def open filename, mode = 'r', &block
|
14
|
+
Zippo::ZipFile.open filename, mode, &block
|
15
|
+
end
|
16
|
+
public :open
|
17
|
+
end
|
18
|
+
end
|