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