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
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
|