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