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