superp-rubyzip 0.1.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.
- checksums.yaml +15 -0
- data/NEWS +176 -0
- data/README.md +175 -0
- data/Rakefile +13 -0
- data/TODO +16 -0
- data/lib/zip.rb +52 -0
- data/lib/zip/central_directory.rb +135 -0
- data/lib/zip/compressor.rb +10 -0
- data/lib/zip/constants.rb +61 -0
- data/lib/zip/decompressor.rb +13 -0
- data/lib/zip/deflater.rb +29 -0
- data/lib/zip/dos_time.rb +49 -0
- data/lib/zip/entry.rb +609 -0
- data/lib/zip/entry_set.rb +86 -0
- data/lib/zip/errors.rb +8 -0
- data/lib/zip/extra_field.rb +90 -0
- data/lib/zip/extra_field/generic.rb +43 -0
- data/lib/zip/extra_field/universal_time.rb +47 -0
- data/lib/zip/extra_field/unix.rb +39 -0
- data/lib/zip/file.rb +419 -0
- data/lib/zip/filesystem.rb +622 -0
- data/lib/zip/inflater.rb +65 -0
- data/lib/zip/input_stream.rb +145 -0
- data/lib/zip/ioextras.rb +186 -0
- data/lib/zip/null_compressor.rb +15 -0
- data/lib/zip/null_decompressor.rb +27 -0
- data/lib/zip/null_input_stream.rb +9 -0
- data/lib/zip/output_stream.rb +175 -0
- data/lib/zip/pass_thru_compressor.rb +23 -0
- data/lib/zip/pass_thru_decompressor.rb +41 -0
- data/lib/zip/streamable_directory.rb +15 -0
- data/lib/zip/streamable_stream.rb +47 -0
- data/lib/zip/version.rb +3 -0
- data/samples/example.rb +91 -0
- data/samples/example_filesystem.rb +33 -0
- data/samples/example_recursive.rb +49 -0
- data/samples/gtkRubyzip.rb +86 -0
- data/samples/qtzip.rb +101 -0
- data/samples/write_simple.rb +13 -0
- data/samples/zipfind.rb +74 -0
- metadata +82 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
module Zip
|
2
|
+
# ZipOutputStream is the basic class for writing zip files. It is
|
3
|
+
# possible to create a ZipOutputStream object directly, passing
|
4
|
+
# the zip file name to the constructor, but more often than not
|
5
|
+
# the ZipOutputStream will be obtained from a ZipFile (perhaps using the
|
6
|
+
# ZipFileSystem interface) object for a particular entry in the zip
|
7
|
+
# archive.
|
8
|
+
#
|
9
|
+
# A ZipOutputStream inherits IOExtras::AbstractOutputStream in order
|
10
|
+
# to provide an IO-like interface for writing to a single zip
|
11
|
+
# entry. Beyond methods for mimicking an IO-object it contains
|
12
|
+
# the method put_next_entry that closes the current entry
|
13
|
+
# and creates a new.
|
14
|
+
#
|
15
|
+
# Please refer to ZipInputStream for example code.
|
16
|
+
#
|
17
|
+
# java.util.zip.ZipOutputStream is the original inspiration for this
|
18
|
+
# class.
|
19
|
+
|
20
|
+
class OutputStream
|
21
|
+
include ::Zip::IOExtras::AbstractOutputStream
|
22
|
+
|
23
|
+
attr_accessor :comment
|
24
|
+
|
25
|
+
# Opens the indicated zip file. If a file with that name already
|
26
|
+
# exists it will be overwritten.
|
27
|
+
def initialize(fileName, stream=false)
|
28
|
+
super()
|
29
|
+
@fileName = fileName
|
30
|
+
if stream
|
31
|
+
@output_stream = ::StringIO.new
|
32
|
+
else
|
33
|
+
@output_stream = ::File.new(@fileName, "wb")
|
34
|
+
end
|
35
|
+
@entry_set = ::Zip::EntrySet.new
|
36
|
+
@compressor = ::Zip::NullCompressor.instance
|
37
|
+
@closed = false
|
38
|
+
@currentEntry = nil
|
39
|
+
@comment = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# Same as #initialize but if a block is passed the opened
|
43
|
+
# stream is passed to the block and closed when the block
|
44
|
+
# returns.
|
45
|
+
class << self
|
46
|
+
def open(fileName)
|
47
|
+
return new(fileName) unless block_given?
|
48
|
+
zos = new(fileName)
|
49
|
+
yield zos
|
50
|
+
ensure
|
51
|
+
zos.close if zos
|
52
|
+
end
|
53
|
+
|
54
|
+
# Same as #open but writes to a filestream instead
|
55
|
+
def write_buffer
|
56
|
+
zos = new('', true)
|
57
|
+
yield zos
|
58
|
+
return zos.close_buffer
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Closes the stream and writes the central directory to the zip file
|
63
|
+
def close
|
64
|
+
return if @closed
|
65
|
+
finalize_current_entry
|
66
|
+
update_local_headers
|
67
|
+
write_central_directory
|
68
|
+
@output_stream.close
|
69
|
+
@closed = true
|
70
|
+
end
|
71
|
+
|
72
|
+
# Closes the stream and writes the central directory to the zip file
|
73
|
+
def close_buffer
|
74
|
+
return @output_stream if @closed
|
75
|
+
finalize_current_entry
|
76
|
+
update_local_headers
|
77
|
+
write_central_directory
|
78
|
+
@closed = true
|
79
|
+
@output_stream
|
80
|
+
end
|
81
|
+
|
82
|
+
# Closes the current entry and opens a new for writing.
|
83
|
+
# +entry+ can be a ZipEntry object or a string.
|
84
|
+
def put_next_entry(entryname, comment = nil, extra = nil, compression_method = Entry::DEFLATED, level = Zlib::DEFAULT_COMPRESSION)
|
85
|
+
raise ZipError, "zip stream is closed" if @closed
|
86
|
+
if entryname.kind_of?(Entry)
|
87
|
+
new_entry = entryname
|
88
|
+
else
|
89
|
+
new_entry = Entry.new(@fileName, entryname.to_s)
|
90
|
+
end
|
91
|
+
new_entry.comment = comment if !comment.nil?
|
92
|
+
if (!extra.nil?)
|
93
|
+
new_entry.extra = ExtraField === extra ? extra : ExtraField.new(extra.to_s)
|
94
|
+
end
|
95
|
+
new_entry.compression_method = compression_method if !compression_method.nil?
|
96
|
+
init_next_entry(new_entry, level)
|
97
|
+
@currentEntry = new_entry
|
98
|
+
end
|
99
|
+
|
100
|
+
def copy_raw_entry(entry)
|
101
|
+
entry = entry.dup
|
102
|
+
raise ZipError, "zip stream is closed" if @closed
|
103
|
+
raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(Entry)
|
104
|
+
finalize_current_entry
|
105
|
+
@entry_set << entry
|
106
|
+
src_pos = entry.local_entry_offset
|
107
|
+
entry.write_local_entry(@output_stream)
|
108
|
+
@compressor = NullCompressor.instance
|
109
|
+
entry.get_raw_input_stream do |is|
|
110
|
+
is.seek(src_pos, IO::SEEK_SET)
|
111
|
+
IOExtras.copy_stream_n(@output_stream, is, entry.compressed_size)
|
112
|
+
end
|
113
|
+
@compressor = NullCompressor.instance
|
114
|
+
@currentEntry = nil
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def finalize_current_entry
|
120
|
+
return unless @currentEntry
|
121
|
+
finish
|
122
|
+
@currentEntry.compressed_size = @output_stream.tell - @currentEntry.local_header_offset - @currentEntry.calculate_local_header_size
|
123
|
+
@currentEntry.size = @compressor.size
|
124
|
+
@currentEntry.crc = @compressor.crc
|
125
|
+
@currentEntry = nil
|
126
|
+
@compressor = NullCompressor.instance
|
127
|
+
end
|
128
|
+
|
129
|
+
def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
|
130
|
+
finalize_current_entry
|
131
|
+
@entry_set << entry
|
132
|
+
entry.write_local_entry(@output_stream)
|
133
|
+
@compressor = get_compressor(entry, level)
|
134
|
+
end
|
135
|
+
|
136
|
+
def get_compressor(entry, level)
|
137
|
+
case entry.compression_method
|
138
|
+
when Entry::DEFLATED then Deflater.new(@output_stream, level)
|
139
|
+
when Entry::STORED then PassThruCompressor.new(@output_stream)
|
140
|
+
else raise ZipCompressionMethodError,
|
141
|
+
"Invalid compression method: '#{entry.compression_method}'"
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def update_local_headers
|
146
|
+
pos = @output_stream.pos
|
147
|
+
@entry_set.each do |entry|
|
148
|
+
@output_stream.pos = entry.local_header_offset
|
149
|
+
entry.write_local_entry(@output_stream)
|
150
|
+
end
|
151
|
+
@output_stream.pos = pos
|
152
|
+
end
|
153
|
+
|
154
|
+
def write_central_directory
|
155
|
+
cdir = CentralDirectory.new(@entry_set, @comment)
|
156
|
+
cdir.write_to_stream(@output_stream)
|
157
|
+
end
|
158
|
+
|
159
|
+
protected
|
160
|
+
|
161
|
+
def finish
|
162
|
+
@compressor.finish
|
163
|
+
end
|
164
|
+
|
165
|
+
public
|
166
|
+
# Modeled after IO.<<
|
167
|
+
def << (data)
|
168
|
+
@compressor << data
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
174
|
+
# rubyzip is free software; you can redistribute it and/or
|
175
|
+
# modify it under the terms of the ruby license.
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Zip
|
2
|
+
class PassThruCompressor < Compressor #:nodoc:all
|
3
|
+
def initialize(outputStream)
|
4
|
+
super()
|
5
|
+
@output_stream = outputStream
|
6
|
+
@crc = Zlib::crc32
|
7
|
+
@size = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def << (data)
|
11
|
+
val = data.to_s
|
12
|
+
@crc = Zlib::crc32(val, @crc)
|
13
|
+
@size += val.bytesize
|
14
|
+
@output_stream << val
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :size, :crc
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
22
|
+
# rubyzip is free software; you can redistribute it and/or
|
23
|
+
# modify it under the terms of the ruby license.
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Zip
|
2
|
+
class PassThruDecompressor < Decompressor #:nodoc:all
|
3
|
+
|
4
|
+
def initialize(input_stream, chars_to_read)
|
5
|
+
super(input_stream)
|
6
|
+
@chars_to_read = chars_to_read
|
7
|
+
@read_so_far = 0
|
8
|
+
@has_returned_empty_string = false
|
9
|
+
end
|
10
|
+
|
11
|
+
def sysread(number_of_bytes = nil, buf = nil)
|
12
|
+
if input_finished?
|
13
|
+
has_returned_empty_string_val = @has_returned_empty_string
|
14
|
+
@has_returned_empty_string = true
|
15
|
+
return '' unless has_returned_empty_string_val
|
16
|
+
return
|
17
|
+
end
|
18
|
+
|
19
|
+
if (number_of_bytes == nil || @read_so_far + number_of_bytes > @chars_to_read)
|
20
|
+
number_of_bytes = @chars_to_read - @read_so_far
|
21
|
+
end
|
22
|
+
@read_so_far += number_of_bytes
|
23
|
+
@input_stream.read(number_of_bytes, buf)
|
24
|
+
end
|
25
|
+
|
26
|
+
def produce_input
|
27
|
+
sysread(::Zip::Decompressor::CHUNK_SIZE)
|
28
|
+
end
|
29
|
+
|
30
|
+
def input_finished?
|
31
|
+
@read_so_far >= @chars_to_read
|
32
|
+
end
|
33
|
+
|
34
|
+
alias :eof :input_finished?
|
35
|
+
alias :eof? :input_finished?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
40
|
+
# rubyzip is free software; you can redistribute it and/or
|
41
|
+
# modify it under the terms of the ruby license.
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Zip
|
2
|
+
class StreamableDirectory < Entry
|
3
|
+
def initialize(zipfile, entry, srcPath = nil, permissionInt = nil)
|
4
|
+
super(zipfile, entry)
|
5
|
+
|
6
|
+
@ftype = :directory
|
7
|
+
entry.get_extra_attributes_from_path(srcPath) if (srcPath)
|
8
|
+
@unix_perms = permissionInt if (permissionInt)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
14
|
+
# rubyzip is free software; you can redistribute it and/or
|
15
|
+
# modify it under the terms of the ruby license.
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Zip
|
2
|
+
class StreamableStream < DelegateClass(Entry) #nodoc:all
|
3
|
+
def initialize(entry)
|
4
|
+
super(entry)
|
5
|
+
@tempFile = Tempfile.new(::File.basename(name), ::File.dirname(zipfile))
|
6
|
+
@tempFile.binmode
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_output_stream
|
10
|
+
if block_given?
|
11
|
+
begin
|
12
|
+
yield(@tempFile)
|
13
|
+
ensure
|
14
|
+
@tempFile.close
|
15
|
+
end
|
16
|
+
else
|
17
|
+
@tempFile
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_input_stream
|
22
|
+
if ! @tempFile.closed?
|
23
|
+
raise StandardError, "cannot open entry for reading while its open for writing - #{name}"
|
24
|
+
end
|
25
|
+
@tempFile.open # reopens tempfile from top
|
26
|
+
@tempFile.binmode
|
27
|
+
if block_given?
|
28
|
+
begin
|
29
|
+
yield(@tempFile)
|
30
|
+
ensure
|
31
|
+
@tempFile.close
|
32
|
+
end
|
33
|
+
else
|
34
|
+
@tempFile
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def write_to_zip_output_stream(aZipOutputStream)
|
39
|
+
aZipOutputStream.put_next_entry(self)
|
40
|
+
get_input_stream { |is| ::Zip::IOExtras.copy_stream(aZipOutputStream, is) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
46
|
+
# rubyzip is free software; you can redistribute it and/or
|
47
|
+
# modify it under the terms of the ruby license.
|
data/lib/zip/version.rb
ADDED
data/samples/example.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << "../lib"
|
4
|
+
system("zip example.zip example.rb gtkRubyzip.rb")
|
5
|
+
|
6
|
+
require 'zip/zip'
|
7
|
+
|
8
|
+
####### Using ZipInputStream alone: #######
|
9
|
+
|
10
|
+
Zip::InputStream.open("example.zip") {
|
11
|
+
|zis|
|
12
|
+
entry = zis.get_next_entry
|
13
|
+
print "First line of '#{entry.name} (#{entry.size} bytes): "
|
14
|
+
puts "'#{zis.gets.chomp}'"
|
15
|
+
entry = zis.get_next_entry
|
16
|
+
print "First line of '#{entry.name} (#{entry.size} bytes): "
|
17
|
+
puts "'#{zis.gets.chomp}'"
|
18
|
+
}
|
19
|
+
|
20
|
+
|
21
|
+
####### Using ZipFile to read the directory of a zip file: #######
|
22
|
+
|
23
|
+
zf = Zip::File.new("example.zip")
|
24
|
+
zf.each_with_index {
|
25
|
+
|entry, index|
|
26
|
+
|
27
|
+
puts "entry #{index} is #{entry.name}, size = #{entry.size}, compressed size = #{entry.compressed_size}"
|
28
|
+
# use zf.get_input_stream(entry) to get a ZipInputStream for the entry
|
29
|
+
# entry can be the ZipEntry object or any object which has a to_s method that
|
30
|
+
# returns the name of the entry.
|
31
|
+
}
|
32
|
+
|
33
|
+
|
34
|
+
####### Using ZipOutputStream to write a zip file: #######
|
35
|
+
|
36
|
+
Zip::OutputStream.open("exampleout.zip") {
|
37
|
+
|zos|
|
38
|
+
zos.put_next_entry("the first little entry")
|
39
|
+
zos.puts "Hello hello hello hello hello hello hello hello hello"
|
40
|
+
|
41
|
+
zos.put_next_entry("the second little entry")
|
42
|
+
zos.puts "Hello again"
|
43
|
+
|
44
|
+
# Use rubyzip or your zip client of choice to verify
|
45
|
+
# the contents of exampleout.zip
|
46
|
+
}
|
47
|
+
|
48
|
+
####### Using ZipFile to change a zip file: #######
|
49
|
+
|
50
|
+
Zip::File.open("exampleout.zip") {
|
51
|
+
|zf|
|
52
|
+
zf.add("thisFile.rb", "example.rb")
|
53
|
+
zf.rename("thisFile.rb", "ILikeThisName.rb")
|
54
|
+
zf.add("Again", "example.rb")
|
55
|
+
}
|
56
|
+
|
57
|
+
# Lets check
|
58
|
+
Zip::File.open("exampleout.zip") {
|
59
|
+
|zf|
|
60
|
+
puts "Changed zip file contains: #{zf.entries.join(', ')}"
|
61
|
+
zf.remove("Again")
|
62
|
+
puts "Without 'Again': #{zf.entries.join(', ')}"
|
63
|
+
}
|
64
|
+
|
65
|
+
####### Using ZipFile to split a zip file: #######
|
66
|
+
|
67
|
+
# Creating large zip file for splitting
|
68
|
+
Zip::OutputStream.open("large_zip_file.zip") do |zos|
|
69
|
+
puts "Creating zip file..."
|
70
|
+
10.times do |i|
|
71
|
+
zos.put_next_entry("large_entry_#{i}.txt")
|
72
|
+
zos.puts "Hello" * 104857600
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Splitting created large zip file
|
77
|
+
part_zips_count = Zip::File.split("large_zip_file.zip", 2097152, false)
|
78
|
+
puts "Zip file splitted in #{part_zips_count} parts"
|
79
|
+
|
80
|
+
# Track splitting an archive
|
81
|
+
Zip::File.split("large_zip_file.zip", 1048576, true, 'part_zip_file') do
|
82
|
+
|part_count, part_index, chunk_bytes, segment_bytes|
|
83
|
+
puts "#{part_index} of #{part_count} part splitting: #{(chunk_bytes.to_f/segment_bytes.to_f * 100).to_i}%"
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
# For other examples, look at zip.rb and ziptest.rb
|
88
|
+
|
89
|
+
# Copyright (C) 2002 Thomas Sondergaard
|
90
|
+
# rubyzip is free software; you can redistribute it and/or
|
91
|
+
# modify it under the terms of the ruby license.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$: << "../lib"
|
4
|
+
|
5
|
+
require 'zip/zipfilesystem'
|
6
|
+
|
7
|
+
EXAMPLE_ZIP = "filesystem.zip"
|
8
|
+
|
9
|
+
File.delete(EXAMPLE_ZIP) if File.exists?(EXAMPLE_ZIP)
|
10
|
+
|
11
|
+
Zip::File.open(EXAMPLE_ZIP, Zip::File::CREATE) {
|
12
|
+
|zf|
|
13
|
+
zf.file.open("file1.txt", "w") { |os| os.write "first file1.txt" }
|
14
|
+
zf.dir.mkdir("dir1")
|
15
|
+
zf.dir.chdir("dir1")
|
16
|
+
zf.file.open("file1.txt", "w") { |os| os.write "second file1.txt" }
|
17
|
+
puts zf.file.read("file1.txt")
|
18
|
+
puts zf.file.read("../file1.txt")
|
19
|
+
zf.dir.chdir("..")
|
20
|
+
zf.file.open("file2.txt", "w") { |os| os.write "first file2.txt" }
|
21
|
+
puts "Entries: #{zf.entries.join(', ')}"
|
22
|
+
}
|
23
|
+
|
24
|
+
Zip::File.open(EXAMPLE_ZIP) {
|
25
|
+
|zf|
|
26
|
+
puts "Entries from reloaded zip: #{zf.entries.join(', ')}"
|
27
|
+
}
|
28
|
+
|
29
|
+
# For other examples, look at zip.rb and ziptest.rb
|
30
|
+
|
31
|
+
# Copyright (C) 2003 Thomas Sondergaard
|
32
|
+
# rubyzip is free software; you can redistribute it and/or
|
33
|
+
# modify it under the terms of the ruby license.
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'zip/zip'
|
2
|
+
|
3
|
+
# This is a simple example which uses rubyzip to
|
4
|
+
# recursively generate a zip file from the contents of
|
5
|
+
# a specified directory. The directory itself is not
|
6
|
+
# included in the archive, rather just its contents.
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
# directoryToZip = "/tmp/input"
|
10
|
+
# outputFile = "/tmp/out.zip"
|
11
|
+
# zf = ZipFileGenerator.new(directoryToZip, outputFile)
|
12
|
+
# zf.write()
|
13
|
+
class ZipFileGenerator
|
14
|
+
|
15
|
+
# Initialize with the directory to zip and the location of the output archive.
|
16
|
+
def initialize(inputDir, outputFile)
|
17
|
+
@inputDir = inputDir
|
18
|
+
@outputFile = outputFile
|
19
|
+
end
|
20
|
+
|
21
|
+
# Zip the input directory.
|
22
|
+
def write()
|
23
|
+
entries = Dir.entries(@inputDir); entries.delete("."); entries.delete("..")
|
24
|
+
io = Zip::File.open(@outputFile, Zip::File::CREATE);
|
25
|
+
|
26
|
+
writeEntries(entries, "", io)
|
27
|
+
io.close();
|
28
|
+
end
|
29
|
+
|
30
|
+
# A helper method to make the recursion work.
|
31
|
+
private
|
32
|
+
def writeEntries(entries, path, io)
|
33
|
+
|
34
|
+
entries.each { |e|
|
35
|
+
zipFilePath = path == "" ? e : File.join(path, e)
|
36
|
+
diskFilePath = File.join(@inputDir, zipFilePath)
|
37
|
+
puts "Deflating " + diskFilePath
|
38
|
+
if File.directory?(diskFilePath)
|
39
|
+
io.mkdir(zipFilePath)
|
40
|
+
subdir =Dir.entries(diskFilePath); subdir.delete("."); subdir.delete("..")
|
41
|
+
writeEntries(subdir, zipFilePath, io)
|
42
|
+
else
|
43
|
+
io.get_output_stream(zipFilePath) { |f| f.puts(File.open(diskFilePath, "rb").read())}
|
44
|
+
end
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|