doo_dah 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
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,3 @@
1
+ ---
2
+ Duplication:
3
+ max_calls: 2
@@ -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,13 @@
1
+ module DosTime
2
+ module Formatter
3
+
4
+ def to_dos_time
5
+ (sec >> 1) + (min << 5) + (hour << 11)
6
+ end
7
+
8
+ def to_dos_date
9
+ day + (month << 5) + ((year - 1980) << 9)
10
+ end
11
+
12
+ end
13
+ end
@@ -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,4 @@
1
+ require 'doo_dah/dos_time'
2
+ require 'doo_dah/zip_header'
3
+ require 'doo_dah/zip_entry'
4
+ require 'doo_dah/zip_output_stream'
@@ -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
@@ -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
+