/
convert.rb
289 lines (272 loc) · 16.5 KB
/
convert.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# Protocol Buffers - Google's data interchange format
# Copyright 2022 Google Inc. All rights reserved.
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
##
# Implementation details below are subject to breaking changes without
# warning and are intended for use only within the gem.
module Google
module Protobuf
module Internal
module Convert
# Arena should be the
# @param value [Object] Value to convert
# @param arena [Arena] Arena that owns the Message where the MessageValue
# will be set
# @return [Google::Protobuf::FFI::MessageValue]
def convert_ruby_to_upb(value, arena, c_type, msg_or_enum_def)
raise ArgumentError.new "Expected Descriptor or EnumDescriptor, instead got #{msg_or_enum_def.class}" unless [NilClass, Descriptor, EnumDescriptor].include? msg_or_enum_def.class
return_value = Google::Protobuf::FFI::MessageValue.new
case c_type
when :float
raise TypeError.new "Expected number type for float field '#{name}' (given #{value.class})." unless value.respond_to? :to_f
return_value[:float_val] = value.to_f
when :double
raise TypeError.new "Expected number type for double field '#{name}' (given #{value.class})." unless value.respond_to? :to_f
return_value[:double_val] = value.to_f
when :bool
raise TypeError.new "Invalid argument for boolean field '#{name}' (given #{value.class})." unless [TrueClass, FalseClass].include? value.class
return_value[:bool_val] = value
when :string
raise TypeError.new "Invalid argument for string field '#{name}' (given #{value.class})." unless [Symbol, String].include? value.class
begin
string_value = value.to_s.encode("UTF-8")
rescue Encoding::UndefinedConversionError
# TODO - why not include the field name here?
raise Encoding::UndefinedConversionError.new "String is invalid UTF-8"
end
return_value[:str_val][:size] = string_value.bytesize
return_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, string_value.bytesize)
# TODO - how important is it to still use arena malloc, versus the following?
# buffer = ::FFI::MemoryPointer.new(:char, string_value.bytesize)
# buffer.put_bytes(0, string_value)
# return_value[:str_val][:data] = buffer
raise NoMemoryError.new "Cannot allocate #{string_value.bytesize} bytes for string on Arena" if return_value[:str_val][:data].nil? || return_value[:str_val][:data].null?
return_value[:str_val][:data].write_string(string_value)
when :bytes
raise TypeError.new "Invalid argument for bytes field '#{name}' (given #{value.class})." unless value.is_a? String
string_value = value.encode("ASCII-8BIT")
return_value[:str_val][:size] = string_value.bytesize
return_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, string_value.bytesize)
raise NoMemoryError.new "Cannot allocate #{string_value.bytesize} bytes for bytes on Arena" if return_value[:str_val][:data].nil? || return_value[:str_val][:data].null?
return_value[:str_val][:data].write_string_length(string_value, string_value.bytesize)
when :message
raise TypeError.new "nil message not allowed here." if value.nil?
if value.is_a? Hash
raise RuntimeError.new "Attempted to initialize message from Hash for field #{name} but have no definition" if msg_or_enum_def.nil?
new_message = msg_or_enum_def.msgclass.
send(:private_constructor, arena, initial_value: value)
return_value[:msg_val] = new_message.instance_variable_get(:@msg)
return return_value
end
descriptor = value.class.respond_to?(:descriptor) ? value.class.descriptor : nil
if descriptor != msg_or_enum_def
wkt = Google::Protobuf::FFI.get_well_known_type(msg_or_enum_def)
case wkt
when :Timestamp
raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'." unless value.kind_of? Time
new_message = Google::Protobuf::FFI.new_message_from_def msg_or_enum_def, arena
sec = Google::Protobuf::FFI::MessageValue.new
sec[:int64_val] = value.tv_sec
sec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 1
raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, sec_field_def, sec, arena
nsec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 2
nsec = Google::Protobuf::FFI::MessageValue.new
nsec[:int32_val] = value.tv_nsec
raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, nsec_field_def, nsec, arena
return_value[:msg_val] = new_message
when :Duration
raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'." unless value.kind_of? Numeric
new_message = Google::Protobuf::FFI.new_message_from_def msg_or_enum_def, arena
sec = Google::Protobuf::FFI::MessageValue.new
sec[:int64_val] = value
sec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 1
raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, sec_field_def, sec, arena
nsec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 2
nsec = Google::Protobuf::FFI::MessageValue.new
nsec[:int32_val] = ((value.to_f - value.to_i) * 1000000000).round
raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, nsec_field_def, nsec, arena
return_value[:msg_val] = new_message
else
raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'."
end
else
arena.fuse(value.instance_variable_get(:@arena))
return_value[:msg_val] = value.instance_variable_get :@msg
end
when :enum
return_value[:int32_val] = case value
when Numeric
value.to_i
when String, Symbol
enum_number = EnumDescriptor.send(:lookup_name, msg_or_enum_def, value.to_s)
#TODO add the bad value to the error message after tests pass
raise RangeError.new "Unknown symbol value for enum field '#{name}'." if enum_number.nil?
enum_number
else
raise TypeError.new "Expected number or symbol type for enum field '#{name}'."
end
#TODO After all tests pass, improve error message across integer type by including actual offending value
when :int32
raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
raise RangeError.new "Value assigned to int32 field '#{name}' (given #{value.class}) with more than 32-bits." unless value.to_i.bit_length < 32
return_value[:int32_val] = value.to_i
when :uint32
raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
raise RangeError.new "Assigning negative value to unsigned integer field '#{name}' (given #{value.class})." if value < 0
raise RangeError.new "Value assigned to uint32 field '#{name}' (given #{value.class}) with more than 32-bits." unless value.to_i.bit_length < 33
return_value[:uint32_val] = value.to_i
when :int64
raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
raise RangeError.new "Value assigned to int64 field '#{name}' (given #{value.class}) with more than 64-bits." unless value.to_i.bit_length < 64
return_value[:int64_val] = value.to_i
when :uint64
raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
raise RangeError.new "Assigning negative value to unsigned integer field '#{name}' (given #{value.class})." if value < 0
raise RangeError.new "Value assigned to uint64 field '#{name}' (given #{value.class}) with more than 64-bits." unless value.to_i.bit_length < 65
return_value[:uint64_val] = value.to_i
else
raise RuntimeError.new "Unsupported type #{c_type}"
end
return_value
end
##
# Safe to call without an arena if the caller has checked that c_type
# is not :message.
# @param message_value [Google::Protobuf::FFI::MessageValue] Value to be converted.
# @param c_type [Google::Protobuf::FFI::CType] Enum representing the type of message_value
# @param msg_or_enum_def [::FFI::Pointer] Pointer to the MsgDef or EnumDef definition
# @param arena [Google::Protobuf::Internal::Arena] Arena to create Message instances, if needed
def convert_upb_to_ruby(message_value, c_type, msg_or_enum_def = nil, arena = nil)
throw TypeError.new "Expected MessageValue but got #{message_value.class}" unless message_value.is_a? Google::Protobuf::FFI::MessageValue
case c_type
when :bool
message_value[:bool_val]
when :int32
message_value[:int32_val]
when :uint32
message_value[:uint32_val]
when :double
message_value[:double_val]
when :int64
message_value[:int64_val]
when :uint64
message_value[:uint64_val]
when :string
if message_value[:str_val][:size].zero?
""
else
message_value[:str_val][:data].read_string_length(message_value[:str_val][:size]).force_encoding("UTF-8").freeze
end
when :bytes
if message_value[:str_val][:size].zero?
""
else
message_value[:str_val][:data].read_string_length(message_value[:str_val][:size]).force_encoding("ASCII-8BIT").freeze
end
when :float
message_value[:float_val]
when :enum
EnumDescriptor.send(:lookup_value, msg_or_enum_def, message_value[:int32_val]) || message_value[:int32_val]
when :message
raise "Null Arena for message" if arena.nil?
Descriptor.send(:get_message, message_value[:msg_val], msg_or_enum_def, arena)
else
raise RuntimeError.new "Unexpected type #{c_type}"
end
end
def to_h_internal(msg, message_descriptor)
return nil if msg.nil? or msg.null?
hash = {}
iter = ::FFI::MemoryPointer.new(:size_t, 1)
iter.write(:size_t, Google::Protobuf::FFI::Upb_Message_Begin)
message_value = Google::Protobuf::FFI::MessageValue.new
field_def_ptr = ::FFI::MemoryPointer.new :pointer
while Google::Protobuf::FFI::message_next(msg, message_descriptor, nil, field_def_ptr, message_value, iter) do
field_descriptor = FieldDescriptor.from_native field_def_ptr.get_pointer(0)
if field_descriptor.map?
hash_entry = map_create_hash(message_value[:map_val], field_descriptor)
elsif field_descriptor.repeated?
hash_entry = repeated_field_create_array(message_value[:array_val], field_descriptor, field_descriptor.type)
else
hash_entry = scalar_create_hash(message_value, field_descriptor.type, field_descriptor: field_descriptor)
end
hash[field_descriptor.name.to_sym] = hash_entry
end
hash
end
def map_create_hash(map_ptr, field_descriptor)
return {} if map_ptr.nil? or map_ptr.null?
return_value = {}
message_descriptor = field_descriptor.send(:subtype)
key_field_def = Google::Protobuf::FFI.get_field_by_number(message_descriptor, 1)
key_field_type = Google::Protobuf::FFI.get_type(key_field_def)
value_field_def = Google::Protobuf::FFI.get_field_by_number(message_descriptor, 2)
value_field_type = Google::Protobuf::FFI.get_type(value_field_def)
iter = ::FFI::MemoryPointer.new(:size_t, 1)
iter.write(:size_t, Google::Protobuf::FFI::Upb_Map_Begin)
while Google::Protobuf::FFI.map_next(map_ptr, iter) do
iter_size_t = iter.read(:size_t)
key_message_value = Google::Protobuf::FFI.map_key(map_ptr, iter_size_t)
value_message_value = Google::Protobuf::FFI.map_value(map_ptr, iter_size_t)
hash_key = convert_upb_to_ruby(key_message_value, key_field_type)
hash_value = scalar_create_hash(value_message_value, value_field_type, msg_or_enum_descriptor: value_field_def.subtype)
return_value[hash_key] = hash_value
end
return_value
end
def repeated_field_create_array(array, field_descriptor, type)
return_value = []
n = (array.nil? || array.null?) ? 0 : Google::Protobuf::FFI.array_size(array)
0.upto(n - 1) do |i|
message_value = Google::Protobuf::FFI.get_msgval_at(array, i)
return_value << scalar_create_hash(message_value, type, field_descriptor: field_descriptor)
end
return_value
end
# @param field_descriptor [FieldDescriptor] Descriptor of the field to convert to a hash.
def scalar_create_hash(message_value, type, field_descriptor: nil, msg_or_enum_descriptor: nil)
if [:message, :enum].include? type
if field_descriptor.nil?
if msg_or_enum_descriptor.nil?
raise "scalar_create_hash requires either a FieldDescriptor, MessageDescriptor, or EnumDescriptor as an argument, but received only nil"
end
else
msg_or_enum_descriptor = field_descriptor.subtype
end
if type == :message
to_h_internal(message_value[:msg_val], msg_or_enum_descriptor)
elsif type == :enum
convert_upb_to_ruby message_value, type, msg_or_enum_descriptor
end
else
convert_upb_to_ruby message_value, type
end
end
def message_value_deep_copy(message_value, type, descriptor, arena)
raise unless message_value.is_a? Google::Protobuf::FFI::MessageValue
new_message_value = Google::Protobuf::FFI::MessageValue.new
case type
when :string, :bytes
# TODO - how important is it to still use arena malloc, versus using FFI MemoryPointers?
new_message_value[:str_val][:size] = message_value[:str_val][:size]
new_message_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, message_value[:str_val][:size])
raise NoMemoryError.new "Allocation failed" if new_message_value[:str_val][:data].nil? or new_message_value[:str_val][:data].null?
Google::Protobuf::FFI.memcpy(new_message_value[:str_val][:data], message_value[:str_val][:data], message_value[:str_val][:size])
when :message
new_message_value[:msg_val] = descriptor.msgclass.send(:deep_copy, message_value[:msg_val], arena).instance_variable_get(:@msg)
else
Google::Protobuf::FFI.memcpy(new_message_value.to_ptr, message_value.to_ptr, Google::Protobuf::FFI::MessageValue.size)
end
new_message_value
end
end
end
end
end