gpgme-loongson 2.0.18
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/examples/edit.rb +77 -0
- data/examples/genkey.rb +55 -0
- data/examples/keylist.rb +7 -0
- data/examples/roundtrip.rb +42 -0
- data/examples/sign.rb +31 -0
- data/examples/verify.rb +8 -0
- data/ext/gpgme/extconf.rb +227 -0
- data/ext/gpgme/gpgme_n.c +3071 -0
- data/lib/gpgme.rb +108 -0
- data/lib/gpgme/compat.rb +48 -0
- data/lib/gpgme/constants.rb +268 -0
- data/lib/gpgme/crypto.rb +357 -0
- data/lib/gpgme/ctx.rb +525 -0
- data/lib/gpgme/data.rb +208 -0
- data/lib/gpgme/engine.rb +87 -0
- data/lib/gpgme/error.rb +66 -0
- data/lib/gpgme/io_callbacks.rb +21 -0
- data/lib/gpgme/key.rb +248 -0
- data/lib/gpgme/key_common.rb +43 -0
- data/lib/gpgme/key_sig.rb +35 -0
- data/lib/gpgme/misc.rb +73 -0
- data/lib/gpgme/signature.rb +85 -0
- data/lib/gpgme/sub_key.rb +67 -0
- data/lib/gpgme/user_id.rb +20 -0
- data/lib/gpgme/version.rb +4 -0
- data/test/crypto_test.rb +246 -0
- data/test/ctx_test.rb +471 -0
- data/test/data_test.rb +142 -0
- data/test/files/testkey_pub.gpg +52 -0
- data/test/files/testkey_sec.gpg +54 -0
- data/test/gpgme_test.rb +12 -0
- data/test/key_test.rb +209 -0
- data/test/signature_test.rb +52 -0
- data/test/sub_key_test.rb +48 -0
- data/test/support/resources.rb +532 -0
- data/test/test_helper.rb +114 -0
- metadata +170 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f27306478e9c9925425e60f3cc2155bd4f3050c95c1af4d878044cb5006dc83b
|
4
|
+
data.tar.gz: 7f703700032b47ec14758c7d7dfc59b00b4932c5437d408b2ccafb0745cbabe2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e0e913a62be465111c918039f7dd4ad2311a79c2c9aade95c58d467fed4e389d1bdd60f35f5b8d2823e38436c8b8762163a86b2e67b9c3bff5dbb1772142660e
|
7
|
+
data.tar.gz: 4ba23bf0e71ccc5b30648d02cafda4799161c73a827a8117e307da81df1ec3f4cb4b7d5ea1e2d680a36e9673076cae7164cf4eb5dd3013620824e341e944a431
|
data/examples/edit.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'gpgme'
|
3
|
+
|
4
|
+
# If you do not have gpg-agent installed, comment out the following
|
5
|
+
# and set it as :passphrase_callback.
|
6
|
+
#
|
7
|
+
# def passfunc(hook, uid_hint, passphrase_info, prev_was_bad, fd)
|
8
|
+
# $stderr.write("Passphrase for #{uid_hint}: ")
|
9
|
+
# $stderr.flush
|
10
|
+
# begin
|
11
|
+
# system('stty -echo')
|
12
|
+
# io = IO.for_fd(fd, 'w')
|
13
|
+
# io.puts(gets)
|
14
|
+
# io.flush
|
15
|
+
# ensure
|
16
|
+
# (0 ... $_.length).each do |i| $_[i] = ?0 end if $_
|
17
|
+
# system('stty echo')
|
18
|
+
# end
|
19
|
+
# $stderr.puts
|
20
|
+
# end
|
21
|
+
|
22
|
+
unless ENV['GPG_AGENT_INFO']
|
23
|
+
$stderr.puts("gpg-agent is not running. See the comment in #{$0}.")
|
24
|
+
exit(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
unless ENV['GNUPGHOME']
|
28
|
+
$stderr.write('As GNUPGHOME is not set, the generated key pair will be stored into *your* keyring. Really proceed? (y/N) ')
|
29
|
+
$stderr.flush
|
30
|
+
exit(1) unless gets.chomp == 'y'
|
31
|
+
end
|
32
|
+
|
33
|
+
unless ARGV.length == 1
|
34
|
+
$stderr.puts("Usage: #{$0} KEYGRIP")
|
35
|
+
exit(1)
|
36
|
+
end
|
37
|
+
|
38
|
+
def progfunc(hook, what, type, current, total)
|
39
|
+
$stderr.write("#{what}: #{current}/#{total}\r")
|
40
|
+
$stderr.flush
|
41
|
+
end
|
42
|
+
|
43
|
+
def editfunc(hook, status, args, fd)
|
44
|
+
case status
|
45
|
+
when GPGME::GPGME_STATUS_GET_BOOL
|
46
|
+
begin
|
47
|
+
$stderr.write("#{args} (y/n) ")
|
48
|
+
$stderr.flush
|
49
|
+
line = gets
|
50
|
+
end until line =~ /\A\s*[ny]\s*\z/
|
51
|
+
io = IO.for_fd(fd)
|
52
|
+
io.puts(line.strip)
|
53
|
+
io.flush
|
54
|
+
when GPGME::GPGME_STATUS_GET_LINE, GPGME::GPGME_STATUS_GET_HIDDEN
|
55
|
+
$stderr.write("#{args}: ")
|
56
|
+
$stderr.flush
|
57
|
+
line = gets
|
58
|
+
io = IO.for_fd(fd)
|
59
|
+
io.puts(line)
|
60
|
+
io.flush
|
61
|
+
else
|
62
|
+
$stderr.puts([status, args].inspect)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
ctx = GPGME::Ctx.new({:progress_callback => method(:progfunc),
|
67
|
+
# :passphrase_callback => method(:passfunc)
|
68
|
+
})
|
69
|
+
keystr = ARGV.shift
|
70
|
+
keys = ctx.keys(keystr)
|
71
|
+
if keys.empty?
|
72
|
+
$stderr.puts("Can't find key for \"#{keystr}\"")
|
73
|
+
exit(1)
|
74
|
+
end
|
75
|
+
|
76
|
+
$stderr.puts(keys.first.inspect)
|
77
|
+
ctx.edit_key(keys.first, method(:editfunc))
|
data/examples/genkey.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'gpgme'
|
3
|
+
|
4
|
+
# If you do not have gpg-agent installed, comment out the following
|
5
|
+
# and set it as :passphrase_callback.
|
6
|
+
#
|
7
|
+
# def passfunc(hook, uid_hint, passphrase_info, prev_was_bad, fd)
|
8
|
+
# $stderr.write("Passphrase for #{uid_hint}: ")
|
9
|
+
# $stderr.flush
|
10
|
+
# begin
|
11
|
+
# system('stty -echo')
|
12
|
+
# io = IO.for_fd(fd, 'w')
|
13
|
+
# io.puts(gets)
|
14
|
+
# io.flush
|
15
|
+
# ensure
|
16
|
+
# (0 ... $_.length).each do |i| $_[i] = ?0 end if $_
|
17
|
+
# system('stty echo')
|
18
|
+
# end
|
19
|
+
# $stderr.puts
|
20
|
+
# end
|
21
|
+
|
22
|
+
unless ENV['GPG_AGENT_INFO']
|
23
|
+
$stderr.puts("gpg-agent is not running. See the comment in #{$0}.")
|
24
|
+
exit(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
unless ENV['GNUPGHOME']
|
28
|
+
$stderr.write('As GNUPGHOME is not set, the generated key pair will be stored into *your* keyring. Really proceed? (y/N) ')
|
29
|
+
$stderr.flush
|
30
|
+
exit(1) unless gets.chomp == 'y'
|
31
|
+
end
|
32
|
+
|
33
|
+
def progfunc(hook, what, type, current, total)
|
34
|
+
$stderr.write("#{what}: #{current}/#{total}\r")
|
35
|
+
$stderr.flush
|
36
|
+
end
|
37
|
+
|
38
|
+
ctx = GPGME::Ctx.new({:progress_callback => method(:progfunc),
|
39
|
+
# :passphrase_callback => method(:passfunc)
|
40
|
+
})
|
41
|
+
|
42
|
+
ctx.genkey(<<'EOF', nil, nil)
|
43
|
+
<GnupgKeyParms format="internal">
|
44
|
+
Key-Type: DSA
|
45
|
+
Key-Length: 1024
|
46
|
+
Subkey-Type: ELG-E
|
47
|
+
Subkey-Length: 1024
|
48
|
+
Name-Real: Joe Tester
|
49
|
+
Name-Comment: with stupid passphrase
|
50
|
+
Name-Email: joe@foo.bar
|
51
|
+
Expire-Date: 0
|
52
|
+
Passphrase: abc
|
53
|
+
</GnupgKeyParms>
|
54
|
+
EOF
|
55
|
+
$stderr.puts
|
data/examples/keylist.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'gpgme'
|
3
|
+
|
4
|
+
# If you do not have gpg-agent installed, comment out the following
|
5
|
+
# and set it as :passphrase_callback.
|
6
|
+
#
|
7
|
+
# def passfunc(hook, uid_hint, passphrase_info, prev_was_bad, fd)
|
8
|
+
# $stderr.write("Passphrase for #{uid_hint}: ")
|
9
|
+
# $stderr.flush
|
10
|
+
# begin
|
11
|
+
# system('stty -echo')
|
12
|
+
# io = IO.for_fd(fd, 'w')
|
13
|
+
# io.puts(gets)
|
14
|
+
# io.flush
|
15
|
+
# ensure
|
16
|
+
# (0 ... $_.length).each do |i| $_[i] = ?0 end if $_
|
17
|
+
# system('stty echo')
|
18
|
+
# end
|
19
|
+
# $stderr.puts
|
20
|
+
# end
|
21
|
+
|
22
|
+
unless ENV['GPG_AGENT_INFO']
|
23
|
+
$stderr.puts("gpg-agent is not running. See the comment in #{$0}.")
|
24
|
+
exit(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
plain = 'test test test'
|
28
|
+
puts("Plaintext:\n#{plain}")
|
29
|
+
|
30
|
+
# Perform symmetric encryption on PLAIN.
|
31
|
+
crypto = GPGME::Crypto.new(:armor => true)
|
32
|
+
cipher = crypto.encrypt(plain, {:symmetric => true,
|
33
|
+
# :passphrase_callback => method(:passfunc)
|
34
|
+
})
|
35
|
+
str = cipher.read
|
36
|
+
puts("Ciphertext:\n#{str}")
|
37
|
+
|
38
|
+
cipher = GPGME::Data.new(str)
|
39
|
+
plain = crypto.decrypt(cipher, {
|
40
|
+
# :passphrase_callback => method(:passfunc)
|
41
|
+
})
|
42
|
+
puts("Plaintext:\n#{plain.read}")
|
data/examples/sign.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'gpgme'
|
3
|
+
|
4
|
+
# If you do not have gpg-agent installed, comment out the following
|
5
|
+
# and set it as :passphrase_callback.
|
6
|
+
#
|
7
|
+
# def passfunc(hook, uid_hint, passphrase_info, prev_was_bad, fd)
|
8
|
+
# $stderr.write("Passphrase for #{uid_hint}: ")
|
9
|
+
# $stderr.flush
|
10
|
+
# begin
|
11
|
+
# system('stty -echo')
|
12
|
+
# io = IO.for_fd(fd, 'w')
|
13
|
+
# io.puts(gets)
|
14
|
+
# io.flush
|
15
|
+
# ensure
|
16
|
+
# (0 ... $_.length).each do |i| $_[i] = ?0 end if $_
|
17
|
+
# system('stty echo')
|
18
|
+
# end
|
19
|
+
# $stderr.puts
|
20
|
+
# end
|
21
|
+
|
22
|
+
unless ENV['GPG_AGENT_INFO']
|
23
|
+
$stderr.puts("gpg-agent is not running. See the comment in #{$0}.")
|
24
|
+
exit(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
crypto = GPGME::Crypto.new
|
28
|
+
signature = crypto.clearsign('test test test', {
|
29
|
+
# :passphrase_callback => method(:passfunc)
|
30
|
+
})
|
31
|
+
puts signature.read
|
data/examples/verify.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'mkmf'
|
2
|
+
|
3
|
+
# Available options:
|
4
|
+
#
|
5
|
+
# --enable-clean (default)
|
6
|
+
# --disable-clean
|
7
|
+
#
|
8
|
+
# This file is largely based on Nokogiri's extconf.rb.
|
9
|
+
|
10
|
+
ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))
|
11
|
+
|
12
|
+
if arg_config('--clean')
|
13
|
+
require 'pathname'
|
14
|
+
require 'fileutils'
|
15
|
+
|
16
|
+
root = Pathname(ROOT)
|
17
|
+
pwd = Pathname(Dir.pwd)
|
18
|
+
|
19
|
+
# Skip if this is a development work tree
|
20
|
+
unless (root + '.git').exist?
|
21
|
+
message "Cleaning files only used during build.\n"
|
22
|
+
|
23
|
+
# (root + 'tmp') cannot be removed at this stage because
|
24
|
+
# gpgme_n.so is yet to be copied to lib.
|
25
|
+
|
26
|
+
# clean the ports build directory
|
27
|
+
Pathname.glob(pwd.join('tmp', '*', 'ports')) { |dir|
|
28
|
+
FileUtils.rm_rf(dir, { :verbose => true })
|
29
|
+
FileUtils.rmdir(dir.parent, { :parents => true, :verbose => true })
|
30
|
+
}
|
31
|
+
|
32
|
+
# ports installation can be safely removed if statically linked.
|
33
|
+
FileUtils.rm_rf(root + 'ports', { :verbose => true })
|
34
|
+
end
|
35
|
+
|
36
|
+
exit
|
37
|
+
end
|
38
|
+
|
39
|
+
if arg_config('--use-system-libraries', ENV['RUBY_GPGME_USE_SYSTEM_LIBRARIES'])
|
40
|
+
unless find_executable('gpgme-config')
|
41
|
+
$stderr.puts("gpgme-config not found")
|
42
|
+
exit(1)
|
43
|
+
end
|
44
|
+
|
45
|
+
$CFLAGS += ' ' << `gpgme-config --cflags`.chomp
|
46
|
+
$libs += ' ' << `gpgme-config --libs`.chomp
|
47
|
+
else
|
48
|
+
message <<-'EOS'
|
49
|
+
************************************************************************
|
50
|
+
IMPORTANT! gpgme gem uses locally built versions of required C libraries,
|
51
|
+
namely libgpg-error, libassuan, and gpgme.
|
52
|
+
|
53
|
+
If this is a concern for you and you want to use the system library
|
54
|
+
instead, abort this installation process and reinstall gpgme gem as
|
55
|
+
follows:
|
56
|
+
|
57
|
+
gem install gpgme -- --use-system-libraries
|
58
|
+
|
59
|
+
************************************************************************
|
60
|
+
EOS
|
61
|
+
|
62
|
+
require 'rubygems'
|
63
|
+
require 'mini_portile2'
|
64
|
+
|
65
|
+
libgpg_error_recipe = MiniPortile.new('libgpg-error', '1.32').tap do |recipe|
|
66
|
+
recipe.target = File.join(ROOT, "ports")
|
67
|
+
recipe.files = [{
|
68
|
+
:url => "http://10.110.36.228/#{recipe.name}-#{recipe.version}.tar.bz2",
|
69
|
+
:sha256 => '7b9f732b772380103a793c09a961b88bbb76e9f25c4cd1b30c543196c3021bf0'
|
70
|
+
}]
|
71
|
+
recipe.configure_options = [
|
72
|
+
'--disable-shared',
|
73
|
+
'--enable-static',
|
74
|
+
'--disable-nls',
|
75
|
+
"CFLAGS=-fPIC #{ENV["CFLAGS"]}",
|
76
|
+
]
|
77
|
+
checkpoint = "#{recipe.target}/#{recipe.name}-#{recipe.version}-#{recipe.host}.installed"
|
78
|
+
unless File.exist?(checkpoint)
|
79
|
+
recipe.cook
|
80
|
+
FileUtils.touch checkpoint
|
81
|
+
end
|
82
|
+
recipe.activate
|
83
|
+
end
|
84
|
+
|
85
|
+
libassuan_recipe = MiniPortile.new('libassuan', '2.5.1').tap do |recipe|
|
86
|
+
recipe.target = File.join(ROOT, "ports")
|
87
|
+
recipe.files = [{
|
88
|
+
:url => "http://10.110.36.228/#{recipe.name}-#{recipe.version}.tar.bz2",
|
89
|
+
:sha256 => '35fa6460782f530022accb093977cb32b91e44d8b7d1e81c1bcb70d50c20d12e'
|
90
|
+
}]
|
91
|
+
recipe.configure_options = [
|
92
|
+
'--disable-shared',
|
93
|
+
'--enable-static',
|
94
|
+
"--with-gpg-error-prefix=#{libgpg_error_recipe.path}",
|
95
|
+
"CFLAGS=-fPIC #{ENV["CFLAGS"]}",
|
96
|
+
]
|
97
|
+
checkpoint = "#{recipe.target}/#{recipe.name}-#{recipe.version}-#{recipe.host}.installed"
|
98
|
+
unless File.exist?(checkpoint)
|
99
|
+
recipe.cook
|
100
|
+
FileUtils.touch checkpoint
|
101
|
+
end
|
102
|
+
recipe.activate
|
103
|
+
end
|
104
|
+
|
105
|
+
gpgme_recipe = MiniPortile.new('gpgme', '1.12.0').tap do |recipe|
|
106
|
+
recipe.target = File.join(ROOT, "ports")
|
107
|
+
recipe.files = [{
|
108
|
+
:url => "http://10.110.36.228/#{recipe.name}-#{recipe.version}.tar.bz2",
|
109
|
+
:sha256 => '5fb99b163e16d634f9e96fede1a01ddc53f96e91130b8ce37a53ebe2de19273d'
|
110
|
+
}]
|
111
|
+
recipe.configure_options = [
|
112
|
+
'--disable-shared',
|
113
|
+
'--enable-static',
|
114
|
+
"--with-gpg-error-prefix=#{libgpg_error_recipe.path}",
|
115
|
+
"--with-libassuan-prefix=#{libassuan_recipe.path}",
|
116
|
+
# GPGME 1.5.0 assumes gpgsm is present if gpgconf is found.
|
117
|
+
# However, on some systems (e.g. Debian), they are splitted into
|
118
|
+
# separate packages.
|
119
|
+
'--disable-gpgconf-test',
|
120
|
+
'--disable-gpg-test',
|
121
|
+
'--disable-gpgsm-test',
|
122
|
+
'--disable-g13-test',
|
123
|
+
# We only need the C API.
|
124
|
+
'--disable-languages',
|
125
|
+
"CFLAGS=-fPIC #{ENV["CFLAGS"]}",
|
126
|
+
]
|
127
|
+
checkpoint = "#{recipe.target}/#{recipe.name}-#{recipe.version}-#{recipe.host}.installed"
|
128
|
+
unless File.exist?(checkpoint)
|
129
|
+
recipe.cook
|
130
|
+
FileUtils.touch checkpoint
|
131
|
+
end
|
132
|
+
recipe.activate
|
133
|
+
end
|
134
|
+
|
135
|
+
# special treatment to link with static libraries
|
136
|
+
$libs = $libs.shellsplit.tap {|libs|
|
137
|
+
File.join(gpgme_recipe.path, "bin", "gpgme-config").tap {|config|
|
138
|
+
# call config scripts explicit with 'sh' for compat with Windows
|
139
|
+
$CPPFLAGS = `sh #{config} --cflags`.strip << ' ' << $CPPFLAGS
|
140
|
+
`sh #{config} --libs`.strip.shellsplit.each {|arg|
|
141
|
+
case arg
|
142
|
+
when /\A-L(.+)\z/
|
143
|
+
lpath=$1
|
144
|
+
# Prioritize ports' directories
|
145
|
+
if lpath.start_with?(ROOT + '/')
|
146
|
+
$LIBPATH = [lpath] | $LIBPATH
|
147
|
+
else
|
148
|
+
$LIBPATH = $LIBPATH | [lpath]
|
149
|
+
end
|
150
|
+
when /\A-l(.+)\z/
|
151
|
+
# Resolve absolute paths of local static libraries to avoid
|
152
|
+
# linking with system libraries.
|
153
|
+
libname_to_recipe = {
|
154
|
+
'gpgme' => gpgme_recipe,
|
155
|
+
'assuan' => libassuan_recipe,
|
156
|
+
'gpg-error' => libgpg_error_recipe
|
157
|
+
}
|
158
|
+
recipe = libname_to_recipe[$1]
|
159
|
+
if recipe
|
160
|
+
libs.push(File.join(recipe.path, 'lib', "lib#{$1}.#{$LIBEXT}"))
|
161
|
+
else
|
162
|
+
libs.push(arg)
|
163
|
+
end
|
164
|
+
else
|
165
|
+
$LDFLAGS << ' ' << arg.shellescape
|
166
|
+
end
|
167
|
+
}
|
168
|
+
}
|
169
|
+
}.shelljoin
|
170
|
+
|
171
|
+
message 'checking for linker flags for static linking... '
|
172
|
+
case
|
173
|
+
when try_link('int main(void) { return 0; }',
|
174
|
+
['-Wl,-Bstatic', '-lgpgme', '-Wl,-Bdynamic'].shelljoin)
|
175
|
+
message "-Wl,-Bstatic\n"
|
176
|
+
|
177
|
+
$libs = $libs.shellsplit.map {|arg|
|
178
|
+
case arg
|
179
|
+
when '-lgpgme', '-lassuan', '-lgpg-error'
|
180
|
+
['-Wl,-Bstatic', arg, '-Wl,-Bdynamic']
|
181
|
+
else
|
182
|
+
arg
|
183
|
+
end
|
184
|
+
}.flatten.shelljoin
|
185
|
+
else
|
186
|
+
message "NONE\n"
|
187
|
+
end
|
188
|
+
|
189
|
+
unless have_header 'gpgme.h'
|
190
|
+
abort <<-EOS
|
191
|
+
************************************************************************
|
192
|
+
ERROR! Cannot locate 'gpgme.h'.
|
193
|
+
************************************************************************
|
194
|
+
EOS
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
checking_for('gpgme >= 1.1.3') do
|
199
|
+
if try_run(<<'End')
|
200
|
+
#include <gpgme.h>
|
201
|
+
#include <stdlib.h>
|
202
|
+
int main (void) {
|
203
|
+
return gpgme_check_version ("1.1.3") == NULL;
|
204
|
+
}
|
205
|
+
End
|
206
|
+
true
|
207
|
+
else
|
208
|
+
$CFLAGS += ' -DRUBY_GPGME_NEED_WORKAROUND_KEYLIST_NEXT'
|
209
|
+
false
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
have_func('gpgme_op_export_keys')
|
214
|
+
|
215
|
+
create_makefile ('gpgme_n')
|
216
|
+
|
217
|
+
if enable_config('clean', true)
|
218
|
+
# Do not clean if run in a development work tree.
|
219
|
+
File.open('Makefile', 'a') { |mk|
|
220
|
+
mk.print <<EOF
|
221
|
+
all: clean-ports
|
222
|
+
|
223
|
+
clean-ports: $(DLLIB)
|
224
|
+
-$(Q)$(RUBY) $(srcdir)/extconf.rb --clean
|
225
|
+
EOF
|
226
|
+
}
|
227
|
+
end
|