///|
/// Source coordinates are relative to the supplied view. Lines, Unicode-scalar
/// columns and document numbers are 1-based; they are not byte offsets.
pub(all) struct Location {
document : Int32
line : Int32
column : Int32
} derive(Debug, Eq)
///|
pub(all) enum ErrorKind {
Encoding
Syntax
Unsupported
DuplicateKey
Limit
Bindings
} derive(Debug, Eq)
///|
pub(all) suberror YamlError {
YamlError(kind~ : ErrorKind, location~ : Location?, message~ : String)
} derive(Debug)
///|
pub fn YamlError::message(self : YamlError) -> String {
let YamlError(kind=_, location~, message~) = self
match location {
Some(p) =>
"YAML document \{p.document} at \{p.line}:\{p.column}: \{message}"
None => "YAML: " + message
}
}
///|
priv struct Reader {
parser : @parser.Parser
mut document : Int32
mut nodes_left : Int32
mut payload_left : Int32
anchors : Map[Int32, Decoded]
invalid_character : @parser.Marker?
}
///|
fn Reader::error(
self : Reader,
kind : ErrorKind,
mark : @parser.Marker,
message : String,
) -> YamlError {
YamlError(
kind~,
location=Some({
document: self.document,
line: mark.line,
column: mark.col + 1,
}),
message~,
)
}
///|
fn Reader::next(
self : Reader,
) -> (@parser.Event, @parser.Marker) raise YamlError {
let result = self.parser.next_token() catch {
@parser.YamlError::UnsupportedVersion(mark~) =>
raise self.error(Unsupported, mark, "only YAML 1.2 is supported")
@parser.YamlError::ResourceLimit(mark~, info~) =>
raise self.error(Limit, mark, info)
@parser.YamlError::YamlError(mark~, info~) =>
raise self.error(Syntax, mark, info)
}
if self.invalid_character is Some(mark) && result.1.index >= mark.index {
raise self.error(Syntax, mark, "non-printable character in YAML source")
}
result
}
///|
fn digits(s : StringView, radix~ : Int32 = 10) -> Bool {
!s.is_empty() &&
s
.iter()
.all(c => {
match c {
'0'..<='7' => true
'8'..<='9' => radix >= 10
'a'..<='f' | 'A'..<='F' => radix == 16
_ => false
}
})
}
///|
fn unsigned(s : StringView) -> StringView {
match s {
['+' | '-', .. rest] => rest
_ => s
}
}
///|
fn integer_parts(s : StringView) -> (StringView, Int32)? {
let (body, radix) = match s {
['0', 'o', .. rest] => (rest, 8)
['0', 'x', .. rest] => (rest, 16)
_ => (unsigned(s), 10)
}
if digits(body, radix~) {
Some((if radix == 10 { s } else { body }, radix))
} else {
None
}
}
///|
fn integer(s : String) -> @value.Value? {
match integer_parts(s) {
Some((body, radix)) =>
Some(
@value.Value::from_bigint(
@bigint.BigInt::from_string(body.to_owned(), radix~),
),
)
None => None
}
}
///|
fn float_spelling(s : StringView) -> Bool {
let s = unsigned(s)
let mut i = 0
let mut count = 0
while i < s.length() && s[i] >= '0' && s[i] <= '9' {
i += 1
count += 1
}
if i < s.length() && s[i] == '.' {
i += 1
while i < s.length() && s[i] >= '0' && s[i] <= '9' {
i += 1
count += 1
}
}
if count == 0 {
return false
}
if i < s.length() && (s[i] == 'e' || s[i] == 'E') {
i += 1
return digits(unsigned(s[i:]))
}
i == s.length()
}
///|
priv enum CoreTag {
Str
Null
Bool
Int
Float
Seq
Map
}
///|
fn Reader::tag(
self : Reader,
tag : @parser.Tag?,
fallback : CoreTag,
nonspecific : CoreTag,
mark : @parser.Marker,
) -> CoreTag raise YamlError {
match tag {
None => fallback
Some({ handle: "", suffix: "!", }) => nonspecific
Some(t) =>
match t.handle + t.suffix {
"tag:yaml.org,2002:str" => Str
"tag:yaml.org,2002:null" => Null
"tag:yaml.org,2002:bool" => Bool
"tag:yaml.org,2002:int" => Int
"tag:yaml.org,2002:float" => Float
"tag:yaml.org,2002:seq" => Seq
"tag:yaml.org,2002:map" => Map
name =>
raise self.error(Unsupported, mark, "unsupported YAML tag: " + name)
}
}
}
///|
fn scalar_tag(s : String, style : @parser.TScalarStyle) -> CoreTag {
if !(style is Plain) {
return Str
}
match s {
"" | "~" | "null" | "Null" | "NULL" => Null
"true" | "True" | "TRUE" | "false" | "False" | "FALSE" => Bool
".inf"
| ".Inf"
| ".INF"
| "+.inf"
| "+.Inf"
| "+.INF"
| "-.inf"
| "-.Inf"
| "-.INF"
| ".nan"
| ".NaN"
| ".NAN" => Float
_ =>
if integer_parts(s) is Some(_) {
Int
} else if float_spelling(s) {
Float
} else {
Str
}
}
}
///|
fn Reader::scalar(
self : Reader,
s : String,
style : @parser.TScalarStyle,
tag : @parser.Tag?,
mark : @parser.Marker,
) -> @value.Value raise YamlError {
let kind = self.tag(tag, scalar_tag(s, style), Str, mark)
let invalid = () => {
self.error(
Unsupported,
mark,
"scalar does not match declared YAML tag: " + s,
)
}
match kind {
Str => @value.Value::from_string(s)
Null =>
match s {
"" | "~" | "null" | "Null" | "NULL" => Null
_ => raise invalid()
}
Bool =>
match s {
"true" | "True" | "TRUE" => @value.Value::from_bool(true)
"false" | "False" | "FALSE" => @value.Value::from_bool(false)
_ => raise invalid()
}
Int =>
match integer(s) {
Some(v) => v
None => raise invalid()
}
Float =>
match s {
".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" =>
@value.Value::from_float(@double.infinity)
"-.inf" | "-.Inf" | "-.INF" =>
@value.Value::from_float(@double.neg_infinity)
".nan" | ".NaN" | ".NAN" =>
@value.Value::from_float(@double.not_a_number)
_ => {
if !float_spelling(s) {
raise invalid()
}
let decimal = @ion_core.Decimal::parse(
s.replace(old="e", new="d").replace(old="E", new="d"),
) catch {
e => raise self.error(Limit, mark, e.message())
}
@value.Value::from_decimal(decimal)
}
}
Seq | Map =>
raise self.error(Unsupported, mark, "collection tag on a scalar")
}
}
///|
priv struct Decoded {
value : @value.Value
nodes : Int32
payload : Int32
height : Int32
}
///|
fn copy_value(value : @value.Value) -> @value.Value {
match value {
List(items) => @value.Value::from_list(items.map(copy_value))
Tuple(t) =>
@value.Value::from_tuple(
@value.Tuple(t.fields().map(f => (f.0, copy_value(f.1)))),
)
_ => value
}
}
///|
fn payload_size(s : String) -> Int32 {
let mut n = 0
for c in s {
let code = c.to_int()
n += if code <= 0x7f {
1
} else if code <= 0x7ff {
2
} else if code <= 0xffff {
3
} else {
4
}
}
n
}
///|
fn Reader::node(
self : Reader,
event : @parser.Event,
mark : @parser.Marker,
depth : Int32,
) -> Decoded raise YamlError {
if event is Alias(id~) {
guard self.anchors.get(id) is Some(node) else {
raise self.error(Syntax, mark, "cyclic or undefined YAML alias")
}
if node.nodes > self.nodes_left ||
node.payload > self.payload_left ||
depth + node.height > 256 {
raise self.error(
Limit,
mark,
"alias expansion exceeds node, UTF-8 payload or nesting budget",
)
}
self.nodes_left -= node.nodes
self.payload_left -= node.payload
self.parser.charge_alias(node.payload, mark) catch {
@parser.YamlError::ResourceLimit(mark~, info~) =>
raise self.error(Limit, mark, info)
_ => raise self.error(Limit, mark, "alias payload budget exceeded")
}
return { ..node, value: copy_value(node.value), }
}
if self.nodes_left <= 0 {
raise self.error(Limit, mark, "expanded node budget exceeded")
}
self.nodes_left -= 1
let (id, node) = match event {
Scalar(value~, style~, tag~, id~) => {
let payload = payload_size(value)
if payload > self.payload_left {
raise self.error(
Limit,
mark,
"scalar payload exceeds the UTF-8 byte budget",
)
}
self.payload_left -= payload
(
id,
Decoded::{
value: self.scalar(value, style, tag, mark),
nodes: 1,
payload,
height: 0,
},
)
}
SequenceStart(tag~, id~) => {
if !(self.tag(tag, Seq, Seq, mark) is Seq) {
raise self.error(Unsupported, mark, "tag does not match sequence node")
}
if depth >= 256 {
raise self.error(Limit, mark, "nesting exceeds 256 containers")
}
let items = []
let mut nodes = 1
let mut payload = 0
let mut height = 1
while true {
let (event, mark) = self.next()
if event is SequenceEnd {
break
}
let node = self.node(event, mark, depth + 1)
items.push(node.value)
nodes += node.nodes
payload += node.payload
height = height.max(node.height + 1)
}
(
id,
Decoded::{
value: @value.Value::from_list(items),
nodes,
payload,
height,
},
)
}
MappingStart(tag~, id~) => {
if !(self.tag(tag, Map, Map, mark) is Map) {
raise self.error(Unsupported, mark, "tag does not match mapping node")
}
if depth >= 256 {
raise self.error(Limit, mark, "nesting exceeds 256 containers")
}
let fields = []
let names : Map[String, Unit] = Map([])
let mut nodes = 1
let mut payload = 0
let mut height = 1
while true {
let (event, mark) = self.next()
if event is MappingEnd {
break
}
let key = self.node(event, mark, depth + 1)
guard key.value is String(name) else {
raise self.error(
Unsupported,
mark,
"mapping keys must be strings; quote the key or use !!str",
)
}
if names.contains(name) {
raise self.error(DuplicateKey, mark, "duplicate mapping key: " + name)
}
names[name] = ()
let (event, mark) = self.next()
let node = self.node(event, mark, depth + 1)
fields.push((name, node.value))
nodes += key.nodes + node.nodes
payload += key.payload + node.payload
height = height.max(node.height + 1)
}
(
id,
Decoded::{
value: @value.Value::from_tuple(@value.Tuple(fields)),
nodes,
payload,
height,
},
)
}
_ => raise self.error(Syntax, mark, "expected a YAML value")
}
if id != 0 {
self.anchors[id] = node
}
node
}
///|
/// Decode YAML input without rounding finite numbers through binary64.
pub fn read_text(source : StringView) -> Array[@value.Value] raise YamlError {
read_limited(source, max_nodes=1000000, max_payload=67108864)
}
///|
fn read_limited(
source : StringView,
max_nodes~ : Int32,
max_payload~ : Int32,
) -> Array[@value.Value] raise YamlError {
let reader = Reader::{
parser: @parser.Parser(source, max_nodes~, max_payload~),
document: 1,
nodes_left: max_nodes,
payload_left: max_payload,
anchors: Map([]),
invalid_character: invalid_character(source),
}
let result = []
while true {
let (event, mark) = reader.next()
match event {
StreamStart => ()
StreamEnd => break
DocumentStart => {
reader.anchors.clear()
let (event, mark) = reader.next()
result.push(reader.node(event, mark, 0).value)
}
DocumentEnd => reader.document += 1
_ => raise reader.error(Syntax, mark, "expected a document")
}
}
result
}
///|
pub fn read_bytes(source : BytesView) -> Array[@value.Value] raise YamlError {
if source[:source.length().min(4)].iter().any(b => b == 0) {
raise YamlError(
kind=Encoding,
location=None,
message="expected UTF-8 YAML; UTF-16 and UTF-32 are not supported",
)
}
let text = @utf8.decode(source) catch {
_ =>
raise YamlError(
kind=Encoding,
location=None,
message="expected UTF-8 YAML; UTF-16 and UTF-32 are not supported",
)
}
read_text(text)
}
///|
fn invalid_character(source : StringView) -> @parser.Marker? {
let mut line = 1
let mut column = 0
let mut index = 0
let mut previous_cr = false
for c in source {
let n = c.to_int()
if (n < 32 && c != '\t' && c != '\n' && c != '\r') ||
(n >= 127 && n <= 159 && n != 133) ||
(n >= 0xd800 && n <= 0xdfff) ||
n == 0xfffe ||
n == 0xffff {
return Some(@parser.Marker::{ index, line, col: column, })
}
if c == '\r' {
line += 1
column = 0
} else if c == '\n' {
if !previous_cr {
line += 1
}
column = 0
} else {
column += 1
}
previous_cr = c == '\r'
index += 1
}
None
}