kobako 0.26.0 → 0.28.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +103 -0
  4. data/Cargo.lock +117 -140
  5. data/README.md +156 -118
  6. data/ROADMAP.md +23 -22
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +38 -0
  9. data/crates/kobako-runtime/Cargo.toml +2 -2
  10. data/crates/kobako-runtime/README.md +10 -12
  11. data/crates/kobako-runtime/src/error.rs +8 -9
  12. data/crates/kobako-runtime/src/lib.rs +1 -1
  13. data/crates/kobako-runtime/src/profile.rs +1 -1
  14. data/crates/kobako-runtime/src/runtime.rs +31 -17
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -10
  16. data/crates/kobako-runtime/src/yielder.rs +9 -6
  17. data/crates/kobako-transport/CHANGELOG.md +35 -0
  18. data/crates/kobako-transport/Cargo.toml +1 -1
  19. data/crates/kobako-transport/README.md +43 -27
  20. data/crates/kobako-transport/src/abi.rs +11 -14
  21. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  22. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  23. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  24. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  25. data/crates/kobako-transport/src/envelope/reply.rs +77 -53
  26. data/crates/kobako-transport/src/envelope/request.rs +295 -0
  27. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  28. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  29. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  30. data/crates/kobako-transport/src/envelope.rs +19 -12
  31. data/crates/kobako-transport/src/lib.rs +0 -2
  32. data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
  33. data/crates/kobako-wasmtime/Cargo.toml +7 -5
  34. data/crates/kobako-wasmtime/README.md +8 -10
  35. data/crates/kobako-wasmtime/src/abi.rs +5 -8
  36. data/crates/kobako-wasmtime/src/ambient.rs +2 -2
  37. data/crates/kobako-wasmtime/src/cache.rs +25 -59
  38. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  39. data/crates/kobako-wasmtime/src/config.rs +11 -14
  40. data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
  41. data/crates/kobako-wasmtime/src/driver.rs +133 -86
  42. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  43. data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
  44. data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
  45. data/crates/kobako-wasmtime/src/invocation.rs +55 -372
  46. data/crates/kobako-wasmtime/src/io.rs +286 -0
  47. data/crates/kobako-wasmtime/src/lib.rs +3 -24
  48. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  49. data/crates/kobako-wasmtime/src/trap.rs +32 -45
  50. data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
  51. data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
  52. data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
  53. data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
  54. data/data/kobako.wasm +0 -0
  55. data/ext/kobako/Cargo.toml +2 -2
  56. data/ext/kobako/src/runtime/bridge.rs +38 -47
  57. data/ext/kobako/src/runtime/errors.rs +26 -32
  58. data/ext/kobako/src/runtime/gvl.rs +4 -12
  59. data/ext/kobako/src/runtime.rs +169 -212
  60. data/lib/kobako/capture.rb +1 -4
  61. data/lib/kobako/catalog/extensions.rb +21 -59
  62. data/lib/kobako/catalog/flags.rb +41 -0
  63. data/lib/kobako/catalog/handles.rb +7 -54
  64. data/lib/kobako/catalog/services.rb +7 -61
  65. data/lib/kobako/catalog/snippets.rb +42 -66
  66. data/lib/kobako/catalog.rb +2 -10
  67. data/lib/kobako/codec/decoder.rb +9 -28
  68. data/lib/kobako/codec/encoder.rb +3 -22
  69. data/lib/kobako/codec/error.rb +6 -27
  70. data/lib/kobako/codec/ext_types.rb +72 -94
  71. data/lib/kobako/codec/handle_walk.rb +65 -130
  72. data/lib/kobako/codec/nesting.rb +3 -8
  73. data/lib/kobako/codec/state.rb +4 -10
  74. data/lib/kobako/codec/utils.rb +6 -34
  75. data/lib/kobako/codec.rb +3 -22
  76. data/lib/kobako/constant_name.rb +8 -0
  77. data/lib/kobako/context.rb +51 -112
  78. data/lib/kobako/errors.rb +51 -127
  79. data/lib/kobako/execution.rb +16 -21
  80. data/lib/kobako/extension.rb +23 -40
  81. data/lib/kobako/handle.rb +7 -12
  82. data/lib/kobako/outcome.rb +51 -80
  83. data/lib/kobako/payload/arguments.rb +1 -16
  84. data/lib/kobako/payload.rb +4 -12
  85. data/lib/kobako/pool.rb +23 -53
  86. data/lib/kobako/runtime.rb +7 -21
  87. data/lib/kobako/sandbox.rb +96 -133
  88. data/lib/kobako/sandbox_options.rb +38 -84
  89. data/lib/kobako/snippet/binary.rb +1 -16
  90. data/lib/kobako/snippet/source.rb +1 -16
  91. data/lib/kobako/snippet.rb +1 -9
  92. data/lib/kobako/transport/call.rb +1 -14
  93. data/lib/kobako/transport/dispatcher.rb +112 -195
  94. data/lib/kobako/transport/error.rb +6 -20
  95. data/lib/kobako/transport/exposure.rb +5 -10
  96. data/lib/kobako/transport/reflection.rb +39 -50
  97. data/lib/kobako/transport/run.rb +17 -71
  98. data/lib/kobako/transport/yielder.rb +21 -73
  99. data/lib/kobako/transport.rb +3 -15
  100. data/lib/kobako/unresolved.rb +4 -11
  101. data/lib/kobako/usage.rb +2 -6
  102. data/lib/kobako/version.rb +1 -1
  103. data/sig/kobako/catalog/extensions.rbs +2 -2
  104. data/sig/kobako/catalog/flags.rbs +17 -0
  105. data/sig/kobako/catalog/services.rbs +0 -6
  106. data/sig/kobako/catalog/snippets.rbs +4 -4
  107. data/sig/kobako/codec/ext_types.rbs +10 -8
  108. data/sig/kobako/codec/handle_walk.rbs +9 -7
  109. data/sig/kobako/codec/utils.rbs +2 -2
  110. data/sig/kobako/codec.rbs +2 -2
  111. data/sig/kobako/constant_name.rbs +3 -0
  112. data/sig/kobako/context.rbs +2 -3
  113. data/sig/kobako/errors.rbs +6 -0
  114. data/sig/kobako/outcome.rbs +6 -8
  115. data/sig/kobako/runtime.rbs +12 -8
  116. data/sig/kobako/sandbox.rbs +4 -0
  117. data/sig/kobako/sandbox_options.rbs +2 -6
  118. data/sig/kobako/transport/dispatcher.rbs +14 -18
  119. data/sig/kobako/transport/reflection.rbs +5 -3
  120. data/sig/kobako/transport/run.rbs +1 -3
  121. data/sig/kobako/transport/yielder.rbs +3 -3
  122. data/sig/kobako/transport.rbs +7 -6
  123. metadata +16 -4
  124. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  125. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  126. data/rust-toolchain.toml +0 -11
