kobako 0.12.2 → 0.14.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 +41 -0
- data/Cargo.lock +126 -137
- data/README.md +5 -3
- data/ROADMAP.md +26 -0
- data/crates/kobako-runtime/CHANGELOG.md +15 -0
- data/crates/kobako-runtime/Cargo.toml +1 -1
- data/crates/kobako-runtime/README.md +1 -1
- data/crates/kobako-runtime/src/error.rs +8 -0
- data/crates/kobako-runtime/src/lib.rs +4 -2
- data/crates/kobako-runtime/src/profile.rs +35 -0
- data/crates/kobako-runtime/src/runtime.rs +7 -0
- data/crates/kobako-wasmtime/CHANGELOG.md +15 -0
- data/crates/kobako-wasmtime/Cargo.toml +4 -4
- data/crates/kobako-wasmtime/README.md +1 -1
- data/crates/kobako-wasmtime/src/ambient.rs +5 -2
- data/crates/kobako-wasmtime/src/config.rs +10 -2
- data/crates/kobako-wasmtime/src/driver.rs +13 -1
- data/crates/kobako-wasmtime/src/frames.rs +105 -5
- data/crates/kobako-wasmtime/src/instance_pre.rs +7 -6
- data/crates/kobako-wasmtime/src/invocation.rs +4 -5
- data/crates/kobako-wasmtime/src/trap.rs +42 -21
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +1 -1
- data/ext/kobako/src/lib.rs +0 -2
- data/ext/kobako/src/runtime.rs +114 -38
- data/lib/kobako/catalog/handles.rb +2 -2
- data/lib/kobako/catalog/namespaces.rb +2 -2
- data/lib/kobako/catalog/snippets.rb +1 -1
- data/lib/kobako/codec/decoder.rb +7 -7
- data/lib/kobako/codec/encoder.rb +2 -2
- data/lib/kobako/codec/error.rb +3 -3
- data/lib/kobako/codec/factory.rb +10 -10
- data/lib/kobako/codec/handle_walk.rb +17 -21
- data/lib/kobako/codec/utils.rb +9 -9
- data/lib/kobako/codec.rb +4 -4
- data/lib/kobako/errors.rb +14 -39
- data/lib/kobako/handle.rb +5 -2
- data/lib/kobako/namespace.rb +3 -3
- data/lib/kobako/outcome.rb +1 -0
- data/lib/kobako/sandbox.rb +61 -33
- data/lib/kobako/sandbox_options.rb +51 -9
- data/lib/kobako/transport/dispatcher.rb +17 -14
- data/lib/kobako/transport/request.rb +2 -2
- data/lib/kobako/transport/response.rb +2 -2
- data/lib/kobako/transport/run.rb +1 -1
- data/lib/kobako/transport/yield.rb +3 -3
- data/lib/kobako/transport/yielder.rb +6 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +0 -1
- data/release-please-config.json +66 -6
- data/sig/kobako/codec/handle_walk.rbs +2 -2
- data/sig/kobako/codec.rbs +11 -0
- data/sig/kobako/handle.rbs +0 -2
- data/sig/kobako/runtime.rbs +8 -3
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +11 -2
- data/sig/kobako/transport/dispatcher.rbs +8 -8
- data/sig/kobako/transport/run.rbs +1 -1
- data/sig/kobako/transport/yielder.rbs +2 -2
- data/sig/kobako/transport.rbs +8 -0
- metadata +3 -4
- data/ext/kobako/src/snapshot.rs +0 -75
- data/lib/kobako/snapshot.rb +0 -32
- data/sig/kobako/snapshot.rbs +0 -12
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//! Kobako::Runtime — wraps a `kobako_wasmtime::Driver` + the Ruby seams
|
|
6
6
|
//!
|
|
7
7
|
//! constructed via `Kobako::Runtime.from_path(path, timeout, memory_limit,
|
|
8
|
-
//! stdout_limit, stderr_limit)`. Every invocation (`#eval` / `#run`)
|
|
8
|
+
//! stdout_limit, stderr_limit, profile)`. Every invocation (`#eval` / `#run`)
|
|
9
9
|
//! instantiates a fresh instance and discards the whole Store afterwards —
|
|
10
10
|
//! the per-invocation instance discipline. The run mechanics —
|
|
11
11
|
//! engine/module caches, caps, trap classification — live in the
|
|
@@ -21,24 +21,25 @@
|
|
|
21
21
|
//!
|
|
22
22
|
//! This file owns the `Kobako::Runtime` magnus class itself — the Ruby
|
|
23
23
|
//! init() that registers the class, the byte↔`RString` shuttling, the
|
|
24
|
-
//! dispatch-Proc GC root, and the per-invocation usage
|
|
24
|
+
//! dispatch-Proc GC root, and the per-invocation usage / capture readouts.
|
|
25
25
|
|
|
26
26
|
mod bridge;
|
|
27
27
|
mod errors;
|
|
28
28
|
|
|
29
|
-
use magnus::{
|
|
29
|
+
use magnus::{
|
|
30
|
+
function, gc, method, prelude::*, typed_data::DataTypeFunctions, value::Opaque,
|
|
31
|
+
Error as MagnusError, RArray, RModule, RString, Ruby, Symbol, TypedData, Value,
|
|
32
|
+
};
|
|
30
33
|
|
|
31
|
-
use std::cell::Cell;
|
|
34
|
+
use std::cell::{Cell, RefCell};
|
|
32
35
|
use std::path::Path;
|
|
33
36
|
use std::sync::Arc;
|
|
34
37
|
use std::time::Duration;
|
|
35
38
|
|
|
36
|
-
use magnus::{gc, typed_data::DataTypeFunctions, value::Opaque, RArray, TypedData, Value};
|
|
37
|
-
|
|
38
|
-
use crate::snapshot::Snapshot;
|
|
39
39
|
use kobako_runtime::dispatch::DispatchHandler;
|
|
40
|
+
use kobako_runtime::profile::Profile;
|
|
40
41
|
use kobako_runtime::runtime::{Entry, Frames, Runtime as ContractRuntime};
|
|
41
|
-
use kobako_runtime::snapshot::{Completion, Snapshot as RuntimeSnapshot, Usage};
|
|
42
|
+
use kobako_runtime::snapshot::{Capture, Completion, Snapshot as RuntimeSnapshot, Usage};
|
|
42
43
|
use kobako_wasmtime::{Config, Driver};
|
|
43
44
|
|
|
44
45
|
/// Copy the bytes of `s` into a fresh `Vec<u8>`. Single safe entry to
|
|
@@ -52,6 +53,15 @@ fn rstring_to_vec(s: RString) -> Vec<u8> {
|
|
|
52
53
|
unsafe { s.as_slice() }.to_vec()
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
/// The pre-invocation sentinel for one capture channel: no bytes, cap
|
|
57
|
+
/// not reached. Fresh `Vec`s per call because `Capture` owns its buffer.
|
|
58
|
+
fn empty_capture() -> Capture {
|
|
59
|
+
Capture {
|
|
60
|
+
bytes: Vec::new(),
|
|
61
|
+
truncated: false,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
// ---------------------------------------------------------------------------
|
|
56
66
|
// Ruby init
|
|
57
67
|
// ---------------------------------------------------------------------------
|
|
@@ -65,11 +75,13 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
65
75
|
// intermediate hierarchy is registered.
|
|
66
76
|
|
|
67
77
|
let runtime = kobako.define_class("Runtime", ruby.class_object())?;
|
|
68
|
-
runtime.define_singleton_method("from_path", function!(Runtime::from_path,
|
|
78
|
+
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 6))?;
|
|
69
79
|
runtime.define_method("on_dispatch=", method!(Runtime::set_on_dispatch, 1))?;
|
|
70
80
|
runtime.define_method("eval", method!(Runtime::eval, 3))?;
|
|
71
81
|
runtime.define_method("run", method!(Runtime::run, 3))?;
|
|
72
82
|
runtime.define_method("usage", method!(Runtime::usage, 0))?;
|
|
83
|
+
runtime.define_method("captures", method!(Runtime::captures, 0))?;
|
|
84
|
+
runtime.define_method("profile", method!(Runtime::profile, 0))?;
|
|
73
85
|
// The guest re-enters for a block yield through a frame-scoped
|
|
74
86
|
// `Kobako::Runtime::GuestYielder` the dispatcher hands the Proc, not a
|
|
75
87
|
// method on Runtime.
|
|
@@ -93,11 +105,17 @@ struct Runtime {
|
|
|
93
105
|
// reference the one Proc this `Opaque` pins. `Cell` is sound under the
|
|
94
106
|
// GVL (see the `unsafe impl Sync` below).
|
|
95
107
|
on_dispatch: Cell<Option<Opaque<Value>>>,
|
|
96
|
-
// Usage of the most recent invocation,
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
// invocation.
|
|
108
|
+
// Usage of the most recent invocation, stashed here so `#usage` reads
|
|
109
|
+
// survive the per-invocation Store teardown and the trap path's
|
|
110
|
+
// raise. Zeroed before the first invocation.
|
|
100
111
|
last_usage: Cell<Usage>,
|
|
112
|
+
// Output captures of the most recent invocation, stashed for the same
|
|
113
|
+
// reason as `last_usage`: the trap path raises, and this readout is
|
|
114
|
+
// what keeps the guest's partial output readable after a rescue.
|
|
115
|
+
// `RefCell` (not `Cell`) because `Capture` owns its byte buffer; the
|
|
116
|
+
// same GVL single-thread discipline applies (see the `unsafe impl
|
|
117
|
+
// Sync` below). Empty before the first invocation.
|
|
118
|
+
last_captures: RefCell<(Capture, Capture)>,
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
impl DataTypeFunctions for Runtime {
|
|
@@ -118,11 +136,11 @@ impl DataTypeFunctions for Runtime {
|
|
|
118
136
|
}
|
|
119
137
|
|
|
120
138
|
// SAFETY: magnus requires `Send + Sync` on TypedData types. The
|
|
121
|
-
// `on_dispatch` / `last_usage` `Cell`s
|
|
122
|
-
// unavailable, but every access to them
|
|
123
|
-
// thread at a time — Ruby method calls,
|
|
124
|
-
// holds the GVL. No cross-thread access to
|
|
125
|
-
// stays auto-derived.
|
|
139
|
+
// `on_dispatch` / `last_usage` `Cell`s and the `last_captures` `RefCell`
|
|
140
|
+
// make the auto-derived `Sync` unavailable, but every access to them
|
|
141
|
+
// happens under the GVL on a single thread at a time — Ruby method calls,
|
|
142
|
+
// and a GC `mark` pass that also holds the GVL. No cross-thread access to
|
|
143
|
+
// any of them can occur. `Send` stays auto-derived.
|
|
126
144
|
unsafe impl Sync for Runtime {}
|
|
127
145
|
|
|
128
146
|
impl Runtime {
|
|
@@ -135,15 +153,18 @@ impl Runtime {
|
|
|
135
153
|
/// (`None` disables); `memory_limit` is the linear-memory cap in
|
|
136
154
|
/// bytes (`None` disables); `stdout_limit_bytes` / `stderr_limit_bytes`
|
|
137
155
|
/// are the per-channel output caps (`None`
|
|
138
|
-
/// disables)
|
|
139
|
-
/// (`
|
|
140
|
-
///
|
|
156
|
+
/// disables); `profile` is the isolation rung the driver builds
|
|
157
|
+
/// (`:permissive` / `:hermetic`). All five are validated by the
|
|
158
|
+
/// caller (`Kobako::Sandbox`); this method only refuses non-finite
|
|
159
|
+
/// or non-positive timeouts and off-ladder profiles as a defence in
|
|
160
|
+
/// depth.
|
|
141
161
|
fn from_path(
|
|
142
162
|
path: String,
|
|
143
163
|
timeout_seconds: Option<f64>,
|
|
144
164
|
memory_limit: Option<usize>,
|
|
145
165
|
stdout_limit_bytes: Option<usize>,
|
|
146
166
|
stderr_limit_bytes: Option<usize>,
|
|
167
|
+
profile: Symbol,
|
|
147
168
|
) -> Result<Self, MagnusError> {
|
|
148
169
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
149
170
|
let timeout = match timeout_seconds {
|
|
@@ -161,6 +182,19 @@ impl Runtime {
|
|
|
161
182
|
));
|
|
162
183
|
}
|
|
163
184
|
};
|
|
185
|
+
// Fail closed on an off-ladder rung: an unrecognized posture must
|
|
186
|
+
// never fall back to a grant. Same defence-in-depth posture as the
|
|
187
|
+
// timeout guard above — `SandboxOptions` is the primary validator.
|
|
188
|
+
let profile = match profile.name()?.as_ref() {
|
|
189
|
+
"hermetic" => Profile::Hermetic,
|
|
190
|
+
"permissive" => Profile::Permissive,
|
|
191
|
+
other => {
|
|
192
|
+
return Err(MagnusError::new(
|
|
193
|
+
ruby.exception_arg_error(),
|
|
194
|
+
format!("profile must be :permissive or :hermetic, got :{other}"),
|
|
195
|
+
));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
164
198
|
|
|
165
199
|
let driver = Driver::new(
|
|
166
200
|
Path::new(&path),
|
|
@@ -169,6 +203,7 @@ impl Runtime {
|
|
|
169
203
|
timeout,
|
|
170
204
|
stdout_limit_bytes,
|
|
171
205
|
stderr_limit_bytes,
|
|
206
|
+
profile,
|
|
172
207
|
},
|
|
173
208
|
)
|
|
174
209
|
.map_err(|e| errors::setup_to_magnus(&ruby, e))?;
|
|
@@ -179,6 +214,7 @@ impl Runtime {
|
|
|
179
214
|
wall_time: 0.0,
|
|
180
215
|
memory_peak: 0,
|
|
181
216
|
}),
|
|
217
|
+
last_captures: RefCell::new((empty_capture(), empty_capture())),
|
|
182
218
|
})
|
|
183
219
|
}
|
|
184
220
|
|
|
@@ -204,7 +240,7 @@ impl Runtime {
|
|
|
204
240
|
/// One-shot mruby source execution (`#eval`). The Ruby-facing entry:
|
|
205
241
|
/// builds the dispatch handler from the registered Proc, hands the
|
|
206
242
|
/// three stdin frames (`preamble`, `source`, `snippets`) and the source
|
|
207
|
-
/// to the driver, and settles the
|
|
243
|
+
/// to the driver, and settles the invocation through
|
|
208
244
|
/// `finish_invocation` — or maps a could-not-start `Error` onto its
|
|
209
245
|
/// `Kobako::*` exception. The run mechanics — frames, caps, trap
|
|
210
246
|
/// classification — live in `kobako_wasmtime::Driver`.
|
|
@@ -213,7 +249,7 @@ impl Runtime {
|
|
|
213
249
|
preamble: RString,
|
|
214
250
|
source: RString,
|
|
215
251
|
snippets: RString,
|
|
216
|
-
) -> Result<
|
|
252
|
+
) -> Result<RString, MagnusError> {
|
|
217
253
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
218
254
|
let handler = self.build_handler();
|
|
219
255
|
let preamble = rstring_to_vec(preamble);
|
|
@@ -233,8 +269,8 @@ impl Runtime {
|
|
|
233
269
|
self.finish_invocation(&ruby, snapshot)
|
|
234
270
|
}
|
|
235
271
|
|
|
236
|
-
/// Execute one entrypoint dispatch (`__kobako_run`) and return
|
|
237
|
-
///
|
|
272
|
+
/// Execute one entrypoint dispatch (`__kobako_run`) and return the
|
|
273
|
+
/// guest's raw outcome bytes.
|
|
238
274
|
///
|
|
239
275
|
/// The two-frame stdin protocol (preamble + snippets; no user source
|
|
240
276
|
/// frame — docs/wire-codec.md § Invocation channels) plus the
|
|
@@ -246,7 +282,7 @@ impl Runtime {
|
|
|
246
282
|
preamble: RString,
|
|
247
283
|
snippets: RString,
|
|
248
284
|
envelope: RString,
|
|
249
|
-
) -> Result<
|
|
285
|
+
) -> Result<RString, MagnusError> {
|
|
250
286
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
251
287
|
let handler = self.build_handler();
|
|
252
288
|
let preamble = rstring_to_vec(preamble);
|
|
@@ -268,20 +304,27 @@ impl Runtime {
|
|
|
268
304
|
self.finish_invocation(&ruby, snapshot)
|
|
269
305
|
}
|
|
270
306
|
|
|
271
|
-
/// Settle one invocation's `Snapshot` at the Ruby boundary: usage
|
|
272
|
-
///
|
|
273
|
-
///
|
|
274
|
-
/// is
|
|
307
|
+
/// Settle one invocation's `Snapshot` at the Ruby boundary: usage and
|
|
308
|
+
/// the two output captures are recorded on every outcome, so the
|
|
309
|
+
/// `#usage` / `#captures` readouts survive the trap path's raise —
|
|
310
|
+
/// that is what keeps the guest's partial output readable after the
|
|
311
|
+
/// Host App rescues the trap. A completed guest invocation returns
|
|
312
|
+
/// its raw outcome bytes; the Sandbox layer decodes them.
|
|
275
313
|
fn finish_invocation(
|
|
276
314
|
&self,
|
|
277
315
|
ruby: &Ruby,
|
|
278
316
|
snapshot: RuntimeSnapshot,
|
|
279
|
-
) -> Result<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
317
|
+
) -> Result<RString, MagnusError> {
|
|
318
|
+
let RuntimeSnapshot {
|
|
319
|
+
completion,
|
|
320
|
+
stdout,
|
|
321
|
+
stderr,
|
|
322
|
+
usage,
|
|
323
|
+
} = snapshot;
|
|
324
|
+
self.last_usage.set(usage);
|
|
325
|
+
self.last_captures.replace((stdout, stderr));
|
|
326
|
+
match completion {
|
|
327
|
+
Completion::Outcome(bytes) => Ok(ruby.str_from_slice(&bytes)),
|
|
285
328
|
Completion::Trap(trap) => Err(errors::trap_to_magnus(ruby, trap)),
|
|
286
329
|
}
|
|
287
330
|
}
|
|
@@ -313,8 +356,8 @@ impl Runtime {
|
|
|
313
356
|
/// size captured at invocation entry. `0` before the first
|
|
314
357
|
/// invocation.
|
|
315
358
|
///
|
|
316
|
-
/// Reads the `last_usage` Cell `finish_invocation` populated
|
|
317
|
-
///
|
|
359
|
+
/// Reads the `last_usage` Cell `finish_invocation` populated before
|
|
360
|
+
/// the per-invocation Store was discarded.
|
|
318
361
|
fn usage(&self) -> Result<RArray, MagnusError> {
|
|
319
362
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
320
363
|
let usage = self.last_usage.get();
|
|
@@ -323,4 +366,37 @@ impl Runtime {
|
|
|
323
366
|
arr.push(usage.memory_peak)?;
|
|
324
367
|
Ok(arr)
|
|
325
368
|
}
|
|
369
|
+
|
|
370
|
+
/// Return the per-last-invocation output captures as a Ruby 4-tuple
|
|
371
|
+
/// `[stdout_bytes, stdout_truncated, stderr_bytes, stderr_truncated]`
|
|
372
|
+
/// — the flat positional layout mirrors `#usage`, and the element
|
|
373
|
+
/// order matches the destructure in `Kobako::Sandbox#read_captures!`;
|
|
374
|
+
/// reorder both sides together.
|
|
375
|
+
///
|
|
376
|
+
/// Reads the `last_captures` pair `finish_invocation` stashed on
|
|
377
|
+
/// every outcome, so the readout also covers the trap path, where
|
|
378
|
+
/// `#eval` / `#run` raise instead of returning outcome bytes.
|
|
379
|
+
/// Empty bytes and `false` flags before the first invocation.
|
|
380
|
+
fn captures(&self) -> Result<RArray, MagnusError> {
|
|
381
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
382
|
+
let captures = self.last_captures.borrow();
|
|
383
|
+
let (stdout, stderr) = &*captures;
|
|
384
|
+
let arr = ruby.ary_new_capa(4);
|
|
385
|
+
arr.push(ruby.str_from_slice(&stdout.bytes))?;
|
|
386
|
+
arr.push(stdout.truncated)?;
|
|
387
|
+
arr.push(ruby.str_from_slice(&stderr.bytes))?;
|
|
388
|
+
arr.push(stderr.truncated)?;
|
|
389
|
+
Ok(arr)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/// Return the isolation profile the driver built, as a Symbol
|
|
393
|
+
/// (`:hermetic` / `:permissive`) — the declaration the Sandbox
|
|
394
|
+
/// compares against the posture its `profile:` option requested.
|
|
395
|
+
fn profile(&self) -> Symbol {
|
|
396
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
397
|
+
match self.driver.profile() {
|
|
398
|
+
Profile::Hermetic => ruby.to_symbol("hermetic"),
|
|
399
|
+
Profile::Permissive => ruby.to_symbol("permissive"),
|
|
400
|
+
}
|
|
401
|
+
}
|
|
326
402
|
}
|
|
@@ -95,7 +95,7 @@ module Kobako
|
|
|
95
95
|
end
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
-
# Guard
|
|
98
|
+
# Guard #alloc against issuing an ID past the cap. Returns +nil+
|
|
99
99
|
# on success; raises +Kobako::HandleExhaustedError+ at exhaustion.
|
|
100
100
|
def ensure_capacity!
|
|
101
101
|
cap = Kobako::Handle::MAX_ID
|
|
@@ -107,7 +107,7 @@ module Kobako
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Single source of truth for the "unknown Handle id" raise used by
|
|
110
|
-
#
|
|
110
|
+
# #fetch. Returns +nil+ on success; raises +Kobako::SandboxError+
|
|
111
111
|
# when +id+ is not currently bound.
|
|
112
112
|
def require_bound!(id)
|
|
113
113
|
return if @entries.key?(id)
|
|
@@ -71,7 +71,7 @@ module Kobako
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
# Encode the preamble as msgpack bytes for stdin Frame 1 delivery.
|
|
74
|
-
# Routes through
|
|
74
|
+
# Routes through Kobako::Codec::Encoder like every other host-side
|
|
75
75
|
# wire encode so there is a single codec path; the preamble carries
|
|
76
76
|
# only Strings and Arrays, so none of the kobako ext types actually
|
|
77
77
|
# fire. Structure: +[["Namespace", ["MemberA", "MemberB"]], ...]+.
|
|
@@ -106,7 +106,7 @@ module Kobako
|
|
|
106
106
|
self
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
-
# Returns +true+ when
|
|
109
|
+
# Returns +true+ when #seal! has been called, +false+ otherwise.
|
|
110
110
|
def sealed?
|
|
111
111
|
@sealed
|
|
112
112
|
end
|
|
@@ -43,7 +43,7 @@ module Kobako
|
|
|
43
43
|
#
|
|
44
44
|
# The bytes are memoized — the table is replayed verbatim on every
|
|
45
45
|
# invocation after sealing, so Frame 3 never changes between
|
|
46
|
-
# encodes;
|
|
46
|
+
# encodes; #register drops the memo while the table is still open.
|
|
47
47
|
# Unlike +Catalog::Namespaces#encode+, which gates its memo on the
|
|
48
48
|
# seal, this one can fill eagerly and invalidate in +#register+
|
|
49
49
|
# because every mutation funnels through that single method — there is
|
data/lib/kobako/codec/decoder.rb
CHANGED
|
@@ -12,23 +12,23 @@ module Kobako
|
|
|
12
12
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping).
|
|
13
13
|
#
|
|
14
14
|
# Translates msgpack gem exceptions into the kobako error taxonomy
|
|
15
|
-
# (
|
|
15
|
+
# (Truncated, InvalidType, InvalidEncoding, UnsupportedType) so
|
|
16
16
|
# callers can pattern-match on the SPEC's wire-violation categories
|
|
17
17
|
# without leaking the gem's internal exception classes.
|
|
18
18
|
#
|
|
19
|
-
# Public API is a single function —
|
|
19
|
+
# Public API is a single function — +.decode+. The decoder is
|
|
20
20
|
# stateless; the +MessagePack::Unpacker+ instance is built per call
|
|
21
21
|
# because callers always decode exactly one wire value at a time.
|
|
22
22
|
module Decoder
|
|
23
23
|
# Decode +bytes+ into one Ruby value and validate transitively
|
|
24
|
-
# against the SPEC type mapping. Raises
|
|
25
|
-
# or
|
|
24
|
+
# against the SPEC type mapping. Raises Truncated, InvalidType,
|
|
25
|
+
# or InvalidEncoding on wire violations.
|
|
26
26
|
#
|
|
27
27
|
# When a block is given, the decoded value is yielded and the block's
|
|
28
28
|
# result is returned — wire Value Objects use this to build themselves
|
|
29
29
|
# from the decoded payload. The block runs inside this method's
|
|
30
30
|
# rescue, so a Value Object's +ArgumentError+ invariant failure
|
|
31
|
-
# surfaces as
|
|
31
|
+
# surfaces as InvalidType without a separate Utils.with_boundary
|
|
32
32
|
# wrapper at the call site.
|
|
33
33
|
def self.decode(bytes)
|
|
34
34
|
value = Factory.load(bytes.b)
|
|
@@ -37,7 +37,7 @@ 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 InvalidType.
|
|
41
41
|
rescue ::MessagePack::UnknownExtTypeError, ::MessagePack::MalformedFormatError,
|
|
42
42
|
::MessagePack::StackError, ::ArgumentError => e
|
|
43
43
|
raise InvalidType, e.message
|
|
@@ -53,7 +53,7 @@ module Kobako
|
|
|
53
53
|
# Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
|
|
54
54
|
# str family but does not validate the bytes; +bin+ family decodes
|
|
55
55
|
# to ASCII-8BIT. Walk the tree once and reject invalid UTF-8 in any
|
|
56
|
-
# str-typed leaf via
|
|
56
|
+
# str-typed leaf via Utils.assert_utf8!. Kobako::Fault
|
|
57
57
|
# payloads are validated transitively: +Factory.unpack_fault+
|
|
58
58
|
# feeds the inner ext-0x02 bytes back through this Decoder, so their
|
|
59
59
|
# +str+ fields are already covered by the time control returns here.
|
data/lib/kobako/codec/encoder.rb
CHANGED
|
@@ -14,9 +14,9 @@ module Kobako
|
|
|
14
14
|
# strings, arrays, and maps go through the gem's narrowest-encoding
|
|
15
15
|
# logic; the three kobako-specific ext types (0x00 Symbol, 0x01
|
|
16
16
|
# Capability Handle, 0x02 Exception envelope) are registered on
|
|
17
|
-
# the cached
|
|
17
|
+
# the cached Kobako::Codec::Factory singleton.
|
|
18
18
|
#
|
|
19
|
-
# Public API is a single function —
|
|
19
|
+
# Public API is a single function — +.encode+. The codec is stateless;
|
|
20
20
|
# there is no buffer accumulator and no streaming write API. Callers
|
|
21
21
|
# that need to concatenate multiple encodings build the bytes
|
|
22
22
|
# themselves.
|
data/lib/kobako/codec/error.rb
CHANGED
|
@@ -7,11 +7,11 @@ module Kobako
|
|
|
7
7
|
# The wire codec implements the binary contract pinned in
|
|
8
8
|
# {docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type Mapping.
|
|
9
9
|
# Every wire violation surfaces as a
|
|
10
|
-
# subclass of
|
|
10
|
+
# subclass of Error so callers can pattern-match on the specific
|
|
11
11
|
# fault while still rescuing all codec faults via this base class.
|
|
12
12
|
#
|
|
13
13
|
# Higher layers (e.g. the Sandbox dispatch loop) translate these into
|
|
14
|
-
# the public
|
|
14
|
+
# the public Kobako::SandboxError / Kobako::TrapError taxonomy.
|
|
15
15
|
class Error < StandardError; end
|
|
16
16
|
|
|
17
17
|
# Input ended before the type prefix or payload was fully consumed.
|
|
@@ -21,7 +21,7 @@ module Kobako
|
|
|
21
21
|
# type mapping (e.g. an unknown ext code, or a reserved msgpack tag).
|
|
22
22
|
class InvalidType < Error; end
|
|
23
23
|
|
|
24
|
-
# A msgpack
|
|
24
|
+
# A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
|
|
25
25
|
# payload was not valid UTF-8 — both are wire violations per SPEC.
|
|
26
26
|
class InvalidEncoding < Error; end
|
|
27
27
|
|
data/lib/kobako/codec/factory.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Kobako
|
|
|
16
16
|
# § Ext Types).
|
|
17
17
|
#
|
|
18
18
|
# The factory is the single place in the host gem that touches the
|
|
19
|
-
# msgpack API — both
|
|
19
|
+
# msgpack API — both Encoder and Decoder delegate through it, so
|
|
20
20
|
# the three kobako ext codes (0x00 Symbol, 0x01 Capability Handle,
|
|
21
21
|
# 0x02 Exception envelope) are configured exactly once at first use.
|
|
22
22
|
#
|
|
@@ -93,11 +93,11 @@ module Kobako
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
# Validate the ext-0x00 payload as UTF-8 and intern. Raises
|
|
96
|
-
#
|
|
96
|
+
# InvalidEncoding on invalid bytes — SPEC forbids the
|
|
97
97
|
# binary-encoding fallback that msgpack-gem's default unpacker
|
|
98
98
|
# would otherwise apply. The re-tag step lives here because the
|
|
99
99
|
# msgpack ext-type unpacker hands us binary bytes; the assertion
|
|
100
|
-
# itself is shared with
|
|
100
|
+
# itself is shared with Decoder via Utils.assert_utf8!. The
|
|
101
101
|
# +"Symbol"+ label keeps the error message in Ruby vocabulary
|
|
102
102
|
# rather than wire-ext-code vocabulary.
|
|
103
103
|
def unpack_symbol(payload)
|
|
@@ -125,7 +125,7 @@ module Kobako
|
|
|
125
125
|
# Peel off the fixext-4 frame, hand the bytes to the
|
|
126
126
|
# Host-Gem-internal +Kobako::Handle.restore+ factory, and
|
|
127
127
|
# translate the +ArgumentError+ raised by Handle's invariants
|
|
128
|
-
# into a wire-layer +InvalidType+ via
|
|
128
|
+
# into a wire-layer +InvalidType+ via Codec::Utils.with_boundary.
|
|
129
129
|
# The Value Object owns the id-range contract; this method only
|
|
130
130
|
# owns the frame shape.
|
|
131
131
|
def unpack_handle(payload)
|
|
@@ -136,11 +136,11 @@ module Kobako
|
|
|
136
136
|
Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
# Encode the inner ext-0x02 map via
|
|
139
|
+
# Encode the inner ext-0x02 map via Encoder (not +factory.dump+) so
|
|
140
140
|
# the embedded payload flows through the same boundary as a top-level
|
|
141
141
|
# encode — nested kobako values (Handle, nested Fault) reach the
|
|
142
142
|
# registered ext-type packers via the cached singleton. A +details+
|
|
143
|
-
# chain nested past
|
|
143
|
+
# chain nested past MAX_EXT_DEPTH has no wire representation and
|
|
144
144
|
# surfaces as +UnsupportedType+.
|
|
145
145
|
def pack_fault(fault)
|
|
146
146
|
within_ext_frame(UnsupportedType) do
|
|
@@ -149,12 +149,12 @@ module Kobako
|
|
|
149
149
|
end
|
|
150
150
|
|
|
151
151
|
# Peel the embedded msgpack map and hand it to +Kobako::Fault.new+
|
|
152
|
-
# inside
|
|
152
|
+
# inside Decoder.decode's block form, so the value-object's
|
|
153
153
|
# +ArgumentError+ invariants surface as +InvalidType+ through the
|
|
154
|
-
# decoder boundary. Inner decode goes through
|
|
154
|
+
# decoder boundary. Inner decode goes through Decoder (not
|
|
155
155
|
# +factory.load+) so the embedded +str+ payloads flow through the
|
|
156
156
|
# same UTF-8 validation as a top-level decode. A nested ext 0x02 in
|
|
157
|
-
# +details+ re-enters this method, so
|
|
157
|
+
# +details+ re-enters this method, so #within_ext_frame bounds the
|
|
158
158
|
# chain depth to keep it from exhausting the native stack.
|
|
159
159
|
def unpack_fault(payload)
|
|
160
160
|
within_ext_frame(InvalidType) do
|
|
@@ -167,7 +167,7 @@ module Kobako
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
# Track ext-envelope re-entry depth and refuse a chain past
|
|
170
|
-
#
|
|
170
|
+
# MAX_EXT_DEPTH, raising +over_limit+ so the failure lands in the
|
|
171
171
|
# caller's existing wire-error class. The counter is thread-scoped and
|
|
172
172
|
# balanced by the +ensure+, so a raise mid-chain still unwinds it to
|
|
173
173
|
# its entry value.
|
|
@@ -5,16 +5,16 @@ require_relative "../handle"
|
|
|
5
5
|
module Kobako
|
|
6
6
|
module Codec
|
|
7
7
|
# Substitutes Capability Handles into and out of a Ruby value tree at
|
|
8
|
-
# the host↔guest boundary.
|
|
8
|
+
# the host↔guest boundary. #deep_wrap allocates a +Kobako::Handle+ for
|
|
9
9
|
# each non-wire-representable leaf on the host→guest +#run+ argument
|
|
10
|
-
# path;
|
|
11
|
-
# host object on every guest→host value path.
|
|
12
|
-
# by-value codec-type predicate that decides which leaves
|
|
10
|
+
# path; #deep_restore resolves each wire-decoded Handle back to its
|
|
11
|
+
# host object on every guest→host value path. #representable? is the
|
|
12
|
+
# by-value codec-type predicate that decides which leaves #deep_wrap
|
|
13
13
|
# must wrap: the closed 12-entry wire type set
|
|
14
14
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
15
15
|
# Mapping).
|
|
16
16
|
#
|
|
17
|
-
# All helpers are pure except
|
|
17
|
+
# All helpers are pure except #deep_wrap, whose only side effect is
|
|
18
18
|
# allocating new Handle ids into the supplied table.
|
|
19
19
|
module HandleWalk
|
|
20
20
|
module_function
|
|
@@ -24,7 +24,7 @@ module Kobako
|
|
|
24
24
|
# unsigned +uint 64+ maximum
|
|
25
25
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
|
|
26
26
|
# Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
|
|
27
|
-
# Anchored as a +Range+ so
|
|
27
|
+
# Anchored as a +Range+ so #primitive_type? stays a single
|
|
28
28
|
# dispatch line. This is the codec's encode domain — not to
|
|
29
29
|
# be confused with the Handle id range, which lives on
|
|
30
30
|
# +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
|
|
@@ -41,13 +41,13 @@ module Kobako
|
|
|
41
41
|
# representable. Integers outside the codec's signed-64 /
|
|
42
42
|
# unsigned-64 union are rejected so the predicate agrees with the
|
|
43
43
|
# msgpack gem's encode-time +RangeError+ behaviour the codec
|
|
44
|
-
# already surfaces as
|
|
44
|
+
# already surfaces as UnsupportedType.
|
|
45
45
|
def representable?(value)
|
|
46
46
|
primitive_type?(value) || container_representable?(value)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
50
|
-
# leaf that fails
|
|
50
|
+
# leaf that fails #representable? with a +Kobako::Handle+
|
|
51
51
|
# allocated from +handler+. The
|
|
52
52
|
# walk only descends through representable container shapes
|
|
53
53
|
# (Array, Hash) one structural level at a time; a non-representable
|
|
@@ -61,13 +61,9 @@ module Kobako
|
|
|
61
61
|
# whose leaves are either representable or +Kobako::Handle+
|
|
62
62
|
# tokens.
|
|
63
63
|
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
# inside a block would resolve against the enclosing +self+
|
|
68
|
-
# (still +HandleWalk+ at definition time, but the qualified form
|
|
69
|
-
# keeps the dispatch readable when the recursive call sits inside a
|
|
70
|
-
# Proc captured from elsewhere).
|
|
64
|
+
# Recursive calls spell the +HandleWalk.+ receiver so the dispatch
|
|
65
|
+
# stays valid even when a block is captured and run under a
|
|
66
|
+
# different +self+ (+module_function+ privatizes the instance copies).
|
|
71
67
|
def deep_wrap(value, handler)
|
|
72
68
|
case value
|
|
73
69
|
when ::Array then value.map { |element| HandleWalk.deep_wrap(element, handler) }
|
|
@@ -79,7 +75,7 @@ module Kobako
|
|
|
79
75
|
|
|
80
76
|
# Deep-walk Array / Hash containers in +value+ and replace every
|
|
81
77
|
# +Kobako::Handle+ leaf with the host-side object +handler+ resolves
|
|
82
|
-
# it to. The symmetric inverse of
|
|
78
|
+
# it to. The symmetric inverse of #deep_wrap: that walk allocates objects
|
|
83
79
|
# into Handles on the host→guest argument path; this walk resolves
|
|
84
80
|
# Handles back to their objects wherever a guest→host payload carries
|
|
85
81
|
# one — an invocation result, a yield-block result, or a dispatch
|
|
@@ -107,9 +103,9 @@ module Kobako
|
|
|
107
103
|
end
|
|
108
104
|
end
|
|
109
105
|
|
|
110
|
-
# The non-container branch of
|
|
106
|
+
# The non-container branch of #representable?: returns +true+ for
|
|
111
107
|
# the scalar leaves and an existing Handle. Not part of the
|
|
112
|
-
# public surface; reach for
|
|
108
|
+
# public surface; reach for #representable? instead.
|
|
113
109
|
def primitive_type?(value)
|
|
114
110
|
case value
|
|
115
111
|
when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
|
|
@@ -118,10 +114,10 @@ module Kobako
|
|
|
118
114
|
end
|
|
119
115
|
end
|
|
120
116
|
|
|
121
|
-
# The container branch of
|
|
117
|
+
# The container branch of #representable?: recurses into Array
|
|
122
118
|
# elements and Hash key+value pairs through the public
|
|
123
|
-
#
|
|
124
|
-
#
|
|
119
|
+
# #representable?. Not part of the public surface; reach for
|
|
120
|
+
# #representable? instead.
|
|
125
121
|
def container_representable?(value)
|
|
126
122
|
case value
|
|
127
123
|
when ::Array then value.all? { |element| HandleWalk.representable?(element) }
|
data/lib/kobako/codec/utils.rb
CHANGED
|
@@ -10,18 +10,18 @@ module Kobako
|
|
|
10
10
|
# - UTF-8 assertion at the codec boundary
|
|
11
11
|
# ({docs/wire-codec.md}[link:../../../docs/wire-codec.md]
|
|
12
12
|
# § str/bin Encoding Rules and § Ext Types → ext 0x00). Used by
|
|
13
|
-
#
|
|
13
|
+
# Decoder when walking +str+ family payloads and by Factory
|
|
14
14
|
# when validating the +ext 0x00+ Symbol payload.
|
|
15
15
|
# - +ArgumentError+ translation at the codec boundary
|
|
16
|
-
# (
|
|
17
|
-
#
|
|
16
|
+
# (#with_boundary) so the public taxonomy stays
|
|
17
|
+
# Kobako::Codec::Error.
|
|
18
18
|
#
|
|
19
19
|
# Both helpers are pure — they only inspect inputs, never mutate them.
|
|
20
|
-
# The host↔guest Handle substitution walk lives in
|
|
20
|
+
# The host↔guest Handle substitution walk lives in HandleWalk.
|
|
21
21
|
module Utils
|
|
22
22
|
module_function
|
|
23
23
|
|
|
24
|
-
# Raise
|
|
24
|
+
# Raise InvalidEncoding unless +string+'s bytes are valid under
|
|
25
25
|
# its current encoding tag. +label+ is the caller-supplied prefix
|
|
26
26
|
# for the error message (e.g. +"str payload"+, +"Symbol payload"+).
|
|
27
27
|
def assert_utf8!(string, label)
|
|
@@ -32,13 +32,13 @@ module Kobako
|
|
|
32
32
|
|
|
33
33
|
# Run +block+ at the codec boundary: a value object raises
|
|
34
34
|
# +ArgumentError+ when an invariant is violated at construction, and
|
|
35
|
-
# this helper surfaces that as
|
|
36
|
-
# stays
|
|
35
|
+
# this helper surfaces that as InvalidType so the public taxonomy
|
|
36
|
+
# stays Kobako::Codec::Error and never leaks +ArgumentError+ from
|
|
37
37
|
# the Ruby standard library.
|
|
38
38
|
#
|
|
39
39
|
# Reach for this only where a value object is constructed outside a
|
|
40
|
-
#
|
|
41
|
-
# mapping (worked example:
|
|
40
|
+
# Decoder.decode block, whose rescue already performs the same
|
|
41
|
+
# mapping (worked example: Factory#unpack_handle building
|
|
42
42
|
# +Handle.restore+ from a raw fixext payload). Do not use it for
|
|
43
43
|
# general-purpose validation outside the codec boundary —
|
|
44
44
|
# host-layer +ArgumentError+ values should propagate unchanged.
|
data/lib/kobako/codec.rb
CHANGED
|
@@ -18,12 +18,12 @@ module Kobako
|
|
|
18
18
|
# the kobako root so the codec can register them without depending
|
|
19
19
|
# upward on Transport.
|
|
20
20
|
#
|
|
21
|
-
# Backed by the official +msgpack+ gem via
|
|
22
|
-
#
|
|
21
|
+
# Backed by the official +msgpack+ gem via Factory; Encoder and
|
|
22
|
+
# Decoder are thin wrappers that register the three kobako-specific
|
|
23
23
|
# ext types (0x00 Symbol, 0x01 Capability Handle, 0x02 Exception
|
|
24
24
|
# envelope) on a single +MessagePack::Factory+ instance. The Rust side
|
|
25
|
-
# mirrors this layer as the +codec+ module in the +kobako-
|
|
26
|
-
# the ext-code constants live as module-private values on
|
|
25
|
+
# mirrors this layer as the +codec+ module in the +kobako-codec+ crate;
|
|
26
|
+
# the ext-code constants live as module-private values on Factory
|
|
27
27
|
# alongside +codec::EXT_SYMBOL+ / +codec::EXT_HANDLE+ /
|
|
28
28
|
# +codec::EXT_ERRENV+ on that side.
|
|
29
29
|
module Codec
|