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