kobako 0.27.0 → 0.29.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 +241 -0
- data/Cargo.lock +132 -126
- data/README.md +240 -147
- data/ROADMAP.md +28 -22
- data/SECURITY.md +2 -2
- data/crates/kobako-runtime/CHANGELOG.md +117 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +16 -10
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +12 -2
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +43 -17
- data/crates/kobako-runtime/src/snapshot.rs +33 -15
- data/crates/kobako-runtime/src/yielder.rs +14 -6
- data/crates/kobako-transport/CHANGELOG.md +45 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +21 -12
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +103 -61
- data/crates/kobako-transport/src/envelope/request.rs +305 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
- data/crates/kobako-wasmtime/Cargo.toml +23 -16
- data/crates/kobako-wasmtime/README.md +6 -4
- data/crates/kobako-wasmtime/src/abi.rs +67 -36
- data/crates/kobako-wasmtime/src/ambient.rs +64 -5
- data/crates/kobako-wasmtime/src/cache.rs +204 -127
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +69 -15
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
- data/crates/kobako-wasmtime/src/driver.rs +174 -125
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
- data/crates/kobako-wasmtime/src/invocation.rs +84 -412
- data/crates/kobako-wasmtime/src/io.rs +89 -0
- data/crates/kobako-wasmtime/src/lib.rs +7 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +66 -59
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +177 -113
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +11 -13
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +190 -312
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +29 -0
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +17 -60
- data/lib/kobako/catalog/snippets.rb +41 -69
- data/lib/kobako/catalog.rb +13 -10
- data/lib/kobako/codec/decoder.rb +10 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +21 -11
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +10 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +83 -117
- data/lib/kobako/errors.rb +57 -132
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +25 -55
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +128 -137
- data/lib/kobako/sandbox_options.rb +71 -90
- data/lib/kobako/snippet/bytecode.rb +11 -0
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +2 -10
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +127 -206
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +10 -17
- data/lib/kobako/transport/reflection.rb +108 -63
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +20 -94
- data/lib/kobako/transport.rb +4 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +13 -0
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +3 -5
- data/sig/kobako/catalog/snippets.rbs +5 -5
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +4 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +15 -8
- data/sig/kobako/errors.rbs +10 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -45
- data/sig/kobako/sandbox.rbs +11 -1
- data/sig/kobako/sandbox_options.rbs +25 -7
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +15 -19
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +19 -4
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +5 -9
- data/sig/kobako/transport.rbs +9 -8
- metadata +75 -10
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/lib/kobako/snippet/binary.rb +0 -26
- data/rust-toolchain.toml +0 -15
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
//! A Mount: the Grant handing a Filesystem to the guest at an absolute
|
|
2
|
+
//! guest path, with the access it allows there. The Mount decides each
|
|
3
|
+
//! open and each change itself, so a runtime asks it rather than reading
|
|
4
|
+
//! its access, and a Filesystem of a third party's checks nothing.
|
|
5
|
+
|
|
6
|
+
use std::fmt;
|
|
7
|
+
use std::sync::Arc;
|
|
8
|
+
|
|
9
|
+
use crate::filesystem::{Access, Filesystem};
|
|
10
|
+
|
|
11
|
+
/// The Host App's decision for one path a guest opens or changes, made in
|
|
12
|
+
/// a Mount's access's place.
|
|
13
|
+
pub trait Policy: Send + Sync {
|
|
14
|
+
/// Whether the guest may reach `path`, the absolute guest path, with
|
|
15
|
+
/// `access`.
|
|
16
|
+
fn allows(&self, path: &str, access: Access) -> bool;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Why a Mount refused a call.
|
|
20
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
21
|
+
pub enum Denial {
|
|
22
|
+
/// A change beneath a Mount allowing only reads.
|
|
23
|
+
ReadOnly,
|
|
24
|
+
/// The Mount's Policy said no.
|
|
25
|
+
Refused,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// A Filesystem at an absolute guest path. Its path bounds what a Policy
|
|
29
|
+
/// can allow either way.
|
|
30
|
+
#[derive(Clone)]
|
|
31
|
+
pub struct Mount {
|
|
32
|
+
at: String,
|
|
33
|
+
access: Access,
|
|
34
|
+
filesystem: Arc<dyn Filesystem>,
|
|
35
|
+
policy: Option<Arc<dyn Policy>>,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// A path no Mount can stand at: one that is not an absolute guest path.
|
|
39
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
40
|
+
pub struct MountPathError(String);
|
|
41
|
+
|
|
42
|
+
impl fmt::Display for MountPathError {
|
|
43
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
44
|
+
write!(
|
|
45
|
+
f,
|
|
46
|
+
"mount path must be an absolute guest path, got {:?}",
|
|
47
|
+
self.0
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl std::error::Error for MountPathError {}
|
|
53
|
+
|
|
54
|
+
/// An absolute guest path has no empty, `.`, or `..` component; the root
|
|
55
|
+
/// is `/` alone.
|
|
56
|
+
fn is_guest_path(at: &str) -> bool {
|
|
57
|
+
at == "/"
|
|
58
|
+
|| at.strip_prefix('/').is_some_and(|rest| {
|
|
59
|
+
rest.split('/')
|
|
60
|
+
.all(|part| !part.is_empty() && part != "." && part != "..")
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
impl Mount {
|
|
65
|
+
/// A Mount of `filesystem` at `at`, refused unless `at` is an absolute
|
|
66
|
+
/// guest path, which every path the Mount answers for is built on.
|
|
67
|
+
pub fn new(
|
|
68
|
+
at: impl Into<String>,
|
|
69
|
+
filesystem: Arc<dyn Filesystem>,
|
|
70
|
+
access: Access,
|
|
71
|
+
) -> Result<Self, MountPathError> {
|
|
72
|
+
let at = at.into();
|
|
73
|
+
if !is_guest_path(&at) {
|
|
74
|
+
return Err(MountPathError(at));
|
|
75
|
+
}
|
|
76
|
+
Ok(Self {
|
|
77
|
+
at,
|
|
78
|
+
access,
|
|
79
|
+
filesystem,
|
|
80
|
+
policy: None,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// This Mount with `policy` deciding each open and each change in its
|
|
85
|
+
/// access's place.
|
|
86
|
+
pub fn with_policy(self, policy: Arc<dyn Policy>) -> Self {
|
|
87
|
+
Self {
|
|
88
|
+
policy: Some(policy),
|
|
89
|
+
..self
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// The absolute guest path the Filesystem appears at.
|
|
94
|
+
pub fn at(&self) -> &str {
|
|
95
|
+
&self.at
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The Filesystem an engine hands each call beneath this Mount.
|
|
99
|
+
pub fn filesystem(&self) -> &Arc<dyn Filesystem> {
|
|
100
|
+
&self.filesystem
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Whether a change beneath this Mount may be allowed at all: it was made
|
|
104
|
+
/// to be written, or a Policy decides each change in its access's place.
|
|
105
|
+
/// The Policy itself is not asked.
|
|
106
|
+
pub fn allows_writes(&self) -> bool {
|
|
107
|
+
self.policy.is_some() || self.access == Access::ReadWrite
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// The absolute guest path `path`, relative to this Mount's root,
|
|
111
|
+
/// stands for.
|
|
112
|
+
pub fn guest_path(&self, path: &str) -> String {
|
|
113
|
+
match (self.at.as_str(), path) {
|
|
114
|
+
(at, "") => at.to_string(),
|
|
115
|
+
("/", path) => format!("/{path}"),
|
|
116
|
+
(at, path) => format!("{at}/{path}"),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Whether the guest may reach `path`, relative to this Mount's root,
|
|
121
|
+
/// with `access`. A Policy judges the path as the Filesystem stores
|
|
122
|
+
/// its names, so a host folding case or Unicode form cannot hand it
|
|
123
|
+
/// one spelling of an entry it refuses by another; a Filesystem that
|
|
124
|
+
/// cannot spell it refuses.
|
|
125
|
+
pub fn check(&self, path: &str, access: Access) -> Result<(), Denial> {
|
|
126
|
+
match &self.policy {
|
|
127
|
+
Some(policy) => {
|
|
128
|
+
let stored = self
|
|
129
|
+
.filesystem
|
|
130
|
+
.stored_path(path)
|
|
131
|
+
.map_err(|_| Denial::Refused)?;
|
|
132
|
+
if policy.allows(&self.guest_path(&stored), access) {
|
|
133
|
+
Ok(())
|
|
134
|
+
} else {
|
|
135
|
+
Err(Denial::Refused)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
None if access == Access::ReadWrite && self.access == Access::Read => {
|
|
139
|
+
Err(Denial::ReadOnly)
|
|
140
|
+
}
|
|
141
|
+
None => Ok(()),
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Whether a directory may move beneath this Mount. Under a Policy it
|
|
146
|
+
/// may not: everything beneath it would take paths the Policy never
|
|
147
|
+
/// judged, so a move is refused as one across devices and the guest
|
|
148
|
+
/// copies entry by entry instead, as Landlock refuses such a move.
|
|
149
|
+
pub fn moves_directories(&self) -> bool {
|
|
150
|
+
self.policy.is_none()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Whether a path beneath this Mount may pass through a link the
|
|
154
|
+
/// Filesystem holds. Under a Policy it may not: the link leads to a
|
|
155
|
+
/// path the Policy never judged, so the call is refused as one past
|
|
156
|
+
/// too many links.
|
|
157
|
+
pub fn follows_links(&self) -> bool {
|
|
158
|
+
self.policy.is_none()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// This Mount as a new Invocation starts from it, over the Filesystem
|
|
162
|
+
/// that Invocation receives; beneath a Policy, the one that follows no
|
|
163
|
+
/// link, so each call reaches the path the Policy judged.
|
|
164
|
+
pub fn for_invocation(&self) -> Self {
|
|
165
|
+
let filesystem = self
|
|
166
|
+
.filesystem
|
|
167
|
+
.for_invocation()
|
|
168
|
+
.unwrap_or_else(|| self.filesystem.clone());
|
|
169
|
+
let filesystem = match self.policy {
|
|
170
|
+
Some(_) => filesystem.for_policy().unwrap_or(filesystem),
|
|
171
|
+
None => filesystem,
|
|
172
|
+
};
|
|
173
|
+
Self {
|
|
174
|
+
filesystem,
|
|
175
|
+
..self.clone()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
/// The ordered isolation ladder a runtime builds one rung of.
|
|
10
10
|
///
|
|
11
11
|
/// `Hermetic` is the full ambient-denial posture: ambient time and
|
|
12
|
-
/// entropy denied at the WASI boundary, no filesystem /
|
|
13
|
-
/// network reachability, and no host import beyond the wire ABI's
|
|
14
|
-
/// `
|
|
15
|
-
/// live
|
|
12
|
+
/// entropy denied at the WASI boundary, no ambient filesystem /
|
|
13
|
+
/// environment / network reachability, and no host import beyond the wire ABI's
|
|
14
|
+
/// `__kobako_upcall`. `Permissive` differs only in the ambient
|
|
15
|
+
/// authority it grants — live time and entropy at the WASI boundary. Ordering
|
|
16
16
|
/// follows strength (`Permissive < Hermetic`), so a floor check is a
|
|
17
17
|
/// plain comparison.
|
|
18
18
|
///
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//! Engine-neutral host runtime contract.
|
|
2
2
|
//!
|
|
3
3
|
//! The contract a wasm engine must satisfy to drive a kobako guest: take a
|
|
4
|
-
//! per-invocation entry plus its
|
|
4
|
+
//! per-invocation entry plus its Preamble, run one invocation on a
|
|
5
5
|
//! fresh instance, and return the observable `Snapshot` — `Ok` iff the
|
|
6
6
|
//! guest export ran, `Err` when the invocation never started. Nothing here
|
|
7
7
|
//! mentions `magnus` or a Ruby type — a frontend supplies the dispatch
|
|
@@ -9,29 +9,32 @@
|
|
|
9
9
|
|
|
10
10
|
use std::sync::Arc;
|
|
11
11
|
|
|
12
|
+
use kobako_transport::envelope::Preamble;
|
|
13
|
+
|
|
12
14
|
use crate::dispatch::DispatchHandler;
|
|
13
15
|
use crate::error::InvokeError;
|
|
16
|
+
use crate::grant::{Grant, GrantKind};
|
|
14
17
|
use crate::profile::Profile;
|
|
15
18
|
use crate::snapshot::Snapshot;
|
|
16
19
|
|
|
17
20
|
/// The per-invocation entry: a one-shot mruby source (`Eval`) or an
|
|
18
|
-
/// entrypoint
|
|
19
|
-
/// `
|
|
21
|
+
/// entrypoint and its payload (`Run`). Both ride alongside the Sandbox's
|
|
22
|
+
/// `Preamble`; the runtime frames `Run` and the `Preamble` onto the wire,
|
|
23
|
+
/// so a frontend never encodes either. The payload is owned so that framing it needs no
|
|
24
|
+
/// second copy.
|
|
20
25
|
///
|
|
21
|
-
/// Closed rather than `#[non_exhaustive]`:
|
|
22
|
-
///
|
|
23
|
-
///
|
|
24
|
-
///
|
|
26
|
+
/// Closed rather than `#[non_exhaustive]`: each arm is an invocation the
|
|
27
|
+
/// downcall carries under its own tag, and a runtime that has not
|
|
28
|
+
/// learned a new one should fail to compile rather than fall into a
|
|
29
|
+
/// wildcard.
|
|
25
30
|
pub enum Entry<'a> {
|
|
26
|
-
Eval {
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
pub preamble: &'a [u8],
|
|
34
|
-
pub snippets: &'a [u8],
|
|
31
|
+
Eval {
|
|
32
|
+
source: &'a [u8],
|
|
33
|
+
},
|
|
34
|
+
Run {
|
|
35
|
+
entrypoint: &'a str,
|
|
36
|
+
payload: Vec<u8>,
|
|
37
|
+
},
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/// Engine-neutral runtime: drives one guest invocation on a fresh instance
|
|
@@ -47,16 +50,39 @@ pub struct Frames<'a> {
|
|
|
47
50
|
/// wrapper and GC-marking it; the runtime itself touches no frontend
|
|
48
51
|
/// value.
|
|
49
52
|
pub trait Runtime {
|
|
53
|
+
/// `grants` are this Invocation's, each of a kind `supports` declared;
|
|
54
|
+
/// the runtime presents them to the guest and keeps none afterwards.
|
|
50
55
|
fn invoke(
|
|
51
56
|
&self,
|
|
52
57
|
entry: Entry<'_>,
|
|
53
|
-
|
|
58
|
+
preamble: &Preamble,
|
|
54
59
|
handler: Option<Arc<dyn DispatchHandler>>,
|
|
60
|
+
grants: &[Grant],
|
|
55
61
|
) -> Result<Snapshot, InvokeError>;
|
|
56
62
|
|
|
63
|
+
/// Whether this runtime presents a Grant of `kind` to the guest. A
|
|
64
|
+
/// frontend refuses to register one it does not, before any guest runs.
|
|
65
|
+
/// The default supports none, so an engine that has not learned Grants
|
|
66
|
+
/// still serves every invocation without one.
|
|
67
|
+
fn supports(&self, _kind: GrantKind) -> bool {
|
|
68
|
+
false
|
|
69
|
+
}
|
|
70
|
+
|
|
57
71
|
/// The isolation profile this runtime provides. Deliberately
|
|
58
72
|
/// without a default: every engine states its posture explicitly,
|
|
59
73
|
/// and a frontend refuses construction when the declaration falls
|
|
60
74
|
/// below the floor its host application requested.
|
|
61
75
|
fn profile(&self) -> Profile;
|
|
76
|
+
|
|
77
|
+
/// The bytecode of the Preamble's source Snippets, one per snippet in
|
|
78
|
+
/// order, compiled on a fresh instance that runs none of them. A
|
|
79
|
+
/// frontend hands over the Preamble an invocation sends and replays the
|
|
80
|
+
/// answer in its place, so every invocation after skips the compiler.
|
|
81
|
+
/// `None` whenever there is nothing to replay, an answer that would
|
|
82
|
+
/// carry that Preamble past the payload cap included: the frontend
|
|
83
|
+
/// keeps the source. The default compiles nothing, so an engine that
|
|
84
|
+
/// does not offer it still serves every invocation.
|
|
85
|
+
fn compile(&self, _preamble: &Preamble) -> Option<Vec<Vec<u8>>> {
|
|
86
|
+
None
|
|
87
|
+
}
|
|
62
88
|
}
|
|
@@ -4,44 +4,49 @@
|
|
|
4
4
|
//! frontend type and uniform across success and trap: how the invocation
|
|
5
5
|
//! completed, the two captured output channels, and the resource usage.
|
|
6
6
|
//! A `Snapshot` exists iff the guest export ran — a failure to even start
|
|
7
|
-
//! the invocation
|
|
7
|
+
//! the invocation comes back as the `Err` of `invoke` instead.
|
|
8
8
|
//!
|
|
9
9
|
//! `Usage` and `Capture` are shared with the tier above rather than
|
|
10
10
|
//! translated at its boundary: what they carry is already frontend-free,
|
|
11
11
|
//! so the Rust host SDK re-exports these types themselves.
|
|
12
12
|
|
|
13
13
|
use crate::error::Trap;
|
|
14
|
+
use kobako_transport::envelope::Outcome;
|
|
14
15
|
|
|
15
16
|
/// One captured output channel: the bytes the guest wrote (already clipped
|
|
16
17
|
/// to the channel's cap) and whether the cap was reached. `Default` is the
|
|
17
18
|
/// pre-invocation sentinel — no bytes, cap not reached — shared by every
|
|
18
19
|
/// frontend's before-first-invocation readout.
|
|
19
20
|
#[derive(Default)]
|
|
21
|
+
#[non_exhaustive]
|
|
20
22
|
pub struct Capture {
|
|
21
23
|
pub bytes: Vec<u8>,
|
|
22
24
|
pub truncated: bool,
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
/// How the guest invocation completed: `Outcome`
|
|
26
|
-
///
|
|
27
|
-
///
|
|
28
|
-
/// survives it.
|
|
27
|
+
/// How the guest invocation completed: `Outcome` is the result the guest
|
|
28
|
+
/// left, already framed; `Trap` is an engine fault after the export call
|
|
29
|
+
/// started, kept as a value so the rest of the `Snapshot` survives it.
|
|
29
30
|
///
|
|
30
|
-
///
|
|
31
|
-
///
|
|
32
|
-
///
|
|
33
|
-
///
|
|
31
|
+
/// A guest that returned without an Outcome the envelope can frame —
|
|
32
|
+
/// nothing written included — ends as a `Trap`: with no record to read,
|
|
33
|
+
/// there is nothing a frontend could attribute.
|
|
34
|
+
///
|
|
35
|
+
/// Closed rather than `#[non_exhaustive]`: an invocation either returned
|
|
36
|
+
/// a result or failed to, and there is no third way for one to end. A
|
|
37
|
+
/// wildcard would only hide a frontend that had not been told otherwise.
|
|
34
38
|
pub enum Completion {
|
|
35
|
-
Outcome(
|
|
39
|
+
Outcome(Outcome),
|
|
36
40
|
Trap(Trap),
|
|
37
41
|
}
|
|
38
42
|
|
|
39
|
-
/// Resource usage of one guest invocation, measured across the
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
-
/// `memory.grow` delta in bytes past the entry-time
|
|
43
|
-
/// (all zeros) is the pre-invocation sentinel.
|
|
43
|
+
/// Resource usage of one guest invocation, each measured across the span
|
|
44
|
+
/// its cap governs: `wall_time` is the seconds from setting the guest up to
|
|
45
|
+
/// its export call returning, the span the deadline covers; `memory_peak`
|
|
46
|
+
/// is the high-water `memory.grow` delta in bytes past the entry-time
|
|
47
|
+
/// baseline. `Default` (all zeros) is the pre-invocation sentinel.
|
|
44
48
|
#[derive(Clone, Copy, Default)]
|
|
49
|
+
#[non_exhaustive]
|
|
45
50
|
pub struct Usage {
|
|
46
51
|
pub wall_time: f64,
|
|
47
52
|
pub memory_peak: usize,
|
|
@@ -51,9 +56,22 @@ pub struct Usage {
|
|
|
51
56
|
/// kinds: captures and usage are collected on trap and outcome alike.
|
|
52
57
|
/// What a frontend exposes from the trap path is its own contract
|
|
53
58
|
/// decision.
|
|
59
|
+
#[non_exhaustive]
|
|
54
60
|
pub struct Snapshot {
|
|
55
61
|
pub completion: Completion,
|
|
56
62
|
pub stdout: Capture,
|
|
57
63
|
pub stderr: Capture,
|
|
58
64
|
pub usage: Usage,
|
|
59
65
|
}
|
|
66
|
+
|
|
67
|
+
impl Snapshot {
|
|
68
|
+
/// What an engine hands back from one invocation.
|
|
69
|
+
pub fn new(completion: Completion, stdout: Capture, stderr: Capture, usage: Usage) -> Self {
|
|
70
|
+
Self {
|
|
71
|
+
completion,
|
|
72
|
+
stdout,
|
|
73
|
+
stderr,
|
|
74
|
+
usage,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -4,19 +4,27 @@
|
|
|
4
4
|
//! During a guest→host dispatch, a Service method may yield to a guest
|
|
5
5
|
//! block. The host drives that re-entry through a `Yielder`: it ships the
|
|
6
6
|
//! yield-arguments payload into the in-flight guest and returns the
|
|
7
|
-
//! Yield Reply
|
|
7
|
+
//! decoded Yield Reply. What backs the re-entry — a wasmtime `Caller`, some
|
|
8
8
|
//! other engine handle — is the implementer's concern; the dispatch
|
|
9
9
|
//! contract sees only this trait.
|
|
10
10
|
|
|
11
|
+
use kobako_transport::envelope::YieldReply;
|
|
12
|
+
|
|
11
13
|
use crate::error::Trap;
|
|
12
14
|
|
|
13
15
|
/// Host-initiated re-entry into the in-flight guest instance to run a
|
|
14
16
|
/// yielded block.
|
|
15
17
|
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
18
|
-
///
|
|
19
|
-
///
|
|
18
|
+
/// Each open Call has a `frame`, counted from `0` at the outermost.
|
|
19
|
+
/// `yield_to_block` ships `args` as a `yield` downcall to the block of the
|
|
20
|
+
/// open Call `frame` names, and returns the decoded Yield Reply,
|
|
21
|
+
/// `Unsupported` arm included, or a `Trap` — surfaced through the
|
|
22
|
+
/// frontend's trap-error mapping — when the re-entry traps, the guest
|
|
23
|
+
/// returns an empty or unframeable result, or a payload exceeds the
|
|
24
|
+
/// 16 MiB cap.
|
|
20
25
|
pub trait Yielder {
|
|
21
|
-
|
|
26
|
+
/// The frame of the Call this Yielder was lent for.
|
|
27
|
+
fn frame(&self) -> u32;
|
|
28
|
+
|
|
29
|
+
fn yield_to_block(&mut self, frame: u32, args: &[u8]) -> Result<YieldReply, Trap>;
|
|
22
30
|
}
|
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.19.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.18.0...kobako-transport-v0.19.0) (2026-10-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* in kobako-transport, YieldReply::Break carries no payload and Reply gains a Break arm, so a Guest Binary built against the earlier version 5 wire must be rebuilt.
|
|
9
|
+
* the Guest ABI is version 5, so a Guest Binary built against version 4 is refused at Sandbox construction until rebuilt. The Yielder trait of kobako-runtime gains frame and takes the frame in yield_to_block; the Guest trait of kobako-core and the MrbGuest trait of kobako-mruby take the frame in yield_to_block; Downcall::Yield carries frame and args.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* let a block's break or return unwind a Service as a plain method ([4e9eca0](https://github.com/elct9620/kobako/commit/4e9eca05f1647c8b6409fc00d08317b431c0c648))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* run a stored block as the block of the Call that carried it ([bf33dc1](https://github.com/elct9620/kobako/commit/bf33dc1fd40c83498c6c12b0be52b3a835a144cf))
|
|
19
|
+
|
|
20
|
+
## [0.18.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.17.0...kobako-transport-v0.18.0) (2026-10-13)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### ⚠ BREAKING CHANGES
|
|
24
|
+
|
|
25
|
+
* **transport:** the stdin layout a Guest Binary reads changed within ABI 4. kobako_transport::envelope::Frames is replaced by Preamble, which is #[non_exhaustive] and built from Default; Bindings and Snippets no longer have their own decode and encode. Guest::eval and Guest::run, MrbGuest::eval and MrbGuest::run, and Runtime::invoke take a Preamble in place of Frames. kobako_core re-exports Preamble and Flag in place of Frames.
|
|
26
|
+
* **mruby:** CodecError::Unsupported is renamed to CodecError::Unserved; a codec of one's own that refuses a position returns the new name.
|
|
27
|
+
* **transport:** the ABI version is 4, so a Guest Binary built for ABI 3 is refused at Sandbox construction. kobako_core::export_guest! emits __kobako_downcall in place of __kobako_eval, __kobako_run, __kobako_yield_to_block and __kobako_take_outcome, and the host import is __kobako_upcall. kobako_core::abi::{take_outcome, write_outcome, write_reply, decode_run} are replaced by serve and answer, and the frames module is no longer public. Reply, YieldReply and Outcome gain an Unsupported arm and take their tags from the shared registry, and kobako_core::DispatchError and kobako::YieldError gain an Unsupported arm. A yield to a guest without block support raises Kobako::UnsupportedError at the Service's yield instead of failing the invocation as a trap.
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
* **transport:** carry a compile request and its answer ([f30958e](https://github.com/elct9620/kobako/commit/f30958e7d61c95667c32cfee7d9b2d2f74550a8e)), closes [#75](https://github.com/elct9620/kobako/issues/75)
|
|
32
|
+
* **transport:** carry each operation as a tag on one channel per direction ([c7d574b](https://github.com/elct9620/kobako/commit/c7d574b59a42e58632f2bdc46cc9a8e4c7ac6121)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
33
|
+
* **transport:** keep an unknown request's body, and refuse tag 0x00 ([edb64df](https://github.com/elct9620/kobako/commit/edb64df8f87694d17c5b6fd71ac63329023306ec)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
34
|
+
* **transport:** name the Frames a Sandbox is set up with ([52b25aa](https://github.com/elct9620/kobako/commit/52b25aa48d321ce8ea7dc9532cfade062118c64f)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
35
|
+
* **transport:** set an invocation up from a Preamble of tagged records ([84d4498](https://github.com/elct9620/kobako/commit/84d4498464f94d7312f26931ddd4ec43894a6654))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Bug Fixes
|
|
39
|
+
|
|
40
|
+
* **transport:** read only an unassigned tag as an unknown request ([fcb81b5](https://github.com/elct9620/kobako/commit/fcb81b5c38d001b729a10287443c5012281b76b6)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
41
|
+
* **transport:** size a counted list by what its bytes can hold ([cfcc9fb](https://github.com/elct9620/kobako/commit/cfcc9fb6e79ec0bd1e6bf3a15659220aab0f6914))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Code Refactoring
|
|
45
|
+
|
|
46
|
+
* **mruby:** name a codec's refused position Unserved ([91979f9](https://github.com/elct9620/kobako/commit/91979f9546a3fa2d2dec8c136ed6affc521ba6fb)), closes [#73](https://github.com/elct9620/kobako/issues/73)
|
|
47
|
+
|
|
3
48
|
## [0.17.0](https://github.com/elct9620/kobako/compare/kobako-transport-v0.16.0...kobako-transport-v0.17.0) (2026-09-20)
|
|
4
49
|
|
|
5
50
|
|
|
@@ -9,22 +9,29 @@ same in every assembly. This crate is both of them, and nothing else.
|
|
|
9
9
|
|
|
10
10
|
| Module | What it fixes |
|
|
11
11
|
|---|---|
|
|
12
|
-
| `envelope` | the **core envelope**, the outer frame each message rides in
|
|
13
|
-
| `abi` | the values
|
|
12
|
+
| `envelope` | the **core envelope**, the outer frame each message rides in |
|
|
13
|
+
| `abi` | the values both sides agree on before exchanging a byte |
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
The envelope covers the downcall's and the upcall's requests under one
|
|
16
|
+
tag registry. It also covers a dispatch round-trip, how an invocation
|
|
17
|
+
ended, and what an invocation is handed, and re-exports every message
|
|
18
|
+
type. The ABI values are the version, the packed `(ptr, len)` return
|
|
19
|
+
layout, the length prefix the Preamble rides stdin behind, and the
|
|
20
|
+
message size cap.
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
## Opaque Payload
|
|
23
|
+
|
|
24
|
+
The envelope reads a message's routing fields and its answer's tag
|
|
18
25
|
without decoding a payload byte. Everything the resolved method consumes
|
|
19
|
-
rides through opaque
|
|
20
|
-
|
|
26
|
+
rides through opaque. A decoded envelope borrows the buffer it came from,
|
|
27
|
+
so that payload reaches its reader as a view rather than a copy.
|
|
21
28
|
|
|
22
29
|
```text
|
|
23
30
|
Call { method, payload }
|
|
24
31
|
└── opaque here; read by whoever owns the schema
|
|
25
32
|
```
|
|
26
33
|
|
|
27
|
-
##
|
|
34
|
+
## Dependency Position
|
|
28
35
|
|
|
29
36
|
This crate depends on no other, and every kobako tier depends on it —
|
|
30
37
|
including any third-party engine, codec, or guest composing against them.
|
|
@@ -34,17 +41,19 @@ host kobako-runtime · kobako-wasmtime ──┐
|
|
|
34
41
|
guest kobako-core · kobako-mruby ────────┴──> kobako-transport
|
|
35
42
|
```
|
|
36
43
|
|
|
37
|
-
The
|
|
38
|
-
|
|
39
|
-
the golden vectors in this crate are derived from that document.
|
|
44
|
+
The golden vectors in this crate are derived from the kobako repository's
|
|
45
|
+
byte-layout document, not from this code.
|
|
40
46
|
|
|
41
47
|
## Usage
|
|
42
48
|
|
|
49
|
+
Depend on it directly only to build a tier of your own; a guest shell
|
|
50
|
+
reaches the same types through `kobako-core`.
|
|
51
|
+
|
|
43
52
|
```toml
|
|
44
53
|
[dependencies]
|
|
45
|
-
kobako-transport = "0.
|
|
54
|
+
kobako-transport = "0.19.0" # x-release-please-version
|
|
46
55
|
```
|
|
47
56
|
|
|
48
57
|
## License
|
|
49
58
|
|
|
50
|
-
Apache-2.0
|
|
59
|
+
Licensed under [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
//! The ABI's fixed values — the numbers and layouts a host and a guest
|
|
2
2
|
//! must already agree on before either can read the other's bytes.
|
|
3
3
|
//!
|
|
4
|
-
//! The
|
|
5
|
-
//!
|
|
6
|
-
//!
|
|
4
|
+
//! The symbols that carry the ABI are declared where they are
|
|
5
|
+
//! implemented: the guest contract crate emits the exports, the driver
|
|
6
|
+
//! resolves them and provides the import. What lives here is what both sides must spell the
|
|
7
7
|
//! same way, and each value is spelled once.
|
|
8
|
-
//!
|
|
9
|
-
//! [ABI signatures]: ../../../docs/wire-codec.md
|
|
10
8
|
|
|
11
9
|
/// The Guest ABI version. A host accepts a Guest Binary only when the
|
|
12
|
-
/// version it reports equals this one, so a
|
|
13
|
-
/// rather than a negotiation.
|
|
14
|
-
|
|
10
|
+
/// version it reports equals this one, so a framing or symbol change is
|
|
11
|
+
/// an increment rather than a negotiation. A new request tag needs
|
|
12
|
+
/// neither, but a new answer arm is an increment.
|
|
13
|
+
pub const ABI_VERSION: u32 = 5;
|
|
15
14
|
|
|
16
|
-
/// Width in bytes of the length prefix that
|
|
17
|
-
///
|
|
15
|
+
/// Width in bytes of the length prefix that frames a message on WASI
|
|
16
|
+
/// stdin, such as the Preamble.
|
|
18
17
|
pub const FRAME_LEN_SIZE: usize = 4;
|
|
19
18
|
|
|
20
19
|
/// Single-dispatch size cap: 16 MiB in either direction, applied to the
|
|
@@ -26,16 +25,14 @@ pub const MAX_DISPATCH_PAYLOAD: usize = 16 * 1024 * 1024;
|
|
|
26
25
|
/// length before it can check anything else about it, so this bounds what a
|
|
27
26
|
/// declared length may make it allocate. It sits above
|
|
28
27
|
/// `MAX_DISPATCH_PAYLOAD` because it guards the read rather than enforcing
|
|
29
|
-
/// the contract:
|
|
30
|
-
/// its merits.
|
|
28
|
+
/// the contract: the host enforces the 16 MiB cap before it sends a frame.
|
|
31
29
|
pub const MAX_FRAME_LEN: usize = 64 * 1024 * 1024;
|
|
32
30
|
|
|
33
31
|
// The read guard must admit every frame the contract allows, so a refusal
|
|
34
32
|
// names the cap rather than the allocation ceiling.
|
|
35
33
|
const _: () = assert!(MAX_FRAME_LEN > MAX_DISPATCH_PAYLOAD);
|
|
36
34
|
|
|
37
|
-
/// Pack `(ptr, len)` into the u64 the
|
|
38
|
-
/// functions answer with: the high 32 bits carry the linear-memory pointer,
|
|
35
|
+
/// Pack `(ptr, len)` into the u64 the downcall and the upcall answer with: the high 32 bits carry the linear-memory pointer,
|
|
39
36
|
/// the low 32 the byte length. A `len` of zero is the failure signal.
|
|
40
37
|
///
|
|
41
38
|
/// ```text
|
|
@@ -54,21 +54,33 @@ impl<'a> Reader<'a> {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/// A `u32` count followed by that many byte strings, each carrying
|
|
57
|
-
/// UTF-8.
|
|
58
|
-
/// is the only list shape a reader needs.
|
|
57
|
+
/// UTF-8.
|
|
59
58
|
///
|
|
60
59
|
/// The elements are copied rather than borrowed: a binding's path and
|
|
61
60
|
/// a panic's `available` names outlive the frame buffer they arrive in.
|
|
62
61
|
pub(crate) fn text_list(&mut self) -> Result<Vec<String>, DecodeError> {
|
|
62
|
+
self.list(|reader| Ok(reader.text()?.to_owned()))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// A `u32` count followed by that many byte strings of any content,
|
|
66
|
+
/// copied for the same reason as `text_list`.
|
|
67
|
+
pub(crate) fn bytes_list(&mut self) -> Result<Vec<Vec<u8>>, DecodeError> {
|
|
68
|
+
self.list(|reader| Ok(reader.bytes()?.to_vec()))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn list<T>(
|
|
72
|
+
&mut self,
|
|
73
|
+
mut element: impl FnMut(&mut Self) -> Result<T, DecodeError>,
|
|
74
|
+
) -> Result<Vec<T>, DecodeError> {
|
|
63
75
|
let count = self.u32()? as usize;
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
if count > self.remaining().len() {
|
|
76
|
+
// Every element opens with a `u32` length, so a count past what the
|
|
77
|
+
// bytes left can hold is refused before it sizes the allocation.
|
|
78
|
+
if count > self.remaining().len() / size_of::<u32>() {
|
|
67
79
|
return Err(MALFORMED);
|
|
68
80
|
}
|
|
69
81
|
let mut out = Vec::with_capacity(count);
|
|
70
82
|
for _ in 0..count {
|
|
71
|
-
out.push(self
|
|
83
|
+
out.push(element(self)?);
|
|
72
84
|
}
|
|
73
85
|
Ok(out)
|
|
74
86
|
}
|