kobako 0.1.2 → 0.2.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/Cargo.lock +1 -1
- data/README.md +95 -60
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/wasm/cache.rs +39 -1
- data/ext/kobako/src/wasm/dispatch.rs +20 -20
- data/ext/kobako/src/wasm/host_state.rs +261 -34
- data/ext/kobako/src/wasm/instance.rs +467 -272
- data/ext/kobako/src/wasm.rs +50 -19
- data/lib/kobako/capture.rb +46 -0
- data/lib/kobako/codec/decoder.rb +66 -0
- data/lib/kobako/codec/encoder.rb +37 -0
- data/lib/kobako/codec/error.rb +33 -0
- data/lib/kobako/codec/factory.rb +155 -0
- data/lib/kobako/codec/utils.rb +55 -0
- data/lib/kobako/codec.rb +27 -0
- data/lib/kobako/errors.rb +24 -1
- data/lib/kobako/outcome/panic.rb +42 -0
- data/lib/kobako/outcome.rb +133 -0
- data/lib/kobako/rpc/dispatcher.rb +169 -0
- data/lib/kobako/rpc/envelope.rb +118 -0
- data/lib/kobako/{wire/exception.rb → rpc/fault.rb} +6 -4
- data/lib/kobako/{wire → rpc}/handle.rb +4 -2
- data/lib/kobako/{registry → rpc}/handle_table.rb +9 -9
- data/lib/kobako/{registry/service_group.rb → rpc/namespace.rb} +20 -11
- data/lib/kobako/rpc/server.rb +156 -0
- data/lib/kobako/rpc.rb +11 -0
- data/lib/kobako/sandbox.rb +149 -69
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako/wasm.rb +6 -16
- data/lib/kobako.rb +2 -0
- data/sig/kobako/capture.rbs +13 -0
- data/sig/kobako/codec/decoder.rbs +11 -0
- data/sig/kobako/codec/encoder.rbs +7 -0
- data/sig/kobako/codec/error.rbs +18 -0
- data/sig/kobako/codec/factory.rbs +31 -0
- data/sig/kobako/codec/utils.rbs +9 -0
- data/sig/kobako/errors.rbs +52 -0
- data/sig/kobako/outcome/panic.rbs +34 -0
- data/sig/kobako/outcome.rbs +24 -0
- data/sig/kobako/rpc/dispatcher.rbs +33 -0
- data/sig/kobako/rpc/envelope.rbs +51 -0
- data/sig/kobako/rpc/fault.rbs +20 -0
- data/sig/kobako/rpc/handle.rbs +19 -0
- data/sig/kobako/rpc/handle_table.rbs +25 -0
- data/sig/kobako/rpc/namespace.rbs +24 -0
- data/sig/kobako/rpc/server.rbs +37 -0
- data/sig/kobako/rpc.rbs +4 -0
- data/sig/kobako/sandbox.rbs +53 -0
- data/sig/kobako/wasm.rbs +37 -0
- data/sig/kobako.rbs +0 -1
- metadata +37 -17
- data/lib/kobako/registry/dispatcher.rb +0 -168
- data/lib/kobako/registry.rb +0 -160
- data/lib/kobako/sandbox/outcome_decoder.rb +0 -100
- data/lib/kobako/sandbox/output_buffer.rb +0 -79
- data/lib/kobako/wire/codec/decoder.rb +0 -87
- data/lib/kobako/wire/codec/encoder.rb +0 -41
- data/lib/kobako/wire/codec/error.rb +0 -35
- data/lib/kobako/wire/codec/factory.rb +0 -136
- data/lib/kobako/wire/codec.rb +0 -44
- data/lib/kobako/wire/envelope/payloads.rb +0 -145
- data/lib/kobako/wire/envelope.rb +0 -147
- data/lib/kobako/wire.rb +0 -40
data/lib/kobako/registry.rb
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "msgpack"
|
|
4
|
-
require_relative "errors"
|
|
5
|
-
require_relative "wire"
|
|
6
|
-
require_relative "registry/service_group"
|
|
7
|
-
require_relative "registry/handle_table"
|
|
8
|
-
require_relative "registry/dispatcher"
|
|
9
|
-
|
|
10
|
-
module Kobako
|
|
11
|
-
# Kobako::Registry — per-Sandbox container of Service Groups and Handle
|
|
12
|
-
# state. Manages capability injection and guest-initiated RPC dispatch
|
|
13
|
-
# ({SPEC.md B-07..B-21}[link:../../SPEC.md]).
|
|
14
|
-
#
|
|
15
|
-
# Public API:
|
|
16
|
-
#
|
|
17
|
-
# registry = Kobako::Registry.new
|
|
18
|
-
# group = registry.define(:MyService) # => ServiceGroup
|
|
19
|
-
# group.bind(:KV, kv_object) # => group (chainable)
|
|
20
|
-
# registry.to_preamble # => array for Frame 1
|
|
21
|
-
# registry.dispatch(request_bytes) # => msgpack bytes (delegated to Dispatcher)
|
|
22
|
-
#
|
|
23
|
-
# Service Groups are defined in +Kobako::Registry::ServiceGroup+
|
|
24
|
-
# (lib/kobako/registry/service_group.rb). The opaque Handle allocator lives
|
|
25
|
-
# in +Kobako::Registry::HandleTable+ (lib/kobako/registry/handle_table.rb).
|
|
26
|
-
# Dispatch helpers live in +Kobako::Registry::Dispatcher+
|
|
27
|
-
# (lib/kobako/registry/dispatcher.rb).
|
|
28
|
-
class Registry
|
|
29
|
-
# Ruby constant-name pattern shared by Group and Member names
|
|
30
|
-
# ({SPEC.md B-07/B-08 Notes}[link:../../SPEC.md]). Referenced by both
|
|
31
|
-
# +#define+ here and +ServiceGroup#bind+ — single source of truth so
|
|
32
|
-
# the validation rule cannot drift between the two boundaries.
|
|
33
|
-
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
34
|
-
|
|
35
|
-
# Build a fresh Registry. +handle_table+ is an internal seam that
|
|
36
|
-
# injects a pre-configured +HandleTable+; tests pass one whose +next_id+
|
|
37
|
-
# is pinned near +MAX_ID+ to exercise the B-21 cap-exhaustion path
|
|
38
|
-
# without 2³¹ allocations. Production callers leave it at the default.
|
|
39
|
-
def initialize(handle_table: HandleTable.new)
|
|
40
|
-
@groups = {}
|
|
41
|
-
@handle_table = handle_table
|
|
42
|
-
@sealed = false
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Declare or retrieve the Group named +name+ (idempotent — SPEC.md B-10).
|
|
46
|
-
# +name+ is a constant-form name as a +Symbol+ or +String+ (must satisfy
|
|
47
|
-
# +NAME_PATTERN+). Returns the +Kobako::Registry::ServiceGroup+ for that
|
|
48
|
-
# name, creating it if it does not exist. Raises +ArgumentError+ when
|
|
49
|
-
# +name+ is malformed, or when called after the owning Sandbox has been
|
|
50
|
-
# sealed by +#run+.
|
|
51
|
-
def define(name)
|
|
52
|
-
raise ArgumentError, "cannot define after Sandbox#run has been invoked" if @sealed
|
|
53
|
-
|
|
54
|
-
name_str = name.to_s
|
|
55
|
-
unless NAME_PATTERN.match?(name_str)
|
|
56
|
-
raise ArgumentError,
|
|
57
|
-
"GroupName must match #{NAME_PATTERN.inspect} (got #{name.inspect})"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
@groups[name_str] ||= ServiceGroup.new(name_str)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Resolve a +target+ path of the form +"GroupName::MemberName"+ to the
|
|
64
|
-
# bound Host object. +target+ is a two-level path using the +::+
|
|
65
|
-
# separator. Returns the bound Host object. Raises +KeyError+ when the
|
|
66
|
-
# group or the member is not bound.
|
|
67
|
-
def lookup(target)
|
|
68
|
-
group, member_name, group_name = resolve_pair(target)
|
|
69
|
-
raise KeyError, "no service group named #{group_name.inspect}" if group.nil?
|
|
70
|
-
raise KeyError, "no member #{target.inspect} bound in registry" unless member_name
|
|
71
|
-
|
|
72
|
-
group.fetch(member_name)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Returns +true+ when +target+ (a +"GroupName::MemberName"+ path) resolves
|
|
76
|
-
# to a bound member, +false+ otherwise.
|
|
77
|
-
def bound?(target)
|
|
78
|
-
group, member_name, = resolve_pair(target)
|
|
79
|
-
!group.nil? && !member_name.nil? && !group[member_name].nil?
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Returns all declared +Kobako::Registry::ServiceGroup+ instances as an
|
|
83
|
-
# +Array+.
|
|
84
|
-
def groups
|
|
85
|
-
@groups.values
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Returns the number of declared groups as an +Integer+.
|
|
89
|
-
def size
|
|
90
|
-
@groups.size
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Returns +true+ when no groups have been declared, +false+ otherwise.
|
|
94
|
-
def empty?
|
|
95
|
-
@groups.empty?
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# Structured Frame 1 description. Called by +Sandbox#run+ when assembling
|
|
99
|
-
# stdin Frame 1 ({SPEC.md B-02}[link:../../SPEC.md]). Returns an
|
|
100
|
-
# unencoded preamble array — an +Array+ of two-element +[name, members]+
|
|
101
|
-
# arrays, one per declared group.
|
|
102
|
-
def to_preamble
|
|
103
|
-
@groups.values.map(&:to_preamble)
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery
|
|
107
|
-
# ({SPEC.md B-02}[link:../../SPEC.md]). Uses plain MessagePack (no
|
|
108
|
-
# kobako ext types) because the preamble contains only strings — no
|
|
109
|
-
# Handles or Exception envelopes. Structure:
|
|
110
|
-
# +[["GroupName", ["MemberA", "MemberB"]], ...]+. Returns a binary
|
|
111
|
-
# +String+ of msgpack bytes.
|
|
112
|
-
def guest_preamble
|
|
113
|
-
MessagePack.pack(to_preamble)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Mark the Registry as sealed. Called by `Sandbox#run` on first run.
|
|
117
|
-
# After sealing, #define raises ArgumentError. Idempotent.
|
|
118
|
-
def seal!
|
|
119
|
-
@sealed = true
|
|
120
|
-
self
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
# Returns +true+ when {#seal!} has been called, +false+ otherwise.
|
|
124
|
-
def sealed?
|
|
125
|
-
@sealed
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
# Reset the HandleTable for a new +#run+ boundary. Called by +Sandbox#run+
|
|
129
|
-
# before each invocation ({SPEC.md B-19}[link:../../SPEC.md]).
|
|
130
|
-
def reset_handles!
|
|
131
|
-
@handle_table.reset!
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
# Dispatch a single RPC request and return the encoded response bytes
|
|
135
|
-
# ({SPEC.md B-12}[link:../../SPEC.md]). +request_bytes+ is a
|
|
136
|
-
# msgpack-encoded Request envelope. Called by the Rust ext from inside
|
|
137
|
-
# +__kobako_rpc_call+. Always returns a binary +String+ — never raises.
|
|
138
|
-
# Delegates to +Dispatcher.dispatch+ which reifies any failure as a
|
|
139
|
-
# +Response.err+ envelope so the guest sees the failure as a normal RPC
|
|
140
|
-
# error rather than a wasm trap.
|
|
141
|
-
def dispatch(request_bytes)
|
|
142
|
-
Dispatcher.dispatch(request_bytes, self)
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Expose the +Kobako::Registry::HandleTable+ for tests and wire-layer
|
|
146
|
-
# Handle wrapping.
|
|
147
|
-
attr_reader :handle_table
|
|
148
|
-
|
|
149
|
-
private
|
|
150
|
-
|
|
151
|
-
# Split +target+ on the +::+ separator and resolve the group half.
|
|
152
|
-
# Returns +[group_or_nil, member_str_or_nil, group_name_str]+ so each
|
|
153
|
-
# public method ({#lookup} / {#bound?}) only owns its boundary
|
|
154
|
-
# semantics (raise vs predicate).
|
|
155
|
-
def resolve_pair(target)
|
|
156
|
-
group_name, member_name = target.to_s.split("::", 2)
|
|
157
|
-
[@groups[group_name], member_name, group_name]
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
end
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kobako
|
|
4
|
-
class Sandbox
|
|
5
|
-
# Pure-function decoder for the OUTCOME_BUFFER bytes returned by
|
|
6
|
-
# +__kobako_run+. Maps a tagged msgpack envelope to either the unwrapped
|
|
7
|
-
# mruby return value or a raised three-layer
|
|
8
|
-
# ({SPEC.md "Error Scenarios"}[link:../../../SPEC.md]) exception.
|
|
9
|
-
#
|
|
10
|
-
# * tag 0x01, decode OK → return Result.value
|
|
11
|
-
# * tag 0x01, decode fails → SandboxError (E-09)
|
|
12
|
-
# * tag 0x02, origin="service" → ServiceError (E-13)
|
|
13
|
-
# * tag 0x02, origin="sandbox"/missing → SandboxError (E-04..E-07)
|
|
14
|
-
# * tag 0x02, decode fails → SandboxError (E-08)
|
|
15
|
-
# * unknown tag → TrapError (E-03)
|
|
16
|
-
module OutcomeDecoder
|
|
17
|
-
module_function
|
|
18
|
-
|
|
19
|
-
def decode(bytes)
|
|
20
|
-
tag, body = split_outcome_tag(bytes)
|
|
21
|
-
case tag
|
|
22
|
-
when Kobako::Wire::Envelope::OUTCOME_TAG_RESULT
|
|
23
|
-
decode_result(body)
|
|
24
|
-
when Kobako::Wire::Envelope::OUTCOME_TAG_PANIC
|
|
25
|
-
decode_panic(body)
|
|
26
|
-
else
|
|
27
|
-
raise trap_for_tag(tag)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# TrapError for unknown or absent tag
|
|
32
|
-
# ({SPEC.md ABI Signatures}[link:../../../SPEC.md]: len=0 and unknown-tag
|
|
33
|
-
# both walk the trap path).
|
|
34
|
-
def trap_for_tag(tag)
|
|
35
|
-
return TrapError.new("guest exited without writing an outcome (len=0)") if tag.nil?
|
|
36
|
-
|
|
37
|
-
TrapError.new(format("unknown outcome tag 0x%<tag>02x", tag: tag))
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def split_outcome_tag(bytes)
|
|
41
|
-
bytes = bytes.b
|
|
42
|
-
[bytes.getbyte(0), bytes.byteslice(1, bytes.bytesize - 1)]
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Decode failure on a known Result tag is a SandboxError (E-09): the
|
|
46
|
-
# framing was fine, but the wrapped value is unrepresentable.
|
|
47
|
-
def decode_result(body)
|
|
48
|
-
Kobako::Wire::Envelope.decode_result(body).value
|
|
49
|
-
rescue Kobako::Wire::Codec::Error => e
|
|
50
|
-
raise wire_violation_error("result envelope decode failed: #{e.message}")
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
# Decode failure on a known Panic tag is a SandboxError (E-08). Either
|
|
54
|
-
# path raises — on success the decoded Panic is mapped to its three-
|
|
55
|
-
# layer exception via +build_panic_error+ and raised; on wire-decode
|
|
56
|
-
# failure the rescue path raises the wire-violation +SandboxError+.
|
|
57
|
-
# Symmetric with +decode_result+ — both have signature
|
|
58
|
-
# "decode body and return value, or raise".
|
|
59
|
-
def decode_panic(body)
|
|
60
|
-
raise build_panic_error(Kobako::Wire::Envelope.decode_panic(body))
|
|
61
|
-
rescue Kobako::Wire::Codec::Error => e
|
|
62
|
-
raise wire_violation_error("panic envelope decode failed: #{e.message}")
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Map a decoded Panic envelope into the corresponding three-layer
|
|
66
|
-
# Ruby exception. +origin == "service"+ → ServiceError (with the
|
|
67
|
-
# +::Disconnected+ subclass selected when the panic carries the
|
|
68
|
-
# disconnected sentinel —
|
|
69
|
-
# {SPEC "Error Classes"}[link:../../../SPEC.md]); everything else
|
|
70
|
-
# → SandboxError.
|
|
71
|
-
def build_panic_error(panic)
|
|
72
|
-
panic_target_class(panic).new(
|
|
73
|
-
panic.message,
|
|
74
|
-
origin: panic.origin,
|
|
75
|
-
klass: panic.klass,
|
|
76
|
-
backtrace_lines: panic.backtrace,
|
|
77
|
-
details: panic.details
|
|
78
|
-
)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# {SPEC "Error Classes"}[link:../../../SPEC.md]: when
|
|
82
|
-
# +origin="service"+ and the panic +class+ field names
|
|
83
|
-
# +ServiceError::Disconnected+, surface that subclass so callers can
|
|
84
|
-
# rescue the disconnected path specifically (E-14).
|
|
85
|
-
def panic_target_class(panic)
|
|
86
|
-
return SandboxError unless panic.origin == Kobako::Wire::Envelope::Panic::ORIGIN_SERVICE
|
|
87
|
-
|
|
88
|
-
panic.klass == "Kobako::ServiceError::Disconnected" ? ServiceError::Disconnected : ServiceError
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def wire_violation_error(message)
|
|
92
|
-
SandboxError.new(
|
|
93
|
-
message,
|
|
94
|
-
origin: Kobako::Wire::Envelope::Panic::ORIGIN_SANDBOX,
|
|
95
|
-
klass: "Kobako::WireError"
|
|
96
|
-
)
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kobako
|
|
4
|
-
class Sandbox
|
|
5
|
-
# In-memory bounded byte buffer for one of the guest's output channels.
|
|
6
|
-
# Tracks accumulated bytes (binary-encoded) and enforces the per-channel
|
|
7
|
-
# cap by truncating-with-marker ({SPEC.md B-04}[link:../../../SPEC.md]).
|
|
8
|
-
#
|
|
9
|
-
# When the accumulated byte count would exceed the limit, the buffer keeps
|
|
10
|
-
# as many leading bytes as fit and seals itself. Subsequent appends are
|
|
11
|
-
# discarded. On the next read, +OUTPUT_TRUNCATION_MARKER+ is appended to
|
|
12
|
-
# signal the overflow to the caller.
|
|
13
|
-
class OutputBuffer
|
|
14
|
-
# Marker appended to a buffer that hit its capture limit
|
|
15
|
-
# ({SPEC.md B-04}[link:../../../SPEC.md]).
|
|
16
|
-
OUTPUT_TRUNCATION_MARKER = "[truncated]"
|
|
17
|
-
|
|
18
|
-
attr_reader :limit
|
|
19
|
-
|
|
20
|
-
def initialize(limit)
|
|
21
|
-
raise ArgumentError, "limit must be a positive Integer" unless limit.is_a?(Integer) && limit.positive?
|
|
22
|
-
|
|
23
|
-
@limit = limit
|
|
24
|
-
@bytes = String.new(encoding: Encoding::ASCII_8BIT)
|
|
25
|
-
@truncated = false
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Append +bytes+ to the buffer. If the append would push the
|
|
29
|
-
# cumulative byte count past the limit, the buffer keeps as many
|
|
30
|
-
# leading bytes as fit and seals itself; subsequent appends are
|
|
31
|
-
# discarded. {SPEC.md B-04}[link:../../../SPEC.md] — truncation is a
|
|
32
|
-
# non-error outcome.
|
|
33
|
-
def <<(bytes)
|
|
34
|
-
return self if @truncated
|
|
35
|
-
|
|
36
|
-
appended = bytes.to_s.b
|
|
37
|
-
room = @limit - @bytes.bytesize
|
|
38
|
-
if appended.bytesize <= room
|
|
39
|
-
@bytes << appended
|
|
40
|
-
else
|
|
41
|
-
@bytes << appended.byteslice(0, room) if room.positive?
|
|
42
|
-
@truncated = true
|
|
43
|
-
end
|
|
44
|
-
self
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Returns +true+ when the buffer was sealed by an overflow.
|
|
48
|
-
def truncated?
|
|
49
|
-
@truncated
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Returns the number of bytes currently stored.
|
|
53
|
-
def bytesize
|
|
54
|
-
@bytes.bytesize
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# Returns +true+ when the buffer is empty.
|
|
58
|
-
def empty?
|
|
59
|
-
@bytes.empty?
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# Returns the accumulated bytes as a UTF-8 String, with the
|
|
63
|
-
# +[truncated]+ marker appended when the buffer overflowed.
|
|
64
|
-
def to_s
|
|
65
|
-
copy = @bytes.dup
|
|
66
|
-
copy << OUTPUT_TRUNCATION_MARKER.b if @truncated
|
|
67
|
-
copy.force_encoding(Encoding::UTF_8)
|
|
68
|
-
copy.valid_encoding? ? copy : copy.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Reset the buffer to empty. Used at the per-+#run+ boundary.
|
|
72
|
-
def clear
|
|
73
|
-
@bytes.clear
|
|
74
|
-
@truncated = false
|
|
75
|
-
self
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "msgpack"
|
|
4
|
-
|
|
5
|
-
require_relative "error"
|
|
6
|
-
require_relative "../handle"
|
|
7
|
-
require_relative "../exception"
|
|
8
|
-
require_relative "factory"
|
|
9
|
-
|
|
10
|
-
module Kobako
|
|
11
|
-
module Wire
|
|
12
|
-
module Codec
|
|
13
|
-
# Module-level entry point for the host side of the kobako wire
|
|
14
|
-
# (SPEC.md → Wire Codec → Type Mapping).
|
|
15
|
-
#
|
|
16
|
-
# Translates msgpack gem exceptions into the kobako error taxonomy
|
|
17
|
-
# ({Truncated}, {InvalidType}, {InvalidEncoding}, {UnsupportedType}) so
|
|
18
|
-
# callers can pattern-match on the SPEC's wire-violation categories
|
|
19
|
-
# without leaking the gem's internal exception classes.
|
|
20
|
-
#
|
|
21
|
-
# Public API is a single function — {.decode}. The decoder is
|
|
22
|
-
# stateless; the +MessagePack::Unpacker+ instance is built per call
|
|
23
|
-
# because callers always decode exactly one wire value at a time.
|
|
24
|
-
module Decoder
|
|
25
|
-
# The msgpack gem raises these for type/format violations; +ArgumentError+
|
|
26
|
-
# also comes from our ext-type validators (Handle id range, Exception
|
|
27
|
-
# type whitelist). All surface as {InvalidType}.
|
|
28
|
-
INVALID_TYPE_ERRORS = [
|
|
29
|
-
::MessagePack::UnknownExtTypeError,
|
|
30
|
-
::MessagePack::MalformedFormatError,
|
|
31
|
-
::MessagePack::StackError,
|
|
32
|
-
::ArgumentError
|
|
33
|
-
].freeze
|
|
34
|
-
private_constant :INVALID_TYPE_ERRORS
|
|
35
|
-
|
|
36
|
-
# +UnpackError+ is the gem's umbrella class for short-read / incomplete-buffer
|
|
37
|
-
# faults; +EOFError+ covers underflow at the buffer edge. Both map to {Truncated}.
|
|
38
|
-
TRUNCATED_ERRORS = [::MessagePack::UnpackError, ::EOFError].freeze
|
|
39
|
-
private_constant :TRUNCATED_ERRORS
|
|
40
|
-
|
|
41
|
-
# Decode +bytes+ into one Ruby value and validate transitively
|
|
42
|
-
# against the SPEC type mapping. Raises {Truncated}, {InvalidType},
|
|
43
|
-
# or {InvalidEncoding} on wire violations.
|
|
44
|
-
def self.decode(bytes)
|
|
45
|
-
value = Factory.instance.load(bytes.b)
|
|
46
|
-
validate_utf8!(value)
|
|
47
|
-
value
|
|
48
|
-
rescue *INVALID_TYPE_ERRORS => e
|
|
49
|
-
raise InvalidType, e.message
|
|
50
|
-
rescue *TRUNCATED_ERRORS => e
|
|
51
|
-
raise Truncated, e.message
|
|
52
|
-
rescue ::EncodingError => e
|
|
53
|
-
raise InvalidEncoding, e.message
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# SPEC pins +str+ family payloads to UTF-8 (Wire Codec → str/bin
|
|
57
|
-
# Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
|
|
58
|
-
# str family but does not validate the bytes; +bin+ family decodes
|
|
59
|
-
# to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
|
|
60
|
-
# str-typed leaf. {Exception} payloads are validated transitively:
|
|
61
|
-
# +Factory.unpack_exception+ feeds the inner ext-0x02 bytes back
|
|
62
|
-
# through this Decoder, so their +str+ fields are already covered
|
|
63
|
-
# by the time control returns here.
|
|
64
|
-
def self.validate_utf8!(value)
|
|
65
|
-
case value
|
|
66
|
-
when String then validate_string_utf8!(value)
|
|
67
|
-
when Array then value.each { |v| validate_utf8!(v) }
|
|
68
|
-
when Hash then value.each_pair { |k, v| validate_pair_utf8!(k, v) }
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
private_class_method :validate_utf8!
|
|
72
|
-
|
|
73
|
-
def self.validate_string_utf8!(value)
|
|
74
|
-
return unless value.encoding == Encoding::UTF_8
|
|
75
|
-
raise InvalidEncoding, "str payload is not valid UTF-8" unless value.valid_encoding?
|
|
76
|
-
end
|
|
77
|
-
private_class_method :validate_string_utf8!
|
|
78
|
-
|
|
79
|
-
def self.validate_pair_utf8!(key, value)
|
|
80
|
-
validate_utf8!(key)
|
|
81
|
-
validate_utf8!(value)
|
|
82
|
-
end
|
|
83
|
-
private_class_method :validate_pair_utf8!
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "msgpack"
|
|
4
|
-
|
|
5
|
-
require_relative "error"
|
|
6
|
-
require_relative "../handle"
|
|
7
|
-
require_relative "../exception"
|
|
8
|
-
require_relative "factory"
|
|
9
|
-
|
|
10
|
-
module Kobako
|
|
11
|
-
module Wire
|
|
12
|
-
module Codec
|
|
13
|
-
# Module-level entry point for the host side of the kobako wire
|
|
14
|
-
# (SPEC.md → Wire Codec → Type Mapping).
|
|
15
|
-
#
|
|
16
|
-
# The codec backbone is the official +msgpack+ gem: integers, floats,
|
|
17
|
-
# strings, arrays, and maps go through the gem's narrowest-encoding
|
|
18
|
-
# logic; the three kobako-specific ext types (0x00 Symbol, 0x01
|
|
19
|
-
# Capability Handle, 0x02 Exception envelope) are registered on
|
|
20
|
-
# +Factory+ via {Kobako::Wire::Codec::Factory.instance}.
|
|
21
|
-
#
|
|
22
|
-
# Public API is a single function — {.encode}. The codec is stateless;
|
|
23
|
-
# there is no buffer accumulator and no streaming write API. Callers
|
|
24
|
-
# that need to concatenate multiple encodings build the bytes
|
|
25
|
-
# themselves (see {Envelope.encode_outcome} for the canonical example).
|
|
26
|
-
module Encoder
|
|
27
|
-
# Encode +value+ to wire bytes (binary-encoded String).
|
|
28
|
-
# Wire violations surface as +UnsupportedType+: SPEC's 12-entry type
|
|
29
|
-
# mapping is a closed set, and anything outside it is rejected by
|
|
30
|
-
# the msgpack gem itself (arbitrary objects raise +NoMethodError+
|
|
31
|
-
# from missing +to_msgpack+, integers outside i64..u64 raise
|
|
32
|
-
# +RangeError+).
|
|
33
|
-
def self.encode(value)
|
|
34
|
-
Factory.instance.dump(value)
|
|
35
|
-
rescue ::RangeError, ::NoMethodError => e
|
|
36
|
-
raise UnsupportedType, e.message
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Kobako
|
|
4
|
-
module Wire
|
|
5
|
-
module Codec
|
|
6
|
-
# Base class for all wire-codec faults raised by the pure-Ruby host codec.
|
|
7
|
-
#
|
|
8
|
-
# The wire codec implements the binary contract pinned in SPEC.md
|
|
9
|
-
# (Wire Codec → Type Mapping). 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
|
|
16
|
-
|
|
17
|
-
# Input ended before the type prefix or payload was fully consumed.
|
|
18
|
-
class Truncated < Error; end
|
|
19
|
-
|
|
20
|
-
# The type byte at the current position is not in the 12-entry kobako
|
|
21
|
-
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag).
|
|
22
|
-
class InvalidType < Error; end
|
|
23
|
-
|
|
24
|
-
# A msgpack `str` payload was not valid UTF-8, or an ext 0x00 Symbol
|
|
25
|
-
# payload was not valid UTF-8 — both are wire violations per SPEC.
|
|
26
|
-
class InvalidEncoding < Error; end
|
|
27
|
-
|
|
28
|
-
# The encoder was handed a Ruby object whose type has no wire
|
|
29
|
-
# representation (e.g. Range, Time). Higher layers may catch this
|
|
30
|
-
# and re-route the value through Handle allocation, but at the
|
|
31
|
-
# codec level it is a hard error.
|
|
32
|
-
class UnsupportedType < Error; end
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "msgpack"
|
|
4
|
-
|
|
5
|
-
require_relative "error"
|
|
6
|
-
require_relative "../handle"
|
|
7
|
-
require_relative "../exception"
|
|
8
|
-
|
|
9
|
-
module Kobako
|
|
10
|
-
module Wire
|
|
11
|
-
module Codec
|
|
12
|
-
# Cached +MessagePack::Factory+ that owns the kobako wire ext-type
|
|
13
|
-
# registration (SPEC.md → Wire Codec → Ext Types).
|
|
14
|
-
#
|
|
15
|
-
# The factory is the single place in the host gem that touches msgpack
|
|
16
|
-
# API — both {Encoder} and {Decoder} delegate through it, so the three
|
|
17
|
-
# kobako ext codes (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
|
|
18
|
-
# envelope) are configured exactly once at module load.
|
|
19
|
-
module Factory
|
|
20
|
-
# MessagePack ext type code reserved for Symbol
|
|
21
|
-
# (SPEC.md → Wire Codec → Ext Types → ext 0x00). Module-private —
|
|
22
|
-
# mirrors +codec::EXT_SYMBOL+ on the Rust side.
|
|
23
|
-
EXT_SYMBOL = 0x00
|
|
24
|
-
# MessagePack ext type code reserved for Capability Handle
|
|
25
|
-
# (SPEC.md → Wire Codec → Ext Types → ext 0x01). Module-private —
|
|
26
|
-
# mirrors +codec::EXT_HANDLE+ on the Rust side.
|
|
27
|
-
EXT_HANDLE = 0x01
|
|
28
|
-
# MessagePack ext type code reserved for Exception envelope
|
|
29
|
-
# (SPEC.md → Wire Codec → Ext Types → ext 0x02). Module-private —
|
|
30
|
-
# mirrors +codec::EXT_ERRENV+ on the Rust side.
|
|
31
|
-
EXT_ERRENV = 0x02
|
|
32
|
-
private_constant :EXT_SYMBOL, :EXT_HANDLE, :EXT_ERRENV
|
|
33
|
-
|
|
34
|
-
# Returns the lazily-built process-wide +MessagePack::Factory+.
|
|
35
|
-
def self.instance
|
|
36
|
-
@instance ||= build
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Build a fresh factory. Exposed for tests that need an isolated
|
|
40
|
-
# instance; production code should call {.instance}.
|
|
41
|
-
def self.build
|
|
42
|
-
factory = MessagePack::Factory.new
|
|
43
|
-
register_symbol_type(factory)
|
|
44
|
-
register_handle_type(factory)
|
|
45
|
-
register_exception_type(factory)
|
|
46
|
-
factory
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.register_symbol_type(factory)
|
|
50
|
-
factory.register_type(
|
|
51
|
-
EXT_SYMBOL, Symbol,
|
|
52
|
-
packer: lambda(&:name),
|
|
53
|
-
unpacker: ->(payload) { decode_symbol(payload) }
|
|
54
|
-
)
|
|
55
|
-
end
|
|
56
|
-
private_class_method :register_symbol_type
|
|
57
|
-
|
|
58
|
-
# Validate the ext-0x00 payload as UTF-8 and intern. Raises
|
|
59
|
-
# {InvalidEncoding} on invalid bytes — SPEC forbids the
|
|
60
|
-
# binary-encoding fallback that msgpack-gem's default unpacker
|
|
61
|
-
# would otherwise apply.
|
|
62
|
-
def self.decode_symbol(payload)
|
|
63
|
-
name = payload.b.force_encoding(Encoding::UTF_8)
|
|
64
|
-
raise InvalidEncoding, "ext 0x00 payload is not valid UTF-8" unless name.valid_encoding?
|
|
65
|
-
|
|
66
|
-
name.to_sym
|
|
67
|
-
end
|
|
68
|
-
private_class_method :decode_symbol
|
|
69
|
-
|
|
70
|
-
def self.register_handle_type(factory)
|
|
71
|
-
factory.register_type(
|
|
72
|
-
EXT_HANDLE, Handle,
|
|
73
|
-
packer: ->(handle) { [handle.id].pack("N") },
|
|
74
|
-
unpacker: ->(payload) { decode_handle(payload) }
|
|
75
|
-
)
|
|
76
|
-
end
|
|
77
|
-
private_class_method :register_handle_type
|
|
78
|
-
|
|
79
|
-
def self.register_exception_type(factory)
|
|
80
|
-
factory.register_type(
|
|
81
|
-
EXT_ERRENV, Exception,
|
|
82
|
-
packer: ->(exc) { pack_exception(exc) },
|
|
83
|
-
unpacker: ->(payload) { unpack_exception(payload) }
|
|
84
|
-
)
|
|
85
|
-
end
|
|
86
|
-
private_class_method :register_exception_type
|
|
87
|
-
|
|
88
|
-
# Peel off the fixext-4 frame, hand the bytes to +Handle.new+, and
|
|
89
|
-
# translate the +ArgumentError+ raised by Handle's invariants into
|
|
90
|
-
# a wire-layer +InvalidType+ via {Codec.translate_value_object_error}.
|
|
91
|
-
# The Value Object owns the id-range contract; this method only
|
|
92
|
-
# owns the frame shape.
|
|
93
|
-
def self.decode_handle(payload)
|
|
94
|
-
bytes = payload.b
|
|
95
|
-
raise InvalidType, "ext 0x01 payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
|
|
96
|
-
|
|
97
|
-
Codec.translate_value_object_error { Handle.new(bytes.unpack1("N")) }
|
|
98
|
-
end
|
|
99
|
-
private_class_method :decode_handle
|
|
100
|
-
|
|
101
|
-
# Encode the inner ext-0x02 map via {Encoder} (not +factory.dump+) so
|
|
102
|
-
# the embedded payload flows through the same boundary as a top-level
|
|
103
|
-
# encode — nested kobako values (Handle, nested Exception) reach the
|
|
104
|
-
# registered ext-type packers via the cached {.instance}.
|
|
105
|
-
def self.pack_exception(exc)
|
|
106
|
-
Encoder.encode("type" => exc.type, "message" => exc.message, "details" => exc.details)
|
|
107
|
-
end
|
|
108
|
-
private_class_method :pack_exception
|
|
109
|
-
|
|
110
|
-
# Peel the embedded msgpack map and hand it to +Exception.new+;
|
|
111
|
-
# translate the value-object's +ArgumentError+ into +InvalidType+
|
|
112
|
-
# at the wire boundary. Inner decode goes through {Decoder} (not
|
|
113
|
-
# +factory.load+) so the embedded +str+ payloads flow through the
|
|
114
|
-
# same UTF-8 validation as a top-level decode.
|
|
115
|
-
#
|
|
116
|
-
# This establishes a runtime cycle Factory → Decoder → Factory:
|
|
117
|
-
# the cached +.instance+ feeds +Decoder.decode+, which re-enters
|
|
118
|
-
# this method when a nested ext 0x02 appears inside +details+. The
|
|
119
|
-
# recursion is bounded by msgpack nesting depth — identical to
|
|
120
|
-
# nested Array / Hash payloads — so no extra guard is needed.
|
|
121
|
-
# Do not switch back to +factory.load+ to "simplify": that path
|
|
122
|
-
# bypasses UTF-8 validation and re-opens the Decoder's special
|
|
123
|
-
# case for Exception (removed in M5).
|
|
124
|
-
def self.unpack_exception(payload)
|
|
125
|
-
map = Decoder.decode(payload)
|
|
126
|
-
raise InvalidType, "ext 0x02 payload must be a map" unless map.is_a?(Hash)
|
|
127
|
-
|
|
128
|
-
Codec.translate_value_object_error do
|
|
129
|
-
Exception.new(type: map["type"], message: map["message"], details: map["details"])
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
private_class_method :unpack_exception
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
end
|
data/lib/kobako/wire/codec.rb
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "codec/error"
|
|
4
|
-
|
|
5
|
-
module Kobako
|
|
6
|
-
module Wire
|
|
7
|
-
# Host-side MessagePack codec for the kobako wire contract — the
|
|
8
|
-
# byte-level layer (SPEC.md → Wire Codec). The envelope layer
|
|
9
|
-
# (Kobako::Wire::Envelope) sits on top of this and pins the four
|
|
10
|
-
# logical message shapes (Request / Response / Result / Panic).
|
|
11
|
-
#
|
|
12
|
-
# Backed by the official +msgpack+ gem via {Factory}; {Encoder} and
|
|
13
|
-
# {Decoder} are thin wrappers that register the three kobako-specific
|
|
14
|
-
# ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
|
|
15
|
-
# envelope) on a single +MessagePack::Factory+ instance. The Rust side
|
|
16
|
-
# mirrors this layer as the +codec+ module in the +kobako-wasm+ crate;
|
|
17
|
-
# the ext-code constants live as module-private values on {Factory}
|
|
18
|
-
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
19
|
-
# +codec::EXT_ERRENV+ on that side.
|
|
20
|
-
module Codec
|
|
21
|
-
# Wire-boundary translator: every wire Value Object (Handle /
|
|
22
|
-
# Exception / Request / Response / Panic / Outcome) raises
|
|
23
|
-
# +ArgumentError+ when an invariant is violated at construction.
|
|
24
|
-
# The wire boundary surfaces those violations to callers as
|
|
25
|
-
# +InvalidType+ so the public taxonomy stays +Codec::Error+ and
|
|
26
|
-
# never leaks +ArgumentError+ from the Ruby standard library.
|
|
27
|
-
#
|
|
28
|
-
# Wrap any block that constructs a wire Value Object from decoded
|
|
29
|
-
# bytes with this helper to keep the five decode sites (Request /
|
|
30
|
-
# Response / Panic / Handle ext / Exception ext) uniform. Do not
|
|
31
|
-
# use it for general-purpose validation outside the wire boundary
|
|
32
|
-
# — host-layer +ArgumentError+ values should propagate unchanged.
|
|
33
|
-
def self.translate_value_object_error
|
|
34
|
-
yield
|
|
35
|
-
rescue ::ArgumentError => e
|
|
36
|
-
raise InvalidType, e.message
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
require_relative "codec/factory"
|
|
43
|
-
require_relative "codec/encoder"
|
|
44
|
-
require_relative "codec/decoder"
|