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