Skip to content
Closed
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
All notable changes to this project will be documented here.

## [Unreleased]
### Fixed
- Reject negative list counts instead of silently reading the rest of
the file or raising a misleading "early end-of-file".
- Reject negative element counts with a `PlyParseError`.
- Skip empty header lines, as documented in 1.1.3.

## [1.1.5] - 2026-07-25
### Fixed
Expand Down
22 changes: 14 additions & 8 deletions plyfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -615,9 +615,10 @@ def _read_txt(self, stream):
for prop in self.properties:
try:
self._data[prop.name][k] = prop._from_fields(fields)
except StopIteration:
raise PlyElementParseError("early end-of-line",
self, k, prop)
except StopIteration as exc:
raise PlyElementParseError(
exc.value if exc.value else "early end-of-line",
self, k, prop)
except ValueError:
raise PlyElementParseError("malformed input",
self, k, prop)
Expand Down Expand Up @@ -667,9 +668,10 @@ def _read_bin(self, stream, byte_order):
try:
self._data[prop.name][k] = \
prop._read_bin(stream, byte_order)
except StopIteration:
raise PlyElementParseError("early end-of-file",
self, k, prop)
except StopIteration as exc:
raise PlyElementParseError(
exc.value if exc.value else "early end-of-file",
self, k, prop)

def _write_bin(self, stream, byte_order):
"""
Expand Down Expand Up @@ -944,6 +946,8 @@ def _from_fields(self, fields):
(len_t, val_t) = self.list_dtype()

n = int(_np.dtype(len_t).type(next(fields)))
if n < 0:
raise StopIteration("negative list length %d" % n)

data = _np.loadtxt(list(_islice(fields, n)), val_t, ndmin=1)
if len(data) < n:
Expand Down Expand Up @@ -1145,8 +1149,6 @@ def consume(self, raw_line):
Parse and internalize one line of input.
"""
self.lines += 1
if not raw_line:
self._error("early end-of-file")

line = raw_line.strip()
if line == '':
Expand Down Expand Up @@ -1215,6 +1217,8 @@ def parse_element(self, data):
count = int(fields[1])
except ValueError:
self._error("expected integer count")
if count < 0:
self._error("expected non-negative integer count")

self.elements.append((name, [], count, []))
self._allowed = ['element', 'comment', 'property', 'end_header']
Expand Down Expand Up @@ -1415,6 +1419,8 @@ def _read_array(stream, dtype, n):
StopIteration
If `n` elements could not be read.
"""
if n < 0:
raise StopIteration("negative array length %d" % int(n))
try:
size = int(_np.dtype(dtype).itemsize) * int(n)
return _np.frombuffer(stream.read(size), dtype)
Expand Down
73 changes: 70 additions & 3 deletions test/test_plyfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,22 @@ def ply_list_a(fmt, n, data):
"row 0: property 'a': early end-of-file"),

(ply_abc('binary_little_endian', 2, b'\x01\x02\x03'),
"row 1: early end-of-file")
"row 1: early end-of-file"),

(b'ply\nformat binary_little_endian 1.0\nelement test 1\n'
b'property list int8 int8 a\nend_header\n'
b'\xff\x01\x02\x03',
"row 0: property 'a': negative array length -1"),

(b'ply\nformat binary_little_endian 1.0\nelement test 1\n'
b'property list int32 int a\nend_header\n'
b'\xfd\xff\xff\xff\x01\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00',
"row 0: property 'a': negative array length -3"),

(b'ply\nformat ascii 1.0\nelement test 1\n'
b'property list int8 int8 a\nend_header\n'
b'-3 1 2 3\n',
"row 0: property 'a': negative list length -3"),
]


Expand Down Expand Up @@ -769,7 +784,9 @@ def test_invalid_property_names():
invalid_header_cases = [
(b'plyy\n', 1),
(b'ply xxx\n', 1),
(b'ply\n\n', 2),
# A blank header line is skipped, so the error is reported at the
# following line, where the file actually ends.
(b'ply\n\n', 3),
(b'ply\nformat\n', 2),
(b'ply\nelement vertex 0\n', 2),
(b'ply\nformat asciii 1.0\n', 2),
Expand All @@ -788,7 +805,11 @@ def test_invalid_property_names():
(b'ply\nformat ascii 1.0\nelement vertex 0\n'
b'property list ucharr int extra\n', 4),
(b'ply\nformat ascii 1.0\nelement vertex 0\n'
b'property float x\nend_header xxx\n', 5)
b'property float x\nend_header xxx\n', 5),
(b'ply\nformat ascii 1.0\nelement vertex -1\nproperty float x\n'
b'end_header\n', 3),
(b'ply\nformat binary_little_endian 1.0\nelement vertex -1\n'
b'property float x\nend_header\n', 3),
]


Expand All @@ -802,6 +823,52 @@ def test_header_parse_error(s, line):
assert e.exc_val.line == line


def test_negative_element_count():
with Raises(PlyHeaderParseError) as e:
PlyData.read(BytesIO(b'ply\nformat ascii 1.0\nelement vertex -1\n'
b'property float x\nend_header\n'))
assert e.exc_val.line == 3
assert e.exc_val.message == "expected non-negative integer count"


def test_negative_list_count(tmpdir):
# A negative binary list count must be rejected on every stream type
# with the same message, not silently read as the rest of the file.
string = (b'ply\nformat binary_little_endian 1.0\nelement test 1\n'
b'property list int8 int8 a\nend_header\n'
b'\xff\x01\x02\x03')
filename = tmpdir.join('test.ply')
with filename.open('wb') as f:
f.write(string)
for s in (BytesIO(string), str(filename)):
with Raises(PlyElementParseError) as e:
PlyData.read(s)
assert str(e) == ("element 'test': row 0: property 'a': "
"negative array length -1")


def test_blank_header_lines():
# Empty header lines are skipped (CHANGELOG 1.1.3).
string = (b'ply\n\nformat ascii 1.0\n\nelement vertex 1\n'
b'property float x\n\nend_header\n0\n')
ply = PlyData.read(BytesIO(string))
assert ply['vertex'].count == 1
assert ply['vertex']['x'][0] == 0.0


def test_zero_list_count():
string = (b'ply\nformat binary_little_endian 1.0\nelement test 1\n'
b'property list int8 int8 a\nend_header\n\x00')
ply = PlyData.read(BytesIO(string))
assert len(ply['test']['a'][0]) == 0


def test_zero_element_count():
ply = PlyData.read(BytesIO(b'ply\nformat ascii 1.0\nelement vertex 0\n'
b'property float x\nend_header\n'))
assert ply['vertex'].count == 0


invalid_arrays = [
numpy.zeros((2, 2)),
numpy.array([(0, (0, 0))],
Expand Down
Loading