From 6b81e34d676070e6fe519b525c27621ea6f5fe68 Mon Sep 17 00:00:00 2001 From: Piolie Date: Mon, 21 Dec 2020 00:56:30 -0300 Subject: [PATCH 01/22] Improve handling of PPM headers --- src/PIL/PpmImagePlugin.py | 76 +++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index abf4d651dc5..05993a7d15e 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -20,7 +20,7 @@ # # -------------------------------------------------------------------- -b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" +B_WHITESPACE = b"\x20\x09\x0a\x0b\x0c\x0d" MODES = { # standard @@ -49,25 +49,39 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _token(self, s=b""): + def _read_token(self, token=b""): + def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF + while True: + c = self.fp.read(1) + if c in b"\r\n": + break + + while True: # read until non-whitespace is found + c = self.fp.read(1) + if c == b"#": # found comment, ignore it + _ignore_comment() + continue + if c in B_WHITESPACE: # found whitespace, ignore it + if c == b"": # reached EOF + raise EOFError("Reached EOF while reading header") + continue + break + + token += c + while True: # read until next whitespace c = self.fp.read(1) - if not c or c in b_whitespace: + if c == b"#": + _ignore_comment() + continue + if c in B_WHITESPACE: # token ended break - if c > b"\x79": - raise ValueError("Expected ASCII value, found binary") - s = s + c - if len(s) > 9: - raise ValueError("Expected int, got > 9 digits") - return s + token += c + return token def _open(self): - - # check magic - s = self.fp.read(1) - if s != b"P": - raise SyntaxError("not a PPM file") - magic_number = self._token(s) + P = self.fp.read(1) + magic_number = self._read_token(P) mode = MODES[magic_number] self.custom_mimetype = { @@ -83,29 +97,21 @@ def _open(self): self.mode = rawmode = mode for ix in range(3): - while True: - while True: - s = self.fp.read(1) - if s not in b_whitespace: - break - if s == b"": - raise ValueError("File does not extend beyond magic number") - if s != b"#": - break - s = self.fp.readline() - s = int(self._token(s)) - if ix == 0: - xsize = s - elif ix == 1: - ysize = s + try: # check token sanity + token = int(self._read_token()) + except ValueError: + raise SyntaxError("Non-decimal-ASCII found in header") + if ix == 0: # token is the x size + xsize = token + elif ix == 1: # token is the y size + ysize = token if mode == "1": break - elif ix == 2: - # maxgrey - if s > 255: + elif ix == 2: # token is maxval + if token > 255: if not mode == "L": - raise ValueError(f"Too many colors for band: {s}") - if s < 2 ** 16: + raise SyntaxError(f"Too many colors for band: {token}") + if token < 2 ** 16: self.mode = "I" rawmode = "I;16B" else: From 699afe1e8915c8332e1116fbd08ef972deb9af35 Mon Sep 17 00:00:00 2001 From: Piolie Date: Mon, 21 Dec 2020 01:15:49 -0300 Subject: [PATCH 02/22] Improve PPM tests --- Tests/test_file_ppm.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index e7c3fb06ff7..9429f1e2a63 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -1,6 +1,6 @@ import pytest -from PIL import Image +from PIL import Image, UnidentifiedImageError from .helper import assert_image_equal, assert_image_similar, hopper @@ -50,12 +50,30 @@ def test_pnm(tmp_path): assert_image_equal(im, reloaded) +def test_header_with_comments(tmp_path): + path = str(tmp_path / "temp.ppm") + with open(path, "wb") as f: + f.write(b"P6 #comment\n#comment\r12#comment\r8\n128 #comment\n255\n") + + with Image.open(path) as im: + assert im.size == (128, 128) + + +def test_nondecimal_header(tmp_path): + path = str(tmp_path / "temp.djvurle") + with open(path, "wb") as f: + f.write(b"P6\n128\x00") + + with pytest.raises(UnidentifiedImageError): + Image.open(path) + + def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") with open(path, "w") as f: f.write("P6") - with pytest.raises(ValueError): + with pytest.raises(UnidentifiedImageError): Image.open(path) @@ -65,7 +83,7 @@ def test_neg_ppm(): # has been removed. The default opener doesn't accept negative # sizes. - with pytest.raises(OSError): + with pytest.raises(UnidentifiedImageError): Image.open("Tests/images/negative_size.ppm") From d2ad27d70a00efa98560cf5036326abcae850d5e Mon Sep 17 00:00:00 2001 From: Piolie Date: Mon, 4 Jan 2021 01:49:19 -0300 Subject: [PATCH 03/22] Correctly check magic number --- src/PIL/PpmImagePlugin.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 05993a7d15e..b9837a0fe84 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -49,6 +49,16 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" + def _read_magic(self, s=b""): + while True: # read until next whitespace + c = self.fp.read(1) + if c in B_WHITESPACE: + break + s = s + c + if len(s) > 6: # exceeded max magic number length + break + return s + def _read_token(self, token=b""): def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF while True: @@ -80,9 +90,11 @@ def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF return token def _open(self): - P = self.fp.read(1) - magic_number = self._read_token(P) - mode = MODES[magic_number] + magic_number = self._read_magic() + try: + mode = MODES[magic_number] + except KeyError: + raise SyntaxError("Not a PPM image file") from None self.custom_mimetype = { b"P4": "image/x-portable-bitmap", From 4dbe244e42cd7225ef3b77b3d592e3dd522e4386 Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 01:07:14 -0300 Subject: [PATCH 04/22] Add token limit --- src/PIL/PpmImagePlugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index b9837a0fe84..6ece914c350 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -87,6 +87,8 @@ def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF if c in B_WHITESPACE: # token ended break token += c + if len(token) > 9: + raise ValueError(f"Token too long: {token}") return token def _open(self): @@ -109,8 +111,9 @@ def _open(self): self.mode = rawmode = mode for ix in range(3): + token = self._read_token() try: # check token sanity - token = int(self._read_token()) + token = int(token) except ValueError: raise SyntaxError("Non-decimal-ASCII found in header") if ix == 0: # token is the x size From 5d0ad5e2e9a5065d69a00890342334f374277d1c Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 01:15:07 -0300 Subject: [PATCH 05/22] Revert exception types to `ValueError` --- Tests/test_file_ppm.py | 6 +++--- src/PIL/PpmImagePlugin.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 9429f1e2a63..21a810e3037 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -64,7 +64,7 @@ def test_nondecimal_header(tmp_path): with open(path, "wb") as f: f.write(b"P6\n128\x00") - with pytest.raises(UnidentifiedImageError): + with pytest.raises(ValueError): Image.open(path) @@ -73,7 +73,7 @@ def test_truncated_file(tmp_path): with open(path, "w") as f: f.write("P6") - with pytest.raises(UnidentifiedImageError): + with pytest.raises(ValueError): Image.open(path) @@ -83,7 +83,7 @@ def test_neg_ppm(): # has been removed. The default opener doesn't accept negative # sizes. - with pytest.raises(UnidentifiedImageError): + with pytest.raises(OSError): Image.open("Tests/images/negative_size.ppm") diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 6ece914c350..efd845d3b47 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -73,7 +73,7 @@ def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF continue if c in B_WHITESPACE: # found whitespace, ignore it if c == b"": # reached EOF - raise EOFError("Reached EOF while reading header") + raise ValueError("Reached EOF while reading header") continue break @@ -115,7 +115,7 @@ def _open(self): try: # check token sanity token = int(token) except ValueError: - raise SyntaxError("Non-decimal-ASCII found in header") + raise ValueError(f"Non-decimal-ASCII found in header: {token}") if ix == 0: # token is the x size xsize = token elif ix == 1: # token is the y size @@ -125,7 +125,7 @@ def _open(self): elif ix == 2: # token is maxval if token > 255: if not mode == "L": - raise SyntaxError(f"Too many colors for band: {token}") + raise ValueError(f"Too many colors for band: {token}") if token < 2 ** 16: self.mode = "I" rawmode = "I;16B" @@ -156,7 +156,7 @@ def _save(im, fp, filename): elif im.mode == "RGBA": rawmode, head = "RGB", b"P6" else: - raise OSError(f"cannot write mode {im.mode} as PPM") + raise OSError(f"Cannot write mode {im.mode} as PPM") fp.write(head + ("\n%d %d\n" % im.size).encode("ascii")) if head == b"P6": fp.write(b"255\n") From 002e0bd6975974288eb46e1609ba319699dc44ba Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 01:21:35 -0300 Subject: [PATCH 06/22] Add tests for token size and wrong magic number --- Tests/test_file_ppm.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 21a810e3037..0ea6b277eda 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -49,6 +49,13 @@ def test_pnm(tmp_path): with Image.open(f) as reloaded: assert_image_equal(im, reloaded) +def test_not_ppm(tmp_path): + path = str(tmp_path / "temp.djvurle") + with open(path, "wb") as f: + f.write(b"PyXX") + + with pytest.raises(UnidentifiedImageError): + Image.open(path) def test_header_with_comments(tmp_path): path = str(tmp_path / "temp.ppm") @@ -67,6 +74,13 @@ def test_nondecimal_header(tmp_path): with pytest.raises(ValueError): Image.open(path) +def test_token_too_long(tmp_path): + path = str(tmp_path / "temp.djvurle") + with open(path, "wb") as f: + f.write(b"P6\n 0123456789") + + with pytest.raises(ValueError): + Image.open(path) def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") From 73fed77c0c58efd2ce11b8c0e96858bd03655163 Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 14:46:30 -0300 Subject: [PATCH 07/22] Suppress exception context --- src/PIL/PpmImagePlugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index efd845d3b47..8bd7d16db39 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,9 @@ def _open(self): try: # check token sanity token = int(token) except ValueError: - raise ValueError(f"Non-decimal-ASCII found in header: {token}") + raise ValueError( + f"Non-decimal-ASCII found in header: {token}" + ) from None if ix == 0: # token is the x size xsize = token elif ix == 1: # token is the y size From bc5ecfb79c3dca0553e2b863ba7d306f2e0fd434 Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 6 Jan 2021 14:53:30 -0300 Subject: [PATCH 08/22] Make minor changes to tests - add test for maxcolors; - extend coverage for wrong magic number; - fix linting. --- Tests/test_file_ppm.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 0ea6b277eda..77798514dac 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -49,14 +49,16 @@ def test_pnm(tmp_path): with Image.open(f) as reloaded: assert_image_equal(im, reloaded) + def test_not_ppm(tmp_path): path = str(tmp_path / "temp.djvurle") with open(path, "wb") as f: - f.write(b"PyXX") + f.write(b"PyInvalid") with pytest.raises(UnidentifiedImageError): Image.open(path) + def test_header_with_comments(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: @@ -74,6 +76,7 @@ def test_nondecimal_header(tmp_path): with pytest.raises(ValueError): Image.open(path) + def test_token_too_long(tmp_path): path = str(tmp_path / "temp.djvurle") with open(path, "wb") as f: @@ -82,6 +85,16 @@ def test_token_too_long(tmp_path): with pytest.raises(ValueError): Image.open(path) + +def test_too_many_colors(tmp_path): + path = str(tmp_path / "temp.djvurle") + with open(path, "wb") as f: + f.write(b"P6\n1 1\n1000\n") + + with pytest.raises(ValueError): + Image.open(path) + + def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") with open(path, "w") as f: From 50522d932ea93dc09db0fae52a0ebe80a5f4eb08 Mon Sep 17 00:00:00 2001 From: Piolie Date: Sun, 31 Jan 2021 00:31:32 -0300 Subject: [PATCH 09/22] Change max token size to 10 - ...so as not to reject "2,147,483,647" (2 ** 31 - 1); - reword exception message. --- src/PIL/PpmImagePlugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 8bd7d16db39..5ff98e346de 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -87,8 +87,8 @@ def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF if c in B_WHITESPACE: # token ended break token += c - if len(token) > 9: - raise ValueError(f"Token too long: {token}") + if len(token) > 10: + raise ValueError(f"Token too long in file header: {token}") return token def _open(self): From 39288f0fb0755b8ef9bbc4867bda6a19e91287fd Mon Sep 17 00:00:00 2001 From: Piolie Date: Sun, 31 Jan 2021 00:51:39 -0300 Subject: [PATCH 10/22] Create `maxval` variable --- src/PIL/PpmImagePlugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 5ff98e346de..93ce3a4d221 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -125,7 +125,8 @@ def _open(self): if mode == "1": break elif ix == 2: # token is maxval - if token > 255: + maxval = token + if maxval > 255: if not mode == "L": raise ValueError(f"Too many colors for band: {token}") if token < 2 ** 16: From b43654d15954a5af46ad618291b7c859549ccc63 Mon Sep 17 00:00:00 2001 From: Piolie Date: Sun, 10 Jan 2021 18:45:46 -0300 Subject: [PATCH 11/22] Change variable name in `_read_magic()` --- src/PIL/PpmImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 93ce3a4d221..e25b4bcec02 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -49,15 +49,15 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _read_magic(self, s=b""): + def _read_magic(self, magic=b""): while True: # read until next whitespace c = self.fp.read(1) if c in B_WHITESPACE: break - s = s + c - if len(s) > 6: # exceeded max magic number length + magic += c + if len(magic) > 6: # exceeded max magic number length break - return s + return magic def _read_token(self, token=b""): def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF From b6f6fba8cf170e60619da81ff1a24c2ef4364be5 Mon Sep 17 00:00:00 2001 From: Piolie Date: Wed, 13 Jan 2021 18:45:29 -0300 Subject: [PATCH 12/22] Rewrite `_ignore_comment` as one-liner --- src/PIL/PpmImagePlugin.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index e25b4bcec02..95987b4a45d 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -61,10 +61,8 @@ def _read_magic(self, magic=b""): def _read_token(self, token=b""): def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF - while True: - c = self.fp.read(1) - if c in b"\r\n": - break + while self.fp.read(1) not in b"\r\n": + pass while True: # read until non-whitespace is found c = self.fp.read(1) From 41a439da7d7c38e54660011abee948d177f67126 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sun, 21 Mar 2021 14:42:36 +1100 Subject: [PATCH 13/22] Added context managers --- Tests/test_file_ppm.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 77798514dac..94fbc62a2ff 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -56,7 +56,8 @@ def test_not_ppm(tmp_path): f.write(b"PyInvalid") with pytest.raises(UnidentifiedImageError): - Image.open(path) + with Image.open(path): + pass def test_header_with_comments(tmp_path): @@ -74,7 +75,8 @@ def test_nondecimal_header(tmp_path): f.write(b"P6\n128\x00") with pytest.raises(ValueError): - Image.open(path) + with Image.open(path): + pass def test_token_too_long(tmp_path): @@ -83,7 +85,8 @@ def test_token_too_long(tmp_path): f.write(b"P6\n 0123456789") with pytest.raises(ValueError): - Image.open(path) + with Image.open(path): + pass def test_too_many_colors(tmp_path): @@ -92,7 +95,8 @@ def test_too_many_colors(tmp_path): f.write(b"P6\n1 1\n1000\n") with pytest.raises(ValueError): - Image.open(path) + with Image.open(path): + pass def test_truncated_file(tmp_path): From 8ad5172e8858b5dd023612bbacb37f066b577012 Mon Sep 17 00:00:00 2001 From: Piolie Date: Sun, 21 Mar 2021 02:16:39 -0300 Subject: [PATCH 14/22] Fix wrong extension in temp test files --- Tests/test_file_ppm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index 94fbc62a2ff..ecc3401b5fa 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -51,7 +51,7 @@ def test_pnm(tmp_path): def test_not_ppm(tmp_path): - path = str(tmp_path / "temp.djvurle") + path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"PyInvalid") @@ -70,7 +70,7 @@ def test_header_with_comments(tmp_path): def test_nondecimal_header(tmp_path): - path = str(tmp_path / "temp.djvurle") + path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n128\x00") @@ -80,7 +80,7 @@ def test_nondecimal_header(tmp_path): def test_token_too_long(tmp_path): - path = str(tmp_path / "temp.djvurle") + path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n 0123456789") @@ -90,7 +90,7 @@ def test_token_too_long(tmp_path): def test_too_many_colors(tmp_path): - path = str(tmp_path / "temp.djvurle") + path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n1 1\n1000\n") From 9c2cbcf4520fe92d3d6b70bda13e3b1078ff73b1 Mon Sep 17 00:00:00 2001 From: Piolie Date: Mon, 22 Mar 2021 13:06:16 -0300 Subject: [PATCH 15/22] Keep case consistency in error messages Co-authored-by: Andrew Murray <3112309+radarhere@users.noreply.github.com> --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 95987b4a45d..725ddec10f3 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -157,7 +157,7 @@ def _save(im, fp, filename): elif im.mode == "RGBA": rawmode, head = "RGB", b"P6" else: - raise OSError(f"Cannot write mode {im.mode} as PPM") + raise OSError(f"cannot write mode {im.mode} as PPM") fp.write(head + ("\n%d %d\n" % im.size).encode("ascii")) if head == b"P6": fp.write(b"255\n") From e2e87d73c39612781be059072a2a08c30f743459 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 11:01:37 +1100 Subject: [PATCH 16/22] Reverted SyntaxError change to match other plugins --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 725ddec10f3..9ed34a0cd3e 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -94,7 +94,7 @@ def _open(self): try: mode = MODES[magic_number] except KeyError: - raise SyntaxError("Not a PPM image file") from None + raise SyntaxError("not a PPM file") self.custom_mimetype = { b"P4": "image/x-portable-bitmap", From cb4e26783f4c183ed918adc375c5535484b8aac7 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 10:36:34 +1100 Subject: [PATCH 17/22] Retain variable case for backwards compatibility --- src/PIL/PpmImagePlugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 9ed34a0cd3e..4c9e7292d98 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -20,7 +20,7 @@ # # -------------------------------------------------------------------- -B_WHITESPACE = b"\x20\x09\x0a\x0b\x0c\x0d" +b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d" MODES = { # standard @@ -52,7 +52,7 @@ class PpmImageFile(ImageFile.ImageFile): def _read_magic(self, magic=b""): while True: # read until next whitespace c = self.fp.read(1) - if c in B_WHITESPACE: + if c in b_whitespace: break magic += c if len(magic) > 6: # exceeded max magic number length @@ -69,7 +69,7 @@ def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF if c == b"#": # found comment, ignore it _ignore_comment() continue - if c in B_WHITESPACE: # found whitespace, ignore it + if c in b_whitespace: # found whitespace, ignore it if c == b"": # reached EOF raise ValueError("Reached EOF while reading header") continue @@ -82,7 +82,7 @@ def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF if c == b"#": _ignore_comment() continue - if c in B_WHITESPACE: # token ended + if c in b_whitespace: # token ended break token += c if len(token) > 10: From 3426052874c765ace64dc3c116114eff0b1ec9c3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 12:13:08 +1100 Subject: [PATCH 18/22] Removed re-raising of exception --- src/PIL/PpmImagePlugin.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 4c9e7292d98..bb0972bf88b 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -109,13 +109,7 @@ def _open(self): self.mode = rawmode = mode for ix in range(3): - token = self._read_token() - try: # check token sanity - token = int(token) - except ValueError: - raise ValueError( - f"Non-decimal-ASCII found in header: {token}" - ) from None + token = int(self._read_token()) if ix == 0: # token is the x size xsize = token elif ix == 1: # token is the y size From f5b9e2c43af136c13b27c393dba2caaa93d9fd02 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 11:17:25 +1100 Subject: [PATCH 19/22] Explicitly check if magic number is empty --- src/PIL/PpmImagePlugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index bb0972bf88b..89fbc6f3481 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -52,7 +52,7 @@ class PpmImageFile(ImageFile.ImageFile): def _read_magic(self, magic=b""): while True: # read until next whitespace c = self.fp.read(1) - if c in b_whitespace: + if not c or c in b_whitespace: break magic += c if len(magic) > 6: # exceeded max magic number length @@ -69,9 +69,9 @@ def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF if c == b"#": # found comment, ignore it _ignore_comment() continue + if not c: + raise ValueError("Reached EOF while reading header") if c in b_whitespace: # found whitespace, ignore it - if c == b"": # reached EOF - raise ValueError("Reached EOF while reading header") continue break From d96830115feb9e472c07da5d8982d44a9abbaaa3 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 15:22:41 +1100 Subject: [PATCH 20/22] Updated tests --- Tests/test_file_ppm.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_ppm.py b/Tests/test_file_ppm.py index fc930e31471..0e4f1ba6804 100644 --- a/Tests/test_file_ppm.py +++ b/Tests/test_file_ppm.py @@ -50,7 +50,7 @@ def test_pnm(tmp_path): assert_image_equal_tofile(im, f) -def test_not_ppm(tmp_path): +def test_magic(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"PyInvalid") @@ -69,10 +69,10 @@ def test_header_with_comments(tmp_path): assert im.size == (128, 128) -def test_nondecimal_header(tmp_path): +def test_non_integer_token(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: - f.write(b"P6\n128\x00") + f.write(b"P6\nTEST") with pytest.raises(ValueError): with Image.open(path): @@ -82,32 +82,38 @@ def test_nondecimal_header(tmp_path): def test_token_too_long(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: - f.write(b"P6\n 0123456789") + f.write(b"P6\n 01234567890") - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert str(e.value) == "Token too long in file header: b'01234567890'" + def test_too_many_colors(tmp_path): path = str(tmp_path / "temp.ppm") with open(path, "wb") as f: f.write(b"P6\n1 1\n1000\n") - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert str(e.value) == "Too many colors for band: 1000" + def test_truncated_file(tmp_path): path = str(tmp_path / "temp.pgm") with open(path, "w") as f: f.write("P6") - with pytest.raises(ValueError): + with pytest.raises(ValueError) as e: with Image.open(path): pass + assert str(e.value) == "Reached EOF while reading header" + def test_neg_ppm(): # Storage.c accepted negative values for xsize, ysize. the From 4f8173f53f362d92bcb650cf555fbd7c45b6590f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 14:58:05 +1100 Subject: [PATCH 21/22] Refactored to reduce risk of infinite loop --- src/PIL/PpmImagePlugin.py | 50 +++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 89fbc6f3481..8ebc2b06364 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -49,44 +49,38 @@ class PpmImageFile(ImageFile.ImageFile): format = "PPM" format_description = "Pbmplus image" - def _read_magic(self, magic=b""): - while True: # read until next whitespace + def _read_magic(self): + magic = b"" + # read until whitespace or longest available magic number + for _ in range(6): c = self.fp.read(1) if not c or c in b_whitespace: break magic += c - if len(magic) > 6: # exceeded max magic number length - break return magic - def _read_token(self, token=b""): - def _ignore_comment(): # ignores rest of the line; stops at CR, LF or EOF - while self.fp.read(1) not in b"\r\n": - pass - - while True: # read until non-whitespace is found + def _read_token(self): + token = b"" + while len(token) <= 10: # read until next whitespace or limit of 10 characters c = self.fp.read(1) - if c == b"#": # found comment, ignore it - _ignore_comment() - continue if not c: - raise ValueError("Reached EOF while reading header") - if c in b_whitespace: # found whitespace, ignore it - continue - break - - token += c - - while True: # read until next whitespace - c = self.fp.read(1) - if c == b"#": - _ignore_comment() - continue - if c in b_whitespace: # token ended break + elif c in b_whitespace: # token ended + if not token: + # skip whitespace at start + continue + break + elif c == b"#": + # ignores rest of the line; stops at CR, LF or EOF + while self.fp.read(1) not in b"\r\n": + pass + continue token += c - if len(token) > 10: - raise ValueError(f"Token too long in file header: {token}") + if not token: + # Token was not even 1 byte + raise ValueError("Reached EOF while reading header") + elif len(token) > 10: + raise ValueError(f"Token too long in file header: {token}") return token def _open(self): From f7504b1ef91501105a430a891c335a4ff70418fa Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 4 Mar 2022 15:49:37 +1100 Subject: [PATCH 22/22] Changed variable --- src/PIL/PpmImagePlugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py index 8ebc2b06364..9d32927d4ca 100644 --- a/src/PIL/PpmImagePlugin.py +++ b/src/PIL/PpmImagePlugin.py @@ -115,7 +115,7 @@ def _open(self): if maxval > 255: if not mode == "L": raise ValueError(f"Too many colors for band: {token}") - if token < 2 ** 16: + if maxval < 2 ** 16: self.mode = "I" rawmode = "I;16B" else: