1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
//! Capabilities that a script requires to run.
use bitflags::bitflags;
#[cfg(feature = "no_std")]
use std::prelude::v1::*;
bitflags! {
/// Capability flags.
///
/// Order is the wire order and must never change; append only.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Caps: u32 {
/// The script uses floating-point numbers, which are not available under `no_float`.
const FLOAT = 1<<0;
/// The script uses arrays, which are not available under `no_index`.
const ARRAY = 1<<1;
/// The script uses BLOB's, which are not available under `no_index`.
const BLOB = 1<<2;
/// The script uses object maps, which are not available under `no_object`.
const MAP = 1<<3;
/// The script uses decimal numbers, which are only available under `decimal`.
const DECIMAL = 1<<4;
/// The script defines functions, which are not available under `no_function`.
const FUNCTION = 1<<5;
/// The script uses function pointers.
const FN_PTR = 1<<6;
/// The script uses currying on function pointers.
const CURRYING = 1<<7;
/// The script employs indexing, which is not available under `no_index`.
const INDEXING = 1<<8;
/// The script accesses properties, which are not available under `no_object`.
const PROPERTY = 1<<9;
/// The script uses method calling style, which is not available under `no_object`.
const METHOD = 1<<10;
/// The script uses `this`, which is not available under `no_function`.
const THIS = 1<<11;
/// The script uses shared values, which is not available under `no_closure`.
const SHARING = 1<<12;
/// The script uses the `import` statement to import modules, which is not available under `no_module`.
const IMPORT = 1<<13;
/// The script uses the `export` statement to export in modules, which is not available under `no_module`.
const EXPORT = 1<<14;
/// The script uses the custom syntax, which is not available under `no_custom_syntax`.
const CUSTOM_SYNTAX = 1<<15;
}
}
impl std::fmt::Display for Caps {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let description = CAP_FLAGS
.iter()
.filter(|(cap, _, _)| self.contains(*cap))
.map(|(_, name, _)| *name)
.collect::<Vec<_>>()
.join(", ");
f.write_str(&description)?;
// A bit not in the table means the writer knows a capability this build does not.
// Reporting it as unknown.
if !(*self - Self::all()).is_empty() {
if !description.is_empty() {
f.write_str(", ")?;
}
f.write_str("requires an unknown capability")?;
}
if description.is_empty() {
f.write_str("requires nothing")?;
}
Ok(())
}
}
/// A table of all capabilities, their human-readable names,
/// and whether this build has them.
#[rustfmt::skip]
const CAP_FLAGS: &[(Caps, &'static str, bool)] = &[
(Caps::FLOAT, "uses floating-point numbers", !cfg!(feature = "no_float")),
(Caps::ARRAY, "uses arrays", !cfg!(feature = "no_index")),
(Caps::BLOB, "uses BLOB's", !cfg!(feature = "no_index")),
(Caps::MAP, "uses object maps", !cfg!(feature = "no_object")),
(Caps::DECIMAL, "uses decimal numbers", cfg!(feature = "decimal")),
(Caps::FUNCTION, "defines functions", !cfg!(feature = "no_function")),
(Caps::FN_PTR, "uses function pointers", true),
(Caps::CURRYING, "uses currying", true),
(Caps::INDEXING, "uses indexing", !cfg!(feature = "no_index")),
(Caps::PROPERTY, "accesses properties", !cfg!(feature = "no_object")),
(Caps::METHOD, "uses method calling style", !cfg!(feature = "no_object")),
(Caps::THIS, "uses `this`", !cfg!(feature = "no_function")),
(Caps::SHARING, "uses shared values", !cfg!(feature = "no_closure")),
(Caps::IMPORT, "imports modules", !cfg!(feature = "no_module")),
(Caps::EXPORT, "exports data in modules", !cfg!(feature = "no_module")),
// Unsupported features below
(Caps::CUSTOM_SYNTAX, "uses custom syntax", false && !cfg!(feature = "no_custom_syntax")),
];
/// The value representation an artifact was written against.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Abi {
/// `size_of::<rhai::INT>()`. Measured, so `only_i32` set on Rhai alone is
/// still caught.
pub int_bytes: u8,
/// `size_of::<rhai::FLOAT>()`, or 0 under `no_float`.
pub float_bytes: u8,
/// current build's feature flags.
pub caps: Caps,
}
/// How two fingerprints differ.
///
/// Naming the difference is the whole point: "artifact was built for a
/// different rhai" is not something a user can act on.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AbiMismatch {
/// A width differs, which means integers or floats would decode wrong.
DataWidth {
/// Which width differs
what: &'static str,
/// What the writer used
artifact: u8,
/// What this build uses
host: u8,
},
/// Missing capabilities, which means the artifact uses features this build does not have.
MissingCaps {
/// Which capabilities are missing.
caps: String,
},
}
impl core::fmt::Display for AbiMismatch {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::DataWidth {
what,
artifact,
host,
} => write!(
f,
"artifact cannot load because it was written with a {artifact}-byte {what}, but this build has {host}"
),
Self::MissingCaps {
caps: capability,
} => {
write!(
f,
"artifact cannot load because {capability}, but this build does not have the necessary features enabled"
)
}
}
}
}
impl Abi {
/// The fingerprint of the running build.
#[must_use]
pub fn host() -> Self {
let mut caps = Caps::empty();
CAP_FLAGS
.iter()
.for_each(|(flag, _, on)| caps.set(*flag, *on));
Self {
int_bytes: core::mem::size_of::<rhai::INT>() as u8,
#[cfg(not(feature = "no_float"))]
float_bytes: core::mem::size_of::<rhai::FLOAT>() as u8,
#[cfg(feature = "no_float")]
float_bytes: 0,
caps,
}
}
/// Why this fingerprint cannot be loaded by `host`, if it cannot.
///
/// Widths first: they are measured rather than declared, so they are the
/// claim least likely to be lying.
#[must_use]
pub fn is_incompatible_with(self, host: Self) -> Option<AbiMismatch> {
if self.int_bytes != host.int_bytes {
return Some(AbiMismatch::DataWidth {
what: "integer",
artifact: self.int_bytes,
host: host.int_bytes,
});
}
if self.float_bytes != host.float_bytes {
return Some(AbiMismatch::DataWidth {
what: "floating-point number",
artifact: self.float_bytes,
host: host.float_bytes,
});
}
let missing = self.caps - host.caps;
if !missing.is_empty() {
return Some(AbiMismatch::MissingCaps {
caps: missing.to_string(),
});
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_build_can_load_its_own_artifacts() {
assert_eq!(Abi::host().is_incompatible_with(Abi::host()), None);
}
#[test]
fn the_build_measures_the_widths_it_should() {
let abi = Abi::host();
// Against the aliases rather than concrete types, so this holds under
// only_i32 and f32_float as well — the point is that the fingerprint
// reports the build it was taken on, whichever build that is.
assert_eq!(abi.int_bytes as usize, core::mem::size_of::<rhai::INT>(),);
#[cfg(not(feature = "no_float"))]
assert_eq!(
abi.float_bytes as usize,
core::mem::size_of::<rhai::FLOAT>(),
);
#[cfg(feature = "no_float")]
assert_eq!(abi.float_bytes, 0);
}
#[test]
fn a_narrower_int_is_refused_by_name() {
let host = Abi::host();
// Halved rather than named: `only_i32` makes 4 the host's own width,
// and an artifact agreeing with the host is not a mismatch to report.
let narrow = Abi {
int_bytes: host.int_bytes / 2,
..host
};
assert_eq!(
narrow.is_incompatible_with(host),
Some(AbiMismatch::DataWidth {
what: "integer",
artifact: host.int_bytes / 2,
host: host.int_bytes,
}),
);
}
/// The message has to name the flag; that is the difference between an
/// error a user can act on and one they cannot.
#[test]
#[cfg(not(feature = "no_object"))]
fn a_differing_restriction_is_refused_by_name() {
let cap = Caps::MAP;
let desc = CAP_FLAGS
.iter()
.find(|(flag, _, _)| *flag == cap)
.expect("the test is broken")
.1;
// Let's say we need all caps on the host.
let needed = Abi::host();
// But the host has one less than we need, so it cannot load the artifact.
let host = Abi {
caps: needed.caps - cap,
..needed
};
let Some(AbiMismatch::MissingCaps { caps: missing, .. }) =
needed.is_incompatible_with(host)
else {
panic!("a missing cap must be refused");
};
assert_eq!(missing, desc);
}
#[test]
fn a_flag_this_build_has_never_heard_of_does_not_panic() {
let host = Abi::host();
let future = Abi {
caps: host.caps ^ Caps::from_bits_retain(1_u32 << 31),
..host
};
assert!(matches!(
future.is_incompatible_with(host),
Some(AbiMismatch::MissingCaps { .. }),
));
}
}