kobako 0.19.0 → 0.21.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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +127 -0
  4. data/Cargo.lock +15 -5
  5. data/README.md +96 -44
  6. data/ROADMAP.md +1 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +44 -0
  8. data/crates/kobako-runtime/Cargo.toml +8 -1
  9. data/crates/kobako-runtime/README.md +1 -1
  10. data/crates/kobako-runtime/src/dispatch.rs +17 -11
  11. data/crates/kobako-runtime/src/error.rs +38 -6
  12. data/crates/kobako-runtime/src/lib.rs +13 -2
  13. data/crates/kobako-runtime/src/profile.rs +8 -0
  14. data/crates/kobako-runtime/src/runtime.rs +7 -2
  15. data/crates/kobako-runtime/src/snapshot.rs +13 -4
  16. data/crates/kobako-runtime/src/yielder.rs +4 -4
  17. data/crates/kobako-transport/CHANGELOG.md +31 -0
  18. data/crates/kobako-transport/Cargo.toml +27 -0
  19. data/crates/kobako-transport/README.md +43 -0
  20. data/crates/kobako-transport/src/abi.rs +89 -0
  21. data/crates/kobako-transport/src/envelope/bytes.rs +234 -0
  22. data/crates/kobako-transport/src/envelope/call.rs +222 -0
  23. data/crates/kobako-transport/src/envelope/error_record.rs +101 -0
  24. data/crates/kobako-transport/src/envelope/fault.rs +171 -0
  25. data/crates/kobako-transport/src/envelope/invocation_frames.rs +232 -0
  26. data/crates/kobako-transport/src/envelope/outcome.rs +261 -0
  27. data/crates/kobako-transport/src/envelope/reply.rs +240 -0
  28. data/crates/kobako-transport/src/envelope/run.rs +85 -0
  29. data/crates/kobako-transport/src/envelope.rs +73 -0
  30. data/crates/kobako-transport/src/lib.rs +21 -0
  31. data/crates/kobako-wasmtime/CHANGELOG.md +30 -0
  32. data/crates/kobako-wasmtime/Cargo.toml +5 -2
  33. data/crates/kobako-wasmtime/README.md +1 -1
  34. data/crates/kobako-wasmtime/src/config.rs +19 -12
  35. data/crates/kobako-wasmtime/src/dispatch.rs +15 -8
  36. data/crates/kobako-wasmtime/src/driver.rs +8 -27
  37. data/crates/kobako-wasmtime/src/frames.rs +11 -9
  38. data/crates/kobako-wasmtime/src/guest_mem.rs +7 -15
  39. data/crates/kobako-wasmtime/src/instance_pre.rs +2 -2
  40. data/crates/kobako-wasmtime/src/invocation.rs +1 -1
  41. data/crates/kobako-wasmtime/src/lib.rs +1 -1
  42. data/data/kobako.wasm +0 -0
  43. data/ext/kobako/Cargo.toml +7 -2
  44. data/ext/kobako/src/runtime/bridge.rs +103 -40
  45. data/ext/kobako/src/runtime/errors.rs +13 -5
  46. data/ext/kobako/src/runtime/gvl.rs +114 -0
  47. data/ext/kobako/src/runtime.rs +307 -207
  48. data/lib/kobako/catalog/extensions.rb +50 -43
  49. data/lib/kobako/catalog/handles.rb +7 -16
  50. data/lib/kobako/catalog/services.rb +24 -46
  51. data/lib/kobako/catalog/snippets.rb +16 -33
  52. data/lib/kobako/codec/decoder.rb +10 -13
  53. data/lib/kobako/codec/encoder.rb +7 -7
  54. data/lib/kobako/codec/error.rb +6 -6
  55. data/lib/kobako/codec/ext_types.rb +12 -72
  56. data/lib/kobako/codec/handle_walk.rb +6 -6
  57. data/lib/kobako/codec/state.rb +4 -56
  58. data/lib/kobako/codec/utils.rb +4 -4
  59. data/lib/kobako/codec.rb +11 -21
  60. data/lib/kobako/context.rb +208 -0
  61. data/lib/kobako/errors.rb +59 -16
  62. data/lib/kobako/execution.rb +52 -0
  63. data/lib/kobako/extension.rb +24 -10
  64. data/lib/kobako/outcome.rb +71 -128
  65. data/lib/kobako/payload/arguments.rb +59 -0
  66. data/lib/kobako/payload.rb +19 -0
  67. data/lib/kobako/pool.rb +1 -7
  68. data/lib/kobako/sandbox.rb +51 -191
  69. data/lib/kobako/sandbox_options.rb +24 -3
  70. data/lib/kobako/snippet/binary.rb +3 -4
  71. data/lib/kobako/snippet/source.rb +5 -6
  72. data/lib/kobako/snippet.rb +2 -2
  73. data/lib/kobako/transport/call.rb +21 -0
  74. data/lib/kobako/transport/dispatcher.rb +94 -142
  75. data/lib/kobako/transport/error.rb +21 -5
  76. data/lib/kobako/transport/reflection.rb +82 -0
  77. data/lib/kobako/transport/run.rb +25 -33
  78. data/lib/kobako/transport/yielder.rb +27 -36
  79. data/lib/kobako/transport.rb +11 -8
  80. data/lib/kobako/unresolved.rb +17 -0
  81. data/lib/kobako/usage.rb +7 -7
  82. data/lib/kobako/version.rb +1 -1
  83. data/lib/kobako.rb +1 -0
  84. data/release-please-config.json +47 -2
  85. data/sig/kobako/catalog/extensions.rbs +4 -6
  86. data/sig/kobako/catalog/handles.rbs +0 -2
  87. data/sig/kobako/catalog/services.rbs +4 -4
  88. data/sig/kobako/catalog/snippets.rbs +4 -2
  89. data/sig/kobako/codec/error.rbs +4 -4
  90. data/sig/kobako/codec/ext_types.rbs +0 -7
  91. data/sig/kobako/codec/state.rbs +0 -4
  92. data/sig/kobako/codec.rbs +4 -5
  93. data/sig/kobako/context.rbs +36 -0
  94. data/sig/kobako/errors.rbs +27 -6
  95. data/sig/kobako/execution.rbs +24 -0
  96. data/sig/kobako/extension.rbs +4 -2
  97. data/sig/kobako/outcome.rbs +12 -13
  98. data/sig/kobako/payload/arguments.rbs +26 -0
  99. data/sig/kobako/payload.rbs +4 -0
  100. data/sig/kobako/pool.rbs +0 -2
  101. data/sig/kobako/runtime.rbs +44 -14
  102. data/sig/kobako/sandbox.rbs +5 -24
  103. data/sig/kobako/sandbox_options.rbs +9 -2
  104. data/sig/kobako/snippet/binary.rbs +1 -1
  105. data/sig/kobako/snippet/source.rbs +1 -1
  106. data/sig/kobako/transport/call.rbs +21 -0
  107. data/sig/kobako/transport/dispatcher.rbs +9 -11
  108. data/sig/kobako/transport/error.rbs +11 -0
  109. data/sig/kobako/transport/reflection.rbs +15 -0
  110. data/sig/kobako/transport/run.rbs +1 -1
  111. data/sig/kobako/transport/yielder.rbs +2 -2
  112. data/sig/kobako/transport.rbs +8 -2
  113. data/sig/kobako/unresolved.rbs +4 -0
  114. metadata +30 -11
  115. data/lib/kobako/fault.rb +0 -38
  116. data/lib/kobako/outcome/panic.rb +0 -42
  117. data/lib/kobako/transport/request.rb +0 -76
  118. data/lib/kobako/transport/response.rb +0 -83
  119. data/lib/kobako/transport/yield.rb +0 -94
  120. data/sig/kobako/fault.rbs +0 -19
  121. data/sig/kobako/outcome/panic.rbs +0 -34
  122. data/sig/kobako/transport/request.rbs +0 -29
  123. data/sig/kobako/transport/response.rbs +0 -33
  124. data/sig/kobako/transport/yield.rbs +0 -34
