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