///|
pub suberror ManifestParseError {
InvalidJson(String)
DuplicateJsonKey(String)
InvalidField(String)
}
///|
/// Parse a manifest while rejecting duplicate keys and unsupported fields.
/// Use this entry point when a library consumer reads a manifest from JSON.
pub fn parse_manifest_json(text : String) -> Manifest raise ManifestParseError {
match first_duplicate_json_key(text) {
Some(key) => raise ManifestParseError::DuplicateJsonKey(key)
None => ()
}
let json = @json.parse(text) catch {
error => raise ManifestParseError::InvalidJson("\{error}")
}
match first_manifest_field_error(json) {
Some(message) => raise ManifestParseError::InvalidField(message)
None => ()
}
let manifest : Manifest = @json.from_json(json) catch {
error => raise ManifestParseError::InvalidJson("\{error}")
}
for index, spec in manifest.assets {
match asset_spec_error(spec) {
Some(message) =>
raise ManifestParseError::InvalidField("assets[\{index}]: \{message}")
None => ()
}
}
manifest
}
///|
/// JSON objects normally decode into maps, which erase repeated member names.
/// Scan the original text first so ambiguous requirements cannot pass silently.
fn first_duplicate_json_key(text : String) -> String? {
let input = text[:]
let object_keys : Array[Map[String, Bool]] = []
let mut in_string = false
let mut escaped = false
let mut quote_start = 0
for index in 0.. 0 {
let key_literal = input
.view(start_offset=quote_start, end_offset=index + 1)
.to_owned()
let parsed_key = @json.parse(key_literal) catch { _ => Json::null() }
match parsed_key {
String(key) => {
let last = object_keys.length() - 1
let keys = object_keys[last]
if keys.contains(key) {
return Some(key)
}
keys.set(key, true)
object_keys[last] = keys
}
_ => ()
}
}
}
} else if code == 34 {
in_string = true
escaped = false
quote_start = index
} else if code == 123 {
object_keys.push(Map([]))
} else if code == 125 {
ignore(object_keys.pop())
}
}
None
}
///|
/// Reject misspelled keys and fractional dimensions or byte limits instead
/// of silently ignoring or truncating a handoff requirement.
fn first_manifest_field_error(json : Json) -> String? {
match json {
Object(root) =>
for key, value in root {
if key != "assets" && key != "max_total_bytes" {
return Some(
unknown_field_message(key, key, ["assets", "max_total_bytes"]),
)
}
if key == "max_total_bytes" {
match value {
Number(number, ..) if number.floor() != number =>
return Some("max_total_bytes 必须是整数")
Number(number, ..) if number <= 0 =>
return Some("max_total_bytes 必须为正整数")
_ => ()
}
} else {
match value {
Array(assets) =>
for index in 0.. {
let mut width_present = false
let mut height_present = false
for field, field_value in fields {
if field != "file" &&
field != "label" &&
field != "width" &&
field != "height" &&
field != "max_bytes" &&
field != "min_bytes" &&
field != "expected_sha256" &&
field != "aspect" &&
field != "format" {
return Some(
unknown_field_message(
"assets[\{index}].\{field}",
field,
asset_field_names(),
),
)
}
if field == "expected_sha256" {
match field_value {
String(expected) => {
if expected.length() != 64 {
return Some(
"assets[\{index}].expected_sha256 必须是 64 位十六进制字符串",
)
}
for character in expected {
if !is_hex_character(character) {
return Some(
"assets[\{index}].expected_sha256 必须是 64 位十六进制字符串",
)
}
}
}
_ => ()
}
}
if field == "aspect" {
match field_value {
String(aspect) =>
if parse_aspect(aspect) is None {
return Some(
"assets[\{index}].aspect 必须是 W:H 形式的正整数比例",
)
}
_ => ()
}
}
if field == "min_bytes" {
match field_value {
Number(number, ..) if number.floor() != number =>
return Some(
"assets[\{index}].min_bytes 必须是整数",
)
Number(number, ..) if number <= 0 =>
return Some(
"assets[\{index}].min_bytes 必须为正整数",
)
_ => ()
}
}
if field == "format" {
match field_value {
String(format) =>
if canonical_format(format) is None {
return Some(
"assets[\{index}].format 必须是受支持的格式名(如 png、jpeg、webp、svg)",
)
}
_ => ()
}
}
if field == "width" ||
field == "height" ||
field == "max_bytes" {
if field == "width" {
width_present = true
}
if field == "height" {
height_present = true
}
match field_value {
Number(number, ..) if number.floor() != number =>
return Some(
"assets[\{index}].\{field} 必须是整数",
)
Number(number, ..) if number <= 0 =>
return Some(
"assets[\{index}].\{field} 必须为正整数",
)
_ => ()
}
}
}
if width_present != height_present {
return Some(
"assets[\{index}] 的 width 和 height 必须同时提供",
)
}
}
_ => ()
}
}
_ => ()
}
}
}
_ => ()
}
None
}
///|
fn is_hex_character(character : Char) -> Bool {
(character >= '0' && character <= '9') ||
(character >= 'a' && character <= 'f') ||
(character >= 'A' && character <= 'F')
}
///|
fn asset_field_names() -> Array[String] {
[
"file", "label", "width", "height", "max_bytes", "min_bytes", "expected_sha256",
"aspect", "format",
]
}
///|
fn unknown_field_message(
path : String,
field : String,
candidates : Array[String],
) -> String {
match closest_match(field, candidates) {
Some(suggestion) =>
"未知字段 \{path},你是不是想写 \{suggestion}?"
None => "未知字段 \{path}"
}
}