kobako 0.26.0-aarch64-linux → 0.28.0-aarch64-linux
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 +103 -0
- data/README.md +156 -118
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +38 -0
- data/crates/kobako-runtime/README.md +10 -12
- data/crates/kobako-transport/CHANGELOG.md +35 -0
- data/crates/kobako-transport/README.md +43 -27
- data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
- data/crates/kobako-wasmtime/README.md +8 -10
- data/data/kobako.wasm +0 -0
- data/lib/kobako/3.3/kobako.so +0 -0
- data/lib/kobako/3.4/kobako.so +0 -0
- data/lib/kobako/4.0/kobako.so +0 -0
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +41 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/services.rb +7 -61
- data/lib/kobako/catalog/snippets.rb +42 -66
- data/lib/kobako/catalog.rb +2 -10
- data/lib/kobako/codec/decoder.rb +9 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +4 -10
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec.rb +3 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +51 -112
- data/lib/kobako/errors.rb +51 -127
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +23 -53
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +96 -133
- data/lib/kobako/sandbox_options.rb +38 -84
- data/lib/kobako/snippet/binary.rb +1 -16
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +1 -9
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +112 -195
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +5 -10
- data/lib/kobako/transport/reflection.rb +39 -50
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +21 -73
- data/lib/kobako/transport.rb +3 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +17 -0
- data/sig/kobako/catalog/services.rbs +0 -6
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec.rbs +2 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +2 -3
- data/sig/kobako/errors.rbs +6 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/runtime.rbs +12 -8
- data/sig/kobako/sandbox.rbs +4 -0
- data/sig/kobako/sandbox_options.rbs +2 -6
- data/sig/kobako/transport/dispatcher.rbs +14 -18
- data/sig/kobako/transport/reflection.rbs +5 -3
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +7 -6
- metadata +6 -3
- data/rust-toolchain.toml +0 -11
|
@@ -1,50 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../constant_name"
|
|
3
4
|
require_relative "../errors"
|
|
4
5
|
require_relative "../transport/exposure"
|
|
5
6
|
|
|
6
7
|
module Kobako
|
|
7
8
|
module Catalog
|
|
8
|
-
|
|
9
|
-
# bindings keyed by their constant-path name. Holds the flat
|
|
10
|
-
# path→Exposure table and the declared path set every invocation
|
|
11
|
-
# announces 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.paths # => ["MyService::KV"]
|
|
18
|
-
# services.lookup("MyService::KV").object # => kv_object
|
|
19
|
-
#
|
|
20
|
-
# Per-dispatch routing is +Kobako::Transport::Dispatcher+'s
|
|
21
|
-
# responsibility — the Dispatcher resolves a path against the invocation's
|
|
22
|
-
# +Kobako::Context+ (which layers per-invocation providers and +ctx.bind+
|
|
23
|
-
# overrides over these base bindings) and the +Catalog::Handles+, both
|
|
24
|
-
# passed in the per-invocation dispatch Proc the Context hands to
|
|
25
|
-
# +Runtime#eval+ / +#run+.
|
|
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.
|
|
9
|
+
class Services # :nodoc:
|
|
32
10
|
def initialize
|
|
33
11
|
@bindings = {} # : Hash[String, Kobako::Transport::Exposure]
|
|
34
|
-
@sealed = false
|
|
35
12
|
end
|
|
36
13
|
|
|
37
|
-
#
|
|
38
|
-
# +String+ of one or more +::+-separated constant-form segments
|
|
39
|
-
# (+"MyService::KV"+ or a top-level +"File"+). Returns +self+ for
|
|
40
|
-
# chaining. Raises +ArgumentError+ when a segment is malformed, when
|
|
41
|
-
# +path+ collides with an existing binding (a name is a bound Service
|
|
42
|
-
# or a grouping prefix, never both), or when the owning Sandbox has
|
|
43
|
-
# been sealed by its first invocation. The binding records +object+'s
|
|
44
|
-
# Exposure as it stands now.
|
|
14
|
+
# The binding records +object+'s Exposure as it stands now.
|
|
45
15
|
def bind(path, object)
|
|
46
|
-
raise ArgumentError, "cannot bind after first Sandbox invocation" if @sealed
|
|
47
|
-
|
|
48
16
|
path_str = validate_path!(path)
|
|
49
17
|
raise ArgumentError, "Service path #{path_str} conflicts with an existing binding" if collision?(path_str)
|
|
50
18
|
|
|
@@ -52,14 +20,10 @@ module Kobako
|
|
|
52
20
|
self
|
|
53
21
|
end
|
|
54
22
|
|
|
55
|
-
# Returns +true+ iff a Service is bound at +path+ — the declared (Frame 1)
|
|
56
|
-
# key set a per-invocation +ctx.bind+ override may target.
|
|
57
23
|
def bound?(path)
|
|
58
24
|
@bindings.key?(path.to_s)
|
|
59
25
|
end
|
|
60
26
|
|
|
61
|
-
# Resolve a +target+ constant path to the bound Service's Exposure.
|
|
62
|
-
# Raises +KeyError+ when no Service is bound at +target+.
|
|
63
27
|
def lookup(target)
|
|
64
28
|
target_str = target.to_s
|
|
65
29
|
raise KeyError, "no service bound at #{target_str.inspect}" unless @bindings.key?(target_str)
|
|
@@ -67,42 +31,24 @@ module Kobako
|
|
|
67
31
|
@bindings[target_str]
|
|
68
32
|
end
|
|
69
33
|
|
|
70
|
-
# The
|
|
71
|
-
#
|
|
72
|
-
# preamble. The registry holds the bindings; the wire layout is the
|
|
73
|
-
# native side's.
|
|
34
|
+
# The registry holds the bindings; the wire layout is the native
|
|
35
|
+
# side's.
|
|
74
36
|
def paths
|
|
75
37
|
@bindings.keys
|
|
76
38
|
end
|
|
77
39
|
|
|
78
|
-
# Mark the registry as sealed so registration can never alter the
|
|
79
|
-
# declared path set afterwards. Called by +Sandbox+ on the first
|
|
80
|
-
# invocation; afterwards #bind raises ArgumentError. Idempotent;
|
|
81
|
-
# returns +self+.
|
|
82
|
-
def seal!
|
|
83
|
-
@sealed = true
|
|
84
|
-
self
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Returns +true+ when #seal! has been called, +false+ otherwise.
|
|
88
|
-
def sealed?
|
|
89
|
-
@sealed
|
|
90
|
-
end
|
|
91
|
-
|
|
92
40
|
private
|
|
93
41
|
|
|
94
42
|
def validate_path!(path)
|
|
95
43
|
path_str = path.to_s
|
|
96
44
|
segments = path_str.split("::", -1)
|
|
97
|
-
return path_str if !segments.empty? && segments.all? { |seg|
|
|
45
|
+
return path_str if !segments.empty? && segments.all? { |seg| CONSTANT_NAME.match?(seg) }
|
|
98
46
|
|
|
99
47
|
raise ArgumentError,
|
|
100
48
|
"bind path must be constant-form segments joined by '::' (got #{path.inspect})"
|
|
101
49
|
end
|
|
102
50
|
|
|
103
|
-
#
|
|
104
|
-
# existing binding on the +::+ segment boundary — the guardrail that
|
|
105
|
-
# keeps a name from being both a bound Service and a grouping prefix.
|
|
51
|
+
# Keeps a name from being both a bound Service and a grouping prefix.
|
|
106
52
|
def collision?(path)
|
|
107
53
|
@bindings.each_key.any? do |existing|
|
|
108
54
|
existing == path ||
|
|
@@ -1,62 +1,41 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../constant_name"
|
|
3
4
|
require_relative "../snippet"
|
|
4
5
|
|
|
5
6
|
module Kobako
|
|
6
7
|
module Catalog
|
|
7
|
-
|
|
8
|
-
# of preloaded snippets.
|
|
9
|
-
#
|
|
10
|
-
# Entries replay against the fresh +mrb_state+ before per-invocation
|
|
11
|
-
# source / entrypoint resolution. Each +Snippet::Source+ entry's +name+
|
|
12
|
-
# is its canonical identity — the filename baked into the loaded IREP's
|
|
13
|
-
# +debug_info+ that surfaces in every backtrace frame originating from
|
|
14
|
-
# the snippet as +(snippet:Name):line+. Duplicate names within the
|
|
15
|
-
# +code:+ form would produce ambiguous attribution and are rejected at
|
|
16
|
-
# registration time.
|
|
17
|
-
# +Snippet::Binary+ entries carry no host-side name — their canonical
|
|
18
|
-
# name lives in the bytecode's +debug_info+ and is read by the guest at
|
|
19
|
-
# load time; the host does not extract it.
|
|
20
|
-
#
|
|
21
|
-
# Sealing is governed by the owning Sandbox — the registry itself
|
|
22
|
-
# is append-only and exposes no mutation API beyond +#register+; the
|
|
23
|
-
# Sandbox guards +#register+ behind the seal check before delegating.
|
|
24
|
-
class Snippets
|
|
25
|
-
# Ruby constant-name pattern enforced on snippet names.
|
|
26
|
-
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
27
|
-
|
|
8
|
+
class Snippets # :nodoc:
|
|
28
9
|
def initialize
|
|
29
10
|
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
|
|
11
|
+
@compiled = {} # : Hash[Kobako::Snippet::Source, String]
|
|
12
|
+
@compile_tried = false
|
|
30
13
|
end
|
|
31
14
|
|
|
32
|
-
#
|
|
33
|
-
# +[kind, name, body]+ triple +Runtime#eval+ / +#run+ frame into
|
|
34
|
-
# Frame 3 — +kind+ names the form as a Symbol, +name+ is +nil+ for
|
|
35
|
-
# the bytecode form. The entry value objects stay pure carriers, so
|
|
36
|
-
# this collection-tier method reads their attributes externally
|
|
37
|
-
# rather than asking each entry to project itself.
|
|
15
|
+
# Projected here so the entry value objects stay pure carriers.
|
|
38
16
|
def entries
|
|
39
17
|
@entries.map { |entry| entry_tuple(entry) }
|
|
40
18
|
end
|
|
41
19
|
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
#
|
|
20
|
+
# Has the block compile the source snippets, once, so every invocation
|
|
21
|
+
# after replays their bytecode instead of compiling them again. The
|
|
22
|
+
# block takes the #entries and answers one bytecode per source, or
|
|
23
|
+
# +nil+; when nothing comes back the source keeps replaying, and the
|
|
24
|
+
# attempt is not repeated.
|
|
25
|
+
def compile
|
|
26
|
+
return if @compile_tried
|
|
27
|
+
|
|
28
|
+
@compile_tried = true
|
|
29
|
+
sources = @entries.grep(Snippet::Source)
|
|
30
|
+
return if sources.empty?
|
|
31
|
+
|
|
32
|
+
bytecodes = yield(entries) or return
|
|
33
|
+
@compiled = sources.zip(bytecodes).to_h.compact
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Bytecode stays unchecked on the host; the guest validates it at
|
|
38
|
+
# first replay.
|
|
60
39
|
def register(code: nil, name: nil, binary: nil)
|
|
61
40
|
if binary
|
|
62
41
|
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
@@ -69,26 +48,25 @@ module Kobako
|
|
|
69
48
|
|
|
70
49
|
private
|
|
71
50
|
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
# and appends the Source entry.
|
|
51
|
+
# The native side frames a source body only as UTF-8 text, so source
|
|
52
|
+
# read as bytes is re-tagged, and bytes that are not UTF-8 are refused
|
|
53
|
+
# here rather than failing every invocation.
|
|
76
54
|
def register_source!(code, name)
|
|
77
55
|
code, name = ensure_source_args!(code, name)
|
|
56
|
+
body = code.dup.force_encoding(Encoding::UTF_8)
|
|
57
|
+
raise ArgumentError, "code must be UTF-8 text" unless body.valid_encoding?
|
|
58
|
+
|
|
78
59
|
name_sym = normalize_name(name)
|
|
79
60
|
if @entries.any? { |e| e.is_a?(Snippet::Source) && e.name == name_sym }
|
|
80
61
|
raise ArgumentError, "snippet #{name_sym.inspect} already preloaded"
|
|
81
62
|
end
|
|
82
63
|
|
|
83
|
-
@entries << Snippet::Source.new(name: name_sym, body:
|
|
84
|
-
|
|
64
|
+
@entries << Snippet::Source.new(name: name_sym, body: body)
|
|
65
|
+
nil
|
|
85
66
|
end
|
|
86
67
|
|
|
87
|
-
#
|
|
88
|
-
#
|
|
89
|
-
# present. The +code:+ type check runs before the +name:+
|
|
90
|
-
# presence check so callers passing +code: nil+ explicitly see
|
|
91
|
-
# the type error rather than the "missing keyword" error.
|
|
68
|
+
# The +code:+ type check runs first so an explicit +code: nil+ reads
|
|
69
|
+
# as a type error rather than a missing keyword.
|
|
92
70
|
def ensure_source_args!(code, name)
|
|
93
71
|
raise ArgumentError, "missing keyword: code: + name:, or binary:" if code.nil? && name.nil?
|
|
94
72
|
raise ArgumentError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
@@ -97,9 +75,8 @@ module Kobako
|
|
|
97
75
|
[code, name]
|
|
98
76
|
end
|
|
99
77
|
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
# to ASCII-8BIT so msgpack-ruby picks the +bin+ family on the wire.
|
|
78
|
+
# Kept as bytes whatever encoding the caller tagged them with, since
|
|
79
|
+
# the guest reads them as bytecode rather than text.
|
|
103
80
|
def register_binary!(bytes)
|
|
104
81
|
raise ArgumentError, "binary must be a String, got #{bytes.class}" unless bytes.is_a?(String)
|
|
105
82
|
|
|
@@ -107,14 +84,13 @@ module Kobako
|
|
|
107
84
|
nil
|
|
108
85
|
end
|
|
109
86
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
# +nil+ because the canonical name lives in the bytecode's embedded
|
|
113
|
-
# +debug_info+ and is read by the guest at load time.
|
|
87
|
+
# A bytecode entry carries no name: its canonical name lives in the
|
|
88
|
+
# bytecode's +debug_info+ and is read by the guest at load time.
|
|
114
89
|
def entry_tuple(entry)
|
|
115
90
|
case entry
|
|
116
91
|
when Snippet::Source
|
|
117
|
-
|
|
92
|
+
compiled = @compiled[entry]
|
|
93
|
+
compiled ? [Snippet::Binary::KIND, nil, compiled] : [Snippet::Source::KIND, entry.name.to_s, entry.body]
|
|
118
94
|
else
|
|
119
95
|
[Snippet::Binary::KIND, nil, entry.body]
|
|
120
96
|
end
|
|
@@ -126,9 +102,9 @@ module Kobako
|
|
|
126
102
|
end
|
|
127
103
|
|
|
128
104
|
name_str = name.to_s
|
|
129
|
-
unless
|
|
105
|
+
unless CONSTANT_NAME.match?(name_str)
|
|
130
106
|
raise ArgumentError,
|
|
131
|
-
"snippet name must match #{
|
|
107
|
+
"snippet name must match #{CONSTANT_NAME.inspect} (got #{name.inspect})"
|
|
132
108
|
end
|
|
133
109
|
|
|
134
110
|
name_str.to_sym
|
data/lib/kobako/catalog.rb
CHANGED
|
@@ -3,18 +3,10 @@
|
|
|
3
3
|
require_relative "catalog/handles"
|
|
4
4
|
require_relative "catalog/services"
|
|
5
5
|
require_relative "catalog/snippets"
|
|
6
|
+
require_relative "catalog/flags"
|
|
6
7
|
require_relative "catalog/extensions"
|
|
7
8
|
|
|
8
9
|
module Kobako
|
|
9
|
-
|
|
10
|
-
# allocation tables. Houses the host-side registries the Sandbox owns:
|
|
11
|
-
# +Catalog::Services+ (path→Service binding registry),
|
|
12
|
-
# +Catalog::Snippets+ (preloaded source / bytecode entries),
|
|
13
|
-
# +Catalog::Extensions+ (installed Extensions, composed onto Services /
|
|
14
|
-
# Snippets), and +Catalog::Handles+ (per-invocation Handle ID allocator).
|
|
15
|
-
#
|
|
16
|
-
# See {SPEC.md Refinement → Internal Concepts}[link:../../SPEC.md] for
|
|
17
|
-
# how Catalog fits alongside Transport and Runtime.
|
|
18
|
-
module Catalog
|
|
10
|
+
module Catalog # :nodoc:
|
|
19
11
|
end
|
|
20
12
|
end
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -8,36 +8,17 @@ require_relative "utils"
|
|
|
8
8
|
|
|
9
9
|
module Kobako
|
|
10
10
|
module Codec
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
# (TruncatedInputError, InvalidTypeError, InvalidEncodingError, UnsupportedTypeError) so
|
|
16
|
-
# callers can pattern-match on the SPEC's wire-violation categories
|
|
17
|
-
# without leaking the gem's internal exception classes.
|
|
18
|
-
#
|
|
19
|
-
# Public API is a single function — +.decode+. The decoder is
|
|
20
|
-
# stateless; the +MessagePack::Unpacker+ instance is built per call
|
|
21
|
-
# because callers always decode exactly one wire value at a time.
|
|
22
|
-
module Decoder
|
|
23
|
-
# Decode +bytes+ into one Ruby value and validate transitively
|
|
24
|
-
# against the SPEC type mapping. Raises TruncatedInputError, InvalidTypeError,
|
|
25
|
-
# or InvalidEncodingError on wire violations.
|
|
26
|
-
#
|
|
27
|
-
# When a block is given, the decoded value is yielded and the block's
|
|
28
|
-
# result is returned — wire Value Objects use this to build themselves
|
|
29
|
-
# from the decoded payload. The block runs inside this method's
|
|
30
|
-
# rescue, so a Value Object's +ArgumentError+ invariant failure
|
|
31
|
-
# surfaces as InvalidTypeError without a separate Utils.with_boundary
|
|
32
|
-
# wrapper at the call site.
|
|
11
|
+
module Decoder # :nodoc:
|
|
12
|
+
# The block runs inside this method's rescue, so a Value Object built
|
|
13
|
+
# from the decoded payload reports a broken invariant as
|
|
14
|
+
# InvalidTypeError without its own Utils.with_boundary.
|
|
33
15
|
def self.decode(bytes)
|
|
34
16
|
value = FACTORY.load(bytes.b)
|
|
35
17
|
validate_utf8!(value)
|
|
36
18
|
block_given? ? yield(value) : value
|
|
37
19
|
# msgpack gem raises the format/type errors below; +ArgumentError+
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
# are wire violations, so both map to InvalidTypeError.
|
|
20
|
+
# covers a yielded block's Value Object invariants — a wire violation
|
|
21
|
+
# too, so it maps to InvalidTypeError.
|
|
41
22
|
rescue ::MessagePack::UnknownExtTypeError, ::MessagePack::MalformedFormatError,
|
|
42
23
|
::MessagePack::StackError, ::ArgumentError => e
|
|
43
24
|
raise InvalidTypeError, e.message
|
|
@@ -48,9 +29,9 @@ module Kobako
|
|
|
48
29
|
raise TruncatedInputError, e.message
|
|
49
30
|
end
|
|
50
31
|
|
|
51
|
-
#
|
|
52
|
-
# (
|
|
53
|
-
#
|
|
32
|
+
# +str+ family payloads must be UTF-8
|
|
33
|
+
# (docs/wire/payload-msgpack.md § Text and Bytes). The msgpack gem
|
|
34
|
+
# returns UTF-8-tagged Strings for
|
|
54
35
|
# str family but does not validate the bytes; +bin+ family decodes
|
|
55
36
|
# to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
|
|
56
37
|
# str-typed leaf via Utils.assert_utf8!.
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -7,28 +7,9 @@ require_relative "ext_types"
|
|
|
7
7
|
|
|
8
8
|
module Kobako
|
|
9
9
|
module Codec
|
|
10
|
-
|
|
11
|
-
|
|
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
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -2,34 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
module Kobako
|
|
4
4
|
module Codec
|
|
5
|
-
|
|
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
|
-
#
|
|
18
|
-
class
|
|
8
|
+
# Also raised on encode for a value nested past what the packer can walk.
|
|
9
|
+
class InvalidTypeError < Error; end # :nodoc:
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
# (
|
|
20
|
-
#
|
|
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
|
-
# (
|
|
25
|
-
#
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
symbol.name
|
|
57
|
-
end
|
|
43
|
+
class << self
|
|
44
|
+
private
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
#
|
|
133
|
-
#
|
|
134
|
-
|
|
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
|