1#!/usr/bin/env python3 2"""Generates config files for Android file system properties. 3 4This script is used for generating configuration files for configuring 5Android filesystem properties. Internally, its composed of a plug-able 6interface to support the understanding of new input and output parameters. 7 8Run the help for a list of supported plugins and their capabilities. 9 10Further documentation can be found in the README. 11""" 12 13import argparse 14import configparser 15import ctypes 16import re 17import sys 18import textwrap 19 20# Keep the tool in one file to make it easy to run. 21# pylint: disable=too-many-lines 22 23 24# Lowercase generator used to be inline with @staticmethod. 25class generator(object): # pylint: disable=invalid-name 26 """A decorator class to add commandlet plugins. 27 28 Used as a decorator to classes to add them to 29 the internal plugin interface. Plugins added 30 with @generator() are automatically added to 31 the command line. 32 33 For instance, to add a new generator 34 called foo and have it added just do this: 35 36 @generator("foo") 37 class FooGen(object): 38 ... 39 """ 40 _generators = {} 41 42 def __init__(self, gen): 43 """ 44 Args: 45 gen (str): The name of the generator to add. 46 47 Raises: 48 ValueError: If there is a similarly named generator already added. 49 50 """ 51 self._gen = gen 52 53 if gen in generator._generators: 54 raise ValueError('Duplicate generator name: ' + gen) 55 56 generator._generators[gen] = None 57 58 def __call__(self, cls): 59 60 generator._generators[self._gen] = cls() 61 return cls 62 63 @staticmethod 64 def get(): 65 """Gets the list of generators. 66 67 Returns: 68 The list of registered generators. 69 """ 70 return generator._generators 71 72 73class Utils(object): 74 """Various assorted static utilities.""" 75 76 @staticmethod 77 def in_any_range(value, ranges): 78 """Tests if a value is in a list of given closed range tuples. 79 80 A range tuple is a closed range. That means it's inclusive of its 81 start and ending values. 82 83 Args: 84 value (int): The value to test. 85 range [(int, int)]: The closed range list to test value within. 86 87 Returns: 88 True if value is within the closed range, false otherwise. 89 """ 90 91 return any(lower <= value <= upper for (lower, upper) in ranges) 92 93 @staticmethod 94 def get_login_and_uid_cleansed(aid): 95 """Returns a passwd/group file safe logon and uid. 96 97 This checks that the logon and uid of the AID do not 98 contain the delimiter ":" for a passwd/group file. 99 100 Args: 101 aid (AID): The aid to check 102 103 Returns: 104 logon, uid of the AID after checking its safe. 105 106 Raises: 107 ValueError: If there is a delimiter charcter found. 108 """ 109 logon = aid.friendly 110 uid = aid.normalized_value 111 if ':' in uid: 112 raise ValueError( 113 'Cannot specify delimiter character ":" in uid: "%s"' % uid) 114 if ':' in logon: 115 raise ValueError( 116 'Cannot specify delimiter character ":" in logon: "%s"' % 117 logon) 118 return logon, uid 119 120 121class AID(object): 122 """This class represents an Android ID or an AID. 123 124 Attributes: 125 identifier (str): The identifier name for a #define. 126 value (str) The User Id (uid) of the associate define. 127 found (str) The file it was found in, can be None. 128 normalized_value (str): Same as value, but base 10. 129 friendly (str): The friendly name of aid. 130 """ 131 132 PREFIX = 'AID_' 133 134 # Some of the AIDS like AID_MEDIA_EX had names like mediaex 135 # list a map of things to fixup until we can correct these 136 # at a later date. 137 _FIXUPS = { 138 'media_drm': 'mediadrm', 139 'media_ex': 'mediaex', 140 'media_codec': 'mediacodec' 141 } 142 143 def __init__(self, identifier, value, found, login_shell): 144 """ 145 Args: 146 identifier: The identifier name for a #define <identifier>. 147 value: The value of the AID, aka the uid. 148 found (str): The file found in, not required to be specified. 149 login_shell (str): The shell field per man (5) passwd file. 150 Raises: 151 ValueError: if the friendly name is longer than 31 characters as 152 that is bionic's internal buffer size for name. 153 ValueError: if value is not a valid string number as processed by 154 int(x, 0) 155 """ 156 self.identifier = identifier 157 self.value = value 158 self.found = found 159 self.login_shell = login_shell 160 161 try: 162 self.normalized_value = str(int(value, 0)) 163 except ValueError: 164 raise ValueError( 165 'Invalid "value", not aid number, got: \"%s\"' % value) 166 167 # Where we calculate the friendly name 168 friendly = identifier[len(AID.PREFIX):].lower() 169 self.friendly = AID._fixup_friendly(friendly) 170 171 if len(self.friendly) > 31: 172 raise ValueError( 173 'AID names must be under 32 characters "%s"' % self.friendly) 174 175 def __eq__(self, other): 176 177 return self.identifier == other.identifier \ 178 and self.value == other.value and self.found == other.found \ 179 and self.normalized_value == other.normalized_value \ 180 and self.login_shell == other.login_shell 181 182 def __repr__(self): 183 return "AID { identifier = %s, value = %s, normalized_value = %s, login_shell = %s }" % ( 184 self.identifier, self.value, self.normalized_value, self.login_shell) 185 186 @staticmethod 187 def is_friendly(name): 188 """Determines if an AID is a freindly name or C define. 189 190 For example if name is AID_SYSTEM it returns false, if name 191 was system, it would return true. 192 193 Returns: 194 True if name is a friendly name False otherwise. 195 """ 196 197 return not name.startswith(AID.PREFIX) 198 199 @staticmethod 200 def _fixup_friendly(friendly): 201 """Fixup friendly names that historically don't follow the convention. 202 203 Args: 204 friendly (str): The friendly name. 205 206 Returns: 207 The fixedup friendly name as a str. 208 """ 209 210 if friendly in AID._FIXUPS: 211 return AID._FIXUPS[friendly] 212 213 return friendly 214 215 216class FSConfig(object): 217 """Represents a filesystem config array entry. 218 219 Represents a file system configuration entry for specifying 220 file system capabilities. 221 222 Attributes: 223 mode (str): The mode of the file or directory. 224 user (str): The uid or #define identifier (AID_SYSTEM) 225 group (str): The gid or #define identifier (AID_SYSTEM) 226 caps (str): The capability set. 227 path (str): The path of the file or directory. 228 filename (str): The file it was found in. 229 """ 230 231 def __init__(self, mode, user, group, caps, path, filename): 232 """ 233 Args: 234 mode (str): The mode of the file or directory. 235 user (str): The uid or #define identifier (AID_SYSTEM) 236 group (str): The gid or #define identifier (AID_SYSTEM) 237 caps (str): The capability set as a list. 238 path (str): The path of the file or directory. 239 filename (str): The file it was found in. 240 """ 241 self.mode = mode 242 self.user = user 243 self.group = group 244 self.caps = caps 245 self.path = path 246 self.filename = filename 247 248 def __eq__(self, other): 249 250 return self.mode == other.mode and self.user == other.user \ 251 and self.group == other.group and self.caps == other.caps \ 252 and self.path == other.path and self.filename == other.filename 253 254 def __repr__(self): 255 return 'FSConfig(%r, %r, %r, %r, %r, %r)' % (self.mode, self.user, 256 self.group, self.caps, 257 self.path, self.filename) 258 259 260class CapabilityHeaderParser(object): 261 """Parses capability.h file 262 263 Parses a C header file and extracts lines starting with #define CAP_<name>. 264 """ 265 266 _CAP_DEFINE = re.compile(r'\s*#define\s+(CAP_\S+)\s+(\S+)') 267 _SKIP_CAPS = ['CAP_LAST_CAP', 'CAP_TO_INDEX(x)', 'CAP_TO_MASK(x)'] 268 269 def __init__(self, capability_header): 270 """ 271 Args: 272 capability_header (str): file name for the header file containing AID entries. 273 """ 274 275 self.caps = {} 276 with open(capability_header) as open_file: 277 self._parse(open_file) 278 279 def _parse(self, capability_file): 280 """Parses a capability header file. Internal use only. 281 282 Args: 283 capability_file (file): The open capability header file to parse. 284 """ 285 286 for line in capability_file: 287 match = CapabilityHeaderParser._CAP_DEFINE.match(line) 288 if match: 289 cap = match.group(1) 290 value = match.group(2) 291 292 if not cap in self._SKIP_CAPS: 293 try: 294 self.caps[cap] = int(value, 0) 295 except ValueError: 296 sys.exit('Could not parse capability define "%s":"%s"' 297 % (cap, value)) 298 299 300class AIDHeaderParser(object): 301 """Parses an android_filesystem_config.h file. 302 303 Parses a C header file and extracts lines starting with #define AID_<name> 304 while capturing the OEM defined ranges and ignoring other ranges. It also 305 skips some hardcoded AIDs it doesn't need to generate a mapping for. 306 It provides some basic checks. The information extracted from this file can 307 later be used to quickly check other things (like oem ranges) as well as 308 generating a mapping of names to uids. It was primarily designed to parse 309 the private/android_filesystem_config.h, but any C header should work. 310 """ 311 312 _SKIP_AIDS = [ 313 re.compile(r'%sUNUSED[0-9].*' % AID.PREFIX), 314 re.compile(r'%sAPP' % AID.PREFIX), 315 re.compile(r'%sUSER' % AID.PREFIX) 316 ] 317 _AID_DEFINE = re.compile(r'\s*#define\s+%s.*' % AID.PREFIX) 318 _RESERVED_RANGE = re.compile( 319 r'#define AID_(.+)_RESERVED_(?:(\d+)_)?(START|END)\s+(\d+)') 320 321 # AID lines cannot end with _START or _END, ie AID_FOO is OK 322 # but AID_FOO_START is skiped. Note that AID_FOOSTART is NOT skipped. 323 _AID_SKIP_RANGE = ['_START', '_END'] 324 _COLLISION_OK = ['AID_APP', 'AID_APP_START', 'AID_USER', 'AID_USER_OFFSET'] 325 326 def __init__(self, aid_header): 327 """ 328 Args: 329 aid_header (str): file name for the header 330 file containing AID entries. 331 """ 332 self._aid_header = aid_header 333 self._aid_name_to_value = {} 334 self._aid_value_to_name = {} 335 self._ranges = {} 336 337 with open(aid_header) as open_file: 338 self._parse(open_file) 339 340 try: 341 self._process_and_check() 342 except ValueError as exception: 343 sys.exit('Error processing parsed data: "%s"' % (str(exception))) 344 345 def _parse(self, aid_file): 346 """Parses an AID header file. Internal use only. 347 348 Args: 349 aid_file (file): The open AID header file to parse. 350 """ 351 352 ranges_by_name = {} 353 for lineno, line in enumerate(aid_file): 354 355 def error_message(msg): 356 """Creates an error message with the current parsing state.""" 357 # pylint: disable=cell-var-from-loop 358 return 'Error "{}" in file: "{}" on line: {}'.format( 359 msg, self._aid_header, str(lineno)) 360 361 range_match = self._RESERVED_RANGE.match(line) 362 if range_match: 363 partition, name, start, value = range_match.groups() 364 partition = partition.lower() 365 if name is None: 366 name = "unnamed" 367 start = start == "START" 368 value = int(value, 0) 369 370 if partition == 'oem': 371 partition = 'vendor' 372 373 if partition not in ranges_by_name: 374 ranges_by_name[partition] = {} 375 if name not in ranges_by_name[partition]: 376 ranges_by_name[partition][name] = [None, None] 377 if ranges_by_name[partition][name][0 if start else 1] is not None: 378 sys.exit(error_message("{} of range {} of partition {} was already defined".format( 379 "Start" if start else "End", name, partition))) 380 ranges_by_name[partition][name][0 if start else 1] = value 381 382 if AIDHeaderParser._AID_DEFINE.match(line): 383 chunks = line.split() 384 identifier = chunks[1] 385 value = chunks[2] 386 387 if any( 388 x.match(identifier) 389 for x in AIDHeaderParser._SKIP_AIDS): 390 continue 391 392 try: 393 if not any( 394 identifier.endswith(x) 395 for x in AIDHeaderParser._AID_SKIP_RANGE): 396 self._handle_aid(identifier, value) 397 except ValueError as exception: 398 sys.exit( 399 error_message('{} for "{}"'.format( 400 exception, identifier))) 401 402 for partition in ranges_by_name: 403 for name in ranges_by_name[partition]: 404 start = ranges_by_name[partition][name][0] 405 end = ranges_by_name[partition][name][1] 406 if start is None: 407 sys.exit("Range '%s' for partition '%s' had undefined start" % (name, partition)) 408 if end is None: 409 sys.exit("Range '%s' for partition '%s' had undefined end" % (name, partition)) 410 if start > end: 411 sys.exit("Range '%s' for partition '%s' had start after end. Start: %d, end: %d" % (name, partition, start, end)) 412 413 if partition not in self._ranges: 414 self._ranges[partition] = [] 415 self._ranges[partition].append((start, end)) 416 417 def _handle_aid(self, identifier, value): 418 """Handle an AID C #define. 419 420 Handles an AID, quick checking, generating the friendly name and 421 adding it to the internal maps. Internal use only. 422 423 Args: 424 identifier (str): The name of the #define identifier. ie AID_FOO. 425 value (str): The value associated with the identifier. 426 427 Raises: 428 ValueError: With message set to indicate the error. 429 """ 430 431 aid = AID(identifier, value, self._aid_header, '/system/bin/sh') 432 433 # duplicate name 434 if aid.friendly in self._aid_name_to_value: 435 raise ValueError('Duplicate aid "%s"' % identifier) 436 437 if value in self._aid_value_to_name and aid.identifier not in AIDHeaderParser._COLLISION_OK: 438 raise ValueError( 439 'Duplicate aid value "%s" for %s' % (value, identifier)) 440 441 self._aid_name_to_value[aid.friendly] = aid 442 self._aid_value_to_name[value] = aid.friendly 443 444 def _process_and_check(self): 445 """Process, check and populate internal data structures. 446 447 After parsing and generating the internal data structures, this method 448 is responsible for quickly checking ALL of the acquired data. 449 450 Raises: 451 ValueError: With the message set to indicate the specific error. 452 """ 453 454 # Check for overlapping ranges 455 for ranges in self._ranges.values(): 456 for i, range1 in enumerate(ranges): 457 for range2 in ranges[i + 1:]: 458 if AIDHeaderParser._is_overlap(range1, range2): 459 raise ValueError( 460 "Overlapping OEM Ranges found %s and %s" % 461 (str(range1), str(range2))) 462 463 # No core AIDs should be within any oem range. 464 for aid in self._aid_value_to_name: 465 for ranges in self._ranges.values(): 466 if Utils.in_any_range(int(aid, 0), ranges): 467 name = self._aid_value_to_name[aid] 468 raise ValueError( 469 'AID "%s" value: %u within reserved OEM Range: "%s"' % 470 (name, aid, str(ranges))) 471 472 @property 473 def ranges(self): 474 """Retrieves the OEM closed ranges as a list of tuples. 475 476 Returns: 477 A list of closed range tuples: [ (0, 42), (50, 105) ... ] 478 """ 479 return self._ranges 480 481 @property 482 def aids(self): 483 """Retrieves the list of found AIDs. 484 485 Returns: 486 A list of AID() objects. 487 """ 488 return self._aid_name_to_value.values() 489 490 @staticmethod 491 def _is_overlap(range_a, range_b): 492 """Calculates the overlap of two range tuples. 493 494 A range tuple is a closed range. A closed range includes its endpoints. 495 Note that python tuples use () notation which collides with the 496 mathematical notation for open ranges. 497 498 Args: 499 range_a: The first tuple closed range eg (0, 5). 500 range_b: The second tuple closed range eg (3, 7). 501 502 Returns: 503 True if they overlap, False otherwise. 504 """ 505 506 return max(range_a[0], range_b[0]) <= min(range_a[1], range_b[1]) 507 508 509class FSConfigFileParser(object): 510 """Parses a config.fs ini format file. 511 512 This class is responsible for parsing the config.fs ini format files. 513 It collects and checks all the data in these files and makes it available 514 for consumption post processed. 515 """ 516 517 # These _AID vars work together to ensure that an AID section name 518 # cannot contain invalid characters for a C define or a passwd/group file. 519 # Since _AID_PREFIX is within the set of _AID_MATCH the error logic only 520 # checks end, if you change this, you may have to update the error 521 # detection code. 522 _AID_MATCH = re.compile('%s[A-Z0-9_]+' % AID.PREFIX) 523 _AID_ERR_MSG = 'Expecting upper case, a number or underscore' 524 525 # list of handler to required options, used to identify the 526 # parsing section 527 _SECTIONS = [('_handle_aid', ('value', )), 528 ('_handle_path', ('mode', 'user', 'group', 'caps'))] 529 530 def __init__(self, config_files, ranges): 531 """ 532 Args: 533 config_files ([str]): The list of config.fs files to parse. 534 Note the filename is not important. 535 ranges ({str,[()]): Dictionary of partitions and a list of tuples that correspond to their ranges 536 """ 537 538 self._files = [] 539 self._dirs = [] 540 self._aids = [] 541 542 self._seen_paths = {} 543 # (name to file, value to aid) 544 self._seen_aids = ({}, {}) 545 546 self._ranges = ranges 547 548 self._config_files = config_files 549 550 for config_file in self._config_files: 551 self._parse(config_file) 552 553 def _parse(self, file_name): 554 """Parses and verifies config.fs files. Internal use only. 555 556 Args: 557 file_name (str): The config.fs (PythonConfigParser file format) 558 file to parse. 559 560 Raises: 561 Anything raised by ConfigParser.read() 562 """ 563 564 # Separate config parsers for each file found. If you use 565 # read(filenames...) later files can override earlier files which is 566 # not what we want. Track state across files and enforce with 567 # _handle_dup(). Note, strict ConfigParser is set to true in 568 # Python >= 3.2, so in previous versions same file sections can 569 # override previous 570 # sections. 571 572 config = configparser.ConfigParser() 573 config.read(file_name) 574 575 for section in config.sections(): 576 577 found = False 578 579 for test in FSConfigFileParser._SECTIONS: 580 handler = test[0] 581 options = test[1] 582 583 if all([config.has_option(section, item) for item in options]): 584 handler = getattr(self, handler) 585 handler(file_name, section, config) 586 found = True 587 break 588 589 if not found: 590 sys.exit('Invalid section "%s" in file: "%s"' % (section, 591 file_name)) 592 593 # sort entries: 594 # * specified path before prefix match 595 # ** ie foo before f* 596 # * lexicographical less than before other 597 # ** ie boo before foo 598 # Given these paths: 599 # paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*'] 600 # The sort order would be: 601 # paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*'] 602 # Thus the fs_config tools will match on specified paths before 603 # attempting prefix, and match on the longest matching prefix. 604 self._files.sort(key=FSConfigFileParser._file_key) 605 606 # sort on value of (file_name, name, value, strvalue) 607 # This is only cosmetic so AIDS are arranged in ascending order 608 # within the generated file. 609 self._aids.sort(key=lambda item: item.normalized_value) 610 611 def _verify_valid_range(self, aid): 612 """Verified an AID entry is in a valid range""" 613 614 ranges = None 615 616 partitions = list(self._ranges.keys()) 617 partitions.sort(key=len, reverse=True) 618 for partition in partitions: 619 if aid.friendly.startswith(partition): 620 ranges = self._ranges[partition] 621 break 622 623 if ranges is None: 624 sys.exit('AID "%s" must be prefixed with a partition name' % 625 aid.friendly) 626 627 if not Utils.in_any_range(int(aid.value, 0), ranges): 628 emsg = '"value" for aid "%s" not in valid range %s, got: %s' 629 emsg = emsg % (aid.friendly, str(ranges), aid.value) 630 sys.exit(emsg) 631 632 def _handle_aid(self, file_name, section_name, config): 633 """Verifies an AID entry and adds it to the aid list. 634 635 Calls sys.exit() with a descriptive message of the failure. 636 637 Args: 638 file_name (str): The filename of the config file being parsed. 639 section_name (str): The section name currently being parsed. 640 config (ConfigParser): The ConfigParser section being parsed that 641 the option values will come from. 642 """ 643 644 def error_message(msg): 645 """Creates an error message with current parsing state.""" 646 return '{} for: "{}" file: "{}"'.format(msg, section_name, 647 file_name) 648 649 FSConfigFileParser._handle_dup_and_add('AID', file_name, section_name, 650 self._seen_aids[0]) 651 652 match = FSConfigFileParser._AID_MATCH.match(section_name) 653 invalid = match.end() if match else len(AID.PREFIX) 654 if invalid != len(section_name): 655 tmp_errmsg = ('Invalid characters in AID section at "%d" for: "%s"' 656 % (invalid, FSConfigFileParser._AID_ERR_MSG)) 657 sys.exit(error_message(tmp_errmsg)) 658 659 value = config.get(section_name, 'value') 660 661 if not value: 662 sys.exit(error_message('Found specified but unset "value"')) 663 664 try: 665 aid = AID(section_name, value, file_name, '/bin/sh') 666 except ValueError as exception: 667 sys.exit(error_message(exception)) 668 669 self._verify_valid_range(aid) 670 671 # use the normalized int value in the dict and detect 672 # duplicate definitions of the same value 673 FSConfigFileParser._handle_dup_and_add( 674 'AID', file_name, aid.normalized_value, self._seen_aids[1]) 675 676 # Append aid tuple of (AID_*, base10(value), _path(value)) 677 # We keep the _path version of value so we can print that out in the 678 # generated header so investigating parties can identify parts. 679 # We store the base10 value for sorting, so everything is ascending 680 # later. 681 self._aids.append(aid) 682 683 def _handle_path(self, file_name, section_name, config): 684 """Add a file capability entry to the internal list. 685 686 Handles a file capability entry, verifies it, and adds it to 687 to the internal dirs or files list based on path. If it ends 688 with a / its a dir. Internal use only. 689 690 Calls sys.exit() on any validation error with message set. 691 692 Args: 693 file_name (str): The current name of the file being parsed. 694 section_name (str): The name of the section to parse. 695 config (str): The config parser. 696 """ 697 698 FSConfigFileParser._handle_dup_and_add('path', file_name, section_name, 699 self._seen_paths) 700 701 mode = config.get(section_name, 'mode') 702 user = config.get(section_name, 'user') 703 group = config.get(section_name, 'group') 704 caps = config.get(section_name, 'caps') 705 706 errmsg = ('Found specified but unset option: \"%s" in file: \"' + 707 file_name + '\"') 708 709 if not mode: 710 sys.exit(errmsg % 'mode') 711 712 if not user: 713 sys.exit(errmsg % 'user') 714 715 if not group: 716 sys.exit(errmsg % 'group') 717 718 if not caps: 719 sys.exit(errmsg % 'caps') 720 721 caps = caps.split() 722 723 tmp = [] 724 for cap in caps: 725 try: 726 # test if string is int, if it is, use as is. 727 int(cap, 0) 728 tmp.append(cap) 729 except ValueError: 730 tmp.append('CAP_' + cap.upper()) 731 732 caps = tmp 733 734 if len(mode) == 3: 735 mode = '0' + mode 736 737 try: 738 int(mode, 8) 739 except ValueError: 740 sys.exit('Mode must be octal characters, got: "%s"' % mode) 741 742 if len(mode) != 4: 743 sys.exit('Mode must be 3 or 4 characters, got: "%s"' % mode) 744 745 caps_str = ','.join(caps) 746 747 entry = FSConfig(mode, user, group, caps_str, section_name, file_name) 748 if section_name[-1] == '/': 749 self._dirs.append(entry) 750 else: 751 self._files.append(entry) 752 753 @property 754 def files(self): 755 """Get the list of FSConfig file entries. 756 757 Returns: 758 a list of FSConfig() objects for file paths. 759 """ 760 return self._files 761 762 @property 763 def dirs(self): 764 """Get the list of FSConfig dir entries. 765 766 Returns: 767 a list of FSConfig() objects for directory paths. 768 """ 769 return self._dirs 770 771 @property 772 def aids(self): 773 """Get the list of AID entries. 774 775 Returns: 776 a list of AID() objects. 777 """ 778 return self._aids 779 780 @staticmethod 781 def _file_key(fs_config): 782 """Used as the key paramter to sort. 783 784 This is used as a the function to the key parameter of a sort. 785 it wraps the string supplied in a class that implements the 786 appropriate __lt__ operator for the sort on path strings. See 787 StringWrapper class for more details. 788 789 Args: 790 fs_config (FSConfig): A FSConfig entry. 791 792 Returns: 793 A StringWrapper object 794 """ 795 796 # Wrapper class for custom prefix matching strings 797 class StringWrapper(object): 798 """Wrapper class used for sorting prefix strings. 799 800 The algorithm is as follows: 801 - specified path before prefix match 802 - ie foo before f* 803 - lexicographical less than before other 804 - ie boo before foo 805 806 Given these paths: 807 paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*'] 808 The sort order would be: 809 paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*'] 810 Thus the fs_config tools will match on specified paths before 811 attempting prefix, and match on the longest matching prefix. 812 """ 813 814 def __init__(self, path): 815 """ 816 Args: 817 path (str): the path string to wrap. 818 """ 819 self.is_prefix = path[-1] == '*' 820 if self.is_prefix: 821 self.path = path[:-1] 822 else: 823 self.path = path 824 825 def __lt__(self, other): 826 827 # if were both suffixed the smallest string 828 # is 'bigger' 829 if self.is_prefix and other.is_prefix: 830 result = len(self.path) > len(other.path) 831 # If I am an the suffix match, im bigger 832 elif self.is_prefix: 833 result = False 834 # If other is the suffix match, he's bigger 835 elif other.is_prefix: 836 result = True 837 # Alphabetical 838 else: 839 result = self.path < other.path 840 return result 841 842 return StringWrapper(fs_config.path) 843 844 @staticmethod 845 def _handle_dup_and_add(name, file_name, section_name, seen): 846 """Tracks and detects duplicates. Internal use only. 847 848 Calls sys.exit() on a duplicate. 849 850 Args: 851 name (str): The name to use in the error reporting. The pretty 852 name for the section. 853 file_name (str): The file currently being parsed. 854 section_name (str): The name of the section. This would be path 855 or identifier depending on what's being parsed. 856 seen (dict): The dictionary of seen things to check against. 857 """ 858 if section_name in seen: 859 dups = '"' + seen[section_name] + '" and ' 860 dups += file_name 861 sys.exit('Duplicate %s "%s" found in files: %s' % 862 (name, section_name, dups)) 863 864 seen[section_name] = file_name 865 866 867class BaseGenerator(object): 868 """Interface for Generators. 869 870 Base class for generators, generators should implement 871 these method stubs. 872 """ 873 874 def add_opts(self, opt_group): 875 """Used to add per-generator options to the command line. 876 877 Args: 878 opt_group (argument group object): The argument group to append to. 879 See the ArgParse docs for more details. 880 """ 881 882 raise NotImplementedError("Not Implemented") 883 884 def __call__(self, args): 885 """This is called to do whatever magic the generator does. 886 887 Args: 888 args (dict): The arguments from ArgParse as a dictionary. 889 ie if you specified an argument of foo in add_opts, access 890 it via args['foo'] 891 """ 892 893 raise NotImplementedError("Not Implemented") 894 895 896@generator('fsconfig') 897class FSConfigGen(BaseGenerator): 898 """Generates the android_filesystem_config.h file. 899 900 Output is used in generating fs_config_files and fs_config_dirs. 901 """ 902 903 def __init__(self, *args, **kwargs): 904 BaseGenerator.__init__(args, kwargs) 905 906 self._oem_parser = None 907 self._base_parser = None 908 self._friendly_to_aid = None 909 self._id_to_aid = None 910 self._capability_parser = None 911 912 self._partition = None 913 self._all_partitions = None 914 self._out_file = None 915 self._generate_files = False 916 self._generate_dirs = False 917 918 def add_opts(self, opt_group): 919 920 opt_group.add_argument( 921 'fsconfig', nargs='+', help='The list of fsconfig files to parse') 922 923 opt_group.add_argument( 924 '--aid-header', 925 required=True, 926 help='An android_filesystem_config.h file' 927 ' to parse AIDs and OEM Ranges from') 928 929 opt_group.add_argument( 930 '--capability-header', 931 required=True, 932 help='A capability.h file to parse capability defines from') 933 934 opt_group.add_argument( 935 '--partition', 936 required=True, 937 help='Partition to generate contents for') 938 939 opt_group.add_argument( 940 '--all-partitions', 941 help='Comma separated list of all possible partitions, used to' 942 ' ignore these partitions when generating the output for the system partition' 943 ) 944 945 opt_group.add_argument( 946 '--files', action='store_true', help='Output fs_config_files') 947 948 opt_group.add_argument( 949 '--dirs', action='store_true', help='Output fs_config_dirs') 950 951 opt_group.add_argument('--out_file', required=True, help='Output file') 952 953 def __call__(self, args): 954 955 self._capability_parser = CapabilityHeaderParser( 956 args['capability_header']) 957 self._base_parser = AIDHeaderParser(args['aid_header']) 958 self._oem_parser = FSConfigFileParser(args['fsconfig'], 959 self._base_parser.ranges) 960 961 self._partition = args['partition'] 962 self._all_partitions = args['all_partitions'] 963 964 self._out_file = args['out_file'] 965 966 self._generate_files = args['files'] 967 self._generate_dirs = args['dirs'] 968 969 if self._generate_files and self._generate_dirs: 970 sys.exit('Only one of --files or --dirs can be provided') 971 972 if not self._generate_files and not self._generate_dirs: 973 sys.exit('One of --files or --dirs must be provided') 974 975 base_aids = self._base_parser.aids 976 oem_aids = self._oem_parser.aids 977 978 # Detect name collisions on AIDs. Since friendly works as the 979 # identifier for collision testing and we need friendly later on for 980 # name resolution, just calculate and use friendly. 981 # {aid.friendly: aid for aid in base_aids} 982 base_friendly = {aid.friendly: aid for aid in base_aids} 983 oem_friendly = {aid.friendly: aid for aid in oem_aids} 984 985 base_set = set(base_friendly.keys()) 986 oem_set = set(oem_friendly.keys()) 987 988 common = base_set & oem_set 989 990 if common: 991 emsg = 'Following AID Collisions detected for: \n' 992 for friendly in common: 993 base = base_friendly[friendly] 994 oem = oem_friendly[friendly] 995 emsg += ( 996 'Identifier: "%s" Friendly Name: "%s" ' 997 'found in file "%s" and "%s"' % 998 (base.identifier, base.friendly, base.found, oem.found)) 999 sys.exit(emsg) 1000 1001 self._friendly_to_aid = oem_friendly 1002 self._friendly_to_aid.update(base_friendly) 1003 1004 self._id_to_aid = {aid.identifier: aid for aid in base_aids} 1005 self._id_to_aid.update({aid.identifier: aid for aid in oem_aids}) 1006 1007 self._generate() 1008 1009 def _to_fs_entry(self, fs_config, out_file): 1010 """Converts an FSConfig entry to an fs entry. 1011 1012 Writes the fs_config contents to the output file. 1013 1014 Calls sys.exit() on error. 1015 1016 Args: 1017 fs_config (FSConfig): The entry to convert to write to file. 1018 file (File): The file to write to. 1019 """ 1020 1021 # Get some short names 1022 mode = fs_config.mode 1023 user = fs_config.user 1024 group = fs_config.group 1025 caps = fs_config.caps 1026 path = fs_config.path 1027 1028 emsg = 'Cannot convert "%s" to identifier!' 1029 1030 # convert mode from octal string to integer 1031 mode = int(mode, 8) 1032 1033 # remap names to values 1034 if AID.is_friendly(user): 1035 if user not in self._friendly_to_aid: 1036 sys.exit(emsg % user) 1037 user = self._friendly_to_aid[user].value 1038 else: 1039 if user not in self._id_to_aid: 1040 sys.exit(emsg % user) 1041 user = self._id_to_aid[user].value 1042 1043 if AID.is_friendly(group): 1044 if group not in self._friendly_to_aid: 1045 sys.exit(emsg % group) 1046 group = self._friendly_to_aid[group].value 1047 else: 1048 if group not in self._id_to_aid: 1049 sys.exit(emsg % group) 1050 group = self._id_to_aid[group].value 1051 1052 caps_dict = self._capability_parser.caps 1053 1054 caps_value = 0 1055 1056 try: 1057 # test if caps is an int 1058 caps_value = int(caps, 0) 1059 except ValueError: 1060 caps_split = caps.split(',') 1061 for cap in caps_split: 1062 if cap not in caps_dict: 1063 sys.exit('Unknown cap "%s" found!' % cap) 1064 caps_value += 1 << caps_dict[cap] 1065 1066 path_length_with_null = len(path) + 1 1067 path_length_aligned_64 = (path_length_with_null + 7) & ~7 1068 # 16 bytes of header plus the path length with alignment 1069 length = 16 + path_length_aligned_64 1070 1071 length_binary = bytearray(ctypes.c_uint16(length)) 1072 mode_binary = bytearray(ctypes.c_uint16(mode)) 1073 user_binary = bytearray(ctypes.c_uint16(int(user, 0))) 1074 group_binary = bytearray(ctypes.c_uint16(int(group, 0))) 1075 caps_binary = bytearray(ctypes.c_uint64(caps_value)) 1076 path_binary = ctypes.create_string_buffer(path.encode(), 1077 path_length_aligned_64).raw 1078 1079 out_file.write(length_binary) 1080 out_file.write(mode_binary) 1081 out_file.write(user_binary) 1082 out_file.write(group_binary) 1083 out_file.write(caps_binary) 1084 out_file.write(path_binary) 1085 1086 def _emit_entry(self, fs_config): 1087 """Returns a boolean whether or not to emit the input fs_config""" 1088 1089 path = fs_config.path 1090 1091 if self._partition == 'system': 1092 if not self._all_partitions: 1093 return True 1094 for skip_partition in self._all_partitions.split(','): 1095 if path.startswith(skip_partition) or path.startswith( 1096 'system/' + skip_partition): 1097 return False 1098 return True 1099 else: 1100 if path.startswith( 1101 self._partition) or path.startswith('system/' + 1102 self._partition): 1103 return True 1104 return False 1105 1106 def _generate(self): 1107 """Generates an OEM android_filesystem_config.h header file to stdout. 1108 1109 Args: 1110 files ([FSConfig]): A list of FSConfig objects for file entries. 1111 dirs ([FSConfig]): A list of FSConfig objects for directory 1112 entries. 1113 aids ([AIDS]): A list of AID objects for Android Id entries. 1114 """ 1115 dirs = self._oem_parser.dirs 1116 files = self._oem_parser.files 1117 1118 if self._generate_files: 1119 with open(self._out_file, 'wb') as open_file: 1120 for fs_config in files: 1121 if self._emit_entry(fs_config): 1122 self._to_fs_entry(fs_config, open_file) 1123 1124 if self._generate_dirs: 1125 with open(self._out_file, 'wb') as open_file: 1126 for dir_entry in dirs: 1127 if self._emit_entry(dir_entry): 1128 self._to_fs_entry(dir_entry, open_file) 1129 1130 1131@generator('aidarray') 1132class AIDArrayGen(BaseGenerator): 1133 """Generates the android_id static array.""" 1134 1135 _GENERATED = ('/*\n' 1136 ' * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY!\n' 1137 ' */') 1138 1139 _INCLUDE = '#include <private/android_filesystem_config.h>' 1140 1141 # Note that the android_id name field is of type 'const char[]' instead of 1142 # 'const char*'. While this seems less straightforward as we need to 1143 # calculate the max length of all names, this allows the entire android_ids 1144 # table to be placed in .rodata section instead of .data.rel.ro section, 1145 # resulting in less memory pressure. 1146 _STRUCT_FS_CONFIG = textwrap.dedent(""" 1147 struct android_id_info { 1148 const char name[%d]; 1149 unsigned aid; 1150 };""") 1151 1152 _OPEN_ID_ARRAY = 'static const struct android_id_info android_ids[] = {' 1153 1154 _ID_ENTRY = ' { "%s", %s },' 1155 1156 _CLOSE_FILE_STRUCT = '};' 1157 1158 _COUNT = ('#define android_id_count \\\n' 1159 ' (sizeof(android_ids) / sizeof(android_ids[0]))') 1160 1161 def add_opts(self, opt_group): 1162 1163 opt_group.add_argument( 1164 'hdrfile', help='The android_filesystem_config.h' 1165 'file to parse') 1166 1167 def __call__(self, args): 1168 1169 hdr = AIDHeaderParser(args['hdrfile']) 1170 max_name_length = max(len(aid.friendly) + 1 for aid in hdr.aids) 1171 1172 print(AIDArrayGen._GENERATED) 1173 print() 1174 print(AIDArrayGen._INCLUDE) 1175 print() 1176 print(AIDArrayGen._STRUCT_FS_CONFIG % max_name_length) 1177 print() 1178 print(AIDArrayGen._OPEN_ID_ARRAY) 1179 1180 for aid in hdr.aids: 1181 print(AIDArrayGen._ID_ENTRY % (aid.friendly, aid.identifier)) 1182 1183 print(AIDArrayGen._CLOSE_FILE_STRUCT) 1184 print() 1185 print(AIDArrayGen._COUNT) 1186 print() 1187 1188 1189@generator('oemaid') 1190class OEMAidGen(BaseGenerator): 1191 """Generates the OEM AID_<name> value header file.""" 1192 1193 _GENERATED = ('/*\n' 1194 ' * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY!\n' 1195 ' */') 1196 1197 _GENERIC_DEFINE = "#define %s\t%s" 1198 1199 _FILE_COMMENT = '// Defined in file: \"%s\"' 1200 1201 # Intentional trailing newline for readability. 1202 _FILE_IFNDEF_DEFINE = ('#ifndef GENERATED_OEM_AIDS_H_\n' 1203 '#define GENERATED_OEM_AIDS_H_\n') 1204 1205 _FILE_ENDIF = '#endif' 1206 1207 def __init__(self): 1208 1209 self._old_file = None 1210 1211 def add_opts(self, opt_group): 1212 1213 opt_group.add_argument( 1214 'fsconfig', nargs='+', help='The list of fsconfig files to parse.') 1215 1216 opt_group.add_argument( 1217 '--aid-header', 1218 required=True, 1219 help='An android_filesystem_config.h file' 1220 'to parse AIDs and OEM Ranges from') 1221 1222 def __call__(self, args): 1223 1224 hdr_parser = AIDHeaderParser(args['aid_header']) 1225 1226 parser = FSConfigFileParser(args['fsconfig'], hdr_parser.ranges) 1227 1228 print(OEMAidGen._GENERATED) 1229 1230 print(OEMAidGen._FILE_IFNDEF_DEFINE) 1231 1232 for aid in parser.aids: 1233 self._print_aid(aid) 1234 print() 1235 1236 print(OEMAidGen._FILE_ENDIF) 1237 1238 def _print_aid(self, aid): 1239 """Prints a valid #define AID identifier to stdout. 1240 1241 Args: 1242 aid to print 1243 """ 1244 1245 # print the source file location of the AID 1246 found_file = aid.found 1247 if found_file != self._old_file: 1248 print(OEMAidGen._FILE_COMMENT % found_file) 1249 self._old_file = found_file 1250 1251 print(OEMAidGen._GENERIC_DEFINE % (aid.identifier, aid.value)) 1252 1253 1254@generator('passwd') 1255class PasswdGen(BaseGenerator): 1256 """Generates the /etc/passwd file per man (5) passwd.""" 1257 1258 def __init__(self): 1259 1260 self._old_file = None 1261 1262 def add_opts(self, opt_group): 1263 1264 opt_group.add_argument( 1265 'fsconfig', nargs='+', help='The list of fsconfig files to parse.') 1266 1267 opt_group.add_argument( 1268 '--aid-header', 1269 required=True, 1270 help='An android_filesystem_config.h file' 1271 'to parse AIDs and OEM Ranges from') 1272 1273 opt_group.add_argument( 1274 '--partition', 1275 required=True, 1276 help= 1277 'Filter the input file and only output entries for the given partition.' 1278 ) 1279 1280 def __call__(self, args): 1281 1282 hdr_parser = AIDHeaderParser(args['aid_header']) 1283 1284 parser = FSConfigFileParser(args['fsconfig'], hdr_parser.ranges) 1285 1286 filter_partition = args['partition'] 1287 1288 aids = parser.aids 1289 1290 # nothing to do if no aids defined 1291 if not aids: 1292 return 1293 1294 aids_by_partition = {} 1295 partitions = list(hdr_parser.ranges.keys()) 1296 partitions.sort(key=len, reverse=True) 1297 1298 for aid in aids: 1299 for partition in partitions: 1300 if aid.friendly.startswith(partition): 1301 if partition in aids_by_partition: 1302 aids_by_partition[partition].append(aid) 1303 else: 1304 aids_by_partition[partition] = [aid] 1305 break 1306 1307 if filter_partition in aids_by_partition: 1308 for aid in aids_by_partition[filter_partition]: 1309 self._print_formatted_line(aid) 1310 1311 def _print_formatted_line(self, aid): 1312 """Prints the aid to stdout in the passwd format. Internal use only. 1313 1314 Colon delimited: 1315 login name, friendly name 1316 encrypted password (optional) 1317 uid (int) 1318 gid (int) 1319 User name or comment field 1320 home directory 1321 interpreter (optional) 1322 1323 Args: 1324 aid (AID): The aid to print. 1325 """ 1326 if self._old_file != aid.found: 1327 self._old_file = aid.found 1328 1329 try: 1330 logon, uid = Utils.get_login_and_uid_cleansed(aid) 1331 except ValueError as exception: 1332 sys.exit(exception) 1333 1334 print("%s::%s:%s::/:%s" % (logon, uid, uid, aid.login_shell)) 1335 1336 1337@generator('group') 1338class GroupGen(PasswdGen): 1339 """Generates the /etc/group file per man (5) group.""" 1340 1341 # Overrides parent 1342 def _print_formatted_line(self, aid): 1343 """Prints the aid to stdout in the group format. Internal use only. 1344 1345 Formatted (per man 5 group) like: 1346 group_name:password:GID:user_list 1347 1348 Args: 1349 aid (AID): The aid to print. 1350 """ 1351 if self._old_file != aid.found: 1352 self._old_file = aid.found 1353 1354 try: 1355 logon, uid = Utils.get_login_and_uid_cleansed(aid) 1356 except ValueError as exception: 1357 sys.exit(exception) 1358 1359 print("%s::%s:" % (logon, uid)) 1360 1361 1362@generator('print') 1363class PrintGen(BaseGenerator): 1364 """Prints just the constants and values, separated by spaces, in an easy to 1365 parse format for use by other scripts. 1366 1367 Each line is just the identifier and the value, separated by a space. 1368 """ 1369 1370 def add_opts(self, opt_group): 1371 opt_group.add_argument( 1372 'aid-header', help='An android_filesystem_config.h file.') 1373 1374 def __call__(self, args): 1375 1376 hdr_parser = AIDHeaderParser(args['aid-header']) 1377 aids = hdr_parser.aids 1378 1379 aids.sort(key=lambda item: int(item.normalized_value)) 1380 1381 for aid in aids: 1382 print('%s %s' % (aid.identifier, aid.normalized_value)) 1383 1384 1385def main(): 1386 """Main entry point for execution.""" 1387 1388 opt_parser = argparse.ArgumentParser( 1389 description='A tool for parsing fsconfig config files and producing' + 1390 'digestable outputs.') 1391 subparser = opt_parser.add_subparsers(help='generators') 1392 1393 gens = generator.get() 1394 1395 # for each gen, instantiate and add them as an option 1396 for name, gen in gens.items(): 1397 1398 generator_option_parser = subparser.add_parser(name, help=gen.__doc__) 1399 generator_option_parser.set_defaults(which=name) 1400 1401 opt_group = generator_option_parser.add_argument_group(name + 1402 ' options') 1403 gen.add_opts(opt_group) 1404 1405 args = opt_parser.parse_args() 1406 1407 args_as_dict = vars(args) 1408 which = args_as_dict['which'] 1409 del args_as_dict['which'] 1410 1411 gens[which](args_as_dict) 1412 1413 1414if __name__ == '__main__': 1415 main() 1416