///| A bounded structural scan rejects duplicate object keys before Json's map decoding.
///|
/// Keys are decoded with the official parser, including escaped equivalent spellings.
priv struct JsonScan {
chars : Array[Char]
decoded_keys : Map[String, String]
mut pos : Int
mut depth : Int
}
///|
fn json_white(c : Char) -> Bool {
c == ' ' || c == '\n' || c == '\r' || c == '\t'
}
///|
fn JsonScan::skip_string(self : JsonScan) -> Unit raise {
self.pos += 1
while self.pos < self.chars.length() {
let c = self.chars[self.pos]
self.pos += 1
if c == '"' {
return
}
if c == '\\' {
if self.pos >= self.chars.length() {
fail("Truncated JSON escape")
}
self.pos += 1
}
}
fail("Unterminated JSON string")
}
///|
fn JsonScan::white(self : JsonScan) -> Unit {
while self.pos < self.chars.length() && json_white(self.chars[self.pos]) {
self.pos += 1
}
}
///|
fn JsonScan::string_token(self : JsonScan) -> String raise {
if self.pos >= self.chars.length() || self.chars[self.pos] != '"' {
fail("Expected JSON string")
}
let b = StringBuilder()
b.write_char('"')
self.pos += 1
while self.pos < self.chars.length() {
let c = self.chars[self.pos]
self.pos += 1
b.write_char(c)
if c == '"' {
return b.to_string()
}
if c == '\\' {
if self.pos >= self.chars.length() {
fail("Truncated JSON escape")
}
b.write_char(self.chars[self.pos])
self.pos += 1
}
}
fail("Unterminated JSON string")
}
///|
fn JsonScan::value(self : JsonScan) -> Unit raise {
self.white()
if self.pos >= self.chars.length() {
fail("Truncated JSON value")
}
let c = self.chars[self.pos]
if c == '{' || c == '[' {
self.depth += 1
if self.depth > 64 {
fail("JSON nesting exceeds 64 levels")
}
self.pos += 1
let close = if c == '{' { '}' } else { ']' }
self.white()
let seen : Map[String, Bool] = Map([])
if self.pos < self.chars.length() && self.chars[self.pos] == close {
self.pos += 1
self.depth -= 1
return
}
while self.pos < self.chars.length() {
if c == '{' {
self.white()
let raw_key = self.string_token()
let key = match self.decoded_keys.get(raw_key) {
Some(key) => key
None => {
let key = match @json.parse(raw_key) {
String(s) => s
_ => fail("Invalid object key")
}
self.decoded_keys[raw_key] = key
key
}
}
if seen.contains(key) {
fail("Duplicate JSON object key: " + key)
}
seen[key] = true
self.white()
if self.pos >= self.chars.length() || self.chars[self.pos] != ':' {
fail("Missing JSON colon")
}
self.pos += 1
}
self.value()
self.white()
if self.pos >= self.chars.length() {
fail("Unclosed JSON container")
}
if self.chars[self.pos] == close {
self.pos += 1
self.depth -= 1
return
}
if self.chars[self.pos] != ',' {
fail("Missing JSON comma")
}
self.pos += 1
}
fail("Unclosed JSON container")
} else if c == '"' {
self.skip_string()
} else {
let start = self.pos
while self.pos < self.chars.length() &&
self.chars[self.pos] != ',' &&
self.chars[self.pos] != '}' &&
self.chars[self.pos] != ']' &&
!json_white(self.chars[self.pos]) {
self.pos += 1
}
if self.pos == start {
fail("Invalid JSON value")
}
}
}
///|
fn guarded_json(text : String, limit? : Int = 1048576) -> Json raise {
if text.length() > limit {
fail("JSON input exceeds the size limit")
}
let scan : JsonScan = {
chars: text.iter().collect(),
decoded_keys: Map([]),
pos: 0,
depth: 0,
}
scan.value()
scan.white()
if scan.pos != scan.chars.length() {
fail("Trailing JSON content")
}
@json.parse(text)
}
///|
fn valid_identifier(s : String) -> Bool {
s.length() > 0 &&
s.length() <= 256 &&
!s.iter().any(c => c.to_int() < 32 || c.to_int() == 127)
}