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.
Files changed (64) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +22 -0
  7. data/README.md +83 -0
  8. data/Rakefile +11 -0
  9. data/lib/zippo/binary_structure/base.rb +119 -0
  10. data/lib/zippo/binary_structure/binary_packer.rb +17 -0
  11. data/lib/zippo/binary_structure/binary_unpacker.rb +32 -0
  12. data/lib/zippo/binary_structure/meta.rb +146 -0
  13. data/lib/zippo/binary_structure/structure.rb +24 -0
  14. data/lib/zippo/binary_structure/structure_member.rb +31 -0
  15. data/lib/zippo/binary_structure.rb +6 -0
  16. data/lib/zippo/cd_file_header.rb +36 -0
  17. data/lib/zippo/central_directory_entries_unpacker.rb +23 -0
  18. data/lib/zippo/central_directory_reader.rb +44 -0
  19. data/lib/zippo/end_cd_record.rb +21 -0
  20. data/lib/zippo/filter/base.rb +29 -0
  21. data/lib/zippo/filter/compressor/deflate.rb +23 -0
  22. data/lib/zippo/filter/compressor/store.rb +12 -0
  23. data/lib/zippo/filter/compressor.rb +42 -0
  24. data/lib/zippo/filter/compressors.rb +3 -0
  25. data/lib/zippo/filter/null_filters.rb +15 -0
  26. data/lib/zippo/filter/uncompressor/deflate.rb +25 -0
  27. data/lib/zippo/filter/uncompressor/store.rb +12 -0
  28. data/lib/zippo/filter/uncompressor.rb +59 -0
  29. data/lib/zippo/filter/uncompressors.rb +3 -0
  30. data/lib/zippo/io_zip_member.rb +24 -0
  31. data/lib/zippo/local_file_header.rb +28 -0
  32. data/lib/zippo/version.rb +3 -0
  33. data/lib/zippo/zip_directory.rb +80 -0
  34. data/lib/zippo/zip_file.rb +121 -0
  35. data/lib/zippo/zip_file_writer.rb +57 -0
  36. data/lib/zippo/zip_member.rb +85 -0
  37. data/lib/zippo.rb +18 -0
  38. data/spec/binary_structure_spec.rb +132 -0
  39. data/spec/central_directory_entries_unpacker_spec.rb +29 -0
  40. data/spec/central_directory_parser_spec.rb +50 -0
  41. data/spec/central_directory_unpacker_spec.rb +31 -0
  42. data/spec/compressor_spec.rb +14 -0
  43. data/spec/data/comment.zip +0 -0
  44. data/spec/data/deflate.zip +0 -0
  45. data/spec/data/multi.zip +0 -0
  46. data/spec/data/not_a.zip +1 -0
  47. data/spec/data/test.zip +0 -0
  48. data/spec/deflate_compressor_spec.rb +21 -0
  49. data/spec/deflate_uncompressor_spec.rb +23 -0
  50. data/spec/integration/compressors_spec.rb +21 -0
  51. data/spec/integration/zippo_spec.rb +55 -0
  52. data/spec/io_zip_member_spec.rb +32 -0
  53. data/spec/local_file_header_spec.rb +18 -0
  54. data/spec/spec_helper.rb +12 -0
  55. data/spec/store_compressor_spec.rb +19 -0
  56. data/spec/store_uncompressor_spec.rb +19 -0
  57. data/spec/uncompressor_spec.rb +14 -0
  58. data/spec/zip_directory_spec.rb +63 -0
  59. data/spec/zip_file_spec.rb +50 -0
  60. data/spec/zip_file_writer_spec.rb +42 -0
  61. data/spec/zip_member_spec.rb +42 -0
  62. data/yard_extensions.rb +10 -0
  63. data/zippo.gemspec +23 -0
  64. 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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --order random --color
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ lib/**/*.rb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in zippo.gemspec
4
+ gemspec
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,11 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'yard'
5
+ require './yard_extensions'
6
+
7
+ task :default => :spec
8
+
9
+ RSpec::Core::RakeTask.new
10
+
11
+ YARD::Rake::YardocTask.new
@@ -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,6 @@
1
+ require 'zippo/binary_structure/base'
2
+ require 'zippo/binary_structure/meta'
3
+
4
+ Class.class_eval do
5
+ include Zippo::BinaryStructure::Base
6
+ 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