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
//! TOML, read with `toml` where the extension reads with `@iarna/toml`.
//!
//! **The two follow different TOML versions and the corpus says so.**
//! `@iarna/toml` implements 0.5, where an inline array must hold one
//! type; the `toml` crate implements 1.0, where `[1, 2.5]` is fine. A
//! document with a mixed array is a parse failure there and eight
//! numbers here, and `fixtures/documents/mixed-array.toml` pins it
//! rather than letting it surface as a mystery.
use toml::Value as Toml;
use super::policy::{Coercion, Literal, Value, collect};
pub(crate) fn extract(text: &str) -> Vec<Literal> {
let Ok(parsed) = text.parse::<toml::Table>() else {
return Vec::new();
};
collect(
&Value::Map(parsed.values().map(convert).collect()),
Coercion::Typed,
)
}
fn convert(value: &Toml) -> Value {
match value {
Toml::Integer(number) => Value::Number(*number as f64),
Toml::Float(number) => Value::Number(*number),
Toml::String(text) => Value::Text(text.clone()),
Toml::Array(items) => Value::Seq(items.iter().map(convert).collect()),
Toml::Table(entries) => Value::Map(entries.values().map(convert).collect()),
// A datetime is a typed value, and the extension skips it
// explicitly for the same reason.
Toml::Boolean(_) | Toml::Datetime(_) => Value::Other,
}
}
pub(crate) fn parse_error(text: &str) -> Option<String> {
text.parse::<toml::Table>()
.err()
.map(|error| format!("Failed to parse TOML: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn values(text: &str) -> Vec<f64> {
extract(text)
.into_iter()
.map(|literal| literal.value)
.collect()
}
#[test]
fn integers_and_floats_are_both_numbers() {
assert_eq!(values("a = 8080\nb = 0.0825"), [8080.0, 0.0825]);
}
#[test]
fn a_quoted_number_is_not_a_number() {
assert_eq!(values("a = 42\nb = \"42\""), [42.0]);
}
/// TOML's own literals, resolved by the parser before the policy.
#[test]
fn hex_and_underscored_literals_are_resolved_by_the_parser() {
assert_eq!(values("a = 0x1A"), [26.0]);
assert_eq!(values("a = 1_000"), [1000.0]);
}
/// A TOML integer is an i64 and a JavaScript number is an f64, so
/// past 2^53 the value loses precision — the same precision a
/// JavaScript number loses, which is what makes it the right answer.
///
/// **The extension does not agree here**, and the `differential` job
/// found it: `@iarna/toml` hands back a `BigInt` at or above 2^53
/// and the extension's numeric walk does not recognise one, so it
/// silently reports nothing. SPEC.md, "Deliberate divergences",
/// records which answer to trust and why.
#[test]
fn an_integer_past_the_safe_range_loses_precision_rather_than_vanishing() {
use crate::extract::render::js_number;
assert_eq!(
js_number(values("a = 9007199254740992")[0]),
"9007199254740992"
);
assert_eq!(
js_number(values("a = 9007199254740993")[0]),
"9007199254740992"
);
}
/// TOML integers are 64-bit signed, so one that does not fit is not
/// a valid document and is refused rather than guessed at.
/// `@iarna/toml` wraps it to a negative number that is nowhere in
/// the file; refusing is the answer an audit can act on.
#[test]
fn an_integer_too_large_for_toml_is_a_refusal_not_a_wrapped_value() {
assert!(values("a = 123456789012345680000").is_empty());
assert!(parse_error("a = 123456789012345680000").is_some());
}
#[test]
fn non_finite_values_are_dropped() {
assert!(values("a = inf\nb = nan\nc = -inf").is_empty());
}
#[test]
fn a_datetime_is_not_a_number() {
assert!(values("issued = 1979-05-27").is_empty());
}
#[test]
fn arrays_and_tables_are_followed() {
assert_eq!(
values("limits = [1, 2]\n\n[owner]\nage = 30\n"),
[1.0, 2.0, 30.0]
);
}
/// The version divergence, asserted rather than assumed. The `toml`
/// crate is 1.0 and reads this; `@iarna/toml` is 0.5 and refuses it.
#[test]
fn a_mixed_inline_array_parses_here_and_not_in_the_extension() {
assert_eq!(values("limits = [1, 2.5]"), [1.0, 2.5]);
assert!(parse_error("limits = [1, 2.5]").is_none());
}
#[test]
fn a_broken_document_yields_nothing_and_says_why() {
assert!(values("not = = toml").is_empty());
assert!(parse_error("not = = toml").is_some());
}
}