1# Copyright 2015 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import os 6import shutil 7import stat 8import subprocess 9import sys 10import zipfile_2_7_13 as zipfile 11 12from dependency_manager import exceptions 13 14 15def _WinReadOnlyHandler(func, path, execinfo): 16 if not os.access(path, os.W_OK): 17 os.chmod(path, stat.S_IWRITE) 18 func(path) 19 else: 20 raise execinfo[0], execinfo[1], execinfo[2] 21 22 23def RemoveDir(dir_path): 24 assert os.path.isabs(dir_path) 25 if sys.platform.startswith('win'): 26 dir_path = u'\\\\?\\' + dir_path 27 if os.path.isdir(dir_path): 28 shutil.rmtree(dir_path, onerror=_WinReadOnlyHandler) 29 30 31def VerifySafeArchive(archive): 32 def ResolvePath(path_name): 33 return os.path.realpath(os.path.abspath(path_name)) 34 # Must add pathsep to avoid false positives. 35 # Ex: /tmp/abc/bad_file.py starts with /tmp/a but not /tmp/a/ 36 base_path = ResolvePath(os.getcwd()) + os.path.sep 37 for member in archive.namelist(): 38 if not ResolvePath(os.path.join(base_path, member)).startswith(base_path): 39 raise exceptions.ArchiveError( 40 'Archive %s contains a bad member: %s.' % (archive.filename, member)) 41 42 43def GetModeFromPath(file_path): 44 return stat.S_IMODE(os.stat(file_path).st_mode) 45 46 47def GetModeFromZipInfo(zip_info): 48 return zip_info.external_attr >> 16 49 50 51def SetUnzippedDirPermissions(archive, unzipped_dir): 52 """Set the file permissions in an unzipped archive. 53 54 Designed to be called right after extractall() was called on |archive|. 55 Noop on Win. Otherwise sets the executable bit on files where needed. 56 57 Args: 58 archive: A zipfile.ZipFile object opened for reading. 59 unzipped_dir: A path to a directory containing the unzipped contents 60 of |archive|. 61 """ 62 if sys.platform.startswith('win'): 63 # Windows doesn't have an executable bit, so don't mess with the ACLs. 64 return 65 for zip_info in archive.infolist(): 66 archive_acls = GetModeFromZipInfo(zip_info) 67 if archive_acls & stat.S_IXUSR: 68 # Only preserve owner execurable permissions. 69 unzipped_path = os.path.abspath( 70 os.path.join(unzipped_dir, zip_info.filename)) 71 mode = GetModeFromPath(unzipped_path) 72 os.chmod(unzipped_path, mode | stat.S_IXUSR) 73 74 75def UnzipArchive(archive_path, unzip_path): 76 """Unzips a file if it is a zip file. 77 78 Args: 79 archive_path: The downloaded file to unzip. 80 unzip_path: The destination directory to unzip to. 81 82 Raises: 83 ValueError: If |archive_path| is not a zipfile. 84 """ 85 # TODO(aiolos): Add tests once the refactor is completed. crbug.com/551158 86 if not (archive_path and zipfile.is_zipfile(archive_path)): 87 raise ValueError( 88 'Attempting to unzip a non-archive file at %s' % archive_path) 89 if not os.path.exists(unzip_path): 90 os.makedirs(unzip_path) 91 # The Python ZipFile does not support symbolic links, which makes it 92 # unsuitable for Mac builds. so use ditto instead. crbug.com/700097. 93 if sys.platform.startswith('darwin'): 94 assert os.path.isabs(unzip_path) 95 unzip_cmd = ['ditto', '-x', '-k', archive_path, unzip_path] 96 proc = subprocess.Popen(unzip_cmd, bufsize=0, stdout=subprocess.PIPE, 97 stderr=subprocess.PIPE) 98 proc.communicate() 99 return 100 try: 101 with zipfile.ZipFile(archive_path, 'r') as archive: 102 VerifySafeArchive(archive) 103 assert os.path.isabs(unzip_path) 104 unzip_path_without_prefix = unzip_path 105 if sys.platform.startswith('win'): 106 unzip_path = u'\\\\?\\' + unzip_path 107 archive.extractall(path=unzip_path) 108 SetUnzippedDirPermissions(archive, unzip_path) 109 except: 110 # Hack necessary because isdir doesn't work with escaped paths on Windows. 111 if unzip_path_without_prefix and os.path.isdir(unzip_path_without_prefix): 112 RemoveDir(unzip_path_without_prefix) 113 raise 114