kobako 0.27.0 → 0.28.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 +71 -0
- data/Cargo.lock +117 -140
- data/README.md +100 -125
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +31 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +4 -3
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/lib.rs +1 -1
- data/crates/kobako-runtime/src/profile.rs +1 -1
- data/crates/kobako-runtime/src/runtime.rs +31 -17
- data/crates/kobako-runtime/src/snapshot.rs +13 -10
- data/crates/kobako-runtime/src/yielder.rs +9 -6
- data/crates/kobako-transport/CHANGELOG.md +28 -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/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +77 -53
- data/crates/kobako-transport/src/envelope/request.rs +295 -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 +44 -0
- data/crates/kobako-wasmtime/Cargo.toml +7 -5
- data/crates/kobako-wasmtime/README.md +2 -2
- data/crates/kobako-wasmtime/src/abi.rs +5 -8
- data/crates/kobako-wasmtime/src/ambient.rs +2 -2
- data/crates/kobako-wasmtime/src/cache.rs +25 -59
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +11 -14
- data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
- data/crates/kobako-wasmtime/src/driver.rs +133 -86
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
- data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
- data/crates/kobako-wasmtime/src/invocation.rs +55 -372
- data/crates/kobako-wasmtime/src/io.rs +286 -0
- data/crates/kobako-wasmtime/src/lib.rs +3 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/trap.rs +32 -45
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/runtime/bridge.rs +38 -47
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/gvl.rs +4 -12
- data/ext/kobako/src/runtime.rs +169 -212
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +41 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/services.rb +7 -61
- data/lib/kobako/catalog/snippets.rb +42 -66
- data/lib/kobako/catalog.rb +2 -10
- data/lib/kobako/codec/decoder.rb +9 -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 +4 -10
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec.rb +3 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +51 -112
- data/lib/kobako/errors.rb +51 -127
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- 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 +23 -53
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +96 -133
- data/lib/kobako/sandbox_options.rb +38 -84
- data/lib/kobako/snippet/binary.rb +1 -16
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +1 -9
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +112 -195
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +5 -10
- data/lib/kobako/transport/reflection.rb +39 -50
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +21 -73
- data/lib/kobako/transport.rb +3 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +17 -0
- data/sig/kobako/catalog/services.rbs +0 -6
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec.rbs +2 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +2 -3
- data/sig/kobako/errors.rbs +6 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/runtime.rbs +12 -8
- data/sig/kobako/sandbox.rbs +4 -0
- data/sig/kobako/sandbox_options.rbs +2 -6
- data/sig/kobako/transport/dispatcher.rbs +14 -18
- data/sig/kobako/transport/reflection.rbs +5 -3
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +7 -6
- metadata +16 -4
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/rust-toolchain.toml +0 -15
|
@@ -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,22 @@ 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
|
+
/// The receiver does not serve the request it was sent.
|
|
21
|
+
Unsupported(Unsupported),
|
|
27
22
|
}
|
|
28
23
|
|
|
29
24
|
impl Reply {
|
|
30
25
|
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
31
26
|
let mut reader = Reader::new(bytes);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
TAG_OK => Ok(Reply::Ok(reader.remaining().to_vec())),
|
|
27
|
+
match reader.u8()? {
|
|
28
|
+
tag::OK => Ok(Reply::Ok(reader.remaining().to_vec())),
|
|
35
29
|
// No `finish` here: a Fault owns everything after the tag,
|
|
36
30
|
// including the fields a later contract version appends.
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
tag::FAULT => Ok(Reply::Fault(Fault::read(&mut reader)?)),
|
|
32
|
+
tag::UNSUPPORTED => Ok(Reply::Unsupported(Unsupported::read(&mut reader)?)),
|
|
33
|
+
_ => Err(DecodeError::new(
|
|
34
|
+
"Reply tag must be 0x10 (ok), 0x11 (fault), or 0x15 (unsupported)",
|
|
35
|
+
)),
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
38
|
|
|
@@ -43,12 +40,13 @@ impl Reply {
|
|
|
43
40
|
let mut writer = Writer::new();
|
|
44
41
|
match self {
|
|
45
42
|
Reply::Ok(body) => {
|
|
46
|
-
writer.u8(
|
|
43
|
+
writer.u8(tag::OK).remainder(body);
|
|
47
44
|
}
|
|
48
45
|
Reply::Fault(fault) => {
|
|
49
|
-
writer.u8(
|
|
46
|
+
writer.u8(tag::FAULT);
|
|
50
47
|
fault.write(&mut writer);
|
|
51
48
|
}
|
|
49
|
+
Reply::Unsupported(refusal) => return refusal.encode(),
|
|
52
50
|
}
|
|
53
51
|
writer.into_bytes()
|
|
54
52
|
}
|
|
@@ -66,21 +64,22 @@ pub enum YieldReply {
|
|
|
66
64
|
/// The block raised, or failed in a way the host re-raises at the
|
|
67
65
|
/// yield site.
|
|
68
66
|
Error(ErrorRecord),
|
|
67
|
+
/// The guest serves no block.
|
|
68
|
+
Unsupported(Unsupported),
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
impl YieldReply {
|
|
72
72
|
pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
|
|
73
73
|
let mut reader = Reader::new(bytes);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
YIELD_ERROR => {
|
|
74
|
+
match reader.u8()? {
|
|
75
|
+
tag::OK => Ok(YieldReply::Ok(reader.remaining().to_vec())),
|
|
76
|
+
tag::BREAK => Ok(YieldReply::Break(reader.remaining().to_vec())),
|
|
77
|
+
tag::ERROR => {
|
|
79
78
|
let record = ErrorRecord::read(&mut reader)?;
|
|
80
79
|
reader.finish()?;
|
|
81
80
|
Ok(YieldReply::Error(record))
|
|
82
81
|
}
|
|
83
|
-
|
|
82
|
+
tag::UNSUPPORTED => Ok(YieldReply::Unsupported(Unsupported::read(&mut reader)?)),
|
|
84
83
|
_ => Err(DecodeError::new("Yield Reply tag is not recognised")),
|
|
85
84
|
}
|
|
86
85
|
}
|
|
@@ -89,15 +88,16 @@ impl YieldReply {
|
|
|
89
88
|
let mut writer = Writer::new();
|
|
90
89
|
match self {
|
|
91
90
|
YieldReply::Ok(body) => {
|
|
92
|
-
writer.u8(
|
|
91
|
+
writer.u8(tag::OK).remainder(body);
|
|
93
92
|
}
|
|
94
93
|
YieldReply::Break(body) => {
|
|
95
|
-
writer.u8(
|
|
94
|
+
writer.u8(tag::BREAK).remainder(body);
|
|
96
95
|
}
|
|
97
96
|
YieldReply::Error(record) => {
|
|
98
|
-
writer.u8(
|
|
97
|
+
writer.u8(tag::ERROR);
|
|
99
98
|
record.write(&mut writer);
|
|
100
99
|
}
|
|
100
|
+
YieldReply::Unsupported(refusal) => return refusal.encode(),
|
|
101
101
|
}
|
|
102
102
|
writer.into_bytes()
|
|
103
103
|
}
|
|
@@ -109,19 +109,20 @@ mod tests {
|
|
|
109
109
|
|
|
110
110
|
// @behavior WE-018
|
|
111
111
|
#[test]
|
|
112
|
-
fn
|
|
112
|
+
fn every_reply_arm_round_trips() {
|
|
113
113
|
for reply in [
|
|
114
114
|
Reply::Ok(vec![0x2a]),
|
|
115
115
|
Reply::Fault(Fault::new(
|
|
116
116
|
super::super::FaultKind::Undefined,
|
|
117
117
|
"no such method",
|
|
118
118
|
)),
|
|
119
|
+
Reply::Unsupported(Unsupported::new("no such operation")),
|
|
119
120
|
] {
|
|
120
121
|
let encoded = reply.encode();
|
|
121
122
|
assert_eq!(
|
|
122
123
|
Reply::decode(&encoded),
|
|
123
124
|
Ok(reply),
|
|
124
|
-
"a Reply must survive a host encode and decode on
|
|
125
|
+
"a Reply must survive a host encode and decode on every arm"
|
|
125
126
|
);
|
|
126
127
|
}
|
|
127
128
|
}
|
|
@@ -141,8 +142,8 @@ mod tests {
|
|
|
141
142
|
#[test]
|
|
142
143
|
fn an_unknown_reply_tag_is_refused() {
|
|
143
144
|
assert!(
|
|
144
|
-
Reply::decode(&[
|
|
145
|
-
"a Reply tag
|
|
145
|
+
Reply::decode(&[0x12, 0]).is_err(),
|
|
146
|
+
"a Reply tag outside its arms must be rejected as a wire violation"
|
|
146
147
|
);
|
|
147
148
|
}
|
|
148
149
|
|
|
@@ -166,6 +167,7 @@ mod tests {
|
|
|
166
167
|
message: "no block".into(),
|
|
167
168
|
backtrace: vec!["(eval):1".into()],
|
|
168
169
|
}),
|
|
170
|
+
YieldReply::Unsupported(Unsupported::new("no block support")),
|
|
169
171
|
];
|
|
170
172
|
for arm in arms {
|
|
171
173
|
let encoded = arm.encode();
|
|
@@ -177,22 +179,16 @@ mod tests {
|
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
|
|
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
182
|
// @behavior WE-024
|
|
190
183
|
#[test]
|
|
191
184
|
fn an_unknown_yield_tag_is_refused() {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
185
|
+
for tag in [0x03, 0x11, 0x7f] {
|
|
186
|
+
assert!(
|
|
187
|
+
YieldReply::decode(&[tag]).is_err(),
|
|
188
|
+
"a Yield Reply tag {tag:#04x} outside the live set must be rejected as a \
|
|
189
|
+
wire violation"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
196
192
|
}
|
|
197
193
|
|
|
198
194
|
// @behavior WE-025
|
|
@@ -203,15 +199,38 @@ mod tests {
|
|
|
203
199
|
// is what lets a side read the outcome without a payload codec.
|
|
204
200
|
assert_eq!(
|
|
205
201
|
Reply::Ok(vec![0x2a]).encode(),
|
|
206
|
-
vec![
|
|
207
|
-
"a successful Reply must encode as tag byte
|
|
202
|
+
vec![0x10, 0x2a],
|
|
203
|
+
"a successful Reply must encode as tag byte 0x10 followed by the body alone"
|
|
208
204
|
);
|
|
209
205
|
assert_eq!(
|
|
210
206
|
Reply::Fault(Fault::new(super::super::FaultKind::Runtime, "x")).encode(),
|
|
211
|
-
vec![
|
|
212
|
-
"a fault Reply must encode as tag byte
|
|
207
|
+
vec![0x11, 0, 0, 0, 0, 1, b'x'],
|
|
208
|
+
"a fault Reply must encode as tag byte 0x11 followed by the Fault's own fixed \
|
|
213
209
|
layout, so a guest reads a refusal without reaching a payload codec"
|
|
214
210
|
);
|
|
211
|
+
assert_eq!(
|
|
212
|
+
Reply::Unsupported(Unsupported::new("x")).encode(),
|
|
213
|
+
vec![0x15, 0, 0, 0, 0, 1, b'x'],
|
|
214
|
+
"an unsupported Reply must encode as tag byte 0x15 followed by kind 0 and the \
|
|
215
|
+
message"
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// @behavior WE-068
|
|
220
|
+
#[test]
|
|
221
|
+
fn every_answer_takes_its_ok_tag_from_one_registry() {
|
|
222
|
+
let body = vec![0x2a];
|
|
223
|
+
assert_eq!(
|
|
224
|
+
[
|
|
225
|
+
Reply::Ok(body.clone()).encode()[0],
|
|
226
|
+
YieldReply::Ok(body.clone()).encode()[0],
|
|
227
|
+
super::super::CompileReply::Ok(vec![body.clone()]).encode()[0],
|
|
228
|
+
super::super::Outcome::Ok(body).encode()[0],
|
|
229
|
+
],
|
|
230
|
+
[0x10, 0x10, 0x10, 0x10],
|
|
231
|
+
"a Reply, a Yield Reply, a Compile Reply and an Outcome on their ok arm must all \
|
|
232
|
+
open with tag byte 0x10"
|
|
233
|
+
);
|
|
215
234
|
}
|
|
216
235
|
|
|
217
236
|
// @behavior WE-026
|
|
@@ -219,13 +238,18 @@ mod tests {
|
|
|
219
238
|
fn golden_layout_pins_the_yield_tags() {
|
|
220
239
|
assert_eq!(
|
|
221
240
|
YieldReply::Ok(vec![0xc0]).encode(),
|
|
222
|
-
vec![
|
|
223
|
-
"a Yield Reply carrying the block's value must encode as tag byte
|
|
241
|
+
vec![0x10, 0xc0],
|
|
242
|
+
"a Yield Reply carrying the block's value must encode as tag byte 0x10"
|
|
224
243
|
);
|
|
225
244
|
assert_eq!(
|
|
226
245
|
YieldReply::Break(vec![0xc0]).encode(),
|
|
227
|
-
vec![
|
|
228
|
-
"a Yield Reply carrying a break value must encode as tag byte
|
|
246
|
+
vec![0x12, 0xc0],
|
|
247
|
+
"a Yield Reply carrying a break value must encode as tag byte 0x12"
|
|
248
|
+
);
|
|
249
|
+
assert_eq!(
|
|
250
|
+
YieldReply::Unsupported(Unsupported::new("x")).encode(),
|
|
251
|
+
vec![0x15, 0, 0, 0, 0, 1, b'x'],
|
|
252
|
+
"a Yield Reply from a guest serving no block must encode as tag byte 0x15"
|
|
229
253
|
);
|
|
230
254
|
assert_eq!(
|
|
231
255
|
YieldReply::Error(ErrorRecord {
|
|
@@ -235,12 +259,12 @@ mod tests {
|
|
|
235
259
|
})
|
|
236
260
|
.encode(),
|
|
237
261
|
vec![
|
|
238
|
-
|
|
262
|
+
0x13, // tag: error
|
|
239
263
|
0, 0, 0, 1, b'E', // name
|
|
240
264
|
0, 0, 0, 1, b'm', // message
|
|
241
265
|
0, 0, 0, 0, // backtrace count
|
|
242
266
|
],
|
|
243
|
-
"a Yield Reply carrying a guest failure must encode as tag byte
|
|
267
|
+
"a Yield Reply carrying a guest failure must encode as tag byte 0x13 followed \
|
|
244
268
|
by an Error Record rather than a codec-encoded body"
|
|
245
269
|
);
|
|
246
270
|
}
|
|
@@ -0,0 +1,295 @@
|
|
|
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 bound to the active dispatch; the bytes are
|
|
22
|
+
/// its arguments, codec-encoded.
|
|
23
|
+
Yield(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(body.to_vec()),
|
|
41
|
+
tag::COMPILE if body.is_empty() => Downcall::Compile,
|
|
42
|
+
tag::COMPILE => return Err(DecodeError::new("a compile request carries no body")),
|
|
43
|
+
tag => Downcall::Unknown {
|
|
44
|
+
tag: unknown(tag)?,
|
|
45
|
+
body: body.to_vec(),
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn encode(&self) -> Vec<u8> {
|
|
51
|
+
let mut writer = Writer::new();
|
|
52
|
+
match self {
|
|
53
|
+
Downcall::Eval(source) => writer.u8(tag::EVAL).remainder(source),
|
|
54
|
+
Downcall::Run(run) => writer.u8(tag::RUN).remainder(&run.encode()),
|
|
55
|
+
Downcall::Yield(args) => writer.u8(tag::YIELD).remainder(args),
|
|
56
|
+
Downcall::Compile => writer.u8(tag::COMPILE),
|
|
57
|
+
Downcall::Unknown { tag, body } => writer.u8(tag.get()).remainder(body),
|
|
58
|
+
};
|
|
59
|
+
writer.into_bytes()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// A request the guest sends the host.
|
|
64
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
65
|
+
#[non_exhaustive]
|
|
66
|
+
pub enum Upcall<'a> {
|
|
67
|
+
/// Dispatch the Call to a Receiver.
|
|
68
|
+
Call(Call<'a>),
|
|
69
|
+
/// A tag the registry has not assigned, with the body it carried.
|
|
70
|
+
Unknown { tag: UnknownTag, body: &'a [u8] },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl<'a> Upcall<'a> {
|
|
74
|
+
pub fn decode(bytes: &'a [u8]) -> Result<Self, DecodeError> {
|
|
75
|
+
let mut reader = Reader::new(bytes);
|
|
76
|
+
let tag = read_tag(&mut reader)?;
|
|
77
|
+
let body = reader.remaining();
|
|
78
|
+
Ok(match tag {
|
|
79
|
+
tag::CALL => Upcall::Call(Call::decode(body)?),
|
|
80
|
+
tag => Upcall::Unknown {
|
|
81
|
+
tag: unknown(tag)?,
|
|
82
|
+
body,
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pub fn encode(&self) -> Vec<u8> {
|
|
88
|
+
let mut writer = Writer::new();
|
|
89
|
+
match self {
|
|
90
|
+
Upcall::Call(call) => writer.u8(tag::CALL).remainder(&call.encode()),
|
|
91
|
+
Upcall::Unknown { tag, body } => writer.u8(tag.get()).remainder(body),
|
|
92
|
+
};
|
|
93
|
+
writer.into_bytes()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// A request tag the registry has not assigned. Only such a tag can name
|
|
98
|
+
/// an unknown request, so an unknown request never encodes as an
|
|
99
|
+
/// operation the registry already has.
|
|
100
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
101
|
+
pub struct UnknownTag(u8);
|
|
102
|
+
|
|
103
|
+
impl UnknownTag {
|
|
104
|
+
/// `None` when the registry assigns `tag`, the invalid tag included.
|
|
105
|
+
pub fn new(tag: u8) -> Option<Self> {
|
|
106
|
+
(!tag::is_assigned(tag)).then_some(UnknownTag(tag))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub fn get(self) -> u8 {
|
|
110
|
+
self.0
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn read_tag(reader: &mut Reader<'_>) -> Result<u8, DecodeError> {
|
|
115
|
+
match reader.u8()? {
|
|
116
|
+
tag::INVALID => Err(DecodeError::new("request tag 0x00 is invalid")),
|
|
117
|
+
tag => Ok(tag),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn unknown(tag: u8) -> Result<UnknownTag, DecodeError> {
|
|
122
|
+
UnknownTag::new(tag).ok_or(DecodeError::new(
|
|
123
|
+
"request tag is assigned to the other direction or to an answer",
|
|
124
|
+
))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[cfg(test)]
|
|
128
|
+
mod tests {
|
|
129
|
+
use super::*;
|
|
130
|
+
use crate::envelope::Target;
|
|
131
|
+
|
|
132
|
+
// @behavior WE-071
|
|
133
|
+
#[test]
|
|
134
|
+
fn golden_layout_pins_each_request_tag() {
|
|
135
|
+
let run = Run {
|
|
136
|
+
entrypoint: "M".into(),
|
|
137
|
+
payload: vec![0x90],
|
|
138
|
+
};
|
|
139
|
+
let call = Call {
|
|
140
|
+
target: Target::Path("K"),
|
|
141
|
+
method: "m",
|
|
142
|
+
block_given: false,
|
|
143
|
+
payload: &[0x90],
|
|
144
|
+
};
|
|
145
|
+
assert_eq!(
|
|
146
|
+
(
|
|
147
|
+
Downcall::Eval(b"1".to_vec()).encode(),
|
|
148
|
+
Downcall::Run(run).encode(),
|
|
149
|
+
Downcall::Yield(vec![0x90]).encode(),
|
|
150
|
+
Downcall::Compile.encode(),
|
|
151
|
+
Upcall::Call(call).encode(),
|
|
152
|
+
),
|
|
153
|
+
(
|
|
154
|
+
vec![0x01, b'1'],
|
|
155
|
+
vec![0x02, 0, 0, 0, 1, b'M', 0x90],
|
|
156
|
+
vec![0x03, 0x90],
|
|
157
|
+
vec![0x08],
|
|
158
|
+
vec![
|
|
159
|
+
0x04, // tag: call
|
|
160
|
+
0, // kind: path
|
|
161
|
+
0, 0, 0, 1, b'K', // target
|
|
162
|
+
0, 0, 0, 1, b'm', // method
|
|
163
|
+
0, // block_given
|
|
164
|
+
0x90, // payload
|
|
165
|
+
],
|
|
166
|
+
),
|
|
167
|
+
"each request through Downcall and Upcall must open with its operation's tag \
|
|
168
|
+
followed by its body alone"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[test]
|
|
173
|
+
fn every_downcall_round_trips() {
|
|
174
|
+
for request in [
|
|
175
|
+
Downcall::Eval(b"1 + 1".to_vec()),
|
|
176
|
+
Downcall::Run(Run {
|
|
177
|
+
entrypoint: "Main".into(),
|
|
178
|
+
payload: vec![0x90],
|
|
179
|
+
}),
|
|
180
|
+
Downcall::Yield(vec![0x91, 0x01]),
|
|
181
|
+
Downcall::Compile,
|
|
182
|
+
Downcall::Unknown {
|
|
183
|
+
tag: UnknownTag::new(0xf0).unwrap(),
|
|
184
|
+
body: vec![0xaa, 0xbb],
|
|
185
|
+
},
|
|
186
|
+
] {
|
|
187
|
+
assert_eq!(
|
|
188
|
+
Downcall::decode(&request.encode()),
|
|
189
|
+
Ok(request),
|
|
190
|
+
"a downcall request must survive an encode and decode unchanged"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// @behavior WE-072
|
|
196
|
+
#[test]
|
|
197
|
+
fn an_unassigned_tag_reads_as_unknown_with_its_body() {
|
|
198
|
+
assert_eq!(
|
|
199
|
+
(
|
|
200
|
+
Downcall::decode(&[0x09, 0xff]),
|
|
201
|
+
Upcall::decode(&[0xf0, 0x01, 0x02])
|
|
202
|
+
),
|
|
203
|
+
(
|
|
204
|
+
Ok(Downcall::Unknown {
|
|
205
|
+
tag: UnknownTag::new(0x09).unwrap(),
|
|
206
|
+
body: vec![0xff]
|
|
207
|
+
}),
|
|
208
|
+
Ok(Upcall::Unknown {
|
|
209
|
+
tag: UnknownTag::new(0xf0).unwrap(),
|
|
210
|
+
body: &[0x01, 0x02]
|
|
211
|
+
})
|
|
212
|
+
),
|
|
213
|
+
"a request under a tag the registry has not assigned through Downcall and Upcall \
|
|
214
|
+
must read as unknown with its body intact"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// @behavior WE-073
|
|
219
|
+
#[test]
|
|
220
|
+
fn an_empty_request_is_refused() {
|
|
221
|
+
assert!(
|
|
222
|
+
Downcall::decode(&[]).is_err() && Upcall::decode(&[]).is_err(),
|
|
223
|
+
"a request of no length through Downcall and Upcall must be refused"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// @behavior WE-074
|
|
228
|
+
#[test]
|
|
229
|
+
fn a_request_under_the_invalid_tag_is_refused() {
|
|
230
|
+
assert!(
|
|
231
|
+
Downcall::decode(&[0x00, 0x01]).is_err() && Upcall::decode(&[0x00]).is_err(),
|
|
232
|
+
"a request under tag 0x00 through Downcall and Upcall must be refused"
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// @behavior WE-075
|
|
237
|
+
#[test]
|
|
238
|
+
fn a_request_under_a_tag_assigned_elsewhere_is_refused() {
|
|
239
|
+
assert_eq!(
|
|
240
|
+
(
|
|
241
|
+
Downcall::decode(&[0x04, 0x90]).is_err(),
|
|
242
|
+
Downcall::decode(&[0x10, 0x90]).is_err(),
|
|
243
|
+
Upcall::decode(&[0x01, b'1']).is_err(),
|
|
244
|
+
Upcall::decode(&[0x15, 0x00]).is_err(),
|
|
245
|
+
),
|
|
246
|
+
(true, true, true, true),
|
|
247
|
+
"a request under the other direction's tag or an answer's arm through Downcall and \
|
|
248
|
+
Upcall must be refused rather than read as unknown"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// @behavior WE-084
|
|
253
|
+
#[test]
|
|
254
|
+
fn a_compile_request_carrying_a_body_is_refused() {
|
|
255
|
+
assert!(
|
|
256
|
+
Downcall::decode(&[0x08, 0x90]).is_err(),
|
|
257
|
+
"a compile request with bytes after its tag through Downcall::decode must be \
|
|
258
|
+
refused, since the operation reads its input from the Preamble"
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// @behavior WE-076
|
|
263
|
+
#[test]
|
|
264
|
+
fn an_unknown_tag_cannot_be_made_from_an_assigned_one() {
|
|
265
|
+
assert_eq!(
|
|
266
|
+
[0x00, 0x01, 0x04, 0x08, 0x10, 0x15].map(UnknownTag::new),
|
|
267
|
+
[None; 6],
|
|
268
|
+
"an assigned tag or tag 0x00 through UnknownTag::new must be refused, so an \
|
|
269
|
+
unknown request never encodes as a known operation"
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#[test]
|
|
274
|
+
fn every_upcall_round_trips() {
|
|
275
|
+
let call = Call {
|
|
276
|
+
target: Target::Handle(7),
|
|
277
|
+
method: "m",
|
|
278
|
+
block_given: true,
|
|
279
|
+
payload: &[0x90],
|
|
280
|
+
};
|
|
281
|
+
for request in [
|
|
282
|
+
Upcall::Call(call),
|
|
283
|
+
Upcall::Unknown {
|
|
284
|
+
tag: UnknownTag::new(0xf1).unwrap(),
|
|
285
|
+
body: &[0xaa],
|
|
286
|
+
},
|
|
287
|
+
] {
|
|
288
|
+
assert_eq!(
|
|
289
|
+
Upcall::decode(&request.encode()),
|
|
290
|
+
Ok(request),
|
|
291
|
+
"an upcall request must survive an encode and decode unchanged"
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -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
|
+
}
|