kobako 0.19.0 → 0.21.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 +127 -0
- data/Cargo.lock +15 -5
- data/README.md +96 -44
- data/ROADMAP.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +44 -0
- data/crates/kobako-runtime/Cargo.toml +8 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/dispatch.rs +17 -11
- data/crates/kobako-runtime/src/error.rs +38 -6
- data/crates/kobako-runtime/src/lib.rs +13 -2
- data/crates/kobako-runtime/src/profile.rs +8 -0
- data/crates/kobako-runtime/src/runtime.rs +7 -2
- data/crates/kobako-runtime/src/snapshot.rs +13 -4
- data/crates/kobako-runtime/src/yielder.rs +4 -4
- data/crates/kobako-transport/CHANGELOG.md +31 -0
- data/crates/kobako-transport/Cargo.toml +27 -0
- data/crates/kobako-transport/README.md +43 -0
- data/crates/kobako-transport/src/abi.rs +89 -0
- data/crates/kobako-transport/src/envelope/bytes.rs +234 -0
- data/crates/kobako-transport/src/envelope/call.rs +222 -0
- data/crates/kobako-transport/src/envelope/error_record.rs +101 -0
- data/crates/kobako-transport/src/envelope/fault.rs +171 -0
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +232 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +261 -0
- data/crates/kobako-transport/src/envelope/reply.rs +240 -0
- data/crates/kobako-transport/src/envelope/run.rs +85 -0
- data/crates/kobako-transport/src/envelope.rs +73 -0
- data/crates/kobako-transport/src/lib.rs +21 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +30 -0
- data/crates/kobako-wasmtime/Cargo.toml +5 -2
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/config.rs +19 -12
- data/crates/kobako-wasmtime/src/dispatch.rs +15 -8
- data/crates/kobako-wasmtime/src/driver.rs +8 -27
- data/crates/kobako-wasmtime/src/frames.rs +11 -9
- data/crates/kobako-wasmtime/src/guest_mem.rs +7 -15
- data/crates/kobako-wasmtime/src/instance_pre.rs +2 -2
- data/crates/kobako-wasmtime/src/invocation.rs +1 -1
- data/crates/kobako-wasmtime/src/lib.rs +1 -1
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +7 -2
- data/ext/kobako/src/runtime/bridge.rs +103 -40
- data/ext/kobako/src/runtime/errors.rs +13 -5
- data/ext/kobako/src/runtime/gvl.rs +114 -0
- data/ext/kobako/src/runtime.rs +307 -207
- data/lib/kobako/catalog/extensions.rb +50 -43
- data/lib/kobako/catalog/handles.rb +7 -16
- data/lib/kobako/catalog/services.rb +24 -46
- data/lib/kobako/catalog/snippets.rb +16 -33
- data/lib/kobako/codec/decoder.rb +10 -13
- data/lib/kobako/codec/encoder.rb +7 -7
- data/lib/kobako/codec/error.rb +6 -6
- data/lib/kobako/codec/ext_types.rb +12 -72
- data/lib/kobako/codec/handle_walk.rb +6 -6
- data/lib/kobako/codec/state.rb +4 -56
- data/lib/kobako/codec/utils.rb +4 -4
- data/lib/kobako/codec.rb +11 -21
- data/lib/kobako/context.rb +208 -0
- data/lib/kobako/errors.rb +59 -16
- data/lib/kobako/execution.rb +52 -0
- data/lib/kobako/extension.rb +24 -10
- data/lib/kobako/outcome.rb +71 -128
- data/lib/kobako/payload/arguments.rb +59 -0
- data/lib/kobako/payload.rb +19 -0
- data/lib/kobako/pool.rb +1 -7
- data/lib/kobako/sandbox.rb +51 -191
- data/lib/kobako/sandbox_options.rb +24 -3
- data/lib/kobako/snippet/binary.rb +3 -4
- data/lib/kobako/snippet/source.rb +5 -6
- data/lib/kobako/snippet.rb +2 -2
- data/lib/kobako/transport/call.rb +21 -0
- data/lib/kobako/transport/dispatcher.rb +94 -142
- data/lib/kobako/transport/error.rb +21 -5
- data/lib/kobako/transport/reflection.rb +82 -0
- data/lib/kobako/transport/run.rb +25 -33
- data/lib/kobako/transport/yielder.rb +27 -36
- data/lib/kobako/transport.rb +11 -8
- data/lib/kobako/unresolved.rb +17 -0
- data/lib/kobako/usage.rb +7 -7
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/release-please-config.json +47 -2
- data/sig/kobako/catalog/extensions.rbs +4 -6
- data/sig/kobako/catalog/handles.rbs +0 -2
- data/sig/kobako/catalog/services.rbs +4 -4
- data/sig/kobako/catalog/snippets.rbs +4 -2
- data/sig/kobako/codec/error.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +0 -7
- data/sig/kobako/codec/state.rbs +0 -4
- data/sig/kobako/codec.rbs +4 -5
- data/sig/kobako/context.rbs +36 -0
- data/sig/kobako/errors.rbs +27 -6
- data/sig/kobako/execution.rbs +24 -0
- data/sig/kobako/extension.rbs +4 -2
- data/sig/kobako/outcome.rbs +12 -13
- data/sig/kobako/payload/arguments.rbs +26 -0
- data/sig/kobako/payload.rbs +4 -0
- data/sig/kobako/pool.rbs +0 -2
- data/sig/kobako/runtime.rbs +44 -14
- data/sig/kobako/sandbox.rbs +5 -24
- data/sig/kobako/sandbox_options.rbs +9 -2
- data/sig/kobako/snippet/binary.rbs +1 -1
- data/sig/kobako/snippet/source.rbs +1 -1
- data/sig/kobako/transport/call.rbs +21 -0
- data/sig/kobako/transport/dispatcher.rbs +9 -11
- data/sig/kobako/transport/error.rbs +11 -0
- data/sig/kobako/transport/reflection.rbs +15 -0
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -2
- data/sig/kobako/unresolved.rbs +4 -0
- metadata +30 -11
- data/lib/kobako/fault.rb +0 -38
- data/lib/kobako/outcome/panic.rb +0 -42
- data/lib/kobako/transport/request.rb +0 -76
- data/lib/kobako/transport/response.rb +0 -83
- data/lib/kobako/transport/yield.rb +0 -94
- data/sig/kobako/fault.rbs +0 -19
- data/sig/kobako/outcome/panic.rbs +0 -34
- data/sig/kobako/transport/request.rbs +0 -29
- data/sig/kobako/transport/response.rbs +0 -33
- data/sig/kobako/transport/yield.rbs +0 -34
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../unresolved"
|
|
4
|
+
|
|
3
5
|
module Kobako
|
|
4
6
|
module Catalog
|
|
5
7
|
# Kobako::Catalog::Extensions — per-Sandbox registry of installed
|
|
@@ -7,7 +9,9 @@ module Kobako
|
|
|
7
9
|
# install time (its +source+ into +Catalog::Snippets+, its +backend+
|
|
8
10
|
# path into +Catalog::Services+), asserts declared dependencies are
|
|
9
11
|
# present when the Sandbox seals, and resolves each callable-backed
|
|
10
|
-
# path to a fresh object
|
|
12
|
+
# path to a fresh object for every invocation — returning that map to the
|
|
13
|
+
# driving +Context+ rather than mutating the shared registry, so
|
|
14
|
+
# concurrent invocations stay shared-nothing.
|
|
11
15
|
#
|
|
12
16
|
# Sealing and the reject-after-install guard are governed by the owning
|
|
13
17
|
# Sandbox through +Catalog::Services#sealed?+, the shared seal signal;
|
|
@@ -21,77 +25,87 @@ module Kobako
|
|
|
21
25
|
|
|
22
26
|
# Install +extension+: validate its shape, register its +source+ as a
|
|
23
27
|
# preloaded snippet, and — when it carries a +backend+ — reserve the
|
|
24
|
-
# backend path in +services+. A
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
# +
|
|
29
|
-
#
|
|
30
|
-
# +
|
|
31
|
-
#
|
|
28
|
+
# backend path in +services+. A static +object:+ binds directly; a
|
|
29
|
+
# per-invocation +provider:+ or a fillable backend reserves the path with
|
|
30
|
+
# +Kobako::Unresolved+ (the per-invocation result is layered onto the
|
|
31
|
+
# Context, a fillable stays Unresolved until filled). Raises
|
|
32
|
+
# +ArgumentError+ for the two shapes +#validate!+ owns — +source+ not a
|
|
33
|
+
# String, or a present +backend+ that omits +path+ / +object+ /
|
|
34
|
+
# +provider+. The Extension readers are duck-typed, so an object missing
|
|
35
|
+
# them surfaces the underlying +NoMethodError+, and a malformed +name+ or
|
|
36
|
+
# +backend.path+ surfaces through the +snippets+ / +services+
|
|
37
|
+
# registration it routes to.
|
|
32
38
|
def install(extension, snippets:, services:)
|
|
33
39
|
validate!(extension)
|
|
34
40
|
snippets.register(code: extension.source, name: extension.name)
|
|
35
41
|
backend = extension.backend
|
|
36
|
-
services.bind(backend.path,
|
|
42
|
+
services.bind(backend.path, install_object(backend)) if backend
|
|
37
43
|
@entries << extension
|
|
38
44
|
self
|
|
39
45
|
end
|
|
40
46
|
|
|
41
47
|
# Assert every installed Extension's +depends_on+ names a fellow
|
|
42
|
-
# installed Extension. Runs once, when the Sandbox first seals
|
|
43
|
-
# unmet dependency raises +ArgumentError+ naming the
|
|
44
|
-
# guest runs.
|
|
48
|
+
# installed Extension. Runs once, when the Sandbox first seals
|
|
49
|
+
# successfully; an unmet dependency raises +ArgumentError+ naming the
|
|
50
|
+
# gap, before the guest runs. The asserted flag flips only on success,
|
|
51
|
+
# so a seal that failed re-checks on the next attempt rather than
|
|
52
|
+
# silently passing a broken Sandbox. Idempotent across later invocations.
|
|
45
53
|
def seal!
|
|
46
54
|
return self if @asserted
|
|
47
55
|
|
|
48
|
-
@asserted = true
|
|
49
56
|
assert_dependencies!
|
|
57
|
+
@asserted = true
|
|
50
58
|
self
|
|
51
59
|
end
|
|
52
60
|
|
|
53
|
-
# Resolve each
|
|
54
|
-
#
|
|
55
|
-
# providers yield distinct objects; one
|
|
56
|
-
# Extensions is invoked once and its result
|
|
57
|
-
# identity is resource identity.
|
|
58
|
-
# they stay the object bound at install
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
resolved
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
# Resolve each +provider:+-backed path to this invocation's object and
|
|
62
|
+
# return the +path => object+ map the driving +Context+ layers over the
|
|
63
|
+
# static base bindings. Distinct providers yield distinct objects; one
|
|
64
|
+
# provider shared by several Extensions is invoked once and its result
|
|
65
|
+
# shared, so provider identity is resource identity. Static and fillable
|
|
66
|
+
# backends are omitted — they stay the object bound at install in the
|
|
67
|
+
# base registry (+Kobako::Unresolved+ for a fillable).
|
|
68
|
+
def resolve
|
|
69
|
+
resolved = {} # : Hash[String, untyped]
|
|
70
|
+
by_provider = {} # : Hash[untyped, untyped]
|
|
71
|
+
by_provider.compare_by_identity
|
|
72
|
+
@entries.each { |extension| resolve_backend(extension, resolved, by_provider) }
|
|
73
|
+
resolved
|
|
64
74
|
end
|
|
65
75
|
|
|
66
76
|
private
|
|
67
77
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
# The object a backend binds at install: a static +object:+ directly, or
|
|
79
|
+
# +Kobako::Unresolved+ as the placeholder for a per-invocation +provider:+
|
|
80
|
+
# (layered onto the Context) or a fillable (neither keyword).
|
|
81
|
+
def install_object(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
|
|
82
|
+
|
|
83
|
+
# Resolve one Extension's +provider:+-backed path against the shared
|
|
84
|
+
# per-invocation identity cache (keyed by provider identity) and record
|
|
85
|
+
# it in +resolved+. A static or fillable backend is skipped — its object
|
|
86
|
+
# stays as bound at install in the base registry.
|
|
87
|
+
def resolve_backend(extension, resolved, by_provider)
|
|
73
88
|
backend = extension.backend
|
|
74
89
|
return unless backend
|
|
75
90
|
|
|
76
91
|
provider = backend.provider
|
|
77
|
-
return
|
|
92
|
+
return if provider.nil?
|
|
78
93
|
|
|
79
|
-
|
|
80
|
-
services.refresh(backend.path, object)
|
|
94
|
+
resolved[backend.path.to_s] = by_provider.fetch(provider) { by_provider[provider] = provider.call }
|
|
81
95
|
end
|
|
82
96
|
|
|
83
97
|
# Enforce the Extension-shape checks +#preload+ / +#bind+ do not: a
|
|
84
98
|
# mandatory String +source+ (the install/bind boundary) and a
|
|
85
|
-
# +backend+ that, when present, exposes +path+
|
|
99
|
+
# +backend+ that, when present, exposes +path+ / +object+ / +provider+.
|
|
86
100
|
def validate!(extension)
|
|
87
101
|
source = extension.source
|
|
88
102
|
raise ArgumentError, "Extension #source must be a String, got #{source.class}" unless source.is_a?(String)
|
|
89
103
|
|
|
90
104
|
backend = extension.backend
|
|
91
105
|
return if backend.nil?
|
|
92
|
-
return if backend.respond_to?(:path) && backend.respond_to?(:provider)
|
|
106
|
+
return if backend.respond_to?(:path) && backend.respond_to?(:object) && backend.respond_to?(:provider)
|
|
93
107
|
|
|
94
|
-
raise ArgumentError, "Extension #backend must expose #path and #provider"
|
|
108
|
+
raise ArgumentError, "Extension #backend must expose #path, #object, and #provider"
|
|
95
109
|
end
|
|
96
110
|
|
|
97
111
|
def assert_dependencies!
|
|
@@ -110,13 +124,6 @@ module Kobako
|
|
|
110
124
|
# of a constant token are interchangeable; a value that is neither
|
|
111
125
|
# falls through unchanged to the not-installed path.
|
|
112
126
|
def symbolize(name) = name.is_a?(String) ? name.to_sym : name
|
|
113
|
-
|
|
114
|
-
def callable?(provider) = provider.respond_to?(:call)
|
|
115
|
-
|
|
116
|
-
# A callable provider reserves its path with a placeholder that the
|
|
117
|
-
# per-invocation refresh replaces before any dispatch; a fixed
|
|
118
|
-
# provider is the bound object itself.
|
|
119
|
-
def initial_object(provider) = callable?(provider) ? nil : provider
|
|
120
127
|
end
|
|
121
128
|
end
|
|
122
129
|
end
|
|
@@ -5,10 +5,10 @@ require_relative "../handle"
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Catalog
|
|
7
7
|
# Host-side mapping from opaque integer Handle IDs to Ruby objects.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
8
|
+
# Each invocation's +Kobako::Context+ mints its own table and hands it to
|
|
9
|
+
# the dispatcher, so guest→host dispatch resolves Handle targets and
|
|
10
|
+
# arguments against the same table that host→guest wire encoding allocates
|
|
11
|
+
# into for that run.
|
|
12
12
|
#
|
|
13
13
|
# Lifecycle invariants:
|
|
14
14
|
#
|
|
@@ -17,10 +17,9 @@ module Kobako
|
|
|
17
17
|
# invocation is 1; ID 0 is reserved as the invalid sentinel and is
|
|
18
18
|
# never returned by +#alloc+.
|
|
19
19
|
#
|
|
20
|
-
# -
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
# argument).
|
|
20
|
+
# - A fresh table backs each invocation, so a Handle issued in one
|
|
21
|
+
# invocation resolves in no other — uniformly regardless of
|
|
22
|
+
# allocation source (Service return or host-injected argument).
|
|
24
23
|
#
|
|
25
24
|
# - The cap is +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap
|
|
26
25
|
# raises immediately — no silent truncation, no wrap, no ID reuse.
|
|
@@ -63,14 +62,6 @@ module Kobako
|
|
|
63
62
|
@entries[id]
|
|
64
63
|
end
|
|
65
64
|
|
|
66
|
-
# Clear all entries AND reset the counter to 1. Called at the
|
|
67
|
-
# per-invocation boundary by +Kobako::Sandbox+. Returns +self+.
|
|
68
|
-
def reset!
|
|
69
|
-
@entries.clear
|
|
70
|
-
@next_id = 1
|
|
71
|
-
self
|
|
72
|
-
end
|
|
73
|
-
|
|
74
65
|
# Number of currently-bound entries. Used by tests of the Dispatcher
|
|
75
66
|
# and Codec::HandleWalk#deep_wrap to observe whether each path allocates
|
|
76
67
|
# exactly the Handle entries it should — the +Handles+ table itself never
|
|
@@ -1,42 +1,36 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "handles"
|
|
4
|
-
require_relative "../codec"
|
|
5
3
|
require_relative "../errors"
|
|
6
4
|
|
|
7
5
|
module Kobako
|
|
8
6
|
module Catalog
|
|
9
7
|
# Kobako::Catalog::Services — per-Sandbox registry of Service
|
|
10
8
|
# bindings keyed by their constant-path name. Holds the flat
|
|
11
|
-
# path→object table and the
|
|
9
|
+
# path→object table and the declared path set every invocation
|
|
10
|
+
# announces on Frame 1.
|
|
12
11
|
#
|
|
13
12
|
# Public API:
|
|
14
13
|
#
|
|
15
14
|
# services = Kobako::Catalog::Services.new
|
|
16
15
|
# services.bind("MyService::KV", kv_object) # => services (chainable)
|
|
17
|
-
# services.
|
|
16
|
+
# services.paths # => ["MyService::KV"]
|
|
18
17
|
# services.lookup("MyService::KV") # => kv_object
|
|
19
18
|
#
|
|
20
19
|
# Per-dispatch routing is +Kobako::Transport::Dispatcher+'s
|
|
21
|
-
# responsibility — the Dispatcher
|
|
22
|
-
# +
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
20
|
+
# responsibility — the Dispatcher resolves a path against the invocation's
|
|
21
|
+
# +Kobako::Context+ (which layers per-invocation providers and +ctx.bind+
|
|
22
|
+
# overrides over these base bindings) and the +Catalog::Handles+, both
|
|
23
|
+
# passed in the per-invocation dispatch Proc the Context hands to
|
|
24
|
+
# +Runtime#eval+ / +#run+.
|
|
26
25
|
class Services
|
|
27
26
|
# Ruby constant-name pattern each +::+-separated bind-path segment
|
|
28
27
|
# must match.
|
|
29
28
|
NAME_PATTERN = /\A[A-Z]\w*\z/
|
|
30
29
|
|
|
31
|
-
# Build a fresh registry.
|
|
32
|
-
|
|
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)
|
|
30
|
+
# Build a fresh registry.
|
|
31
|
+
def initialize
|
|
36
32
|
@bindings = {} # : Hash[String, untyped]
|
|
37
|
-
@handler = handler
|
|
38
33
|
@sealed = false
|
|
39
|
-
@encoded = nil # : String?
|
|
40
34
|
end
|
|
41
35
|
|
|
42
36
|
# Bind +object+ as the Service reachable at +path+ — a +Symbol+ or
|
|
@@ -56,6 +50,12 @@ module Kobako
|
|
|
56
50
|
self
|
|
57
51
|
end
|
|
58
52
|
|
|
53
|
+
# Returns +true+ iff a Service is bound at +path+ — the declared (Frame 1)
|
|
54
|
+
# key set a per-invocation +ctx.bind+ override may target.
|
|
55
|
+
def bound?(path)
|
|
56
|
+
@bindings.key?(path.to_s)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
59
|
# Resolve a +target+ constant path to the bound Service. Raises
|
|
60
60
|
# +KeyError+ when no Service is bound at +target+.
|
|
61
61
|
def lookup(target)
|
|
@@ -65,38 +65,16 @@ module Kobako
|
|
|
65
65
|
@bindings[target_str]
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def refresh(path, object)
|
|
75
|
-
key = path.to_s
|
|
76
|
-
@bindings[key] = object if @bindings.key?(key)
|
|
77
|
-
self
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery —
|
|
81
|
-
# a flat array of the bound constant paths, in bind order:
|
|
82
|
-
# +["MyService::KV", "File"]+. Routes through Kobako::Codec::Encoder
|
|
83
|
-
# like every other host-side wire encode; the preamble carries only
|
|
84
|
-
# Strings, so none of the kobako ext types fire. Returns a binary
|
|
85
|
-
# +String+ of msgpack bytes.
|
|
86
|
-
#
|
|
87
|
-
# Once sealed, the bytes are computed once and reused for every
|
|
88
|
-
# subsequent invocation: sealing freezes Service registration at the
|
|
89
|
-
# first invocation, so a bind reaching the registry after the seal
|
|
90
|
-
# raises +ArgumentError+ and never alters Frame 1.
|
|
91
|
-
def encode
|
|
92
|
-
return @encoded if @encoded
|
|
93
|
-
|
|
94
|
-
bytes = Codec::Encoder.encode(@bindings.keys).freeze
|
|
95
|
-
@encoded = bytes if @sealed
|
|
96
|
-
bytes
|
|
68
|
+
# The bound constant paths in bind order — +["MyService::KV",
|
|
69
|
+
# "File"]+ — which +Runtime#eval+ / +#run+ frame into the Frame 1
|
|
70
|
+
# preamble. The registry holds the bindings; the wire layout is the
|
|
71
|
+
# native side's.
|
|
72
|
+
def paths
|
|
73
|
+
@bindings.keys
|
|
97
74
|
end
|
|
98
75
|
|
|
99
|
-
# Mark the registry as sealed
|
|
76
|
+
# Mark the registry as sealed so registration can never alter the
|
|
77
|
+
# declared path set afterwards. Called by +Sandbox+ on the first
|
|
100
78
|
# invocation; afterwards #bind raises ArgumentError. Idempotent;
|
|
101
79
|
# returns +self+.
|
|
102
80
|
def seal!
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../codec"
|
|
4
3
|
require_relative "../snippet"
|
|
5
4
|
|
|
6
5
|
module Kobako
|
|
@@ -28,30 +27,16 @@ module Kobako
|
|
|
28
27
|
|
|
29
28
|
def initialize
|
|
30
29
|
@entries = [] # : Array[Kobako::Snippet::Source | Kobako::Snippet::Binary]
|
|
31
|
-
@encoded = nil # : String?
|
|
32
30
|
end
|
|
33
31
|
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# self-encode.
|
|
43
|
-
#
|
|
44
|
-
# The bytes are memoized — the table is replayed verbatim on every
|
|
45
|
-
# invocation after sealing, so Frame 3 never changes between
|
|
46
|
-
# encodes; #register drops the memo while the table is still open.
|
|
47
|
-
# Unlike +Catalog::Services#encode+, which gates its memo on the
|
|
48
|
-
# seal, this one can fill eagerly and invalidate in +#register+
|
|
49
|
-
# because every mutation funnels through that single method — there is
|
|
50
|
-
# no out-of-sight child object to change the result behind its back.
|
|
51
|
-
def encode
|
|
52
|
-
return @encoded if @encoded
|
|
53
|
-
|
|
54
|
-
@encoded = Codec::Encoder.encode(@entries.map { |entry| entry_payload(entry) }).freeze
|
|
32
|
+
# The registered snippets in insertion order, each projected as the
|
|
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.
|
|
38
|
+
def entries
|
|
39
|
+
@entries.map { |entry| entry_tuple(entry) }
|
|
55
40
|
end
|
|
56
41
|
|
|
57
42
|
# Register one preloaded snippet in either of two forms.
|
|
@@ -73,7 +58,6 @@ module Kobako
|
|
|
73
58
|
# missing keywords, wrong types, malformed +name+, or
|
|
74
59
|
# duplicate +code:+ +name+.
|
|
75
60
|
def register(code: nil, name: nil, binary: nil)
|
|
76
|
-
@encoded = nil
|
|
77
61
|
if binary
|
|
78
62
|
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
79
63
|
|
|
@@ -123,17 +107,16 @@ module Kobako
|
|
|
123
107
|
nil
|
|
124
108
|
end
|
|
125
109
|
|
|
126
|
-
#
|
|
127
|
-
# contribute their host-side +name+; Binary entries
|
|
128
|
-
# because the canonical name lives in the bytecode's embedded
|
|
129
|
-
# +debug_info+ and is read by the guest at load time
|
|
130
|
-
|
|
131
|
-
def entry_payload(entry)
|
|
110
|
+
# Project one entry as its +[kind, name, body]+ triple. Source
|
|
111
|
+
# entries contribute their host-side +name+; Binary entries carry
|
|
112
|
+
# +nil+ because the canonical name lives in the bytecode's embedded
|
|
113
|
+
# +debug_info+ and is read by the guest at load time.
|
|
114
|
+
def entry_tuple(entry)
|
|
132
115
|
case entry
|
|
133
116
|
when Snippet::Source
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
117
|
+
[Snippet::Source::KIND, entry.name.to_s, entry.body]
|
|
118
|
+
else
|
|
119
|
+
[Snippet::Binary::KIND, nil, entry.body]
|
|
137
120
|
end
|
|
138
121
|
end
|
|
139
122
|
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -9,10 +9,10 @@ require_relative "utils"
|
|
|
9
9
|
module Kobako
|
|
10
10
|
module Codec
|
|
11
11
|
# Module-level entry point for the host side of the kobako wire
|
|
12
|
-
# ({docs/wire-
|
|
12
|
+
# ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
|
|
13
13
|
#
|
|
14
14
|
# Translates msgpack gem exceptions into the kobako error taxonomy
|
|
15
|
-
# (
|
|
15
|
+
# (TruncatedInputError, InvalidTypeError, InvalidEncodingError, UnsupportedTypeError) 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
|
#
|
|
@@ -21,14 +21,14 @@ module Kobako
|
|
|
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 TruncatedInputError, InvalidTypeError,
|
|
25
|
+
# or InvalidEncodingError 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 InvalidTypeError without a separate Utils.with_boundary
|
|
32
32
|
# wrapper at the call site.
|
|
33
33
|
def self.decode(bytes)
|
|
34
34
|
value = FACTORY.load(bytes.b)
|
|
@@ -37,26 +37,23 @@ module Kobako
|
|
|
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 InvalidTypeError.
|
|
41
41
|
rescue ::MessagePack::UnknownExtTypeError, ::MessagePack::MalformedFormatError,
|
|
42
42
|
::MessagePack::StackError, ::ArgumentError => e
|
|
43
|
-
raise
|
|
43
|
+
raise InvalidTypeError, e.message
|
|
44
44
|
# +UnpackError+ is the gem's umbrella class for short-read /
|
|
45
45
|
# incomplete-buffer faults; +EOFError+ covers underflow at the
|
|
46
46
|
# buffer edge.
|
|
47
47
|
rescue ::MessagePack::UnpackError, ::EOFError => e
|
|
48
|
-
raise
|
|
48
|
+
raise TruncatedInputError, e.message
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# SPEC pins +str+ family payloads to UTF-8
|
|
52
|
-
# ({docs/wire-
|
|
52
|
+
# ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § str/bin
|
|
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 Utils.assert_utf8!.
|
|
57
|
-
# payloads are validated transitively: +ExtTypes#unpack_fault+
|
|
58
|
-
# feeds the inner ext-0x02 bytes back through this Decoder, so their
|
|
59
|
-
# +str+ fields are already covered by the time control returns here.
|
|
56
|
+
# str-typed leaf via Utils.assert_utf8!.
|
|
60
57
|
class << self
|
|
61
58
|
private
|
|
62
59
|
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -8,13 +8,13 @@ require_relative "ext_types"
|
|
|
8
8
|
module Kobako
|
|
9
9
|
module Codec
|
|
10
10
|
# Module-level entry point for the host side of the kobako wire
|
|
11
|
-
# ({docs/wire-
|
|
11
|
+
# ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
|
|
12
12
|
#
|
|
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
|
-
# logic; the
|
|
16
|
-
# Capability Handle
|
|
17
|
-
#
|
|
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
18
|
#
|
|
19
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
|
|
@@ -22,8 +22,8 @@ module Kobako
|
|
|
22
22
|
# themselves.
|
|
23
23
|
module Encoder
|
|
24
24
|
# Encode +value+ to wire bytes (binary-encoded String).
|
|
25
|
-
# SPEC's
|
|
26
|
-
# rejected as +
|
|
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
27
|
# (ExtTypes#register_unrepresentable), which raises before the msgpack
|
|
28
28
|
# gem can route the value through +to_msgpack+ — so a permissive
|
|
29
29
|
# +method_missing+ object cannot answer that probe and mis-encode. The
|
|
@@ -33,7 +33,7 @@ module Kobako
|
|
|
33
33
|
def self.encode(value)
|
|
34
34
|
FACTORY.dump(value)
|
|
35
35
|
rescue ::RangeError, ::NoMethodError => e
|
|
36
|
-
raise
|
|
36
|
+
raise UnsupportedTypeError, e.message
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
end
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Kobako
|
|
|
5
5
|
# Base class for all wire-codec faults raised by the pure-Ruby host codec.
|
|
6
6
|
#
|
|
7
7
|
# The wire codec implements the binary contract pinned in
|
|
8
|
-
# {docs/wire-
|
|
8
|
+
# {docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping.
|
|
9
9
|
# Every wire violation surfaces as a
|
|
10
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.
|
|
@@ -15,20 +15,20 @@ module Kobako
|
|
|
15
15
|
class Error < StandardError; end
|
|
16
16
|
|
|
17
17
|
# Input ended before the type prefix or payload was fully consumed.
|
|
18
|
-
class
|
|
18
|
+
class TruncatedInputError < Error; end
|
|
19
19
|
|
|
20
|
-
# The type byte at the current position is not in the
|
|
20
|
+
# The type byte at the current position is not in the 11-entry kobako
|
|
21
21
|
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag).
|
|
22
|
-
class
|
|
22
|
+
class InvalidTypeError < Error; end
|
|
23
23
|
|
|
24
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
|
-
class
|
|
26
|
+
class InvalidEncodingError < Error; end
|
|
27
27
|
|
|
28
28
|
# The encoder was handed a Ruby object whose type has no wire
|
|
29
29
|
# representation (e.g. Range, Time). Higher layers may catch this
|
|
30
30
|
# and re-route the value through Handle allocation, but at the
|
|
31
31
|
# codec level it is a hard error.
|
|
32
|
-
class
|
|
32
|
+
class UnsupportedTypeError < Error; end
|
|
33
33
|
end
|
|
34
34
|
end
|