kobako 0.11.1 → 0.12.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.
@@ -42,16 +42,15 @@ mod cache;
42
42
  mod capture;
43
43
  mod config;
44
44
  mod dispatch;
45
+ mod errors;
45
46
  mod exports;
47
+ mod frames;
46
48
  mod guest_mem;
47
49
  mod instance_pre;
48
50
  mod invocation;
49
51
  mod trap;
50
52
 
51
- use magnus::value::Lazy;
52
- use magnus::{
53
- function, method, prelude::*, Error as MagnusError, ExceptionClass, RModule, RString, Ruby,
54
- };
53
+ use magnus::{function, method, prelude::*, Error as MagnusError, RModule, RString, Ruby};
55
54
 
56
55
  use std::cell::Cell;
57
56
  use std::path::Path;
@@ -61,11 +60,8 @@ use magnus::{gc, typed_data::DataTypeFunctions, value::Opaque, RArray, TypedData
61
60
 
62
61
  use crate::snapshot::Snapshot;
63
62
  use wasmtime::{
64
- AsContextMut, InstancePre as WtInstancePre, Memory, ResourceLimiter, Store as WtStore,
65
- TypedFunc,
63
+ AsContextMut, InstancePre as WtInstancePre, ResourceLimiter, Store as WtStore, TypedFunc,
66
64
  };
67
- use wasmtime_wasi::p2::pipe::{MemoryInputPipe, MemoryOutputPipe};
68
- use wasmtime_wasi::WasiCtxBuilder;
69
65
 
70
66
  use self::cache::shared_engine;
71
67
  use self::config::Config;
@@ -93,94 +89,6 @@ pub(crate) fn rstring_to_vec(s: RString) -> Vec<u8> {
93
89
  unsafe { s.as_slice() }.to_vec()
94
90
  }
95
91
 
96
- // ---------------------------------------------------------------------------
97
- // Error classes (lazy-resolved from Ruby once the top-level Kobako error
98
- // hierarchy is loaded by `lib/kobako/errors.rb`). The ext raises directly
99
- // into the invocation-outcome taxonomy (`TrapError` and its subclasses)
100
- // for run-path failures and into the construction-layer `SetupError`
101
- // (and its `ModuleNotBuiltError` subclass) for `from_path` setup failures
102
- // — no engine-specific intermediate layer; the Sandbox layer adds the
103
- // verb prefix and lets the subclass identity flow through unchanged.
104
- // ---------------------------------------------------------------------------
105
-
106
- /// Resolve `Kobako::<name>` as an `ExceptionClass` — the shared body of
107
- /// every error-class `Lazy` below, which differ only in the constant
108
- /// name. The constants are guaranteed present by the time any of these
109
- /// lazies first resolve (`lib/kobako/errors.rb` loads the hierarchy before
110
- /// the ext raises into it), so a missing constant is a build / wiring bug
111
- /// and the `unwrap` is the correct fail-fast.
112
- fn kobako_error_class(ruby: &Ruby, name: &str) -> ExceptionClass {
113
- let kobako: RModule = ruby.class_object().const_get("Kobako").unwrap();
114
- kobako.const_get(name).unwrap()
115
- }
116
-
117
- pub(crate) static SETUP_ERROR: Lazy<ExceptionClass> =
118
- Lazy::new(|ruby| kobako_error_class(ruby, "SetupError"));
119
-
120
- pub(crate) static MODULE_NOT_BUILT_ERROR: Lazy<ExceptionClass> =
121
- Lazy::new(|ruby| kobako_error_class(ruby, "ModuleNotBuiltError"));
122
-
123
- pub(crate) static TRAP_ERROR: Lazy<ExceptionClass> =
124
- Lazy::new(|ruby| kobako_error_class(ruby, "TrapError"));
125
-
126
- pub(crate) static TIMEOUT_ERROR: Lazy<ExceptionClass> =
127
- Lazy::new(|ruby| kobako_error_class(ruby, "TimeoutError"));
128
-
129
- pub(crate) static MEMORY_LIMIT_ERROR: Lazy<ExceptionClass> =
130
- Lazy::new(|ruby| kobako_error_class(ruby, "MemoryLimitError"));
131
-
132
- pub(crate) static SANDBOX_ERROR: Lazy<ExceptionClass> =
133
- Lazy::new(|ruby| kobako_error_class(ruby, "SandboxError"));
134
-
135
- /// Build a `MagnusError` in `class` carrying `msg` — the shared body of
136
- /// the named `*_err` constructors below, which differ only in which
137
- /// error-class `Lazy` they target.
138
- fn error_in(ruby: &Ruby, class: &Lazy<ExceptionClass>, msg: impl Into<String>) -> MagnusError {
139
- MagnusError::new(ruby.get_inner(class), msg.into())
140
- }
141
-
142
- /// Construct a `Kobako::TrapError` magnus error. Used for every
143
- /// invocation-time wasmtime engine failure that is not a configured-cap
144
- /// trap — missing exports, allocation faults, memory write/read failures.
145
- /// Construction-time setup failures use `setup_err`, not this.
146
- pub(crate) fn trap_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
147
- error_in(ruby, &TRAP_ERROR, msg)
148
- }
149
-
150
- /// Construct a `Kobako::SetupError` magnus error. Used for every
151
- /// construction-time failure on the `Runtime.from_path` path before any
152
- /// invocation runs — unreadable artifact, bytes that are not a valid Wasm
153
- /// module, or engine / linker / instantiation setup failure. The
154
- /// `ModuleNotBuiltError` subclass (artifact absent) is
155
- /// raised through `MODULE_NOT_BUILT_ERROR` directly.
156
- pub(crate) fn setup_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
157
- error_in(ruby, &SETUP_ERROR, msg)
158
- }
159
-
160
- /// Construct a `Kobako::TimeoutError` magnus error. Surfaces the
161
- /// wall-clock cap path with the verb prefix added
162
- /// by `Kobako::Sandbox#invoke!`.
163
- pub(crate) fn timeout_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
164
- error_in(ruby, &TIMEOUT_ERROR, msg)
165
- }
166
-
167
- /// Construct a `Kobako::MemoryLimitError` magnus error. Surfaces the
168
- /// linear-memory cap path with the verb prefix
169
- /// added by `Kobako::Sandbox#invoke!`.
170
- pub(crate) fn memory_limit_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
171
- error_in(ruby, &MEMORY_LIMIT_ERROR, msg)
172
- }
173
-
174
- /// Construct a `Kobako::SandboxError` magnus error. Used for the
175
- /// host-side pre-call faults the SPEC attributes to the sandbox / wire
176
- /// layer rather than the Wasm engine — currently the `#run` invocation
177
- /// envelope reservation failure (`__kobako_alloc` returns 0).
178
- /// The runtime is intact, so this must not be a
179
- /// `TrapError`: no discard-and-recreate recovery is owed to the caller.
180
- pub(crate) fn sandbox_err(ruby: &Ruby, msg: impl Into<String>) -> MagnusError {
181
- error_in(ruby, &SANDBOX_ERROR, msg)
182
- }
183
-
184
92
  // ---------------------------------------------------------------------------
