kobako 0.26.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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +103 -0
  4. data/Cargo.lock +117 -140
  5. data/README.md +156 -118
  6. data/ROADMAP.md +23 -22
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +38 -0
  9. data/crates/kobako-runtime/Cargo.toml +2 -2
  10. data/crates/kobako-runtime/README.md +10 -12
  11. data/crates/kobako-runtime/src/error.rs +8 -9
  12. data/crates/kobako-runtime/src/lib.rs +1 -1
  13. data/crates/kobako-runtime/src/profile.rs +1 -1
  14. data/crates/kobako-runtime/src/runtime.rs +31 -17
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -10
  16. data/crates/kobako-runtime/src/yielder.rs +9 -6
  17. data/crates/kobako-transport/CHANGELOG.md +35 -0
  18. data/crates/kobako-transport/Cargo.toml +1 -1
  19. data/crates/kobako-transport/README.md +43 -27
  20. data/crates/kobako-transport/src/abi.rs +11 -14
  21. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  22. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  23. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  24. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  25. data/crates/kobako-transport/src/envelope/reply.rs +77 -53
  26. data/crates/kobako-transport/src/envelope/request.rs +295 -0
  27. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  28. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  29. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  30. data/crates/kobako-transport/src/envelope.rs +19 -12
  31. data/crates/kobako-transport/src/lib.rs +0 -2
  32. data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
  33. data/crates/kobako-wasmtime/Cargo.toml +7 -5
  34. data/crates/kobako-wasmtime/README.md +8 -10
  35. data/crates/kobako-wasmtime/src/abi.rs +5 -8
  36. data/crates/kobako-wasmtime/src/ambient.rs +2 -2
  37. data/crates/kobako-wasmtime/src/cache.rs +25 -59
  38. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  39. data/crates/kobako-wasmtime/src/config.rs +11 -14
  40. data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
  41. data/crates/kobako-wasmtime/src/driver.rs +133 -86
  42. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  43. data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
  44. data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
  45. data/crates/kobako-wasmtime/src/invocation.rs +55 -372
  46. data/crates/kobako-wasmtime/src/io.rs +286 -0
  47. data/crates/kobako-wasmtime/src/lib.rs +3 -24
  48. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  49. data/crates/kobako-wasmtime/src/trap.rs +32 -45
  50. data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
  51. data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
  52. data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
  53. data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
  54. data/data/kobako.wasm +0 -0
  55. data/ext/kobako/Cargo.toml +2 -2
  56. data/ext/kobako/src/runtime/bridge.rs +38 -47
  57. data/ext/kobako/src/runtime/errors.rs +26 -32
  58. data/ext/kobako/src/runtime/gvl.rs +4 -12
  59. data/ext/kobako/src/runtime.rs +169 -212
  60. data/lib/kobako/capture.rb +1 -4
  61. data/lib/kobako/catalog/extensions.rb +21 -59
  62. data/lib/kobako/catalog/flags.rb +41 -0
  63. data/lib/kobako/catalog/handles.rb +7 -54
  64. data/lib/kobako/catalog/services.rb +7 -61
  65. data/lib/kobako/catalog/snippets.rb +42 -66
  66. data/lib/kobako/catalog.rb +2 -10
  67. data/lib/kobako/codec/decoder.rb +9 -28
  68. data/lib/kobako/codec/encoder.rb +3 -22
  69. data/lib/kobako/codec/error.rb +6 -27
  70. data/lib/kobako/codec/ext_types.rb +72 -94
  71. data/lib/kobako/codec/handle_walk.rb +65 -130
  72. data/lib/kobako/codec/nesting.rb +3 -8
  73. data/lib/kobako/codec/state.rb +4 -10
  74. data/lib/kobako/codec/utils.rb +6 -34
  75. data/lib/kobako/codec.rb +3 -22
  76. data/lib/kobako/constant_name.rb +8 -0
  77. data/lib/kobako/context.rb +51 -112
  78. data/lib/kobako/errors.rb +51 -127
  79. data/lib/kobako/execution.rb +16 -21
  80. data/lib/kobako/extension.rb +23 -40
  81. data/lib/kobako/handle.rb +7 -12
  82. data/lib/kobako/outcome.rb +51 -80
  83. data/lib/kobako/payload/arguments.rb +1 -16
  84. data/lib/kobako/payload.rb +4 -12
  85. data/lib/kobako/pool.rb +23 -53
  86. data/lib/kobako/runtime.rb +7 -21
  87. data/lib/kobako/sandbox.rb +96 -133
  88. data/lib/kobako/sandbox_options.rb +38 -84
  89. data/lib/kobako/snippet/binary.rb +1 -16
  90. data/lib/kobako/snippet/source.rb +1 -16
  91. data/lib/kobako/snippet.rb +1 -9
  92. data/lib/kobako/transport/call.rb +1 -14
  93. data/lib/kobako/transport/dispatcher.rb +112 -195
  94. data/lib/kobako/transport/error.rb +6 -20
  95. data/lib/kobako/transport/exposure.rb +5 -10
  96. data/lib/kobako/transport/reflection.rb +39 -50
  97. data/lib/kobako/transport/run.rb +17 -71
  98. data/lib/kobako/transport/yielder.rb +21 -73
  99. data/lib/kobako/transport.rb +3 -15
  100. data/lib/kobako/unresolved.rb +4 -11
  101. data/lib/kobako/usage.rb +2 -6
  102. data/lib/kobako/version.rb +1 -1
  103. data/sig/kobako/catalog/extensions.rbs +2 -2
  104. data/sig/kobako/catalog/flags.rbs +17 -0
  105. data/sig/kobako/catalog/services.rbs +0 -6
  106. data/sig/kobako/catalog/snippets.rbs +4 -4
  107. data/sig/kobako/codec/ext_types.rbs +10 -8
  108. data/sig/kobako/codec/handle_walk.rbs +9 -7
  109. data/sig/kobako/codec/utils.rbs +2 -2
  110. data/sig/kobako/codec.rbs +2 -2
  111. data/sig/kobako/constant_name.rbs +3 -0
  112. data/sig/kobako/context.rbs +2 -3
  113. data/sig/kobako/errors.rbs +6 -0
  114. data/sig/kobako/outcome.rbs +6 -8
  115. data/sig/kobako/runtime.rbs +12 -8
  116. data/sig/kobako/sandbox.rbs +4 -0
  117. data/sig/kobako/sandbox_options.rbs +2 -6
  118. data/sig/kobako/transport/dispatcher.rbs +14 -18
  119. data/sig/kobako/transport/reflection.rbs +5 -3
  120. data/sig/kobako/transport/run.rbs +1 -3
  121. data/sig/kobako/transport/yielder.rbs +3 -3
  122. data/sig/kobako/transport.rbs +7 -6
  123. metadata +16 -4
  124. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  125. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  126. data/rust-toolchain.toml +0 -11
