Skip to content

Commit

Permalink
fix(json): Raise exception when json.loads encounters invalid JSON
Browse files Browse the repository at this point in the history
Fix #16
  • Loading branch information
LukeSavefrogs committed Feb 12, 2024
1 parent 0d8e31e commit 8e7906a
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 24 deletions.
75 changes: 52 additions & 23 deletions src/polyfills/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@

__all__ = ["dumps", "dump", "loads", "load"]


class BaseJSONError(Exception):
""" Base exception for the JSON library. """

class JSONDecodeError(BaseJSONError):
""" Thrown when there is an error while decoding a JSON String. """

def escape_string(string):
# type: (str) -> str
"""Escapes a string so that it can be used as a JSON string.
Expand Down Expand Up @@ -211,8 +218,6 @@ def loads(

# ---> Decode JSON object
if json_str.startswith("{"):
result = {}

for index in range(len(json_str)):
char = json_str[index]

Expand Down Expand Up @@ -259,6 +264,9 @@ def loads(


if char in ["{", "["]:
if result is not None and len(nesting_levels) == 0:
raise JSONDecodeError("You are trying to redefine an existing object! Maye you forgot a comma (',')?")

nesting_levels.append(index)
continue

Expand All @@ -271,7 +279,7 @@ def loads(
continue

if len(last_key) != 2:
raise Exception("Unexpected key tuple length: %s" % str(last_key))
raise JSONDecodeError("Malformed JSON input: Object key was not properly closed")

key_name = json_str[last_key[0]:last_key[1]]

Expand All @@ -294,6 +302,11 @@ def loads(
if len(last_value) == 1:
last_value += (index,)

# Initialize the result if not done already
if result is None:
result = {}


# If something is still in the value buffer then add it to the object.
# This is the case, for example, when a non-object value is last.
if len(nesting_levels) == 0 and len(last_value) == 2:
Expand Down Expand Up @@ -323,15 +336,19 @@ def loads(
# ----> End of value
elif char == ",":
if len(last_key) != 2:
raise Exception("Unexpected key tuple length: %s" % str(last_key))
raise JSONDecodeError("Comma was found but with no preceding value: %s" % str(last_key))
if len(last_value) == 0 or len(last_value) > 2:
raise Exception("Unexpected value tuple length: %s" % str(last_value))
raise JSONDecodeError("Malformed JSON input: Object value was not properly closed")

# Values have only the starting index when are neither strings,
# objects or arrays (i.e. bool, int or null).
if len(last_value) == 1:
last_value += (index,)


# Initialize the result if not done already
if result is None:
result = {}


key_name = json_str[last_key[0]:last_key[1]]
value = json_str[last_value[0]:last_value[1]]
Expand All @@ -350,8 +367,6 @@ def loads(

# ---> Decode JSON array
elif json_str.startswith("["):
result = []

for index in range(len(json_str)):
char = json_str[index]

Expand Down Expand Up @@ -386,6 +401,9 @@ def loads(

# ----> JSON Object/Array
if char in ["{", "["]:
if result is not None and len(nesting_levels) == 0:
raise JSONDecodeError("You are trying to redefine an existing object! Maye you forgot a comma (',')?")

nesting_levels.append(index)
continue

Expand All @@ -412,6 +430,10 @@ def loads(
if len(last_value) == 1:
last_value += (index,)

# Initialize the result if not done already
if result is None:
result = []

# If something is still in the value buffer then add it to the object.
# This is the case, for example, when a non-object value is last.
if len(nesting_levels) == 0 and len(last_value) == 2:
Expand All @@ -433,20 +455,24 @@ def loads(
continue


# ----> End of key
# ----> Wrong syntax
elif char == ":":
continue
raise JSONDecodeError("Unexpected character '%s' inside array" % char)

# ----> End of value
elif char == ",":
if len(last_value) == 0 or len(last_value) > 2:
raise Exception("Unexpected value tuple length: %s" % str(last_value))
raise JSONDecodeError("Malformed JSON input: Array value was not properly closed")

# Values have only the starting index when are neither strings,
# objects or arrays (i.e. bool, int or null).
if len(last_value) == 1:
last_value += (index,)


# Initialize the result if not done already
if result is None:
result = []


value = json_str[last_value[0]:last_value[1]]

Expand All @@ -473,7 +499,7 @@ def loads(
#
# TODO: Convert unicode strings to character and viceversa
if json_str.startswith('"') and json_str.endswith('"'):
return str(json_str[1:-1]) \
result = str(json_str[1:-1]) \
.replace('\\"', '"') \
.replace('\\\\', '\\') \
.replace('\\/', '/') \
Expand All @@ -485,35 +511,38 @@ def loads(
.replace('\\\\u', '\\u')

# ---> Numbers
if REGEX_INTEGER.match(json_str):
return int(json_str)
elif REGEX_INTEGER.match(json_str):
result = int(json_str)

elif REGEX_FLOAT.match(json_str):
return float(json_str)
result = float(json_str)

# ---> Boolean
elif json_str == "true":
if truthy_value is not None:
return truthy_value
result = truthy_value
elif IS_POLYFILL_AVAILABLE:
return _bool.bool(1)
else:
return __true__
result = __true__

elif json_str == "false":
if falsy_value is not None:
return falsy_value
result = falsy_value
elif IS_POLYFILL_AVAILABLE:
return _bool.bool(0)
result = _bool.bool(0)
else:
return __false__
result = __false__

# ---> Null
elif json_str == "null":
return None
result = None

else:
raise Exception("Unhandled json value: %s" % json_str)
raise JSONDecodeError("Unhandled json value: %s" % json_str)

if is_inside_string or nesting_levels:
raise JSONDecodeError("Malformed JSON input")

return result

Expand Down
27 changes: 26 additions & 1 deletion src/polyfills/json/tests/loads/test_loads.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ def test_array_with_spaces(self):


class ObjectTestCase(unittest.TestCase):
def test_empty(self):
self.assertEqual(
json.loads(r'{"key": {}}'),
{"key": {}}
)
def test_basic(self):
self.assertEqual(
json.loads('{"string": "value", "key2": 1, "last": true}'),
Expand Down Expand Up @@ -144,5 +149,25 @@ def test_multi_line(self):
# )
self.assertRaises(Exception, lambda: json.loads('/* This is a comment */'))

class InvalidJSONTestCase(unittest.TestCase):
""" Invalid JSON should raise an exception (see #16) """
def test_invalid_structure(self):
self.assertRaises(json.JSONDecodeError, json.loads, r"""{"}""")
self.assertRaises(json.JSONDecodeError, json.loads, r"""{""")
self.assertRaises(json.JSONDecodeError, json.loads, r"""{"key": "val}""")
self.assertRaises(json.JSONDecodeError, json.loads, r"""{}[]""")
self.assertRaises(json.JSONDecodeError, json.loads, r"""{,}""")
self.assertRaises(json.JSONDecodeError, json.loads, r"""[,{}]""")
self.assertRaises(json.JSONDecodeError, json.loads, r"""["key": "val"]""")
self.assertRaises(json.JSONDecodeError, json.loads, r"""["key", """)
self.assertRaises(json.JSONDecodeError, json.loads, r"""[""")

self.assertRaises(Exception, json.loads, r"""[string string]""")

def test_invalid_types(self):
self.assertRaises(Exception, json.loads, r"""undefined""")
self.assertRaises(Exception, json.loads, r"""random_string""")


if __name__ == '__main__':
unittest.main(verbosity=2)
unittest.main(verbosity=2, failfast=False)

0 comments on commit 8e7906a

Please sign in to comment.