kobako 0.27.0-aarch64-linux → 0.28.0-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +71 -0
  4. data/README.md +100 -125
  5. data/ROADMAP.md +23 -22
  6. data/SECURITY.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +31 -0
  8. data/crates/kobako-runtime/README.md +4 -3
  9. data/crates/kobako-transport/CHANGELOG.md +28 -0
  10. data/crates/kobako-transport/README.md +21 -12
  11. data/crates/kobako-wasmtime/CHANGELOG.md +44 -0
  12. data/crates/kobako-wasmtime/README.md +2 -2
  13. data/data/kobako.wasm +0 -0
  14. data/lib/kobako/3.3/kobako.so +0 -0
  15. data/lib/kobako/3.4/kobako.so +0 -0
  16. data/lib/kobako/4.0/kobako.so +0 -0
  17. data/lib/kobako/capture.rb +1 -4
  18. data/lib/kobako/catalog/extensions.rb +21 -59
  19. data/lib/kobako/catalog/flags.rb +41 -0
  20. data/lib/kobako/catalog/handles.rb +7 -54
  21. data/lib/kobako/catalog/services.rb +7 -61
  22. data/lib/kobako/catalog/snippets.rb +42 -66
  23. data/lib/kobako/catalog.rb +2 -10
  24. data/lib/kobako/codec/decoder.rb +9 -28
  25. data/lib/kobako/codec/encoder.rb +3 -22
  26. data/lib/kobako/codec/error.rb +6 -27
  27. data/lib/kobako/codec/ext_types.rb +72 -94
  28. data/lib/kobako/codec/handle_walk.rb +65 -130
  29. data/lib/kobako/codec/nesting.rb +3 -8
  30. data/lib/kobako/codec/state.rb +4 -10
  31. data/lib/kobako/codec/utils.rb +6 -34
  32. data/lib/kobako/codec.rb +3 -22
  33. data/lib/kobako/constant_name.rb +8 -0
  34. data/lib/kobako/context.rb +51 -112
  35. data/lib/kobako/errors.rb +51 -127
  36. data/lib/kobako/execution.rb +16 -21
  37. data/lib/kobako/extension.rb +23 -40
  38. data/lib/kobako/handle.rb +7 -12
  39. data/lib/kobako/outcome.rb +51 -80
  40. data/lib/kobako/payload/arguments.rb +1 -16
  41. data/lib/kobako/payload.rb +4 -12
  42. data/lib/kobako/pool.rb +23 -53
  43. data/lib/kobako/runtime.rb +7 -21
  44. data/lib/kobako/sandbox.rb +96 -133
  45. data/lib/kobako/sandbox_options.rb +38 -84
  46. data/lib/kobako/snippet/binary.rb +1 -16
  47. data/lib/kobako/snippet/source.rb +1 -16
  48. data/lib/kobako/snippet.rb +1 -9
  49. data/lib/kobako/transport/call.rb +1 -14
  50. data/lib/kobako/transport/dispatcher.rb +112 -195
  51. data/lib/kobako/transport/error.rb +6 -20
  52. data/lib/kobako/transport/exposure.rb +5 -10
  53. data/lib/kobako/transport/reflection.rb +39 -50
  54. data/lib/kobako/transport/run.rb +17 -71
  55. data/lib/kobako/transport/yielder.rb +21 -73
  56. data/lib/kobako/transport.rb +3 -15
  57. data/lib/kobako/unresolved.rb +4 -11
  58. data/lib/kobako/usage.rb +2 -6
  59. data/lib/kobako/version.rb +1 -1
  60. data/sig/kobako/catalog/extensions.rbs +2 -2
  61. data/sig/kobako/catalog/flags.rbs +17 -0
  62. data/sig/kobako/catalog/services.rbs +0 -6
  63. data/sig/kobako/catalog/snippets.rbs +4 -4
  64. data/sig/kobako/codec/ext_types.rbs +10 -8
  65. data/sig/kobako/codec/handle_walk.rbs +9 -7
  66. data/sig/kobako/codec/utils.rbs +2 -2
  67. data/sig/kobako/codec.rbs +2 -2
  68. data/sig/kobako/constant_name.rbs +3 -0
  69. data/sig/kobako/context.rbs +2 -3
  70. data/sig/kobako/errors.rbs +6 -0
  71. data/sig/kobako/outcome.rbs +6 -8
  72. data/sig/kobako/runtime.rbs +12 -8
  73. data/sig/kobako/sandbox.rbs +4 -0
  74. data/sig/kobako/sandbox_options.rbs +2 -6
  75. data/sig/kobako/transport/dispatcher.rbs +14 -18
  76. data/sig/kobako/transport/reflection.rbs +5 -3
  77. data/sig/kobako/transport/run.rbs +1 -3
  78. data/sig/kobako/transport/yielder.rbs +3 -3
  79. data/sig/kobako/transport.rbs +7 -6
  80. metadata +6 -3
  81. data/rust-toolchain.toml +0 -15
@@ -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
- # Kobako::Catalog::Services — per-Sandbox registry of Service
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
- # Bind +object+ as the Service reachable at +path+ — a +Symbol+ or
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 bound constant paths in bind order — +["MyService::KV",
71
- # "File"]+ — which +Runtime#eval+ / +#run+ frame into the Frame 1
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| NAME_PATTERN.match?(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
- # A path collides when it equals, is a prefix of, or extends an
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
- # Kobako::Catalog::Snippets — per-Sandbox insertion-ordered registry
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
- # The registered snippets in insertion order, each projected as the
33
- # +[kind, name, body]+ triple +Runtime#eval+ / +#run+ frame into
34
- # Frame 3 — +kind+ names the form as a Symbol, +name+ is +nil+ for
35
- # the bytecode form. The entry value objects stay pure carriers, so
36
- # this collection-tier method reads their attributes externally
37
- # rather than asking each entry to project itself.
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
- # Register one preloaded snippet in either of two forms.
43
- #
44
- # * Source form +register(code: src, name: Name)+ — +src+ is the
45
- # mruby source as a String; the bytes are re-encoded as UTF-8
46
- # and detached from the caller's reference. +name+ is a Symbol
47
- # or String matching +NAME_PATTERN+. Returns the Symbol form
48
- # of +name+.
49
- # * Binary form +register(binary: bytes)+ — +bytes+ is
50
- # precompiled RITE bytecode as a String, duplicated and forced
51
- # to ASCII-8BIT so msgpack-ruby ships it as +bin+. Returns
52
- # +nil+ — bytecode entries are anonymous on the host side; any
53
- # structural validation is deferred to the guest at first replay.
54
- #
55
- # The two forms are mutually exclusive: shape validation lives
56
- # here so callers (chiefly +Kobako::Sandbox#preload+) collapse to
57
- # a single delegation. Raises +ArgumentError+ on mixed forms,
58
- # missing keywords, wrong types, malformed +name+, or
59
- # duplicate +code:+ +name+.
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
- # Source-form register path. Delegates argument-shape checks to
73
- # +ensure_source_args!+ (which returns the narrowed +[code, name]+
74
- # pair), normalises +name+ to a Symbol, rejects duplicates,
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: code.dup.force_encoding(Encoding::UTF_8))
84
- name_sym
64
+ @entries << Snippet::Source.new(name: name_sym, body: body)
65
+ nil
85
66
  end
86
67
 
87
- # Shape-only validation for the +code:+ + +name:+ pair. Returns
88
- # the pair with +nil+ narrowed away so callers can treat both as
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
- # Binary-form register path. Validates the +binary:+ payload type
101
- # and appends the Binary entry. The bytes are duplicated and forced
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
- # Project one entry as its +[kind, name, body]+ triple. Source
111
- # entries contribute their host-side +name+; Binary entries carry
112
- # +nil+ because the canonical name lives in the bytecode's embedded
113
- # +debug_info+ and is read by the guest at load time.
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
- [Snippet::Source::KIND, entry.name.to_s, entry.body]
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 NAME_PATTERN.match?(name_str)
105
+ unless CONSTANT_NAME.match?(name_str)
130
106
  raise ArgumentError,
131
- "snippet name must match #{NAME_PATTERN.inspect} (got #{name.inspect})"
107
+ "snippet name must match #{CONSTANT_NAME.inspect} (got #{name.inspect})"
132
108
  end
133
109
 
134
110
  name_str.to_sym
@@ -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
- # Kobako::Catalog — Sandbox-level configuration and per-invocation
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
@@ -8,36 +8,17 @@ require_relative "utils"
8
8
 
9
9
  module Kobako
10
10
  module Codec
11
- # Module-level entry point for the host side of the kobako wire
12
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
13
- #
14
- # Translates msgpack gem exceptions into the kobako error taxonomy
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
- # comes from our ext-type validators (Handle id range, Exception type
39
- # whitelist) and from a yielded block's Value Object invariants — both
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
- # SPEC pins +str+ family payloads to UTF-8
52
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § str/bin
53
- # Encoding Rules). The msgpack gem returns UTF-8-tagged Strings for
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!.
@@ -7,28 +7,9 @@ require_relative "ext_types"
7
7
 
8
8
  module Kobako
9
9
  module Codec
10
- # Module-level entry point for the host side of the kobako wire
11
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping).
12
- #
13
- # The codec backbone is the official +msgpack+ gem: integers, floats,
14
- # strings, arrays, and maps go through the gem's narrowest-encoding
15
- # logic; the two kobako-specific ext types (0x00 Symbol, 0x01
16
- # Capability Handle) are registered by ExtTypes on the process-wide
17
- # factory.
18
- #
19
- # Public API is a single function — +.encode+. The codec is stateless;
20
- # there is no buffer accumulator and no streaming write API. Callers
21
- # that need to concatenate multiple encodings build the bytes
22
- # themselves.
23
- module Encoder
24
- # Encode +value+ to wire bytes (binary-encoded String).
25
- # SPEC's 11-entry type mapping is a closed set: a value outside it is
26
- # rejected as +UnsupportedTypeError+ by the factory's +BasicObject+ guard
27
- # (ExtTypes#register_unrepresentable), which raises before the msgpack
28
- # gem can route the value through +to_msgpack+ — so a permissive
29
- # +method_missing+ object cannot answer that probe and mis-encode. The
30
- # rescue below maps the two violations the guard does not reach onto the
31
- # same error: an integer outside i64..u64 (+RangeError+) and any
10
+ module Encoder # :nodoc:
11
+ # The rescue maps the two violations the factory's +BasicObject+ guard
12
+ # does not reach: an integer outside i64..u64 (+RangeError+) and any
32
13
  # packer-internal +NoMethodError+.
33
14
  #
34
15
  # The caller bounds +value+'s nesting first (Nesting): the packer takes
@@ -2,34 +2,13 @@
2
2
 
3
3
  module Kobako
4
4
  module Codec
5
- # Base class for all wire-codec faults raised by the pure-Ruby host codec.
6
- #
7
- # The wire codec implements the binary contract pinned in
8
- # {docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Type Mapping.
9
- # Every wire violation surfaces as a
10
- # subclass of Error so callers can pattern-match on the specific
11
- # fault while still rescuing all codec faults via this base class.
12
- #
13
- # Higher layers (e.g. the Sandbox dispatch loop) translate these into
14
- # the public Kobako::SandboxError / Kobako::TrapError taxonomy.
15
- class Error < StandardError; end
5
+ class Error < StandardError; end # :nodoc:
6
+ class TruncatedInputError < Error; end # :nodoc:
16
7
 
17
- # Input ended before the type prefix or payload was fully consumed.
18
- class TruncatedInputError < Error; end
8
+ # Also raised on encode for a value nested past what the packer can walk.
9
+ class InvalidTypeError < Error; end # :nodoc:
19
10
 
20
- # The type byte at the current position is not in the 11-entry kobako
21
- # type mapping (e.g. an unknown ext code, or a reserved msgpack tag) —
22
- # or, on encode, a value nesting past what the packer can walk.
23
- class InvalidTypeError < Error; end
24
-
25
- # A msgpack +str+ payload was not valid UTF-8, or an ext 0x00 Symbol
26
- # payload was not valid UTF-8 — both are wire violations per SPEC.
27
- class InvalidEncodingError < Error; end
28
-
29
- # The encoder was handed a Ruby object whose type has no wire
30
- # representation (e.g. Range, Time). Higher layers may catch this
31
- # and re-route the value through Handle allocation, but at the
32
- # codec level it is a hard error.
33
- class UnsupportedTypeError < Error; end
11
+ class InvalidEncodingError < Error; end # :nodoc:
12
+ class UnsupportedTypeError < Error; end # :nodoc:
34
13
  end
35
14
  end
@@ -8,21 +8,16 @@ require_relative "state"
8
8
  require_relative "../handle"
9
9
 
10
10
  module Kobako
11
- module Codec
12
- # The kobako wire ext-type conversions
13
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types)
14
- # as pure functions: per-operation decode state is threaded in as an
15
- # argument, so the module itself holds nothing. #build_factory assembles
16
- # the one +MessagePack::Factory+ these conversions are registered on.
17
- module ExtTypes
11
+ module Codec # :nodoc:
12
+ module ExtTypes # :nodoc:
18
13
  # MessagePack ext type code reserved for Symbol
19
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types
20
- # → ext 0x00). Module-private — mirrors +codec::EXT_SYMBOL+ on the
14
+ # (docs/wire/payload-msgpack.md § ext 0x00 — Symbol). Module-private —
15
+ # mirrors +codec::EXT_SYMBOL+ on the
21
16
  # Rust side.
