Fix error wrapping stdout when redirecting
authorChristian Herdtweck <christian.herdtweck@intra2net.com>
Thu, 24 May 2018 10:29:49 +0000 (12:29 +0200)
committerChristian Herdtweck <christian.herdtweck@intra2net.com>
Mon, 8 Oct 2018 07:37:41 +0000 (09:37 +0200)
In some cases (e.g. redirecting stdout to shell), the automatic determination
in ZipFile constructor whether its file argument can seek() and tell() fails.
stdout seems to support both but returns bogus values for tell(), resulting
in negative sizes when writing the archive's end record.

Provide a fix with additional arg force_wrap

Also ensure that CRCs are always correct as suggested in python docs and
stackoverflow

src/zip_stream.py

index 0bda371..d4adf4a 100644 (file)
@@ -78,9 +78,55 @@ def _get_compressor(compress_type):
         return None
 
 
+class BytesTellWrapper:
+    """
+    Wrapper around a write-only stream that supports tell but not seek
+
+    copy of zipfile._Tellable
+    """
+    def __init__(self, fp):
+        self.fp = fp
+        self.offset = 0
+
+    def write(self, data):
+        n = self.fp.write(data)
+        self.offset += n
+        return n
+
+    def tell(self):
+        return self.offset
+
+    def flush(self):
+        self.fp.flush()
+
+    def close(self):
+        self.fp.close()
+
+
 class ZipStream(ZipFile):
     """Subclass of ZipFile that supports non-seekable input and output"""
 
+    def __init__(self, file, *args, **kwargs):
+        """
+        Create ZipStream instance which is like a ZipFile plus functions
+        create_zipinfo and write_stream.
+
+        ZipFile determines whether output stream can seek() and tell().
+        Unfortunately some streams (like sys.stdout.buffer when redirecting
+        output) seem to support these methods but only return 0 from tell.
+
+        :param bool force_wrap: force wrapping of output stream with
+                                BytesTellWrapper.
+        """
+        if 'force_wrap' in kwargs and kwargs['force_wrap']:
+            if isstr(file):
+                raise ValueError('force_wrap only makes sense for streams')
+            del kwargs['force_wrap']
+            super(ZipStream, self).__init__(BytesTellWrapper(file), *args,
+                                            **kwargs)
+        else:
+            super(ZipStream, self).__init__(file, *args, **kwargs)
+
     def create_zipinfo(self, filename, arcname=None):
         """
         Create ZipInfo for given file
@@ -168,7 +214,7 @@ class ZipStream(ZipFile):
                 if not buf:
                     break
                 file_size = file_size + len(buf)
-                CRC = crc32(buf, CRC)
+                CRC = crc32(buf, CRC) & 0xffffffff
                 if cmpr:
                     buf = cmpr.compress(buf)
                     compress_size = compress_size + len(buf)