///|
priv suberror CodecError {
Invalid(String)
}
///|
fn parse_json(text : String) -> Json raise CodecError {
if text.length() > 65536 {
raise Invalid("JSON exceeds 65536 code units")
}
let mut depth = 0
let mut quoted = false
let mut escaped = false
for c in text.iter() {
if quoted {
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
quoted = false
}
} else if c == '"' {
quoted = true
} else if c == '{' || c == '[' {
depth += 1
if depth > 8 {
raise Invalid("JSON nesting exceeds eight containers")
}
} else if c == '}' || c == ']' {
depth -= 1
}
}
// Validate original syntax first: normalization must not legalize 01, 1., etc.
let original = @json.parse(text[:]) catch {
_ => raise Invalid("invalid JSON")
}
let normalized = exact_json_numbers(text)
if normalized == text {
original
} else {
@json.parse(normalized[:]) catch {
_ => raise Invalid("invalid JSON")
}
}
}
///|
fn codec_fields(
value : Json,
allowed : Array[String],
) -> Map[String, Json] raise CodecError {
guard value is Object(fields) else { raise Invalid("expected object") }
for key, _ in fields {
if !allowed.contains(key) {
raise Invalid("unknown field: " + key)
}
}
fields
}
///|
fn required(fields : Map[String, Json], key : String) -> Json raise CodecError {
match fields.get(key) {
Some(v) => v
None => raise Invalid("missing field: " + key)
}
}
///|
fn codec_string(value : Json) -> String raise CodecError {
guard value is String(s) else { raise Invalid("expected string") }
s
}
///|
fn codec_int(value : Json) -> Int32 raise CodecError {
guard value is Number(x, ..) else { raise Invalid("expected integer") }
if x != x || x < -1.0 || x > 1000000.0 {
raise Invalid("integer outside -1..1000000")
}
let n = x.to_int()
if n.to_double() != x {
raise Invalid("fractional integer")
}
n
}
///|
fn codec_array(value : Json, limit : Int32) -> Array[Json] raise CodecError {
guard value is Array(items) else { raise Invalid("expected array") }
if items.length() > limit {
raise Invalid("array exceeds schema bound")
}
items
}
///|
fn demand_json(value : Json) -> Demand raise CodecError {
let f = codec_fields(value, ["id", "material", "length", "quantity"])
demand(
codec_string(required(f, "id")),
codec_string(required(f, "material")),
codec_int(required(f, "length")),
codec_int(required(f, "quantity")),
)
}
///|
fn stock_json(value : Json) -> Stock raise CodecError {
let f = codec_fields(value, [
"id", "material", "length", "cost", "quantity", "trim_start", "trim_end",
])
stock(
codec_string(required(f, "id")),
codec_string(required(f, "material")),
codec_int(required(f, "length")),
codec_int(required(f, "cost")),
codec_int(required(f, "quantity")),
trim_start=codec_int(required(f, "trim_start")),
trim_end=codec_int(required(f, "trim_end")),
)
}
///|
fn problem_json(value : Json) -> Problem raise CodecError {
let f = codec_fields(value, [
"schema", "demands", "stocks", "kerf", "min_remnant",
])
if codec_string(required(f, "schema")) != "mooncutstock.problem/1" {
raise Invalid("unsupported problem schema")
}
let ds = codec_array(required(f, "demands"), 12).map(demand_json)
let ss = codec_array(required(f, "stocks"), 16).map(stock_json)
match
problem(
ds,
ss,
kerf=codec_int(required(f, "kerf")),
min_remnant=codec_int(required(f, "min_remnant")),
) {
Ok(p) => p
Err(e) => raise Invalid(e)
}
}
///|
pub fn decode_problem(text : String) -> Result[Problem, String] {
Ok(problem_json(parse_json(text))) catch {
Invalid(e) => Err(e)
}
}
///|
// All numbers in the interchange schemas are bounded integers. Check the source
// decimal, not a rounded Double; 1.0000000000000001 and 1e-400 are not integers.
// Input syntax was already checked by the standard parser.
fn exact_json_number(token : String) -> String raise CodecError {
let chars = token.iter().to_array()
let negative = chars[0] == '-'
let mut at = if negative { 1 } else { 0 }
let digits : Array[Int32] = []
let mut fractional = 0
let mut after_dot = false
while at < chars.length() && chars[at] != 'e' && chars[at] != 'E' {
if chars[at] == '.' {
after_dot = true
} else {
digits.push(chars[at].to_int() - 48)
if after_dot {
fractional += 1
}
}
at += 1
}
let mut exponent = 0
if at < chars.length() {
at += 1
let minus = chars[at] == '-'
if chars[at] == '-' || chars[at] == '+' {
at += 1
}
while at < chars.length() {
// Larger magnitudes cannot cancel a <=65536-unit mantissa. Saturation
// prevents overflow even for a malicious exponent with thousands of digits.
exponent = (exponent * 10 + chars[at].to_int() - 48).min(100000)
at += 1
}
if minus {
exponent = -exponent
}
}
let mut first = 0
while first < digits.length() && digits[first] == 0 {
first += 1
}
if first == digits.length() {
return "0"
}
let shift = exponent - fractional
let mut end = digits.length()
if shift < 0 {
if -shift > end - first {
raise Invalid("fractional integer")
}
for i in (end + shift).. 10 {
raise Invalid("JSON integer outside supported range")
}
let mut n = 0L
for i in first.. 2147483647L {
raise Invalid("JSON integer outside supported range")
}
if negative {
(-n).to_string()
} else {
n.to_string()
}
}
///|
fn exact_json_numbers(text : String) -> String raise CodecError {
let chars = text.iter().to_array()
let out = StringBuilder()
let mut quoted = false
let mut escaped = false
let mut i = 0
while i < chars.length() {
let c = chars[i]
if quoted {
out.write_char(c)
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == '"' {
quoted = false
}
} else if c == '"' {
quoted = true
out.write_char(c)
} else if c == '-' || (c >= '0' && c <= '9') {
let token = StringBuilder()
while i < chars.length() {
let n = chars[i]
if !((n >= '0' && n <= '9') ||
n == '-' ||
n == '+' ||
n == '.' ||
n == 'e' ||
n == 'E') {
break
}
token.write_char(n)
i += 1
}
out.write_string(exact_json_number(token.to_string()))
continue
} else {
out.write_char(c)
}
i += 1
}
out.to_string()
}