@@ -7,28 +7,9 @@ require_relative "ext_types"
7
7
 
8
8
  module Kobako
9
9
  module Codec
10
- # Module-level entry point for the host side of the kobako wire
11
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
12
- #
13
- # The codec backbone is the official +msgpack+ gem: integers, floats,
14
- # strings, arrays, and maps go through the gem's narrowest-encoding
15
- # logic; the two kobako-specific ext types (0x00 Symbol, 0x01
16
- # Capability Handle) are registered by ExtTypes on the process-wide
17
- # factory.
18
- #
19
- # Public API is a single function — +.encode+. The codec is stateless;
20
- # there is no buffer accumulator and no streaming write API. Callers
21
- # that need to concatenate multiple encodings build the bytes
22
- # themselves.
23
- module Encoder
24
- # Encode +value+ to wire bytes (binary-encoded String).
25
- # SPEC's 11-entry type mapping is a closed set: a value outside it is
26
- # rejected as +UnsupportedTypeError+ by the factory's +BasicObject+ guard
27
- # (ExtTypes#register_unrepresentable), which raises before the msgpack
28
- # gem can route the value through +to_msgpack+ — so a permissive
29
- # +method_missing+ object cannot answer that probe and mis-encode. The
30
- # rescue below maps the two violations the guard does not reach onto the
31
- # same error: an integer outside i64..u64 (+RangeError+) and any
10
+ module Encoder # :nodoc:
11
+ # The rescue maps the two violations the factory's +BasicObject+ guard
12
+ # does not reach: an integer outside i64..u64 (+RangeError+) and any
32
13
  # packer-internal +NoMethodError+.
33
14
  #
34
15
  # The caller bounds +value+'s nesting first (Nesting): the packer takes
@@ -2,34 +2,13 @@
2
2
 
3
3
  module Kobako
4
4
  module Codec
5
- # Base class for all wire-codec faults raised by the pure-Ruby host codec.
6
- #
7
- # The wire codec implements the binary contract pinned in
8
- # {docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping.
9
- # Every wire violation surfaces as a
10
- # subclass of Error so callers can pattern-match on the specific
11
- # fault while still rescuing all codec faults via this base class.
12
- #
13
- # Higher layers (e.g. the Sandbox dispatch loop) translate these into
14
- # the public Kobako::SandboxError / Kobako::TrapError taxonomy.
15
- class Error < StandardError; end
5
+ class Error < StandardError; end # :nodoc:
6
+ class TruncatedInputError < Error; end # :nodoc:
16
7
 
