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