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.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/README.adoc +773 -0
  3. data/Rakefile +8 -0
  4. data/docs/Gemfile +7 -0
  5. data/docs/README.md +85 -0
  6. data/docs/_config.yml +137 -0
  7. data/docs/_guides/index.adoc +14 -0
  8. data/docs/_guides/io-streaming.adoc +226 -0
  9. data/docs/_guides/migration.adoc +218 -0
  10. data/docs/_guides/performance.adoc +189 -0
  11. data/docs/_pages/buffer.adoc +85 -0
  12. data/docs/_pages/extension-types.adoc +117 -0
  13. data/docs/_pages/factory-pattern.adoc +115 -0
  14. data/docs/_pages/index.adoc +20 -0
  15. data/docs/_pages/serialization.adoc +159 -0
  16. data/docs/_pages/streaming.adoc +97 -0
  17. data/docs/_pages/symbol-extension.adoc +69 -0
  18. data/docs/_pages/timestamp-extension.adoc +88 -0
  19. data/docs/_references/api.adoc +360 -0
  20. data/docs/_references/extensions.adoc +198 -0
  21. data/docs/_references/format.adoc +301 -0
  22. data/docs/_references/index.adoc +14 -0
  23. data/docs/_tutorials/extension-types.adoc +170 -0
  24. data/docs/_tutorials/getting-started.adoc +165 -0
  25. data/docs/_tutorials/index.adoc +14 -0
  26. data/docs/_tutorials/thread-safety.adoc +157 -0
  27. data/docs/index.adoc +77 -0
  28. data/docs/lychee.toml +42 -0
  29. data/lib/messagepack/bigint.rb +131 -0
  30. data/lib/messagepack/buffer.rb +534 -0
  31. data/lib/messagepack/core_ext.rb +34 -0
  32. data/lib/messagepack/error.rb +24 -0
  33. data/lib/messagepack/extensions/base.rb +55 -0
  34. data/lib/messagepack/extensions/registry.rb +154 -0
  35. data/lib/messagepack/extensions/symbol.rb +38 -0
  36. data/lib/messagepack/extensions/timestamp.rb +110 -0
  37. data/lib/messagepack/extensions/value.rb +38 -0
  38. data/lib/messagepack/factory.rb +349 -0
  39. data/lib/messagepack/format.rb +99 -0
  40. data/lib/messagepack/packer.rb +702 -0
  41. data/lib/messagepack/symbol.rb +4 -0
  42. data/lib/messagepack/time.rb +29 -0
  43. data/lib/messagepack/timestamp.rb +4 -0
  44. data/lib/messagepack/unpacker.rb +1418 -0
  45. data/lib/messagepack/version.rb +5 -0
  46. data/lib/messagepack.rb +81 -0
  47. 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
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Symbol is defined in extensions/symbol.rb
4
+ require_relative 'extensions/symbol'