Home | History | Annotate | Download | only in server2
      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