doo_dah 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +2 -0
- data/README +3 -0
- data/Rakefile +66 -0
- data/VERSION +1 -0
- data/config/defaults.reek +3 -0
- data/example/multi-language.rb +36 -0
- data/lib/doo_dah/dos_time.rb +13 -0
- data/lib/doo_dah/zip_entry.rb +50 -0
- data/lib/doo_dah/zip_header.rb +128 -0
- data/lib/doo_dah/zip_output_stream.rb +76 -0
- data/lib/doo_dah.rb +4 -0
- data/spec/central_directory_spec.rb +125 -0
- data/spec/dos_time_spec.rb +50 -0
- data/spec/local_directory_header_spec.rb +74 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/byte_matcher.rb +29 -0
- data/spec/support/write_capturing_example_group.rb +22 -0
- data/spec/zip_entry_header_spec.rb +96 -0
- data/spec/zip_entry_spec.rb +65 -0
- data/spec/zip_header_spec.rb +16 -0
- data/spec/zip_output_stream_spec.rb +149 -0
- metadata +134 -0
data/.rspec
ADDED
data/README
ADDED
@@ -0,0 +1,3 @@
|
|
1
|
+
DooDah is a simple Ruby gem for creating zip files that are well suited to streaming. This is achieved by only supporting the zip STORE method (for predictable size), and by use of the optional data descriptor after each file so that output of file data can start before a whole file has been processed.
|
2
|
+
|
3
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "doo_dah"
|
8
|
+
gem.summary = 'Creates zip files suitable for streaming.'
|
9
|
+
gem.description = <<-END_DESCRIPTION
|
10
|
+
This gem creates zip files using the STORE method - i.e. with no compression. This enables the generation of
|
11
|
+
zip files with a known size from streamed data, providing the size of the input files is known.
|
12
|
+
END_DESCRIPTION
|
13
|
+
gem.email = "mcollas@yahoo.com"
|
14
|
+
gem.homepage = "http://github.com/michaelcollas/doo_dah"
|
15
|
+
gem.authors = ["Michael Collas"]
|
16
|
+
gem.add_development_dependency "rspec", ">= 2.8.0"
|
17
|
+
gem.add_development_dependency "reek", ">= 1.2.8"
|
18
|
+
gem.add_development_dependency "sexp_processor", ">= 3.0.4"
|
19
|
+
gem.files.exclude('.gitignore')
|
20
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
21
|
+
end
|
22
|
+
Jeweler::GemcutterTasks.new
|
23
|
+
rescue LoadError
|
24
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
25
|
+
end
|
26
|
+
|
27
|
+
require 'rspec/core/rake_task'
|
28
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
29
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
30
|
+
end
|
31
|
+
|
32
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
33
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
34
|
+
spec.rcov = true
|
35
|
+
#spec.rcov_opts = ['-T', '--include-file', 'lib\/doo_dah', '--exclude', 'spec\/']
|
36
|
+
spec.rcov_opts = ['-T', '--spec-only', '--include-file', 'lib\/doo_dah', '--exclude', 'spec\/,gems\/']
|
37
|
+
end
|
38
|
+
|
39
|
+
task :spec => :check_dependencies
|
40
|
+
|
41
|
+
begin
|
42
|
+
require 'reek/rake/task'
|
43
|
+
Reek::Rake::Task.new do |t|
|
44
|
+
t.fail_on_error = true
|
45
|
+
t.verbose = false
|
46
|
+
t.source_files = 'lib/**/*.rb'
|
47
|
+
t.reek_opts << ' --quiet'
|
48
|
+
t.ruby_opts << '-r' << 'rubygems'
|
49
|
+
end
|
50
|
+
rescue LoadError
|
51
|
+
task :reek do
|
52
|
+
abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
task :default => :spec
|
57
|
+
|
58
|
+
require 'rake/rdoctask'
|
59
|
+
Rake::RDocTask.new do |rdoc|
|
60
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
61
|
+
|
62
|
+
rdoc.rdoc_dir = 'rdoc'
|
63
|
+
rdoc.title = "doo_dah #{version}"
|
64
|
+
rdoc.rdoc_files.include('README*')
|
65
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
66
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'doo_dah'
|
3
|
+
|
4
|
+
z = DooDah::ZipOutputStream.new(STDOUT)
|
5
|
+
|
6
|
+
content = "the name of this file is written in simplified Chinese\n"
|
7
|
+
z.create_entry('这里插入一个聪明的说', content.size) do |e|
|
8
|
+
e.write_file_data(content)
|
9
|
+
end
|
10
|
+
|
11
|
+
content = "the name of this file is written in Japanese\n"
|
12
|
+
z.create_entry('ここに巧妙なことわざを挿入', content.size) do |e|
|
13
|
+
e.write_file_data(content)
|
14
|
+
end
|
15
|
+
|
16
|
+
content = "the name of this file is written in Hebrew\n"
|
17
|
+
z.create_entry('הכנס אומרים חכם פה', content.size) do |e|
|
18
|
+
e.write_file_data(content)
|
19
|
+
end
|
20
|
+
|
21
|
+
content = "the name of this file is written in Arabic\n"
|
22
|
+
z.create_entry('إدراج قائلا ذكي هنا', content.size) do |e|
|
23
|
+
e.write_file_data(content)
|
24
|
+
end
|
25
|
+
|
26
|
+
content = "This zip file contains five files (including this one). If the text \n"+
|
27
|
+
"encoding has worked correctly, the first file name should be in \n" +
|
28
|
+
"Chinese, the second in Japanese, the third in Hebrew, and the fourth \n" +
|
29
|
+
"in Arabic. Hebrew and Arabic are both right-to-left reading languages, \n" +
|
30
|
+
"so some zip programs may present a listing that has the whole row \n" +
|
31
|
+
"reversed.\n"
|
32
|
+
z.create_entry('readme.txt', content.size) do |e|
|
33
|
+
e.write_file_data(content)
|
34
|
+
end
|
35
|
+
|
36
|
+
z.close
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module DooDah
|
4
|
+
|
5
|
+
class ZipEntry
|
6
|
+
include LocalDirectoryHeader
|
7
|
+
include CentralDirectoryHeader
|
8
|
+
|
9
|
+
attr_reader :size, :crc, :name, :closed
|
10
|
+
alias_method :closed?, :closed
|
11
|
+
|
12
|
+
extend ::Forwardable
|
13
|
+
def_delegators :@zip_stream, :write, :current_offset
|
14
|
+
|
15
|
+
def initialize(zip_stream, name, size = 0)
|
16
|
+
@zip_stream = zip_stream
|
17
|
+
@name = name
|
18
|
+
@crc = 0
|
19
|
+
@size = size
|
20
|
+
@closed = false
|
21
|
+
write_local_header
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
return if closed
|
26
|
+
write_local_footer
|
27
|
+
@closed = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def write_file_data(data)
|
31
|
+
raise 'Zip entry already closed' if closed
|
32
|
+
@size += write(data)
|
33
|
+
@crc = Zlib::crc32(data, @crc)
|
34
|
+
end
|
35
|
+
|
36
|
+
def last_modified_time
|
37
|
+
@last_modified_time ||= Time.now.extend(DosTime::Formatter).to_dos_time
|
38
|
+
end
|
39
|
+
|
40
|
+
def last_modified_date
|
41
|
+
@last_modified_date ||= Time.now.extend(DosTime::Formatter).to_dos_date
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_accessor :local_header_offset
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module DooDah
|
2
|
+
|
3
|
+
module LittleEndianByteWriter
|
4
|
+
|
5
|
+
def write_byte(value)
|
6
|
+
write([value].pack('C'))
|
7
|
+
end
|
8
|
+
|
9
|
+
alias_method :write_u8, :write_byte
|
10
|
+
|
11
|
+
def write_word(value)
|
12
|
+
write([value].pack('v'))
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :write_u16, :write_word
|
16
|
+
|
17
|
+
def write_dword(value)
|
18
|
+
write([value].pack('V'))
|
19
|
+
end
|
20
|
+
|
21
|
+
alias_method :write_u32, :write_dword
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
module ZipHeader
|
26
|
+
def write_signature(signature)
|
27
|
+
write([signature].pack('V'))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module ZipEntryHeader
|
32
|
+
include ZipHeader
|
33
|
+
|
34
|
+
STORED = 0
|
35
|
+
DEFLATED = 8
|
36
|
+
LOCAL_ENTRY_HEADER_SIGNATURE = 0x04034b50
|
37
|
+
CENTRAL_ENTRY_HEADER_SIGNATURE = 0x02014b50
|
38
|
+
END_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
|
39
|
+
LOCAL_ENTRY_FOOTER_SIGNATURE = 0x08074b50
|
40
|
+
LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30
|
41
|
+
LOCAL_ENTRY_TRAILING_DESCRIPTOR_LENGTH = 4+4+4
|
42
|
+
VERSION_NEEDED_TO_EXTRACT = 10
|
43
|
+
GP_FLAGS_CRC_UNKNOWN = 0x0008
|
44
|
+
GP_FLAGS_UTF8 = 0x0800
|
45
|
+
|
46
|
+
def write_common_header()
|
47
|
+
flags = GP_FLAGS_UTF8
|
48
|
+
flags |= GP_FLAGS_CRC_UNKNOWN if crc.zero?
|
49
|
+
write([
|
50
|
+
VERSION_NEEDED_TO_EXTRACT, # version needed to extract
|
51
|
+
flags,
|
52
|
+
STORED,
|
53
|
+
last_modified_time,
|
54
|
+
last_modified_date,
|
55
|
+
crc,
|
56
|
+
size, # compressed_size = size (stored)
|
57
|
+
size,
|
58
|
+
name ? name.length : 0,
|
59
|
+
0 # extra length
|
60
|
+
].pack('vvvvvVVVvv'))
|
61
|
+
end
|
62
|
+
|
63
|
+
def write_name
|
64
|
+
write name
|
65
|
+
end
|
66
|
+
|
67
|
+
def write_infozip_utf8_name
|
68
|
+
[0x7075, name.size + 5, 1, file-name-crc].pack('vvCV')
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
module LocalDirectoryHeader
|
74
|
+
include ZipEntryHeader
|
75
|
+
|
76
|
+
def write_local_header
|
77
|
+
self.local_header_offset = current_offset
|
78
|
+
write_signature(LOCAL_ENTRY_HEADER_SIGNATURE)
|
79
|
+
write_common_header
|
80
|
+
write_name
|
81
|
+
end
|
82
|
+
|
83
|
+
def write_local_footer
|
84
|
+
write_signature(LOCAL_ENTRY_FOOTER_SIGNATURE)
|
85
|
+
write([crc, size, size].pack('VVV'))
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
|
90
|
+
module CentralDirectoryHeader
|
91
|
+
include ZipEntryHeader
|
92
|
+
|
93
|
+
def write_central_header
|
94
|
+
write_signature(CENTRAL_ENTRY_HEADER_SIGNATURE)
|
95
|
+
write_version_made_by
|
96
|
+
write_common_header
|
97
|
+
write([
|
98
|
+
0, # file comment length
|
99
|
+
0, # start of file disk number
|
100
|
+
0, # internal attributes = binary
|
101
|
+
(010 << 12 | 0644) << 16, # external attributes = file, rw-r--r--
|
102
|
+
local_header_offset
|
103
|
+
].pack('vvvVV'))
|
104
|
+
write_name
|
105
|
+
end
|
106
|
+
|
107
|
+
def write_end_of_central_directory
|
108
|
+
central_directory_size = current_offset - central_directory_offset
|
109
|
+
write_signature(END_CENTRAL_DIRECTORY_SIGNATURE)
|
110
|
+
end_of_central_directory = [
|
111
|
+
0, # disk number
|
112
|
+
0, # disk with directory
|
113
|
+
entry_count, # entries on this disk
|
114
|
+
entry_count, # total entries
|
115
|
+
central_directory_size,
|
116
|
+
central_directory_offset,
|
117
|
+
0 # zip file comment length
|
118
|
+
].pack('vvvvVVv')
|
119
|
+
write(end_of_central_directory)
|
120
|
+
end
|
121
|
+
|
122
|
+
def write_version_made_by
|
123
|
+
write([10, 3].pack('CC')) # version, file system type
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
$KCODE = 'U'
|
2
|
+
require 'zlib'
|
3
|
+
|
4
|
+
module DooDah
|
5
|
+
|
6
|
+
class ZipOutputStream
|
7
|
+
|
8
|
+
class EntryOpen < Exception
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(output_stream)
|
12
|
+
@output_stream = output_stream
|
13
|
+
@total_bytes_written = 0
|
14
|
+
@entries = []
|
15
|
+
end
|
16
|
+
|
17
|
+
def create_entry(name, size=0)
|
18
|
+
raise EntryOpen if entry_open?
|
19
|
+
begin
|
20
|
+
yield start_entry(name, size)
|
21
|
+
ensure
|
22
|
+
end_current_entry
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# TODO: take block instead of using a start/end method pair?
|
27
|
+
def start_entry(name, size=0)
|
28
|
+
end_current_entry
|
29
|
+
new_entry = ZipEntry.new(self, name, size)
|
30
|
+
@entries << new_entry
|
31
|
+
new_entry
|
32
|
+
end
|
33
|
+
|
34
|
+
def end_current_entry
|
35
|
+
return unless current_entry
|
36
|
+
current_entry.close
|
37
|
+
end
|
38
|
+
|
39
|
+
def entry_count
|
40
|
+
@entries.size
|
41
|
+
end
|
42
|
+
|
43
|
+
def close
|
44
|
+
end_current_entry
|
45
|
+
@central_directory_offset = current_offset
|
46
|
+
@entries.each { |entry| entry.write_central_header }
|
47
|
+
write_end_of_central_directory
|
48
|
+
end
|
49
|
+
|
50
|
+
def current_offset
|
51
|
+
@total_bytes_written
|
52
|
+
end
|
53
|
+
|
54
|
+
def write(data)
|
55
|
+
bytes_written = @output_stream.write(data)
|
56
|
+
@total_bytes_written += bytes_written
|
57
|
+
bytes_written
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :central_directory_offset
|
63
|
+
include CentralDirectoryHeader
|
64
|
+
|
65
|
+
def current_entry
|
66
|
+
@entries.last
|
67
|
+
end
|
68
|
+
|
69
|
+
def entry_open?
|
70
|
+
current_entry && !current_entry.closed?
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
data/lib/doo_dah.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DooDah
|
4
|
+
|
5
|
+
describe CentralDirectoryHeader, :type => :write_capturing do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@entry = stub().extend(CentralDirectoryHeader)
|
9
|
+
record_writes @entry
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#write_central_header' do
|
13
|
+
|
14
|
+
before do
|
15
|
+
@entry.stub(:local_header_offset => 0, :write_signature => nil, :write_common_header => nil, :write_name => nil)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should write the central directory signature bytes' do
|
19
|
+
@entry.should_receive(:write_signature).with(0x02014b50)
|
20
|
+
@entry.write_central_header
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should write the zip specification version in version made by as 1.0 at first byte after signature' do
|
24
|
+
@entry.write_central_header
|
25
|
+
written[0,1].should have_bytes(10)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should write the attribute compatability as type 3 (unix) in version made by at the second byte after the signature' do
|
29
|
+
@entry.write_central_header
|
30
|
+
written[1,1].should have_bytes(3)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should write the common header' do
|
34
|
+
@entry.should_receive(:write_common_header)
|
35
|
+
@entry.write_central_header
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should write 2 bytes of 0 as file comment length in first bytes after common header' do
|
39
|
+
@entry.write_central_header
|
40
|
+
written[2,2].should have_bytes(0, 0)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should write 2 bytes of 0 as disk number for start of file 2 bytes after common header' do
|
44
|
+
@entry.write_central_header
|
45
|
+
written[4,2].should have_bytes(0, 0)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should write 2 bytes of 0 as internal attributes 4 bytes after common header' do
|
49
|
+
@entry.write_central_header
|
50
|
+
written[6,2].should have_bytes(0, 0)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should write 4 bytes of attributes for unix file rw-r--r-- 6 bytes after common header' do
|
54
|
+
@entry.write_central_header
|
55
|
+
written[8, 4].should have_bytes(0x00, 0x00, 0xa4, 0x81)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should write 4 bytes of offset to the local header record 10 bytes after the common header' do
|
59
|
+
@entry.stub(:local_header_offset => 0x98765432)
|
60
|
+
@entry.write_central_header
|
61
|
+
written[12, 4].should have_bytes(0x32, 0x54, 0x76, 0x98)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should write the file name' do
|
65
|
+
@entry.should_receive(:write_name)
|
66
|
+
@entry.write_central_header
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#write_end_of_central_directory' do
|
72
|
+
|
73
|
+
before do
|
74
|
+
@entry.stub(:central_directory_offset => 0, :current_offset => 0, :write_signature => nil, :entry_count => 0)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should write the end of central directory signature' do
|
78
|
+
@entry.should_receive(:write_signature).with(0x06054b50)
|
79
|
+
@entry.write_end_of_central_directory
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'should write the disk number as 0 in the 2 bytes following the signature' do
|
83
|
+
@entry.write_end_of_central_directory
|
84
|
+
written[0, 2].should have_bytes(0, 0)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'should write 2 bytes of zero as the directory disk number 2 bytes after the signature' do
|
88
|
+
@entry.write_end_of_central_directory
|
89
|
+
written[2, 2].should have_bytes(0, 0)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should write 2 bytes of entry count on this disk 4 bytes after the signature' do
|
93
|
+
@entry.stub(:entry_count => 0x0123)
|
94
|
+
@entry.write_end_of_central_directory
|
95
|
+
written[4, 2].should have_bytes(0x23, 0x01)
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should write 2 bytes of entry count on this disk 6 bytes after the signature' do
|
99
|
+
@entry.stub(:entry_count => 0x0234)
|
100
|
+
@entry.write_end_of_central_directory
|
101
|
+
written[6, 2].should have_bytes(0x34, 0x02)
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should write 4 bytes of central directory size 8 bytes after the signature' do
|
105
|
+
@entry.stub(:current_offset => 0x98765432, :central_directory_offset => 0x90000000)
|
106
|
+
@entry.write_end_of_central_directory
|
107
|
+
written[8, 4].should have_bytes(0x32, 0x54, 0x76, 0x08)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'should write 4 bytes of central directory offset 12 bytes after the signature' do
|
111
|
+
@entry.stub(:central_directory_offset => 0x13243546)
|
112
|
+
@entry.write_end_of_central_directory
|
113
|
+
written[12, 4].should have_bytes(0x46, 0x35, 0x24, 0x13)
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should write 2 bytes of zero as the zip file comment length 16 bytes after the signature' do
|
117
|
+
@entry.write_end_of_central_directory
|
118
|
+
written[16, 2].should have_bytes(0, 0)
|
119
|
+
end
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DosTime::Formatter do
|
4
|
+
|
5
|
+
def dos_time(time)
|
6
|
+
time.extend(DosTime::Formatter)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe '#to_dos_date' do
|
10
|
+
|
11
|
+
before do
|
12
|
+
@sample_dos_date = dos_time(Time.local(2011, 11, 14)).to_dos_date
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should have the day number in the lowest 5 bits' do
|
16
|
+
(@sample_dos_date & 0x1f).should == 14
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should have the month number in the four bits starting at bit 5' do
|
20
|
+
(@sample_dos_date >> 5 & 0x0f).should == 11
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should the have the year as an offset from 1980 in the 7 bits starting at bit 9' do
|
24
|
+
(@sample_dos_date >> 9 & 0x7f).should == 2011 - 1980
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#to_dos_time' do
|
30
|
+
|
31
|
+
before do
|
32
|
+
@sample_dos_time = dos_time(Time.local(2011, 11, 14, 18, 58, 37)).to_dos_time
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should have half the second number in the lowest 5 bits' do
|
36
|
+
(@sample_dos_time & 0x1f).should == 37.div(2)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should have the minute number in the six bits starting at bit 5' do
|
40
|
+
(@sample_dos_time >> 5 & 0x3f).should == 58
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should the have the hour number in the 5 bits starting at bit 11' do
|
44
|
+
(@sample_dos_time >> 11 & 0x1f).should == 18
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DooDah
|
4
|
+
|
5
|
+
describe LocalDirectoryHeader, :type => :write_capturing do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@entry = Object.new.extend(LocalDirectoryHeader)
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#write_local_header' do
|
12
|
+
|
13
|
+
before do
|
14
|
+
@entry.stub(:current_offset => 0, :local_header_offset= => nil, :write_signature => nil, :write_common_header => nil, :write_name => nil)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should write a local directory entry signature' do
|
18
|
+
@entry.should_receive(:write_signature).with(0x04034b50)
|
19
|
+
@entry.write_local_header
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should write a common header' do
|
23
|
+
@entry.should_receive(:write_common_header)
|
24
|
+
@entry.write_local_header
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should write the file name' do
|
28
|
+
@entry.should_receive(:write_name)
|
29
|
+
@entry.write_local_header
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should capture the current output stream position' do
|
33
|
+
@entry.stub(:current_offset => 1234)
|
34
|
+
@entry.should_receive(:local_header_offset=).with(1234)
|
35
|
+
@entry.write_local_header
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
describe '#write_local_footer' do
|
41
|
+
|
42
|
+
before do
|
43
|
+
@entry.stub(:write_signature => nil, :crc => 0, :size => 0)
|
44
|
+
record_writes(@entry)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should write a local directory entry footer' do
|
48
|
+
@entry.should_receive(:write_signature).with(0x08074b50)
|
49
|
+
@entry.write_local_footer
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should write the crc value in the four bytes after the signature' do
|
53
|
+
@entry.stub(:crc => 0x1234)
|
54
|
+
@entry.write_local_footer
|
55
|
+
written[0, 4].should have_bytes(0x34, 0x12)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should write the uncompressed file size in the four bytes starting at byte 4 after the signature' do
|
59
|
+
@entry.stub(:size => 0x5678)
|
60
|
+
@entry.write_local_footer
|
61
|
+
written[4, 4].should have_bytes(0x78, 0x56)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should write the compressed file size in the four bytes starting at byte 8 after the signature' do
|
65
|
+
@entry.stub(:size => 0x1324)
|
66
|
+
@entry.write_local_footer
|
67
|
+
written[8, 4].should have_bytes(0x24, 0x13)
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'rubygems'
|
4
|
+
require 'doo_dah'
|
5
|
+
require 'rspec'
|
6
|
+
require 'rspec/autorun'
|
7
|
+
require 'spec/support/byte_matcher'
|
8
|
+
require 'spec/support/write_capturing_example_group'
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
RSpec::Matchers.define :bytes do |*expected|
|
2
|
+
|
3
|
+
match do |actual|
|
4
|
+
@start_byte ||= 0
|
5
|
+
actual.bytes.to_a[@start_byte, expected.length] == expected
|
6
|
+
end
|
7
|
+
|
8
|
+
chain :at do |start_byte|
|
9
|
+
@start_byte = start_byte
|
10
|
+
end
|
11
|
+
|
12
|
+
failure_message_for_should do |actual|
|
13
|
+
actual_bytes = actual.bytes.to_a.collect {|b| "0x%02x" % b }.join(',')
|
14
|
+
"expected #{expected_bytes_description} to be #{actual_bytes}"
|
15
|
+
end
|
16
|
+
|
17
|
+
description do
|
18
|
+
"#{expected_bytes_description} starting at byte #{@start_byte}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def expected_bytes_description
|
22
|
+
expected.collect {|b| "0x%02x" % b }.join(',')
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
module RSpec::Matchers
|
28
|
+
alias_method(:have_bytes, :bytes)
|
29
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module WriteCapturing
|
2
|
+
|
3
|
+
attr_reader :writer
|
4
|
+
attr_reader :written
|
5
|
+
|
6
|
+
def self.included(target)
|
7
|
+
target.before do
|
8
|
+
@written = ''
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def record_writes(new_writer)
|
13
|
+
@writer = new_writer
|
14
|
+
writer.stub(:write) { |new_value| @written << new_value; new_value.size }
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
RSpec.configure do |configuration|
|
20
|
+
configuration.include(WriteCapturing, :type => :write_capturing)
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DooDah
|
4
|
+
|
5
|
+
describe ZipEntryHeader, :type => :write_capturing do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@header = stub(:crc => 0, :last_modified_time => 0, :last_modified_date => 0, :size => 0, :name => '')
|
9
|
+
@header.extend(ZipEntryHeader)
|
10
|
+
record_writes @header
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#write_common_header' do
|
14
|
+
|
15
|
+
it 'should write the version needed to extract in the first 2 bytes' do
|
16
|
+
@header.write_common_header
|
17
|
+
written[0, 2].should have_bytes(10, 0)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should write the flag bytes with the UTF8 bit set' do
|
21
|
+
@header.write_common_header
|
22
|
+
written[3, 1].should have_bytes(0x08)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should write the flag bytes with the CRC_UNKNOWN bit set if the crc is zero' do
|
26
|
+
@header.write_common_header
|
27
|
+
written[2, 1].should have_bytes(0x08)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'should write the flag bytes with the CRC_UNKNOWN bit unset if the crc is non-zero' do
|
31
|
+
@header.stub(:crc => 1234)
|
32
|
+
@header.write_common_header
|
33
|
+
written[2, 1].should have_bytes(0x00)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should write the compression method as STORED' do
|
37
|
+
@header.write_common_header
|
38
|
+
written[4, 2].should have_bytes(0,0)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should write the last modified time as a 2 byte long starting at byte 6' do
|
42
|
+
@header.stub(:last_modified_time => 0x1234)
|
43
|
+
@header.write_common_header
|
44
|
+
written[6, 2].should have_bytes(0x34, 0x12)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should write the last modified date as a 2 byte long starting at byte 8' do
|
48
|
+
@header.stub(:last_modified_date => 0x5678)
|
49
|
+
@header.write_common_header
|
50
|
+
written[8, 2].should have_bytes(0x78, 0x56)
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'should write the crc in the four bytes starting at byte 10' do
|
54
|
+
@header.stub(:crc => 0x13243546)
|
55
|
+
@header.write_common_header
|
56
|
+
written[10, 4].should have_bytes(0x46, 0x35, 0x24, 0x13)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should write the file size in the four bytes starting at byte 14' do
|
60
|
+
@header.stub(:size => 0x24354657)
|
61
|
+
@header.write_common_header
|
62
|
+
written[14, 4].should have_bytes(0x57, 0x46, 0x35, 0x24)
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'should write the file size in the four bytes starting at byte 18' do
|
66
|
+
@header.stub(:size => 0x31425364)
|
67
|
+
@header.write_common_header
|
68
|
+
written[18, 4].should have_bytes(0x64, 0x53, 0x42, 0x31)
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should write the length of the file name in the two bytes starting at byte 22' do
|
72
|
+
@header.stub(:name => 'x' * 258)
|
73
|
+
@header.write_common_header
|
74
|
+
written[22, 2].should have_bytes(0x02, 0x01)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'should write 0 as the length of the extra fields in the two bytes starting at byte 24' do
|
78
|
+
@header.write_common_header
|
79
|
+
written[24, 2].should have_bytes(0, 0)
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#write_name' do
|
85
|
+
|
86
|
+
it 'should write the file name to the output' do
|
87
|
+
@header.stub(:name => 'foo')
|
88
|
+
@header.write_name
|
89
|
+
written.should == 'foo'
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DooDah
|
4
|
+
|
5
|
+
describe ZipEntry do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@bytes_per_write = 123
|
9
|
+
@zip_stream = stub(:write => @bytes_per_write, :current_offset => 0)
|
10
|
+
@zip_entry = ZipEntry.new(@zip_stream, 'file name')
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should retain its name' do
|
14
|
+
@zip_entry.name.should == 'file name'
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should write a local entry header when created' do
|
18
|
+
zip_entry = ZipEntry.allocate
|
19
|
+
zip_entry.should_receive(:write_local_header)
|
20
|
+
zip_entry.send(:initialize, @zip_stream, 'file name')
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should write file data through to the zip stream when file data is written' do
|
24
|
+
file_contents = 'this is the content of a file added to the zip file'
|
25
|
+
@zip_stream.should_receive(:write).with(file_contents)
|
26
|
+
@zip_entry.write_file_data(file_contents)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should keep a count of total file data written for the entry' do
|
30
|
+
@zip_entry.write_file_data('')
|
31
|
+
@zip_entry.write_file_data('')
|
32
|
+
@zip_entry.size.should == 2 * @bytes_per_write
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should maintain a crc32 for all of the file data written for the entry' do
|
36
|
+
@zip_entry.write_file_data "this is part 1 of the file content\n"
|
37
|
+
@zip_entry.write_file_data "and this is part 2"
|
38
|
+
@zip_entry.crc.should == 2192856224
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'should write a local entry footer when closed' do
|
42
|
+
@zip_entry.should_receive(:write_local_footer)
|
43
|
+
@zip_entry.close
|
44
|
+
end
|
45
|
+
|
46
|
+
describe 'after being closed' do
|
47
|
+
|
48
|
+
before do
|
49
|
+
@zip_entry.close
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should not write a local footer if close is called again' do
|
53
|
+
@zip_entry.should_not_receive(:write_zip_footer)
|
54
|
+
@zip_entry.close
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should raise an error if an attempt is made to write file data' do
|
58
|
+
proc { @zip_entry.write_file_data('something irrelevant') }.should raise_error
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DooDah
|
4
|
+
|
5
|
+
describe ZipHeader, :type => :write_capturing do
|
6
|
+
|
7
|
+
it 'should write signature as a four byte little endian unsigned long' do
|
8
|
+
header = Object.new.extend(ZipHeader)
|
9
|
+
record_writes(header)
|
10
|
+
header.write_signature(0x02FD01FE)
|
11
|
+
written.should have_bytes(0xFE, 0x01, 0xFD, 0x02)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module DooDah
|
4
|
+
|
5
|
+
describe ZipOutputStream do
|
6
|
+
|
7
|
+
before do
|
8
|
+
@output_stream = stub(:write => 7)
|
9
|
+
@zip = ZipOutputStream.new(@output_stream)
|
10
|
+
@entry = stub(:close => nil, :closed? => false)
|
11
|
+
ZipEntry.stub(:new => @entry)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe '#start_entry' do
|
15
|
+
|
16
|
+
it 'should create a ZipEntry with the zip output stream as its owner' do
|
17
|
+
ZipEntry.should_receive(:new).with(@zip, 'entry name', 123)
|
18
|
+
@zip.start_entry('entry name', 123)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should return the new entry' do
|
22
|
+
@zip.start_entry('entry name', 123).should == @entry
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should ensure that the next most recently created entry is closed' do
|
26
|
+
entry1 = @zip.start_entry('first entry', 0)
|
27
|
+
entry1.should_receive(:close)
|
28
|
+
@zip.start_entry('entry2', 0)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#create_entry' do
|
34
|
+
|
35
|
+
it 'should create a new ZipEntry' do
|
36
|
+
ZipEntry.should_receive(:new).with(@zip, 'entry name', 123)
|
37
|
+
@zip.create_entry('entry name', 123) {}
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should yield the new entry to a block' do
|
41
|
+
yielded_zip_entry = nil
|
42
|
+
@zip.create_entry('entry name', 123) {|zip_entry| yielded_zip_entry = zip_entry}
|
43
|
+
yielded_zip_entry.should == @entry
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'should close the new entry on return from the block' do
|
47
|
+
@entry.should_receive(:close)
|
48
|
+
@zip.create_entry('entry name', 123) {}
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should close the new entry if an exception is raised during execution of the provided block' do
|
52
|
+
@entry.should_receive(:close)
|
53
|
+
expected_error = Class.new(Exception)
|
54
|
+
begin
|
55
|
+
@zip.create_entry('entry name', 123) { |zip_entry| raise expected_error }
|
56
|
+
rescue expected_error
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'when there is already a zip entry started but not ended' do
|
61
|
+
|
62
|
+
before do
|
63
|
+
@zip.start_entry('existing entry')
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should raise an error' do
|
67
|
+
lambda { @zip.create_entry('another entry') {} }.should raise_error(ZipOutputStream::EntryOpen)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should not create a new ZipEntry' do
|
71
|
+
ZipEntry.should_not_receive(:new)
|
72
|
+
begin
|
73
|
+
@zip.create_entry('another entry')
|
74
|
+
rescue ZipOutputStream::EntryOpen
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
describe '#end_current_entry' do
|
83
|
+
|
84
|
+
it 'should close the most recently created entry' do
|
85
|
+
entry = @zip.start_entry('name', 0)
|
86
|
+
entry.should_receive(:close)
|
87
|
+
@zip.end_current_entry
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should not fail if no entry has been created' do
|
91
|
+
lambda { @zip.end_current_entry }.should_not raise_error
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#close' do
|
97
|
+
|
98
|
+
before do
|
99
|
+
ZipEntry.stub(:new => stub(:close => nil, :write_central_header => nil))
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'should ensure that the most recently started entry is closed' do
|
103
|
+
entry = @zip.start_entry('first entry', 0)
|
104
|
+
entry.should_receive(:close)
|
105
|
+
@zip.close
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should cause each entry created by the zip output stream to write a central directory header' do
|
109
|
+
entries = [@zip.start_entry('first', 0), @zip.start_entry('second', 0), @zip.start_entry('third', 0)]
|
110
|
+
entries.each { |entry| entry.should_receive(:write_central_header) }
|
111
|
+
@zip.close
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should write an end of central directory record' do
|
115
|
+
@zip.should_receive(:write_end_of_central_directory)
|
116
|
+
@zip.close
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should record the current offset as the start of the central directory before writing central header records' do
|
120
|
+
@zip.stub(:current_offset => 789)
|
121
|
+
entry = @zip.start_entry('first entry', 0)
|
122
|
+
entry.stub(:write_central_header) do
|
123
|
+
@zip.send(:central_directory_offset).should == 789
|
124
|
+
end
|
125
|
+
@zip.close
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'should have a count of the number of entries it has created' do
|
131
|
+
@zip.start_entry('name', 0)
|
132
|
+
@zip.start_entry('name', 0)
|
133
|
+
@zip.entry_count.should == 2
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should send data unchanged to the output stream when #write() is called' do
|
137
|
+
@output_stream.should_receive(:write).with('7 bytes')
|
138
|
+
@zip.write('7 bytes')
|
139
|
+
end
|
140
|
+
|
141
|
+
it '#current_offset should provide a total count of the number of bytes written' do
|
142
|
+
@zip.write('7 bytes')
|
143
|
+
@zip.write('7 bytes')
|
144
|
+
@zip.current_offset.should == 14
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
metadata
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: doo_dah
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Michael Collas
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-06-04 00:00:00 +10:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rspec
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 47
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 8
|
33
|
+
- 0
|
34
|
+
version: 2.8.0
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: reek
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 15
|
46
|
+
segments:
|
47
|
+
- 1
|
48
|
+
- 2
|
49
|
+
- 8
|
50
|
+
version: 1.2.8
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: sexp_processor
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 15
|
62
|
+
segments:
|
63
|
+
- 3
|
64
|
+
- 0
|
65
|
+
- 4
|
66
|
+
version: 3.0.4
|
67
|
+
type: :development
|
68
|
+
version_requirements: *id003
|
69
|
+
description: " This gem creates zip files using the STORE method - i.e. with no compression. This enables the generation of\n zip files with a known size from streamed data, providing the size of the input files is known. \n"
|
70
|
+
email: mcollas@yahoo.com
|
71
|
+
executables: []
|
72
|
+
|
73
|
+
extensions: []
|
74
|
+
|
75
|
+
extra_rdoc_files:
|
76
|
+
- README
|
77
|
+
files:
|
78
|
+
- .rspec
|
79
|
+
- README
|
80
|
+
- Rakefile
|
81
|
+
- VERSION
|
82
|
+
- config/defaults.reek
|
83
|
+
- example/multi-language.rb
|
84
|
+
- lib/doo_dah.rb
|
85
|
+
- lib/doo_dah/dos_time.rb
|
86
|
+
- lib/doo_dah/zip_entry.rb
|
87
|
+
- lib/doo_dah/zip_header.rb
|
88
|
+
- lib/doo_dah/zip_output_stream.rb
|
89
|
+
- spec/central_directory_spec.rb
|
90
|
+
- spec/dos_time_spec.rb
|
91
|
+
- spec/local_directory_header_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
- spec/support/byte_matcher.rb
|
94
|
+
- spec/support/write_capturing_example_group.rb
|
95
|
+
- spec/zip_entry_header_spec.rb
|
96
|
+
- spec/zip_entry_spec.rb
|
97
|
+
- spec/zip_header_spec.rb
|
98
|
+
- spec/zip_output_stream_spec.rb
|
99
|
+
has_rdoc: true
|
100
|
+
homepage: http://github.com/michaelcollas/doo_dah
|
101
|
+
licenses: []
|
102
|
+
|
103
|
+
post_install_message:
|
104
|
+
rdoc_options: []
|
105
|
+
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
hash: 3
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
hash: 3
|
123
|
+
segments:
|
124
|
+
- 0
|
125
|
+
version: "0"
|
126
|
+
requirements: []
|
127
|
+
|
128
|
+
rubyforge_project:
|
129
|
+
rubygems_version: 1.5.2
|
130
|
+
signing_key:
|
131
|
+
specification_version: 3
|
132
|
+
summary: Creates zip files suitable for streaming.
|
133
|
+
test_files: []
|
134
|
+
|