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