Repository: pyinstaller/pyinstaller

Issue:
archive_viewer not able to find CArchive
This affects the use of archive_viewer.py

In the 3.2 release of pyinstaller in archive/readers.py at lines 147 and 149 there is a constant of 4096 hardcoded for the range of searching for the "MAGIC" value back from the end of the archive. In my case, I had a certificate of length 7248 bytes appended to the archive and it never found the "MAGIC" value location. Perhaps a better way is to start with 4096 and if MAGIC is not found, keep doubling the "EndBlockLength" and search again until the "EndBlockLength" is greater than the file length (or some algorithm like that).


Discussion:
This only effects the use of archive_viewer, since `CArchiveReader.checkmagic` is only used by the archive_viewer. Nevertheless we should be careful changing it.

Beside doubling the size to be read another possibility would be to simply read another 4096 bytes each time. On the other hand, this size does not really matter. So we could simple read 8K, 10K or 16K and add another block of this size. But then again, there is no need to keep all this data in memory, so something like this should work, too:
```
buf = b'' ; pos = -1
while True:
    seek back 4096 bytes
    buf = read(min(filelen, 4096)) + buf  # TODO: avoid overlapping data
    pos = buf.rfind(self.MAGIC)
    if pos >= 0:
       break
 else:
     # not found
     raise RuntimeError("%s is not a valid %s archive file" %
                       (self.path, self.__class__.__name__))
```
Below is some minimally tested code for this issue.  One thing to consider is how far back in the archive should one try to find the cookie?  The further one goes, the more chance of a false positive - but likely other things like the unpack of the TOC will fail.  Below is diff from the current version of readers.py that I downloaded 3 hours ago.  If this code looks OK, I can upload the new file.

```
[live@localhost for_release]$ diff ../orig/readers.py readers.py
146,150c146,167
<         
<         self.lib.seek(max(0, filelen-4096)) 
<         searchpos = self.lib.tell()
<         buf = self.lib.read(min(filelen, 4096))
<         pos = buf.rfind(self.MAGIC)
---
>  
>         # cant make myself not try to use a symbolic constant ....
>         _BUFSIZE = 4096
>         buf = b'' ; pos = -1
>         seekpos = filelen
>         ## 0th seeksize is the full buffer size
>         seeksize = _BUFSIZE
>         ## next subtract takes care of a cookie that crosses a search area boundary
>         seeksize1toN = _BUFSIZE - (2 * self._cookie_size)
> 
>         ## while not found, then continue back through archive to try and find the cookie
>         while pos == -1:
>             seekpos = max(0, seekpos - seeksize)
>             self.lib.seek(seekpos) 
>             searchpos = self.lib.tell()
>             buf = self.lib.read(min(filelen, _BUFSIZE))
>             pos = buf.rfind(self.MAGIC)
>             ## change next from value 0 to max(0,filelen - (X * _BUFSIZE)) to limit search area
>             if seekpos <= 0:
>                 break
>             seeksize = seeksize1toN
> 
152c169
<             raise RuntimeError("%s is not a valid %s archive file" %
---
>             raise RuntimeError("%s is not a valid %s archive file -- CANNOT find MAGIC cookie" %
158c175
<             raise RuntimeError("%s is not a valid %s archive file" %
---
>             raise RuntimeError("%s is not a valid %s archive file -- bad MAGIC cookie" %
164c181
<                 raise RuntimeError('Problem with embedded archive in %s' %
---
>                 raise RuntimeError('Problem with embedded archive length in %s' %
[live@localhost for_release]$
```
> Below is some minimally tested code for this issue.

Please submit a pull-request instead of posting patches. I'm not going to copy-and-paste raw patches out of an issue. This is not how github works and makes a lot of work.

Please also have a look at the [Development Guide](https://github.com/pyinstaller/pyinstaller/wiki/Development) and mind adding a test-case for this. Thanks.

Resolving patch:
diff --git a/PyInstaller/archive/readers.py b/PyInstaller/archive/readers.py
--- a/PyInstaller/archive/readers.py
+++ b/PyInstaller/archive/readers.py
@@ -17,6 +17,7 @@
 # TODO clean up this module
 
 import struct
+import os
 
 
 from PyInstaller.loader.pyimod02_archive import ArchiveReader
@@ -142,19 +143,36 @@ def checkmagic(self):
         if self.length:
             self.lib.seek(self.start + self.length, 0)
         else:
-            self.lib.seek(0, 2)
-        filelen = self.lib.tell()
-
-        self.lib.seek(max(0, filelen-4096))
-        searchpos = self.lib.tell()
-        buf = self.lib.read(min(filelen, 4096))
-        pos = buf.rfind(self.MAGIC)
-        if pos == -1:
+            self.lib.seek(0, os.SEEK_END)
+        end_pos = self.lib.tell()
+
+        SEARCH_CHUNK_SIZE = 8192
+        magic_offset = -1
+        while end_pos >= len(self.MAGIC):
+            start_pos = max(end_pos - SEARCH_CHUNK_SIZE, 0)
+            chunk_size = end_pos - start_pos
+            # Is the remaining chunk large enough to hold the pattern?
+            if chunk_size < len(self.MAGIC):
+                break
+            # Read and scan the chunk
+            self.lib.seek(start_pos, os.SEEK_SET)
+            buf = self.lib.read(chunk_size)
+            pos = buf.rfind(self.MAGIC)
+            if pos != -1:
+                magic_offset = start_pos + pos
+                break
+            # Adjust search location for next chunk; ensure proper
+            # overlap
+            end_pos = start_pos + len(self.MAGIC) - 1
+        if magic_offset == -1:
             raise RuntimeError("%s is not a valid %s archive file" %
                                (self.path, self.__class__.__name__))
-        filelen = searchpos + pos + self._cookie_size
+        filelen = magic_offset + self._cookie_size
+        # Read the whole cookie
+        self.lib.seek(magic_offset, os.SEEK_SET)
+        buf = self.lib.read(self._cookie_size)
         (magic, totallen, tocpos, toclen, pyvers, pylib_name) = struct.unpack(
-            self._cookie_format, buf[pos:pos+self._cookie_size])
+            self._cookie_format, buf)
         if magic != self.MAGIC:
             raise RuntimeError("%s is not a valid %s archive file" %
                                (self.path, self.__class__.__name__))
diff --git a/PyInstaller/utils/cliutils/archive_viewer.py b/PyInstaller/utils/cliutils/archive_viewer.py
--- a/PyInstaller/utils/cliutils/archive_viewer.py
+++ b/PyInstaller/utils/cliutils/archive_viewer.py
@@ -65,7 +65,6 @@ def main(name, brief, debug, rec_debug, **unused_options):
         if cmd == 'U':
             if len(stack) > 1:
                 arch = stack[-1][1]
-                arch.lib.close()
                 del stack[-1]
             name, arch = stack[-1]
             show(name, arch)
@@ -106,8 +105,6 @@ def main(name, brief, debug, rec_debug, **unused_options):
 
 def do_cleanup():
     global stack, cleanup
-    for (name, arch) in stack:
-        arch.lib.close()
     stack = []
     for filename in cleanup:
         try:


Test patch: