diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 8ad9b57..acaa97b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,6 +13,8 @@ jobs: matrix: python-version: ['2.7', '3.7', '3.8', '3.10'] runs-on: [ubuntu-latest] + container: + image: "python:${{ matrix.python-version }}-buster" env: PYTHON: ${{ matrix.python-version }} steps: @@ -20,14 +22,14 @@ jobs: with: fetch-depth: 0 # fetch all history for setuptools_scm to be able to read tags - - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Install python dependencies run: | - pip install wheel build tox - pip install .[dev] + apt-get update + apt-get -y install sudo + pip install --upgrade pip + sudo chown root . + sudo -H pip install wheel build tox + sudo -H pip install .[dev] - name: Determine pyenv id: pyenv diff --git a/llsd/base.py b/llsd/base.py index cbeab54..e7204ca 100644 --- a/llsd/base.py +++ b/llsd/base.py @@ -31,6 +31,8 @@ ALL_CHARS = str(bytearray(range(256))) if PY2 else bytes(range(256)) +MAX_FORMAT_DEPTH = 200 +MAX_PARSE_DEPTH = 200 class _LLSD: __metaclass__ = abc.ABCMeta @@ -209,7 +211,7 @@ def _parse_datestr(datestr): return datetime.datetime(year, month, day, hour, minute, second, usec) -def _bool_to_python(node): +def _bool_to_python(node, depth=0): "Convert boolean node to a python object." val = node.text or '' try: @@ -220,7 +222,7 @@ def _bool_to_python(node): return bool(val) -def _int_to_python(node): +def _int_to_python(node, depth=0): "Convert integer node to a python object." val = node.text or '' if not val.strip(): @@ -228,7 +230,7 @@ def _int_to_python(node): return int(val) -def _real_to_python(node): +def _real_to_python(node, depth=0): "Convert floating point node to a python object." val = node.text or '' if not val.strip(): @@ -236,19 +238,19 @@ def _real_to_python(node): return float(val) -def _uuid_to_python(node): +def _uuid_to_python(node, depth=0): "Convert uuid node to a python object." if node.text: return uuid.UUID(hex=node.text) return uuid.UUID(int=0) -def _str_to_python(node): +def _str_to_python(node, depth=0): "Convert string node to a python object." return node.text or '' -def _bin_to_python(node): +def _bin_to_python(node, depth=0): base = node.get('encoding') or 'base64' try: if base == 'base16': @@ -267,7 +269,7 @@ def _bin_to_python(node): return LLSDParseError("Bad binary data: " + str(exc)) -def _date_to_python(node): +def _date_to_python(node, depth=0): "Convert date node to a python object." val = node.text or '' if not val: @@ -275,30 +277,30 @@ def _date_to_python(node): return _parse_datestr(val) -def _uri_to_python(node): +def _uri_to_python(node, depth=0): "Convert uri node to a python object." val = node.text or '' return uri(val) -def _map_to_python(node): +def _map_to_python(node, depth=0): "Convert map node to a python object." result = {} for index in range(len(node))[::2]: if node[index].text is None: - result[''] = _to_python(node[index+1]) + result[''] = _to_python(node[index+1], depth+1) else: - result[node[index].text] = _to_python(node[index+1]) + result[node[index].text] = _to_python(node[index+1], depth+1) return result -def _array_to_python(node): +def _array_to_python(node, depth=0): "Convert array node to a python object." - return [_to_python(child) for child in node] + return [_to_python(child, depth+1) for child in node] NODE_HANDLERS = dict( - undef=lambda x: None, + undef=lambda x,y: None, boolean=_bool_to_python, integer=_int_to_python, real=_real_to_python, @@ -312,9 +314,12 @@ def _array_to_python(node): ) -def _to_python(node): +def _to_python(node, depth=0): "Convert node to a python object." - return NODE_HANDLERS[node.tag](node) + if depth > MAX_PARSE_DEPTH: + raise LLSDParseError("Cannot parse depth of more than %d" % MAX_PARSE_DEPTH) + + return NODE_HANDLERS[node.tag](node, depth) class LLSDBaseFormatter(object): diff --git a/llsd/serde_binary.py b/llsd/serde_binary.py index 6f0d93e..e4ac7c5 100644 --- a/llsd/serde_binary.py +++ b/llsd/serde_binary.py @@ -5,7 +5,7 @@ import uuid from llsd.base import (_LLSD, LLSDBaseParser, LLSDSerializationError, BINARY_HEADER, - _str_to_bytes, binary, is_integer, is_string, uri) + MAX_FORMAT_DEPTH, MAX_PARSE_DEPTH, _str_to_bytes, binary, is_integer, is_string, uri) try: @@ -15,14 +15,13 @@ # Python 3: 'range()' is already lazy pass - class LLSDBinaryParser(LLSDBaseParser): """ Parse application/llsd+binary to a python object. See http://wiki.secondlife.com/wiki/LLSD#Binary_Serialization """ - __slots__ = ['_dispatch', '_keep_binary'] + __slots__ = ['_dispatch', '_keep_binary', '_depth'] def __init__(self): super(LLSDBinaryParser, self).__init__() @@ -63,6 +62,7 @@ def __init__(self): # entries in _dispatch. for c, func in _dispatch_dict.items(): self._dispatch[ord(c)] = func + self._depth = 0 def parse(self, something, ignore_binary = False): """ @@ -82,6 +82,9 @@ def parse(self, something, ignore_binary = False): def _parse(self): "The actual parser which is called recursively when necessary." + if self._depth > MAX_PARSE_DEPTH: + self._error("Parse depth exceeded maximum depth of %d." % MAX_PARSE_DEPTH) + cc = self._getc() try: func = self._dispatch[ord(cc)] @@ -97,6 +100,7 @@ def _parse_map(self): count = 0 cc = self._getc() key = b'' + self._depth += 1 while (cc != b'}') and (count < size): if cc == b'k': key = self._parse_string() @@ -110,16 +114,19 @@ def _parse_map(self): cc = self._getc() if cc != b'}': self._error("invalid map close token") + self._depth -= 1 return rv def _parse_array(self): "Parse a single llsd array" rv = [] + self._depth += 1 size = struct.unpack("!i", self._getc(4))[0] for count in range(size): rv.append(self._parse()) if self._getc() != b']': self._error("invalid array close token") + self._depth -= 1 return rv def _parse_string(self): @@ -164,15 +171,19 @@ def format_binary(something): def write_binary(stream, something): stream.write(b'\n') - _write_binary_recurse(stream, something) + _write_binary_recurse(stream, something, 0) -def _write_binary_recurse(stream, something): +def _write_binary_recurse(stream, something, depth): "Binary formatter workhorse." + + if depth > MAX_FORMAT_DEPTH: + raise LLSDSerializationError("Cannot serialize depth of more than %d" % MAX_FORMAT_DEPTH) + if something is None: stream.write(b'!') elif isinstance(something, _LLSD): - _write_binary_recurse(stream, something.thing) + _write_binary_recurse(stream, something.thing, depth) elif isinstance(something, bool): stream.write(b'1' if something else b'0') elif is_integer(something): @@ -202,27 +213,27 @@ def _write_binary_recurse(stream, something): seconds_since_epoch = calendar.timegm(something.timetuple()) stream.writelines([b'd', struct.pack(' MAX_PARSE_DEPTH: + self._error("Parse depth exceeded max of %d" % MAX_PARSE_DEPTH) try: func = self._dispatch[ord(cc)] except IndexError: @@ -182,6 +185,7 @@ def _parse_map(self, cc): rv = {} key = b'' found_key = False + self._depth += 1 # skip the beginning '{' cc = self._getc() while (cc != b'}'): @@ -207,6 +211,7 @@ def _parse_map(self, cc): else: self._error("missing separator") cc = self._getc() + self._depth -= 1 return rv @@ -217,6 +222,7 @@ def _parse_array(self, cc): array: [ object, object, object ] """ rv = [] + self._depth += 1 # skip the beginning '[' cc = self._getc() while (cc != b']'): @@ -227,7 +233,7 @@ def _parse_array(self, cc): continue rv.append(self._parse(cc)) cc = self._getc() - + self._depth -= 1 return rv def _parse_uuid(self, cc): @@ -411,6 +417,11 @@ class LLSDNotationFormatter(LLSDBaseFormatter): See http://wiki.secondlife.com/wiki/LLSD#Notation_Serialization """ + + def __init__(self): + super(LLSDNotationFormatter, self).__init__() + self._depth = 0 + def _LLSD(self, v): return self._generate(v.thing) def _UNDEF(self, v): @@ -443,18 +454,22 @@ def _DATE(self, v): def _ARRAY(self, v): self.stream.write(b'[') delim = b'' + self._depth += 1 for item in v: self.stream.write(delim) self._generate(item) delim = b',' + self._depth -= 1 self.stream.write(b']') def _MAP(self, v): self.stream.write(b'{') delim = b'' + self._depth += 1 for key, value in v.items(): self.stream.writelines([delim, b"'", self._esc(UnicodeType(key)), b"':"]) self._generate(value) delim = b',' + self._depth -= 1 self.stream.write(b'}') def _esc(self, data, quote=b"'"): @@ -466,6 +481,9 @@ def _generate(self, something): :param something: a python object (typically a dict) to be serialized. """ + if self._depth > MAX_FORMAT_DEPTH: + raise LLSDSerializationError("Cannot serialize depth of more than %d" % MAX_FORMAT_DEPTH) + t = type(something) handler = self.type_map.get(t) if handler: diff --git a/llsd/serde_xml.py b/llsd/serde_xml.py index 7dfeaa2..a7da4e7 100644 --- a/llsd/serde_xml.py +++ b/llsd/serde_xml.py @@ -1,11 +1,10 @@ import base64 import io import re -import types from llsd.base import (_LLSD, ALL_CHARS, LLSDBaseParser, LLSDBaseFormatter, XML_HEADER, - LLSDParseError, LLSDSerializationError, UnicodeType, - _format_datestr, _str_to_bytes, _to_python, is_unicode) + MAX_FORMAT_DEPTH, LLSDParseError, LLSDSerializationError, UnicodeType, + _format_datestr, _str_to_bytes, _to_python, is_unicode, PY2) from llsd.fastest_elementtree import ElementTreeError, fromstring, parse as _parse INVALID_XML_BYTES = b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c'\ @@ -14,7 +13,21 @@ INVALID_XML_RE = re.compile(r'[\x00-\x08\x0b\x0c\x0e-\x1f]') +XML_ESC_TRANS = {} +if not PY2: + XML_ESC_TRANS = str.maketrans({'&': '&', + '<':'<', + '>':'>', + u'\uffff':None, # cannot be parsed + u'\ufffe':None}) # cannot be parsed + + for x in INVALID_XML_BYTES: + XML_ESC_TRANS[x] = None + def remove_invalid_xml_bytes(b): + """ + Remove characters that aren't allowed in xml. + """ try: # Dropping chars that cannot be parsed later on. The # translate() function was benchmarked to be the fastest way @@ -25,6 +38,24 @@ def remove_invalid_xml_bytes(b): # unit tests) return INVALID_XML_RE.sub('', b) +# only python2, which is not covered by coverage tests +def xml_esc(v): # pragma: no cover + "Escape string or unicode object v for xml output" + + # Use is_unicode() instead of is_string() because in python 2, str is + # bytes, not unicode, and should not be "encode()"d. Attempts to + # encode("utf-8") a bytes type will result in an implicit + # decode("ascii") that will throw a UnicodeDecodeError if the string + # contains non-ascii characters. + if is_unicode(v): + # we need to drop these invalid characters because they + # cannot be parsed (and encode() doesn't drop them for us) + v = v.replace(u'\uffff', u'') + v = v.replace(u'\ufffe', u'') + v = v.encode('utf-8') + v = remove_invalid_xml_bytes(v) + return v.replace(b'&',b'&').replace(b'<',b'<').replace(b'>',b'>') + class LLSDXMLFormatter(LLSDBaseFormatter): """ @@ -37,78 +68,83 @@ class LLSDXMLFormatter(LLSDBaseFormatter): this class since the module level format_xml() is the most convenient interface to this functionality. """ - def _elt(self, name, contents=None): - """ - Serialize a single element. - If 'contents' is omitted, write . - If 'contents' is bytes, write contents. - If 'contents' is str, write contents.encode('utf8'). - """ - if not contents: - self.stream.writelines([b"<", name, b" />"]) - else: - self.stream.writelines([b"<", name, b">", - _str_to_bytes(contents), - b""]) - - def xml_esc(self, v): - "Escape string or unicode object v for xml output" - - # Use is_unicode() instead of is_string() because in python 2, str is - # bytes, not unicode, and should not be "encode()"d. Attempts to - # encode("utf-8") a bytes type will result in an implicit - # decode("ascii") that will throw a UnicodeDecodeError if the string - # contains non-ascii characters. - if is_unicode(v): - # we need to drop these invalid characters because they - # cannot be parsed (and encode() doesn't drop them for us) - v = v.replace(u'\uffff', u'') - v = v.replace(u'\ufffe', u'') - v = v.encode('utf-8') - v = remove_invalid_xml_bytes(v) - return v.replace(b'&',b'&').replace(b'<',b'<').replace(b'>',b'>') + def __init__(self, indent_atom = b'', eol = b''): + "Construct a serializer." + # Call the super class constructor so that we have the type map + super(LLSDXMLFormatter, self).__init__() + self._indent_atom = indent_atom + self._eol = eol + self._depth = 1 + + def _indent(self): + pass def _LLSD(self, v): return self._generate(v.thing) def _UNDEF(self, _v): - return self._elt(b'undef') + self.stream.writelines([b'', self._eol]) def _BOOLEAN(self, v): if v: - return self._elt(b'boolean', b'true') - else: - return self._elt(b'boolean', b'false') + return self.stream.writelines([b'true', self._eol]) + self.stream.writelines([b'false', self._eol]) def _INTEGER(self, v): - return self._elt(b'integer', str(v)) + self.stream.writelines([b'', str(v).encode('utf-8'), b'', self._eol]) def _REAL(self, v): - return self._elt(b'real', repr(v)) + self.stream.writelines([b'', str(v).encode('utf-8'), b'', self._eol]) def _UUID(self, v): if v.int == 0: - return self._elt(b'uuid') - else: - return self._elt(b'uuid', str(v)) + return self.stream.writelines([b'', self._eol]) + self.stream.writelines([b'', str(v).encode('utf-8'), b'', self._eol]) def _BINARY(self, v): - return self._elt(b'binary', base64.b64encode(v).strip()) - def _STRING(self, v): - return self._elt(b'string', self.xml_esc(v)) - def _URI(self, v): - return self._elt(b'uri', self.xml_esc(str(v))) + self.stream.writelines([b'', base64.b64encode(v).strip(), b'', self._eol]) + + if PY2: + def _STRING(self, v): + return self.stream.writelines([b'', _str_to_bytes(xml_esc(v)), b'', self._eol]) + def _URI(self, v): + return self.stream.writelines([b'', _str_to_bytes(xml_esc(v)), b'', self._eol]) + else: + def _STRING(self, v): + self.stream.writelines([b'', v.translate(XML_ESC_TRANS).encode('utf-8'), b'', self._eol]) + def _URI(self, v): + self.stream.writelines([b'', str(v).translate(XML_ESC_TRANS).encode('utf-8'), b'', self._eol]) + def _DATE(self, v): - return self._elt(b'date', _format_datestr(v)) + self.stream.writelines([b'', _format_datestr(v), b'', self._eol]) def _ARRAY(self, v): - self.stream.write(b'') + self.stream.writelines([b'', self._eol]) + self._depth += 1 for item in v: + self._indent() self._generate(item) - self.stream.write(b'') + self._depth -= 1 + self.stream.writelines([b'', self._eol]) def _MAP(self, v): - self.stream.write(b'') + self.stream.writelines([b'', self._eol]) + self._depth += 1 for key, value in v.items(): - self._elt(b'key', self.xml_esc(UnicodeType(key))) + self._indent() + if PY2: # pragma: no cover + self.stream.writelines([b'', + xml_esc(UnicodeType(key)), + b'', + self._eol]) + else: + self.stream.writelines([b'', + UnicodeType(key).translate(XML_ESC_TRANS).encode('utf-8'), + b'', + self._eol]) + self._indent() self._generate(value) - self.stream.write(b'') + self._depth -= 1 + self._indent() + self.stream.writelines([b'', self._eol]) def _generate(self, something): "Generate xml from a single python object." + if self._depth - 1 > MAX_FORMAT_DEPTH: + raise LLSDSerializationError("Cannot serialize depth of more than %d" % MAX_FORMAT_DEPTH) t = type(something) if t in self.type_map: return self.type_map[t](something) @@ -121,11 +157,10 @@ def _generate(self, something): def _write(self, something): """ Serialize a python object to self.stream as application/llsd+xml. - :param something: A python object (typically a dict) to be serialized. """ - self.stream.write(b'' - b'') + self.stream.writelines([b'', self._eol, + b'', self._eol]) self._generate(something) self.stream.write(b'') @@ -143,59 +178,14 @@ class LLSDXMLPrettyFormatter(LLSDXMLFormatter): This class is not necessarily suited for serializing very large objects. It sorts on dict (llsd map) keys alphabetically to ease human reading. """ - def __init__(self, indent_atom = None): + def __init__(self, indent_atom = b' ', eol = b'\n'): "Construct a pretty serializer." # Call the super class constructor so that we have the type map - super(LLSDXMLPrettyFormatter, self).__init__() - - # Private data used for indentation. - self._indent_level = 1 - if indent_atom is None: - self._indent_atom = b' ' - else: - self._indent_atom = indent_atom + super(LLSDXMLPrettyFormatter, self).__init__(indent_atom = indent_atom, eol = eol) def _indent(self): "Write an indentation based on the atom and indentation level." - self.stream.writelines([self._indent_atom] * self._indent_level) - - def _ARRAY(self, v): - "Recursively format an array with pretty turned on." - self.stream.write(b'\n') - self._indent_level += 1 - for item in v: - self._indent() - self._generate(item) - self.stream.write(b'\n') - self._indent_level -= 1 - self._indent() - self.stream.write(b'') - - def _MAP(self, v): - "Recursively format a map with pretty turned on." - self.stream.write(b'\n') - self._indent_level += 1 - # sorted list of keys - for key in sorted(v): - self._indent() - self._elt(b'key', UnicodeType(key)) - self.stream.write(b'\n') - self._indent() - self._generate(v[key]) - self.stream.write(b'\n') - self._indent_level -= 1 - self._indent() - self.stream.write(b'') - - def _write(self, something): - """ - Serialize a python object to self.stream as 'pretty' application/llsd+xml. - - :param something: a python object (typically a dict) to be serialized. - """ - self.stream.write(b'\n') - self._generate(something) - self.stream.write(b'\n') + self.stream.writelines([self._indent_atom] * self._depth) def format_pretty_xml(something): diff --git a/tests/bench.py b/tests/bench.py index f907997..722e67b 100644 --- a/tests/bench.py +++ b/tests/bench.py @@ -45,6 +45,9 @@ """ _bench_data = llsd.parse_xml(BENCH_DATA_XML) + + + BENCH_DATA_BINARY = llsd.format_binary(_bench_data) BENCH_DATA_NOTATION = llsd.format_notation(_bench_data) @@ -78,6 +81,40 @@ def binary_stream(): f.seek(0) yield f +def build_deep_xml(): + deep_data = {} + curr_data = deep_data + for i in range(198): + curr_data["curr_data"] = {} + curr_data["integer"] = 7 + curr_data["string"] = "string" + curr_data["map"] = { "item1": 2.345, "item2": [1,2,3], "item3": {"item4": llsd.uri("http://foo.bar.com")}} + curr_data = curr_data["curr_data"] + + return deep_data +_deep_bench_data = build_deep_xml() + +def build_wide_xml(): + + wide_xml = b""" +wide_array" +""" + wide_data = {} + for i in range(100000): + wide_data["item"+str(i)] = {"item1":2.345, "item2": [1,2,3], "item3": "string", "item4":{"subitem": llsd.uri("http://foo.bar.com")}} + return wide_data +_wide_bench_data = build_wide_xml() + +def build_wide_array_xml(): + + wide_xml = b""" +wide_array" +""" + wide_data = [] + for i in range(100000): + wide_data.append([2.345,[1,2,3], "string", [llsd.uri("http://foo.bar.com")]]) + return wide_data +_wide_array_bench_data = build_wide_array_xml() def bench_stream(parse, stream): ret = parse(stream) @@ -125,3 +162,35 @@ def test_format_notation(benchmark): def test_format_binary(benchmark): benchmark(llsd.format_binary, _bench_data) + +def test_format_xml_deep(benchmark): + benchmark(llsd.format_xml, _deep_bench_data) + +def test_format_xml_wide(benchmark): + benchmark(llsd.format_xml, _wide_bench_data) + +def test_format_notation_deep(benchmark): + benchmark(llsd.format_notation, _deep_bench_data) + +def test_format_notation_wide(benchmark): + benchmark(llsd.format_notation, _wide_bench_data) + +def test_format_notation_wide_array(benchmark): + benchmark(llsd.format_notation, _wide_array_bench_data) + +def test_format_binary_deep(benchmark): + benchmark(llsd.format_binary, _deep_bench_data) + +def test_format_binary_wide(benchmark): + benchmark(llsd.format_binary, _wide_bench_data) + +def test_format_binary_wide_array(benchmark): + benchmark(llsd.format_binary, _wide_array_bench_data) + +def test_parse_xml_deep(benchmark): + deep_data = llsd.format_xml(_deep_bench_data) + benchmark(llsd.parse_xml, deep_data) + +def test_parse_binary_deep(benchmark): + deep_data = llsd.format_binary(_deep_bench_data) + benchmark(llsd.parse_binary, deep_data) diff --git a/tests/llsd_test.py b/tests/llsd_test.py index 46f64fe..b274a39 100644 --- a/tests/llsd_test.py +++ b/tests/llsd_test.py @@ -16,7 +16,7 @@ import pytest import llsd -from llsd.base import PY2, is_integer, is_string, is_unicode +from llsd.base import PY2, is_integer, is_string, is_unicode, MAX_FORMAT_DEPTH, MAX_PARSE_DEPTH from llsd.serde_xml import remove_invalid_xml_bytes from tests.fuzz import LLSDFuzzer @@ -527,6 +527,26 @@ def testParseNotationHalfTruncatedHex(self): def testParseNotationInvalidHex(self): self.assertRaises(llsd.LLSDParseError, self.llsd.parse, b"'\\xzz'") + def testDeepMap(self): + """ + Test formatting of a deeply nested map + """ + + test_map = {"foo":"bar", "depth":0} + max_depth = MAX_FORMAT_DEPTH - 1 + for depth in range(max_depth): + test_map = {"foo":"bar", "depth":depth, "next":test_map} + + # this should not throw an exception. + test_notation_out = self.llsd.as_notation(test_map) + + test_notation_parsed = self.llsd.parse(io.BytesIO(test_notation_out)) + self.assertEqual(test_map, test_notation_parsed) + + test_map = {"foo":"bar", "depth":depth, "next":test_map} + # this should throw an exception. + self.assertRaises(llsd.LLSDSerializationError, self.llsd.as_notation, test_map) + class LLSDBinaryUnitTest(unittest.TestCase): """ @@ -964,6 +984,26 @@ def testParseDelimitedString(self): self.assertEqual('\t\x07\x08\x0c\n\r\t\x0b\x0fp', llsd.parse(delimited_string)) + def testDeepMap(self): + """ + Test formatting of a deeply nested map + """ + + test_map = {"foo":"bar", "depth":0} + max_depth = MAX_FORMAT_DEPTH -1 + for depth in range(max_depth): + test_map = {"foo":"bar", "depth":depth, "next":test_map} + + # this should not throw an exception. + test_binary_out = self.llsd.as_binary(test_map) + + test_binary_parsed = self.llsd.parse(io.BytesIO(test_binary_out)) + self.assertEqual(test_map, test_binary_parsed) + + test_map = {"foo":"bar", "depth":depth, "next":test_map} + # this should throw an exception. + self.assertRaises(llsd.LLSDSerializationError, self.llsd.as_binary, test_map) + class LLSDPythonXMLUnitTest(unittest.TestCase): @@ -1345,20 +1385,6 @@ def testMap(self): map_within_map_xml) self.assertXMLRoundtrip({}, blank_map_xml) - def testDeepMap(self): - """ - Test that formatting a deeply nested map does not cause a RecursionError - """ - - test_map = {"foo":"bar", "depth":0, "next":None} - max_depth = 200 - for depth in range(max_depth): - test_map = {"foo":"bar", "depth":depth, "next":test_map} - - # this should not throw an exception. - test_xml = self.llsd.as_xml(test_map) - - def testBinary(self): """ Test the parse and serialization of input type : binary. @@ -1493,6 +1519,26 @@ def testFormatPrettyXML(self): self.assertEqual(result[result.find(b"?>") + 2: len(result)], format_xml_result[format_xml_result.find(b"?>") + 2: len(format_xml_result)]) + def testDeepMap(self): + """ + Test formatting of a deeply nested map + """ + + test_map = {"foo":"bar", "depth":0} + max_depth = MAX_FORMAT_DEPTH - 1 + for depth in range(max_depth): + test_map = {"foo":"bar", "depth":depth, "next":test_map} + + # this should not throw an exception. + test_xml_out = self.llsd.as_xml(test_map) + + test_xml_parsed = self.llsd.parse(io.BytesIO(test_xml_out)) + self.assertEqual(test_map, test_xml_parsed) + + test_map = {"foo":"bar", "depth":depth, "next":test_map} + # this should throw an exception. + self.assertRaises(llsd.LLSDSerializationError, self.llsd.as_xml, test_map) + def testLLSDSerializationFailure(self): """ Test serialization function as_xml with an object of non-supported type.