kobako 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +103 -0
- data/Cargo.lock +117 -140
- 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/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +10 -12
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/lib.rs +1 -1
- data/crates/kobako-runtime/src/profile.rs +1 -1
- data/crates/kobako-runtime/src/runtime.rs +31 -17
- data/crates/kobako-runtime/src/snapshot.rs +13 -10
- data/crates/kobako-runtime/src/yielder.rs +9 -6
- data/crates/kobako-transport/CHANGELOG.md +35 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +43 -27
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +77 -53
- data/crates/kobako-transport/src/envelope/request.rs +295 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
- data/crates/kobako-wasmtime/Cargo.toml +7 -5
- data/crates/kobako-wasmtime/README.md +8 -10
- data/crates/kobako-wasmtime/src/abi.rs +5 -8
- data/crates/kobako-wasmtime/src/ambient.rs +2 -2
- data/crates/kobako-wasmtime/src/cache.rs +25 -59
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +11 -14
- data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
- data/crates/kobako-wasmtime/src/driver.rs +133 -86
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
- data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
- data/crates/kobako-wasmtime/src/invocation.rs +55 -372
- data/crates/kobako-wasmtime/src/io.rs +286 -0
- data/crates/kobako-wasmtime/src/lib.rs +3 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/trap.rs +32 -45
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/runtime/bridge.rs +38 -47
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/gvl.rs +4 -12
- data/ext/kobako/src/runtime.rs +169 -212
- 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 +16 -4
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/rust-toolchain.toml +0 -11
|
@@ -4,37 +4,17 @@ require_relative "../unresolved"
|
|
|
4
4
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Catalog
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
# present when the Sandbox seals, and resolves each callable-backed
|
|
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.
|
|
15
|
-
#
|
|
16
|
-
# Sealing and the reject-after-install guard are governed by the owning
|
|
17
|
-
# Sandbox through +Catalog::Services#sealed?+, the shared seal signal;
|
|
18
|
-
# this registry only records entries and enforces the Extension-shape
|
|
19
|
-
# and dependency rules.
|
|
20
|
-
class Extensions
|
|
7
|
+
# The installed Extensions, composed onto the Service and snippet
|
|
8
|
+
# registries. Providers resolve afresh for each invocation and are handed
|
|
9
|
+
# back rather than stored, so concurrent invocations share nothing.
|
|
10
|
+
class Extensions # :nodoc:
|
|
21
11
|
def initialize
|
|
22
12
|
@entries = [] # : Array[untyped]
|
|
23
13
|
@asserted = false
|
|
24
14
|
end
|
|
25
15
|
|
|
26
|
-
#
|
|
27
|
-
#
|
|
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.
|
|
16
|
+
# The Extension readers are duck-typed, so a malformed +name+ or
|
|
17
|
+
# +backend.path+ surfaces through the registry it routes to.
|
|
38
18
|
def install(extension, snippets:, services:)
|
|
39
19
|
validate!(extension)
|
|
40
20
|
snippets.register(code: extension.source, name: extension.name)
|
|
@@ -44,27 +24,24 @@ module Kobako
|
|
|
44
24
|
self
|
|
45
25
|
end
|
|
46
26
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
# so a seal that failed re-checks on the next attempt rather than
|
|
52
|
-
# silently passing a broken Sandbox. Idempotent across later invocations.
|
|
53
|
-
def seal!
|
|
27
|
+
# The asserted flag flips only on success, so a check that failed
|
|
28
|
+
# re-runs on the next invocation rather than silently passing a broken
|
|
29
|
+
# Sandbox.
|
|
30
|
+
def assert_dependencies!
|
|
54
31
|
return self if @asserted
|
|
55
32
|
|
|
56
|
-
|
|
33
|
+
extension, dependency = unmet_dependency
|
|
34
|
+
if extension
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"Extension #{extension.name.inspect} depends on #{dependency.inspect}, which is not installed"
|
|
37
|
+
end
|
|
38
|
+
|
|
57
39
|
@asserted = true
|
|
58
40
|
self
|
|
59
41
|
end
|
|
60
42
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
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).
|
|
43
|
+
# One provider shared by several Extensions is invoked once and its
|
|
44
|
+
# result shared, so provider identity is resource identity.
|
|
68
45
|
def resolve
|
|
69
46
|
resolved = {} # : Hash[String, untyped]
|
|
70
47
|
by_provider = {} # : Hash[untyped, untyped]
|
|
@@ -75,15 +52,8 @@ module Kobako
|
|
|
75
52
|
|
|
76
53
|
private
|
|
77
54
|
|
|
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
55
|
def install_object(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
|
|
82
56
|
|
|
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
57
|
def resolve_backend(extension, resolved, by_provider)
|
|
88
58
|
backend = extension.backend
|
|
89
59
|
return unless backend
|
|
@@ -94,9 +64,6 @@ module Kobako
|
|
|
94
64
|
resolved[backend.path.to_s] = by_provider.fetch(provider) { by_provider[provider] = provider.call }
|
|
95
65
|
end
|
|
96
66
|
|
|
97
|
-
# Enforce the Extension-shape checks +#preload+ / +#bind+ do not: a
|
|
98
|
-
# mandatory String +source+ (the install/bind boundary) and a
|
|
99
|
-
# +backend+ that, when present, exposes +path+ / +object+ / +provider+.
|
|
100
67
|
def validate!(extension)
|
|
101
68
|
source = extension.source
|
|
102
69
|
raise ArgumentError, "Extension #source must be a String, got #{source.class}" unless source.is_a?(String)
|
|
@@ -108,21 +75,16 @@ module Kobako
|
|
|
108
75
|
raise ArgumentError, "Extension #backend must expose #path, #object, and #provider"
|
|
109
76
|
end
|
|
110
77
|
|
|
111
|
-
def
|
|
78
|
+
def unmet_dependency
|
|
112
79
|
names = @entries.map { |extension| symbolize(extension.name) }
|
|
113
80
|
@entries.each do |extension|
|
|
114
81
|
(extension.depends_on || []).each do |dependency|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
raise ArgumentError,
|
|
118
|
-
"Extension #{extension.name.inspect} depends on #{dependency.inspect}, which is not installed"
|
|
82
|
+
return [extension, dependency] unless names.include?(symbolize(dependency))
|
|
119
83
|
end
|
|
120
84
|
end
|
|
85
|
+
nil
|
|
121
86
|
end
|
|
122
87
|
|
|
123
|
-
# Match names and dependencies by Symbol so the Symbol-or-String forms
|
|
124
|
-
# of a constant token are interchangeable; a value that is neither
|
|
125
|
-
# falls through unchanged to the not-installed path.
|
|
126
88
|
def symbolize(name) = name.is_a?(String) ? name.to_sym : name
|
|
127
89
|
end
|
|
128
90
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kobako
|
|
4
|
+
module Catalog
|
|
5
|
+
class Flags # :nodoc:
|
|
6
|
+
def initialize
|
|
7
|
+
@entries = {} # : Hash[String, [String, bool]]
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Each entry is the name, the value, and whether the flag is critical.
|
|
11
|
+
def entries
|
|
12
|
+
@entries.map { |name, (value, critical)| [name, value, critical] }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def register(name, value, advisory:)
|
|
16
|
+
unless name.is_a?(Symbol) || name.is_a?(String)
|
|
17
|
+
raise ArgumentError, "flag name must be a Symbol or String, got #{name.class}"
|
|
18
|
+
end
|
|
19
|
+
raise ArgumentError, "flag value must be a String, got #{value.class}" unless value.is_a?(String)
|
|
20
|
+
|
|
21
|
+
key = utf8!(name.to_s, "flag name")
|
|
22
|
+
raise ArgumentError, "flag #{key.inspect} already set" if @entries.key?(key)
|
|
23
|
+
|
|
24
|
+
@entries[key] = [utf8!(value, "flag value"), !advisory]
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# The native side carries a flag only as UTF-8 text, so text read as
|
|
31
|
+
# bytes is re-tagged, and bytes that are not UTF-8 are refused here
|
|
32
|
+
# rather than failing every invocation.
|
|
33
|
+
def utf8!(text, what)
|
|
34
|
+
utf8 = text.dup.force_encoding(Encoding::UTF_8)
|
|
35
|
+
raise ArgumentError, "#{what} must be UTF-8 text" unless utf8.valid_encoding?
|
|
36
|
+
|
|
37
|
+
utf8
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -7,30 +7,11 @@ require_relative "../transport/exposure"
|
|
|
7
7
|
|
|
8
8
|
module Kobako
|
|
9
9
|
module Catalog
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
# Lifecycle invariants:
|
|
17
|
-
#
|
|
18
|
-
# - Handle IDs are allocated by a monotonically increasing counter
|
|
19
|
-
# scoped to a single invocation. The first ID issued in an
|
|
20
|
-
# invocation is 1; ID 0 is reserved as the invalid sentinel and is
|
|
21
|
-
# never returned by +#alloc+.
|
|
22
|
-
#
|
|
23
|
-
# - A fresh table backs each invocation, so a Handle issued in one
|
|
24
|
-
# invocation resolves in no other — uniformly regardless of
|
|
25
|
-
# allocation source (Service return or host-injected argument).
|
|
26
|
-
#
|
|
27
|
-
# - The cap is +0x7fff_ffff+ (2³¹ − 1). Allocation beyond the cap
|
|
28
|
-
# raises immediately — no silent truncation, no wrap, no ID reuse.
|
|
29
|
-
class Handles
|
|
30
|
-
# Build a fresh, empty table. +next_id+ is an internal seam that
|
|
31
|
-
# sets the starting value of the monotonic counter (defaults to 1);
|
|
32
|
-
# tests pass a value near +Kobako::Handle::MAX_ID+ to exercise
|
|
33
|
-
# the cap-exhaustion path without 2³¹ allocations.
|
|
10
|
+
# One invocation's Handle table, mapping opaque ids to host objects. Each
|
|
11
|
+
# invocation mints its own, so a Handle resolves only where it was issued.
|
|
12
|
+
class Handles # :nodoc:
|
|
13
|
+
# +next_id+ lets tests start near +Kobako::Handle::MAX_ID+ to reach
|
|
14
|
+
# exhaustion without 2³¹ allocations.
|
|
34
15
|
def initialize(next_id: 1)
|
|
35
16
|
@entries = {} # : Hash[Integer, Kobako::Transport::Exposure]
|
|
36
17
|
surfaces = {} # : Hash[Module, Set[Symbol]]
|
|
@@ -38,19 +19,6 @@ module Kobako
|
|
|
38
19
|
@next_id = next_id
|
|
39
20
|
end
|
|
40
21
|
|
|
41
|
-
# Bind +object+ in the table and return a +Kobako::Handle+ token
|
|
42
|
-
# for it. +object+ is any host-side Ruby object to bind. Returns a
|
|
43
|
-
# freshly-allocated +Kobako::Handle+ whose +#id+ falls in
|
|
44
|
-
# +[Kobako::Handle::MIN_ID, Kobako::Handle::MAX_ID]+. Raises
|
|
45
|
-
# +Kobako::HandleExhaustedError+ if the next ID would exceed the
|
|
46
|
-
# cap. The cap is anchored on +Kobako::Handle+ — the wire codec
|
|
47
|
-
# and the allocator share the same invariant.
|
|
48
|
-
#
|
|
49
|
-
# Returning a Handle (rather than a bare Integer id) keeps the
|
|
50
|
-
# allocator's output a domain entity. An id is the Handle's only
|
|
51
|
-
# content, so the same internal +Kobako::Handle.restore+ constructor
|
|
52
|
-
# serves both this allocator and the codec's wire-decode path.
|
|
53
|
-
#
|
|
54
22
|
# The entry records the object's Exposure as it stands at mint, so a
|
|
55
23
|
# call made through this Handle is authorized against the reference
|
|
56
24
|
# the guest was given.
|
|
@@ -63,27 +31,17 @@ module Kobako
|
|
|
63
31
|
Kobako::Handle.restore(id)
|
|
64
32
|
end
|
|
65
33
|
|
|
66
|
-
# Resolve a Handle ID to its bound object — the very object +#alloc+
|
|
67
|
-
# received, so a Handle crossing back restores to it. +id+ is a Handle
|
|
68
|
-
# ID previously returned by +#alloc+. Raises +Kobako::SandboxError+ if
|
|
69
|
-
# +id+ is not currently bound.
|
|
70
34
|
def fetch(id)
|
|
71
35
|
exposure(id).object
|
|
72
36
|
end
|
|
73
37
|
|
|
74
|
-
# The Exposure +id+ was minted with, which authorizes a call the guest
|
|
75
|
-
# makes through that Handle. Raises +Kobako::SandboxError+ if +id+ is
|
|
76
|
-
# not currently bound.
|
|
77
38
|
def exposure(id)
|
|
78
39
|
require_bound!(id)
|
|
79
40
|
@entries[id]
|
|
80
41
|
end
|
|
81
42
|
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
# exactly the Handle entries it should — the +Handles+ table itself never
|
|
85
|
-
# consults its own size, but the surrounding code's allocation
|
|
86
|
-
# contract is part of the observable boundary.
|
|
43
|
+
# The table never consults its own size; tests read it to pin how many
|
|
44
|
+
# Handles each path allocates.
|
|
87
45
|
def size
|
|
88
46
|
@entries.size
|
|
89
47
|
end
|
|
@@ -113,8 +71,6 @@ module Kobako
|
|
|
113
71
|
end
|
|
114
72
|
end
|
|
115
73
|
|
|
116
|
-
# Guard #alloc against issuing an ID past the cap. Returns +nil+
|
|
117
|
-
# on success; raises +Kobako::HandleExhaustedError+ at exhaustion.
|
|
118
74
|
def ensure_capacity!
|
|
119
75
|
cap = Kobako::Handle::MAX_ID
|
|
120
76
|
return unless @next_id > cap
|
|
@@ -124,9 +80,6 @@ module Kobako
|
|
|
124
80
|
"in a single invocation (limit #{cap})"
|
|
125
81
|
end
|
|
126
82
|
|
|
127
|
-
# Single source of truth for the "unknown Handle id" raise used by
|
|
128
|
-
# #fetch. Returns +nil+ on success; raises +Kobako::SandboxError+
|
|
129
|
-
# when +id+ is not currently bound.
|
|
130
83
|
def require_bound!(id)
|
|
131
84
|
return if @entries.key?(id)
|
|
132
85
|
|
|
@@ -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!.
|