diff --git a/Tests/images/timeout-041dd17dfde800360a47a172269df127af138c6b.dds b/Tests/images/timeout-041dd17dfde800360a47a172269df127af138c6b.dds new file mode 100644 index 00000000000..8831f8fb294 Binary files /dev/null and b/Tests/images/timeout-041dd17dfde800360a47a172269df127af138c6b.dds differ diff --git a/Tests/images/timeout-52d106579505547091ef69b58341351a37c23e31.dds b/Tests/images/timeout-52d106579505547091ef69b58341351a37c23e31.dds new file mode 100644 index 00000000000..2815eb01059 Binary files /dev/null and b/Tests/images/timeout-52d106579505547091ef69b58341351a37c23e31.dds differ diff --git a/Tests/images/timeout-755a4d204f4208e3597ac3391edebee196462bd0.dds b/Tests/images/timeout-755a4d204f4208e3597ac3391edebee196462bd0.dds new file mode 100644 index 00000000000..1debdffa8d9 Binary files /dev/null and b/Tests/images/timeout-755a4d204f4208e3597ac3391edebee196462bd0.dds differ diff --git a/Tests/images/timeout-c60a3d7314213624607bfb3e38d551a8b24a7435.dds b/Tests/images/timeout-c60a3d7314213624607bfb3e38d551a8b24a7435.dds new file mode 100644 index 00000000000..d032ab7fe43 Binary files /dev/null and b/Tests/images/timeout-c60a3d7314213624607bfb3e38d551a8b24a7435.dds differ diff --git a/Tests/test_file_dds.py b/Tests/test_file_dds.py index 931ff02f1fb..98b9e0fb586 100644 --- a/Tests/test_file_dds.py +++ b/Tests/test_file_dds.py @@ -15,6 +15,7 @@ assert_image_similar, assert_image_similar_tofile, hopper, + timeout_unless_slower_valgrind, ) TEST_FILE_DXT1 = "Tests/images/dxt1-rgb-4bbp-noalpha_MipMaps-1.dds" @@ -540,3 +541,18 @@ def test_save_large_file(tmp_path: Path, pixel_format: str, mode: str) -> None: im = hopper(mode).resize((440, 440)) # should not error in valgrind im.save(tmp_path / "img.dds", pixel_format=pixel_format) + + +@timeout_unless_slower_valgrind(1) +@pytest.mark.parametrize( + "test_file", + [ + "Tests/images/timeout-041dd17dfde800360a47a172269df127af138c6b.dds", + "Tests/images/timeout-755a4d204f4208e3597ac3391edebee196462bd0.dds", + "Tests/images/timeout-52d106579505547091ef69b58341351a37c23e31.dds", + "Tests/images/timeout-c60a3d7314213624607bfb3e38d551a8b24a7435.dds", + ], +) +def test_timeout(test_file) -> None: + with Image.open(test_file) as im: + im.load() diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py index 312f602a6b1..bfd14a2a99b 100644 --- a/src/PIL/DdsImagePlugin.py +++ b/src/PIL/DdsImagePlugin.py @@ -503,21 +503,34 @@ def decode(self, buffer: bytes | Image.SupportsArrayInterface) -> tuple[int, int while mask >> (offset + 1) << (offset + 1) == mask: offset += 1 mask_offsets.append(offset) - mask_totals.append(mask >> offset) + mask_total = mask >> offset + if not mask_total: + mask_totals.append(0) + else: + mask_totals.append(255 / mask_total) data = bytearray() bytecount = bitcount // 8 dest_length = self.state.xsize * self.state.ysize * len(masks) - while len(data) < dest_length: - value = int.from_bytes(self.fd.read(bytecount), "little") + # consume the data + has_more = True + while len(data) < dest_length and has_more: + chunk = self.fd.read(bytecount) + # work around BufferedIO not being seekable + has_more = len(chunk) > 0 + value = int.from_bytes(chunk, "little") for i, mask in enumerate(masks): masked_value = value & mask # Remove the zero padding, and scale it to 8 bits - data += o8( - int(((masked_value >> mask_offsets[i]) / mask_totals[i]) * 255) - if mask_totals[i] - else 0 - ) + data += o8(int((masked_value >> mask_offsets[i]) * mask_totals[i])) + + # extra padding pixels -- always all 0 + if len(data) < dest_length: + pixel = bytearray() + pixel += o8(0) + ct_bytes = dest_length - len(data) + data += pixel * ct_bytes + self.set_as_raw(data) return -1, 0