17
- # Input ended before the type prefix or payload was fully consumed.
18
- class TruncatedInputError < Error; end
8
+ # Also raised on encode for a value nested past what the packer can walk.
9
+ class InvalidTypeError < Error; end # :nodoc:
19
10
 
20
- # The type byte at the current position is not in the 11-entry kobako
21
- # type mapping (e.g. an unknown ext code, or a reserved msgpack tag) —
22
- # or, on encode, a value nesting past what the packer can walk.
23
- class InvalidTypeError < Error; end
24
-
25
- # A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
26
- # payload was not valid UTF-8 — both are wire violations per SPEC.
27
- class InvalidEncodingError < Error; end
28
-
29
- # The encoder was handed a Ruby object whose type has no wire
30
- # representation (e.g. Range, Time). Higher layers may catch this
31
- # and re-route the value through Handle allocation, but at the
32
- # codec level it is a hard error.
33
- class UnsupportedTypeError < Error; end
11
+ class InvalidEncodingError < Error; end # :nodoc:
12
+ class UnsupportedTypeError < Error; end # :nodoc:
34
13
  end
35
14
  end
@@ -8,21 +8,16 @@ require_relative "state"
8
8
  require_relative "../handle"
9
9
 
10
10
  module Kobako
11
- module Codec
12
- # The kobako wire ext-type conversions
13
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types)
14
- # as pure functions: per-operation decode state is threaded in as an
15
- # argument, so the module itself holds nothing. #build_factory assembles
16
- # the one +MessagePack::Factory+ these conversions are registered on.
17
- module ExtTypes
11
+ module Codec # :nodoc:
12
+ module ExtTypes # :nodoc:
18
13
  # MessagePack ext type code reserved for Symbol
19
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types
20
- # → ext 0x00). Module-private — mirrors +codec::EXT_SYMBOL+ on the
14
+ # (docs/wire/payload-msgpack.md § ext 0x00 — Symbol). Module-private —
15
+ # mirrors +codec::EXT_SYMBOL+ on the
21
16
  # Rust side.
22
17
  EXT_SYMBOL = 0x00
23
18
  # MessagePack ext type code reserved for Capability Handle
24
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types
25
- # → ext 0x01). Module-private — mirrors +codec::EXT_HANDLE+ on the
19
+ # (docs/wire/payload-msgpack.md § ext 0x01 — Handle). Module-private —
20
+ # mirrors +codec::EXT_HANDLE+ on the
26
21
  # Rust side.
27
22
  EXT_HANDLE = 0x01
28
23
  private_constant :EXT_SYMBOL, :EXT_HANDLE
@@ -31,19 +26,13 @@ module Kobako
31
26
  # never emitted (the guard's packer always raises) and never decoded
32
27
  # (no unpacker is registered, so the id stays an UnknownExtTypeError on
33
28
  # the wire), so it is not a wire ext type: deliberately not named
34
- # +EXT_*+ like the two real ext codes, since it has no Rust-side mirror
35
- # and must stay outside the wire-symmetry inventory.
29
+ # +EXT_*+ like the two real ext codes, since it has no Rust-side mirror.
36
30
  UNREPRESENTABLE_GUARD_ID = 0x7F
37
31
  private_constant :UNREPRESENTABLE_GUARD_ID
38
32
 
39
- module_function
40
-
41
- # Assemble a +MessagePack::Factory+ with the two kobako ext types plus
42
- # the unrepresentable-value guard registered, frozen because
43
- # registration is its only mutation and happens exactly once. The
44
- # stateful conversions resolve their per-operation state at call time,
45
- # so one registered factory serves every thread.
46
- def build_factory
33
+ # The stateful conversions resolve their per-operation state at call
34
+ # time, so one frozen factory serves every thread.
35
+ def self.build_factory
47
36
  factory = MessagePack::Factory.new
48
37
  register_symbol(factory)
49
38
  register_handle(factory)
@@ -51,89 +40,78 @@ module Kobako
51
40
  factory.freeze
52
41
  end
53
42
 
54
- # Symbol-to-name packer for the ext-0x00 registration.
55
- def pack_symbol(symbol)
56
- symbol.name
57
- end
43
+ class << self
44
+ private
58
45
 
59
- # Validate the ext-0x00 payload as UTF-8 and intern. Raises
60
- # InvalidEncodingError on invalid bytes — SPEC forbids the
61
- # binary-encoding fallback that msgpack-gem's default unpacker
62
- # would otherwise apply. The re-tag step lives here because the
63
- # msgpack ext-type unpacker hands us binary bytes; the assertion
64
- # itself is shared with Decoder via Utils.assert_utf8!. The
65
- # +"Symbol"+ label keeps the error message in Ruby vocabulary
66
- # rather than wire-ext-code vocabulary.
67
- def unpack_symbol(payload)
68
- name = payload.b.force_encoding(Encoding::UTF_8)
69
- Utils.assert_utf8!(name, "Symbol payload")
70
- name.to_sym
71
- end
46
+ def pack_symbol(symbol)
47
+ symbol.name
48
+ end
72
49
 
73
- # Handle-id packer for the ext-0x01 registration: the fixext-4
74
- # big-endian id frame.
75
- def pack_handle(handle)
76
- [handle.id].pack("N")
77
- end
50
+ # Refuses the binary-encoding fallback that msgpack-gem's default
51
+ # unpacker would otherwise apply to invalid bytes.
52
+ def unpack_symbol(payload)
53
+ name = payload.b.force_encoding(Encoding::UTF_8)
54
+ Utils.assert_utf8!(name, "Symbol payload")
55
+ name.to_sym
56
+ end
78
57
 
79
- # Peel off the fixext-4 frame, hand the bytes to the
80
- # Host-Gem-internal +Kobako::Handle.restore+ factory, and
81
- # translate the +ArgumentError+ raised by Handle's invariants
82
- # into a wire-layer +InvalidTypeError+ via Codec::Utils.with_boundary.
83
- # The Value Object owns the id-range contract; this method only
84
- # owns the frame shape. Records the Handle sighting on +state+ so a
85
- # Handle-free decode can skip the downstream resolution walk.
86
- def unpack_handle(payload, state)
87
- state.record_handle!
88
- bytes = payload.b
89
- raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
58
+ def pack_handle(handle)
59
+ [handle.id].pack("N")
60
+ end
90
61
 
91
- id = bytes.unpack1("N") # : Integer
92
- Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
93
- end
62
+ # Handle owns the id-range contract; this method owns only the frame
63
+ # shape. The sighting is recorded so a Handle-free decode can skip the
64
+ # downstream resolution walk.
65
+ def unpack_handle(payload, state)
66
+ state.record_handle!
67
+ bytes = payload.b
68
+ raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
94
69
 
95
- def register_symbol(factory)
96
- factory.register_type(
97
- EXT_SYMBOL, Symbol,
98
- packer: ->(symbol) { ExtTypes.pack_symbol(symbol) },
99
- unpacker: ->(payload) { ExtTypes.unpack_symbol(payload) }
100
- )
101
- end
70
+ id = bytes.unpack1("N") # : Integer
71
+ Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
72
+ end
102
73
 
103
- def register_handle(factory)
104
- factory.register_type(
105
- EXT_HANDLE, Kobako::Handle,
106
- packer: ->(handle) { ExtTypes.pack_handle(handle) },
107
- unpacker: ->(payload) { ExtTypes.unpack_handle(payload, State.current) }
108
- )
109
- end
74
+ def register_symbol(factory)
75
+ factory.register_type(
76
+ EXT_SYMBOL, Symbol,
77
+ packer: ->(symbol) { pack_symbol(symbol) },
78
+ unpacker: ->(payload) { unpack_symbol(payload) }
79
+ )
80
+ end
81
+
82
+ def register_handle(factory)
83
+ factory.register_type(
84
+ EXT_HANDLE, Kobako::Handle,
85
+ packer: ->(handle) { pack_handle(handle) },
86
+ unpacker: ->(payload) { unpack_handle(payload, State.current) }
87
+ )
88
+ end
110
89
 
111
- # A catch-all packer that rejects any value with no wire representation
112
- # as +UnsupportedTypeError+. Registered on +BasicObject+ so it also covers
113
- # BasicObject-based proxies; the narrower Symbol / Handle
114
- # registrations still win by most-specific match, and native types never
115
- # reach it. Packer-only: the guard never writes bytes, so its id is inert
116
- # and the decode surface stays fail-closed.
117
- #
118
- # This makes the host's non-wire detection a positive allowlist — a value
119
- # outside the type set is rejected here rather than routed to +to_msgpack+
120
- # — matching the guest's classname allowlist and the Rust codec's closed
121
- # +Value+ enum. Without it, a value with a permissive +method_missing+
122
- # answers the codec's +to_msgpack+ probe and mis-encodes as +nil+ instead
123
- # of crossing as a Capability Handle.
124
- def register_unrepresentable(factory)
125
- factory.register_type(
126
- UNREPRESENTABLE_GUARD_ID, BasicObject,
127
- packer: ->(_value) { raise UnsupportedTypeError, "value has no wire representation" }
128
- )
90
+ # A catch-all packer that rejects any value with no wire representation
91
+ # as +UnsupportedTypeError+. Registered on +BasicObject+ so it also covers
92
+ # BasicObject-based proxies; the narrower Symbol / Handle
93
+ # registrations still win by most-specific match, and native types never
94
+ # reach it. Packer-only: the guard never writes bytes, so its id is inert
95
+ # and the decode surface stays fail-closed.
96
+ #
97
+ # This makes the host's non-wire detection a positive allowlist — a value
98
+ # outside the type set is rejected here rather than routed to +to_msgpack+
99
+ # — matching the guest's classname allowlist and the Rust codec's closed
100
+ # +Value+ enum. Without it, a value with a permissive +method_missing+
101
+ # answers the codec's +to_msgpack+ probe and mis-encodes as +nil+ instead
102
+ # of crossing as a Capability Handle.
103
+ def register_unrepresentable(factory)
104
+ factory.register_type(
105
+ UNREPRESENTABLE_GUARD_ID, BasicObject,
106
+ packer: ->(_value) { raise UnsupportedTypeError, "value has no wire representation" }
107
+ )
108
+ end
129
109
  end
130
110
  end
131
111
 
132
- # The process-wide registered factory: ext registration is paid once at
133
- # load, and a registered +MessagePack::Factory+ only reads its type
134
- # registry afterwards, so every thread shares this instance for byte
135
- # work.
136
- FACTORY = ExtTypes.build_factory
112
+ # Registered once at load and only read afterwards, so every thread
113
+ # shares it.
114
+ FACTORY = ExtTypes.build_factory # :nodoc:
137
115
  private_constant :FACTORY
138
116
  end
139
117
  end
@@ -5,26 +5,11 @@ require_relative "../errors"
5
5
 
6
6
  module Kobako
7
7
  module Codec
8
- # Substitutes Capability Handles into and out of a Ruby value tree at
9
- # the host↔guest boundary. #deep_wrap allocates a +Kobako::Handle+ for
10
- # each non-wire-representable leaf on the host→guest +#run+ argument
11
- # path; #deep_restore resolves each wire-decoded Handle back to its
12
- # host object on every guest→host value path. #representable? is the
13
- # by-value codec-type predicate that decides which leaves #deep_wrap
14
- # must wrap: the closed 11-entry wire type set
15
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type
16
- # Mapping).
17
- #
18
- # All helpers are pure except #deep_wrap, whose only side effect is
19
- # allocating new Handle ids into the supplied table.
20
- module HandleWalk
21
- module_function
22
-
8
+ module HandleWalk # :nodoc:
23
9
  # Inclusive Integer range the msgpack gem encodes without raising
24
10
  # +RangeError+ at encode time — signed +int 64+ minimum through
25
- # unsigned +uint 64+ maximum
26
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type
27
- # Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
11
+ # unsigned +uint 64+ maximum (docs/wire/payload-msgpack.md § Integer
12
+ # Range).
28
13
  # Anchored as a +Range+ so #primitive_type? stays a single
29
14
  # dispatch line. This is the codec's encode domain — not to
30
15
  # be confused with the Handle id range, which lives on
@@ -32,141 +17,91 @@ module Kobako
32
17
  # represents a different concept entirely.
33
18
  MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
34
19
 
35
- # Codec-type predicate
36
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type
37
- # Mapping). Returns +true+ when +value+ belongs to the closed
38
- # 11-entry codec type set — +nil+, +TrueClass+, +FalseClass+,
39
- # +Integer+ (in the +i64..u64+ value domain), +Float+, +String+,
40
- # +Symbol+, +Kobako::Handle+, +Array+ whose every element is itself
41
- # representable, or +Hash+ whose every key and value are
42
- # representable. Integers outside the codec's signed-64 /
43
- # unsigned-64 union are rejected so the predicate agrees with the
44
- # msgpack gem's encode-time +RangeError+ behaviour the codec
45
- # already surfaces as UnsupportedTypeError.
46
- #
47
- # The optional +depth+ bounds the recursive descent at the wire's
48
- # structural nesting cap so a self-referential container — reachable
49
- # as a cyclic Hash key on the +#run+ argument path — reads as
50
- # non-representable rather than looping, letting #deep_wrap_hash
51
- # refuse it instead of overflowing this walk.
52
- def representable?(value, depth = 0)
20
+ # Whether +value+ falls in the codec's type set
21
+ # (docs/wire/payload-msgpack.md § Type Mapping). The +depth+ bound
22
+ # makes a self-referential container —
23
+ # reachable as a cyclic Hash key on the +#run+ argument path — read as
24
+ # non-representable rather than looping.
25
+ def self.representable?(value, depth = 0)
53
26
  return false if depth > MAX_NESTING_DEPTH
54
27
 
55
28
  primitive_type?(value) || container_representable?(value, depth)
56
29
  end
57
30
 
58
- # Deep-walk Array / Hash containers in +value+ and replace every
59
- # leaf that fails #representable? with a +Kobako::Handle+
60
- # allocated from +handler+. The
61
- # walk only descends through representable container shapes
62
- # (Array, Hash) one structural level at a time; a non-representable
63
- # leaf is wrapped as-is without inspecting its internal structure.
64
- # An existing +Kobako::Handle+ is representable and passes through
65
- # unchanged — auto-wrap never re-wraps a Handle. Only Hash *values*
66
- # are wrapped: a Hash *key* must already be wire-representable, since a
67
- # key is not auto-wrapped the way a value is — a non-representable key
68
- # raises +Kobako::SandboxError+ rather than crossing as an opaque token
69
- # the guest→host restore walk could not round-trip.
70
- #
71
- # +value+ may be any Ruby value; +handler+ must respond to
72
- # +#alloc(object) -> Kobako::Handle+ (a host-side
73
- # +Kobako::Catalog::Handles+). Returns a structurally equivalent value
74
- # whose leaves are either representable or +Kobako::Handle+
75
- # tokens.
76
- #
77
- # Recursive calls spell the +HandleWalk.+ receiver so the dispatch
78
- # stays valid even when a block is captured and run under a
79
- # different +self+ (+module_function+ privatizes the instance copies).
80
- def deep_wrap(value, handler, depth = 0)
31
+ # Only Hash values are wrapped: a non-representable key raises rather
32
+ # than crossing as a token the guest→host restore walk could not
33
+ # round-trip.
34
+ def self.deep_wrap(value, handles, depth = 0)
81
35
  guard_nesting!(depth)
82
36
  case value
83
- when ::Array then value.map { |element| HandleWalk.deep_wrap(element, handler, depth + 1) }
84
- when ::Hash then HandleWalk.deep_wrap_hash(value, handler, depth)
37
+ when ::Array then value.map { |element| deep_wrap(element, handles, depth + 1) }
38
+ when ::Hash then deep_wrap_hash(value, handles, depth)
85
39
  else
86
- representable?(value) ? value : handler.alloc(value)
87
- end
88
- end
89
-
90
- # Refuse a wrap walk that has descended past the wire's structural
91
- # nesting bound — a reference cycle necessarily trips it — so a +#run+
92
- # argument fails as a clean +Kobako::SandboxError+ rather than an
93
- # unbounded host recursion.
94
- def guard_nesting!(depth)
95
- return unless depth > MAX_NESTING_DEPTH
96
-
97
- raise Kobako::SandboxError,
98
- "a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels within the Run payload and " \
99
- "cannot cross the sandbox boundary (possible reference cycle)"
100
- end
101
-
102
- # Wrap a Hash on the host→guest argument path: each value's
103
- # non-representable leaves become Handles, while each key must be
104
- # wire-representable as-is. A stateful object may cross the boundary as
105
- # a Hash value but not as a key — the one deliberate asymmetry with the
106
- # guest→host restore walk, which resolves Handle keys the guest built.
107
- def deep_wrap_hash(hash, handler, depth)
108
- wrapped = {} # : Hash[untyped, untyped]
109
- hash.each do |key, val|
110
- unless HandleWalk.representable?(key, depth + 1)
111
- raise Kobako::SandboxError,
112
- "a Hash passed to #run has a key that cannot cross the sandbox boundary " \
113
- "(#{key.class}); only wire-representable values may be Hash keys"
114
- end
115
- wrapped[key] = HandleWalk.deep_wrap(val, handler, depth + 1)
40
+ representable?(value) ? value : handles.alloc(value)
116
41
  end
117
- wrapped
118
42
  end
119
43
 
