diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1f154b8..0e417d0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -94,4 +94,4 @@ jobs: - name: Publish package uses: pypa/gh-action-pypi-publish@release/v1 - if: startsWith(github.event.ref, 'refs/tags/v') + if: startsWith(github.event.ref, 'refs/tags/v') \ No newline at end of file diff --git a/llsd/base.py b/llsd/base.py index e7204ca..676eb92 100644 --- a/llsd/base.py +++ b/llsd/base.py @@ -410,14 +410,24 @@ def _reset(self, something): # string is so large that the overhead of copying it into a # BytesIO is significant, advise caller to pass a stream instead. self._stream = io.BytesIO(something) - elif something.seekable(): - # 'something' is already a seekable stream, use directly - self._stream = something + elif isinstance(something, io.IOBase): + # 'something' is a proper IO stream - must be seekable for parsing + if something.seekable(): + self._stream = something + else: + raise LLSDParseError( + "Cannot parse LLSD from non-seekable stream." + ) else: - # 'something' isn't seekable, wrap in BufferedReader - # (let BufferedReader handle the problem of passing an - # inappropriate object) - self._stream = io.BufferedReader(something) + # Invalid input type - raise a clear error + # This catches MagicMock and other non-stream objects that might + # have read/seek attributes but aren't actual IO streams + raise LLSDParseError( + "Cannot parse LLSD from {0}. " + "Expected bytes or a file-like object (io.IOBase subclass).".format( + type(something).__name__ + ) + ) def starts_with(self, pattern): """ diff --git a/tests/llsd_test.py b/tests/llsd_test.py index 073a974..c1900db 100644 --- a/tests/llsd_test.py +++ b/tests/llsd_test.py @@ -1977,3 +1977,59 @@ def test_uuid_map_key(self): self.assertEqual(llsd.format_notation(llsdmap), b"{'00000000-0000-0000-0000-000000000000':'uuid'}") +@unittest.skipIf(PY2, "These tests require Python 3") +class InvalidInputTypes(unittest.TestCase): + ''' + Tests for handling invalid input types that should raise LLSDParseError + instead of hanging or consuming infinite memory. + ''' + + def test_parse_magicmock_raises_error(self): + ''' + Parsing a MagicMock object should raise LLSDParseError, not hang. + This is a regression test for a bug where llsd.parse() would go into + an infinite loop when passed a MagicMock (e.g., from an improperly + mocked requests.Response.content). + ''' + from unittest.mock import MagicMock + mock = MagicMock() + with self.assertRaises(llsd.LLSDParseError) as context: + llsd.parse(mock) + self.assertIn('MagicMock', str(context.exception)) + + def test_parse_string_raises_error(self): + ''' + Parsing a string (not bytes) should raise LLSDParseError. + Only applies to Python 3 where str and bytes are distinct. + ''' + with self.assertRaises(llsd.LLSDParseError) as context: + llsd.parse('not bytes') + self.assertIn('str', str(context.exception)) + + def test_parse_none_raises_error(self): + ''' + Parsing None should raise LLSDParseError. + ''' + with self.assertRaises(llsd.LLSDParseError) as context: + llsd.parse(None) + self.assertIn('NoneType', str(context.exception)) + + def test_parse_int_raises_error(self): + ''' + Parsing an integer should raise LLSDParseError. + ''' + with self.assertRaises(llsd.LLSDParseError) as context: + llsd.parse(42) + self.assertIn('int', str(context.exception)) + + def test_parse_non_seekable_stream_raises_error(self): + ''' + Parsing a non-seekable stream should raise LLSDParseError. + ''' + stream = io.BytesIO() + stream.seekable = lambda: False + with self.assertRaises(llsd.LLSDParseError) as context: + llsd.parse(stream) + self.assertIn('non-seekable', str(context.exception)) + +