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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +71 -0
  4. data/Cargo.lock +117 -140
  5. data/README.md +100 -125
  6. data/ROADMAP.md +23 -22
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +31 -0
  9. data/crates/kobako-runtime/Cargo.toml +2 -2
  10. data/crates/kobako-runtime/README.md +4 -3
  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 +28 -0
  18. data/crates/kobako-transport/Cargo.toml +1 -1
  19. data/crates/kobako-transport/README.md +21 -12
  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 +44 -0
  33. data/crates/kobako-wasmtime/Cargo.toml +7 -5
  34. data/crates/kobako-wasmtime/README.md +2 -2
  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 -15
@@ -0,0 +1,513 @@
1
+ //! The Preamble — what an invocation reads before its entry runs: the
2
+ //! paths the host bound, the snippets it preloaded, and the flags it set.
3
+ //!
4
+ //! It is a counted list of records rather than fixed positions, so a later
5
+ //! kind joins as a registry entry instead of a new position every reader
6
+ //! must expect. Each record says whether a reader that does not know its
7
+ //! kind may skip it, which keeps a restriction from lapsing silently on an
8
+ //! older Guest Binary.
9
+
10
+ use super::bytes::{Reader, Writer};
11
+ use super::{tag, DecodeError};
12
+
13
+ const SNIPPET_SOURCE: u8 = 0;
14
+ const SNIPPET_BYTECODE: u8 = 1;
15
+
16
+ const ADVISORY: u8 = 0;
17
+ const CRITICAL: u8 = 1;
18
+
19
+ /// What the host sets an invocation up with, read by the guest before any
20
+ /// entry runs. A later record kind becomes a field here, so it is built
21
+ /// from `Default` rather than a literal.
22
+ #[derive(Debug, Clone, PartialEq, Eq, Default)]
23
+ #[non_exhaustive]
24
+ pub struct Preamble {
25
+ pub bindings: Bindings,
26
+ pub snippets: Snippets,
27
+ pub flags: Vec<Flag>,
28
+ }
29
+
30
+ /// The bound constant paths the guest installs proxies from. An empty
31
+ /// list means no Service is bound.
32
+ #[derive(Debug, Clone, PartialEq, Eq, Default)]
33
+ pub struct Bindings {
34
+ pub paths: Vec<String>,
35
+ }
36
+
37
+ /// One preloaded snippet. Source carries the filename the guest compiles
38
+ /// under; bytecode does not, because its filename — when it has one — lives
39
+ /// in the bytecode's own debug section.
40
+ #[derive(Debug, Clone, PartialEq, Eq)]
41
+ pub enum Snippet {
42
+ Source { name: String, body: String },
43
+ Bytecode { body: Vec<u8> },
44
+ }
45
+
46
+ /// The preloaded snippets, in insertion order.
47
+ #[derive(Debug, Clone, PartialEq, Eq, Default)]
48
+ pub struct Snippets {
49
+ pub entries: Vec<Snippet>,
50
+ }
51
+
52
+ /// A switch the host sets for this invocation's VM, by name. An empty
53
+ /// `value` is a plain switch. A Guest Binary that does not recognise the
54
+ /// name refuses the invocation when the flag is `critical`, and skips it
55
+ /// otherwise.
56
+ #[derive(Debug, Clone, PartialEq, Eq)]
57
+ pub struct Flag {
58
+ pub name: String,
59
+ pub value: String,
60
+ pub critical: bool,
61
+ }
62
+
63
+ impl Preamble {
64
+ /// A record of a kind this reader does not know is skipped when
65
+ /// advisory and refused when critical. Bindings and snippets each
66
+ /// appear at most once; an absent one reads as empty.
67
+ pub fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
68
+ let mut reader = Reader::new(bytes);
69
+ let count = reader.u32()? as usize;
70
+ // Each record costs at least six bytes, its two leading bytes and a
71
+ // body length, so a count past the bytes left can never be
72
+ // satisfied; refusing it bounds the work.
73
+ if count > reader.remaining().len() {
74
+ return Err(DecodeError::new(
75
+ "Preamble declares more records than it holds",
76
+ ));
77
+ }
78
+ let mut bindings = None;
79
+ let mut snippets = None;
80
+ let mut flags = Vec::new();
81
+ for _ in 0..count {
82
+ let critical = match reader.u8()? {
83
+ ADVISORY => false,
84
+ CRITICAL => true,
85
+ _ => return Err(DecodeError::new("Preamble criticality must be 0 or 1")),
86
+ };
87
+ let kind = reader.u8()?;
88
+ let body = reader.bytes()?;
89
+ match kind {
90
+ tag::BINDINGS => once(&mut bindings, decode_bindings(body)?)?,
91
+ tag::SNIPPETS => once(&mut snippets, decode_snippets(body)?)?,
92
+ tag::FLAG => flags.push(decode_flag(body, critical)?),
93
+ kind if tag::is_assigned(kind) => {
94
+ return Err(DecodeError::new(
95
+ "Preamble record kind is assigned to another message",
96
+ ))
97
+ }
98
+ _ if critical => {
99
+ return Err(DecodeError::new(
100
+ "Preamble carries a critical record of a kind this reader does not know",
101
+ ))
102
+ }
103
+ _ => {}
104
+ }
105
+ }
106
+ reader.finish()?;
107
+ Ok(Preamble {
108
+ bindings: bindings.unwrap_or_default(),
109
+ snippets: snippets.unwrap_or_default(),
110
+ flags,
111
+ })
112
+ }
113
+
114
+ /// Empty bindings and snippets are left out, since an absent record
115
+ /// already reads as empty.
116
+ pub fn encode(&self) -> Vec<u8> {
117
+ let mut records: Vec<(bool, u8, Vec<u8>)> = Vec::new();
118
+ if !self.bindings.paths.is_empty() {
119
+ records.push((true, tag::BINDINGS, encode_bindings(&self.bindings)));
120
+ }
121
+ if !self.snippets.entries.is_empty() {
122
+ records.push((true, tag::SNIPPETS, encode_snippets(&self.snippets)));
123
+ }
124
+ for flag in &self.flags {
125
+ records.push((flag.critical, tag::FLAG, encode_flag(flag)));
126
+ }
127
+ let mut writer = Writer::new();
128
+ writer.u32(records.len() as u32);
129
+ for (critical, kind, body) in &records {
130
+ writer
131
+ .u8(if *critical { CRITICAL } else { ADVISORY })
132
+ .u8(*kind)
133
+ .bytes(body);
134
+ }
135
+ writer.into_bytes()
136
+ }
137
+ }
138
+
139
+ fn once<T>(slot: &mut Option<T>, value: T) -> Result<(), DecodeError> {
140
+ if slot.replace(value).is_some() {
141
+ return Err(DecodeError::new(
142
+ "Preamble repeats a record that appears once",
143
+ ));
144
+ }
145
+ Ok(())
146
+ }
147
+
148
+ fn decode_bindings(bytes: &[u8]) -> Result<Bindings, DecodeError> {
149
+ let mut reader = Reader::new(bytes);
150
+ let paths = reader.text_list()?;
151
+ reader.finish()?;
152
+ Ok(Bindings { paths })
153
+ }
154
+
155
+ fn encode_bindings(bindings: &Bindings) -> Vec<u8> {
156
+ let mut writer = Writer::new();
157
+ writer.list(&bindings.paths);
158
+ writer.into_bytes()
159
+ }
160
+
161
+ fn decode_snippets(bytes: &[u8]) -> Result<Snippets, DecodeError> {
162
+ let mut reader = Reader::new(bytes);
163
+ let count = reader.u32()? as usize;
164
+ // Each entry costs at least a kind byte and a `u32` length, so a count
165
+ // past what the bytes left can hold is refused before it sizes the
166
+ // allocation.
167
+ if count > reader.remaining().len() / (1 + size_of::<u32>()) {
168
+ return Err(DecodeError::new(
169
+ "Preamble snippets declare more entries than they hold",
170
+ ));
171
+ }
172
+ let mut entries = Vec::with_capacity(count);
173
+ for _ in 0..count {
174
+ entries.push(match reader.u8()? {
175
+ SNIPPET_SOURCE => Snippet::Source {
176
+ name: reader.text()?.to_owned(),
177
+ body: reader.text()?.to_owned(),
178
+ },
179
+ SNIPPET_BYTECODE => Snippet::Bytecode {
180
+ body: reader.bytes()?.to_vec(),
181
+ },
182
+ _ => return Err(DecodeError::new("Preamble snippet kind must be 0 or 1")),
183
+ });
184
+ }
185
+ reader.finish()?;
186
+ Ok(Snippets { entries })
187
+ }
188
+
189
+ fn encode_snippets(snippets: &Snippets) -> Vec<u8> {
190
+ let mut writer = Writer::new();
191
+ writer.u32(snippets.entries.len() as u32);
192
+ for entry in &snippets.entries {
193
+ match entry {
194
+ Snippet::Source { name, body } => {
195
+ writer
196
+ .u8(SNIPPET_SOURCE)
197
+ .bytes(name.as_bytes())
198
+ .bytes(body.as_bytes());
199
+ }
200
+ Snippet::Bytecode { body } => {
201
+ writer.u8(SNIPPET_BYTECODE).bytes(body);
202
+ }
203
+ }
204
+ }
205
+ writer.into_bytes()
206
+ }
207
+
208
+ fn decode_flag(bytes: &[u8], critical: bool) -> Result<Flag, DecodeError> {
209
+ let mut reader = Reader::new(bytes);
210
+ let name = reader.text()?.to_owned();
211
+ let value = reader.text()?.to_owned();
212
+ reader.finish()?;
213
+ Ok(Flag {
214
+ name,
215
+ value,
216
+ critical,
217
+ })
218
+ }
219
+
220
+ fn encode_flag(flag: &Flag) -> Vec<u8> {
221
+ let mut writer = Writer::new();
222
+ writer
223
+ .bytes(flag.name.as_bytes())
224
+ .bytes(flag.value.as_bytes());
225
+ writer.into_bytes()
226
+ }
227
+
228
+ #[cfg(test)]
229
+ mod tests {
230
+ use super::*;
231
+
232
+ fn flag(name: &str, value: &str, critical: bool) -> Flag {
233
+ Flag {
234
+ name: name.into(),
235
+ value: value.into(),
236
+ critical,
237
+ }
238
+ }
239
+
240
+ fn record(critical: u8, kind: u8, body: &[u8]) -> Vec<u8> {
241
+ let mut writer = Writer::new();
242
+ writer.u8(critical).u8(kind).bytes(body);
243
+ writer.into_bytes()
244
+ }
245
+
246
+ fn preamble_of(records: &[Vec<u8>]) -> Vec<u8> {
247
+ let mut bytes = (records.len() as u32).to_be_bytes().to_vec();
248
+ for record in records {
249
+ bytes.extend_from_slice(record);
250
+ }
251
+ bytes
252
+ }
253
+
254
+ // @behavior WE-050
255
+ #[test]
256
+ fn an_empty_preamble_counts_no_records() {
257
+ let encoded = Preamble::default().encode();
258
+ assert_eq!(
259
+ (encoded.as_slice(), Preamble::decode(&encoded)),
260
+ (&[0u8, 0, 0, 0][..], Ok(Preamble::default())),
261
+ "a Preamble with nothing set up through encode must write a zero count and read \
262
+ back empty"
263
+ );
264
+ }
265
+
266
+ // @behavior WE-051
267
+ #[test]
268
+ fn bindings_round_trip_every_path() {
269
+ let preamble = Preamble {
270
+ bindings: Bindings {
271
+ paths: vec!["MyService::KV".into(), "File".into()],
272
+ },
273
+ ..Preamble::default()
274
+ };
275
+ assert_eq!(
276
+ Preamble::decode(&preamble.encode()),
277
+ Ok(preamble),
278
+ "every bound path through a Preamble's encode and decode must arrive in order"
279
+ );
280
+ }
281
+
282
+ // @behavior WE-052
283
+ #[test]
284
+ fn snippets_round_trip_both_kinds_in_order() {
285
+ let preamble = Preamble {
286
+ snippets: Snippets {
287
+ entries: vec![
288
+ Snippet::Source {
289
+ name: "Helper".into(),
290
+ body: "def helper; end".into(),
291
+ },
292
+ Snippet::Bytecode {
293
+ body: vec![0x52, 0x49, 0x54, 0x45],
294
+ },
295
+ ],
296
+ },
297
+ ..Preamble::default()
298
+ };
299
+ assert_eq!(
300
+ Preamble::decode(&preamble.encode()),
301
+ Ok(preamble),
302
+ "source and bytecode snippets through a Preamble's encode and decode must arrive \
303
+ in insertion order"
304
+ );
305
+ }
306
+
307
+ // @behavior WE-053
308
+ #[test]
309
+ fn an_absent_record_reads_as_empty() {
310
+ let bytes = preamble_of(&[record(0, 0x07, &encode_flag(&flag("f", "", false)))]);
311
+ let preamble = Preamble::decode(&bytes).expect("a flag-only Preamble must decode");
312
+ assert_eq!(
313
+ (preamble.bindings, preamble.snippets),
314
+ (Bindings::default(), Snippets::default()),
315
+ "a Preamble carrying neither bindings nor snippets through decode must read both \
316
+ as empty"
317
+ );
318
+ }
319
+
320
+ // @behavior WE-054
321
+ #[test]
322
+ fn golden_layout_pins_the_record_list() {
323
+ let preamble = Preamble {
324
+ bindings: Bindings {
325
+ paths: vec!["A".into()],
326
+ },
327
+ flags: vec![flag("n", "v", false)],
328
+ ..Preamble::default()
329
+ };
330
+ assert_eq!(
331
+ preamble.encode(),
332
+ vec![
333
+ 0, 0, 0, 2, // record count
334
+ 1, 0x05, // critical, kind: bindings
335
+ 0, 0, 0, 9, // body length
336
+ 0, 0, 0, 1, // path count
337
+ 0, 0, 0, 1, b'A', //
338
+ 0, 0x07, // advisory, kind: flag
339
+ 0, 0, 0, 10, // body length
340
+ 0, 0, 0, 1, b'n', // name
341
+ 0, 0, 0, 1, b'v', // value
342
+ ],
343
+ "a Preamble through encode must write a count, then each record as its \
344
+ criticality, its kind, and its length-prefixed body"
345
+ );
346
+ }
347
+
348
+ // @behavior WE-055
349
+ #[test]
350
+ fn golden_layout_pins_the_snippet_entry_shape() {
351
+ let snippets = Snippets {
352
+ entries: vec![
353
+ Snippet::Source {
354
+ name: "N".into(),
355
+ body: "b".into(),
356
+ },
357
+ Snippet::Bytecode { body: vec![0x52] },
358
+ ],
359
+ };
360
+ assert_eq!(
361
+ encode_snippets(&snippets),
362
+ vec![
363
+ 0, 0, 0, 2, // entry count
364
+ 0, // kind: source
365
+ 0, 0, 0, 1, b'N', // name, source only
366
+ 0, 0, 0, 1, b'b', // body
367
+ 1, // kind: bytecode
368
+ 0, 0, 0, 1, 0x52, // body, no name
369
+ ],
370
+ "a source entry must encode as kind byte 0 with a name and a bytecode entry as \
371
+ kind byte 1 with none, so the two stay distinguishable by their tag alone"
372
+ );
373
+ }
374
+
375
+ // @behavior WE-056
376
+ #[test]
377
+ fn an_unknown_snippet_kind_is_refused() {
378
+ let bytes = preamble_of(&[record(1, 0x06, &[0, 0, 0, 1, 9])]);
379
+ assert!(
380
+ Preamble::decode(&bytes).is_err(),
381
+ "a snippet kind that is neither source nor bytecode through Preamble::decode must \
382
+ be refused"
383
+ );
384
+ }
385
+
386
+ // @behavior WE-057
387
+ #[test]
388
+ fn a_count_the_preamble_cannot_satisfy_is_refused() {
389
+ let records = [0xff, 0xff, 0xff, 0xff];
390
+ let snippets = preamble_of(&[record(1, 0x06, &[0xff, 0xff, 0xff, 0xff])]);
391
+ assert!(
392
+ Preamble::decode(&records).is_err() && Preamble::decode(&snippets).is_err(),
393
+ "a record or snippet count larger than what follows through Preamble::decode must \
394
+ be refused before anything is allocated for it"
395
+ );
396
+ }
397
+
398
+ // @behavior WE-058
399
+ #[test]
400
+ fn trailing_bytes_are_refused() {
401
+ let mut past_the_list = Preamble::default().encode();
402
+ past_the_list.push(0);
403
+ let past_a_body = preamble_of(&[record(1, 0x05, &[0, 0, 0, 0, 0])]);
404
+ assert!(
405
+ Preamble::decode(&past_the_list).is_err() && Preamble::decode(&past_a_body).is_err(),
406
+ "bytes past the last record or past a record body's last field through \
407
+ Preamble::decode must fail loudly as a framing desync"
408
+ );
409
+ }
410
+
411
+ // @behavior WE-077
412
+ #[test]
413
+ fn an_advisory_record_of_an_unknown_kind_is_skipped() {
414
+ let bytes = preamble_of(&[
415
+ record(0, 0x7e, b"anything"),
416
+ record(
417
+ 1,
418
+ 0x05,
419
+ &encode_bindings(&Bindings {
420
+ paths: vec!["A".into()],
421
+ }),
422
+ ),
423
+ ]);
424
+ assert_eq!(
425
+ Preamble::decode(&bytes).map(|preamble| preamble.bindings.paths),
426
+ Ok(vec!["A".to_string()]),
427
+ "an advisory record of a kind the registry has not assigned through \
428
+ Preamble::decode must be skipped, leaving the records around it intact"
429
+ );
430
+ }
431
+
432
+ // @behavior WE-078
433
+ #[test]
434
+ fn a_critical_record_of_an_unknown_kind_is_refused() {
435
+ let bytes = preamble_of(&[record(1, 0x7e, b"anything")]);
436
+ assert!(
437
+ Preamble::decode(&bytes).is_err(),
438
+ "a critical record of a kind the registry has not assigned through \
439
+ Preamble::decode must be refused"
440
+ );
441
+ }
442
+
443
+ // @behavior WE-079
444
+ #[test]
445
+ fn a_record_kind_assigned_to_another_message_is_refused() {
446
+ for kind in [0x00, 0x01, 0x10] {
447
+ let bytes = preamble_of(&[record(0, kind, b"")]);
448
+ assert!(
449
+ Preamble::decode(&bytes).is_err(),
450
+ "an advisory record of kind {kind:#04x}, which the registry assigns elsewhere, \
451
+ through Preamble::decode must be refused rather than skipped"
452
+ );
453
+ }
454
+ }
455
+
456
+ // @behavior WE-080
457
+ #[test]
458
+ fn a_repeated_record_is_refused() {
459
+ let bindings = encode_bindings(&Bindings::default());
460
+ let snippets = encode_snippets(&Snippets::default());
461
+ for (kind, body) in [(0x05, bindings), (0x06, snippets)] {
462
+ let bytes = preamble_of(&[record(1, kind, &body), record(1, kind, &body)]);
463
+ assert!(
464
+ Preamble::decode(&bytes).is_err(),
465
+ "a second record of kind {kind:#04x} through Preamble::decode must be refused"
466
+ );
467
+ }
468
+ }
469
+
470
+ // @behavior WE-081
471
+ #[test]
472
+ fn flags_round_trip_with_their_criticality_in_order() {
473
+ let preamble = Preamble {
474
+ flags: vec![flag("strict", "", true), flag("limit", "8", false)],
475
+ ..Preamble::default()
476
+ };
477
+ assert_eq!(
478
+ Preamble::decode(&preamble.encode()),
479
+ Ok(preamble),
480
+ "flags through a Preamble's encode and decode must arrive in order, each with its \
481
+ value and criticality"
482
+ );
483
+ }
484
+
485
+ // @behavior WE-082
486
+ #[test]
487
+ fn a_criticality_that_is_neither_value_is_refused() {
488
+ let bytes = preamble_of(&[record(2, 0x07, &encode_flag(&flag("f", "", false)))]);
489
+ assert!(
490
+ Preamble::decode(&bytes).is_err(),
491
+ "a criticality byte other than 0 or 1 through Preamble::decode must be refused"
492
+ );
493
+ }
494
+
495
+ // @behavior WE-083
496
+ #[test]
497
+ fn records_read_the_same_in_any_order() {
498
+ let bindings = record(
499
+ 1,
500
+ 0x05,
501
+ &encode_bindings(&Bindings {
502
+ paths: vec!["A".into()],
503
+ }),
504
+ );
505
+ let flag = record(0, 0x07, &encode_flag(&flag("f", "", false)));
506
+ assert_eq!(
507
+ Preamble::decode(&preamble_of(&[flag.clone(), bindings.clone()])),
508
+ Preamble::decode(&preamble_of(&[bindings, flag])),
509
+ "the same records in another order through Preamble::decode must read as the same \
510
+ Preamble"
511
+ );
512
+ }
513
+ }