120
- # Deep-walk Array / Hash containers in +value+ and replace every
121
- # +Kobako::Handle+ leaf with the host-side object +handler+ resolves
122
- # it to. The symmetric inverse of #deep_wrap: that walk allocates objects
123
- # into Handles on the host→guest argument path; this walk resolves
124
- # Handles back to their objects wherever a guest→host payload carries
125
- # one — an invocation result, a yield-block result, or a dispatch
126
- # argument. The walk descends through Array elements and Hash keys and
127
- # values one structural level at a time; any non-Handle leaf passes
128
- # through unchanged.
129
- #
130
- # +value+ is a decoded Ruby value (a Handle here is a wire-decoded
131
- # +Kobako::Handle+, never a guest-forged one); +handler+ must
132
- # respond to +#fetch(id) -> object+ (a host-side
133
- # +Kobako::Catalog::Handles+). +handler.fetch+ raises
134
- # +Kobako::SandboxError+ for an id with no live binding, the
135
- # corrupted-runtime fallback.
136
- def deep_restore(value, handler)
44
+ # The inverse of #deep_wrap. A Handle here was decoded off the wire,
45
+ # never forged by the guest, and +handles.fetch+ refuses an id with no
46
+ # live binding.
47
+ def self.deep_restore(value, handles)
137
48
  case value
138
- when ::Array then value.map { |element| HandleWalk.deep_restore(element, handler) }
49
+ when ::Array then value.map { |element| deep_restore(element, handles) }
139
50
  when ::Hash
140
51
  # Rebuilt with each key restored: two distinct Handle keys that
141
52
  # resolve to equal host objects collapse to the later pair, as in
142
53
  # any Ruby Hash. The guest authored this payload, so that collapse
143
54
  # is its own concern, not a fidelity guarantee the host owes it.
144
- value.to_h { |key, val| [HandleWalk.deep_restore(key, handler), HandleWalk.deep_restore(val, handler)] }
145
- when Kobako::Handle then handler.fetch(value.id)
55
+ value.to_h { |key, val| [deep_restore(key, handles), deep_restore(val, handles)] }
56
+ when Kobako::Handle then handles.fetch(value.id)
146
57
  else value
147
58
  end
148
59
  end
149
60
 
150
- # The non-container branch of #representable?: returns +true+ for
151
- # the scalar leaves and an existing Handle. Not part of the
152
- # public surface; reach for #representable? instead.
153
- def primitive_type?(value)
154
- case value
155
- when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
156
- when ::Integer then MSGPACK_INT_RANGE.cover?(value)
157
- else false
61
+ class << self
62
+ private
63
+
64
+ # A reference cycle necessarily trips the nesting bound, so a +#run+
65
+ # argument fails as a clean +Kobako::SandboxError+ rather than an
66
+ # unbounded host recursion.
67
+ def guard_nesting!(depth)
68
+ return unless depth > MAX_NESTING_DEPTH
69
+
70
+ raise Kobako::SandboxError,
71
+ "a #run argument nests deeper than #{MAX_NESTING_DEPTH} levels within the Run payload and " \
72
+ "cannot cross the sandbox boundary (possible reference cycle)"
158
73
  end
159
- end
160
74
 
161
- # The container branch of #representable?: recurses into Array
162
- # elements and Hash key+value pairs through the public
163
- # #representable?. Not part of the public surface; reach for
164
- # #representable? instead.
165
- def container_representable?(value, depth)
166
- case value
167
- when ::Array then value.all? { |element| HandleWalk.representable?(element, depth + 1) }
168
- when ::Hash then value.all? { |pair| pair.all? { |member| HandleWalk.representable?(member, depth + 1) } }
169
- else false
75
+ # A stateful object may cross the boundary as a Hash value but not as
76
+ # a key — the one deliberate asymmetry with the guest→host restore
77
+ # walk, which resolves Handle keys the guest built.
78
+ def deep_wrap_hash(hash, handles, depth)
79
+ wrapped = {} # : Hash[untyped, untyped]
80
+ hash.each do |key, val|
81
+ unless representable?(key, depth + 1)
82
+ raise Kobako::SandboxError,
83
+ "a Hash passed to #run has a key that cannot cross the sandbox boundary " \
84
+ "(#{key.class}); only wire-representable values may be Hash keys"
85
+ end
86
+ wrapped[key] = deep_wrap(val, handles, depth + 1)
87
+ end
88
+ wrapped
89
+ end
90
+
91
+ def primitive_type?(value)
92
+ case value
93
+ when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
94
+ when ::Integer then MSGPACK_INT_RANGE.cover?(value)
95
+ else false
96
+ end
97
+ end
98
+
99
+ def container_representable?(value, depth)
100
+ case value
101
+ when ::Array then value.all? { |element| representable?(element, depth + 1) }
102
+ when ::Hash then value.all? { |pair| pair.all? { |member| representable?(member, depth + 1) } }
103
+ else false
104
+ end
170
105
  end
171
106
  end
172
107
  end