22
17
  EXT_SYMBOL = 0x00
23
18
  # MessagePack ext type code reserved for Capability Handle
24
- # ({docs/wire/payload-msgpack.md}[link:../../../docs/wire/payload-msgpack.md] § Ext Types
25
- # → ext 0x01). Module-private — mirrors +codec::EXT_HANDLE+ on the
19
+ # (docs/wire/payload-msgpack.md § ext 0x01 — Handle). Module-private —
20
+ # mirrors +codec::EXT_HANDLE+ on the
26
21
  # Rust side.
27
22
  EXT_HANDLE = 0x01
28
23
  private_constant :EXT_SYMBOL, :EXT_HANDLE
@@ -31,19 +26,13 @@ module Kobako
31
26
  # never emitted (the guard's packer always raises) and never decoded
32
27
  # (no unpacker is registered, so the id stays an UnknownExtTypeError on
33
28
  # the wire), so it is not a wire ext type: deliberately not named
34
- # +EXT_*+ like the two real ext codes, since it has no Rust-side mirror
35
- # and must stay outside the wire-symmetry inventory.
29
+ # +EXT_*+ like the two real ext codes, since it has no Rust-side mirror.
36
30
  UNREPRESENTABLE_GUARD_ID = 0x7F
37
31
  private_constant :UNREPRESENTABLE_GUARD_ID
38
32
 
39
- module_function
40
-
41
- # Assemble a +MessagePack::Factory+ with the two kobako ext types plus
42
- # the unrepresentable-value guard registered, frozen because
43
- # registration is its only mutation and happens exactly once. The
44
- # stateful conversions resolve their per-operation state at call time,
45
- # so one registered factory serves every thread.
46
- def build_factory
33
+ # The stateful conversions resolve their per-operation state at call
34
+ # time, so one frozen factory serves every thread.
35
+ def self.build_factory
47
36
  factory = MessagePack::Factory.new
48
37
  register_symbol(factory)
49
38
  register_handle(factory)
@@ -51,89 +40,78 @@ module Kobako
51
40
  factory.freeze
52
41
  end
53
42
 
54
- # Symbol-to-name packer for the ext-0x00 registration.
55
- def pack_symbol(symbol)
56
- symbol.name
57
- end
43
+ class << self
44
+ private
58
45
 
59
- # Validate the ext-0x00 payload as UTF-8 and intern. Raises
60
- # InvalidEncodingError on invalid bytes — SPEC forbids the
61
- # binary-encoding fallback that msgpack-gem's default unpacker
62
- # would otherwise apply. The re-tag step lives here because the
63
- # msgpack ext-type unpacker hands us binary bytes; the assertion
64
- # itself is shared with Decoder via Utils.assert_utf8!. The
65
- # +"Symbol"+ label keeps the error message in Ruby vocabulary
66
- # rather than wire-ext-code vocabulary.
67
- def unpack_symbol(payload)
68
- name = payload.b.force_encoding(Encoding::UTF_8)
69
- Utils.assert_utf8!(name, "Symbol payload")
70
- name.to_sym
71
- end
46
+ def pack_symbol(symbol)
47
+ symbol.name
48
+ end
72
49
 
73
- # Handle-id packer for the ext-0x01 registration: the fixext-4
74
- # big-endian id frame.
75
- def pack_handle(handle)
76
- [handle.id].pack("N")
77
- end
50
+ # Refuses the binary-encoding fallback that msgpack-gem's default
51
+ # unpacker would otherwise apply to invalid bytes.
52
+ def unpack_symbol(payload)
53
+ name = payload.b.force_encoding(Encoding::UTF_8)
54
+ Utils.assert_utf8!(name, "Symbol payload")
55
+ name.to_sym
56
+ end
78
57
 
79
- # Peel off the fixext-4 frame, hand the bytes to the
80
- # Host-Gem-internal +Kobako::Handle.restore+ factory, and
81
- # translate the +ArgumentError+ raised by Handle's invariants
82
- # into a wire-layer +InvalidTypeError+ via Codec::Utils.with_boundary.
83
- # The Value Object owns the id-range contract; this method only
84
- # owns the frame shape. Records the Handle sighting on +state+ so a
85
- # Handle-free decode can skip the downstream resolution walk.
86
- def unpack_handle(payload, state)
87
- state.record_handle!
88
- bytes = payload.b
89
- raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
58
+ def pack_handle(handle)
59
+ [handle.id].pack("N")
60
+ end
90
61
 
