messagepack 1.0.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 +7 -0
- data/README.adoc +773 -0
- data/Rakefile +8 -0
- data/docs/Gemfile +7 -0
- data/docs/README.md +85 -0
- data/docs/_config.yml +137 -0
- data/docs/_guides/index.adoc +14 -0
- data/docs/_guides/io-streaming.adoc +226 -0
- data/docs/_guides/migration.adoc +218 -0
- data/docs/_guides/performance.adoc +189 -0
- data/docs/_pages/buffer.adoc +85 -0
- data/docs/_pages/extension-types.adoc +117 -0
- data/docs/_pages/factory-pattern.adoc +115 -0
- data/docs/_pages/index.adoc +20 -0
- data/docs/_pages/serialization.adoc +159 -0
- data/docs/_pages/streaming.adoc +97 -0
- data/docs/_pages/symbol-extension.adoc +69 -0
- data/docs/_pages/timestamp-extension.adoc +88 -0
- data/docs/_references/api.adoc +360 -0
- data/docs/_references/extensions.adoc +198 -0
- data/docs/_references/format.adoc +301 -0
- data/docs/_references/index.adoc +14 -0
- data/docs/_tutorials/extension-types.adoc +170 -0
- data/docs/_tutorials/getting-started.adoc +165 -0
- data/docs/_tutorials/index.adoc +14 -0
- data/docs/_tutorials/thread-safety.adoc +157 -0
- data/docs/index.adoc +77 -0
- data/docs/lychee.toml +42 -0
- data/lib/messagepack/bigint.rb +131 -0
- data/lib/messagepack/buffer.rb +534 -0
- data/lib/messagepack/core_ext.rb +34 -0
- data/lib/messagepack/error.rb +24 -0
- data/lib/messagepack/extensions/base.rb +55 -0
- data/lib/messagepack/extensions/registry.rb +154 -0
- data/lib/messagepack/extensions/symbol.rb +38 -0
- data/lib/messagepack/extensions/timestamp.rb +110 -0
- data/lib/messagepack/extensions/value.rb +38 -0
- data/lib/messagepack/factory.rb +349 -0
- data/lib/messagepack/format.rb +99 -0
- data/lib/messagepack/packer.rb +702 -0
- data/lib/messagepack/symbol.rb +4 -0
- data/lib/messagepack/time.rb +29 -0
- data/lib/messagepack/timestamp.rb +4 -0
- data/lib/messagepack/unpacker.rb +1418 -0
- data/lib/messagepack/version.rb +5 -0
- data/lib/messagepack.rb +81 -0
- metadata +94 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'buffer'
|
|
4
|
+
require_relative 'format'
|
|
5
|
+
|
|
6
|
+
module Messagepack
|
|
7
|
+
# Packer serializes Ruby objects into MessagePack binary format.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# packer = Messagepack::Packer.new
|
|
11
|
+
# packer.write("hello")
|
|
12
|
+
# packer.write([1, 2, 3])
|
|
13
|
+
# data = packer.full_pack
|
|
14
|
+
#
|
|
15
|
+
class Packer
|
|
16
|
+
# Disable dup and clone as they have weird semantics
|
|
17
|
+
undef_method :dup
|
|
18
|
+
undef_method :clone
|
|
19
|
+
|
|
20
|
+
attr_reader :buffer, :compatibility_mode, :frozen
|
|
21
|
+
|
|
22
|
+
# Predicate method for compatibility_mode
|
|
23
|
+
def compatibility_mode?
|
|
24
|
+
@compatibility_mode
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Private factory method for creating a packer with specific internal state
|
|
29
|
+
# Used for recursive extension serialization to avoid fragile metaprogramming
|
|
30
|
+
#
|
|
31
|
+
# @param buffer [BinaryBuffer] The buffer to use
|
|
32
|
+
# @param ext_registry [ExtensionRegistry::Packer] The extension registry to use
|
|
33
|
+
# @return [Packer] A new packer with the specified buffer and registry
|
|
34
|
+
#
|
|
35
|
+
def create_for_recursion(buffer, ext_registry)
|
|
36
|
+
packer = allocate
|
|
37
|
+
packer.instance_variable_set(:@buffer, buffer)
|
|
38
|
+
packer.instance_variable_set(:@compatibility_mode, false)
|
|
39
|
+
packer.instance_variable_set(:@ext_registry, ext_registry)
|
|
40
|
+
packer.instance_variable_set(:@to_msgpack_method, :to_msgpack)
|
|
41
|
+
packer.instance_variable_set(:@to_msgpack_arg, packer)
|
|
42
|
+
packer.instance_variable_set(:@frozen, false)
|
|
43
|
+
packer
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# int64 bounds for oversized integer extension checking
|
|
48
|
+
INT64_MIN = -2**63
|
|
49
|
+
INT64_MAX = 2**63 - 1
|
|
50
|
+
private_constant :INT64_MIN, :INT64_MAX
|
|
51
|
+
|
|
52
|
+
def initialize(io = nil, options = nil)
|
|
53
|
+
# Handle various initialization patterns:
|
|
54
|
+
# Packer.new
|
|
55
|
+
# Packer.new(io)
|
|
56
|
+
# Packer.new(options_hash)
|
|
57
|
+
# Packer.new(io, options_hash)
|
|
58
|
+
|
|
59
|
+
compatibility_mode = false
|
|
60
|
+
|
|
61
|
+
if io.is_a?(Hash)
|
|
62
|
+
# Packer.new({}) or Packer.new(options_hash)
|
|
63
|
+
options = io
|
|
64
|
+
io = nil # Reset io to nil since the first arg is options
|
|
65
|
+
io = options[:io] if options.key?(:io)
|
|
66
|
+
compatibility_mode = options[:compatibility_mode] if options.key?(:compatibility_mode)
|
|
67
|
+
elsif options.is_a?(Hash)
|
|
68
|
+
# Packer.new(io, options_hash) or Packer.new(StringIO.new, {})
|
|
69
|
+
compatibility_mode = options[:compatibility_mode] if options.key?(:compatibility_mode)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@buffer = BinaryBuffer.new(io)
|
|
73
|
+
@compatibility_mode = compatibility_mode
|
|
74
|
+
@ext_registry = ExtensionRegistry::Packer.new
|
|
75
|
+
@to_msgpack_method = :to_msgpack
|
|
76
|
+
@to_msgpack_arg = self
|
|
77
|
+
@frozen = false # Custom frozen flag for pool
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Set the extension registry for this packer.
|
|
81
|
+
# Used internally by Factory to inject custom type registrations.
|
|
82
|
+
#
|
|
83
|
+
# @param registry [ExtensionRegistry::Packer] The extension registry to use
|
|
84
|
+
#
|
|
85
|
+
def extension_registry=(registry)
|
|
86
|
+
@ext_registry = registry
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Mark this packer as frozen for pool use.
|
|
90
|
+
# Prevents type registration when used from a pool.
|
|
91
|
+
#
|
|
92
|
+
def freeze_for_pool
|
|
93
|
+
@frozen = true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Main API: Write any Ruby object
|
|
97
|
+
def write(value)
|
|
98
|
+
# Fast-path: Skip registry lookup for native types UNLESS they have custom registration
|
|
99
|
+
# Native types with custom extensions should still use the registry
|
|
100
|
+
check_registry = !native_type?(value) || @ext_registry.type_registered?(value.class)
|
|
101
|
+
|
|
102
|
+
if check_registry
|
|
103
|
+
# First check if this type is registered as an extension
|
|
104
|
+
data = @ext_registry.lookup(value)
|
|
105
|
+
if data
|
|
106
|
+
type_id, packer_proc, flags = data
|
|
107
|
+
# Convert Symbol to Proc if needed
|
|
108
|
+
proc = packer_proc.is_a?(Symbol) ? ->(obj) { obj.send(packer_proc) } : packer_proc
|
|
109
|
+
|
|
110
|
+
# Handle oversized_integer_extension for Integer
|
|
111
|
+
if value.is_a?(Integer)
|
|
112
|
+
if flags & 0x02 != 0
|
|
113
|
+
# oversized_integer_extension flag is set
|
|
114
|
+
if value >= INT64_MIN && value <= INT64_MAX
|
|
115
|
+
# Fits in native int64 format, use native serialization
|
|
116
|
+
# Fall through to standard serialization below
|
|
117
|
+
data = nil
|
|
118
|
+
else
|
|
119
|
+
# Too large, use the extension packer
|
|
120
|
+
payload = proc.call(value)
|
|
121
|
+
write_extension_direct(type_id, payload)
|
|
122
|
+
return self
|
|
123
|
+
end
|
|
124
|
+
else
|
|
125
|
+
# Integer is registered but oversized_integer_extension flag is NOT set
|
|
126
|
+
# Ignore the extension and use standard serialization
|
|
127
|
+
# This allows big integers to raise RangeError as expected
|
|
128
|
+
data = nil
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
if data
|
|
133
|
+
# For recursive packers, pass self as second argument
|
|
134
|
+
if flags & 0x01 != 0
|
|
135
|
+
# Recursive packer - create a temporary buffer to capture the output
|
|
136
|
+
temp_buffer = BinaryBuffer.new
|
|
137
|
+
temp_packer = self.class.create_for_recursion(temp_buffer, @ext_registry)
|
|
138
|
+
proc.call(value, temp_packer)
|
|
139
|
+
payload = temp_buffer.to_s
|
|
140
|
+
write_extension_direct(type_id, payload)
|
|
141
|
+
else
|
|
142
|
+
# Non-recursive - get the payload and write it as extension
|
|
143
|
+
payload = proc.call(value)
|
|
144
|
+
write_extension_direct(type_id, payload)
|
|
145
|
+
end
|
|
146
|
+
return self
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Use standard serialization
|
|
152
|
+
case value
|
|
153
|
+
when NilClass then write_nil_internal
|
|
154
|
+
when TrueClass then write_true_internal
|
|
155
|
+
when FalseClass then write_false_internal
|
|
156
|
+
when Integer then write_integer_internal(value)
|
|
157
|
+
when Float then write_float_internal(value)
|
|
158
|
+
when String
|
|
159
|
+
# Check if string is binary (ASCII-8BIT) vs UTF-8 text
|
|
160
|
+
if value.encoding == Encoding::ASCII_8BIT
|
|
161
|
+
write_binary_internal(value)
|
|
162
|
+
else
|
|
163
|
+
write_string_internal(value)
|
|
164
|
+
end
|
|
165
|
+
when Symbol then write_symbol_internal(value)
|
|
166
|
+
when Array then write_array_internal(value)
|
|
167
|
+
when Hash then write_hash_internal(value)
|
|
168
|
+
else
|
|
169
|
+
write_extension_internal(value)
|
|
170
|
+
end
|
|
171
|
+
self
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
alias pack write
|
|
175
|
+
|
|
176
|
+
# Flush buffer to IO if present
|
|
177
|
+
def flush
|
|
178
|
+
@buffer.flush
|
|
179
|
+
self
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Return packed string and reset buffer
|
|
183
|
+
def full_pack
|
|
184
|
+
flush if @buffer.io
|
|
185
|
+
result = @buffer.to_s
|
|
186
|
+
reset
|
|
187
|
+
@buffer.io ? nil : result
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Return current packed string without resetting buffer
|
|
191
|
+
def to_s
|
|
192
|
+
@buffer.io ? '' : @buffer.to_s
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
alias to_str to_s
|
|
196
|
+
|
|
197
|
+
# Reset buffer state
|
|
198
|
+
def reset
|
|
199
|
+
@buffer.reset
|
|
200
|
+
@frozen = false
|
|
201
|
+
self
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Public API for custom to_msgpack implementations
|
|
205
|
+
|
|
206
|
+
def write_nil
|
|
207
|
+
write_nil_internal
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def write_true
|
|
212
|
+
write_true_internal
|
|
213
|
+
self
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def write_false
|
|
217
|
+
write_false_internal
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def write_integer(value)
|
|
222
|
+
raise ::TypeError, "value must be an Integer" unless value.is_a?(Integer)
|
|
223
|
+
write_integer_internal(value)
|
|
224
|
+
self
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def write_float(value)
|
|
228
|
+
raise ::TypeError, "value must be numeric" unless value.is_a?(Numeric)
|
|
229
|
+
write_float_internal(value)
|
|
230
|
+
self
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def write_float32(value)
|
|
234
|
+
raise ArgumentError, "value must be numeric" unless value.is_a?(Numeric)
|
|
235
|
+
@buffer.write_byte(Format::FLOAT32)
|
|
236
|
+
@buffer.write_float32(value)
|
|
237
|
+
self
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def write_string(value)
|
|
241
|
+
raise ::TypeError, "value must be a String" unless value.is_a?(String)
|
|
242
|
+
write_string_internal(value)
|
|
243
|
+
self
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def write_binary(value)
|
|
247
|
+
raise ::TypeError, "value must be a String" unless value.is_a?(String)
|
|
248
|
+
write_binary_internal(value)
|
|
249
|
+
self
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def write_bin(value)
|
|
253
|
+
write_binary(value)
|
|
254
|
+
self
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def write_bin_header(length)
|
|
258
|
+
write_binary_header(length)
|
|
259
|
+
self
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def write_array_header(count)
|
|
263
|
+
raise ::TypeError, "count must be an Integer" unless count.is_a?(Integer)
|
|
264
|
+
write_array_header_internal(count)
|
|
265
|
+
self
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def write_map_header(count)
|
|
269
|
+
raise ::TypeError, "count must be an Integer" unless count.is_a?(Integer)
|
|
270
|
+
write_map_header_internal(count)
|
|
271
|
+
self
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def write_extension(type, payload = nil)
|
|
275
|
+
# The test calls write_extension("hello") with one argument and expects TypeError
|
|
276
|
+
# because the first argument should be an Integer type ID, not a String
|
|
277
|
+
raise ::TypeError, "type must be an Integer" unless type.is_a?(Integer)
|
|
278
|
+
|
|
279
|
+
# Validate type range (-128 to 127)
|
|
280
|
+
unless type >= -128 && type <= 127
|
|
281
|
+
raise RangeError, "type must be -128..127 but got #{type.inspect}"
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
if payload.nil?
|
|
285
|
+
# Called with (type, payload) form where payload is yielded
|
|
286
|
+
payload = yield if block_given?
|
|
287
|
+
write_extension_direct(type, payload)
|
|
288
|
+
else
|
|
289
|
+
write_extension_direct(type, payload)
|
|
290
|
+
end
|
|
291
|
+
self
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Additional convenience methods not in the core API
|
|
295
|
+
def write_array(value)
|
|
296
|
+
raise ::TypeError, "value must be an Array" unless value.is_a?(Array)
|
|
297
|
+
write_array_internal(value)
|
|
298
|
+
self
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def write_hash(value)
|
|
302
|
+
raise ::TypeError, "value must be a Hash" unless value.is_a?(Hash)
|
|
303
|
+
write_hash_internal(value)
|
|
304
|
+
self
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def write_symbol(value)
|
|
308
|
+
raise ::TypeError, "value must be a Symbol" unless value.is_a?(Symbol)
|
|
309
|
+
write_symbol_internal(value)
|
|
310
|
+
self
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def write_int(value)
|
|
314
|
+
write_integer(value)
|
|
315
|
+
self
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Extension type registration
|
|
319
|
+
|
|
320
|
+
def register_type(type_id, klass, packer_proc = nil, &block)
|
|
321
|
+
# Handle multiple calling patterns:
|
|
322
|
+
# register_type(type_id, klass) { |obj| ... }
|
|
323
|
+
# register_type(type_id, klass, :method_name)
|
|
324
|
+
# register_type(type_id, klass, proc)
|
|
325
|
+
# register_type(type_id, klass, &:method)
|
|
326
|
+
|
|
327
|
+
raise FrozenError, "can't modify frozen Messagepack::Packer" if @frozen
|
|
328
|
+
|
|
329
|
+
if block_given?
|
|
330
|
+
packer_proc = block
|
|
331
|
+
elsif packer_proc.is_a?(Symbol)
|
|
332
|
+
# Convert symbol to proc (use a local variable to avoid capture issues)
|
|
333
|
+
method_name = packer_proc
|
|
334
|
+
packer_proc = ->(obj) { obj.send(method_name) }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
@ext_registry.register(type_id, klass, packer_proc)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def registered_types
|
|
341
|
+
@ext_registry.registered_types
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def type_registered?(klass_or_type)
|
|
345
|
+
@ext_registry.type_registered?(klass_or_type)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
private
|
|
349
|
+
|
|
350
|
+
# Check if a value is a native MessagePack type that doesn't need registry lookup
|
|
351
|
+
# Native types are: nil, true, false, Integer, Float, String, Symbol, Array, Hash
|
|
352
|
+
# Time is handled separately (it's registered as an extension)
|
|
353
|
+
def native_type?(value)
|
|
354
|
+
case value
|
|
355
|
+
when NilClass, TrueClass, FalseClass, Integer, Float, String, Symbol, Array, Hash
|
|
356
|
+
true
|
|
357
|
+
else
|
|
358
|
+
false
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Internal type dispatch
|
|
363
|
+
|
|
364
|
+
def write_nil_internal
|
|
365
|
+
@buffer.write_byte(Format::NIL)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def write_true_internal
|
|
369
|
+
@buffer.write_byte(Format::TRUE)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def write_false_internal
|
|
373
|
+
@buffer.write_byte(Format::FALSE)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def write_integer_internal(value)
|
|
377
|
+
if value >= 0
|
|
378
|
+
write_positive_integer(value)
|
|
379
|
+
else
|
|
380
|
+
write_negative_integer(value)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def write_positive_integer(value)
|
|
385
|
+
if value <= Format::POSITIVE_FIXNUM_MAX
|
|
386
|
+
write_fixint(value)
|
|
387
|
+
elsif value <= 0xff
|
|
388
|
+
write_uint8(value)
|
|
389
|
+
elsif value <= 0xffff
|
|
390
|
+
write_uint16(value)
|
|
391
|
+
elsif value <= 0xffffffff
|
|
392
|
+
write_uint32(value)
|
|
393
|
+
elsif value <= 0xffffffffffffffff
|
|
394
|
+
write_uint64(value)
|
|
395
|
+
else
|
|
396
|
+
raise RangeError, "integer too large for MessagePack: #{value}"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def write_negative_integer(value)
|
|
401
|
+
if value >= -32
|
|
402
|
+
write_fixint(value)
|
|
403
|
+
elsif value >= -0x80
|
|
404
|
+
write_int8(value)
|
|
405
|
+
elsif value >= -0x8000
|
|
406
|
+
write_int16(value)
|
|
407
|
+
elsif value >= -0x80000000
|
|
408
|
+
write_int32(value)
|
|
409
|
+
elsif value >= -0x8000000000000000
|
|
410
|
+
write_int64(value)
|
|
411
|
+
else
|
|
412
|
+
raise RangeError, "integer too small for MessagePack: #{value}"
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def write_bignum_internal(value)
|
|
417
|
+
# Handle big integers outside 64-bit range
|
|
418
|
+
# Convert to binary representation
|
|
419
|
+
num_bytes = (value.abs.bit_length + 7) / 8
|
|
420
|
+
is_negative = value < 0
|
|
421
|
+
|
|
422
|
+
if is_negative
|
|
423
|
+
# For negative numbers, store as two's complement
|
|
424
|
+
# We need to handle the sign properly
|
|
425
|
+
abs_value = value.abs
|
|
426
|
+
data = []
|
|
427
|
+
remaining = abs_value
|
|
428
|
+
while remaining > 0
|
|
429
|
+
data << (remaining & 0xff)
|
|
430
|
+
remaining >>= 8
|
|
431
|
+
end
|
|
432
|
+
# Two's complement: invert and add 1
|
|
433
|
+
data = data.map { |b| (~b) & 0xff }
|
|
434
|
+
i = 0
|
|
435
|
+
while i < data.length && data[i] == 0
|
|
436
|
+
i += 1
|
|
437
|
+
end
|
|
438
|
+
if i < data.length
|
|
439
|
+
data[i] += 1
|
|
440
|
+
else
|
|
441
|
+
data << 1
|
|
442
|
+
end
|
|
443
|
+
payload = data.pack('C*')
|
|
444
|
+
else
|
|
445
|
+
# Positive number - big-endian bytes
|
|
446
|
+
abs_value = value
|
|
447
|
+
data = []
|
|
448
|
+
while abs_value > 0
|
|
449
|
+
data.unshift(abs_value & 0xff)
|
|
450
|
+
abs_value >>= 8
|
|
451
|
+
end
|
|
452
|
+
payload = data.pack('C*')
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Use bin format for raw binary data
|
|
456
|
+
write_binary_header(payload.bytesize)
|
|
457
|
+
@buffer.write_bytes(payload)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def write_float_internal(value)
|
|
461
|
+
@buffer.write_byte(Format::FLOAT64)
|
|
462
|
+
@buffer.write_float64(value)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def write_string_internal(value)
|
|
466
|
+
data = utf8_compatible?(value) ? value : transcode_to_utf8(value)
|
|
467
|
+
write_string_header_internal(data.bytesize)
|
|
468
|
+
@buffer.write_bytes(data)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def write_symbol_internal(value)
|
|
472
|
+
# Symbols are encoded as strings
|
|
473
|
+
# If the symbol was created from a binary string, encode as bin
|
|
474
|
+
data = value.to_s
|
|
475
|
+
if data.encoding == Encoding::ASCII_8BIT
|
|
476
|
+
write_binary_internal(data)
|
|
477
|
+
else
|
|
478
|
+
write_string_internal(data)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def write_array_internal(value)
|
|
483
|
+
write_array_header_internal(value.size)
|
|
484
|
+
value.each { |item| write(item) }
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def write_hash_internal(value)
|
|
488
|
+
write_map_header_internal(value.size)
|
|
489
|
+
value.each { |k, v| write(k); write(v) }
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def write_binary_internal(value)
|
|
493
|
+
write_binary_header(value.bytesize)
|
|
494
|
+
@buffer.write_bytes(value)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def write_extension_internal(value)
|
|
498
|
+
if value.is_a?(Integer)
|
|
499
|
+
# type, payload form
|
|
500
|
+
type = value
|
|
501
|
+
payload = yield if block_given?
|
|
502
|
+
write_extension_direct(type, payload)
|
|
503
|
+
return self
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Try extension registry
|
|
507
|
+
type_id, packer_proc = @ext_registry.lookup(value)
|
|
508
|
+
|
|
509
|
+
if type_id
|
|
510
|
+
payload = packer_proc.call(value)
|
|
511
|
+
write_extension_direct(type_id, payload)
|
|
512
|
+
elsif value.respond_to?(@to_msgpack_method)
|
|
513
|
+
# Use to_msgpack if available
|
|
514
|
+
value.send(@to_msgpack_method, @to_msgpack_arg)
|
|
515
|
+
else
|
|
516
|
+
# Try to call to_msgpack and let NoMethodError propagate
|
|
517
|
+
value.send(@to_msgpack_method, @to_msgpack_arg)
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def write_extension_direct(type, payload)
|
|
522
|
+
length = payload.bytesize
|
|
523
|
+
|
|
524
|
+
case length
|
|
525
|
+
when 1
|
|
526
|
+
@buffer.write_byte(Format::FIXEXT1)
|
|
527
|
+
@buffer.write_byte(type)
|
|
528
|
+
@buffer.write_bytes(payload)
|
|
529
|
+
when 2
|
|
530
|
+
@buffer.write_byte(Format::FIXEXT2)
|
|
531
|
+
@buffer.write_byte(type)
|
|
532
|
+
@buffer.write_bytes(payload)
|
|
533
|
+
when 4
|
|
534
|
+
@buffer.write_byte(Format::FIXEXT4)
|
|
535
|
+
@buffer.write_byte(type)
|
|
536
|
+
@buffer.write_bytes(payload)
|
|
537
|
+
when 8
|
|
538
|
+
@buffer.write_byte(Format::FIXEXT8)
|
|
539
|
+
@buffer.write_byte(type)
|
|
540
|
+
@buffer.write_bytes(payload)
|
|
541
|
+
when 16
|
|
542
|
+
@buffer.write_byte(Format::FIXEXT16)
|
|
543
|
+
@buffer.write_byte(type)
|
|
544
|
+
@buffer.write_bytes(payload)
|
|
545
|
+
else
|
|
546
|
+
if length <= 0xff
|
|
547
|
+
@buffer.write_byte(Format::EXT8)
|
|
548
|
+
@buffer.write_byte(length)
|
|
549
|
+
@buffer.write_byte(type)
|
|
550
|
+
elsif length <= 0xffff
|
|
551
|
+
@buffer.write_byte(Format::EXT16)
|
|
552
|
+
@buffer.write_big_endian_uint16(length)
|
|
553
|
+
@buffer.write_byte(type)
|
|
554
|
+
else
|
|
555
|
+
@buffer.write_byte(Format::EXT32)
|
|
556
|
+
@buffer.write_big_endian_uint32(length)
|
|
557
|
+
@buffer.write_byte(type)
|
|
558
|
+
end
|
|
559
|
+
@buffer.write_bytes(payload)
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Format helpers
|
|
564
|
+
|
|
565
|
+
def write_fixint(value)
|
|
566
|
+
# Convert to unsigned byte representation
|
|
567
|
+
byte = value & 0xff
|
|
568
|
+
@buffer.write_byte(byte)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def write_uint8(value)
|
|
572
|
+
@buffer.write_byte(Format::UINT8)
|
|
573
|
+
@buffer.write_byte(value)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def write_uint16(value)
|
|
577
|
+
@buffer.write_byte(Format::UINT16)
|
|
578
|
+
@buffer.write_big_endian_uint16(value)
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def write_uint32(value)
|
|
582
|
+
@buffer.write_byte(Format::UINT32)
|
|
583
|
+
@buffer.write_big_endian_uint32(value)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def write_uint64(value)
|
|
587
|
+
@buffer.write_byte(Format::UINT64)
|
|
588
|
+
@buffer.write_big_endian_uint64(value)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def write_int8(value)
|
|
592
|
+
@buffer.write_byte(Format::INT8)
|
|
593
|
+
@buffer.write_byte(value & 0xff)
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def write_int16(value)
|
|
597
|
+
@buffer.write_byte(Format::INT16)
|
|
598
|
+
@buffer.write_big_endian_uint16(value & 0xffff)
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def write_int32(value)
|
|
602
|
+
@buffer.write_byte(Format::INT32)
|
|
603
|
+
@buffer.write_big_endian_uint32(value & 0xffffffff)
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def write_int64(value)
|
|
607
|
+
@buffer.write_byte(Format::INT64)
|
|
608
|
+
@buffer.write_big_endian_int64(value)
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def write_string_header_internal(length)
|
|
612
|
+
case length
|
|
613
|
+
when 0..31
|
|
614
|
+
@buffer.write_byte(Format::FIXRAW_MIN | length)
|
|
615
|
+
when 0..0xff
|
|
616
|
+
# In compatibility mode, skip str8 and use str16 directly
|
|
617
|
+
if @compatibility_mode
|
|
618
|
+
@buffer.write_byte(Format::STR16)
|
|
619
|
+
@buffer.write_big_endian_uint16(length)
|
|
620
|
+
else
|
|
621
|
+
@buffer.write_byte(Format::STR8)
|
|
622
|
+
@buffer.write_byte(length)
|
|
623
|
+
end
|
|
624
|
+
when 0..0xffff
|
|
625
|
+
@buffer.write_byte(Format::STR16)
|
|
626
|
+
@buffer.write_big_endian_uint16(length)
|
|
627
|
+
when 0..0xffffffff
|
|
628
|
+
@buffer.write_byte(Format::STR32)
|
|
629
|
+
@buffer.write_big_endian_uint32(length)
|
|
630
|
+
else
|
|
631
|
+
raise Error, "String too large: #{length} bytes"
|
|
632
|
+
end
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
def write_binary_header(length)
|
|
636
|
+
if @compatibility_mode
|
|
637
|
+
# In compatibility mode, use str format instead of bin format
|
|
638
|
+
write_string_header_internal(length)
|
|
639
|
+
else
|
|
640
|
+
case length
|
|
641
|
+
when 0..0xff
|
|
642
|
+
@buffer.write_byte(Format::BIN8)
|
|
643
|
+
@buffer.write_byte(length)
|
|
644
|
+
when 0..0xffff
|
|
645
|
+
@buffer.write_byte(Format::BIN16)
|
|
646
|
+
@buffer.write_big_endian_uint16(length)
|
|
647
|
+
when 0..0xffffffff
|
|
648
|
+
@buffer.write_byte(Format::BIN32)
|
|
649
|
+
@buffer.write_big_endian_uint32(length)
|
|
650
|
+
else
|
|
651
|
+
raise Error, "Binary too large: #{length} bytes"
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def write_array_header_internal(count)
|
|
657
|
+
case count
|
|
658
|
+
when 0..15
|
|
659
|
+
@buffer.write_byte(Format::FIXARRAY_MIN | count)
|
|
660
|
+
when 0..0xffff
|
|
661
|
+
@buffer.write_byte(Format::ARRAY16)
|
|
662
|
+
@buffer.write_big_endian_uint16(count)
|
|
663
|
+
when 0..0xffffffff
|
|
664
|
+
@buffer.write_byte(Format::ARRAY32)
|
|
665
|
+
@buffer.write_big_endian_uint32(count)
|
|
666
|
+
else
|
|
667
|
+
raise Error, "Array too large: #{count} elements"
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def write_map_header_internal(count)
|
|
672
|
+
case count
|
|
673
|
+
when 0..15
|
|
674
|
+
@buffer.write_byte(Format::FIXMAP_MIN | count)
|
|
675
|
+
when 0..0xffff
|
|
676
|
+
@buffer.write_byte(Format::MAP16)
|
|
677
|
+
@buffer.write_big_endian_uint16(count)
|
|
678
|
+
when 0..0xffffffff
|
|
679
|
+
@buffer.write_byte(Format::MAP32)
|
|
680
|
+
@buffer.write_big_endian_uint32(count)
|
|
681
|
+
else
|
|
682
|
+
raise Error, "Map too large: #{count} entries"
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Encoding helpers
|
|
687
|
+
|
|
688
|
+
def utf8_compatible?(string)
|
|
689
|
+
string.encoding == Encoding::UTF_8 || string.ascii_only?
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
def binary?(string)
|
|
693
|
+
string.encoding == Encoding::BINARY
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def transcode_to_utf8(string)
|
|
697
|
+
string.encode(Encoding::UTF_8)
|
|
698
|
+
rescue EncodingError => e
|
|
699
|
+
raise Error, "Failed to encode string to UTF-8: #{e.message}"
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
end
|