• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#
2# Copyright (C) 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15#
16"""Parser for Android's version script information."""
17from __future__ import annotations
18
19from dataclasses import dataclass, field
20import logging
21import re
22from typing import (
23    Dict,
24    Iterable,
25    Iterator,
26    List,
27    Mapping,
28    NewType,
29    Optional,
30    TextIO,
31    Tuple,
32    Union,
33)
34
35
36ApiMap = Mapping[str, int]
37Arch = NewType('Arch', str)
38Tag = NewType('Tag', str)
39
40
41ALL_ARCHITECTURES = (
42    Arch('arm'),
43    Arch('arm64'),
44    Arch('x86'),
45    Arch('x86_64'),
46)
47
48
49# Arbitrary magic number. We use the same one in api-level.h for this purpose.
50FUTURE_API_LEVEL = 10000
51
52
53def logger() -> logging.Logger:
54    """Return the main logger for this module."""
55    return logging.getLogger(__name__)
56
57
58@dataclass
59class Tags:
60    """Container class for the tags attached to a symbol or version."""
61
62    tags: tuple[Tag, ...] = field(default_factory=tuple)
63
64    @classmethod
65    def from_strs(cls, strs: Iterable[str]) -> Tags:
66        """Constructs tags from a collection of strings.
67
68        Does not decode API levels.
69        """
70        return Tags(tuple(Tag(s) for s in strs))
71
72    def __contains__(self, tag: Union[Tag, str]) -> bool:
73        return tag in self.tags
74
75    def __iter__(self) -> Iterator[Tag]:
76        yield from self.tags
77
78    @property
79    def has_mode_tags(self) -> bool:
80        """Returns True if any mode tags (apex, llndk, etc) are set."""
81        return self.has_apex_tags or self.has_llndk_tags
82
83    @property
84    def has_apex_tags(self) -> bool:
85        """Returns True if any APEX tags are set."""
86        return 'apex' in self.tags or 'systemapi' in self.tags
87
88    @property
89    def has_llndk_tags(self) -> bool:
90        """Returns True if any LL-NDK tags are set."""
91        return 'llndk' in self.tags
92
93    @property
94    def has_platform_only_tags(self) -> bool:
95        """Returns True if any platform-only tags are set."""
96        return 'platform-only' in self.tags
97
98
99@dataclass
100class Symbol:
101    """A symbol definition from a symbol file."""
102
103    name: str
104    tags: Tags
105
106
107@dataclass
108class Version:
109    """A version block of a symbol file."""
110
111    name: str
112    base: Optional[str]
113    tags: Tags
114    symbols: List[Symbol]
115
116    @property
117    def is_private(self) -> bool:
118        """Returns True if this version block is private (platform only)."""
119        return self.name.endswith('_PRIVATE') or self.name.endswith('_PLATFORM')
120
121
122def get_tags(line: str, api_map: ApiMap) -> Tags:
123    """Returns a list of all tags on this line."""
124    _, _, all_tags = line.strip().partition('#')
125    return Tags(tuple(
126        decode_api_level_tag(Tag(e), api_map)
127        for e in re.split(r'\s+', all_tags) if e.strip()
128    ))
129
130
131def is_api_level_tag(tag: Tag) -> bool:
132    """Returns true if this tag has an API level that may need decoding."""
133    if tag.startswith('introduced='):
134        return True
135    if tag.startswith('introduced-'):
136        return True
137    if tag.startswith('versioned='):
138        return True
139    return False
140
141
142def decode_api_level(api: str, api_map: ApiMap) -> int:
143    """Decodes the API level argument into the API level number.
144
145    For the average case, this just decodes the integer value from the string,
146    but for unreleased APIs we need to translate from the API codename (like
147    "O") to the future API level for that codename.
148    """
149    try:
150        return int(api)
151    except ValueError:
152        pass
153
154    if api == "current":
155        return FUTURE_API_LEVEL
156
157    return api_map[api]
158
159
160def decode_api_level_tag(tag: Tag, api_map: ApiMap) -> Tag:
161    """Decodes API level code name in a tag.
162
163    Raises:
164        ParseError: An unknown version name was found in a tag.
165    """
166    if not is_api_level_tag(tag):
167        return tag
168
169    name, value = split_tag(tag)
170    try:
171        decoded = str(decode_api_level(value, api_map))
172        return Tag(f'{name}={decoded}')
173    except KeyError as ex:
174        raise ParseError(f'Unknown version name in tag: {tag}') from ex
175
176
177def split_tag(tag: Tag) -> Tuple[str, str]:
178    """Returns a key/value tuple of the tag.
179
180    Raises:
181        ValueError: Tag is not a key/value type tag.
182
183    Returns: Tuple of (key, value) of the tag. Both components are strings.
184    """
185    if '=' not in tag:
186        raise ValueError('Not a key/value tag: ' + tag)
187    key, _, value = tag.partition('=')
188    return key, value
189
190
191def get_tag_value(tag: Tag) -> str:
192    """Returns the value of a key/value tag.
193
194    Raises:
195        ValueError: Tag is not a key/value type tag.
196
197    Returns: Value part of tag as a string.
198    """
199    return split_tag(tag)[1]
200
201
202def _should_omit_tags(tags: Tags, arch: Arch, api: int, llndk: bool,
203                      apex: bool) -> bool:
204    """Returns True if the tagged object should be omitted.
205
206    This defines the rules shared between version tagging and symbol tagging.
207    """
208    # The apex and llndk tags will only exclude APIs from other modes. If in
209    # APEX or LLNDK mode and neither tag is provided, we fall back to the
210    # default behavior because all NDK symbols are implicitly available to APEX
211    # and LLNDK.
212    if tags.has_mode_tags:
213        if not apex and not llndk:
214            return True
215        if apex and not tags.has_apex_tags:
216            return True
217        if llndk and not tags.has_llndk_tags:
218            return True
219    if not symbol_in_arch(tags, arch):
220        return True
221    if not symbol_in_api(tags, arch, api):
222        return True
223    return False
224
225
226def should_omit_version(version: Version, arch: Arch, api: int, llndk: bool,
227                        apex: bool) -> bool:
228    """Returns True if the version section should be omitted.
229
230    We want to omit any sections that do not have any symbols we'll have in the
231    stub library. Sections that contain entirely future symbols or only symbols
232    for certain architectures.
233    """
234    if version.is_private:
235        return True
236    if version.tags.has_platform_only_tags:
237        return True
238    return _should_omit_tags(version.tags, arch, api, llndk, apex)
239
240
241def should_omit_symbol(symbol: Symbol, arch: Arch, api: int, llndk: bool,
242                       apex: bool) -> bool:
243    """Returns True if the symbol should be omitted."""
244    return _should_omit_tags(symbol.tags, arch, api, llndk, apex)
245
246
247def symbol_in_arch(tags: Tags, arch: Arch) -> bool:
248    """Returns true if the symbol is present for the given architecture."""
249    has_arch_tags = False
250    for tag in tags:
251        if tag == arch:
252            return True
253        if tag in ALL_ARCHITECTURES:
254            has_arch_tags = True
255
256    # If there were no arch tags, the symbol is available for all
257    # architectures. If there were any arch tags, the symbol is only available
258    # for the tagged architectures.
259    return not has_arch_tags
260
261
262def symbol_in_api(tags: Iterable[Tag], arch: Arch, api: int) -> bool:
263    """Returns true if the symbol is present for the given API level."""
264    introduced_tag = None
265    arch_specific = False
266    for tag in tags:
267        # If there is an arch-specific tag, it should override the common one.
268        if tag.startswith('introduced=') and not arch_specific:
269            introduced_tag = tag
270        elif tag.startswith('introduced-' + arch + '='):
271            introduced_tag = tag
272            arch_specific = True
273        elif tag == 'future':
274            return api == FUTURE_API_LEVEL
275
276    if introduced_tag is None:
277        # We found no "introduced" tags, so the symbol has always been
278        # available.
279        return True
280
281    return api >= int(get_tag_value(introduced_tag))
282
283
284def symbol_versioned_in_api(tags: Iterable[Tag], api: int) -> bool:
285    """Returns true if the symbol should be versioned for the given API.
286
287    This models the `versioned=API` tag. This should be a very uncommonly
288    needed tag, and is really only needed to fix versioning mistakes that are
289    already out in the wild.
290
291    For example, some of libc's __aeabi_* functions were originally placed in
292    the private version, but that was incorrect. They are now in LIBC_N, but
293    when building against any version prior to N we need the symbol to be
294    unversioned (otherwise it won't resolve on M where it is private).
295    """
296    for tag in tags:
297        if tag.startswith('versioned='):
298            return api >= int(get_tag_value(tag))
299    # If there is no "versioned" tag, the tag has been versioned for as long as
300    # it was introduced.
301    return True
302
303
304class ParseError(RuntimeError):
305    """An error that occurred while parsing a symbol file."""
306
307
308class MultiplyDefinedSymbolError(RuntimeError):
309    """A symbol name was multiply defined."""
310    def __init__(self, multiply_defined_symbols: Iterable[str]) -> None:
311        super().__init__(
312            'Version script contains multiple definitions for: {}'.format(
313                ', '.join(multiply_defined_symbols)))
314        self.multiply_defined_symbols = multiply_defined_symbols
315
316
317class SymbolFileParser:
318    """Parses NDK symbol files."""
319    def __init__(self, input_file: TextIO, api_map: ApiMap, arch: Arch,
320                 api: int, llndk: bool, apex: bool) -> None:
321        self.input_file = input_file
322        self.api_map = api_map
323        self.arch = arch
324        self.api = api
325        self.llndk = llndk
326        self.apex = apex
327        self.current_line: Optional[str] = None
328
329    def parse(self) -> List[Version]:
330        """Parses the symbol file and returns a list of Version objects."""
331        versions = []
332        while self.next_line():
333            assert self.current_line is not None
334            if '{' in self.current_line:
335                versions.append(self.parse_version())
336            else:
337                raise ParseError(
338                    f'Unexpected contents at top level: {self.current_line}')
339
340        self.check_no_duplicate_symbols(versions)
341        return versions
342
343    def check_no_duplicate_symbols(self, versions: Iterable[Version]) -> None:
344        """Raises errors for multiply defined symbols.
345
346        This situation is the normal case when symbol versioning is actually
347        used, but this script doesn't currently handle that. The error message
348        will be a not necessarily obvious "error: redefition of 'foo'" from
349        stub.c, so it's better for us to catch this situation and raise a
350        better error.
351        """
352        symbol_names = set()
353        multiply_defined_symbols = set()
354        for version in versions:
355            if should_omit_version(version, self.arch, self.api, self.llndk,
356                                   self.apex):
357                continue
358
359            for symbol in version.symbols:
360                if should_omit_symbol(symbol, self.arch, self.api, self.llndk,
361                                      self.apex):
362                    continue
363
364                if symbol.name in symbol_names:
365                    multiply_defined_symbols.add(symbol.name)
366                symbol_names.add(symbol.name)
367        if multiply_defined_symbols:
368            raise MultiplyDefinedSymbolError(
369                sorted(list(multiply_defined_symbols)))
370
371    def parse_version(self) -> Version:
372        """Parses a single version section and returns a Version object."""
373        assert self.current_line is not None
374        name = self.current_line.split('{')[0].strip()
375        tags = get_tags(self.current_line, self.api_map)
376        symbols: List[Symbol] = []
377        global_scope = True
378        cpp_symbols = False
379        while self.next_line():
380            if '}' in self.current_line:
381                # Line is something like '} BASE; # tags'. Both base and tags
382                # are optional here.
383                base = self.current_line.partition('}')[2]
384                base = base.partition('#')[0].strip()
385                if not base.endswith(';'):
386                    raise ParseError(
387                        'Unterminated version/export "C++" block (expected ;).')
388                if cpp_symbols:
389                    cpp_symbols = False
390                else:
391                    base = base.rstrip(';').rstrip()
392                    return Version(name, base or None, tags, symbols)
393            elif 'extern "C++" {' in self.current_line:
394                cpp_symbols = True
395            elif not cpp_symbols and ':' in self.current_line:
396                visibility = self.current_line.split(':')[0].strip()
397                if visibility == 'local':
398                    global_scope = False
399                elif visibility == 'global':
400                    global_scope = True
401                else:
402                    raise ParseError('Unknown visiblity label: ' + visibility)
403            elif global_scope and not cpp_symbols:
404                symbols.append(self.parse_symbol())
405            else:
406                # We're in a hidden scope or in 'extern "C++"' block. Ignore
407                # everything.
408                pass
409        raise ParseError('Unexpected EOF in version block.')
410
411    def parse_symbol(self) -> Symbol:
412        """Parses a single symbol line and returns a Symbol object."""
413        assert self.current_line is not None
414        if ';' not in self.current_line:
415            raise ParseError(
416                'Expected ; to terminate symbol: ' + self.current_line)
417        if '*' in self.current_line:
418            raise ParseError(
419                'Wildcard global symbols are not permitted.')
420        # Line is now in the format "<symbol-name>; # tags"
421        name, _, _ = self.current_line.strip().partition(';')
422        tags = get_tags(self.current_line, self.api_map)
423        return Symbol(name, tags)
424
425    def next_line(self) -> str:
426        """Returns the next non-empty non-comment line.
427
428        A return value of '' indicates EOF.
429        """
430        line = self.input_file.readline()
431        while not line.strip() or line.strip().startswith('#'):
432            line = self.input_file.readline()
433
434            # We want to skip empty lines, but '' indicates EOF.
435            if not line:
436                break
437        self.current_line = line
438        return self.current_line
439