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
|
@@ -2,18 +2,11 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! Success-versus-fault is decided by a tag byte rather than a reserved
|
|
4
4
|
//! payload value, so a side learns the outcome of a Call whatever schema
|
|
5
|
-
//! the payload carries.
|
|
5
|
+
//! the payload carries. Each tag is the answer's arm in the shared
|
|
6
|
+
//! registry, so the same byte means the same arm in every answer.
|
|
6
7
|
|
|
7
8
|
use super::bytes::{Reader, Writer};
|
|
8
|
-
use super::{DecodeError, ErrorRecord, Fault};
|
|
9
|
-
|
|
10
|
-
const TAG_OK: u8 = 0;
|
|
11
|
-
const TAG_FAULT: u8 = 1;
|
|
12
|
-
|
|
13
|
-
const YIELD_OK: u8 = 0x01;
|
|
14
|
-
const YIELD_BREAK: u8 = 0x02;
|
|
15
|
-
const YIELD_RESERVED: u8 = 0x03;
|
|
16
|
-
const YIELD_ERROR: u8 = 0x04;
|
|
9
|
+
use super::{tag, DecodeError, ErrorRecord, Fault, Unsupported};
|
|
17
10
|
|
|
18
11
|
/// The answer to one dispatch Call.
|
|
19
12
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
@@ -24,18 +17,29 @@ pub enum Reply {
|
|
|
24
17
|
/// every field of a Fault is kobako's, so a guest reads a refusal
|
|
25
18
|
/// whatever schema it speaks.
|
|
26
19
|
Fault(Fault),
|
|
20
|
+
/// A block exit unwound the method; the guest continues that exit,
|
|
21
|
+
/// which it kept, so nothing of it crosses.
|
|
22
|
+
Break,
|
|
23
|
+
/// The receiver does not serve the request it was sent.
|
|
24
|
+
Unsupported(Unsupported),
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
impl Reply {
|
|
30
28
|
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
31
29
|
let mut reader = Reader::new(bytes);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
TAG_OK => Ok(Reply::Ok(reader.remaining().to_vec())),
|
|
30
|
+
match reader.u8()? {
|
|
31
|
+
tag::OK => Ok(Reply::Ok(reader.remaining().to_vec())),
|
|
35
32
|
// No `finish` here: a Fault owns everything after the tag,
|
|
36
33
|
// including the fields a later contract version appends.
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
tag::FAULT => Ok(Reply::Fault(Fault::read(&mut reader)?)),
|
|
35
|
+
tag::BREAK => {
|
|
36
|
+
reader.finish()?;
|
|
37
|
+
Ok(Reply::Break)
|
|
38
|
+
}
|
|
39
|
+
tag::UNSUPPORTED => Ok(Reply::Unsupported(Unsupported::read(&mut reader)?)),
|
|
40
|
+
_ => Err(DecodeError::new(
|
|
41
|
+
"Reply tag must be 0x10 (ok), 0x11 (fault), 0x12 (break), or 0x15 (unsupported)",
|
|
42
|
+
)),
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
@@ -43,44 +47,51 @@ impl Reply {
|
|
|
43
47
|
let mut writer = Writer::new();
|
|
44
48
|
match self {
|
|
45
49
|
Reply::Ok(body) => {
|
|
46
|
-
writer.u8(
|
|
50
|
+
writer.u8(tag::OK).remainder(body);
|
|
47
51
|
}
|
|
48
52
|
Reply::Fault(fault) => {
|
|
49
|
-
writer.u8(
|
|
53
|
+
writer.u8(tag::FAULT);
|
|
50
54
|
fault.write(&mut writer);
|
|
51
55
|
}
|
|
56
|
+
Reply::Break => {
|
|
57
|
+
writer.u8(tag::BREAK);
|
|
58
|
+
}
|
|
59
|
+
Reply::Unsupported(refusal) => return refusal.encode(),
|
|
52
60
|
}
|
|
53
61
|
writer.into_bytes()
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
|
|
57
|
-
/// The answer to one Yield Call.
|
|
58
|
-
/// Reply has no counterpart for, because a block can end its caller.
|
|
65
|
+
/// The answer to one Yield Call.
|
|
59
66
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
60
67
|
pub enum YieldReply {
|
|
61
68
|
/// The block completed; the bytes are its value, codec-encoded.
|
|
62
69
|
Ok(Vec<u8>),
|
|
63
|
-
/// The block ran `break
|
|
64
|
-
///
|
|
65
|
-
Break
|
|
70
|
+
/// The block ran `break` or `return` to a frame past the Service.
|
|
71
|
+
/// The guest keeps the exit, so the Service is only unwound.
|
|
72
|
+
Break,
|
|
66
73
|
/// The block raised, or failed in a way the host re-raises at the
|
|
67
74
|
/// yield site.
|
|
68
75
|
Error(ErrorRecord),
|
|
76
|
+
/// The guest serves no block.
|
|
77
|
+
Unsupported(Unsupported),
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
impl YieldReply {
|
|
72
81
|
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
73
82
|
let mut reader = Reader::new(bytes);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
match reader.u8()? {
|
|
84
|
+
tag::OK => Ok(YieldReply::Ok(reader.remaining().to_vec())),
|
|
85
|
+
tag::BREAK => {
|
|
86
|
+
reader.finish()?;
|
|
87
|
+
Ok(YieldReply::Break)
|
|
88
|
+
}
|
|
89
|
+
tag::ERROR => {
|
|
79
90
|
let record = ErrorRecord::read(&mut reader)?;
|
|
80
91
|
reader.finish()?;
|
|
81
92
|
Ok(YieldReply::Error(record))
|
|
82
93
|
}
|
|
83
|
-
|
|
94
|
+
tag::UNSUPPORTED => Ok(YieldReply::Unsupported(Unsupported::read(&mut reader)?)),
|
|
84
95
|
_ => Err(DecodeError::new("Yield Reply tag is not recognised")),
|
|
85
96
|
}
|
|
86
97
|
}
|
|
@@ -89,15 +100,16 @@ impl YieldReply {
|
|
|
89
100
|
let mut writer = Writer::new();
|
|
90
101
|
match self {
|
|
91
102
|
YieldReply::Ok(body) => {
|
|
92
|
-
writer.u8(
|
|
103
|
+
writer.u8(tag::OK).remainder(body);
|
|
93
104
|
}
|
|
94
|
-
YieldReply::Break
|
|
95
|
-
writer.u8(
|
|
105
|
+
YieldReply::Break => {
|
|
106
|
+
writer.u8(tag::BREAK);
|
|
96
107
|
}
|
|
97
108
|
YieldReply::Error(record) => {
|
|
98
|
-
writer.u8(
|
|
109
|
+
writer.u8(tag::ERROR);
|
|
99
110
|
record.write(&mut writer);
|
|
100
111
|
}
|
|
112
|
+
YieldReply::Unsupported(refusal) => return refusal.encode(),
|
|
101
113
|
}
|
|
102
114
|
writer.into_bytes()
|
|
103
115
|
}
|
|
@@ -109,19 +121,21 @@ mod tests {
|
|
|
109
121
|
|
|
110
122
|
// @behavior WE-018
|
|
111
123
|
#[test]
|
|
112
|
-
fn
|
|
124
|
+
fn every_reply_arm_round_trips() {
|
|
113
125
|
for reply in [
|
|
114
126
|
Reply::Ok(vec![0x2a]),
|
|
115
127
|
Reply::Fault(Fault::new(
|
|
116
128
|
super::super::FaultKind::Undefined,
|
|
117
129
|
"no such method",
|
|
118
130
|
)),
|
|
131
|
+
Reply::Break,
|
|
132
|
+
Reply::Unsupported(Unsupported::new("no such operation")),
|
|
119
133
|
] {
|
|
120
134
|
let encoded = reply.encode();
|
|
121
135
|
assert_eq!(
|
|
122
136
|
Reply::decode(&encoded),
|
|
123
137
|
Ok(reply),
|
|
124
|
-
"a Reply must survive a host encode and decode on
|
|
138
|
+
"a Reply must survive a host encode and decode on every arm"
|
|
125
139
|
);
|
|
126
140
|
}
|
|
127
141
|
}
|
|
@@ -141,8 +155,8 @@ mod tests {
|
|
|
141
155
|
#[test]
|
|
142
156
|
fn an_unknown_reply_tag_is_refused() {
|
|
143
157
|
assert!(
|
|
144
|
-
Reply::decode(&[
|
|
145
|
-
"a Reply tag
|
|
158
|
+
Reply::decode(&[0x13, 0]).is_err(),
|
|
159
|
+
"a Reply tag outside its arms must be rejected as a wire violation"
|
|
146
160
|
);
|
|
147
161
|
}
|
|
148
162
|
|
|
@@ -160,12 +174,13 @@ mod tests {
|
|
|
160
174
|
fn every_live_yield_arm_round_trips() {
|
|
161
175
|
let arms = [
|
|
162
176
|
YieldReply::Ok(vec![0xa4]),
|
|
163
|
-
YieldReply::Break
|
|
177
|
+
YieldReply::Break,
|
|
164
178
|
YieldReply::Error(ErrorRecord {
|
|
165
179
|
name: "LocalJumpError".into(),
|
|
166
180
|
message: "no block".into(),
|
|
167
181
|
backtrace: vec!["(eval):1".into()],
|
|
168
182
|
}),
|
|
183
|
+
YieldReply::Unsupported(Unsupported::new("no block support")),
|
|
169
184
|
];
|
|
170
185
|
for arm in arms {
|
|
171
186
|
let encoded = arm.encode();
|
|
@@ -177,22 +192,16 @@ mod tests {
|
|
|
177
192
|
}
|
|
178
193
|
}
|
|
179
194
|
|
|
180
|
-
// @behavior WE-023
|
|
181
|
-
#[test]
|
|
182
|
-
fn the_reserved_yield_tag_is_refused() {
|
|
183
|
-
assert!(
|
|
184
|
-
YieldReply::decode(&[0x03]).is_err(),
|
|
185
|
-
"Yield Reply tag 0x03 must be rejected by both peers so it stays reserved"
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
195
|
// @behavior WE-024
|
|
190
196
|
#[test]
|
|
191
197
|
fn an_unknown_yield_tag_is_refused() {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
for tag in [0x03, 0x11, 0x7f] {
|
|
199
|
+
assert!(
|
|
200
|
+
YieldReply::decode(&[tag]).is_err(),
|
|
201
|
+
"a Yield Reply tag {tag:#04x} outside the live set must be rejected as a \
|
|
202
|
+
wire violation"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
196
205
|
}
|
|
197
206
|
|
|
198
207
|
// @behavior WE-025
|
|
@@ -203,15 +212,43 @@ mod tests {
|
|
|
203
212
|
// is what lets a side read the outcome without a payload codec.
|
|
204
213
|
assert_eq!(
|
|
205
214
|
Reply::Ok(vec![0x2a]).encode(),
|
|
206
|
-
vec![
|
|
207
|
-
"a successful Reply must encode as tag byte
|
|
215
|
+
vec![0x10, 0x2a],
|
|
216
|
+
"a successful Reply must encode as tag byte 0x10 followed by the body alone"
|
|
208
217
|
);
|
|
209
218
|
assert_eq!(
|
|
210
219
|
Reply::Fault(Fault::new(super::super::FaultKind::Runtime, "x")).encode(),
|
|
211
|
-
vec![
|
|
212
|
-
"a fault Reply must encode as tag byte
|
|
220
|
+
vec![0x11, 0, 0, 0, 0, 1, b'x'],
|
|
221
|
+
"a fault Reply must encode as tag byte 0x11 followed by the Fault's own fixed \
|
|
213
222
|
layout, so a guest reads a refusal without reaching a payload codec"
|
|
214
223
|
);
|
|
224
|
+
assert_eq!(
|
|
225
|
+
Reply::Unsupported(Unsupported::new("x")).encode(),
|
|
226
|
+
vec![0x15, 0, 0, 0, 0, 1, b'x'],
|
|
227
|
+
"an unsupported Reply must encode as tag byte 0x15 followed by kind 0 and the \
|
|
228
|
+
message"
|
|
229
|
+
);
|
|
230
|
+
assert_eq!(
|
|
231
|
+
Reply::Break.encode(),
|
|
232
|
+
vec![0x12],
|
|
233
|
+
"a break Reply must encode as tag byte 0x12 alone, since the guest keeps the exit"
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// @behavior WE-068
|
|
238
|
+
#[test]
|
|
239
|
+
fn every_answer_takes_its_ok_tag_from_one_registry() {
|
|
240
|
+
let body = vec![0x2a];
|
|
241
|
+
assert_eq!(
|
|
242
|
+
[
|
|
243
|
+
Reply::Ok(body.clone()).encode()[0],
|
|
244
|
+
YieldReply::Ok(body.clone()).encode()[0],
|
|
245
|
+
super::super::CompileReply::Ok(vec![body.clone()]).encode()[0],
|
|
246
|
+
super::super::Outcome::Ok(body).encode()[0],
|
|
247
|
+
],
|
|
248
|
+
[0x10, 0x10, 0x10, 0x10],
|
|
249
|
+
"a Reply, a Yield Reply, a Compile Reply and an Outcome on their ok arm must all \
|
|
250
|
+
open with tag byte 0x10"
|
|
251
|
+
);
|
|
215
252
|
}
|
|
216
253
|
|
|
217
254
|
// @behavior WE-026
|
|
@@ -219,13 +256,18 @@ mod tests {
|
|
|
219
256
|
fn golden_layout_pins_the_yield_tags() {
|
|
220
257
|
assert_eq!(
|
|
221
258
|
YieldReply::Ok(vec![0xc0]).encode(),
|
|
222
|
-
vec![
|
|
223
|
-
"a Yield Reply carrying the block's value must encode as tag byte
|
|
259
|
+
vec![0x10, 0xc0],
|
|
260
|
+
"a Yield Reply carrying the block's value must encode as tag byte 0x10"
|
|
261
|
+
);
|
|
262
|
+
assert_eq!(
|
|
263
|
+
YieldReply::Break.encode(),
|
|
264
|
+
vec![0x12],
|
|
265
|
+
"a Yield Reply for a block exit must encode as tag byte 0x12 alone"
|
|
224
266
|
);
|
|
225
267
|
assert_eq!(
|
|
226
|
-
YieldReply::
|
|
227
|
-
vec![
|
|
228
|
-
"a Yield Reply
|
|
268
|
+
YieldReply::Unsupported(Unsupported::new("x")).encode(),
|
|
269
|
+
vec![0x15, 0, 0, 0, 0, 1, b'x'],
|
|
270
|
+
"a Yield Reply from a guest serving no block must encode as tag byte 0x15"
|
|
229
271
|
);
|
|
230
272
|
assert_eq!(
|
|
231
273
|
YieldReply::Error(ErrorRecord {
|
|
@@ -235,12 +277,12 @@ mod tests {
|
|
|
235
277
|
})
|
|
236
278
|
.encode(),
|
|
237
279
|
vec![
|
|
238
|
-
|
|
280
|
+
0x13, // tag: error
|
|
239
281
|
0, 0, 0, 1, b'E', // name
|
|
240
282
|
0, 0, 0, 1, b'm', // message
|
|
241
283
|
0, 0, 0, 0, // backtrace count
|
|
242
284
|
],
|
|
243
|
-
"a Yield Reply carrying a guest failure must encode as tag byte
|
|
285
|
+
"a Yield Reply carrying a guest failure must encode as tag byte 0x13 followed \
|
|
244
286
|
by an Error Record rather than a codec-encoded body"
|
|
245
287
|
);
|
|
246
288
|
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
//! The requests a downcall and an upcall carry: a tag naming the
|
|
2
|
+
//! operation, then that operation's body.
|
|
3
|
+
//!
|
|
4
|
+
//! A tag the registry has not assigned reads as `Unknown` with its body
|
|
5
|
+
//! intact, so a receiver can serve it or answer it as unsupported. An
|
|
6
|
+
//! assigned tag already means one thing, so one this direction does not
|
|
7
|
+
//! carry as a request is a wire violation, as are an empty request and
|
|
8
|
+
//! the invalid tag; each body's own layout refuses what it cannot frame.
|
|
9
|
+
|
|
10
|
+
use super::bytes::{Reader, Writer};
|
|
11
|
+
use super::{tag, Call, DecodeError, Run};
|
|
12
|
+
|
|
13
|
+
/// A request the host sends the guest.
|
|
14
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
15
|
+
#[non_exhaustive]
|
|
16
|
+
pub enum Downcall {
|
|
17
|
+
/// Run the user source, UTF-8, as one invocation.
|
|
18
|
+
Eval(Vec<u8>),
|
|
19
|
+
/// Call the entrypoint the Run names.
|
|
20
|
+
Run(Run),
|
|
21
|
+
/// Re-enter the block of the open Call `frame` counts to, from `0`
|
|
22
|
+
/// at the outermost; `args` are its arguments, codec-encoded.
|
|
23
|
+
Yield { frame: u32, args: Vec<u8> },
|
|
24
|
+
/// Compile the Preamble's source Snippets without running them. It
|
|
25
|
+
/// carries no body: what it compiles is the Preamble an invocation
|
|
26
|
+
/// would read.
|
|
27
|
+
Compile,
|
|
28
|
+
/// A tag the registry has not assigned, with the body it carried.
|
|
29
|
+
Unknown { tag: UnknownTag, body: Vec<u8> },
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Downcall {
|
|
33
|
+
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
34
|
+
let mut reader = Reader::new(bytes);
|
|
35
|
+
let tag = read_tag(&mut reader)?;
|
|
36
|
+
let body = reader.remaining();
|
|
37
|
+
Ok(match tag {
|
|
38
|
+
tag::EVAL => Downcall::Eval(body.to_vec()),
|
|
39
|
+
tag::RUN => Downcall::Run(Run::decode(body)?),
|
|
40
|
+
tag::YIELD => Downcall::Yield {
|
|
41
|
+
frame: reader.u32()?,
|
|
42
|
+
args: reader.remaining().to_vec(),
|
|
43
|
+
},
|
|
44
|
+
tag::COMPILE if body.is_empty() => Downcall::Compile,
|
|
45
|
+
tag::COMPILE => return Err(DecodeError::new("a compile request carries no body")),
|
|
46
|
+
tag => Downcall::Unknown {
|
|
47
|
+
tag: unknown(tag)?,
|
|
48
|
+
body: body.to_vec(),
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
pub fn encode(&self) -> Vec<u8> {
|
|
54
|
+
let mut writer = Writer::new();
|
|
55
|
+
match self {
|
|
56
|
+
Downcall::Eval(source) => writer.u8(tag::EVAL).remainder(source),
|
|
57
|
+
Downcall::Run(run) => writer.u8(tag::RUN).remainder(&run.encode()),
|
|
58
|
+
Downcall::Yield { frame, args } => writer.u8(tag::YIELD).u32(*frame).remainder(args),
|
|
59
|
+
Downcall::Compile => writer.u8(tag::COMPILE),
|
|
60
|
+
Downcall::Unknown { tag, body } => writer.u8(tag.get()).remainder(body),
|
|
61
|
+
};
|
|
62
|
+
writer.into_bytes()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// A request the guest sends the host.
|
|
67
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
68
|
+
#[non_exhaustive]
|
|
69
|
+
pub enum Upcall<'a> {
|
|
70
|
+
/// Dispatch the Call to a Receiver.
|
|
71
|
+
Call(Call<'a>),
|
|
72
|
+
/// A tag the registry has not assigned, with the body it carried.
|
|
73
|
+
Unknown { tag: UnknownTag, body: &'a [u8] },
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl<'a> Upcall<'a> {
|
|
77
|
+
pub fn decode(bytes: &'a [u8]) -> Result<Self, DecodeError> {
|
|
78
|
+
let mut reader = Reader::new(bytes);
|
|
79
|
+
let tag = read_tag(&mut reader)?;
|
|
80
|
+
let body = reader.remaining();
|
|
81
|
+
Ok(match tag {
|
|
82
|
+
tag::CALL => Upcall::Call(Call::decode(body)?),
|
|
83
|
+
tag => Upcall::Unknown {
|
|
84
|
+
tag: unknown(tag)?,
|
|
85
|
+
body,
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub fn encode(&self) -> Vec<u8> {
|
|
91
|
+
let mut writer = Writer::new();
|
|
92
|
+
match self {
|
|
93
|
+
Upcall::Call(call) => writer.u8(tag::CALL).remainder(&call.encode()),
|
|
94
|
+
Upcall::Unknown { tag, body } => writer.u8(tag.get()).remainder(body),
|
|
95
|
+
};
|
|
96
|
+
writer.into_bytes()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// A request tag the registry has not assigned. Only such a tag can name
|
|
101
|
+
/// an unknown request, so an unknown request never encodes as an
|
|
102
|
+
/// operation the registry already has.
|
|
103
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
104
|
+
pub struct UnknownTag(u8);
|
|
105
|
+
|
|
106
|
+
impl UnknownTag {
|
|
107
|
+
/// `None` when the registry assigns `tag`, the invalid tag included.
|
|
108
|
+
pub fn new(tag: u8) -> Option<Self> {
|
|
109
|
+
(!tag::is_assigned(tag)).then_some(UnknownTag(tag))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub fn get(self) -> u8 {
|
|
113
|
+
self.0
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fn read_tag(reader: &mut Reader<'_>) -> Result<u8, DecodeError> {
|
|
118
|
+
match reader.u8()? {
|
|
119
|
+
tag::INVALID => Err(DecodeError::new("request tag 0x00 is invalid")),
|
|
120
|
+
tag => Ok(tag),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn unknown(tag: u8) -> Result<UnknownTag, DecodeError> {
|
|
125
|
+
UnknownTag::new(tag).ok_or(DecodeError::new(
|
|
126
|
+
"request tag is assigned to the other direction or to an answer",
|
|
127
|
+
))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
#[cfg(test)]
|
|
131
|
+
mod tests {
|
|
132
|
+
use super::*;
|
|
133
|
+
use crate::envelope::Target;
|
|
134
|
+
|
|
135
|
+
// @behavior WE-071
|
|
136
|
+
#[test]
|
|
137
|
+
fn golden_layout_pins_each_request_tag() {
|
|
138
|
+
let run = Run {
|
|
139
|
+
entrypoint: "M".into(),
|
|
140
|
+
payload: vec![0x90],
|
|
141
|
+
};
|
|
142
|
+
let call = Call {
|
|
143
|
+
target: Target::Path("K"),
|
|
144
|
+
method: "m",
|
|
145
|
+
block_given: false,
|
|
146
|
+
payload: &[0x90],
|
|
147
|
+
};
|
|
148
|
+
assert_eq!(
|
|
149
|
+
(
|
|
150
|
+
Downcall::Eval(b"1".to_vec()).encode(),
|
|
151
|
+
Downcall::Run(run).encode(),
|
|
152
|
+
Downcall::Yield {
|
|
153
|
+
frame: 2,
|
|
154
|
+
args: vec![0x90],
|
|
155
|
+
}
|
|
156
|
+
.encode(),
|
|
157
|
+
Downcall::Compile.encode(),
|
|
158
|
+
Upcall::Call(call).encode(),
|
|
159
|
+
),
|
|
160
|
+
(
|
|
161
|
+
vec![0x01, b'1'],
|
|
162
|
+
vec![0x02, 0, 0, 0, 1, b'M', 0x90],
|
|
163
|
+
vec![0x03, 0, 0, 0, 2, 0x90],
|
|
164
|
+
vec![0x08],
|
|
165
|
+
vec![
|
|
166
|
+
0x04, // tag: call
|
|
167
|
+
0, // kind: path
|
|
168
|
+
0, 0, 0, 1, b'K', // target
|
|
169
|
+
0, 0, 0, 1, b'm', // method
|
|
170
|
+
0, // block_given
|
|
171
|
+
0x90, // payload
|
|
172
|
+
],
|
|
173
|
+
),
|
|
174
|
+
"each request through Downcall and Upcall must open with its operation's tag \
|
|
175
|
+
followed by its body alone"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[test]
|
|
180
|
+
fn every_downcall_round_trips() {
|
|
181
|
+
for request in [
|
|
182
|
+
Downcall::Eval(b"1 + 1".to_vec()),
|
|
183
|
+
Downcall::Run(Run {
|
|
184
|
+
entrypoint: "Main".into(),
|
|
185
|
+
payload: vec![0x90],
|
|
186
|
+
}),
|
|
187
|
+
Downcall::Yield {
|
|
188
|
+
frame: 0,
|
|
189
|
+
args: vec![0x91, 0x01],
|
|
190
|
+
},
|
|
191
|
+
Downcall::Compile,
|
|
192
|
+
Downcall::Unknown {
|
|
193
|
+
tag: UnknownTag::new(0xf0).unwrap(),
|
|
194
|
+
body: vec![0xaa, 0xbb],
|
|
195
|
+
},
|
|
196
|
+
] {
|
|
197
|
+
assert_eq!(
|
|
198
|
+
Downcall::decode(&request.encode()),
|
|
199
|
+
Ok(request),
|
|
200
|
+
"a downcall request must survive an encode and decode unchanged"
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// @behavior WE-072
|
|
206
|
+
#[test]
|
|
207
|
+
fn an_unassigned_tag_reads_as_unknown_with_its_body() {
|
|
208
|
+
assert_eq!(
|
|
209
|
+
(
|
|
210
|
+
Downcall::decode(&[0x09, 0xff]),
|
|
211
|
+
Upcall::decode(&[0xf0, 0x01, 0x02])
|
|
212
|
+
),
|
|
213
|
+
(
|
|
214
|
+
Ok(Downcall::Unknown {
|
|
215
|
+
tag: UnknownTag::new(0x09).unwrap(),
|
|
216
|
+
body: vec![0xff]
|
|
217
|
+
}),
|
|
218
|
+
Ok(Upcall::Unknown {
|
|
219
|
+
tag: UnknownTag::new(0xf0).unwrap(),
|
|
220
|
+
body: &[0x01, 0x02]
|
|
221
|
+
})
|
|
222
|
+
),
|
|
223
|
+
"a request under a tag the registry has not assigned through Downcall and Upcall \
|
|
224
|
+
must read as unknown with its body intact"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// @behavior WE-073
|
|
229
|
+
#[test]
|
|
230
|
+
fn an_empty_request_is_refused() {
|
|
231
|
+
assert!(
|
|
232
|
+
Downcall::decode(&[]).is_err() && Upcall::decode(&[]).is_err(),
|
|
233
|
+
"a request of no length through Downcall and Upcall must be refused"
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// @behavior WE-074
|
|
238
|
+
#[test]
|
|
239
|
+
fn a_request_under_the_invalid_tag_is_refused() {
|
|
240
|
+
assert!(
|
|
241
|
+
Downcall::decode(&[0x00, 0x01]).is_err() && Upcall::decode(&[0x00]).is_err(),
|
|
242
|
+
"a request under tag 0x00 through Downcall and Upcall must be refused"
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// @behavior WE-075
|
|
247
|
+
#[test]
|
|
248
|
+
fn a_request_under_a_tag_assigned_elsewhere_is_refused() {
|
|
249
|
+
assert_eq!(
|
|
250
|
+
(
|
|
251
|
+
Downcall::decode(&[0x04, 0x90]).is_err(),
|
|
252
|
+
Downcall::decode(&[0x10, 0x90]).is_err(),
|
|
253
|
+
Upcall::decode(&[0x01, b'1']).is_err(),
|
|
254
|
+
Upcall::decode(&[0x15, 0x00]).is_err(),
|
|
255
|
+
),
|
|
256
|
+
(true, true, true, true),
|
|
257
|
+
"a request under the other direction's tag or an answer's arm through Downcall and \
|
|
258
|
+
Upcall must be refused rather than read as unknown"
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// @behavior WE-084
|
|
263
|
+
#[test]
|
|
264
|
+
fn a_compile_request_carrying_a_body_is_refused() {
|
|
265
|
+
assert!(
|
|
266
|
+
Downcall::decode(&[0x08, 0x90]).is_err(),
|
|
267
|
+
"a compile request with bytes after its tag through Downcall::decode must be \
|
|
268
|
+
refused, since the operation reads its input from the Preamble"
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// @behavior WE-076
|
|
273
|
+
#[test]
|
|
274
|
+
fn an_unknown_tag_cannot_be_made_from_an_assigned_one() {
|
|
275
|
+
assert_eq!(
|
|
276
|
+
[0x00, 0x01, 0x04, 0x08, 0x10, 0x15].map(UnknownTag::new),
|
|
277
|
+
[None; 6],
|
|
278
|
+
"an assigned tag or tag 0x00 through UnknownTag::new must be refused, so an \
|
|
279
|
+
unknown request never encodes as a known operation"
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#[test]
|
|
284
|
+
fn every_upcall_round_trips() {
|
|
285
|
+
let call = Call {
|
|
286
|
+
target: Target::Handle(7),
|
|
287
|
+
method: "m",
|
|
288
|
+
block_given: true,
|
|
289
|
+
payload: &[0x90],
|
|
290
|
+
};
|
|
291
|
+
for request in [
|
|
292
|
+
Upcall::Call(call),
|
|
293
|
+
Upcall::Unknown {
|
|
294
|
+
tag: UnknownTag::new(0xf1).unwrap(),
|
|
295
|
+
body: &[0xaa],
|
|
296
|
+
},
|
|
297
|
+
] {
|
|
298
|
+
assert_eq!(
|
|
299
|
+
Upcall::decode(&request.encode()),
|
|
300
|
+
Ok(request),
|
|
301
|
+
"an upcall request must survive an encode and decode unchanged"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
//! Run is Call's reverse-direction sibling — `entrypoint` routes it, the
|
|
4
4
|
//! payload feeds it. It carries no `method` because the entrypoint is
|
|
5
5
|
//! invoked through its own `#call`, and no `block_given` because `#run`
|
|
6
|
-
//! supplies no block. It
|
|
7
|
-
//! frame, so its length reaches the guest as an export argument.
|
|
6
|
+
//! supplies no block. It is the body of a `run` downcall.
|
|
8
7
|
|
|
9
8
|
use super::bytes::{Reader, Writer};
|
|
10
9
|
use super::DecodeError;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
//! The tag registry: the first byte of every downcall and upcall message,
|
|
2
|
+
//! and the kind of every Preamble record.
|
|
3
|
+
//!
|
|
4
|
+
//! One number space covers requests, answers, and records alike, so a byte
|
|
5
|
+
//! means one thing in every message and an answer's arm never collides
|
|
6
|
+
//! across operations. The registry grows append-only; a receiver answers a
|
|
7
|
+
//! request tag it does not serve as unsupported.
|
|
8
|
+
|
|
9
|
+
/// Never assigned, so a zeroed message cannot pass for one that was written.
|
|
10
|
+
pub(crate) const INVALID: u8 = 0x00;
|
|
11
|
+
pub(crate) const EVAL: u8 = 0x01;
|
|
12
|
+
pub(crate) const RUN: u8 = 0x02;
|
|
13
|
+
pub(crate) const YIELD: u8 = 0x03;
|
|
14
|
+
pub(crate) const CALL: u8 = 0x04;
|
|
15
|
+
pub(crate) const BINDINGS: u8 = 0x05;
|
|
16
|
+
pub(crate) const SNIPPETS: u8 = 0x06;
|
|
17
|
+
pub(crate) const FLAG: u8 = 0x07;
|
|
18
|
+
pub(crate) const COMPILE: u8 = 0x08;
|
|
19
|
+
|
|
20
|
+
pub(crate) const OK: u8 = 0x10;
|
|
21
|
+
pub(crate) const FAULT: u8 = 0x11;
|
|
22
|
+
pub(crate) const BREAK: u8 = 0x12;
|
|
23
|
+
pub(crate) const ERROR: u8 = 0x13;
|
|
24
|
+
pub(crate) const PANIC: u8 = 0x14;
|
|
25
|
+
pub(crate) const UNSUPPORTED: u8 = 0x15;
|
|
26
|
+
|
|
27
|
+
/// Whether the registry gives `tag` a meaning, the invalid tag included.
|
|
28
|
+
/// An assigned tag means one thing in every message, so it can never
|
|
29
|
+
/// stand for an operation this registry does not know.
|
|
30
|
+
pub(crate) fn is_assigned(tag: u8) -> bool {
|
|
31
|
+
matches!(
|
|
32
|
+
tag,
|
|
33
|
+
INVALID
|
|
34
|
+
| EVAL
|
|
35
|
+
| RUN
|
|
36
|
+
| YIELD
|
|
37
|
+
| CALL
|
|
38
|
+
| BINDINGS
|
|
39
|
+
| SNIPPETS
|
|
40
|
+
| FLAG
|
|
41
|
+
| COMPILE
|
|
42
|
+
| OK
|
|
43
|
+
| FAULT
|
|
44
|
+
| BREAK
|
|
45
|
+
| ERROR
|
|
46
|
+
| PANIC
|
|
47
|
+
| UNSUPPORTED
|
|
48
|
+
)
|
|
49
|
+
}
|