@@ -4,14 +4,9 @@ require_relative "error"
4
4
 
5
5
  module Kobako
6
6
  module Codec
7
- # The structural nesting bound
8
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md]
9
- # § Structural Nesting Depth), checked before a value reaches the
10
- # packer. The packer takes no depth limit and walks a list or map in
11
- # frames that carry no stack guard, so a value it cannot finish — a
12
- # reference cycle necessarily is one — must be refused before it is
13
- # handed over.
14
- module Nesting
7
+ # Refuses a value nested past the wire's bound before the packer sees it,
8
+ # since the packer walks without a stack guard and a cycle never ends.
9
+ module Nesting # :nodoc:
15
10
  # Raise InvalidTypeError when +value+ nests past MAX_NESTING_DEPTH; a
16
11
  # value nested exactly to the bound passes.
17
12
  def self.assert_within_bound!(value, depth = 0)
@@ -2,11 +2,9 @@
2
2
 
3
3
  module Kobako
4
4
  module Codec
5
- # Codec-internal, per-thread state of the operation in flight: whether
6
- # a Capability Handle crossed the current decode. Thread scoping is
7
- # what makes plain instance variables sound — host codec calls run
8
- # synchronously on their owning thread.
9
- class State
5
+ # Whether a Capability Handle crossed the decode in flight, kept per
6
+ # thread so plain instance variables stay sound.
7
+ class State # :nodoc:
10
8
  # Thread-local slot holding the calling thread's State.
11
9
  STATE_KEY = :__kobako_codec_state__
12
10
  private_constant :STATE_KEY
@@ -22,12 +20,8 @@ module Kobako
22
20
  @carried_handle = false
23
21
  end
24
22
 
25
- # Bracket a decode and return the block's result together with
26
- # whether the decoded tree carried an ext 0x01 Capability Handle.
27
23
  # ExtTypes#unpack_handle is the sole chokepoint every Handle passes
28
- # through, so one decode pass records the whole tree and a caller
29
- # can skip an all-identity Handle-resolution walk when none was
30
- # present.
24
+ # through, so one decode pass records the whole tree.
31
25
  def track_handles
32
26
  @carried_handle = false
33
27
  result = yield
@@ -4,45 +4,17 @@ require_relative "error"
4
4
 
5
5
  module Kobako
6
6
  module Codec
7
- # Byte-boundary helpers shared by the host-side encoder and decoder.
8
- # Two concerns live here:
9
- #
10
- # - UTF-8 assertion at the codec boundary
11
- # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
12
- # § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
13
- # Decoder when walking +str+ family payloads and by ExtTypes
14
- # when validating the +ext 0x00+ Symbol payload.
15
- # - +ArgumentError+ translation at the codec boundary
16
- # (#with_boundary) so the public taxonomy stays
17
- # Kobako::Codec::Error.
18
- #
19
- # Both helpers are pure — they only inspect inputs, never mutate them.
20
- # The host↔guest Handle substitution walk lives in HandleWalk.
21
- module Utils
22
- module_function
23
-
24
- # Raise InvalidEncodingError unless +string+'s bytes are valid under
25
- # its current encoding tag. +label+ is the caller-supplied prefix
26
- # for the error message (e.g. +"str payload"+, +"Symbol payload"+).
27
- def assert_utf8!(string, label)
7
+ module Utils # :nodoc:
8
+ def self.assert_utf8!(string, label)
28
9
  return if string.valid_encoding?
29
10
 
30
11
  raise InvalidEncodingError, "#{label} is not valid UTF-8"
31
12
  end
32
13
 
33
- # Run +block+ at the codec boundary: a value object raises
34
- # +ArgumentError+ when an invariant is violated at construction, and
35
- # this helper surfaces that as InvalidTypeError so the public taxonomy
36
- # stays Kobako::Codec::Error and never leaks +ArgumentError+ from
37
- # the Ruby standard library.
38
- #
39
- # Reach for this only where a value object is constructed outside a
40
- # Decoder.decode block, whose rescue already performs the same
41
- # mapping (worked example: ExtTypes#unpack_handle building
42
- # +Handle.restore+ from a raw fixext payload). Do not use it for
43
- # general-purpose validation outside the codec boundary —
44
- # host-layer +ArgumentError+ values should propagate unchanged.
45
- def with_boundary
14
+ # Only for a value object built outside a Decoder.decode block, whose
15
+ # rescue already maps the same way; a host-layer +ArgumentError+
16
+ # elsewhere should propagate unchanged.
17
+ def self.with_boundary
46
18
  yield
47
19
  rescue ::ArgumentError => e
48
20
  raise InvalidTypeError, e.message