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.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +241 -0
  4. data/Cargo.lock +132 -126
  5. data/README.md +240 -147
  6. data/ROADMAP.md +28 -22
  7. data/SECURITY.md +2 -2
  8. data/crates/kobako-runtime/CHANGELOG.md +117 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +16 -10
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/error.rs +8 -9
  13. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  14. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  15. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  18. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  19. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  20. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  21. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  22. data/crates/kobako-runtime/src/grant.rs +20 -0
  23. data/crates/kobako-runtime/src/lib.rs +12 -2
  24. data/crates/kobako-runtime/src/mount.rs +178 -0
  25. data/crates/kobako-runtime/src/profile.rs +4 -4
  26. data/crates/kobako-runtime/src/runtime.rs +43 -17
  27. data/crates/kobako-runtime/src/snapshot.rs +33 -15
  28. data/crates/kobako-runtime/src/yielder.rs +14 -6
  29. data/crates/kobako-transport/CHANGELOG.md +45 -0
  30. data/crates/kobako-transport/Cargo.toml +1 -1
  31. data/crates/kobako-transport/README.md +21 -12
  32. data/crates/kobako-transport/src/abi.rs +11 -14
  33. data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
  34. data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
  35. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  36. data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
  37. data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
  38. data/crates/kobako-transport/src/envelope/reply.rs +103 -61
  39. data/crates/kobako-transport/src/envelope/request.rs +305 -0
  40. data/crates/kobako-transport/src/envelope/run.rs +1 -2
  41. data/crates/kobako-transport/src/envelope/tag.rs +49 -0
  42. data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
  43. data/crates/kobako-transport/src/envelope.rs +19 -12
  44. data/crates/kobako-transport/src/lib.rs +0 -2
  45. data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
  46. data/crates/kobako-wasmtime/Cargo.toml +23 -16
  47. data/crates/kobako-wasmtime/README.md +6 -4
  48. data/crates/kobako-wasmtime/src/abi.rs +67 -36
  49. data/crates/kobako-wasmtime/src/ambient.rs +64 -5
  50. data/crates/kobako-wasmtime/src/cache.rs +204 -127
  51. data/crates/kobako-wasmtime/src/capture.rs +4 -11
  52. data/crates/kobako-wasmtime/src/config.rs +69 -15
  53. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  54. data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
  55. data/crates/kobako-wasmtime/src/driver.rs +174 -125
  56. data/crates/kobako-wasmtime/src/exports.rs +88 -34
  57. data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
  58. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  59. data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
  60. data/crates/kobako-wasmtime/src/invocation.rs +84 -412
  61. data/crates/kobako-wasmtime/src/io.rs +89 -0
  62. data/crates/kobako-wasmtime/src/lib.rs +7 -24
  63. data/crates/kobako-wasmtime/src/limiter.rs +253 -0
  64. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  65. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  66. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  67. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  68. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  69. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  70. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  71. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  72. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  73. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  74. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  75. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  76. data/crates/kobako-wasmtime/src/trap.rs +66 -59
  77. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  78. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  79. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  80. data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
  81. data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
  82. data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
  83. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  84. data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
  85. data/data/kobako.wasm +0 -0
  86. data/ext/kobako/Cargo.toml +4 -2
  87. data/ext/kobako/src/runtime/bridge.rs +177 -113
  88. data/ext/kobako/src/runtime/errors.rs +26 -32
  89. data/ext/kobako/src/runtime/escape.rs +53 -0
  90. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  91. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  92. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  93. data/ext/kobako/src/runtime/gvl.rs +11 -13
  94. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  95. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  96. data/ext/kobako/src/runtime.rs +190 -312
  97. data/lib/kobako/capture.rb +1 -4
  98. data/lib/kobako/catalog/extensions.rb +21 -59
  99. data/lib/kobako/catalog/flags.rb +29 -0
  100. data/lib/kobako/catalog/grants.rb +30 -0
  101. data/lib/kobako/catalog/handles.rb +7 -54
  102. data/lib/kobako/catalog/mount.rb +27 -0
  103. data/lib/kobako/catalog/services.rb +17 -60
  104. data/lib/kobako/catalog/snippets.rb +41 -69
  105. data/lib/kobako/catalog.rb +13 -10
  106. data/lib/kobako/codec/decoder.rb +10 -28
  107. data/lib/kobako/codec/encoder.rb +3 -22
  108. data/lib/kobako/codec/error.rb +6 -27
  109. data/lib/kobako/codec/ext_types.rb +72 -94
  110. data/lib/kobako/codec/handle_walk.rb +65 -130
  111. data/lib/kobako/codec/nesting.rb +3 -8
  112. data/lib/kobako/codec/state.rb +21 -11
  113. data/lib/kobako/codec/utils.rb +6 -34
  114. data/lib/kobako/codec/value_budget.rb +86 -0
  115. data/lib/kobako/codec.rb +10 -22
  116. data/lib/kobako/constant_name.rb +8 -0
  117. data/lib/kobako/context.rb +83 -117
  118. data/lib/kobako/errors.rb +57 -132
  119. data/lib/kobako/execution.rb +16 -21
  120. data/lib/kobako/extension.rb +23 -40
  121. data/lib/kobako/filesystem.rb +38 -0
  122. data/lib/kobako/handle.rb +7 -12
  123. data/lib/kobako/outcome.rb +51 -80
  124. data/lib/kobako/payload/arguments.rb +1 -16
  125. data/lib/kobako/payload.rb +4 -12
  126. data/lib/kobako/pool.rb +25 -55
  127. data/lib/kobako/runtime.rb +7 -21
  128. data/lib/kobako/sandbox.rb +128 -137
  129. data/lib/kobako/sandbox_options.rb +71 -90
  130. data/lib/kobako/snippet/bytecode.rb +11 -0
  131. data/lib/kobako/snippet/source.rb +1 -16
  132. data/lib/kobako/snippet.rb +2 -10
  133. data/lib/kobako/transport/block_failure.rb +35 -0
  134. data/lib/kobako/transport/call.rb +1 -14
  135. data/lib/kobako/transport/dispatcher.rb +127 -206
  136. data/lib/kobako/transport/error.rb +6 -20
  137. data/lib/kobako/transport/exposure.rb +10 -17
  138. data/lib/kobako/transport/reflection.rb +108 -63
  139. data/lib/kobako/transport/run.rb +17 -71
  140. data/lib/kobako/transport/yielder.rb +20 -94
  141. data/lib/kobako/transport.rb +4 -15
  142. data/lib/kobako/unresolved.rb +4 -11
  143. data/lib/kobako/usage.rb +2 -6
  144. data/lib/kobako/version.rb +1 -1
  145. data/lib/kobako.rb +1 -0
  146. data/sig/kobako/catalog/extensions.rbs +2 -2
  147. data/sig/kobako/catalog/flags.rbs +13 -0
  148. data/sig/kobako/catalog/grants.rbs +13 -0
  149. data/sig/kobako/catalog/mount.rbs +28 -0
  150. data/sig/kobako/catalog/services.rbs +3 -5
  151. data/sig/kobako/catalog/snippets.rbs +5 -5
  152. data/sig/kobako/catalog.rbs +1 -0
  153. data/sig/kobako/codec/ext_types.rbs +10 -8
  154. data/sig/kobako/codec/handle_walk.rbs +9 -7
  155. data/sig/kobako/codec/state.rbs +7 -1
  156. data/sig/kobako/codec/utils.rbs +2 -2
  157. data/sig/kobako/codec/value_budget.rbs +23 -0
  158. data/sig/kobako/codec.rbs +4 -2
  159. data/sig/kobako/constant_name.rbs +3 -0
  160. data/sig/kobako/context.rbs +15 -8
  161. data/sig/kobako/errors.rbs +10 -4
  162. data/sig/kobako/filesystem.rbs +88 -0
  163. data/sig/kobako/guest_yielder.rbs +9 -0
  164. data/sig/kobako/outcome.rbs +6 -8
  165. data/sig/kobako/pool.rbs +1 -1
  166. data/sig/kobako/runtime.rbs +21 -45
  167. data/sig/kobako/sandbox.rbs +11 -1
  168. data/sig/kobako/sandbox_options.rbs +25 -7
  169. data/sig/kobako/snapshot.rbs +20 -0
  170. data/sig/kobako/snippet/bytecode.rbs +12 -0
  171. data/sig/kobako/transport/block_failure.rbs +13 -0
  172. data/sig/kobako/transport/dispatcher.rbs +15 -19
  173. data/sig/kobako/transport/exposure.rbs +1 -1
  174. data/sig/kobako/transport/reflection.rbs +19 -4
  175. data/sig/kobako/transport/run.rbs +1 -3
  176. data/sig/kobako/transport/yielder.rbs +5 -9
  177. data/sig/kobako/transport.rbs +9 -8
  178. metadata +75 -10
  179. data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
  180. data/crates/kobako-wasmtime/src/frames.rs +0 -273
  181. data/lib/kobako/snippet/binary.rb +0 -26
  182. data/rust-toolchain.toml +0 -15
  183. 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
