multi_zip 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.travis.yml +17 -0
- data/BACKEND_CONSTANTS.md +26 -0
- data/Gemfile +19 -0
- data/Guardfile +77 -0
- data/LICENSE.txt +22 -0
- data/README.md +294 -0
- data/Rakefile +11 -0
- data/lib/multi_zip/backend/archive_zip.rb +111 -0
- data/lib/multi_zip/backend/rubyzip.rb +73 -0
- data/lib/multi_zip/backend/zipruby.rb +94 -0
- data/lib/multi_zip/errors.rb +35 -0
- data/lib/multi_zip/version.rb +3 -0
- data/lib/multi_zip.rb +195 -0
- data/multi_zip.gemspec +24 -0
- data/spec/backend_shared_example.rb +487 -0
- data/spec/fixtures/invalid.zip +1 -0
- data/spec/fixtures/test.zip +0 -0
- data/spec/lib/multi_zip/backend/archive_zip_spec.rb +6 -0
- data/spec/lib/multi_zip/backend/rubyzip_spec.rb +8 -0
- data/spec/lib/multi_zip/backend/zipruby_spec.rb +8 -0
- data/spec/lib/multi_zip_spec.rb +53 -0
- data/spec/spec_helper.rb +142 -0
- metadata +120 -0
@@ -0,0 +1,487 @@
|
|
1
|
+
shared_examples 'zip backend' do |backend_name|
|
2
|
+
let(:filename) { archive_fixture_filename }
|
3
|
+
subject { MultiZip.new(filename, :backend => backend_name) }
|
4
|
+
|
5
|
+
before do
|
6
|
+
apply_constants(backend_name)
|
7
|
+
# subject.backend = backend_name
|
8
|
+
end
|
9
|
+
after { stash_constants(backend_name) }
|
10
|
+
|
11
|
+
describe '#read_member' do
|
12
|
+
context "backend: #{backend_name}" do
|
13
|
+
context 'member found' do
|
14
|
+
archive_member_files.each do |member_file|
|
15
|
+
it "returns '#{member_file}' as a string" do
|
16
|
+
expect(
|
17
|
+
subject.read_member(member_file).bytesize
|
18
|
+
).to eq(
|
19
|
+
archive_member_size(member_file)
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'member not found' do
|
26
|
+
it_behaves_like 'raises MemberNotFoundError', :read_member, 'doesnt_exist'
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'member is a directory' do
|
30
|
+
it_behaves_like 'raises MemberNotFoundError', :read_member, archive_member_directories.first
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'archive not found' do
|
34
|
+
it_behaves_like 'raises ArchiveNotFoundError', :read_member, archive_member_files.first
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'archive is not a file' do
|
38
|
+
it 'raises ArchiveNotFoundError'
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'archive cannot be accessed due to permissions' do
|
42
|
+
it 'raises ArchiveNotAccessibleError'
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'invalid or unreadable archive' do
|
46
|
+
it_behaves_like 'raises InvalidArchiveError', :read_member, archive_member_files.first
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#read_members' do
|
52
|
+
context "backend: #{backend_name}" do
|
53
|
+
context 'all members found' do
|
54
|
+
it 'returns the member content as an array a string in order of args' do
|
55
|
+
extracted_files = subject.read_members(archive_member_files)
|
56
|
+
|
57
|
+
archive_member_files.each_with_index do |member, i|
|
58
|
+
expect(extracted_files[i].bytesize).to eq(archive_member_size(member))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'one of the members is a directory' do
|
64
|
+
it_behaves_like 'raises MemberNotFoundError', :read_members,
|
65
|
+
[ archive_member_files.first, archive_member_directories.first ]
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'one of the members is not found' do
|
69
|
+
it_behaves_like 'raises MemberNotFoundError', :read_members,
|
70
|
+
[ archive_member_files.first, 'doesnt_exist' ]
|
71
|
+
end
|
72
|
+
|
73
|
+
context 'archive not found' do
|
74
|
+
it_behaves_like 'raises ArchiveNotFoundError', :read_members, archive_member_files
|
75
|
+
end
|
76
|
+
|
77
|
+
context 'archive is not a file' do
|
78
|
+
it 'raises ArchiveNotFoundError'
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'archive cannot be accessed due to permissions' do
|
82
|
+
it 'raises ArchiveNotAccessibleError'
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'invalid or unreadable archive' do
|
86
|
+
it_behaves_like 'raises InvalidArchiveError', :read_members,
|
87
|
+
[ archive_member_files.first, archive_member_directories.first ]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#extract_member' do
|
93
|
+
context "backend: #{backend_name}" do
|
94
|
+
let(:tempfile) { Tempfile.new('multi_zip_test') }
|
95
|
+
|
96
|
+
context 'member found' do
|
97
|
+
let!(:extraction_return) { subject.extract_member(archive_member_files.first, tempfile.path) }
|
98
|
+
after { tempfile.delete }
|
99
|
+
it 'writes the file to the local filesystem' do
|
100
|
+
expect(tempfile.size).to eq(archive_member_size(archive_member_files.first))
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'returns the file system path written to' do
|
104
|
+
expect(extraction_return).to eq(tempfile.path)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'member is a directory' do
|
109
|
+
it 'raises MemberNotFoundError' do
|
110
|
+
expect(
|
111
|
+
lambda { subject.extract_member(archive_member_directories.first, tempfile.path) }
|
112
|
+
).to raise_error(MultiZip::MemberNotFoundError)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context 'member not found' do
|
117
|
+
it 'raises MemberNotFoundError' do
|
118
|
+
expect(
|
119
|
+
lambda { subject.extract_member('doesnt_exist', tempfile.path) }
|
120
|
+
).to raise_error(MultiZip::MemberNotFoundError)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'archive not found' do
|
125
|
+
it_behaves_like 'raises ArchiveNotFoundError', :extract_member, archive_member_files.first, 'destination'
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'archive is not a file' do
|
129
|
+
it 'raises ArchiveNotFoundError'
|
130
|
+
end
|
131
|
+
|
132
|
+
context 'archive cannot be accessed due to permissions' do
|
133
|
+
it 'raises ArchiveNotAccessibleError'
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'invalid or unreadable archive' do
|
137
|
+
let(:filename) { invalid_archive_fixture_filename }
|
138
|
+
it 'raises ArchiveInvalidError' do
|
139
|
+
expect(
|
140
|
+
lambda { subject.extract_member(archive_member_files.first, tempfile.path) }
|
141
|
+
).to raise_error(MultiZip::InvalidArchiveError)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe '#list_members' do
|
148
|
+
context "backend: #{backend_name}" do
|
149
|
+
context 'file contains members' do
|
150
|
+
it 'returns array member file names' do
|
151
|
+
expect(subject.list_members).to eq(archive_member_names)
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'prefix provided' do
|
155
|
+
context 'files with that prefix exist' do
|
156
|
+
it 'returns only files with that prefix' do
|
157
|
+
expect(subject.list_members('dir_1/')).to eq(
|
158
|
+
[ 'dir_1/', 'dir_1/file_3.txt' ]
|
159
|
+
)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'no files with that prefix exist' do
|
164
|
+
it 'returns empty array' do
|
165
|
+
expect(subject.list_members('doesnt_exist/')).to eq( [ ] )
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
context 'contains no members, is empty archive' do
|
172
|
+
it 'returns empty array'
|
173
|
+
end
|
174
|
+
|
175
|
+
context 'archive not found' do
|
176
|
+
it_behaves_like 'raises ArchiveNotFoundError', :list_members
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'archive is not a file' do
|
180
|
+
it 'raises ArchiveNotFoundError'
|
181
|
+
end
|
182
|
+
|
183
|
+
context 'archive cannot be accessed due to permissions' do
|
184
|
+
it 'raises ArchiveNotAccessibleError'
|
185
|
+
end
|
186
|
+
|
187
|
+
context 'invalid or unreadable archive' do
|
188
|
+
it_behaves_like 'raises InvalidArchiveError', :list_members
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe '#member_exists?' do
|
194
|
+
context "backend: #{backend_name}" do
|
195
|
+
context 'member is a file' do
|
196
|
+
it 'returns true if member exists' do
|
197
|
+
expect(subject.member_exists?(archive_member_files.first)).to be_truthy
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context 'member is a directory' do
|
202
|
+
it 'returns true if member exists' do
|
203
|
+
expect(subject.member_exists?(archive_member_directories.first)).to be_truthy
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'returns false if member does not exist' do
|
208
|
+
expect(subject.member_exists?('does_not_exist')).to be_falsey
|
209
|
+
end
|
210
|
+
|
211
|
+
context 'archive not found' do
|
212
|
+
it_behaves_like 'raises ArchiveNotFoundError', :member_exists?, archive_member_files.first
|
213
|
+
end
|
214
|
+
|
215
|
+
context 'archive is not a file' do
|
216
|
+
it 'raises ArchiveNotFoundError'
|
217
|
+
end
|
218
|
+
|
219
|
+
context 'archive cannot be accessed due to permissions' do
|
220
|
+
it 'raises ArchiveNotAccessibleError'
|
221
|
+
end
|
222
|
+
|
223
|
+
context 'invalid or unreadable archive' do
|
224
|
+
it_behaves_like 'raises InvalidArchiveError', :member_exists?, archive_member_files.first
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
describe '#write_member' do
|
230
|
+
context "backend: #{backend_name}" do
|
231
|
+
after { FileUtils.rm(filename) if File.exists?(filename) }
|
232
|
+
|
233
|
+
let(:filename) { "/tmp/multizip_test.zip" }
|
234
|
+
let(:member_file_name) { 'test_member_file' }
|
235
|
+
let(:member_file_contents) { 'file contents here' }
|
236
|
+
|
237
|
+
context 'archive did not exist' do
|
238
|
+
before { expect(File.exists?(filename)).to be_falsey }
|
239
|
+
|
240
|
+
let!(:result) do
|
241
|
+
subject.write_member(member_file_name, member_file_contents)
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'archive is created' do
|
245
|
+
expect(File.exists?(filename)).to be_truthy
|
246
|
+
end
|
247
|
+
|
248
|
+
context 'member added successfully' do
|
249
|
+
it 'returns true' do
|
250
|
+
expect(result).to be_truthy
|
251
|
+
end
|
252
|
+
it 'adds the member to the file' do
|
253
|
+
expect(
|
254
|
+
subject.read_member(member_file_name)
|
255
|
+
).to eq(
|
256
|
+
member_file_contents
|
257
|
+
)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context 'member not successfully added' do
|
262
|
+
it 'raises MemberNotAddedError'
|
263
|
+
it 'does not add member to the archive/archive is empty'
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
context 'archive already exists' do
|
268
|
+
before do
|
269
|
+
FileUtils.cp(archive_fixture_filename, filename)
|
270
|
+
expect(File.exists?(filename)).to be_truthy
|
271
|
+
end
|
272
|
+
|
273
|
+
let!(:preexisting_members) { subject.list_members }
|
274
|
+
|
275
|
+
let!(:result) do
|
276
|
+
subject.write_member(member_file_name, member_file_contents)
|
277
|
+
end
|
278
|
+
|
279
|
+
context 'member added successfully' do
|
280
|
+
it 'returns true' do
|
281
|
+
expect(result).to be_truthy
|
282
|
+
end
|
283
|
+
|
284
|
+
context 'member with that name already exists' do
|
285
|
+
let(:member_file_name) { 'mimetype' }
|
286
|
+
it 'returns true' do
|
287
|
+
expect(result).to be_truthy
|
288
|
+
end
|
289
|
+
it 'it overwrites the member file name with new data' do
|
290
|
+
expect(
|
291
|
+
subject.read_member(member_file_name)
|
292
|
+
).to eq(
|
293
|
+
member_file_contents
|
294
|
+
)
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'adds the member to the file' do
|
299
|
+
expect(
|
300
|
+
subject.read_member(member_file_name)
|
301
|
+
).to eq(
|
302
|
+
member_file_contents
|
303
|
+
)
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'does not remove preexisting members' do
|
307
|
+
expect(
|
308
|
+
subject.list_members - preexisting_members
|
309
|
+
).to eq(
|
310
|
+
[ member_file_name ]
|
311
|
+
)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context 'member not successfully added' do
|
316
|
+
it 'raises MemberNotAddedError'
|
317
|
+
it 'does not add member to the archive'
|
318
|
+
it 'does not remove preexisting members from the archive'
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
context 'archive is not a file' do
|
323
|
+
it 'raises ArchiveNotFoundError'
|
324
|
+
end
|
325
|
+
|
326
|
+
context 'archive cannot be accessed due to permissions' do
|
327
|
+
it 'raises ArchiveNotAccessibleError'
|
328
|
+
end
|
329
|
+
|
330
|
+
context 'invalid or unreadable archive' do
|
331
|
+
it 'raises ArchiveInvalidError'
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
describe '#remove_member' do
|
337
|
+
context "backend: #{backend_name}" do
|
338
|
+
subject { MultiZip.new(temp_filename, :backend => backend_name) }
|
339
|
+
|
340
|
+
let(:temp_filename) { "/tmp/multizip_test.zip" }
|
341
|
+
let(:member_file_name) { archive_member_files.first }
|
342
|
+
|
343
|
+
after do
|
344
|
+
FileUtils.rm(temp_filename) if File.exists?(temp_filename)
|
345
|
+
end
|
346
|
+
|
347
|
+
context 'archive exists' do
|
348
|
+
before do
|
349
|
+
FileUtils.cp(filename, temp_filename)
|
350
|
+
expect(
|
351
|
+
MultiZip.new(temp_filename).member_exists?(member_file_name)
|
352
|
+
).to be_truthy
|
353
|
+
end
|
354
|
+
|
355
|
+
let!(:result) do
|
356
|
+
subject.remove_member(member_file_name)
|
357
|
+
end
|
358
|
+
|
359
|
+
context 'member removed successfully' do
|
360
|
+
it 'returns true' do
|
361
|
+
expect(result).to be_truthy
|
362
|
+
end
|
363
|
+
it 'removes the member from the file' do
|
364
|
+
expect(
|
365
|
+
MultiZip.new(temp_filename).member_exists?(member_file_name)
|
366
|
+
).to be_falsey
|
367
|
+
end
|
368
|
+
it 'does not remove any other members' do
|
369
|
+
zip = MultiZip.new(temp_filename)
|
370
|
+
(archive_member_files - [member_file_name]).each do |mfn|
|
371
|
+
expect(zip.member_exists?(mfn)).to be_truthy
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
context 'member not successfully added' do
|
377
|
+
it 'raises MemberNotRemovedError'
|
378
|
+
it 'does not remove member from the archive'
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
context 'archive not found' do
|
383
|
+
it_behaves_like 'raises ArchiveNotFoundError', :remove_member, archive_member_files.first
|
384
|
+
end
|
385
|
+
|
386
|
+
context 'archive is not a file' do
|
387
|
+
it 'raises ArchiveNotFoundError'
|
388
|
+
end
|
389
|
+
|
390
|
+
context 'archive cannot be accessed due to permissions' do
|
391
|
+
it 'raises ArchiveNotAccessibleError'
|
392
|
+
end
|
393
|
+
|
394
|
+
context 'invalid or unreadable archive' do
|
395
|
+
it 'raises ArchiveInvalidError'
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
|
400
|
+
describe '#remove_members' do
|
401
|
+
context "backend: #{backend_name}" do
|
402
|
+
subject { MultiZip.new(temp_filename, :backend => backend_name) }
|
403
|
+
|
404
|
+
let(:temp_filename) { "/tmp/multizip_test.zip" }
|
405
|
+
let(:member_file_names) { archive_member_files[0..1] }
|
406
|
+
|
407
|
+
after do
|
408
|
+
FileUtils.rm(temp_filename) if File.exists?(temp_filename)
|
409
|
+
end
|
410
|
+
|
411
|
+
context 'archive exists' do
|
412
|
+
before do
|
413
|
+
FileUtils.cp(filename, temp_filename)
|
414
|
+
member_file_names.each do |mfn|
|
415
|
+
expect(
|
416
|
+
MultiZip.new(temp_filename).member_exists?(mfn)
|
417
|
+
).to be_truthy
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
let!(:result) do
|
422
|
+
subject.remove_members(member_file_names)
|
423
|
+
end
|
424
|
+
|
425
|
+
context 'members removed successfully' do
|
426
|
+
it 'returns true' do
|
427
|
+
expect(result).to be_truthy
|
428
|
+
end
|
429
|
+
it 'removes the members from the file' do
|
430
|
+
member_file_names.each do |mfn|
|
431
|
+
expect(
|
432
|
+
MultiZip.new(temp_filename).member_exists?(mfn)
|
433
|
+
).to be_falsey
|
434
|
+
end
|
435
|
+
end
|
436
|
+
it 'does not remove any other members' do
|
437
|
+
zip = MultiZip.new(temp_filename)
|
438
|
+
(archive_member_files - member_file_names).each do |mfn|
|
439
|
+
expect(zip.member_exists?(mfn)).to be_truthy
|
440
|
+
end
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
context 'member not successfully added' do
|
445
|
+
it 'raises MemberNotRemovedError'
|
446
|
+
it 'does not remove member from the archive'
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
context 'archive not found' do
|
451
|
+
it_behaves_like 'raises ArchiveNotFoundError', :remove_members, archive_member_files
|
452
|
+
end
|
453
|
+
|
454
|
+
context 'archive is not a file' do
|
455
|
+
it 'raises ArchiveNotFoundError'
|
456
|
+
end
|
457
|
+
|
458
|
+
context 'archive cannot be accessed due to permissions' do
|
459
|
+
it 'raises ArchiveNotAccessibleError'
|
460
|
+
end
|
461
|
+
|
462
|
+
context 'invalid or unreadable archive' do
|
463
|
+
it 'raises ArchiveInvalidError'
|
464
|
+
end
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
shared_examples 'raises MemberNotFoundError' do |*args|
|
470
|
+
it 'raises MemberNotFoundError' do
|
471
|
+
expect(lambda{ subject.send(args.shift, *args) }).to raise_error(MultiZip::MemberNotFoundError)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
475
|
+
shared_examples 'raises InvalidArchiveError' do |*args|
|
476
|
+
let(:filename) { invalid_archive_fixture_filename }
|
477
|
+
it 'raises InvalidArchiveError' do
|
478
|
+
expect(lambda{ subject.send(args.shift, *args) }).to raise_error(MultiZip::InvalidArchiveError)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
shared_examples 'raises ArchiveNotFoundError' do |*args|
|
483
|
+
let(:filename) { 'doesnt_exist' }
|
484
|
+
it 'raises ArchiveNotFoundError' do
|
485
|
+
expect(lambda{ subject.send(args.shift, *args) }).to raise_error(MultiZip::ArchiveNotFoundError)
|
486
|
+
end
|
487
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
This is not a valid zip archive.
|
Binary file
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe MultiZip do
|
4
|
+
let(:filename) { archive_fixture_filename }
|
5
|
+
let(:subject) { MultiZip.new(filename) }
|
6
|
+
|
7
|
+
describe '.new' do
|
8
|
+
it 'accepts a block'
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#backend=' do
|
12
|
+
context 'supported backends' do
|
13
|
+
before do
|
14
|
+
# so we don't get NoSupportedBackendError in subject.
|
15
|
+
expect_any_instance_of(MultiZip).to receive(:default_backend)
|
16
|
+
end
|
17
|
+
|
18
|
+
backends_to_test.each do |backend_name|
|
19
|
+
it "sets backend to #{backend_name}" do
|
20
|
+
subject.backend = backend_name
|
21
|
+
expect(subject.backend).to eq(backend_name.to_sym)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'unknown backend' do
|
27
|
+
it 'raises exception' do
|
28
|
+
expect(
|
29
|
+
lambda { subject.backend = 'unsupported' }
|
30
|
+
).to raise_exception(MultiZip::NoSupportedBackendError)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#backend" do
|
36
|
+
context "no backend specified" do
|
37
|
+
backends_to_test.each do |gem_name|
|
38
|
+
context "#{gem_name} gem has been required" do
|
39
|
+
before do
|
40
|
+
apply_constants(gem_name)
|
41
|
+
end
|
42
|
+
|
43
|
+
after { stash_constants(gem_name) }
|
44
|
+
|
45
|
+
it "is :#{gem_name}" do
|
46
|
+
expect(subject.backend).to eq(gem_name.to_sym)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|