Repository: python-pillow/Pillow

Issue:
Image.verify raises struct.error (instead of IndexError) on Pillow > 2.7.0
```
Traceback (most recent call last):
  File "daguerre/tests/unit/test_adjustments.py", line 329, in test_adjust__broken
    self.assertRaises(IndexError, image.verify)
  File "unittest/case.py", line 475, in assertRaises
    callableObj(*args, **kwargs)
  File "PIL/PngImagePlugin.py", line 544, in verify
    self.png.verify()
  File "PIL/PngImagePlugin.py", line 163, in verify
    self.crc(cid, ImageFile._safe_read(self.fp, length))
  File "PIL/PngImagePlugin.py", line 142, in crc
    crc2 = i16(self.fp.read(2)), i16(self.fp.read(2))
  File "PIL/_binary.py", line 53, in i16be
    return unpack(">H", c[o:o+2])[0]
error: unpack requires a string argument of length 2
```

Similar to #1163 in form, but maybe not in cause.



Discussion:
Here's the image I'm trying to verify:

![broken](https://cloud.githubusercontent.com/assets/299979/13469525/961aa10c-e05c-11e5-8e0a-333175820e04.png)

Verified that this test (for IndexError) works on 2.3.0, the last version I was testing with. I'll check a few more versions to try to narrow it down.

2.9.0: Broken
2.8.2: Broken
2.7.0: Works

The underlying implementation of the binary functions changed to `struct.unpack` at about 2.7 and every time that we throw a `struct.error` it's because of that change.

That change was in PR #1121, which was released in 2.8.0.

Given that you've created a PR in another project to act on this, are you right for this issue to be closed?

I mean... I need to support both in any case, so I don't care. The question is more what you feel is a more appropriate error as far as I'm concerned. :-) I'm guessing struct.error might make more sense? But OTOH I personally wouldn't have known that was a struct.error if you hadn't told me, which makes the error message pretty opaque.

But yeah, I personally don't need anything from you on this.

We've treated this as an error for opening files, it makes sense to do it for verify. (though, verify is a very inconsistently supported function, currently it's only available for pngs)


Resolving patch:
diff --git a/PIL/PngImagePlugin.py b/PIL/PngImagePlugin.py
--- a/PIL/PngImagePlugin.py
+++ b/PIL/PngImagePlugin.py
@@ -36,6 +36,7 @@
 import logging
 import re
 import zlib
+import struct
 
 from PIL import Image, ImageFile, ImagePalette, _binary
 
@@ -106,6 +107,7 @@ def __init__(self, fp):
 
     def read(self):
         "Fetch a new chunk. Returns header information."
+        cid = None
 
         if self.queue:
             cid, pos, length = self.queue[-1]
@@ -116,7 +118,7 @@ def read(self):
             cid = s[4:]
             pos = self.fp.tell()
             length = i32(s)
-
+            
         if not is_cid(cid):
             raise SyntaxError("broken PNG file (chunk %s)" % repr(cid))
 
@@ -138,11 +140,15 @@ def call(self, cid, pos, length):
     def crc(self, cid, data):
         "Read and verify checksum"
 
-        crc1 = Image.core.crc32(data, Image.core.crc32(cid))
-        crc2 = i16(self.fp.read(2)), i16(self.fp.read(2))
-        if crc1 != crc2:
-            raise SyntaxError("broken PNG file"
-                              "(bad header checksum in %s)" % cid)
+        try:
+            crc1 = Image.core.crc32(data, Image.core.crc32(cid))
+            crc2 = i16(self.fp.read(2)), i16(self.fp.read(2))
+            if crc1 != crc2:
+                raise SyntaxError("broken PNG file (bad header checksum in %s)"
+                                  % cid)
+        except struct.error:
+            raise SyntaxError("broken PNG file (incomplete checksum in %s)"
+                              % cid)
 
     def crc_skip(self, cid, data):
         "Read checksum.  Used if the C module is not present"
@@ -157,7 +163,11 @@ def verify(self, endchunk=b"IEND"):
         cids = []
 
         while True:
-            cid, pos, length = self.read()
+            try:
+                cid, pos, length = self.read()
+            except struct.error:
+                raise IOError("truncated PNG file")
+
             if cid == endchunk:
                 break
             self.crc(cid, ImageFile._safe_read(self.fp, length))


Test patch:
diff --git a/Tests/test_file_png.py b/Tests/test_file_png.py
--- a/Tests/test_file_png.py
+++ b/Tests/test_file_png.py
@@ -253,6 +253,22 @@ def test_load_verify(self):
         im.load()
         self.assertRaises(RuntimeError, im.verify)
 
+    def test_verify_struct_error(self):
+        # Check open/load/verify exception (#1755)
+
+        # offsets to test, -10: breaks in i32() in read. (IOError)
+        #                  -13: breaks in crc, txt chunk.
+        #                  -14: malformed chunk
+
+        for offset in (-10, -13, -14):
+            with open(TEST_PNG_FILE,'rb') as f:
+                test_file = f.read()[:offset]
+
+            im = Image.open(BytesIO(test_file))
+            self.assertTrue(im.fp is not None)
+            self.assertRaises((IOError, SyntaxError), im.verify)
+
+
     def test_roundtrip_dpi(self):
         # Check dpi roundtripping