- let tag = reader.u8()?;
33
- match tag {
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
- TAG_FAULT => Ok(Reply::Fault(Fault::read(&mut reader)?)),
38
- _ => Err(DecodeError::new("Reply tag must be 0 (ok) or 1 (fault)")),
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(TAG_OK).remainder(body);
50
+ writer.u8(tag::OK).remainder(body);
47
51
  }
48
52
  Reply::Fault(fault) => {
49
- writer.u8(TAG_FAULT);
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. Carries a `Break` outcome the dispatch
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`; the bytes are the break value,
64
- /// codec-encoded. It returns to the guest rather than to host code.
65
- Break(Vec<u8>),
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
- 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 => {
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
- YIELD_RESERVED => Err(DecodeError::new("Yield Reply tag 0x03 is reserved")),
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(YIELD_OK).remainder(body);
103
+ writer.u8(tag::OK).remainder(body);
93
104
  }
94
- YieldReply::Break(body) => {
95
- writer.u8(YIELD_BREAK).remainder(body);
105
+ YieldReply::Break => {
106
+ writer.u8(tag::BREAK);
96
107
  }
97
108
  YieldReply::Error(record) => {
98
- writer.u8(YIELD_ERROR);
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 both_reply_arms_round_trip() {
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 both arms"
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(&[9, 0]).is_err(),
145
- "a Reply tag that is neither ok nor fault must be rejected as a wire violation"
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(vec![0xa4]),
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
- assert!(
193
- YieldReply::decode(&[0x7f]).is_err(),
194
- "a Yield Reply tag outside the live set must be rejected as a wire violation"
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![0, 0x2a],
207
- "a successful Reply must encode as tag byte 0 followed by the body alone"
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![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 \
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![0x01, 0xc0],
223
- "a Yield Reply carrying the block's value must encode as tag byte 0x01"
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::Break(vec![0xc0]).encode(),
227
- vec![0x02, 0xc0],
228
- "a Yield Reply carrying a break value must encode as tag byte 0x02"
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
- 0x04, // tag: error
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 0x04 followed \
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 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
+ }