1# Copyright 2013 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
5from copy import deepcopy
6
7from file_system import FileSystem, StatInfo, FileNotFoundError
8from future import Future
9
10
11def _GetAsyncFetchCallback(unpatched_files_future,
12                           patched_files_future,
13                           dirs_value,
14                           patched_file_system):
15  def patch_directory_listing(path, original_listing):
16    added, deleted, modified = (
17        patched_file_system._GetDirectoryListingFromPatch(path))
18    if original_listing is None:
19      if len(added) == 0:
20        raise FileNotFoundError('Directory %s not found in the patch.' % path)
21      return added
22    return list((set(original_listing) | set(added)) - set(deleted))
23
24  def resolve():
25    files = unpatched_files_future.Get()
26    files.update(patched_files_future.Get())
27    files.update(
28        dict((path, patch_directory_listing(path, dirs_value[path]))
29             for path in dirs_value))
30    return files
31
32  return resolve
33
34
35class PatchedFileSystem(FileSystem):
36  ''' Class to fetch resources with a patch applied.
37  '''
38  def __init__(self, base_file_system, patcher):
39    self._base_file_system = base_file_system
40    self._patcher = patcher
41
42  def Read(self, paths, skip_not_found=False):
43    patched_files = set()
44    added, deleted, modified = self._patcher.GetPatchedFiles()
45    if set(paths) & set(deleted):
46      def raise_file_not_found():
47        raise FileNotFoundError('Files are removed from the patch.')
48      return Future(callback=raise_file_not_found)
49    patched_files |= (set(added) | set(modified))
50    dir_paths = set(path for path in paths if path.endswith('/'))
51    file_paths = set(paths) - dir_paths
52    patched_paths = file_paths & patched_files
53    unpatched_paths = file_paths - patched_files
54    return Future(callback=_GetAsyncFetchCallback(
55        self._base_file_system.Read(unpatched_paths,
56                                    skip_not_found=skip_not_found),
57        self._patcher.Apply(patched_paths, self._base_file_system),
58        self._TryReadDirectory(dir_paths),
59        self))
60
61  def Refresh(self):
62    return self._base_file_system.Refresh()
63
64  ''' Given the list of patched files, it's not possible to determine whether
65  a directory to read exists in self._base_file_system. So try reading each one
66  and handle FileNotFoundError.
67  '''
68  def _TryReadDirectory(self, paths):
69    value = {}
70    for path in paths:
71      assert path.endswith('/')
72      try:
73        value[path] = self._base_file_system.ReadSingle(path).Get()
74      except FileNotFoundError:
75        value[path] = None
76    return value
77
78  def _GetDirectoryListingFromPatch(self, path):
79    assert path.endswith('/')
80    def _FindChildrenInPath(files, path):
81      result = []
82      for f in files:
83        if f.startswith(path):
84          child_path = f[len(path):]
85          if '/' in child_path:
86            child_name = child_path[0:child_path.find('/') + 1]
87          else:
88            child_name = child_path
89          result.append(child_name)
90      return result
91
92    added, deleted, modified = (tuple(
93        _FindChildrenInPath(files, path)
94        for files in self._patcher.GetPatchedFiles()))
95
96    # A patch applies to files only. It cannot delete directories.
97    deleted_files = [child for child in deleted if not child.endswith('/')]
98    # However, these directories are actually modified because their children
99    # are patched.
100    modified += [child for child in deleted if child.endswith('/')]
101
102    return (added, deleted_files, modified)
103
104  def _PatchStat(self, stat_info, version, added, deleted, modified):
105    assert len(added) + len(deleted) + len(modified) > 0
106    assert stat_info.child_versions is not None
107
108    # Deep copy before patching to make sure it doesn't interfere with values
109    # cached in memory.
110    stat_info = deepcopy(stat_info)
111
112    stat_info.version = version
113    for child in added + modified:
114      stat_info.child_versions[child] = version
115    for child in deleted:
116      if stat_info.child_versions.get(child):
117        del stat_info.child_versions[child]
118
119    return stat_info
120
121  def Stat(self, path):
122    version = self._patcher.GetVersion()
123    assert version is not None
124    version = 'patched_%s' % version
125
126    directory, filename = path.rsplit('/', 1)
127    added, deleted, modified = self._GetDirectoryListingFromPatch(
128        directory + '/')
129
130    if len(added) > 0:
131      # There are new files added. It's possible (if |directory| is new) that
132      # self._base_file_system.Stat will throw an exception.
133      try:
134        stat_info = self._PatchStat(
135            self._base_file_system.Stat(directory + '/'),
136            version,
137            added,
138            deleted,
139            modified)
140      except FileNotFoundError:
141        stat_info = StatInfo(
142            version,
143            dict((child, version) for child in added + modified))
144    elif len(deleted) + len(modified) > 0:
145      # No files were added.
146      stat_info = self._PatchStat(self._base_file_system.Stat(directory + '/'),
147                                  version,
148                                  added,
149                                  deleted,
150                                  modified)
151    else:
152      # No changes are made in this directory.
153      return self._base_file_system.Stat(path)
154
155    if stat_info.child_versions is not None:
156      if filename:
157        if filename in stat_info.child_versions:
158          stat_info = StatInfo(stat_info.child_versions[filename])
159        else:
160          raise FileNotFoundError('%s was not in child versions' % filename)
161    return stat_info
162
163  def GetIdentity(self):
164    return '%s(%s,%s)' % (self.__class__.__name__,
165                          self._base_file_system.GetIdentity(),
166                          self._patcher.GetIdentity())
167