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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +103 -0
- data/Cargo.lock +117 -140
- data/README.md +156 -118
- data/ROADMAP.md +23 -22
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +38 -0
- data/crates/kobako-runtime/Cargo.toml +2 -2
- data/crates/kobako-runtime/README.md +10 -12
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/lib.rs +1 -1
- data/crates/kobako-runtime/src/profile.rs +1 -1
- data/crates/kobako-runtime/src/runtime.rs +31 -17
- data/crates/kobako-runtime/src/snapshot.rs +13 -10
- data/crates/kobako-runtime/src/yielder.rs +9 -6
- data/crates/kobako-transport/CHANGELOG.md +35 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +43 -27
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +77 -53
- data/crates/kobako-transport/src/envelope/request.rs +295 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +51 -0
- data/crates/kobako-wasmtime/Cargo.toml +7 -5
- data/crates/kobako-wasmtime/README.md +8 -10
- data/crates/kobako-wasmtime/src/abi.rs +5 -8
- data/crates/kobako-wasmtime/src/ambient.rs +2 -2
- data/crates/kobako-wasmtime/src/cache.rs +25 -59
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +11 -14
- data/crates/kobako-wasmtime/src/dispatch.rs +70 -80
- data/crates/kobako-wasmtime/src/driver.rs +133 -86
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +145 -138
- data/crates/kobako-wasmtime/src/instance_pre.rs +11 -22
- data/crates/kobako-wasmtime/src/invocation.rs +55 -372
- data/crates/kobako-wasmtime/src/io.rs +286 -0
- data/crates/kobako-wasmtime/src/lib.rs +3 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/trap.rs +32 -45
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +69 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +94 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +34 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +50 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +2 -2
- data/ext/kobako/src/runtime/bridge.rs +38 -47
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/gvl.rs +4 -12
- data/ext/kobako/src/runtime.rs +169 -212
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +41 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/services.rb +7 -61
- data/lib/kobako/catalog/snippets.rb +42 -66
- data/lib/kobako/catalog.rb +2 -10
- data/lib/kobako/codec/decoder.rb +9 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +4 -10
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec.rb +3 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +51 -112
- data/lib/kobako/errors.rb +51 -127
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +23 -53
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +96 -133
- data/lib/kobako/sandbox_options.rb +38 -84
- data/lib/kobako/snippet/binary.rb +1 -16
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +1 -9
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +112 -195
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +5 -10
- data/lib/kobako/transport/reflection.rb +39 -50
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +21 -73
- data/lib/kobako/transport.rb +3 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +17 -0
- data/sig/kobako/catalog/services.rbs +0 -6
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec.rbs +2 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +2 -3
- data/sig/kobako/errors.rbs +6 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/runtime.rbs +12 -8
- data/sig/kobako/sandbox.rbs +4 -0
- data/sig/kobako/sandbox_options.rbs +2 -6
- data/sig/kobako/transport/dispatcher.rbs +14 -18
- data/sig/kobako/transport/reflection.rbs +5 -3
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +3 -3
- data/sig/kobako/transport.rbs +7 -6
- metadata +16 -4
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/rust-toolchain.toml +0 -11
|
@@ -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
|
+
}
|