kobako 0.13.0 → 0.15.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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +81 -0
- data/Cargo.lock +3 -3
- data/README.md +107 -32
- data/ROADMAP.md +25 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +14 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/error.rs +8 -0
- data/crates/kobako-runtime/src/snapshot.rs +7 -3
- data/crates/kobako-wasmtime/CHANGELOG.md +14 -0
- data/crates/kobako-wasmtime/Cargo.toml +2 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/capture.rs +17 -13
- data/crates/kobako-wasmtime/src/driver.rs +7 -8
- data/crates/kobako-wasmtime/src/frames.rs +3 -10
- data/crates/kobako-wasmtime/src/guest_mem.rs +8 -1
- data/crates/kobako-wasmtime/src/instance_pre.rs +7 -6
- data/crates/kobako-wasmtime/src/invocation.rs +4 -5
- data/crates/kobako-wasmtime/src/trap.rs +65 -71
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/runtime/errors.rs +9 -39
- data/ext/kobako/src/runtime.rs +5 -20
- data/lib/kobako/catalog/handles.rb +3 -3
- data/lib/kobako/catalog/services.rb +123 -0
- data/lib/kobako/catalog/snippets.rb +2 -2
- data/lib/kobako/catalog.rb +2 -2
- data/lib/kobako/codec/decoder.rb +10 -10
- data/lib/kobako/codec/encoder.rb +5 -5
- data/lib/kobako/codec/error.rb +3 -3
- data/lib/kobako/codec/ext_types.rb +169 -0
- data/lib/kobako/codec/handle_walk.rb +17 -21
- data/lib/kobako/codec/state.rb +98 -0
- data/lib/kobako/codec/utils.rb +9 -9
- data/lib/kobako/codec.rb +23 -6
- data/lib/kobako/errors.rb +14 -39
- data/lib/kobako/fault.rb +1 -1
- data/lib/kobako/handle.rb +6 -3
- data/lib/kobako/outcome.rb +14 -7
- data/lib/kobako/pool.rb +1 -1
- data/lib/kobako/sandbox.rb +30 -25
- data/lib/kobako/transport/dispatcher.rb +41 -31
- data/lib/kobako/transport/request.rb +16 -11
- data/lib/kobako/transport/response.rb +11 -4
- data/lib/kobako/transport/run.rb +1 -1
- data/lib/kobako/transport/yield.rb +7 -4
- data/lib/kobako/transport/yielder.rb +20 -12
- data/lib/kobako/version.rb +1 -1
- data/release-please-config.json +37 -7
- data/sig/kobako/catalog/services.rbs +25 -0
- data/sig/kobako/codec/ext_types.rbs +31 -0
- data/sig/kobako/codec/handle_walk.rbs +2 -2
- data/sig/kobako/codec/state.rbs +20 -0
- data/sig/kobako/codec.rbs +14 -0
- data/sig/kobako/handle.rbs +0 -2
- data/sig/kobako/sandbox.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +8 -8
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +8 -0
- metadata +8 -7
- data/lib/kobako/catalog/namespaces.rb +0 -115
- data/lib/kobako/codec/factory.rb +0 -187
- data/lib/kobako/namespace.rb +0 -78
- data/sig/kobako/catalog/namespaces.rbs +0 -17
- data/sig/kobako/codec/factory.rbs +0 -34
- data/sig/kobako/namespace.rbs +0 -21
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "handles"
|
|
4
|
+
require_relative "../codec"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
|
|
7
|
+
module Kobako
|
|
8
|
+
module Catalog
|
|
9
|
+
# Kobako::Catalog::Services — per-Sandbox registry of Service
|
|
10
|
+
# bindings keyed by their constant-path name. Holds the flat
|
|
11
|
+
# path→object table and the preamble emitted on Frame 1.
|
|
12
|
+
#
|
|
13
|
+
# Public API:
|
|
14
|
+
#
|
|
15
|
+
# services = Kobako::Catalog::Services.new
|
|
16
|
+
# services.bind("MyService::KV", kv_object) # => services (chainable)
|
|
17
|
+
# services.encode # => msgpack bytes for Frame 1
|
|
18
|
+
# services.lookup("MyService::KV") # => kv_object
|
|
19
|
+
#
|
|
20
|
+
# Per-dispatch routing is +Kobako::Transport::Dispatcher+'s
|
|
21
|
+
# responsibility — the Dispatcher receives this registry and the
|
|
22
|
+
# +Catalog::Handles+ as arguments from the +Runtime#on_dispatch+ Proc
|
|
23
|
+
# that +Kobako::Sandbox#initialize+ installs. The registry holds an
|
|
24
|
+
# injected +Catalog::Handles+ reference so dispatch target resolution
|
|
25
|
+
# and host→guest auto-wrap share the same Sandbox-owned allocator.
|
|
26
|
+
class Services
|
|
27
|
+
# Ruby constant-name pattern each +::+-separated bind-path segment
|
|
28
|
+
# must match.
|
|
29
|
+
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
30
|
+
|
|
31
|
+
# Build a fresh registry. +handler+ is an internal seam that injects
|
|
32
|
+
# a pre-configured +Catalog::Handles+; tests pass one whose +next_id+
|
|
33
|
+
# is pinned near +MAX_ID+ to exercise the cap-exhaustion path
|
|
34
|
+
# without 2³¹ allocations. Production callers leave it at the default.
|
|
35
|
+
def initialize(handler: Catalog::Handles.new)
|
|
36
|
+
@bindings = {} # : Hash[String, untyped]
|
|
37
|
+
@handler = handler
|
|
38
|
+
@sealed = false
|
|
39
|
+
@encoded = nil # : String?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Bind +object+ as the Service reachable at +path+ — a +Symbol+ or
|
|
43
|
+
# +String+ of one or more +::+-separated constant-form segments
|
|
44
|
+
# (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
|
|
45
|
+
# chaining. Raises +ArgumentError+ when a segment is malformed, when
|
|
46
|
+
# +path+ collides with an existing binding (a name is a bound Service
|
|
47
|
+
# or a grouping prefix, never both), or when the owning Sandbox has
|
|
48
|
+
# been sealed by its first invocation.
|
|
49
|
+
def bind(path, object)
|
|
50
|
+
raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
|
|
51
|
+
|
|
52
|
+
path_str = validate_path!(path)
|
|
53
|
+
raise ArgumentError, "Service path #{path_str} conflicts with an existing binding" if collision?(path_str)
|
|
54
|
+
|
|
55
|
+
@bindings[path_str] = object
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Resolve a +target+ constant path to the bound Service. Raises
|
|
60
|
+
# +KeyError+ when no Service is bound at +target+.
|
|
61
|
+
def lookup(target)
|
|
62
|
+
target_str = target.to_s
|
|
63
|
+
raise KeyError, "no service bound at #{target_str.inspect}" unless @bindings.key?(target_str)
|
|
64
|
+
|
|
65
|
+
@bindings[target_str]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery —
|
|
69
|
+
# a flat array of the bound constant paths, in bind order:
|
|
70
|
+
# +["MyService::KV", "File"]+. Routes through Kobako::Codec::Encoder
|
|
71
|
+
# like every other host-side wire encode; the preamble carries only
|
|
72
|
+
# Strings, so none of the kobako ext types fire. Returns a binary
|
|
73
|
+
# +String+ of msgpack bytes.
|
|
74
|
+
#
|
|
75
|
+
# Once sealed, the bytes are computed once and reused for every
|
|
76
|
+
# subsequent invocation: sealing freezes Service registration at the
|
|
77
|
+
# first invocation, so a bind reaching the registry after the seal
|
|
78
|
+
# raises +ArgumentError+ and never alters Frame 1.
|
|
79
|
+
def encode
|
|
80
|
+
return @encoded if @encoded
|
|
81
|
+
|
|
82
|
+
bytes = Codec::Encoder.encode(@bindings.keys).freeze
|
|
83
|
+
@encoded = bytes if @sealed
|
|
84
|
+
bytes
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Mark the registry as sealed. Called by +Sandbox+ on the first
|
|
88
|
+
# invocation; afterwards #bind raises ArgumentError. Idempotent;
|
|
89
|
+
# returns +self+.
|
|
90
|
+
def seal!
|
|
91
|
+
@sealed = true
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Returns +true+ when #seal! has been called, +false+ otherwise.
|
|
96
|
+
def sealed?
|
|
97
|
+
@sealed
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def validate_path!(path)
|
|
103
|
+
path_str = path.to_s
|
|
104
|
+
segments = path_str.split("::", -1)
|
|
105
|
+
return path_str if !segments.empty? && segments.all? { |seg| NAME_PATTERN.match?(seg) }
|
|
106
|
+
|
|
107
|
+
raise ArgumentError,
|
|
108
|
+
"bind path must be constant-form segments joined by '::' (got #{path.inspect})"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A path collides when it equals, is a prefix of, or extends an
|
|
112
|
+
# existing binding on the +::+ segment boundary — the guardrail that
|
|
113
|
+
# keeps a name from being both a bound Service and a grouping prefix.
|
|
114
|
+
def collision?(path)
|
|
115
|
+
@bindings.each_key.any? do |existing|
|
|
116
|
+
existing == path ||
|
|
117
|
+
existing.start_with?("#{path}::") ||
|
|
118
|
+
path.start_with?("#{existing}::")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -43,8 +43,8 @@ module Kobako
|
|
|
43
43
|
#
|
|
44
44
|
# The bytes are memoized — the table is replayed verbatim on every
|
|
45
45
|
# invocation after sealing, so Frame 3 never changes between
|
|
46
|
-
# encodes;
|
|
47
|
-
# Unlike +Catalog::
|
|
46
|
+
# encodes; #register drops the memo while the table is still open.
|
|
47
|
+
# Unlike +Catalog::Services#encode+, which gates its memo on the
|
|
48
48
|
# seal, this one can fill eagerly and invalidate in +#register+
|
|
49
49
|
# because every mutation funnels through that single method — there is
|
|
50
50
|
# no out-of-sight child object to change the result behind its back.
|
data/lib/kobako/catalog.rb
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "catalog/handles"
|
|
4
|
-
require_relative "catalog/
|
|
4
|
+
require_relative "catalog/services"
|
|
5
5
|
require_relative "catalog/snippets"
|
|
6
6
|
|
|
7
7
|
module Kobako
|
|
8
8
|
# Kobako::Catalog — Sandbox-level configuration and per-invocation
|
|
9
9
|
# allocation tables. Houses the three host-side registries the Sandbox
|
|
10
|
-
# owns: +Catalog::
|
|
10
|
+
# owns: +Catalog::Services+ (path→Service binding registry),
|
|
11
11
|
# +Catalog::Snippets+ (preloaded source / bytecode entries), and
|
|
12
12
|
# +Catalog::Handles+ (per-invocation Handle ID allocator).
|
|
13
13
|
#
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "msgpack"
|
|
4
4
|
|
|
5
5
|
require_relative "error"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "ext_types"
|
|
7
7
|
require_relative "utils"
|
|
8
8
|
|
|
9
9
|
module Kobako
|
|
@@ -12,32 +12,32 @@ module Kobako
|
|
|
12
12
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
|
|
13
13
|
#
|
|
14
14
|
# Translates msgpack gem exceptions into the kobako error taxonomy
|
|
15
|
-
# (
|
|
15
|
+
# (Truncated, InvalidType, InvalidEncoding, UnsupportedType) so
|
|
16
16
|
# callers can pattern-match on the SPEC's wire-violation categories
|
|
17
17
|
# without leaking the gem's internal exception classes.
|
|
18
18
|
#
|
|
19
|
-
# Public API is a single function —
|
|
19
|
+
# Public API is a single function — +.decode+. The decoder is
|
|
20
20
|
# stateless; the +MessagePack::Unpacker+ instance is built per call
|
|
21
21
|
# because callers always decode exactly one wire value at a time.
|
|
22
22
|
module Decoder
|
|
23
23
|
# Decode +bytes+ into one Ruby value and validate transitively
|
|
24
|
-
# against the SPEC type mapping. Raises
|
|
25
|
-
# or
|
|
24
|
+
# against the SPEC type mapping. Raises Truncated, InvalidType,
|
|
25
|
+
# or InvalidEncoding on wire violations.
|
|
26
26
|
#
|
|
27
27
|
# When a block is given, the decoded value is yielded and the block's
|
|
28
28
|
# result is returned — wire Value Objects use this to build themselves
|
|
29
29
|
# from the decoded payload. The block runs inside this method's
|
|
30
30
|
# rescue, so a Value Object's +ArgumentError+ invariant failure
|
|
31
|
-
# surfaces as
|
|
31
|
+
# surfaces as InvalidType without a separate Utils.with_boundary
|
|
32
32
|
# wrapper at the call site.
|
|
33
33
|
def self.decode(bytes)
|
|
34
|
-
value =
|
|
34
|
+
value = FACTORY.load(bytes.b)
|
|
35
35
|
validate_utf8!(value)
|
|
36
36
|
block_given? ? yield(value) : value
|
|
37
37
|
# msgpack gem raises the format/type errors below; +ArgumentError+
|
|
38
38
|
# comes from our ext-type validators (Handle id range, Exception type
|
|
39
39
|
# whitelist) and from a yielded block's Value Object invariants — both
|
|
40
|
-
# are wire violations, so both map to
|
|
40
|
+
# are wire violations, so both map to InvalidType.
|
|
41
41
|
rescue ::MessagePack::UnknownExtTypeError, ::MessagePack::MalformedFormatError,
|
|
42
42
|
::MessagePack::StackError, ::ArgumentError => e
|
|
43
43
|
raise InvalidType, e.message
|
|
@@ -53,8 +53,8 @@ module Kobako
|
|
|
53
53
|
# Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
|
|
54
54
|
# str family but does not validate the bytes; +bin+ family decodes
|
|
55
55
|
# to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
|
|
56
|
-
# str-typed leaf via
|
|
57
|
-
# payloads are validated transitively: +
|
|
56
|
+
# str-typed leaf via Utils.assert_utf8!. Kobako::Fault
|
|
57
|
+
# payloads are validated transitively: +ExtTypes#unpack_fault+
|
|
58
58
|
# feeds the inner ext-0x02 bytes back through this Decoder, so their
|
|
59
59
|
# +str+ fields are already covered by the time control returns here.
|
|
60
60
|
class << self
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "msgpack"
|
|
4
4
|
|
|
5
5
|
require_relative "error"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "ext_types"
|
|
7
7
|
|
|
8
8
|
module Kobako
|
|
9
9
|
module Codec
|
|
@@ -13,10 +13,10 @@ module Kobako
|
|
|
13
13
|
# The codec backbone is the official +msgpack+ gem: integers, floats,
|
|
14
14
|
# strings, arrays, and maps go through the gem's narrowest-encoding
|
|
15
15
|
# logic; the three kobako-specific ext types (0x00 Symbol, 0x01
|
|
16
|
-
# Capability Handle, 0x02 Exception envelope) are registered
|
|
17
|
-
# the
|
|
16
|
+
# Capability Handle, 0x02 Exception envelope) are registered by
|
|
17
|
+
# ExtTypes on the process-wide factory.
|
|
18
18
|
#
|
|
19
|
-
# Public API is a single function —
|
|
19
|
+
# Public API is a single function — +.encode+. The codec is stateless;
|
|
20
20
|
# there is no buffer accumulator and no streaming write API. Callers
|
|
21
21
|
# that need to concatenate multiple encodings build the bytes
|
|
22
22
|
# themselves.
|
|
@@ -32,7 +32,7 @@ module Kobako
|
|
|
32
32
|
# +NoMethodError+ is likewise reported as +UnsupportedType+ rather than
|
|
33
33
|
# propagating.
|
|
34
34
|
def self.encode(value)
|
|
35
|
-
|
|
35
|
+
FACTORY.dump(value)
|
|
36
36
|
rescue ::RangeError, ::NoMethodError => e
|
|
37
37
|
raise UnsupportedType, e.message
|
|
38
38
|
end
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -7,11 +7,11 @@ module Kobako
|
|
|
7
7
|
# The wire codec implements the binary contract pinned in
|
|
8
8
|
# {docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping.
|
|
9
9
|
# Every wire violation surfaces as a
|
|
10
|
-
# subclass of
|
|
10
|
+
# subclass of Error so callers can pattern-match on the specific
|
|
11
11
|
# fault while still rescuing all codec faults via this base class.
|
|
12
12
|
#
|
|
13
13
|
# Higher layers (e.g. the Sandbox dispatch loop) translate these into
|
|
14
|
-
# the public
|
|
14
|
+
# the public Kobako::SandboxError / Kobako::TrapError taxonomy.
|
|
15
15
|
class Error < StandardError; end
|
|
16
16
|
|
|
17
17
|
# Input ended before the type prefix or payload was fully consumed.
|
|
@@ -21,7 +21,7 @@ module Kobako
|
|
|
21
21
|
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag).
|
|
22
22
|
class InvalidType < Error; end
|
|
23
23
|
|
|
24
|
-
# A msgpack
|
|
24
|
+
# A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
|
|
25
25
|
# payload was not valid UTF-8 — both are wire violations per SPEC.
|
|
26
26
|
class InvalidEncoding < Error; end
|
|
27
27
|
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "msgpack"
|
|
4
|
+
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "utils"
|
|
7
|
+
require_relative "state"
|
|
8
|
+
require_relative "../handle"
|
|
9
|
+
require_relative "../fault"
|
|
10
|
+
|
|
11
|
+
module Kobako
|
|
12
|
+
module Codec
|
|
13
|
+
# The kobako wire ext-type conversions
|
|
14
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types)
|
|
15
|
+
# as pure functions: per-operation decode state is threaded in as an
|
|
16
|
+
# argument, so the module itself holds nothing. #build_factory assembles
|
|
17
|
+
# the one +MessagePack::Factory+ these conversions are registered on.
|
|
18
|
+
module ExtTypes
|
|
19
|
+
# MessagePack ext type code reserved for Symbol
|
|
20
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
21
|
+
# → ext 0x00). Module-private — mirrors +codec::EXT_SYMBOL+ on the
|
|
22
|
+
# Rust side.
|
|
23
|
+
EXT_SYMBOL = 0x00
|
|
24
|
+
# MessagePack ext type code reserved for Capability Handle
|
|
25
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
26
|
+
# → ext 0x01). Module-private — mirrors +codec::EXT_HANDLE+ on the
|
|
27
|
+
# Rust side.
|
|
28
|
+
EXT_HANDLE = 0x01
|
|
29
|
+
# MessagePack ext type code reserved for Exception envelope
|
|
30
|
+
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Ext Types
|
|
31
|
+
# → ext 0x02). Module-private — mirrors +codec::EXT_ERRENV+ on the
|
|
32
|
+
# Rust side.
|
|
33
|
+
EXT_ERRENV = 0x02
|
|
34
|
+
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Assemble a +MessagePack::Factory+ with the three kobako ext types
|
|
39
|
+
# registered, frozen because registration is its only mutation and
|
|
40
|
+
# happens exactly once. The stateful conversions resolve their
|
|
41
|
+
# per-operation state at call time, so one registered factory serves
|
|
42
|
+
# every thread.
|
|
43
|
+
def build_factory
|
|
44
|
+
factory = MessagePack::Factory.new
|
|
45
|
+
register_symbol(factory)
|
|
46
|
+
register_handle(factory)
|
|
47
|
+
register_fault(factory)
|
|
48
|
+
factory.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Symbol-to-name packer for the ext-0x00 registration.
|
|
52
|
+
def pack_symbol(symbol)
|
|
53
|
+
symbol.name
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Validate the ext-0x00 payload as UTF-8 and intern. Raises
|
|
57
|
+
# InvalidEncoding on invalid bytes — SPEC forbids the
|
|
58
|
+
# binary-encoding fallback that msgpack-gem's default unpacker
|
|
59
|
+
# would otherwise apply. The re-tag step lives here because the
|
|
60
|
+
# msgpack ext-type unpacker hands us binary bytes; the assertion
|
|
61
|
+
# itself is shared with Decoder via Utils.assert_utf8!. The
|
|
62
|
+
# +"Symbol"+ label keeps the error message in Ruby vocabulary
|
|
63
|
+
# rather than wire-ext-code vocabulary.
|
|
64
|
+
def unpack_symbol(payload)
|
|
65
|
+
name = payload.b.force_encoding(Encoding::UTF_8)
|
|
66
|
+
Utils.assert_utf8!(name, "Symbol payload")
|
|
67
|
+
name.to_sym
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Handle-id packer for the ext-0x01 registration: the fixext-4
|
|
71
|
+
# big-endian id frame.
|
|
72
|
+
def pack_handle(handle)
|
|
73
|
+
[handle.id].pack("N")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Peel off the fixext-4 frame, hand the bytes to the
|
|
77
|
+
# Host-Gem-internal +Kobako::Handle.restore+ factory, and
|
|
78
|
+
# translate the +ArgumentError+ raised by Handle's invariants
|
|
79
|
+
# into a wire-layer +InvalidType+ via Codec::Utils.with_boundary.
|
|
80
|
+
# The Value Object owns the id-range contract; this method only
|
|
81
|
+
# owns the frame shape. Records the Handle sighting on +state+ so a
|
|
82
|
+
# Handle-free decode can skip the downstream resolution walk.
|
|
83
|
+
def unpack_handle(payload, state)
|
|
84
|
+
state.record_handle!
|
|
85
|
+
bytes = payload.b
|
|
86
|
+
raise InvalidType, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
87
|
+
|
|
88
|
+
id = bytes.unpack1("N") # : Integer
|
|
89
|
+
Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Encode the inner ext-0x02 map via Encoder (not the raw factory) so
|
|
93
|
+
# the embedded payload flows through the same boundary as a top-level
|
|
94
|
+
# encode — nested kobako values (Handle, nested Fault) reach the
|
|
95
|
+
# registered ext-type packers. A +details+ chain nested past the
|
|
96
|
+
# +state+ depth cap has no wire representation and surfaces as
|
|
97
|
+
# +UnsupportedType+. In a payload position (+state+ inside a
|
|
98
|
+
# forbid_faults bracket) the envelope has no wire representation at
|
|
99
|
+
# all, so the refusal routes the value into the position's
|
|
100
|
+
# non-representable handling — the Dispatcher's auto-wrap rescue,
|
|
101
|
+
# or a raise at the yield site.
|
|
102
|
+
def pack_fault(fault, state)
|
|
103
|
+
if state.faults_forbidden?
|
|
104
|
+
raise UnsupportedType, "Kobako::Fault has no wire representation in a payload position"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
state.within_ext_frame(UnsupportedType) do
|
|
108
|
+
Encoder.encode("type" => fault.type, "message" => fault.message, "details" => fault.details)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
|
|
113
|
+
# inside Decoder.decode's block form, so the value-object's
|
|
114
|
+
# +ArgumentError+ invariants surface as +InvalidType+ through the
|
|
115
|
+
# decoder boundary. Inner decode goes through Decoder (not the raw
|
|
116
|
+
# factory) so the embedded +str+ payloads flow through the same
|
|
117
|
+
# UTF-8 validation as a top-level decode. A nested ext 0x02 in
|
|
118
|
+
# +details+ re-enters this method, so the +state+ ext-frame guard
|
|
119
|
+
# bounds the chain depth to keep it from exhausting the native stack.
|
|
120
|
+
# In a payload position (+state+ inside a forbid_faults bracket) the
|
|
121
|
+
# envelope is a wire violation outright — its sole legal position is
|
|
122
|
+
# the Response fault field.
|
|
123
|
+
def unpack_fault(payload, state)
|
|
124
|
+
if state.faults_forbidden?
|
|
125
|
+
raise InvalidType, "Fault envelope (ext 0x02) is not a legal value in a payload position"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
state.within_ext_frame(InvalidType) do
|
|
129
|
+
Decoder.decode(payload) do |map|
|
|
130
|
+
raise InvalidType, "Fault payload must be a map" unless map.is_a?(Hash)
|
|
131
|
+
|
|
132
|
+
Kobako::Fault.new(type: map["type"], message: map["message"], details: map["details"])
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def register_symbol(factory)
|
|
138
|
+
factory.register_type(
|
|
139
|
+
EXT_SYMBOL, Symbol,
|
|
140
|
+
packer: ->(symbol) { ExtTypes.pack_symbol(symbol) },
|
|
141
|
+
unpacker: ->(payload) { ExtTypes.unpack_symbol(payload) }
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def register_handle(factory)
|
|
146
|
+
factory.register_type(
|
|
147
|
+
EXT_HANDLE, Kobako::Handle,
|
|
148
|
+
packer: ->(handle) { ExtTypes.pack_handle(handle) },
|
|
149
|
+
unpacker: ->(payload) { ExtTypes.unpack_handle(payload, State.current) }
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def register_fault(factory)
|
|
154
|
+
factory.register_type(
|
|
155
|
+
EXT_ERRENV, Kobako::Fault,
|
|
156
|
+
packer: ->(fault) { ExtTypes.pack_fault(fault, State.current) },
|
|
157
|
+
unpacker: ->(payload) { ExtTypes.unpack_fault(payload, State.current) }
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The process-wide registered factory: ext registration is paid once at
|
|
163
|
+
# load, and a registered +MessagePack::Factory+ only reads its type
|
|
164
|
+
# registry afterwards, so every thread shares this instance for byte
|
|
165
|
+
# work.
|
|
166
|
+
FACTORY = ExtTypes.build_factory
|
|
167
|
+
private_constant :FACTORY
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -5,16 +5,16 @@ require_relative "../handle"
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Codec
|
|
7
7
|
# Substitutes Capability Handles into and out of a Ruby value tree at
|
|
8
|
-
# the host↔guest boundary.
|
|
8
|
+
# the host↔guest boundary. #deep_wrap allocates a +Kobako::Handle+ for
|
|
9
9
|
# each non-wire-representable leaf on the host→guest +#run+ argument
|
|
10
|
-
# path;
|
|
11
|
-
# host object on every guest→host value path.
|
|
12
|
-
# by-value codec-type predicate that decides which leaves
|
|
10
|
+
# path; #deep_restore resolves each wire-decoded Handle back to its
|
|
11
|
+
# host object on every guest→host value path. #representable? is the
|
|
12
|
+
# by-value codec-type predicate that decides which leaves #deep_wrap
|
|
13
13
|
# must wrap: the closed 12-entry wire type set
|
|
14
14
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
15
15
|
# Mapping).
|
|
16
16
|
#
|
|
17
|
-
# All helpers are pure except
|
|
17
|
+
# All helpers are pure except #deep_wrap, whose only side effect is
|
|
18
18
|
# allocating new Handle ids into the supplied table.
|
|
19
19
|
module HandleWalk
|
|
20
20
|
module_function
|
|
@@ -24,7 +24,7 @@ module Kobako
|
|
|
24
24
|
# unsigned +uint 64+ maximum
|
|
25
25
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
26
26
|
# Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
|
|
27
|
-
# Anchored as a +Range+ so
|
|
27
|
+
# Anchored as a +Range+ so #primitive_type? stays a single
|
|
28
28
|
# dispatch line. This is the codec's encode domain — not to
|
|
29
29
|
# be confused with the Handle id range, which lives on
|
|
30
30
|
# +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
|
|
@@ -41,13 +41,13 @@ module Kobako
|
|
|
41
41
|
# representable. Integers outside the codec's signed-64 /
|
|
42
42
|
# unsigned-64 union are rejected so the predicate agrees with the
|
|
43
43
|
# msgpack gem's encode-time +RangeError+ behaviour the codec
|
|
44
|
-
# already surfaces as
|
|
44
|
+
# already surfaces as UnsupportedType.
|
|
45
45
|
def representable?(value)
|
|
46
46
|
primitive_type?(value) || container_representable?(value)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
50
|
-
# leaf that fails
|
|
50
|
+
# leaf that fails #representable? with a +Kobako::Handle+
|
|
51
51
|
# allocated from +handler+. The
|
|
52
52
|
# walk only descends through representable container shapes
|
|
53
53
|
# (Array, Hash) one structural level at a time; a non-representable
|
|
@@ -61,13 +61,9 @@ module Kobako
|
|
|
61
61
|
# whose leaves are either representable or +Kobako::Handle+
|
|
62
62
|
# tokens.
|
|
63
63
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# inside a block would resolve against the enclosing +self+
|
|
68
|
-
# (still +HandleWalk+ at definition time, but the qualified form
|
|
69
|
-
# keeps the dispatch readable when the recursive call sits inside a
|
|
70
|
-
# Proc captured from elsewhere).
|
|
64
|
+
# Recursive calls spell the +HandleWalk.+ receiver so the dispatch
|
|
65
|
+
# stays valid even when a block is captured and run under a
|
|
66
|
+
# different +self+ (+module_function+ privatizes the instance copies).
|
|
71
67
|
def deep_wrap(value, handler)
|
|
72
68
|
case value
|
|
73
69
|
when ::Array then value.map { |element| HandleWalk.deep_wrap(element, handler) }
|
|
@@ -79,7 +75,7 @@ module Kobako
|
|
|
79
75
|
|
|
80
76
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
81
77
|
# +Kobako::Handle+ leaf with the host-side object +handler+ resolves
|
|
82
|
-
# it to. The symmetric inverse of
|
|
78
|
+
# it to. The symmetric inverse of #deep_wrap: that walk allocates objects
|
|
83
79
|
# into Handles on the host→guest argument path; this walk resolves
|
|
84
80
|
# Handles back to their objects wherever a guest→host payload carries
|
|
85
81
|
# one — an invocation result, a yield-block result, or a dispatch
|
|
@@ -107,9 +103,9 @@ module Kobako
|
|
|
107
103
|
end
|
|
108
104
|
end
|
|
109
105
|
|
|
110
|
-
# The non-container branch of
|
|
106
|
+
# The non-container branch of #representable?: returns +true+ for
|
|
111
107
|
# the scalar leaves and an existing Handle. Not part of the
|
|
112
|
-
# public surface; reach for
|
|
108
|
+
# public surface; reach for #representable? instead.
|
|
113
109
|
def primitive_type?(value)
|
|
114
110
|
case value
|
|
115
111
|
when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
|
|
@@ -118,10 +114,10 @@ module Kobako
|
|
|
118
114
|
end
|
|
119
115
|
end
|
|
120
116
|
|
|
121
|
-
# The container branch of
|
|
117
|
+
# The container branch of #representable?: recurses into Array
|
|
122
118
|
# elements and Hash key+value pairs through the public
|
|
123
|
-
#
|
|
124
|
-
#
|
|
119
|
+
# #representable?. Not part of the public surface; reach for
|
|
120
|
+
# #representable? instead.
|
|
125
121
|
def container_representable?(value)
|
|
126
122
|
case value
|
|
127
123
|
when ::Array then value.all? { |element| HandleWalk.representable?(element) }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Codec
|
|
5
|
+
# Codec-internal, per-thread state of the operation in flight: the
|
|
6
|
+
# ext-envelope nesting depth and whether a Capability Handle crossed
|
|
7
|
+
# the current decode. Thread scoping is what makes plain instance
|
|
8
|
+
# variables sound — host codec calls run synchronously on their
|
|
9
|
+
# owning thread, and a nested decode (an ext 0x02 Fault re-entering
|
|
10
|
+
# through its +details+) reuses the same thread instance, so the
|
|
11
|
+
# depth counter accumulates across the re-entry instead of resetting.
|
|
12
|
+
class State
|
|
13
|
+
# An ext 0x02 (Fault) envelope nests through its +details+ field, and
|
|
14
|
+
# each level re-enters the codec with a fresh +MessagePack+ unpacker
|
|
15
|
+
# whose built-in stack guard resets — so ext-envelope depth is tracked
|
|
16
|
+
# here instead. The cap matches the wire's overall nesting bound and
|
|
17
|
+
# keeps a nested chain from exhausting the native stack: an over-deep
|
|
18
|
+
# chain fails as a clean wire error, never a stack-level trap.
|
|
19
|
+
MAX_EXT_DEPTH = 128
|
|
20
|
+
private_constant :MAX_EXT_DEPTH
|
|
21
|
+
|
|
22
|
+
# Thread-local slot holding the calling thread's State.
|
|
23
|
+
STATE_KEY = :__kobako_codec_state__
|
|
24
|
+
private_constant :STATE_KEY
|
|
25
|
+
|
|
26
|
+
# The calling thread's State, built on first use so the mutable
|
|
27
|
+
# state stays isolated to the thread that runs the codec call.
|
|
28
|
+
def self.current
|
|
29
|
+
Thread.current[STATE_KEY] ||= new
|
|
30
|
+
end
|
|
31
|
+
private_class_method :new
|
|
32
|
+
|
|
33
|
+
def initialize
|
|
34
|
+
@ext_depth = 0
|
|
35
|
+
@carried_handle = false
|
|
36
|
+
@faults_forbidden = false
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Bracket a decode and return the block's result together with
|
|
40
|
+
# whether the decoded tree carried an ext 0x01 Capability Handle.
|
|
41
|
+
# ExtTypes#unpack_handle is the sole chokepoint every Handle passes
|
|
42
|
+
# through, so one decode pass records the whole tree and a caller
|
|
43
|
+
# can skip an all-identity Handle-resolution walk when none was
|
|
44
|
+
# present.
|
|
45
|
+
def track_handles
|
|
46
|
+
@carried_handle = false
|
|
47
|
+
result = yield
|
|
48
|
+
[result, @carried_handle]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Record that an ext 0x01 Capability Handle crossed the current
|
|
52
|
+
# decode; #track_handles reports it to the bracketing caller.
|
|
53
|
+
def record_handle!
|
|
54
|
+
@carried_handle = true
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Bracket a codec operation in a payload position, where an ext 0x02
|
|
58
|
+
# Fault envelope has no legal wire representation: the fault field of
|
|
59
|
+
# an error Response is its only home. The ext-type conversions
|
|
60
|
+
# consult #faults_forbidden? and refuse the envelope in both
|
|
61
|
+
# directions while the bracket is open. Save/restore keeps a nested
|
|
62
|
+
# legal operation on the same thread unaffected.
|
|
63
|
+
def forbid_faults
|
|
64
|
+
previous = @faults_forbidden
|
|
65
|
+
@faults_forbidden = true
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
@faults_forbidden = previous
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Whether the operation in flight sits inside a #forbid_faults
|
|
72
|
+
# bracket — i.e. in a payload position where ext 0x02 is a wire
|
|
73
|
+
# violation.
|
|
74
|
+
def faults_forbidden?
|
|
75
|
+
@faults_forbidden
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Track ext-envelope re-entry depth and refuse a chain past
|
|
79
|
+
# MAX_EXT_DEPTH, raising +over_limit+ so the failure lands in the
|
|
80
|
+
# caller's existing wire-error class. The next depth is checked before
|
|
81
|
+
# it is committed, so an over-deep rejection leaves the counter
|
|
82
|
+
# untouched, and the +ensure+ restores the entry value on the way out.
|
|
83
|
+
def within_ext_frame(over_limit)
|
|
84
|
+
depth = @ext_depth + 1
|
|
85
|
+
raise over_limit, "ext envelope nesting exceeds #{MAX_EXT_DEPTH} levels" if depth > MAX_EXT_DEPTH
|
|
86
|
+
|
|
87
|
+
@ext_depth = depth
|
|
88
|
+
begin
|
|
89
|
+
yield
|
|
90
|
+
ensure
|
|
91
|
+
@ext_depth = depth - 1
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private_constant :State
|
|
97
|
+
end
|
|
98
|
+
end
|