Python zipfile removes execute permissions from binaries

The reason for this can be found in the _extract_member() method in zipfile.py, it only calls shutil.copyfileobj() which will write the output file without any execute bits.

The easiest way to solve this is by subclassing ZipFile and changing extract() (or patching in an extended version. By default it is:

def extract(self, member, path=None, pwd=None):
    """Extract a member from the archive to the current working directory,
       using its full name. Its file information is extracted as accurately
       as possible. `member' may be a filename or a ZipInfo object. You can
       specify a different directory using `path'.
    """
    if not isinstance(member, ZipInfo):
        member = self.getinfo(member)

    if path is None:
        path = os.getcwd()

    return self._extract_member(member, path, pwd)

This last line should be changed to actually set the mode based on the original attributes. You can do it this way:

import os
import sys
from zipfile import ZipFile, ZipInfo

class MyZipFile(ZipFile):

    if sys.version_info < (3, 6):

        def extract(self, member, path=None, pwd=None):
            if not isinstance(member, ZipInfo):
                member = self.getinfo(member)
            if path is None:
                path = os.getcwd()
            ret_val = self._extract_member(member, path, pwd)
            attr = member.external_attr >> 16
            os.chmod(ret_val, attr)
            return ret_val

    else:

        def _extract_member(member, ZipInfo):
            if not isinstance(member, ZipInfo):
                member = self.getinfo(member)
            path = super(ZipFile, self)._extract_member(member, targetpath, pwd)

            if member.external_attr >  0xffff:
                 os.chmod(path, member.external_attr >> 16)
            return path


with MyZipFile('test.zip') as zfp:
    zfp.extractall()

(The above is based on Python 3.5 and assumes the zipfile is called test.zip)


As noted by Rafael Almeida, extractall does not work on python 3.6. A simple workaround is to also override the extractall method so that it calls extract instead of _extract_member. Not clean, but working until ZipFile has a more comprehensive solution.

class MyZipFile(ZipFile):

    def extract(self, member, path=None, pwd=None):
        if not isinstance(member, ZipInfo):
            member = self.getinfo(member)

        if path is None:
            path = os.getcwd()

        ret_val = self._extract_member(member, path, pwd)
        attr = member.external_attr >> 16
        if attr != 0:
            os.chmod(ret_val, attr)
        return ret_val

    def extractall(self, path=None, members=None, pwd=None):
        if members is None:
            members = self.namelist()

        if path is None:
            path = os.getcwd()
        else:
            path = os.fspath(path)

        for zipinfo in members:
            self.extract(zipinfo, path, pwd)

This works on python 3.6:

from zipfile import ZipFile, ZipInfo


class ZipFileWithPermissions(ZipFile):
""" Custom ZipFile class handling file permissions. """
    def _extract_member(self, member, targetpath, pwd):
        if not isinstance(member, ZipInfo):
            member = self.getinfo(member)

        targetpath = super()._extract_member(member, targetpath, pwd)

        attr = member.external_attr >> 16
        if attr != 0:
            os.chmod(targetpath, attr)
        return targetpath