Skip to content

Commit 96aeb38

Browse files
authored
Fix infinite loop when parsing MagicMock objects (#32)
2 parents f00d36b + 74daf22 commit 96aeb38

File tree

2 files changed

+73
-7
lines changed

2 files changed

+73
-7
lines changed

llsd/base.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -410,14 +410,24 @@ def _reset(self, something):
410410
# string is so large that the overhead of copying it into a
411411
# BytesIO is significant, advise caller to pass a stream instead.
412412
self._stream = io.BytesIO(something)
413-
elif something.seekable():
414-
# 'something' is already a seekable stream, use directly
415-
self._stream = something
413+
elif isinstance(something, io.IOBase):
414+
# 'something' is a proper IO stream - must be seekable for parsing
415+
if something.seekable():
416+
self._stream = something
417+
else:
418+
raise LLSDParseError(
419+
"Cannot parse LLSD from non-seekable stream."
420+
)
416421
else:
417-
# 'something' isn't seekable, wrap in BufferedReader
418-
# (let BufferedReader handle the problem of passing an
419-
# inappropriate object)
420-
self._stream = io.BufferedReader(something)
422+
# Invalid input type - raise a clear error
423+
# This catches MagicMock and other non-stream objects that might
424+
# have read/seek attributes but aren't actual IO streams
425+
raise LLSDParseError(
426+
"Cannot parse LLSD from {0}. "
427+
"Expected bytes or a seekable io.IOBase object.".format(
428+
type(something).__name__
429+
)
430+
)
421431

422432
def starts_with(self, pattern):
423433
"""

tests/llsd_test.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1977,3 +1977,59 @@ def test_uuid_map_key(self):
19771977
self.assertEqual(llsd.format_notation(llsdmap), b"{'00000000-0000-0000-0000-000000000000':'uuid'}")
19781978

19791979

1980+
class InvalidInputTypes(unittest.TestCase):
1981+
'''
1982+
Tests for handling invalid input types that should raise LLSDParseError
1983+
instead of hanging or consuming infinite memory.
1984+
'''
1985+
1986+
@unittest.skipIf(PY2, "MagicMock requires Python 3")
1987+
def test_parse_magicmock_raises_error(self):
1988+
'''
1989+
Parsing a MagicMock object should raise LLSDParseError, not hang.
1990+
This is a regression test for a bug where llsd.parse() would go into
1991+
an infinite loop when passed a MagicMock (e.g., from an improperly
1992+
mocked requests.Response.content).
1993+
'''
1994+
from unittest.mock import MagicMock
1995+
mock = MagicMock()
1996+
with self.assertRaises(llsd.LLSDParseError) as context:
1997+
llsd.parse(mock)
1998+
self.assertIn('MagicMock', str(context.exception))
1999+
2000+
def test_parse_string_raises_error(self):
2001+
'''
2002+
Parsing a string (not bytes) should raise LLSDParseError.
2003+
Only applies to Python 3 where str and bytes are distinct.
2004+
'''
2005+
with self.assertRaises(llsd.LLSDParseError) as context:
2006+
llsd.parse(b'not bytes'.decode('ascii'))
2007+
self.assertIn('unicode' if PY2 else 'str', str(context.exception))
2008+
2009+
def test_parse_none_raises_error(self):
2010+
'''
2011+
Parsing None should raise LLSDParseError.
2012+
'''
2013+
with self.assertRaises(llsd.LLSDParseError) as context:
2014+
llsd.parse(None)
2015+
self.assertIn('NoneType', str(context.exception))
2016+
2017+
def test_parse_int_raises_error(self):
2018+
'''
2019+
Parsing an integer should raise LLSDParseError.
2020+
'''
2021+
with self.assertRaises(llsd.LLSDParseError) as context:
2022+
llsd.parse(42)
2023+
self.assertIn('int', str(context.exception))
2024+
2025+
def test_parse_non_seekable_stream_raises_error(self):
2026+
'''
2027+
Parsing a non-seekable stream should raise LLSDParseError.
2028+
'''
2029+
stream = io.BytesIO()
2030+
stream.seekable = lambda: False
2031+
with self.assertRaises(llsd.LLSDParseError) as context:
2032+
llsd.parse(stream)
2033+
self.assertIn('non-seekable', str(context.exception))
2034+
2035+

0 commit comments

Comments
 (0)