Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/bench.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion .github/workflows/cla.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ 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'
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 }}
Expand Down
24 changes: 17 additions & 7 deletions llsd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
56 changes: 56 additions & 0 deletions tests/llsd_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Loading