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
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
MDM2MjkzNjVlMWI3OTlmYmU5NTJjMTVhZmExMzU2ODRjYmMwZWNiMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
Zjk3OWIwMTBjZWJiMmQyOTUwNzQ1YzgzOWExODc0NThiNjMwMDhmOA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NjI5ZDU5MTdiMWU0ZDc5NjBhNzMwMWY2YTRkNzYyZDRjZDMzNTFmOTcxMWI4
|
10
|
+
NDI3NGYzYWU5ZWQyOTEzYzY5N2U4YzFhNTA1ZWE2NTRlMTEzNTUzYzEyYmIx
|
11
|
+
ZGY4Njc5NjE1YWY5NWI4NjZjM2UxNGMxZTRiZGQ4MTFkZGM0OWU=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YmJkYjQ3YmQ2YWFjNjRhNGZmNWI5MTlhOGMzYjliMjg5MWQ4YzRhZTVhMGY3
|
14
|
+
OGIyZDJiMzM1NDNmMzY5MGVmNjFlZmFhMWM2MDY4ZjVmNDdiYzI4MmE1MDhk
|
15
|
+
N2I5Zjk1YzRlYTIwZWY2NDY5ZDMwYzMyNzEzZGI5NjMzZDFiZDE=
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--order random --color
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
lib/**/*.rb
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012-2013 Jonathon M. Abbott
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Zippo
|
2
|
+
|
3
|
+
Zippo is a fast zip library for ruby.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'zippo'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install zippo
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
It can be called in block form:
|
22
|
+
|
23
|
+
Zippo.open("file.zip") do |zip|
|
24
|
+
str = zip["file.txt"]
|
25
|
+
other = zip["other/file.txt"]
|
26
|
+
puts str
|
27
|
+
end
|
28
|
+
|
29
|
+
Or without a block:
|
30
|
+
|
31
|
+
zip = Zippo.open("file.zip")
|
32
|
+
puts zip["file.txt"]
|
33
|
+
zip.close
|
34
|
+
|
35
|
+
### Inserting archive members
|
36
|
+
|
37
|
+
Files can be inserted into the zip using the
|
38
|
+
insert method. Note that no data will be written until the
|
39
|
+
ZipFile is closed:
|
40
|
+
|
41
|
+
zip = Zippo.open("out.zip", "w")
|
42
|
+
zip.insert "file1.txt", "path/to/1.txt"
|
43
|
+
zip.insert "file2.txt", "path/to/2.txt"
|
44
|
+
zip.close
|
45
|
+
|
46
|
+
#### By path
|
47
|
+
|
48
|
+
zip.insert "out.txt", "something.txt"
|
49
|
+
|
50
|
+
#### Directly from a string buffer
|
51
|
+
|
52
|
+
zip["other.txt"] = "now is the time"
|
53
|
+
|
54
|
+
#### By IO
|
55
|
+
|
56
|
+
io = File.open("foo.dat")
|
57
|
+
zip.insert "data.dat", io
|
58
|
+
|
59
|
+
#### From another zip file (direct stream copy)
|
60
|
+
|
61
|
+
Inserting zip data from one file into another allows the
|
62
|
+
compressed data to be reused from the original zip file
|
63
|
+
(avoiding uncompression and recompression):
|
64
|
+
|
65
|
+
other = Zippo.open("other.zip")
|
66
|
+
zip.insert "final.bin", other["final.bin"]
|
67
|
+
|
68
|
+
## TODO
|
69
|
+
|
70
|
+
- implement date handling
|
71
|
+
- implement unix attribute handling
|
72
|
+
|
73
|
+
## Contributing
|
74
|
+
|
75
|
+
1. Fork it
|
76
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
77
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
78
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
79
|
+
5. Create new Pull Request
|
80
|
+
|
81
|
+
## License
|
82
|
+
|
83
|
+
MIT License. Copyright (c) 2012-2013 Jonathon M. Abbott
|
data/Rakefile
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'zippo/binary_structure/structure_member'
|
2
|
+
require 'zippo/binary_structure/structure'
|
3
|
+
require 'zippo/binary_structure/binary_packer'
|
4
|
+
require 'zippo/binary_structure/binary_unpacker'
|
5
|
+
|
6
|
+
module Zippo
|
7
|
+
# BinaryStructure defines a class level DSL for
|
8
|
+
# implementing binary strucutures.
|
9
|
+
#
|
10
|
+
# The class will then have an ::Unpacker and ::Packer class
|
11
|
+
# defined underneath it that can be used to read and write
|
12
|
+
# the defined fields from an io.
|
13
|
+
#
|
14
|
+
# The DSL itself is fairly simple, fields are defined with
|
15
|
+
# a field name, "packing code" (per standard ruby
|
16
|
+
# Array#pack) and possibly options.
|
17
|
+
#
|
18
|
+
# - the :signature option indicates the field is a fixed
|
19
|
+
# signature
|
20
|
+
# - the :size => <field> option indicates the field is a variable
|
21
|
+
# width size field, with the size previously recorded in
|
22
|
+
# the specified field
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# binary_structure do
|
26
|
+
# field :foo, 'L'
|
27
|
+
# field :yay, 'a4', :signature => "baz"
|
28
|
+
# field :bar, 'S'
|
29
|
+
# field :quux, 'a*', :size => :foo
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# @see Array#pack
|
33
|
+
module BinaryStructure
|
34
|
+
module Base
|
35
|
+
def binary_structure &block
|
36
|
+
@structure = Structure.create(self, &block)
|
37
|
+
self.const_set :Packer, Class.new(BinaryPacker)
|
38
|
+
self::Packer.structure = @structure
|
39
|
+
self.const_set :Unpacker, Class.new(BinaryUnpacker)
|
40
|
+
self::Unpacker.structure = @structure
|
41
|
+
|
42
|
+
@structure.fields.each do |field|
|
43
|
+
attr_reader field.name
|
44
|
+
if @structure.dependent? field.name
|
45
|
+
define_method "#{field.name}=" do |value|
|
46
|
+
raise "can't mutate a dependent field"
|
47
|
+
end
|
48
|
+
else
|
49
|
+
if field.dependent
|
50
|
+
class_eval """
|
51
|
+
def #{field.name}= value
|
52
|
+
@#{field.dependent} = value.bytesize
|
53
|
+
@#{field.name} = value
|
54
|
+
end
|
55
|
+
"""
|
56
|
+
else
|
57
|
+
attr_writer field.name
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
include InstanceMethods
|
62
|
+
extend ClassMethods
|
63
|
+
Base.after_structure_definition_hooks_for(self)
|
64
|
+
end
|
65
|
+
class << self
|
66
|
+
def after_structure_definition_hooks_for(klass)
|
67
|
+
@hooks.each do |hook|
|
68
|
+
hook.call(klass)
|
69
|
+
end if @hooks
|
70
|
+
end
|
71
|
+
def after_structure_definition &block
|
72
|
+
@hooks ||= []
|
73
|
+
@hooks << block
|
74
|
+
end
|
75
|
+
end
|
76
|
+
module InstanceMethods
|
77
|
+
def defaults
|
78
|
+
self.class.structure.fields.each do |field|
|
79
|
+
instance_variable_set "@#{field.name}", field.options[:default] if field.options[:default]
|
80
|
+
instance_variable_set "@#{field.name}", field.options[:signature] if field.options[:signature]
|
81
|
+
end
|
82
|
+
self
|
83
|
+
end
|
84
|
+
def size
|
85
|
+
self.class.structure.fields.map do |field|
|
86
|
+
if field.dependent
|
87
|
+
send field.dependent
|
88
|
+
else
|
89
|
+
field.width
|
90
|
+
end
|
91
|
+
end.inject(&:+)
|
92
|
+
end
|
93
|
+
|
94
|
+
def convert_to other
|
95
|
+
other.default.tap do |obj|
|
96
|
+
self.class.common_fields_with(other).each do |field|
|
97
|
+
obj.instance_variable_set "@#{field}", send(field)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
module ClassMethods
|
103
|
+
attr_reader :structure
|
104
|
+
def default
|
105
|
+
new.defaults
|
106
|
+
end
|
107
|
+
|
108
|
+
# Returns the fields that this data type has in common with other.
|
109
|
+
#
|
110
|
+
# - common fields are fields with the same name
|
111
|
+
# - signature fields are never common
|
112
|
+
def common_fields_with(other)
|
113
|
+
structure.fields.map(&:name) &
|
114
|
+
other.structure.fields.reject(&:signature?).map(&:name)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Zippo
|
2
|
+
module BinaryStructure
|
3
|
+
class BinaryPacker
|
4
|
+
class << self
|
5
|
+
attr_accessor :structure
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(io)
|
9
|
+
@io = io
|
10
|
+
end
|
11
|
+
|
12
|
+
def pack obj
|
13
|
+
@io << self.class.structure.fields.map {|f| obj.send f.name}.pack(self.class.structure.fields.map(&:pack).join(""))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Zippo
|
4
|
+
module BinaryStructure
|
5
|
+
class BinaryUnpacker
|
6
|
+
class << self
|
7
|
+
attr_accessor :structure
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(io)
|
11
|
+
@io = io
|
12
|
+
@io = StringIO.new @io if @io.is_a? String
|
13
|
+
end
|
14
|
+
|
15
|
+
# default implementation
|
16
|
+
# note that this will generally be overridden by
|
17
|
+
# define_unpack_method for optimisation
|
18
|
+
def unpack
|
19
|
+
self.class.structure.owner_class.new.tap do |obj|
|
20
|
+
self.class.structure.fields.each do |field|
|
21
|
+
if field.options[:size]
|
22
|
+
obj.instance_variable_set "@#{field.name}", @io.read(obj.send field.options[:size])
|
23
|
+
else
|
24
|
+
buf = @io.read field.width
|
25
|
+
obj.instance_variable_set "@#{field.name}", buf.unpack(field.pack).first
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'zippo/binary_structure/base'
|
2
|
+
|
3
|
+
module Zippo::BinaryStructure
|
4
|
+
# Profiling shows most of our time is spent iterating over various fields
|
5
|
+
# so let's unroll those loops in advance.
|
6
|
+
module CodeGen
|
7
|
+
class << self
|
8
|
+
# Defines a method on the specified class that sets a bunch of fields at once.
|
9
|
+
def define_helper(klass, meth, fields)
|
10
|
+
buf = []
|
11
|
+
buf << "def #{meth}(#{0.upto(fields.size-1).map{|x|"a#{x}"}.join(',')})"
|
12
|
+
fields.each_with_index do |field, i|
|
13
|
+
buf << "@#{field} = a#{i}"
|
14
|
+
end
|
15
|
+
buf << "end"
|
16
|
+
klass.class_eval buf.join("\n")
|
17
|
+
end
|
18
|
+
|
19
|
+
def fields_as_args(fields)
|
20
|
+
fields.map {|f| "@#{f}"}.join(", ")
|
21
|
+
end
|
22
|
+
|
23
|
+
def call_helper(receiver, meth, fields)
|
24
|
+
"#{receiver}.#{meth}(#{fields_as_args(fields)})"
|
25
|
+
end
|
26
|
+
|
27
|
+
def define_defaults_method_for(klass)
|
28
|
+
buf = []
|
29
|
+
buf << "def defaults"
|
30
|
+
klass.structure.fields.each do |field|
|
31
|
+
buf << %{@#{field.name} = #{field.options[:default].inspect}} if field.options[:default]
|
32
|
+
buf << %{@#{field.name} = #{field.options[:signature].inspect}} if field.options[:signature]
|
33
|
+
end
|
34
|
+
buf << "self"
|
35
|
+
buf << "end"
|
36
|
+
klass.class_eval buf.join("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
# XXX - should write a spec for the "multiple helpers"
|
40
|
+
# implementation, none of the current binary structures would make
|
41
|
+
# use of it, as they all have a bunch of fixed fields,
|
42
|
+
# then the variable fields at the end. a test should
|
43
|
+
#def self.define_unpack_method
|
44
|
+
def define_unpack_method_for(klass)
|
45
|
+
buf = "def unpack\n"
|
46
|
+
buf << "obj = self.class.structure.owner_class.new\n"
|
47
|
+
helper_num = -1 # keep track of the number of helper methods we've created
|
48
|
+
field_buf = []
|
49
|
+
# iterate over the fields, gathering up the fixed fields in a
|
50
|
+
# group. once a variable field is hit, unpack the current group of
|
51
|
+
# fixed fields, then use that to read any variable fields. repeat.
|
52
|
+
klass.structure.fields.each do |field|
|
53
|
+
if field.options[:size]
|
54
|
+
# unpack fixed group
|
55
|
+
unless field_buf.empty?
|
56
|
+
s = field_buf.map(&:width).inject(&:+)
|
57
|
+
buf << %{arr = @io.read(#{s}).unpack("#{field_buf.map(&:pack).join('')}")\n}
|
58
|
+
helper_name = "binary_structure_unpack_helper_#{helper_num += 1}"
|
59
|
+
define_helper(klass.structure.owner_class, helper_name, field_buf.map(&:name))
|
60
|
+
buf << "obj.#{helper_name}(*arr)\n"
|
61
|
+
end
|
62
|
+
# unpack variable-length field
|
63
|
+
buf << %{obj.instance_variable_set :@#{field.name}, @io.read(obj.#{field.options[:size]})\n}
|
64
|
+
field_buf = []
|
65
|
+
else
|
66
|
+
field_buf << field
|
67
|
+
end
|
68
|
+
end
|
69
|
+
buf << "obj\n"
|
70
|
+
buf << "end\n"
|
71
|
+
|
72
|
+
klass.class_eval(buf)
|
73
|
+
end
|
74
|
+
|
75
|
+
def define_converter_for(klass, meth, other)
|
76
|
+
# serialize the klass reference "other"
|
77
|
+
# we can't just use to_s, since that won't work if the class is anonymous
|
78
|
+
class_ref = "ObjectSpace._id2ref(#{other.object_id})"
|
79
|
+
|
80
|
+
common_fields = klass.common_fields_with(other)
|
81
|
+
|
82
|
+
helper_name = "initialize_from_#{object_id}"
|
83
|
+
define_helper(other, helper_name, common_fields)
|
84
|
+
|
85
|
+
default_fields = other.structure.fields.select do |field|
|
86
|
+
field.options[:default] || field.options[:signature]
|
87
|
+
end.reject do |field|
|
88
|
+
common_fields.include? field.name
|
89
|
+
end
|
90
|
+
|
91
|
+
define_helper(other, "other_fields", default_fields.map(&:name))
|
92
|
+
default_values = default_fields.map do |f|
|
93
|
+
f.options[:signature] ||
|
94
|
+
f.options[:default]
|
95
|
+
end
|
96
|
+
|
97
|
+
klass.class_eval """
|
98
|
+
def #{meth}
|
99
|
+
obj = #{class_ref}.new
|
100
|
+
obj.other_fields(#{default_values.map(&:inspect).join(', ')})
|
101
|
+
#{call_helper("obj", helper_name, common_fields)}
|
102
|
+
obj
|
103
|
+
end
|
104
|
+
"""
|
105
|
+
end
|
106
|
+
|
107
|
+
def define_pack_method_for klass
|
108
|
+
buf = []
|
109
|
+
|
110
|
+
fields = klass.structure.fields.map(&:name)
|
111
|
+
packing_string = klass.structure.fields.map(&:pack).join('')
|
112
|
+
|
113
|
+
helper_method =
|
114
|
+
"""
|
115
|
+
def fields_for_packing
|
116
|
+
[#{fields_as_args(fields)}]
|
117
|
+
end
|
118
|
+
"""
|
119
|
+
klass.structure.owner_class.class_eval helper_method
|
120
|
+
|
121
|
+
klass.class_eval do
|
122
|
+
define_method :pack do |obj|
|
123
|
+
@io << obj.fields_for_packing.pack(packing_string)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
Base.after_structure_definition do |klass|
|
131
|
+
# Pre-define the .defaults method
|
132
|
+
CodeGen.define_defaults_method_for klass
|
133
|
+
# Pre-define the .unpack method
|
134
|
+
CodeGen.define_unpack_method_for klass::Unpacker
|
135
|
+
# Pre-define the .pack method
|
136
|
+
CodeGen.define_pack_method_for klass::Packer
|
137
|
+
end
|
138
|
+
|
139
|
+
module InstanceMethods
|
140
|
+
def convert_to other
|
141
|
+
method = :"convert_to_#{other.object_id}"
|
142
|
+
::Zippo::BinaryStructure::CodeGen.define_converter_for(self.class, method, other) unless respond_to? method
|
143
|
+
send method
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Zippo
|
2
|
+
module BinaryStructure
|
3
|
+
class Structure
|
4
|
+
def self.create(owner_class, &block)
|
5
|
+
structure = new(owner_class, &block)
|
6
|
+
structure
|
7
|
+
end
|
8
|
+
def initialize(owner_class, &block)
|
9
|
+
@fields = []
|
10
|
+
@owner_class = owner_class
|
11
|
+
instance_eval &block
|
12
|
+
end
|
13
|
+
def field name, pack, options = {}
|
14
|
+
@fields << StructureMember.new(name, pack, options)
|
15
|
+
end
|
16
|
+
def dependent? field_name
|
17
|
+
fields.detect do |field|
|
18
|
+
field.options[:size] == field_name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
attr_reader :fields, :owner_class
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Zippo
|
2
|
+
module BinaryStructure
|
3
|
+
class StructureMember
|
4
|
+
def initialize(name, pack, options = {})
|
5
|
+
@name = name
|
6
|
+
@pack = pack
|
7
|
+
@options = options
|
8
|
+
@width = StructureMember.width(@pack)
|
9
|
+
end
|
10
|
+
# XXX unspec
|
11
|
+
def dependent
|
12
|
+
options[:size]
|
13
|
+
end
|
14
|
+
# XXX unspec
|
15
|
+
attr_reader :width
|
16
|
+
def self.width(pack)
|
17
|
+
case pack
|
18
|
+
when 'L' then 4
|
19
|
+
when 'S' then 2
|
20
|
+
when /^a(\d+)$/ then $1.to_i
|
21
|
+
when 'a*' then nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
attr_reader :name, :pack, :options
|
25
|
+
|
26
|
+
def signature?
|
27
|
+
options[:signature]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'zippo/binary_structure'
|
2
|
+
|
3
|
+
module Zippo
|
4
|
+
# A Zip central directory file header.
|
5
|
+
class CdFileHeader
|
6
|
+
# The central file header signature from APPNOTE.TXT
|
7
|
+
SIGNATURE = 0x02014b50
|
8
|
+
binary_structure do
|
9
|
+
# @!macro [attach] bs.field
|
10
|
+
# @!attribute [rw] $1
|
11
|
+
field :signature, 'L', :signature => SIGNATURE
|
12
|
+
field :version_made_by, 'S', :default => 0
|
13
|
+
field :version_extractable_by, 'S', :default => 20
|
14
|
+
field :bit_flags, 'S', :default => 0
|
15
|
+
field :compression_method, 'S'
|
16
|
+
field :last_modified_time, 'S', :default => 0
|
17
|
+
field :last_modified_date, 'S', :default => 0
|
18
|
+
field :crc32, 'L'
|
19
|
+
field :compressed_size, 'L'
|
20
|
+
field :uncompressed_size, 'L'
|
21
|
+
# set when name is set
|
22
|
+
field :file_name_length, 'S'
|
23
|
+
# set when extra_field is set
|
24
|
+
field :extra_field_length, 'S', :default => 0
|
25
|
+
# set when file comment is set
|
26
|
+
field :file_comment_length, 'S', :default => 0
|
27
|
+
field :disk_number, 'S', :default => 0
|
28
|
+
field :internal_file_attributes, 'S', :default => 0
|
29
|
+
field :external_file_attributes, 'L', :default => 0
|
30
|
+
field :local_file_header_offset, 'L'
|
31
|
+
field :name, 'a*', :size => :file_name_length
|
32
|
+
field :extra_field, 'a*', :size => :extra_field_length, :default => ''
|
33
|
+
field :comment, 'a*', :default => '', :size => :file_comment_length, :default => ''
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'zippo/cd_file_header'
|
2
|
+
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Zippo
|
6
|
+
# Unpacks an array of CdFileHeaders from an io stream
|
7
|
+
class CentralDirectoryEntriesUnpacker
|
8
|
+
def initialize(io, size, offset)
|
9
|
+
@io = io
|
10
|
+
@size = size
|
11
|
+
@offset = offset
|
12
|
+
@end = @offset + @size
|
13
|
+
end
|
14
|
+
def unpack
|
15
|
+
[].tap do |entries|
|
16
|
+
@io.seek @offset
|
17
|
+
while @io.pos < @end && entry = CdFileHeader::Unpacker.new(@io).unpack
|
18
|
+
entries << entry
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'zippo/end_cd_record'
|
2
|
+
require 'zippo/central_directory_entries_unpacker'
|
3
|
+
|
4
|
+
module Zippo
|
5
|
+
# Reads the zip central directory from an IO stream.
|
6
|
+
class CentralDirectoryReader
|
7
|
+
def initialize(io)
|
8
|
+
@io = io
|
9
|
+
end
|
10
|
+
|
11
|
+
def end_of_cd_record
|
12
|
+
@end_of_cd_record ||= EndCdRecord::Unpacker.new(read_from end_of_cd_record_position).unpack
|
13
|
+
end
|
14
|
+
|
15
|
+
def cd_file_headers
|
16
|
+
@cd_file_headers ||= CentralDirectoryEntriesUnpacker.new(@io, end_of_cd_record.cd_size, end_of_cd_record.cd_offset).unpack
|
17
|
+
end
|
18
|
+
|
19
|
+
def end_of_cd_record_position
|
20
|
+
# XXX implement optimised scanning at -22 position
|
21
|
+
[44, 22].each do |pos|
|
22
|
+
next if pos > @io.size
|
23
|
+
return @io.size - pos if (read 4, -pos) == EndCdRecord::PACKED_SIGNATURE
|
24
|
+
end
|
25
|
+
|
26
|
+
scan_from = @io.size - EndCdRecord::MAX_COMMENT_LENGTH
|
27
|
+
scan_from = 0 if scan_from < 0
|
28
|
+
scan_from + (read_from(scan_from).rindex(EndCdRecord::PACKED_SIGNATURE) or raise "End of Central Directory Record not found")
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
# reads size from the specified offset
|
33
|
+
# if offset is negative, will offset from EOF
|
34
|
+
def read size, offset
|
35
|
+
@io.seek offset, (offset < 0 ? IO::SEEK_END : IO::SEEK_SET)
|
36
|
+
@io.read size
|
37
|
+
end
|
38
|
+
|
39
|
+
# reads from the specified offset until EOF
|
40
|
+
def read_from offset
|
41
|
+
read nil, offset
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'zippo/binary_structure'
|
2
|
+
|
3
|
+
module Zippo
|
4
|
+
# A zip end of central directory record.
|
5
|
+
class EndCdRecord
|
6
|
+
SIGNATURE = 0x06054b50
|
7
|
+
PACKED_SIGNATURE = [SIGNATURE].pack('L')
|
8
|
+
MAX_COMMENT_LENGTH = 1<<16
|
9
|
+
binary_structure do
|
10
|
+
field :signature, 'L', :signature => SIGNATURE
|
11
|
+
field :disk, 'S', :default => 0
|
12
|
+
field :cd_disk, 'S', :default => 0
|
13
|
+
field :records, 'S'
|
14
|
+
field :total_records, 'S'
|
15
|
+
field :cd_size, 'L'
|
16
|
+
field :cd_offset, 'L'
|
17
|
+
field :comment_length, 'S', :default => 0
|
18
|
+
field :comment, 'a*', :default => "", :size => :comment_length
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|