ffi-native 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e2304bce31312de2e522ea76375e8683d6d95b85f4e5ac70075cc7ce08690fc3
4
+ data.tar.gz: 31ffd16c41c38d6b9e6e0c94d562128057f08074491badab7d0e30b8384c91ba
5
+ SHA512:
6
+ metadata.gz: 49a5c875caef86c53ec7e2fab2bd66137b203a6a612d65d08c237d7dfcbb0966778ac99fe890d5fa4aeff3b4c43326c00eb0094a099908a1803d294e2c5c8816
7
+ data.tar.gz: 6851a3a9d0b6028e925dfa0094c73207a21414bdee6129c441a529b5f5fd571e91e51f94384ea02b295213686416f78862ce424bed37ee5f40021ae14fb7637a
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2023, by Samuel Williams.
5
+
6
+ require 'shellwords'
7
+
8
+ require_relative 'loader'
9
+
10
+ module FFI
11
+ module Native
12
+ module ConfigTool
13
+ extend Loader
14
+
15
+ def ffi_load_using_config_tool(command, search_paths: [], names: [], **options)
16
+ return false unless output = ::IO.popen(command).read
17
+
18
+ arguments = ::Shellwords.split(output)
19
+ search_paths = search_paths.dup
20
+ names = names.dup
21
+
22
+ arguments.each do |argument|
23
+ if match = argument.match(/\A(-[lL])(.*)\z/)
24
+ command, value = match.captures
25
+ case command
26
+ when '-L'
27
+ search_paths << value
28
+ when '-l'
29
+ names << value
30
+ end
31
+ elsif File.directory?(argument)
32
+ # Assume it's a search path:
33
+ search_paths << argument
34
+ end
35
+ end
36
+
37
+ result = false
38
+
39
+ # Load all specified libraries:
40
+ names.each do |name|
41
+ result = ffi_load(name, search_paths: search_paths, **options) || result
42
+ end
43
+
44
+ return result
45
+ rescue Errno::ENOENT
46
+ return nil
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,225 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2023, by Samuel Williams.
5
+
6
+ require 'ffi'
7
+
8
+ module FFI
9
+ module Native
10
+ module Library
11
+ def self.extended(target)
12
+ raise "Must only be extended by module, got #{target}!" unless target.kind_of?(Module)
13
+
14
+ target.instance_variable_set(:@ffi_libraries, Array.new)
15
+ target.instance_variable_set(:@ffi_calling_convention, :default)
16
+ target.instance_variable_set(:@ffi_type_map, Hash.new)
17
+ target.instance_variable_set(:@ffi_enumerations, FFI::Enums.new)
18
+ end
19
+
20
+ DEFAULT_FLAGS = FFI::DynamicLibrary::RTLD_LAZY | FFI::DynamicLibrary::RTLD_LOCAL
21
+
22
+ def ffi_open_library(name = nil, flags: DEFAULT_FLAGS)
23
+ @ffi_libraries << DynamicLibrary.open(name, flags)
24
+
25
+ return true
26
+ rescue LoadError, RuntimeError
27
+ # TruffleRuby raises a RuntimeError if the library can't be found.
28
+ return nil
29
+ end
30
+
31
+ def ffi_calling_convention(value = nil)
32
+ if value
33
+ @ffi_calling_convention = value
34
+ end
35
+
36
+ return @ffi_calling_convention
37
+ end
38
+
39
+ def ffi_attach_function(name, argument_types, return_type = :void, as: name, **options)
40
+ argument_types = argument_types.map{|type| self.ffi_find_type(type)}
41
+ return_type = self.ffi_find_type(return_type)
42
+
43
+ options[:convention] ||= @ffi_calling_convention
44
+ options[:type_map] ||= @ffi_type_map
45
+ options[:enums] ||= @ffi_enumerations
46
+
47
+ invoker = nil
48
+
49
+ @ffi_libraries.each do |library|
50
+ function = nil
51
+
52
+ ffi_function_names(name, argument_types).each do |function_name|
53
+ break if function = library.find_function(function_name.to_s)
54
+ end
55
+
56
+ if function
57
+ if argument_types.length > 0 && argument_types.last == FFI::NativeType::VARARGS
58
+ invoker = VariadicInvoker.new(function, argument_types, return_type, options)
59
+ else
60
+ invoker = Function.new(return_type, argument_types, function, options)
61
+ end
62
+
63
+ break
64
+ end
65
+ end
66
+
67
+ if invoker
68
+ invoker.attach(self, as.to_s)
69
+ return true
70
+ else
71
+ raise FFI::NotFoundError.new(name, @ffi_libraries)
72
+ end
73
+ end
74
+
75
+ def ffi_attach_variable(name, type, as: name)
76
+ address = @ffi_libraries.find do |library|
77
+ begin
78
+ library.find_variable(name)
79
+ rescue LoadError
80
+ end
81
+ end
82
+
83
+ if address.nil? || address.null?
84
+ raise FFI::NotFoundError.new(name, @ffi_libraries)
85
+ end
86
+
87
+ if type.is_a?(Class) && type < FFI::Struct
88
+ variable = type.new(address)
89
+
90
+ self.define_singleton_method(as) do
91
+ variable
92
+ end
93
+ else
94
+ container_type = Class.new(FFI::Struct)
95
+ container_type.layout :value, self.ffi_find_type(type)
96
+ container = container_type.new(address)
97
+
98
+ self.define_singleton_method(as) do
99
+ container[:value]
100
+ end
101
+
102
+ self.define_singleton_method(:"#{as}=") do |value|
103
+ container[:value] = value
104
+ end
105
+ end
106
+
107
+ return true
108
+ end
109
+
110
+ def ffi_callback(argument_types, return_type, **options)
111
+ argument_types = argument_types.map{|type| self.ffi_find_type(type)}
112
+ return_type = self.ffi_find_type(return_type)
113
+
114
+ if argument_types.include?(FFI::Type::VARARGS)
115
+ raise ArgumentError, "Callbacks cannot have variadic parameters!"
116
+ end
117
+
118
+ options[:convention] ||= @ffi_calling_convention
119
+ options[:enums] ||= @ffi_enumerations
120
+
121
+ if return_type == Type::STRING
122
+ raise TypeError, "String is not allowed as return type of callbacks!"
123
+ end
124
+
125
+ return FFI::CallbackInfo.new(return_type, argument_types, options)
126
+ end
127
+
128
+ def ffi_define_callback(name, *arguments, **options)
129
+ callback = ffi_callback(*arguments, **options)
130
+
131
+ ffi_define_type(name, callback)
132
+
133
+ return callback
134
+ end
135
+
136
+ def ffi_define_type(name, value)
137
+ case value
138
+ when FFI::Type
139
+ @ffi_type_map[name] = value
140
+ when FFI::DataConverter
141
+ @ffi_type_map[name] = FFI::Type::Mapped.new(value)
142
+ else
143
+ @ffi_type_map[name] = self.ffi_find_type(value)
144
+ end
145
+ end
146
+
147
+ def ffi_define_enumeration(name, *arguments)
148
+ native_type = arguments.first.kind_of?(FFI::Type) ? arguments.shift : nil
149
+
150
+ ffi_define_generic_enumeration(name, FFI::Enum, native_type, *arguments)
151
+ end
152
+
153
+ def ffi_define_bitmask(name, *arguments)
154
+ native_type = arguments.first.kind_of?(FFI::Type) ? arguments.shift : nil
155
+
156
+ ffi_define_generic_enumeration(name, FFI::Bitmask, native_type, *arguments)
157
+ end
158
+
159
+ def ffi_find_type(argument)
160
+ if argument.kind_of?(Type)
161
+ return argument
162
+ end
163
+
164
+ if type = @ffi_type_map[argument]
165
+ return type
166
+ end
167
+
168
+ if argument.is_a?(Class) && argument < Struct
169
+ return Type::POINTER
170
+ end
171
+
172
+ if argument.is_a?(DataConverter)
173
+ # Cache the mapped type:
174
+ return ffi_define_type(argument, Type::Mapped.new(argument))
175
+ end
176
+
177
+ if argument
178
+ return FFI.find_type(argument)
179
+ end
180
+ end
181
+
182
+ private
183
+
184
+ def ffi_define_generic_enumeration(name, klass, native_type, values)
185
+ enumeration = nil
186
+
187
+ if native_type
188
+ enumeration = klass.new(native_type, values, name)
189
+ else
190
+ enumeration = klass.new(values, name)
191
+ end
192
+
193
+ @ffi_enumerations << enumeration
194
+
195
+ ffi_define_type(name, enumeration)
196
+
197
+ return enumeration
198
+ end
199
+
200
+ def ffi_function_names(name, argument_types)
201
+ result = [name]
202
+
203
+ if @ffi_calling_convention == :stdcall
204
+ # Get the size of each parameter:
205
+ size = argument_types.inject(0) do |total, argument|
206
+ size = argument.size
207
+
208
+ # The size must be a multiple of 4:
209
+ size += (4 - size) % 4
210
+
211
+ total + size
212
+ end
213
+
214
+ # win32 naming convention:
215
+ result << "_#{name.to_s}@#{size}"
216
+
217
+ # win64 naming convention:
218
+ result << "#{name.to_s}@#{size}"
219
+ end
220
+
221
+ return result
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2023, by Samuel Williams.
5
+
6
+ require 'ffi'
7
+
8
+ module FFI
9
+ module Native
10
+ module Loader
11
+ def ffi_find_library_path(libname, search_paths)
12
+ search_paths.each do |search_path|
13
+ full_path = File.join(search_path, libname)
14
+ if File.exist?(full_path)
15
+ return full_path
16
+ end
17
+ end
18
+
19
+ return nil
20
+ end
21
+
22
+ def ffi_load(name, search_paths: nil, **options)
23
+ # Try to load the library directly:
24
+ return true if ffi_open_library(name, **options)
25
+
26
+ # If that fails, try to load it from the specified search paths:
27
+ if search_paths&.any?
28
+ name = FFI.map_library_name(name)
29
+
30
+ if path = ffi_find_library_path(name, search_paths)
31
+ return true if ffi_open_library(path, **options)
32
+ end
33
+ end
34
+
35
+ return nil
36
+ end
37
+
38
+ def ffi_load_failure(message)
39
+ raise LoadError, message
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2023, by Samuel Williams.
5
+
6
+ begin
7
+ require 'ffi'
8
+ rescue LoadError
9
+ # Ignore.
10
+
11
+ # The FFI gem has the following in `ffi.rb`, so we need to be careful about load order:
12
+ # Object.send(:remove_const, :FFI) if defined?(::FFI)
13
+ end
14
+
15
+ module FFI
16
+ module Native
17
+ VERSION = "0.4.0"
18
+ end
19
+ end
data/lib/ffi/native.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2021-2023, by Samuel Williams.
5
+
6
+ require_relative 'native/version'
7
+ require_relative 'native/library'
8
+ require_relative 'native/loader'
9
+
10
+ module FFI
11
+ module Native
12
+ def self.included(target)
13
+ target.extend(Library)
14
+ target.extend(Loader)
15
+ end
16
+ end
17
+ end
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2021-2023, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,35 @@
1
+ # FFI::Native
2
+
3
+ A modern interface for building foreign function interfaces for Ruby using the standard [ffi](https://github.com/ffi/ffi) gem.
4
+
5
+ [![Development Status](https://github.com/ioquatix/ffi-native/workflows/Test/badge.svg)](https://github.com/ioquatix/ffi-native/actions?workflow=Test)
6
+
7
+ ## Contributing
8
+
9
+ 1. Fork it
10
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
11
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
12
+ 4. Push to the branch (`git push origin my-new-feature`)
13
+ 5. Create new Pull Request
14
+
15
+ ## License
16
+
17
+ Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights
22
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ copies of the Software, and to permit persons to whom the Software is
24
+ furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in
27
+ all copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
35
+ THE SOFTWARE.
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffi-native
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
14
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
15
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
16
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
17
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
18
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
19
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
20
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
21
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
22
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
23
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
24
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
25
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
26
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
27
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
28
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
30
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
31
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
32
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
33
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
34
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
35
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
36
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
37
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
+ -----END CERTIFICATE-----
40
+ date: 2023-03-12 00:00:00.000000000 Z
41
+ dependencies:
42
+ - !ruby/object:Gem::Dependency
43
+ name: ffi
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: bake
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: bundler
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: covered
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: '0'
91
+ type: :development
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: sus
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '0.18'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '0.18'
112
+ description:
113
+ email:
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - lib/ffi/native.rb
119
+ - lib/ffi/native/config_tool.rb
120
+ - lib/ffi/native/library.rb
121
+ - lib/ffi/native/loader.rb
122
+ - lib/ffi/native/version.rb
123
+ - license.md
124
+ - readme.md
125
+ homepage: https://github.com/ioquatix/ffi-native
126
+ licenses:
127
+ - MIT
128
+ metadata:
129
+ funding_uri: https://github.com/sponsors/ioquatix/
130
+ post_install_message:
131
+ rdoc_options: []
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: 2.4.0
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ requirements: []
145
+ rubygems_version: 3.4.6
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Write a short summary, because RubyGems requires one.
149
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1,5 @@
1
+ +9��SP��}�7o����;t��Cd��R;r�6V泉�jGQpZ��3"�@?� ��W���.���,G:�7�S4QA��[�78%9��U=–���n�8�5�*�Dq�#�� ����u�m&�q]�����d��X�
2
+ Ґ�EVǔ��!DO&�� �=�K
3
+ �ϽX�9��qN0�� #�U�&e�}�Cp�0���"�+E0�W:�<3 ܘ�5���8~�'D������W���_��.:�xjh��RŊ������$�0�2w��g�Lu �g0h�V2N��k�
4
+ ����4絝$��0O���̜������p߇]x�`��~u��
5
+ �U�\^�~