91
- id = bytes.unpack1("N") # : Integer
92
- Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
93
- end
62
+ # Handle owns the id-range contract; this method owns only the frame
63
+ # shape. The sighting is recorded so a Handle-free decode can skip the
64
+ # downstream resolution walk.
65
+ def unpack_handle(payload, state)
66
+ state.record_handle!
67
+ bytes = payload.b
68
+ raise InvalidTypeError, "Handle payload must be 4 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 4
94
69
 
95
- def register_symbol(factory)
96
- factory.register_type(
97
- EXT_SYMBOL, Symbol,
98
- packer: ->(symbol) { ExtTypes.pack_symbol(symbol) },
99
- unpacker: ->(payload) { ExtTypes.unpack_symbol(payload) }
100
- )
101
- end
70
+ id = bytes.unpack1("N") # : Integer
71
+ Codec::Utils.with_boundary { Kobako::Handle.restore(id) }
72
+ end
102
73
 
103
- def register_handle(factory)
104
- factory.register_type(
105
- EXT_HANDLE, Kobako::Handle,
106
- packer: ->(handle) { ExtTypes.pack_handle(handle) },
107
- unpacker: ->(payload) { ExtTypes.unpack_handle(payload, State.current) }
108
- )
109
- end
74
+ def register_symbol(factory)
75
+ factory.register_type(
76
+ EXT_SYMBOL, Symbol,
77
+ packer: ->(symbol) { pack_symbol(symbol) },
78
+ unpacker: ->(payload) { unpack_symbol(payload) }
79
+ )
80
+ end
81
+
82
+ def register_handle(factory)
83
+ factory.register_type(
84
+ EXT_HANDLE, Kobako::Handle,
85
+ packer: ->(handle) { pack_handle(handle) },
86
+ unpacker: ->(payload) { unpack_handle(payload, State.current) }
87
+ )
88
+ end
110
89
 
111
- # A catch-all packer that rejects any value with no wire representation
112
- # as +UnsupportedTypeError+. Registered on +BasicObject+ so it also covers
113
- # BasicObject-based proxies; the narrower Symbol / Handle
114
- # registrations still win by most-specific match, and native types never
115
- # reach it. Packer-only: the guard never writes bytes, so its id is inert
116
- # and the decode surface stays fail-closed.
117
- #
118
- # This makes the host's non-wire detection a positive allowlist — a value
119
- # outside the type set is rejected here rather than routed to +to_msgpack+
120
- # — matching the guest's classname allowlist and the Rust codec's closed
121
- # +Value+ enum. Without it, a value with a permissive +method_missing+
122
- # answers the codec's +to_msgpack+ probe and mis-encodes as +nil+ instead
123
- # of crossing as a Capability Handle.
124
- def register_unrepresentable(factory)
125
- factory.register_type(
126
- UNREPRESENTABLE_GUARD_ID, BasicObject,
127
- packer: ->(_value) { raise UnsupportedTypeError, "value has no wire representation" }
128
- )
90
+ # A catch-all packer that rejects any value with no wire representation
91
+ # as +UnsupportedTypeError+. Registered on +BasicObject+ so it also covers
92
+ # BasicObject-based proxies; the narrower Symbol / Handle
93
+ # registrations still win by most-specific match, and native types never
94
+ # reach it. Packer-only: the guard never writes bytes, so its id is inert
95
+ # and the decode surface stays fail-closed.
96
+ #
97
+ # This makes the host's non-wire detection a positive allowlist — a value
98
+ # outside the type set is rejected here rather than routed to +to_msgpack+
99
+ # — matching the guest's classname allowlist and the Rust codec's closed
100
+ # +Value+ enum. Without it, a value with a permissive +method_missing+
101
+ # answers the codec's +to_msgpack+ probe and mis-encodes as +nil+ instead
102
+ # of crossing as a Capability Handle.
103
+ def register_unrepresentable(factory)
104
+ factory.register_type(
105
+ UNREPRESENTABLE_GUARD_ID, BasicObject,
106
+ packer: ->(_value) { raise UnsupportedTypeError, "value has no wire representation" }
107
+ )
108
+ end
129
109
  end
130
110
  end
131
111
 
132
- # The process-wide registered factory: ext registration is paid once at
133
- # load, and a registered +MessagePack::Factory+ only reads its type
134
- # registry afterwards, so every thread shares this instance for byte
135
- # work.
136
- FACTORY = ExtTypes.build_factory
112
+ # Registered once at load and only read afterwards, so every thread
113
+ # shares it.
114
+ FACTORY = ExtTypes.build_factory # :nodoc:
137
115
  private_constant :FACTORY
138
116
  end
139
117
  end