@@ -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
- let tag = reader.u8()?;
33
- match tag {
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
- TAG_FAULT => Ok(Reply::Fault(Fault::read(&mut reader)?)),
38
- _ => Err(DecodeError::new("Reply tag must be 0 (ok) or 1 (fault)")),
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(TAG_OK).remainder(body);
43
+ writer.u8(tag::OK).remainder(body);
47
44
  }
48
45
  Reply::Fault(fault) => {
49
- writer.u8(TAG_FAULT);
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
- let tag = reader.u8()?;
75
- match tag {
76
- YIELD_OK => Ok(YieldReply::Ok(reader.remaining().to_vec())),
77
- YIELD_BREAK => Ok(YieldReply::Break(reader.remaining().to_vec())),
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
- YIELD_RESERVED => Err(DecodeError::new("Yield Reply tag 0x03 is reserved")),
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(YIELD_OK).remainder(body);
91
+ writer.u8(tag::OK).remainder(body);
93
92
  }
94
93
  YieldReply::Break(body) => {
95
- writer.u8(YIELD_BREAK).remainder(body);
94
+ writer.u8(tag::BREAK).remainder(body);
96
95
  }
97
96
  YieldReply::Error(record) => {
98
- writer.u8(YIELD_ERROR);
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 both_reply_arms_round_trip() {
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 both arms"
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(&[9, 0]).is_err(),
145
- "a Reply tag that is neither ok nor fault must be rejected as a wire violation"
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
- assert!(
193
- YieldReply::decode(&[0x7f]).is_err(),
194
- "a Yield Reply tag outside the live set must be rejected as a wire violation"
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![0, 0x2a],
207
- "a successful Reply must encode as tag byte 0 followed by the body alone"
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![1, 0, 0, 0, 0, 1, b'x'],
212
- "a fault Reply must encode as tag byte 1 followed by the Fault's own fixed \
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![0x01, 0xc0],
223
- "a Yield Reply carrying the block's value must encode as tag byte 0x01"
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![0x02, 0xc0],
228
- "a Yield Reply carrying a break value must encode as tag byte 0x02"
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
- 0x04, // tag: error
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 0x04 followed \
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 rides the command buffer rather than a stdin
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
+ }