185
93
  // Ruby init
186
94
  // ---------------------------------------------------------------------------
@@ -327,7 +235,7 @@ impl Runtime {
327
235
  /// `Invocation::wasi_mut`.
328
236
  fn probe_abi_version(&self, ruby: &Ruby) -> Result<(), MagnusError> {
329
237
  let mut store = self.new_store()?;
330
- install_wasi_frames(&mut store, &self.config, &[])?;
238
+ frames::install_wasi_frames(&mut store, &self.config, &[])?;
331
239
  let instance = self
332
240
  .instance_pre
333
241
  .instantiate(store.as_context_mut())
@@ -335,7 +243,7 @@ impl Runtime {
335
243
  let probe = instance
336
244
  .get_typed_func::<(), u32>(store.as_context_mut(), "__kobako_abi_version")
337
245
  .map_err(|_| {
338
- setup_err(
246
+ errors::setup_err(
339
247
  ruby,
340
248
  format!(
341
249
  "the Guest Binary does not export __kobako_abi_version; \
@@ -344,13 +252,13 @@ impl Runtime {
344
252
  )
345
253
  })?;
346
254
  let reported = probe.call(store.as_context_mut(), ()).map_err(|e| {
347
- setup_err(
255
+ errors::setup_err(
348
256
  ruby,
349
257
  format!("failed to read the Guest Binary's ABI version: {e}"),
350
258
  )
351
259
  })?;
352
260
  if reported != ABI_VERSION {
353
- return Err(setup_err(
261
+ return Err(errors::setup_err(
354
262
  ruby,
355
263
  format!(
356
264
  "the Guest Binary reports ABI version {reported}, but this host \
@@ -396,14 +304,14 @@ impl Runtime {
396
304
 
397
305
  let bytes = rstring_to_vec(args_bytes);
398
306
  let Some(caller) = dispatch::current_caller() else {
399
- return Err(trap_err(
307
+ return Err(errors::trap_err(
400
308
  &ruby,
401
309
  "yield_to_active_invocation called outside an active Sandbox dispatch frame",
402
310
  ));
403
311
  };
404
312
 
405
313
  let resp_bytes =
406
- guest_mem::drive_yield(caller, &bytes).map_err(|msg| trap_err(&ruby, msg))?;
314
+ guest_mem::drive_yield(caller, &bytes).map_err(|msg| errors::trap_err(&ruby, msg))?;
407
315
  Ok(ruby.str_from_slice(&resp_bytes))
408
316
  }
409
317
 
@@ -438,7 +346,7 @@ impl Runtime {
438
346
  ) -> Result<Snapshot, MagnusError> {
439
347
  let ruby = Ruby::get().expect("Ruby thread");
440
348
  let mut store = self.new_store()?;
441
- install_wasi_frames(
349
+ frames::install_wasi_frames(
442
350
  &mut store,
443
351
  &self.config,
444
352
  &[
@@ -448,7 +356,7 @@ impl Runtime {
448
356
  ],
449
357
  )?;
450
358
  let exports = self.instantiate(&ruby, &mut store)?;
451
- let eval = require_export(&ruby, exports.eval.as_ref())?;
359
+ let eval = frames::require_export(&ruby, exports.eval.as_ref())?;
452
360
  self.call_with_caps(&mut store, &exports, eval, ())
453
361
  .map_err(|e| trap::call_err(&ruby, e))?;
454
362
  self.build_snapshot(&ruby, &mut store, &exports)
@@ -472,14 +380,14 @@ impl Runtime {
472
380
  ) -> Result<Snapshot, MagnusError> {
473
381
  let ruby = Ruby::get().expect("Ruby thread");
474
382
  let mut store = self.new_store()?;
475
- install_wasi_frames(
383
+ frames::install_wasi_frames(
476
384
  &mut store,
477
385
  &self.config,
478
386
  &[rstring_to_vec(preamble), rstring_to_vec(snippets)],
479
387
  )?;
480
388
  let exports = self.instantiate(&ruby, &mut store)?;
481
- let run = require_export(&ruby, exports.run.as_ref())?;
482
- let (env_ptr, env_len) = write_envelope(&ruby, &mut store, &exports, envelope)?;
389
+ let run = frames::require_export(&ruby, exports.run.as_ref())?;
390
+ let (env_ptr, env_len) = frames::write_envelope(&ruby, &mut store, &exports, envelope)?;
483
391
  self.call_with_caps(&mut store, &exports, run, (env_ptr, env_len))
484
392
  .map_err(|e| trap::call_err(&ruby, e))?;
485
393
  self.build_snapshot(&ruby, &mut store, &exports)
@@ -544,7 +452,7 @@ impl Runtime {
544
452
  .instance_pre
545
453
  .instantiate(store.as_context_mut())
546
454
  .map_err(|e| {
547
- trap_err(
455
+ errors::trap_err(
548
456
  ruby,
549
457
  format!("failed to instantiate the Sandbox runtime: {e}"),
550
458
  )
@@ -630,7 +538,7 @@ impl Runtime {
630
538
  store: &mut WtStore<Invocation>,
631
539
  exports: &Exports,
632
540
  ) -> Result<Snapshot, MagnusError> {
633
- let return_bytes = fetch_outcome_bytes(ruby, store, exports)?;
541
+ let return_bytes = frames::fetch_outcome_bytes(ruby, store, exports)?;
634
542
  let data = store.data();
635
543
  let (stdout_raw, stderr_raw, wall_time, memory_peak) = (
636
544
  data.stdout_bytes(),
@@ -666,170 +574,3 @@ fn disarm_caps(store: &mut WtStore<Invocation>) {
666
574
  store.data_mut().stop_wall_clock();
667
575
  store.data_mut().disarm_memory_cap();
668
576
  }
669
-
670
- /// Return the resolved `memory` export handle, or raise
671
- /// `Kobako::TrapError` when the loaded module exports no linear
672
- /// memory — the "not a Kobako-shaped runtime" failure mode
673
- /// (`SANDBOX_RUNTIME_NOT_KOBAKO`).
674
- fn require_memory(ruby: &Ruby, exports: &Exports) -> Result<Memory, MagnusError> {
675
- exports
676
- .memory
677
- .ok_or_else(|| trap_err(ruby, SANDBOX_RUNTIME_NOT_KOBAKO))
678
- }
679
-
680
- /// Allocate a `len`-byte buffer in guest linear memory via
681
- /// `__kobako_alloc`, copy `envelope` into it, and return `(ptr, len)`
682
- /// as `i32` values matching the `__kobako_run(env_ptr, env_len)` ABI.
683
- /// Raises `Kobako::TrapError` when the allocation hook is missing or
684
- /// itself traps, and `Kobako::SandboxError` when the hook runs but
685
- /// cannot reserve the buffer (`__kobako_alloc` returns 0) — an
686
- /// intact runtime, not an engine fault.
687
- fn write_envelope(
688
- ruby: &Ruby,
689
- store: &mut WtStore<Invocation>,
690
- exports: &Exports,
691
- envelope: RString,
692
- ) -> Result<(i32, i32), MagnusError> {
693
- let bytes = rstring_to_vec(envelope);
694
- let len_i32 = guest_mem::checked_payload_len(bytes.len()).map_err(|msg| trap_err(ruby, msg))?;
695
-
696
- let alloc = require_export(ruby, exports.alloc.as_ref())?;
697
- let memory = require_memory(ruby, exports)?;
698
-
699
- let ptr = alloc
700
- .call(store.as_context_mut(), bytes.len() as u32)
701
- .map_err(|e| trap_err(ruby, format!("failed to allocate input buffer: {}", e)))?;
702
- if ptr == 0 {
703
- return Err(sandbox_err(
704
- ruby,
705
- "could not allocate input buffer (out of memory)",
706
- ));
707
- }
708
- let data = memory.data_mut(store.as_context_mut());
709
- let range = guest_mem::guest_buffer_range(ptr as usize, bytes.len(), data.len())
710
- .map_err(|msg| trap_err(ruby, msg))?;
711
- data[range].copy_from_slice(&bytes);
712
-
713
- Ok((ptr as i32, len_i32))
714
- }
715
-
716
- /// Build the per-invocation WASI context with stdin carrying every frame
717
- /// in `frames` (each prefixed by its 4-byte big-endian u32 length —
718
- /// docs/wire-codec.md § Invocation channels) plus fresh stdout / stderr
719
- /// pipes, and install it on the invocation's Store. `#eval` passes three
720
- /// frames (preamble, source, snippets), `#run` passes two (preamble,
721
- /// snippets — the invocation envelope arrives via linear memory
722
- /// instead). Each output pipe is sized at `cap + 1` so
723
- /// `capture::clip_capture` can distinguish "wrote exactly cap bytes"
724
- /// from "exceeded cap"; uncapped channels fall back to `usize::MAX` and
725
- /// rely on `memory_limit` for the real ceiling.
726
- /// Raises `Kobako::TrapError` when any frame exceeds the 16 MiB cap that
727
- /// keeps its `u32` length prefix from wrapping.
728
- fn install_wasi_frames(
729
- store: &mut WtStore<Invocation>,
730
- config: &Config,
731
- frames: &[Vec<u8>],
732
- ) -> Result<(), MagnusError> {
733
- let ruby = Ruby::get().expect("Ruby thread");
734
- // Every frame carries the same 16 MiB cap as the `#run` envelope
735
- // (`write_envelope`): the length prefix is a `u32`, so a frame past
736
- // the cap would silently wrap and corrupt the stdin frame stream.
737
- for frame in frames {
738
- guest_mem::checked_payload_len(frame.len()).map_err(|msg| trap_err(&ruby, msg))?;
739
- }
740
-
741
- let total: usize = frames.iter().map(|f| 4 + f.len()).sum();
742
- let mut stdin_content: Vec<u8> = Vec::with_capacity(total);
743
- for frame in frames {
744
- stdin_content.extend_from_slice(&(frame.len() as u32).to_be_bytes());
745
- stdin_content.extend_from_slice(frame);
746
- }
747
-
748
- let stdin_pipe = MemoryInputPipe::new(stdin_content);
749
- let stdout_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stdout_limit_bytes));
750
- let stderr_pipe = MemoryOutputPipe::new(capture::pipe_capacity(config.stderr_limit_bytes));
751
-
752
- let mut builder = WasiCtxBuilder::new();
753
- builder.stdin(stdin_pipe);
754
- builder.stdout(stdout_pipe.clone());
755
- builder.stderr(stderr_pipe.clone());
756
- // Deny the preview1 ambient-authority imports the guest never legitimately
757
- // reaches but the WASI layer would otherwise grant (see `ambient`).
758
- builder.wall_clock(ambient::FrozenWallClock);
759
- builder.monotonic_clock(ambient::FrozenMonotonicClock);
760
- builder.secure_random(ambient::deterministic_rng());
761
- let wasi = builder.build_p1();
762
-
763
- store
764
- .data_mut()
765
- .install_wasi(wasi, stdout_pipe, stderr_pipe);
766
- Ok(())
767
- }
768
-
769
- /// Invoke `__kobako_take_outcome`, decode the packed `(ptr<<32)|len`
770
- /// u64, and copy the OUTCOME_BUFFER slice out of guest memory. Raises
771
- /// `Kobako::TrapError` when the export is missing, `len` exceeds the
772
- /// 16 MiB single-dispatch cap, the `ptr`/`len` arithmetic overflows,
773
- /// the slice falls outside live memory, or the `memory` export itself
774
- /// is absent.
775
- fn fetch_outcome_bytes(
776
- ruby: &Ruby,
777
- store: &mut WtStore<Invocation>,
778
- exports: &Exports,
779
- ) -> Result<Vec<u8>, MagnusError> {
780
- let take = require_export(ruby, exports.take_outcome.as_ref())?;
781
- let mem = require_memory(ruby, exports)?;
782
-
783
- let packed = take
784
- .call(store.as_context_mut(), ())
785
- .map_err(|e| trap_err(ruby, format!("failed to read the Sandbox result: {}", e)))?;
786
- let (ptr, len) = guest_mem::unpack_outcome_packed(packed);
787
- if len > guest_mem::MAX_DISPATCH_PAYLOAD {
788
- return Err(trap_err(ruby, "result payload exceeds the 16 MiB limit"));
789
- }
790
-
791
- let data = mem.data(store.as_context_mut());
792
- let range = guest_mem::guest_buffer_range(ptr, len, data.len()).map_err(|msg| {
793
- trap_err(
794
- ruby,
795
- format!("the Sandbox result is out of bounds: {}", msg),
796
- )
797
- })?;
798
- Ok(data[range].to_vec())
799
- }
800
-
801
- /// User-facing message for the "Sandbox runtime is missing one of the
802
- /// internal Kobako hooks" failure mode. Phrased in caller vocabulary —
803
- /// the underlying ABI symbol names (`__kobako_alloc`, `__kobako_eval`,
804
- /// `__kobako_take_outcome`) are not actionable to callers, and the
805
- /// gem itself raises this error so a self-reference like "matches the
806
- /// kobako gem version" reads as third-person. The actionable
807
- /// diagnosis is "your data/kobako.wasm is out of sync; rebuild it".
808
- const SANDBOX_RUNTIME_MISSING_HOOKS: &str = "Sandbox runtime is missing required hooks; \
809
- rebuild data/kobako.wasm against the installed version";
810
-
811
- /// User-facing message for the "the loaded Wasm module is not a
812
- /// Kobako-shaped runtime at all" failure mode (no linear memory
813
- /// export). Same phrasing philosophy as
814
- /// `SANDBOX_RUNTIME_MISSING_HOOKS`.
815
- const SANDBOX_RUNTIME_NOT_KOBAKO: &str =
816
- "the loaded Wasm module is not a Kobako-compatible runtime";
817
-
818
- /// Return the resolved `TypedFunc` for an ABI export, or raise
819
- /// `Kobako::TrapError` when the option is `None`. Both run-path
820
- /// methods (`#eval`, `#run`) plus the `build_snapshot` readout that
821
- /// drains `OUTCOME_BUFFER` share the same "missing export → Ruby
822
- /// error" boilerplate; this helper collapses those sites onto one
823
- /// safe entry. The user-facing message is intentionally export-
824
- /// agnostic (see `SANDBOX_RUNTIME_MISSING_HOOKS`) — the ABI symbol
825
- /// name is not actionable to callers, so it is not threaded in.
826
- fn require_export<'a, Params, Results>(
827
- ruby: &Ruby,
828
- export: Option<&'a TypedFunc<Params, Results>>,
829
- ) -> Result<&'a TypedFunc<Params, Results>, MagnusError>
830
- where
831
- Params: wasmtime::WasmParams,
832
- Results: wasmtime::WasmResults,
833
- {
834
- export.ok_or_else(|| trap_err(ruby, SANDBOX_RUNTIME_MISSING_HOOKS))
835
- }
@@ -43,9 +43,9 @@ module Kobako
43
43
  # and the allocator share the same invariant.
44
44
  #
45
45
  # Returning a Handle (rather than a bare Integer id) keeps the
46
- # allocator's output a domain entity; +Kobako::Handle.restore+
47
- # is reserved for the codec's wire-decode path, where the id is
48
- # the only thing the bytes carry.
46
+ # allocator's output a domain entity. An id is the Handle's only
47
+ # content, so the same internal +Kobako::Handle.restore+ constructor
48
+ # serves both this allocator and the codec's wire-decode path.
49
49
  def alloc(object)
50
50
  reject_unwrappable!(object)
51
51
  ensure_capacity!
@@ -72,7 +72,7 @@ module Kobako
72
72
  end
73
73
 
74
74
  # Number of currently-bound entries. Used by tests of the Dispatcher
75
- # and Codec::Utils#deep_wrap to observe whether each path allocates
75
+ # and Codec::HandleWalk#deep_wrap to observe whether each path allocates
76
76
  # exactly the Handle entries it should — the +Handles+ table itself never
77
77
  # consults its own size, but the surrounding code's allocation
78
78
  # contract is part of the observable boundary.
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../handle"
4
+
5
+ module Kobako
6
+ module Codec
7
+ # Substitutes Capability Handles into and out of a Ruby value tree at
8
+ # the host↔guest boundary. {deep_wrap} allocates a +Kobako::Handle+ for
9
+ # each non-wire-representable leaf on the host→guest +#run+ argument
10
+ # path; {deep_restore} resolves each wire-decoded Handle back to its
11
+ # host object on every guest→host value path — the +#eval+ / +#run+
12
+ # result and the yield-block result alike. {representable?} is the
13
+ # by-value codec-type predicate that decides which leaves {deep_wrap}
14
+ # must wrap: the closed 12-entry wire type set
15
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
16
+ # Mapping).
17
+ #
18
+ # All helpers are pure except {deep_wrap}, whose only side effect is
19
+ # allocating new Handle ids into the supplied table.
20
+ module HandleWalk
21
+ module_function
22
+
23
+ # Inclusive Integer range the msgpack gem encodes without raising
24
+ # +RangeError+ at encode time — signed +int 64+ minimum through
25
+ # unsigned +uint 64+ maximum
26
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
27
+ # Mapping #3, the +fixint+ / +int 8..64+ / +uint 8..64+ union).
28
+ # Anchored as a +Range+ so {primitive_type?} stays a single
29
+ # dispatch line. This is the codec's encode domain — not to
30
+ # be confused with the Handle id range, which lives on
31
+ # +Kobako::Handle+ as +MIN_ID+ / +MAX_ID+ (1..2^31 − 1) and
32
+ # represents a different concept entirely.
33
+ MSGPACK_INT_RANGE = (-(2**63)..((2**64) - 1))
34
+
35
+ # Codec-type predicate
36
+ # ({docs/wire-codec.md}[link:../../../docs/wire-codec.md] § Type
37
+ # Mapping). Returns +true+ when +value+ belongs to the closed
38
+ # 12-entry codec type set — +nil+, +TrueClass+, +FalseClass+,
39
+ # +Integer+ (in the +i64..u64+ value domain), +Float+, +String+,
40
+ # +Symbol+, +Kobako::Handle+, +Array+ whose every element is itself
41
+ # representable, or +Hash+ whose every key and value are
42
+ # representable. Integers outside the codec's signed-64 /
43
+ # unsigned-64 union are rejected so the predicate agrees with the
44
+ # msgpack gem's encode-time +RangeError+ behaviour the codec
45
+ # already surfaces as {UnsupportedType}.
46
+ def representable?(value)
47
+ primitive_type?(value) || container_representable?(value)
48
+ end
49
+
50
+ # Deep-walk Array / Hash containers in +value+ and replace every
51
+ # leaf that fails {representable?} with a +Kobako::Handle+
52
+ # allocated from +handler+. The
53
+ # walk only descends through representable container shapes
54
+ # (Array, Hash) one structural level at a time; a non-representable
55
+ # leaf is wrapped as-is without inspecting its internal structure.
56
+ # An existing +Kobako::Handle+ is representable and passes through
57
+ # unchanged — auto-wrap never re-wraps a Handle.
58
+ #
59
+ # +value+ may be any Ruby value; +handler+ must respond to
60
+ # +#alloc(object) -> Kobako::Handle+ (a host-side
61
+ # +Kobako::Catalog::Handles+). Returns a structurally equivalent value
62
+ # whose leaves are either representable or +Kobako::Handle+
63
+ # tokens.
64
+ #
65
+ # The block bodies spell +HandleWalk.deep_wrap+ explicitly rather
66
+ # than the unqualified +deep_wrap+ because +module_function+ makes
67
+ # the instance copy of these helpers private; an implicit receiver
68
+ # inside a block would resolve against the enclosing +self+
69
+ # (still +HandleWalk+ at definition time, but the qualified form
70
+ # keeps the dispatch readable when the recursive call sits inside a
71
+ # Proc captured from elsewhere).
72
+ def deep_wrap(value, handler)
73
+ case value
74
+ when ::Array then value.map { |element| HandleWalk.deep_wrap(element, handler) }
75
+ when ::Hash then value.transform_values { |val| HandleWalk.deep_wrap(val, handler) }
76
+ else
77
+ representable?(value) ? value : handler.alloc(value)
78
+ end
79
+ end
80
+
81
+ # Deep-walk Array / Hash containers in +value+ and replace every
82
+ # +Kobako::Handle+ leaf with the host-side object +handler+ resolves
83
+ # it to. The symmetric inverse of {deep_wrap}: that walk allocates objects
84
+ # into Handles on the host→guest argument path; this walk resolves
85
+ # Handles back to their objects on every guest→host value path — the
86
+ # +#eval+ / +#run+ result and the yield-block result alike. The walk
87
+ # descends through Array elements and Hash keys and values one
88
+ # structural level at a time; any non-Handle leaf passes through
89
+ # unchanged.
90
+ #
91
+ # +value+ is a decoded Ruby value (a Handle here is a wire-decoded
92
+ # +Kobako::Handle+, never a guest-forged one); +handler+ must
93
+ # respond to +#fetch(id) -> object+ (a host-side
94
+ # +Kobako::Catalog::Handles+). +handler.fetch+ raises
95
+ # +Kobako::SandboxError+ for an id with no live binding, the
96
+ # corrupted-runtime fallback.
97
+ def deep_restore(value, handler)
98
+ case value
99
+ when ::Array then value.map { |element| HandleWalk.deep_restore(element, handler) }
100
+ when ::Hash
101
+ value.to_h { |key, val| [HandleWalk.deep_restore(key, handler), HandleWalk.deep_restore(val, handler)] }
102
+ when Kobako::Handle then handler.fetch(value.id)
103
+ else value
104
+ end
105
+ end
106
+
107
+ # The non-container branch of {representable?}: returns +true+ for
108
+ # the scalar leaves and an existing Handle. Not part of the
109
+ # public surface; reach for {representable?} instead.
110
+ def primitive_type?(value)
111
+ case value
112
+ when ::NilClass, ::TrueClass, ::FalseClass, ::Float, ::String, ::Symbol, Kobako::Handle then true
113
+ when ::Integer then MSGPACK_INT_RANGE.cover?(value)
114
+ else false
115
+ end
116
+ end
117
+
118
+ # The container branch of {representable?}: recurses into Array
119
+ # elements and Hash key+value pairs through the public
120
+ # {representable?}. Not part of the public surface; reach for
121
+ # {representable?} instead.
122
+ def container_representable?(value)
123
+ case value
124
+ when ::Array then value.all? { |element| HandleWalk.representable?(element) }
125
+ when ::Hash then value.all? { |key, val| HandleWalk.representable?(key) && HandleWalk.representable?(val) }
126
+ else false
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end