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 5 from collections import Mapping 6 7 from api_schema_graph import APISchemaGraph 8 from branch_utility import BranchUtility 9 from extensions_paths import API, JSON_TEMPLATES 10 from third_party.json_schema_compiler.model import UnixName 11 12 13 _EXTENSION_API = 'extension_api.json' 14 15 # The version where api_features.json is first available. 16 _API_FEATURES_MIN_VERSION = 28 17 # The version where permission_ and manifest_features.json are available and 18 # presented in the current format. 19 _ORIGINAL_FEATURES_MIN_VERSION = 20 20 # API schemas are aggregated in extension_api.json up to this version. 21 _EXTENSION_API_MAX_VERSION = 17 22 # The earliest version for which we have SVN data. 23 _SVN_MIN_VERSION = 5 24 25 26 def _GetChannelFromFeatures(api_name, json_fs, filename): 27 '''Finds API channel information from the features |filename| within the the 28 given |json_fs|. Returns None if channel information for the API cannot be 29 located. 30 ''' 31 feature = json_fs.GetFromFile('%s/%s' % (API, filename)).Get().get(api_name) 32 if feature is None: 33 return None 34 if isinstance(feature, Mapping): 35 # The channel information exists as a solitary dict. 36 return feature.get('channel') 37 # The channel information dict is nested within a list for whitelisting 38 # purposes. Take the newest channel out of all of the entries. 39 return BranchUtility.NewestChannel(entry.get('channel') for entry in feature) 40 41 42 def _GetChannelFromApiFeatures(api_name, json_fs): 43 return _GetChannelFromFeatures(api_name, json_fs, '_api_features.json') 44 45 46 def _GetChannelFromManifestFeatures(api_name, json_fs): 47 # _manifest_features.json uses unix_style API names. 48 api_name = UnixName(api_name) 49 return _GetChannelFromFeatures(api_name, json_fs, '_manifest_features.json') 50 51 52 def _GetChannelFromPermissionFeatures(api_name, json_fs): 53 return _GetChannelFromFeatures(api_name, json_fs, '_permission_features.json') 54 55 56 class AvailabilityFinder(object): 57 '''Generates availability information for APIs by looking at API schemas and 58 _features files over multiple release versions of Chrome. 59 ''' 60 61 def __init__(self, 62 branch_utility, 63 compiled_fs_factory, 64 file_system_iterator, 65 host_file_system, 66 object_store_creator): 67 self._branch_utility = branch_utility 68 self._compiled_fs_factory = compiled_fs_factory 69 self._file_system_iterator = file_system_iterator 70 self._host_file_system = host_file_system 71 self._object_store_creator = object_store_creator 72 def create_object_store(category): 73 return object_store_creator.Create(AvailabilityFinder, category=category) 74 self._top_level_object_store = create_object_store('top_level') 75 self._node_level_object_store = create_object_store('node_level') 76 self._json_fs = compiled_fs_factory.ForJson(self._host_file_system) 77 78 def _GetPredeterminedAvailability(self, api_name): 79 '''Checks a configuration file for hardcoded (i.e. predetermined) 80 availability information for an API. 81 ''' 82 api_info = self._json_fs.GetFromFile( 83 '%s/api_availabilities.json' % JSON_TEMPLATES).Get().get(api_name) 84 if api_info is None: 85 return None 86 if api_info['channel'] == 'stable': 87 return self._branch_utility.GetStableChannelInfo(api_info['version']) 88 else: 89 return self._branch_utility.GetChannelInfo(api_info['channel']) 90 91 def _GetApiSchemaFilename(self, api_name, file_system, version): 92 '''Gets the name of the file which may contain the schema for |api_name| in 93 |file_system|, or None if the API is not found. Note that this may be the 94 single _EXTENSION_API file which all APIs share in older versions of Chrome, 95 in which case it is unknown whether the API actually exists there. 96 ''' 97 def under_api_path(path): 98 return '%s/%s' % (API, path) 99 100 if version == 'trunk' or version > _ORIGINAL_FEATURES_MIN_VERSION: 101 # API schema filenames switch format to unix_hacker_style. 102 api_name = UnixName(api_name) 103 104 # |file_system| will cache the results from the ReadSingle() call. 105 filenames = file_system.ReadSingle(API + '/').Get() 106 107 for ext in ('json', 'idl'): 108 filename = '%s.%s' % (api_name, ext) 109 if filename in filenames: 110 return under_api_path(filename) 111 if _EXTENSION_API in filenames: 112 return under_api_path(_EXTENSION_API) 113 # API schema data could not be found in any .json or .idl file. 114 return None 115 116 def _GetApiSchema(self, api_name, file_system, version): 117 '''Searches |file_system| for |api_name|'s API schema data, and processes 118 and returns it if found. 119 ''' 120 api_filename = self._GetApiSchemaFilename(api_name, file_system, version) 121 if api_filename is None: 122 # No file for the API could be found in the given |file_system|. 123 return None 124 125 schema_fs = self._compiled_fs_factory.ForApiSchema(file_system) 126 api_schemas = schema_fs.GetFromFile(api_filename).Get() 127 matching_schemas = [api for api in api_schemas 128 if api['namespace'] == api_name] 129 # There should only be a single matching schema per file, or zero in the 130 # case of no API data being found in _EXTENSION_API. 131 assert len(matching_schemas) <= 1 132 return matching_schemas or None 133 134 def _HasApiSchema(self, api_name, file_system, version): 135 '''Whether or not an API schema for |api_name|exists in the given 136 |file_system|. 137 ''' 138 filename = self._GetApiSchemaFilename(api_name, file_system, version) 139 if filename is None: 140 return False 141 if filename.endswith(_EXTENSION_API): 142 return self._GetApiSchema(api_name, file_system, version) is not None 143 return True 144 145 def _CheckStableAvailability(self, api_name, file_system, version): 146 '''Checks for availability of an API, |api_name|, on the stable channel. 147 Considers several _features.json files, file system existence, and 148 extension_api.json depending on the given |version|. 149 ''' 150 if version < _SVN_MIN_VERSION: 151 # SVN data isn't available below this version. 152 return False 153 available_channel = None 154 json_fs = self._compiled_fs_factory.ForJson(file_system) 155 if version >= _API_FEATURES_MIN_VERSION: 156 # The _api_features.json file first appears in version 28 and should be 157 # the most reliable for finding API availability. 158 available_channel = _GetChannelFromApiFeatures(api_name, json_fs) 159 if version >= _ORIGINAL_FEATURES_MIN_VERSION: 160 # The _permission_features.json and _manifest_features.json files are 161 # present in Chrome 20 and onwards. Use these if no information could be 162 # found using _api_features.json. 163 available_channel = available_channel or ( 164 _GetChannelFromPermissionFeatures(api_name, json_fs) 165 or _GetChannelFromManifestFeatures(api_name, json_fs)) 166 if available_channel is not None: 167 return available_channel == 'stable' 168 if version >= _SVN_MIN_VERSION: 169 # Fall back to a check for file system existence if the API is not 170 # stable in any of the _features.json files, or if the _features files 171 # do not exist (version 19 and earlier). 172 return self._HasApiSchema(api_name, file_system, version) 173 174 def _CheckChannelAvailability(self, api_name, file_system, channel_info): 175 '''Searches through the _features files in a given |file_system|, falling 176 back to checking the file system for API schema existence, to determine 177 whether or not an API is available on the given channel, |channel_info|. 178 ''' 179 json_fs = self._compiled_fs_factory.ForJson(file_system) 180 available_channel = (_GetChannelFromApiFeatures(api_name, json_fs) 181 or _GetChannelFromPermissionFeatures(api_name, json_fs) 182 or _GetChannelFromManifestFeatures(api_name, json_fs)) 183 if (available_channel is None and 184 self._HasApiSchema(api_name, file_system, channel_info.version)): 185 # If an API is not represented in any of the _features files, but exists 186 # in the filesystem, then assume it is available in this version. 187 # The chrome.windows API is an example of this. 188 available_channel = channel_info.channel 189 # If the channel we're checking is the same as or newer than the 190 # |available_channel| then the API is available at this channel. 191 newest = BranchUtility.NewestChannel((available_channel, 192 channel_info.channel)) 193 return available_channel is not None and newest == channel_info.channel 194 195 def _CheckApiAvailability(self, api_name, file_system, channel_info): 196 '''Determines the availability for an API at a certain version of Chrome. 197 Two branches of logic are used depending on whether or not the API is 198 determined to be 'stable' at the given version. 199 ''' 200 if channel_info.channel == 'stable': 201 return self._CheckStableAvailability(api_name, 202 file_system, 203 channel_info.version) 204 return self._CheckChannelAvailability(api_name, 205 file_system, 206 channel_info) 207 208 def GetApiAvailability(self, api_name): 209 '''Performs a search for an API's top-level availability by using a 210 HostFileSystemIterator instance to traverse multiple version of the 211 SVN filesystem. 212 ''' 213 availability = self._top_level_object_store.Get(api_name).Get() 214 if availability is not None: 215 return availability 216 217 # Check for predetermined availability and cache this information if found. 218 availability = self._GetPredeterminedAvailability(api_name) 219 if availability is not None: 220 self._top_level_object_store.Set(api_name, availability) 221 return availability 222 223 def check_api_availability(file_system, channel_info): 224 return self._CheckApiAvailability(api_name, file_system, channel_info) 225 226 availability = self._file_system_iterator.Descending( 227 self._branch_utility.GetChannelInfo('dev'), 228 check_api_availability) 229 if availability is None: 230 # The API wasn't available on 'dev', so it must be a 'trunk'-only API. 231 availability = self._branch_utility.GetChannelInfo('trunk') 232 self._top_level_object_store.Set(api_name, availability) 233 return availability 234 235 def GetApiNodeAvailability(self, api_name): 236 '''Returns an APISchemaGraph annotated with each node's availability (the 237 ChannelInfo at the oldest channel it's available in). 238 ''' 239 availability_graph = self._node_level_object_store.Get(api_name).Get() 240 if availability_graph is not None: 241 return availability_graph 242 243 def assert_not_none(value): 244 assert value is not None 245 return value 246 247 availability_graph = APISchemaGraph() 248 249 host_fs = self._host_file_system 250 trunk_stat = assert_not_none(host_fs.Stat(self._GetApiSchemaFilename( 251 api_name, host_fs, 'trunk'))) 252 253 # Weird object thing here because nonlocal is Python 3. 254 previous = type('previous', (object,), {'stat': None, 'graph': None}) 255 256 def update_availability_graph(file_system, channel_info): 257 version_filename = assert_not_none(self._GetApiSchemaFilename( 258 api_name, file_system, channel_info.version)) 259 version_stat = assert_not_none(file_system.Stat(version_filename)) 260 261 # Important optimisation: only re-parse the graph if the file changed in 262 # the last revision. Parsing the same schema and forming a graph on every 263 # iteration is really expensive. 264 if version_stat == previous.stat: 265 version_graph = previous.graph 266 else: 267 # Keep track of any new schema elements from this version by adding 268 # them to |availability_graph|. 269 # 270 # Calling |availability_graph|.Lookup() on the nodes being updated 271 # will return the |annotation| object -- the current |channel_info|. 272 version_graph = APISchemaGraph(self._GetApiSchema( 273 api_name, file_system, channel_info.version)) 274 availability_graph.Update(version_graph.Subtract(availability_graph), 275 annotation=channel_info) 276 277 previous.stat = version_stat 278 previous.graph = version_graph 279 280 # Continue looping until there are no longer differences between this 281 # version and trunk. 282 return version_stat != trunk_stat 283 284 self._file_system_iterator.Ascending(self.GetApiAvailability(api_name), 285 update_availability_graph) 286 287 self._node_level_object_store.Set(api_name, availability_graph) 288 return availability_graph 289