From 5e19a00a8574457652e403a2c0d221f26a12f5bf Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 9 Dec 2025 17:36:47 +0000 Subject: [PATCH 01/14] fix + tests --- llsd/base.py | 26 +++++++++++++++++++------- tests/llsd_test.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/llsd/base.py b/llsd/base.py index e7204ca..b58e781 100644 --- a/llsd/base.py +++ b/llsd/base.py @@ -410,14 +410,26 @@ 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 + if something.seekable(): + # Seekable stream, use directly + self._stream = something + elif something.readable(): + # Readable but not seekable, wrap in BufferedReader + self._stream = io.BufferedReader(something) + else: + raise LLSDParseError( + "Cannot parse LLSD from non-readable 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( + f"Cannot parse LLSD from {type(something).__name__}. " + "Expected bytes or a file-like object (io.IOBase subclass)." + ) def starts_with(self, pattern): """ diff --git a/tests/llsd_test.py b/tests/llsd_test.py index 073a974..70d2544 100644 --- a/tests/llsd_test.py +++ b/tests/llsd_test.py @@ -1977,3 +1977,47 @@ def test_uuid_map_key(self): self.assertEqual(llsd.format_notation(llsdmap), b"{'00000000-0000-0000-0000-000000000000':'uuid'}") +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. + ''' + 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)) + + From 0f2d59aa42bd1a55d497ec851dd9750eb6c55d49 Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 9 Dec 2025 22:50:57 +0000 Subject: [PATCH 02/14] Fix CI: Use archive.debian.org for Python 2.7 Buster image Debian Buster reached end-of-life and its repositories are no longer available at deb.debian.org. This updates the CI to use archive.debian.org for the Python 2.7 build which requires the Buster image. --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1f154b8..d1354d0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,10 +30,10 @@ jobs: if: matrix.image-variant == '-buster' run: | # Debian Buster reached EOL, switch to archive.debian.org - # Remove security repo entirely (not available in archive) and use main archive sed -i 's|deb.debian.org|archive.debian.org|g' /etc/apt/sources.list - sed -i '/security.debian.org/d' /etc/apt/sources.list + sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list sed -i '/buster-updates/d' /etc/apt/sources.list + sed -i 's|buster/updates|buster|g' /etc/apt/sources.list - name: Install python dependencies run: | From c061551f5611129f31ed71ff461555c2c1cd033c Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 9 Dec 2025 22:57:57 +0000 Subject: [PATCH 03/14] fix --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d1354d0..1f154b8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -30,10 +30,10 @@ jobs: if: matrix.image-variant == '-buster' run: | # Debian Buster reached EOL, switch to archive.debian.org + # Remove security repo entirely (not available in archive) and use main archive sed -i 's|deb.debian.org|archive.debian.org|g' /etc/apt/sources.list - sed -i 's|security.debian.org|archive.debian.org|g' /etc/apt/sources.list + sed -i '/security.debian.org/d' /etc/apt/sources.list sed -i '/buster-updates/d' /etc/apt/sources.list - sed -i 's|buster/updates|buster|g' /etc/apt/sources.list - name: Install python dependencies run: | From 911b91615ca4848ea7b7971ca7c0eaf1c0ccde5e Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 9 Dec 2025 23:01:12 +0000 Subject: [PATCH 04/14] make fix python 2.7 compatible --- llsd/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/llsd/base.py b/llsd/base.py index b58e781..29701ce 100644 --- a/llsd/base.py +++ b/llsd/base.py @@ -427,8 +427,10 @@ def _reset(self, something): # This catches MagicMock and other non-stream objects that might # have read/seek attributes but aren't actual IO streams raise LLSDParseError( - f"Cannot parse LLSD from {type(something).__name__}. " - "Expected bytes or a file-like object (io.IOBase subclass)." + "Cannot parse LLSD from {0}. " + "Expected bytes or a file-like object (io.IOBase subclass).".format( + type(something).__name__ + ) ) def starts_with(self, pattern): From 8c4936fea2f0a8e1ee62726688d78351ec18b021 Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 9 Dec 2025 23:04:26 +0000 Subject: [PATCH 05/14] more python 2.7 related fixes --- tests/llsd_test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/llsd_test.py b/tests/llsd_test.py index 70d2544..61095b3 100644 --- a/tests/llsd_test.py +++ b/tests/llsd_test.py @@ -1990,15 +1990,20 @@ def test_parse_magicmock_raises_error(self): an infinite loop when passed a MagicMock (e.g., from an improperly mocked requests.Response.content). ''' - from unittest.mock import MagicMock + try: + from unittest.mock import MagicMock + except ImportError: + from mock import MagicMock # Python 2.7 mock = MagicMock() with self.assertRaises(llsd.LLSDParseError) as context: llsd.parse(mock) self.assertIn('MagicMock', str(context.exception)) + @unittest.skipIf(PY2, "str is bytes in Python 2") 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') From c63d1c0972fb4fa19404e9e5236695d3f3aecd21 Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 9 Dec 2025 23:08:45 +0000 Subject: [PATCH 06/14] skip new tests for 2.7 --- tests/llsd_test.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/llsd_test.py b/tests/llsd_test.py index 61095b3..1e052b1 100644 --- a/tests/llsd_test.py +++ b/tests/llsd_test.py @@ -1977,6 +1977,7 @@ 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 @@ -1990,16 +1991,12 @@ def test_parse_magicmock_raises_error(self): an infinite loop when passed a MagicMock (e.g., from an improperly mocked requests.Response.content). ''' - try: - from unittest.mock import MagicMock - except ImportError: - from mock import MagicMock # Python 2.7 + from unittest.mock import MagicMock mock = MagicMock() with self.assertRaises(llsd.LLSDParseError) as context: llsd.parse(mock) self.assertIn('MagicMock', str(context.exception)) - @unittest.skipIf(PY2, "str is bytes in Python 2") def test_parse_string_raises_error(self): ''' Parsing a string (not bytes) should raise LLSDParseError. From fdc59625b6535a358366b8ab51d593d4fa8298eb Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 9 Dec 2025 23:14:09 +0000 Subject: [PATCH 07/14] updated for test coverage --- llsd/base.py | 8 ++------ tests/llsd_test.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/llsd/base.py b/llsd/base.py index 29701ce..676eb92 100644 --- a/llsd/base.py +++ b/llsd/base.py @@ -411,16 +411,12 @@ def _reset(self, something): # BytesIO is significant, advise caller to pass a stream instead. self._stream = io.BytesIO(something) elif isinstance(something, io.IOBase): - # 'something' is a proper IO stream + # 'something' is a proper IO stream - must be seekable for parsing if something.seekable(): - # Seekable stream, use directly self._stream = something - elif something.readable(): - # Readable but not seekable, wrap in BufferedReader - self._stream = io.BufferedReader(something) else: raise LLSDParseError( - "Cannot parse LLSD from non-readable stream." + "Cannot parse LLSD from non-seekable stream." ) else: # Invalid input type - raise a clear error diff --git a/tests/llsd_test.py b/tests/llsd_test.py index 1e052b1..c1900db 100644 --- a/tests/llsd_test.py +++ b/tests/llsd_test.py @@ -2022,4 +2022,14 @@ def test_parse_int_raises_error(self): 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)) + From ff4e862bf94c432e22261f137b8a7dc59b5db0d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:45:43 +0000 Subject: [PATCH 08/14] Bump actions/checkout from 4 to 6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v6) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/bench.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bench.yaml b/.github/workflows/bench.yaml index a12206b..ab3ec5f 100644 --- a/.github/workflows/bench.yaml +++ b/.github/workflows/bench.yaml @@ -14,7 +14,7 @@ jobs: name: Run benchmarks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: actions/setup-python@v5 with: From d8bb0c8610d60c063134ef6bccc3f24935465e12 Mon Sep 17 00:00:00 2001 From: Sidd Date: Wed, 10 Dec 2025 14:40:26 +0000 Subject: [PATCH 09/14] skip dependabot from cla --- .github/workflows/cla.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml index c4f291e..0331439 100644 --- a/.github/workflows/cla.yaml +++ b/.github/workflows/cla.yaml @@ -10,6 +10,7 @@ jobs: cla: name: Check CLA runs-on: ubuntu-latest + if: github.actor != 'dependabot[bot]' steps: - name: CLA Assistant if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' From 323df152c51ba41f9b16c6aadc180e3a099782a6 Mon Sep 17 00:00:00 2001 From: Sidd Date: Wed, 10 Dec 2025 14:46:37 +0000 Subject: [PATCH 10/14] workflow dispatch for testing --- .github/workflows/cla.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml index 0331439..88894fc 100644 --- a/.github/workflows/cla.yaml +++ b/.github/workflows/cla.yaml @@ -5,6 +5,12 @@ on: types: [created] pull_request_target: types: [opened, closed, synchronize] + workflow_dispatch: + inputs: + pr_number: + description: 'Pull request number to check' + required: true + type: number jobs: cla: @@ -13,7 +19,7 @@ jobs: if: github.actor != 'dependabot[bot]' steps: - name: CLA Assistant - if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' uses: secondlife-3p/contributor-assistant@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From f297c39676b8796f2bbfded2329bd0ba149b6a22 Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 16 Dec 2025 17:44:36 +0000 Subject: [PATCH 11/14] revert back to actions v4 to keep this branch clean --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1f154b8..df10d5c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: env: PYTHON: ${{ matrix.python-version }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 with: fetch-depth: 0 # fetch all history for setuptools_scm to be able to read tags From d7fffe0d128b6735e37db5e69db4552cfade488e Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 16 Dec 2025 17:47:40 +0000 Subject: [PATCH 12/14] revert checkout versions --- .github/workflows/bench.yaml | 2 +- .github/workflows/ci.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/bench.yaml b/.github/workflows/bench.yaml index ab3ec5f..a12206b 100644 --- a/.github/workflows/bench.yaml +++ b/.github/workflows/bench.yaml @@ -14,7 +14,7 @@ jobs: name: Run benchmarks runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index df10d5c..1f154b8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,7 +22,7 @@ jobs: env: PYTHON: ${{ matrix.python-version }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 # fetch all history for setuptools_scm to be able to read tags From 3cb5181d0ff35fc82679c9779f3e566c77887c7d Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 16 Dec 2025 17:57:13 +0000 Subject: [PATCH 13/14] revert changes that were not needed because covered by previous PR --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From f05de4edac6728b62b4bb2f7eea5cb10579244ae Mon Sep 17 00:00:00 2001 From: Sidd Date: Tue, 16 Dec 2025 17:58:52 +0000 Subject: [PATCH 14/14] revert cla changes that were covered by previous pr --- .github/workflows/cla.yaml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/cla.yaml b/.github/workflows/cla.yaml index 88894fc..c4f291e 100644 --- a/.github/workflows/cla.yaml +++ b/.github/workflows/cla.yaml @@ -5,21 +5,14 @@ on: types: [created] pull_request_target: types: [opened, closed, synchronize] - workflow_dispatch: - inputs: - pr_number: - description: 'Pull request number to check' - required: true - type: number jobs: cla: name: Check CLA runs-on: ubuntu-latest - if: github.actor != 'dependabot[bot]' steps: - name: CLA Assistant - if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch' + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' uses: secondlife-3p/contributor-assistant@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}