zippo 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|