// YAML diagnostic and decoding contracts adapted from sigs.k8s.io/yaml v1.4.0,
// goyaml.v2/decode.go (Apache-2.0) and parserc.go (libyaml MIT).
// https://github.com/kubernetes-sigs/yaml/tree/v1.4.0/goyaml.v2
// Modifications: Implemented bounded event-to-value decoding, scalar resolution,
// stream splitting and diagnostic translation in MoonBit.
// License and retained attribution: LICENSE, NOTICE and licenses/GOYAML-LIBYAML-MIT.txt.
///|
priv enum ParsedYamlNode {
Scalar(String, @yaml.TScalarStyle, @yaml.Tag?)
Sequence(Array[Int])
Mapping(Array[Int])
Alias(Int, String)
}
///|
fn Vm::parse_yaml(self : Vm, source : String, loc : Location) -> Value raise {
if @utf8.encode(source).length() > self.limits.output_bytes {
raise problem(Limit, loc, "YAML input exceeds output byte limit")
}
for c in source.iter() {
if !(c == '\t' ||
c == '\n' ||
c == '\r' ||
c == '\u0085' ||
(c >= ' ' && c <= '~') ||
(c >= '\u00a0' && c <= '\ud7ff') ||
(c >= '\ue000' && c <= '\ufffd') ||
c >= '\u{10000}') {
raise problem(
Runtime,
loc,
"failed to parse YAML: error converting YAML to JSON: yaml: control characters are not allowed",
)
}
}
// Go splits only on an unindented --- followed by whitespace, then decodes
// the first YAML document in each chunk.
let chunks : Array[String] = []
let mut buffer = StringBuilder()
let mut stream = false
let lines = source.split("\n").collect()
for i, line in lines {
self.tick(loc)
if i == lines.length() - 1 && line.is_empty() {
break
}
let line = line.strip_suffix("\r").unwrap_or(line).to_owned()
if line.has_prefix("---") && line[3:].trim().is_empty() {
stream = true
if !buffer.to_string().is_empty() {
chunks.push(buffer.to_string())
buffer = StringBuilder()
continue
}
}
buffer.write_string(line)
buffer.write_char('\n')
}
if !buffer.to_string().is_empty() {
chunks.push(buffer.to_string())
}
if chunks.is_empty() {
// Pinned Go indexes an empty slice here; preserve a bounded diagnostic.
raise problem(Runtime, loc, "cannot parse an empty YAML input")
}
let saved_output = self.output_bytes
self.output_bytes = 0
defer {
self.output_bytes = saved_output
}
let values = chunks.map(chunk => self.yaml_document(chunk, loc))
self.native_result(
if stream {
Json::array(values)
} else {
values[0]
},
loc,
0,
)
}
///|
fn Vm::yaml_document(self : Vm, source : String, loc : Location) -> Json raise {
let source = source.strip_prefix("\ufeff").unwrap_or(source[:])
let parser = @yaml.Parser::new(source, max_depth=self.limits.depth)
let nodes : Array[ParsedYamlNode] = []
let parents : Array[Int] = []
let anchors : Map[Int, Int] = Map([])
let mut root = -1
while true {
self.tick(loc)
let (event, _) = parser.next_token() catch {
@yaml.YamlError(info="recursion limit exceeded", ..) =>
raise problem(Limit, loc, "YAML nesting depth limit exceeded")
@yaml.ParserError(mark~, info~) =>
raise problem(
Runtime,
loc,
"failed to parse YAML: error converting YAML to JSON: " +
yaml_parser_error(mark.line, info),
)
@yaml.YamlError(mark~, info~) =>
raise problem(
Runtime,
loc,
"failed to parse YAML: error converting YAML to JSON: " +
yaml_scanner_error(mark.line, info),
)
}
let (node, anchor) = match event {
StreamStart | DocumentStart => continue
StreamEnd | DocumentEnd => break
MappingEnd | SequenceEnd => {
ignore(parents.pop())
continue
}
Scalar(value~, style~, id~, tag~) =>
(ParsedYamlNode::Scalar(value, style, tag), id)
SequenceStart(id~, ..) => (ParsedYamlNode::Sequence([]), id)
MappingStart(id~, ..) => (ParsedYamlNode::Mapping([]), id)
Alias(id~) => {
guard anchors.get(id) is Some(target) else {
raise problem(Runtime, loc, "unknown YAML alias")
}
(ParsedYamlNode::Alias(target, parser.anchor_name(id)), 0)
}
}
if parents.length() >= self.limits.depth {
raise problem(Limit, loc, "YAML nesting depth limit exceeded")
}
let index = nodes.length()
nodes.push(node)
if anchor != 0 {
anchors[anchor] = index
}
if parents.last() is Some(parent) {
match nodes[parent] {
Sequence(items) | Mapping(items) => items.push(index)
_ => abort("YAML parent is a collection")
}
} else {
root = index
}
if node is Sequence(_) || node is Mapping(_) {
parents.push(index)
}
}
if root < 0 {
return Json::null()
}
self.yaml_to_json(nodes, root, Map([]), 0, loc)
}
///|
// The fixed goyaml.v2 decoder reports parser problem lines without the scanner's
// extra increment. Internal Marker lines are one-based; first-line errors omit
// a line prefix in that decoder. Preserve the parser/scanner distinction.
fn yaml_parser_error(line : Int, info : String) -> String {
let problem = if info.has_prefix("while parsing ") {
match info.find(", ") {
Some(index) => info[index + 2:].to_owned()
None => info
}
} else {
info
}
let problem = match problem {
"duplicate version directive" => "found duplicate %YAML directive"
"did not find expected " =>
"did not find expected "
"the handle wasn't declared" => "found undefined tag handle"
"the TAG directive must only be given at most once per handle in the same document" =>
"found duplicate %TAG directive"
_ if problem.has_prefix("expected ") => "did not find " + problem
_ => problem
}
"yaml: " + (if line > 1 { "line \{line-1}: " } else { "" }) + problem
}
///|
fn yaml_scanner_error(line : Int, info : String) -> String {
let problem = if info.has_prefix("while ") {
match info.find(", ") {
Some(index) => info[index + 2:].to_owned()
None => info
}
} else {
info
}
let problem = match problem {
"tabs disallowed within this context (block indentation)" =>
"found character that cannot start any token"
_ => problem
}
"yaml: " + (if line > 1 { "line \{line}: " } else { "" }) + problem
}
///|
fn Vm::yaml_to_json(
self : Vm,
nodes : Array[ParsedYamlNode],
index : Int,
active : Map[Int, Bool],
depth : Int,
loc : Location,
) -> Json raise {
self.tick(loc)
if depth >= self.limits.depth {
raise problem(Limit, loc, "YAML expansion depth limit exceeded")
}
if active.get(index).unwrap_or(false) {
raise problem(Runtime, loc, "YAML alias contains itself")
}
active[index] = true
defer {
active[index] = false
}
match nodes[index] {
Alias(target, name) => {
if active.get(target).unwrap_or(false) {
raise problem(
Runtime,
loc,
yaml_conversion_error(
"yaml: anchor '" + name + "' value contains itself",
),
)
}
self.yaml_to_json(nodes, target, active, depth + 1, loc)
}
Scalar(s, style, tag) => {
let (value, _, _) = self.yaml_scalar(s, style, tag, loc)
if value is Number(n, ..) && (n.is_inf() || n.is_nan()) {
raise problem(
Runtime,
loc,
yaml_conversion_error(
"json: unsupported value: " + diagnostic_number(n),
),
)
}
self.account_output(@utf8.encode(value.stringify()).length(), loc)
value
}
Sequence(items) => {
self.account_output(2 + items.length(), loc)
Json::array(
items.map(i => self.yaml_to_json(nodes, i, active, depth + 1, loc)),
)
}
Mapping(items) => {
let fields : Map[String, Json] = Map([])
self.account_output(2, loc)
for i = 0; i < items.length(); i = i + 2 {
let key = items[i]
let value = items[i + 1]
if nodes[key] is Scalar("<<", style, tag) &&
((style is @yaml.Plain && tag is None) || yaml_tag(tag) == "merge") {
let merged = self.yaml_to_json(nodes, value, active, depth + 1, loc)
if nodes[value] is Alias(_, _) && !(merged is Object(_)) {
raise problem(
Runtime,
loc,
yaml_conversion_error(
"yaml: map merge requires map or sequence of maps as the value",
),
)
}
match merged {
Object(o) =>
for k, v in o {
fields[k] = v
}
Array(a) =>
for j = a.length() - 1; j >= 0; j = j - 1 {
guard a[j] is Object(o) else {
raise problem(
Runtime,
loc,
yaml_conversion_error(
"yaml: map merge requires map or sequence of maps as the value",
),
)
}
for k, v in o {
fields[k] = v
}
}
_ =>
raise problem(
Runtime,
loc,
yaml_conversion_error(
"yaml: map merge requires map or sequence of maps as the value",
),
)
}
} else {
let name = self.yaml_key(nodes, key, value, 0, loc)
self.account_output(
@utf8.encode(Json::string(name).stringify()).length() + 2,
loc,
)
fields[name] = self.yaml_to_json(nodes, value, active, depth + 1, loc)
}
}
Json::object(fields)
}
}
}
///|
fn Vm::yaml_key(
self : Vm,
nodes : Array[ParsedYamlNode],
index : Int,
value_index : Int,
depth : Int,
loc : Location,
) -> String raise {
self.tick(loc)
if depth >= self.limits.depth {
raise problem(Limit, loc, "YAML key depth limit exceeded")
}
match nodes[index] {
Alias(target, _) =>
self.yaml_key(nodes, target, value_index, depth + 1, loc)
Scalar(s, style, tag) => {
let (v, kind, key) = self.yaml_scalar(s, style, tag, loc)
if kind == "uint" || v is Null {
raise problem(
Runtime,
loc,
yaml_conversion_error(
"unsupported map key of type: " +
(if kind == "uint" { "uint64" } else { "%!s()" }) +
", key: " +
self.yaml_go_value(nodes, index, 0, loc) +
", value: " +
self.yaml_go_value(nodes, value_index, 0, loc),
),
)
}
key
}
_ =>
raise problem(
Runtime,
loc,
yaml_conversion_error(
"yaml: invalid map key: " + self.yaml_go_value(nodes, index, 0, loc),
),
)
}
}
///|
fn yaml_conversion_error(message : String) -> String {
"failed to parse YAML: error converting YAML to JSON: " + message
}
///|
// goyaml's decode errors print Go's %#v representation before JSON conversion.
// Retain integer text so a uint64 map key never passes through binary64.
fn Vm::yaml_go_value(
self : Vm,
nodes : Array[ParsedYamlNode],
index : Int,
depth : Int,
loc : Location,
) -> String raise {
self.tick(loc)
if depth >= self.limits.depth {
raise problem(Limit, loc, "YAML key depth limit exceeded")
}
match nodes[index] {
Alias(target, _) => self.yaml_go_value(nodes, target, depth + 1, loc)
Scalar(s, style, tag) => {
let (value, kind, key) = self.yaml_scalar(s, style, tag, loc)
if kind == "uint" {
let mut n = @string.parse_uint64(key[:])
let digits = "0123456789abcdef".to_array()
let out = []
while n != 0UL {
out.push(digits[(n % 16UL).to_int()])
n = n / 16UL
}
let hex = StringBuilder()
hex.write_string("0x")
for i = out.length() - 1; i >= 0; i = i - 1 {
hex.write_char(out[i])
}
hex.to_string()
} else {
match value {
Null => ""
String(s) => @quote.quote(s)
Number(n, ..) =>
if kind == "int" {
key
} else {
diagnostic_number(n)
}
True => "true"
False => "false"
_ => abort("YAML scalar")
}
}
}
Sequence(items) =>
"[]interface {}{" +
items.map(i => self.yaml_go_value(nodes, i, depth + 1, loc)).join(", ") +
"}"
Mapping(items) => {
let fields = []
for i = 0; i < items.length(); i = i + 2 {
let key = self.yaml_go_value(nodes, items[i], depth + 1, loc)
let value = self.yaml_go_value(nodes, items[i + 1], depth + 1, loc)
fields.push(
key +
":" +
(if value == "" { "interface {}(nil)" } else { value }),
)
}
fields.sort()
"map[interface {}]interface {}{" + fields.join(", ") + "}"
}
}
}