@@ -0,0 +1,234 @@
1
+ //! Byte primitives for the core envelope: unsigned big-endian integers,
2
+ //! length-prefixed byte strings, counted lists, and the trailing remainder.
3
+ //!
4
+ //! `Reader` borrows rather than copies, so a decoded envelope's payload is
5
+ //! a view into the caller's buffer and reaches a frontend without a second
6
+ //! allocation.
7
+
8
+ use super::DecodeError;
9
+
10
+ const MALFORMED: DecodeError = DecodeError::new("core envelope ended before its declared length");
11
+
12
+ /// Cursor over one message. Every read either advances past a complete
13
+ /// field or fails, so a truncated message can never be mistaken for a
14
+ /// short one.
15
+ pub(crate) struct Reader<'a> {
16
+ bytes: &'a [u8],
17
+ pos: usize,
18
+ }
19
+
20
+ impl<'a> Reader<'a> {
21
+ pub(crate) fn new(bytes: &'a [u8]) -> Self {
22
+ Reader { bytes, pos: 0 }
23
+ }
24
+
25
+ pub(crate) fn u8(&mut self) -> Result<u8, DecodeError> {
26
+ let byte = *self.bytes.get(self.pos).ok_or(MALFORMED)?;
27
+ self.pos += 1;
28
+ Ok(byte)
29
+ }
30
+
31
+ pub(crate) fn u32(&mut self) -> Result<u32, DecodeError> {
32
+ let end = self.pos.checked_add(4).ok_or(MALFORMED)?;
33
+ let slice = self.bytes.get(self.pos..end).ok_or(MALFORMED)?;
34
+ self.pos = end;
35
+ // The slice is exactly 4 bytes, so the conversion cannot fail.
36
+ Ok(u32::from_be_bytes(slice.try_into().unwrap()))
37
+ }
38
+
39
+ /// A `u32` length followed by that many bytes.
40
+ pub(crate) fn bytes(&mut self) -> Result<&'a [u8], DecodeError> {
41
+ let len = self.u32()? as usize;
42
+ let end = self.pos.checked_add(len).ok_or(MALFORMED)?;
43
+ let slice = self.bytes.get(self.pos..end).ok_or(MALFORMED)?;
44
+ self.pos = end;
45
+ Ok(slice)
46
+ }
47
+
48
+ /// A byte string carrying UTF-8. Every text field in the envelope is
49
+ /// validated at decode so a frontend receives `&str` without repeating
50
+ /// the check.
51
+ pub(crate) fn text(&mut self) -> Result<&'a str, DecodeError> {
52
+ core::str::from_utf8(self.bytes()?)
53
+ .map_err(|_| DecodeError::new("core envelope text field is not valid UTF-8"))
54
+ }
55
+
56
+ /// A `u32` count followed by that many byte strings, each carrying
57
+ /// UTF-8. Every list the envelope defines is a list of names, so this
58
+ /// is the only list shape a reader needs.
59
+ ///
60
+ /// The elements are copied rather than borrowed: a binding's path and
61
+ /// a panic's `available` names outlive the frame buffer they arrive in.
62
+ pub(crate) fn text_list(&mut self) -> Result<Vec<String>, DecodeError> {
63
+ let count = self.u32()? as usize;
64
+ // A count larger than the bytes left cannot be satisfied; refusing
65
+ // it here bounds the allocation below by the message size.
66
+ if count > self.remaining().len() {
67
+ return Err(MALFORMED);
68
+ }
69
+ let mut out = Vec::with_capacity(count);
70
+ for _ in 0..count {
71
+ out.push(self.text()?.to_owned());
72
+ }
73
+ Ok(out)
74
+ }
75
+
76
+ /// Everything not yet consumed. The trailing field of an envelope that
77
+ /// carries one, so its extent comes from the transport rather than a
78
+ /// repeated length.
79
+ pub(crate) fn remaining(&self) -> &'a [u8] {
80
+ &self.bytes[self.pos..]
81
+ }
82
+
83
+ /// Refuse anything left over. Used by envelopes whose last field is
84
+ /// self-delimiting, where trailing bytes signal a framing desync.
85
+ pub(crate) fn finish(self) -> Result<(), DecodeError> {
86
+ if self.pos == self.bytes.len() {
87
+ Ok(())
88
+ } else {
89
+ Err(DecodeError::new(
90
+ "core envelope carries bytes past its last field",
91
+ ))
92
+ }
93
+ }
94
+ }
95
+
96
+ /// Accumulator for one message.
97
+ #[derive(Default)]
98
+ pub(crate) struct Writer {
99
+ out: Vec<u8>,
100
+ }
101
+
102
+ impl Writer {
103
+ pub(crate) fn new() -> Self {
104
+ Writer::default()
105
+ }
106
+
107
+ pub(crate) fn u8(&mut self, byte: u8) -> &mut Self {
108
+ self.out.push(byte);
109
+ self
110
+ }
111
+
112
+ pub(crate) fn u32(&mut self, value: u32) -> &mut Self {
113
+ self.out.extend_from_slice(&value.to_be_bytes());
114
+ self
115
+ }
116
+
117
+ pub(crate) fn bytes(&mut self, bytes: &[u8]) -> &mut Self {
118
+ self.u32(bytes.len() as u32);
119
+ self.out.extend_from_slice(bytes);
120
+ self
121
+ }
122
+
123
+ /// The trailing field: written without a length, since the transport
124
+ /// already carries the message's extent.
125
+ pub(crate) fn remainder(&mut self, bytes: &[u8]) -> &mut Self {
126
+ self.out.extend_from_slice(bytes);
127
+ self
128
+ }
129
+
130
+ pub(crate) fn list<S: AsRef<[u8]>>(&mut self, items: &[S]) -> &mut Self {
131
+ self.u32(items.len() as u32);
132
+ for item in items {
133
+ self.bytes(item.as_ref());
134
+ }
135
+ self
136
+ }
137
+
138
+ pub(crate) fn into_bytes(self) -> Vec<u8> {
139
+ self.out
140
+ }
141
+ }
142
+
143
+ #[cfg(test)]
144
+ mod tests {
145
+ use super::*;
146
+
147
+ #[test]
148
+ fn round_trips_every_primitive() {
149
+ let mut w = Writer::new();
150
+ w.u8(7)
151
+ .u32(0xdead_beef)
152
+ .bytes(b"kv")
153
+ .list(&[&b"a"[..], &b"bc"[..]]);
154
+ w.remainder(b"tail");
155
+ let encoded = w.into_bytes();
156
+
157
+ let mut r = Reader::new(&encoded);
158
+ assert_eq!(r.u8(), Ok(7), "a u8 must round-trip through the writer");
159
+ assert_eq!(r.u32(), Ok(0xdead_beef), "a u32 must round-trip big-endian");
160
+ assert_eq!(
161
+ r.bytes(),
162
+ Ok(&b"kv"[..]),
163
+ "a length-prefixed byte string must round-trip"
164
+ );
165
+ assert_eq!(
166
+ r.text_list(),
167
+ Ok(vec!["a".to_owned(), "bc".to_owned()]),
168
+ "a counted list must round-trip element for element"
169
+ );
170
+ assert_eq!(
171
+ r.remaining(),
172
+ b"tail",
173
+ "the trailing field must be everything the prefixed fields left"
174
+ );
175
+ }
176
+
177
+ #[test]
178
+ fn u32_is_big_endian_on_the_wire() {
179
+ let mut w = Writer::new();
180
+ w.u32(1);
181
+ assert_eq!(
182
+ w.into_bytes(),
183
+ vec![0, 0, 0, 1],
184
+ "a u32 must encode big-endian so both peers read the same number"
185
+ );
186
+ }
187
+
188
+ #[test]
189
+ fn a_length_running_past_the_end_is_refused() {
190
+ // Declares 8 bytes but supplies 2.
191
+ let bytes = [0, 0, 0, 8, b'h', b'i'];
192
+ let mut r = Reader::new(&bytes);
193
+ assert!(
194
+ r.bytes().is_err(),
195
+ "a byte string whose length overruns the message must be rejected, not truncated"
196
+ );
197
+ }
198
+
199
+ #[test]
200
+ fn a_count_larger_than_the_message_is_refused() {
201
+ // Declares 0xffff_ffff elements in a 4-byte message.
202
+ let bytes = [0xff, 0xff, 0xff, 0xff];
203
+ let mut r = Reader::new(&bytes);
204
+ assert!(
205
+ r.text_list().is_err(),
206
+ "a list count the message cannot satisfy must be rejected before any allocation"
207
+ );
208
+ }
209
+
210
+ #[test]
211
+ fn non_utf8_in_a_text_field_is_refused() {
212
+ let mut w = Writer::new();
213
+ w.bytes(&[0xff, 0xfe]);
214
+ let encoded = w.into_bytes();
215
+ let mut r = Reader::new(&encoded);
216
+ assert!(
217
+ r.text().is_err(),
218
+ "a text field carrying non-UTF-8 bytes must be rejected at decode"
219
+ );
220
+ }
221
+
222
+ #[test]
223
+ fn trailing_bytes_after_a_self_delimiting_field_are_refused() {
224
+ let mut w = Writer::new();
225
+ w.bytes(b"ok").remainder(b"!");
226
+ let encoded = w.into_bytes();
227
+ let mut r = Reader::new(&encoded);
228
+ r.bytes().unwrap();
229
+ assert!(
230
+ r.finish().is_err(),
231
+ "bytes past an envelope's last field must fail loudly as a framing desync"
232
+ );
233
+ }
234
+ }
@@ -0,0 +1,222 @@
1
+ //! The Call envelope — a message inviting the other side to run one
2
+ //! method and answer.
3
+ //!
4
+ //! Borrows from the buffer it decoded, so the payload reaches a frontend
5
+ //! as a view rather than a copy.
6
+
7
+ use super::bytes::{Reader, Writer};
8
+ use super::DecodeError;
9
+
10
+ /// Whether `target` names a bound constant or a capability Handle. The tag
11
+ /// is explicit so a side reads the routing fields without interpreting any
12
+ /// encoding but this one.
13
+ const KIND_PATH: u8 = 0;
14
+ const KIND_HANDLE: u8 = 1;
15
+
16
+ /// The object a Call is addressed to.
17
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
18
+ pub enum Target<'a> {
19
+ /// A bound constant's path — `"MyService::KV"`, or a single segment
20
+ /// like `"File"`.
21
+ Path(&'a str),
22
+ /// A capability Handle, by the id this invocation's table issued.
23
+ Handle(u32),
24
+ }
25
+
26
+ /// One dispatch invitation: where it goes, what to run, whether a block
27
+ /// came with it, and the arguments the resolved method consumes.
28
+ #[derive(Debug, Clone, PartialEq, Eq)]
29
+ pub struct Call<'a> {
30
+ pub target: Target<'a>,
31
+ pub method: &'a str,
32
+ pub block_given: bool,
33
+ pub payload: &'a [u8],
34
+ }
35
+
36
+ impl<'a> Call<'a> {
37
+ pub fn decode(bytes: &'a [u8]) -> Result<Self, DecodeError> {
38
+ let mut reader = Reader::new(bytes);
39
+ let target = match reader.u8()? {
40
+ KIND_PATH => Target::Path(reader.text()?),
41
+ KIND_HANDLE => Target::Handle(reader.u32()?),
42
+ _ => return Err(DecodeError::new("Call kind must be 0 (path) or 1 (handle)")),
43
+ };
44
+ if let Target::Handle(0) = target {
45
+ return Err(DecodeError::new(
46
+ "Call target Handle id 0 is the invalid sentinel",
47
+ ));
48
+ }
49
+ let method = reader.text()?;
50
+ let block_given = match reader.u8()? {
51
+ 0 => false,
52
+ 1 => true,
53
+ _ => return Err(DecodeError::new("Call block_given must be 0 or 1")),
54
+ };
55
+ Ok(Call {
56
+ target,
57
+ method,
58
+ block_given,
59
+ payload: reader.remaining(),
60
+ })
61
+ }
62
+
63
+ pub fn encode(&self) -> Vec<u8> {
64
+ let mut writer = Writer::new();
65
+ match self.target {
66
+ Target::Path(path) => {
67
+ writer.u8(KIND_PATH).bytes(path.as_bytes());
68
+ }
69
+ Target::Handle(id) => {
70
+ writer.u8(KIND_HANDLE).u32(id);
71
+ }
72
+ }
73
+ writer
74
+ .bytes(self.method.as_bytes())
75
+ .u8(u8::from(self.block_given))
76
+ .remainder(self.payload);
77
+ writer.into_bytes()
78
+ }
79
+ }
80
+
81
+ #[cfg(test)]
82
+ mod tests {
83
+ use super::*;
84
+
85
+ #[test]
86
+ fn a_path_target_round_trips() {
87
+ let call = Call {
88
+ target: Target::Path("MyService::KV"),
89
+ method: "get",
90
+ block_given: false,
91
+ payload: b"\x92\x90\x80",
92
+ };
93
+ let encoded = call.encode();
94
+ assert_eq!(
95
+ Call::decode(&encoded),
96
+ Ok(call),
97
+ "a constant-path Call must survive a host encode and decode unchanged"
98
+ );
99
+ }
100
+
101
+ #[test]
102
+ fn a_handle_target_round_trips() {
103
+ let call = Call {
104
+ target: Target::Handle(7),
105
+ method: "commit",
106
+ block_given: true,
107
+ payload: b"",
108
+ };
109
+ let encoded = call.encode();
110
+ assert_eq!(
111
+ Call::decode(&encoded),
112
+ Ok(call),
113
+ "a Handle-targeted Call must survive a host encode and decode unchanged"
114
+ );
115
+ }
116
+
117
+ #[test]
118
+ fn the_payload_is_borrowed_not_interpreted() {
119
+ // Bytes that are not valid msgpack: the envelope layer must carry
120
+ // them through untouched, since the payload's encoding is not its
121
+ // concern.
122
+ let call = Call {
123
+ target: Target::Path("S"),
124
+ method: "m",
125
+ block_given: false,
126
+ payload: &[0xc1, 0xff, 0x00],
127
+ };
128
+ let encoded = call.encode();
129
+ assert_eq!(
130
+ Call::decode(&encoded).map(|c| c.payload.to_vec()),
131
+ Ok(vec![0xc1, 0xff, 0x00]),
132
+ "a payload the envelope cannot parse must still cross unchanged"
133
+ );
134
+ }
135
+
136
+ #[test]
137
+ fn golden_layout_pins_the_path_kind_and_field_order() {
138
+ let call = Call {
139
+ target: Target::Path("S"),
140
+ method: "m",
141
+ block_given: true,
142
+ payload: b"\x01",
143
+ };
144
+ assert_eq!(
145
+ call.encode(),
146
+ vec![
147
+ 0, // kind: path
148
+ 0, 0, 0, 1, b'S', // target
149
+ 0, 0, 0, 1, b'm', // method
150
+ 1, // block_given
151
+ 0x01, // payload remainder
152
+ ],
153
+ "a constant-path Call must encode as kind byte 0 followed by target, method, \
154
+ block flag, and payload in that order"
155
+ );
156
+ }
157
+
158
+ #[test]
159
+ fn golden_layout_pins_the_handle_kind_and_its_bare_id() {
160
+ let call = Call {
161
+ target: Target::Handle(0x0102_0304),
162
+ method: "m",
163
+ block_given: false,
164
+ payload: b"",
165
+ };
166
+ assert_eq!(
167
+ call.encode(),
168
+ vec![
169
+ 1, // kind: handle
170
+ 1, 2, 3, 4, // target: the id alone, with no length prefix
171
+ 0, 0, 0, 1, b'm', // method
172
+ 0, // block_given
173
+ ],
174
+ "a Handle-targeted Call must encode as kind byte 1 followed by the bare \
175
+ big-endian id, not a length-prefixed byte string"
176
+ );
177
+ }
178
+
179
+ #[test]
180
+ fn an_unknown_kind_is_refused() {
181
+ let bytes = [9u8, 0, 0, 0, 0];
182
+ assert!(
183
+ Call::decode(&bytes).is_err(),
184
+ "a Call kind that is neither path nor handle must be rejected as a wire violation"
185
+ );
186
+ }
187
+
188
+ #[test]
189
+ fn handle_id_zero_is_refused() {
190
+ let call_bytes = {
191
+ let mut w = Writer::new();
192
+ w.u8(KIND_HANDLE).u32(0).bytes(b"m").u8(0);
193
+ w.into_bytes()
194
+ };
195
+ assert!(
196
+ Call::decode(&call_bytes).is_err(),
197
+ "the invalid-sentinel Handle id must be rejected in the target position"
198
+ );
199
+ }
200
+
201
+ #[test]
202
+ fn a_non_boolean_block_flag_is_refused() {
203
+ let bytes = {
204
+ let mut w = Writer::new();
205
+ w.u8(KIND_PATH).bytes(b"S").bytes(b"m").u8(2);
206
+ w.into_bytes()
207
+ };
208
+ assert!(
209
+ Call::decode(&bytes).is_err(),
210
+ "a block_given byte that is neither 0 nor 1 must be rejected rather than coerced"
211
+ );
212
+ }
213
+
214
+ #[test]
215
+ fn a_truncated_call_is_refused() {
216
+ let bytes = [KIND_PATH, 0, 0, 0, 4, b'a'];
217
+ assert!(
218
+ Call::decode(&bytes).is_err(),
219
+ "a Call whose target length overruns the message must be rejected, not truncated"
220
+ );
221
+ }
222
+ }
@@ -0,0 +1,101 @@
1
+ //! The guest's report that something it was running raised.
2
+ //!
3
+ //! A block failure and an invocation failure carry the same three fields,
4
+ //! so the host re-raises from either without consulting a payload codec.
5
+ //! Distinct from a Fault, which travels the other way and is categorized
6
+ //! by one of three reserved type names the guest maps to a proxy-side
7
+ //! error.
8
+
9
+ use super::bytes::{Reader, Writer};
10
+ use super::DecodeError;
11
+
12
+ /// The error's name, message, and backtrace as the guest saw them. `name`
13
+ /// rather than `class` because a guest need not be object-oriented to have
14
+ /// named the error it is reporting.
15
+ #[derive(Debug, Clone, PartialEq, Eq, Default)]
16
+ pub struct ErrorRecord {
17
+ pub name: String,
18
+ pub message: String,
19
+ pub backtrace: Vec<String>,
20
+ }
21
+
22
+ impl ErrorRecord {
23
+ pub(crate) fn read(reader: &mut Reader<'_>) -> Result<Self, DecodeError> {
24
+ Ok(ErrorRecord {
25
+ name: reader.text()?.to_owned(),
26
+ message: reader.text()?.to_owned(),
27
+ backtrace: reader.text_list()?,
28
+ })
29
+ }
30
+
31
+ pub(crate) fn write(&self, writer: &mut Writer) {
32
+ writer
33
+ .bytes(self.name.as_bytes())
34
+ .bytes(self.message.as_bytes())
35
+ .list(&self.backtrace);
36
+ }
37
+ }
38
+
39
+ #[cfg(test)]
40
+ mod tests {
41
+ use super::*;
42
+
43
+ fn sample() -> ErrorRecord {
44
+ ErrorRecord {
45
+ name: "RuntimeError".into(),
46
+ message: "boom".into(),
47
+ backtrace: vec!["(eval):1".into()],
48
+ }
49
+ }
50
+
51
+ #[test]
52
+ fn round_trips_through_the_wire() {
53
+ let mut w = Writer::new();
54
+ sample().write(&mut w);
55
+ let encoded = w.into_bytes();
56
+ let mut r = Reader::new(&encoded);
57
+ assert_eq!(
58
+ ErrorRecord::read(&mut r),
59
+ Ok(sample()),
60
+ "an Error Record must survive a host encode and decode unchanged"
61
+ );
62
+ }
63
+
64
+ #[test]
65
+ fn an_empty_backtrace_is_legal() {
66
+ let record = ErrorRecord {
67
+ backtrace: Vec::new(),
68
+ ..sample()
69
+ };
70
+ let mut w = Writer::new();
71
+ record.write(&mut w);
72
+ let encoded = w.into_bytes();
73
+ let mut r = Reader::new(&encoded);
74
+ assert_eq!(
75
+ ErrorRecord::read(&mut r),
76
+ Ok(record),
77
+ "a guest failure with no backtrace must round-trip as an empty list, not a decode error"
78
+ );
79
+ }
80
+
81
+ #[test]
82
+ fn golden_layout_is_name_then_message_then_backtrace() {
83
+ let record = ErrorRecord {
84
+ name: "E".into(),
85
+ message: "m".into(),
86
+ backtrace: vec!["b".into()],
87
+ };
88
+ let mut w = Writer::new();
89
+ record.write(&mut w);
90
+ assert_eq!(
91
+ w.into_bytes(),
92
+ vec![
93
+ 0, 0, 0, 1, b'E', // name
94
+ 0, 0, 0, 1, b'm', // message
95
+ 0, 0, 0, 1, // backtrace count
96
+ 0, 0, 0, 1, b'b',
97
+ ],
98
+ "the Error Record byte layout must stay fixed for both peers to agree"
99
+ );
100
+ }
101
+ }