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 sys
9import zipfile
10
11from dependency_manager import exceptions
12
13
14def _WinReadOnlyHandler(func, path, execinfo):
15  if not os.access(path, os.W_OK):
16    os.chmod(path, stat.S_IWRITE)
17    func(path)
18  else:
19    raise execinfo[0], execinfo[1], execinfo[2]
20
21
22def RemoveDir(dir_path):
23  if os.path.isdir(dir_path):
24    shutil.rmtree(dir_path, onerror=_WinReadOnlyHandler)
25
26
27def VerifySafeArchive(archive):
28  def ResolvePath(path_name):
29    return os.path.realpath(os.path.abspath(path_name))
30  # Must add pathsep to avoid false positives.
31  # Ex: /tmp/abc/bad_file.py starts with /tmp/a but not /tmp/a/
32  base_path = ResolvePath(os.getcwd()) + os.path.sep
33  for member in archive.namelist():
34    if not ResolvePath(os.path.join(base_path, member)).startswith(base_path):
35      raise exceptions.ArchiveError(
36          'Archive %s contains a bad member: %s.' % (archive.filename, member))
37
38
39def GetModeFromPath(file_path):
40  return stat.S_IMODE(os.stat(file_path).st_mode)
41
42
43def GetModeFromZipInfo(zip_info):
44  return zip_info.external_attr >> 16
45
46
47def SetUnzippedDirPermissions(archive, unzipped_dir):
48  """Set the file permissions in an unzipped archive.
49
50     Designed to be called right after extractall() was called on |archive|.
51     Noop on Win. Otherwise sets the executable bit on files where needed.
52
53     Args:
54         archive: A zipfile.ZipFile object opened for reading.
55         unzipped_dir: A path to a directory containing the unzipped contents
56             of |archive|.
57  """
58  if sys.platform.startswith('win'):
59    # Windows doesn't have an executable bit, so don't mess with the ACLs.
60    return
61  for zip_info in archive.infolist():
62    archive_acls = GetModeFromZipInfo(zip_info)
63    if archive_acls & stat.S_IXUSR:
64      # Only preserve owner execurable permissions.
65      unzipped_path = os.path.abspath(
66          os.path.join(unzipped_dir, zip_info.filename))
67      mode = GetModeFromPath(unzipped_path)
68      os.chmod(unzipped_path, mode | stat.S_IXUSR)
69
70
71def UnzipArchive(archive_path, unzip_path):
72  """Unzips a file if it is a zip file.
73
74  Args:
75      archive_path: The downloaded file to unzip.
76      unzip_path: The destination directory to unzip to.
77
78  Raises:
79      ValueError: If |archive_path| is not a zipfile.
80  """
81  # TODO(aiolos): Add tests once the refactor is completed. crbug.com/551158
82  if not (archive_path and zipfile.is_zipfile(archive_path)):
83    raise ValueError(
84        'Attempting to unzip a non-archive file at %s' % archive_path)
85  if not os.path.exists(unzip_path):
86    os.makedirs(unzip_path)
87  try:
88    with zipfile.ZipFile(archive_path, 'r') as archive:
89      VerifySafeArchive(archive)
90      archive.extractall(path=unzip_path)
91      SetUnzippedDirPermissions(archive, unzip_path)
92  except:
93    if unzip_path and os.path.isdir(unzip_path):
94      RemoveDir(unzip_path)
95    raise
96