Skip to content
Merged
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
10 changes: 8 additions & 2 deletions json/internal_types.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ priv struct ParseContext {
mut offset : Int
input : String
end_offset : Int
mut remaining_available_depth : Int
}

///|
fn ParseContext::make(input : String) -> ParseContext {
{ offset: 0, input, end_offset: input.length() }
fn ParseContext::make(input : String, max_nesting_depth : Int) -> ParseContext {
{
offset: 0,
input,
end_offset: input.length(),
remaining_available_depth: max_nesting_depth,
}
}

///|
Expand Down
25 changes: 25 additions & 0 deletions json/json_test.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -526,3 +526,28 @@ test "nested json" {
},
})
}

///|
test "call stack overflow" {
let depth = 50000
let json_string = StringBuilder::new()
for _ in 0..=depth {
json_string.write_char('[')
}
for _ in 0..=depth {
json_string.write_char(']')
}
let _ = @json.parse(json_string.to_string()) catch { _ => null }
let depth = 1500
let json_string = StringBuilder::new()
for _ in 0..=depth {
json_string.write_char('[')
}
for _ in 0..=depth {
json_string.write_char(']')
}
let _ = @json.parse(json_string.to_string(), max_nesting_depth=2000) catch {
_ => null
}

}
4 changes: 2 additions & 2 deletions json/lex_misc.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ fn ParseContext::expect_ascii_char(

///|
test "expect_char" {
let ctx = ParseContext::make("abc")
let ctx = ParseContext::make("abc", 1)
ctx.expect_char('a')
ctx.expect_char('b')
ctx.expect_char('c')
Expand All @@ -103,7 +103,7 @@ test "expect_char" {
///|
test "expect_char with surrogate pair" {
// "\uD83D\uDE00" // todo: shall we allow this?
let ctx = ParseContext::make("a\u{1F600}bc\u{1F600}c")
let ctx = ParseContext::make("a\u{1F600}bc\u{1F600}c", 1)
ctx.expect_char('a')
ctx.expect_char((0x1F600).unsafe_to_char())
ctx.expect_char('b')
Expand Down
36 changes: 30 additions & 6 deletions json/parse.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ pub fn valid(input : String) -> Bool {
}

///|
pub fn parse(input : String) -> Json raise ParseError {
let ctx = ParseContext::make(input)
/// Parse a JSON input string into a Json value, with an optional maximum nesting depth (default is 1024)
pub fn parse(
input : String,
max_nesting_depth? : Int = 1024,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason for this number, i.e. what's the number picked by the others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow python / java(Jackson), the default limit is around 1000.

) -> Json raise ParseError {
let ctx = ParseContext::make(input, max_nesting_depth)
let val = ctx.parse_value()
ctx.lex_skip_whitespace()
if ctx.offset >= ctx.end_offset {
Expand Down Expand Up @@ -59,15 +63,25 @@ fn ParseContext::parse_value2(

///|
fn ParseContext::parse_object(ctx : ParseContext) -> Json raise ParseError {
if ctx.remaining_available_depth <= 0 {
raise DepthLimitExceeded
}
ctx.remaining_available_depth -= 1
let map = Map::new()
loop ctx.lex_property_name() {
RBrace => Json::object(map)
RBrace => {
ctx.remaining_available_depth += 1
Json::object(map)
}
String(name) => {
ctx.lex_after_property_name()
map[name] = ctx.parse_value()
match ctx.lex_after_object_value() {
Comma => continue ctx.lex_property_name2()
RBrace => Json::object(map)
RBrace => {
ctx.remaining_available_depth += 1
Json::object(map)
}
_ => abort("unreachable")
}
}
Expand All @@ -77,15 +91,25 @@ fn ParseContext::parse_object(ctx : ParseContext) -> Json raise ParseError {

///|
fn ParseContext::parse_array(ctx : ParseContext) -> Json raise ParseError {
if ctx.remaining_available_depth <= 0 {
raise DepthLimitExceeded
}
ctx.remaining_available_depth -= 1
let vec = []
loop ctx.lex_value(allow_rbracket=true) {
RBracket => Json::array(vec)
RBracket => {
ctx.remaining_available_depth += 1
Json::array(vec)
}
tok => {
vec.push(ctx.parse_value2(tok))
let tok2 = ctx.lex_after_array_value()
match tok2 {
Comma => continue ctx.lex_value(allow_rbracket=false)
RBracket => Json::array(vec)
RBracket => {
ctx.remaining_available_depth += 1
Json::array(vec)
}
_ => abort("unreachable")
}
}
Expand Down
3 changes: 2 additions & 1 deletion json/pkg.generated.mbti
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ fn[T : FromJson] from_json(Json, path? : JsonPath) -> T raise JsonDecodeError
#callsite(autofill(args_loc, loc))
fn inspect(&ToJson, content? : Json, loc~ : SourceLoc, args_loc~ : ArgsLoc) -> Unit raise InspectError

fn parse(String) -> Json raise ParseError
fn parse(String, max_nesting_depth? : Int) -> Json raise ParseError

fn valid(String) -> Bool

Expand All @@ -22,6 +22,7 @@ pub(all) suberror ParseError {
InvalidEof
InvalidNumber(Position, String)
InvalidIdentEscape(Position)
DepthLimitExceeded
}
impl Eq for ParseError
impl Show for ParseError
Expand Down
5 changes: 5 additions & 0 deletions json/types.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub(all) suberror ParseError {
InvalidEof
InvalidNumber(Position, String)
InvalidIdentEscape(Position)
DepthLimitExceeded
} derive(Eq, ToJson)

///|
Expand Down Expand Up @@ -52,6 +53,10 @@ pub impl Show for ParseError with output(self, logger) {
..write_object(line)
..write_string(", column ")
..write_object(column)
DepthLimitExceeded =>
logger.write_string(
"Depth limit exceeded, please increase the max_nesting_depth parameter",
)
}
}

Expand Down
Loading