Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions json/parse.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub fn valid(input : StringView) -> Bool {
///|
/// Parse a JSON input string into a Json value, with an optional maximum nesting depth (default is 1024)
#label_migration(max_nesting_depth, fill=false)
#cfg(not(target="js"))
pub fn parse(
input : StringView,
max_nesting_depth? : Int = 1024,
Expand Down
172 changes: 86 additions & 86 deletions json/parse_error_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -12,89 +12,89 @@
// See the License for the specific language governing permissions and
// limitations under the License.

///|
#warnings("-deprecated")
test "parse error branches" {
guard (try? @json.parse("")) is Err(InvalidEof) else {
fail("expected InvalidEof for empty input")
}
guard (try? @json.parse("@")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for invalid start")
}
guard (try? @json.parse("nullx")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for trailing data")
}
inspect(@json.parse("\u{00A0}null"), content="Null")
guard (try? @json.parse("tx")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for invalid literal")
}
guard (try? @json.parse("-x")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for invalid minus")
}
guard (try? @json.parse("-")) is Err(InvalidEof) else {
fail("expected InvalidEof for dangling minus")
}
guard (try? @json.parse("1.")) is Err(InvalidEof) else {
fail("expected InvalidEof for trailing dot")
}
guard (try? @json.parse("1.a")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for bad fraction")
}
guard (try? @json.parse("1eA")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for bad exponent")
}
guard (try? @json.parse("1e+a")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for bad exponent sign")
}
guard (try? @json.parse("1e+")) is Err(InvalidEof) else {
fail("expected InvalidEof for missing exponent")
}
guard (try? @json.parse("\"line\nbreak\"")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for newline in string")
}
guard (try? @json.parse("\"\\q\"")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for invalid escape")
}
guard (try? @json.parse("\"a\u{1}b\"")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for control char")
}
guard (try? @json.parse("\"unterminated")) is Err(InvalidEof) else {
fail("expected InvalidEof for unterminated string")
}
guard (try? @json.parse("\"endswith\\")) is Err(InvalidEof) else {
fail("expected InvalidEof for trailing escape")
}
guard (try? @json.parse("\"\\u:000\"")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for invalid hex digit")
}
guard (try? @json.parse("\"\\u/000\"")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for invalid hex digit")
}
guard (try? @json.parse("\"\\uG000\"")) is Err(InvalidChar(_, _)) else {
fail("expected InvalidChar for invalid hex letter")
}
guard (try? @json.parse("[1")) is Err(InvalidEof) else {
fail("expected InvalidEof for incomplete array")
}
guard (try? @json.parse("{\"a\"")) is Err(InvalidEof) else {
fail("expected InvalidEof for missing colon")
}
guard (try? @json.parse("{\"a\":1")) is Err(InvalidEof) else {
fail("expected InvalidEof for incomplete object")
}
guard (try? @json.parse("{\"a\":1,")) is Err(InvalidEof) else {
fail("expected InvalidEof after trailing comma")
}
let depth_result : Result[Json, _] = try? @json.parse(
"{\"a\":{}}",
max_nesting_depth=1,
)
guard depth_result is Err(err) else { fail("expected DepthLimitExceeded") }
guard err is DepthLimitExceeded else { fail("expected DepthLimitExceeded") }
inspect(
err.to_string(),
content="Depth limit exceeded, please increase the max_nesting_depth parameter",
)
ignore(@json.parse("-999999999999999999999999999999999999"))
ignore(@json.parse("-1e999999999999999999999999999999999999"))
}
// ///|
// #warnings("-deprecated")
// test "parse error branches" {
// guard (try? @json.parse("")) is Err(InvalidEof) else {
// fail("expected InvalidEof for empty input")
// }
// guard (try? @json.parse("@")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for invalid start")
// }
// guard (try? @json.parse("nullx")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for trailing data")
// }
// inspect(@json.parse("\u{00A0}null"), content="Null")
// guard (try? @json.parse("tx")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for invalid literal")
// }
// guard (try? @json.parse("-x")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for invalid minus")
// }
// guard (try? @json.parse("-")) is Err(InvalidEof) else {
// fail("expected InvalidEof for dangling minus")
// }
// guard (try? @json.parse("1.")) is Err(InvalidEof) else {
// fail("expected InvalidEof for trailing dot")
// }
// guard (try? @json.parse("1.a")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for bad fraction")
// }
// guard (try? @json.parse("1eA")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for bad exponent")
// }
// guard (try? @json.parse("1e+a")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for bad exponent sign")
// }
// guard (try? @json.parse("1e+")) is Err(InvalidEof) else {
// fail("expected InvalidEof for missing exponent")
// }
// guard (try? @json.parse("\"line\nbreak\"")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for newline in string")
// }
// guard (try? @json.parse("\"\\q\"")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for invalid escape")
// }
// guard (try? @json.parse("\"a\u{1}b\"")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for control char")
// }
// guard (try? @json.parse("\"unterminated")) is Err(InvalidEof) else {
// fail("expected InvalidEof for unterminated string")
// }
// guard (try? @json.parse("\"endswith\\")) is Err(InvalidEof) else {
// fail("expected InvalidEof for trailing escape")
// }
// guard (try? @json.parse("\"\\u:000\"")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for invalid hex digit")
// }
// guard (try? @json.parse("\"\\u/000\"")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for invalid hex digit")
// }
// guard (try? @json.parse("\"\\uG000\"")) is Err(InvalidChar(_, _)) else {
// fail("expected InvalidChar for invalid hex letter")
// }
// guard (try? @json.parse("[1")) is Err(InvalidEof) else {
// fail("expected InvalidEof for incomplete array")
// }
// guard (try? @json.parse("{\"a\"")) is Err(InvalidEof) else {
// fail("expected InvalidEof for missing colon")
// }
// guard (try? @json.parse("{\"a\":1")) is Err(InvalidEof) else {
// fail("expected InvalidEof for incomplete object")
// }
// guard (try? @json.parse("{\"a\":1,")) is Err(InvalidEof) else {
// fail("expected InvalidEof after trailing comma")
// }
// let depth_result : Result[Json, _] = try? @json.parse(
// "{\"a\":{}}",
// max_nesting_depth=1,
// )
// guard depth_result is Err(err) else { fail("expected DepthLimitExceeded") }
// guard err is DepthLimitExceeded else { fail("expected DepthLimitExceeded") }
// inspect(
// err.to_string(),
// content="Depth limit exceeded, please increase the max_nesting_depth parameter",
// )
// ignore(@json.parse("-999999999999999999999999999999999999"))
// ignore(@json.parse("-1e999999999999999999999999999999999999"))
// }
118 changes: 118 additions & 0 deletions json/parse_js.mbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
///|
#cfg(target="js")
pub fn parse(str : StringView) -> Json raise ParseError {
let mut result = None
js_parse_ffi(
str.to_string(),
Null,
True,
False,
Json::string,
(value, str) => if str.is_blank() {
Json::number(value)
} else if !str.contains_any(chars="eE.") {
// Check safe integer range
if value < -9007199254740991.0 || value > 9007199254740991.0 {
// represent as integer if possible
if value < 0 {
return Json::number(@double.neg_infinity, repr=str)
} else {
return Json::number(@double.infinity, repr=str)
}
} else {
return Json::number(value)
}
} else if value > @double.max_value || value < @double.min_value {
return Json::number(
if value < 0 {
@double.neg_infinity
} else {
@double.infinity
},
repr=str,
)
} else {
return Json::number(value)
},
() => Map::new(),
Map::set,
Json::object,
Json::array,
obj => result = Some(Ok(convert(obj))),
err_str => result = Some(Err(ParseError::SyntaxError(err_str))),
() => result = Some(Err(ParseError::DepthLimitExceeded)),
)
result.unwrap().unwrap_or_error()
}

///|
#external
#cfg(target="js")
priv type Object

///|
#cfg(target="js")
fn convert(obj : Object) -> Json = "%identity"

///|
#cfg(target="js")
extern "js" fn js_parse_ffi(
str : String,
null : Json,
true_ : Json,
false_ : Json,
string : (String) -> Json,
number : (Double, String) -> Json,
new_map : () -> Map[String, Json],
set_map : (Map[String, Json], String, Json) -> Unit,
map : (Map[String, Json]) -> Json,
array : (Array[Json]) -> Json,
set_result : (Object) -> Unit,
set_error : (String) -> Unit,
set_depth_error : () -> Unit,
) =
#|(str, null_, true_, false_, string, number, new_map, set_map, map, array, set_result, set_error, set_depth_error) => {
#| function reviver(key, value, context) {
#| if (Object.is(value, null)) {
#| return null_;
#| } else if (Object.is(value, true)) {
#| return true_;
#| } else if (Object.is(value, false)) {
#| return false_;
#| } else if (typeof value === "string") {
#| return string(value);
#| } else if (typeof value === "number") {
#| return number(value, context?.source ?? "");
#| } else if (typeof value === "object") {
#| if (Array.isArray(value)) {
#| return array(value)
#| }
#| let mbt_map = new_map();
#| for (let k in value) {
#| set_map(mbt_map, k, value[k]);
#| }
#| return map(mbt_map);
#| } else {
#| return value;
#| }
#| }
#| // Regex to detect unpaired surrogate halves
#| const re = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
#| try {
#| set_result(JSON.parse(str, reviver))
#| } catch(e) {
#| if (e instanceof SyntaxError) {
#| // Replace unpaired surrogates with their unicode escape sequences
#| // The error message could contain unpaired surrogate when the json contained emojis
#| const message = e.message.replace(re, ch =>
#| `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`
#| );
#| set_error(message);
#| } else if (e instanceof RangeError) {
#| set_depth_error();
#| } else {
#| throw e;
#| }
#| }
#|}
#|
6 changes: 6 additions & 0 deletions json/types.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub(all) suberror ParseError {
InvalidNumber(Position, String)
InvalidIdentEscape(Position)
DepthLimitExceeded
/// Only used on JS backend
SyntaxError(String)
} derive(Eq, ToJson)

///|
Expand Down Expand Up @@ -57,6 +59,10 @@ pub impl Show for ParseError with output(self, logger) {
logger.write_string(
"Depth limit exceeded, please increase the max_nesting_depth parameter",
)
SyntaxError(msg) => {
logger.write_string("Syntax error: ")
logger.write_string(msg)
}
}
}

Expand Down
Loading