5dcbe701cf
If the "--all-partitions" argument is empty, the system fs_config should
be generated with all partitions emitted.
Fixes: 8453f02c
(fs_config: Fix cases without vendor/oem partition)
Change-Id: I058cfd1c8e665b40757998e773c35e26465bf58a
Signed-off-by: dianlujitao <dianlujitao@lineageos.org>
1392 lines
45 KiB
Python
Executable file
1392 lines
45 KiB
Python
Executable file
#!/usr/bin/env python
|
|
"""Generates config files for Android file system properties.
|
|
|
|
This script is used for generating configuration files for configuring
|
|
Android filesystem properties. Internally, its composed of a plug-able
|
|
interface to support the understanding of new input and output parameters.
|
|
|
|
Run the help for a list of supported plugins and their capabilities.
|
|
|
|
Further documentation can be found in the README.
|
|
"""
|
|
|
|
import argparse
|
|
import ConfigParser
|
|
import ctypes
|
|
import re
|
|
import sys
|
|
import textwrap
|
|
|
|
# Keep the tool in one file to make it easy to run.
|
|
# pylint: disable=too-many-lines
|
|
|
|
|
|
# Lowercase generator used to be inline with @staticmethod.
|
|
class generator(object): # pylint: disable=invalid-name
|
|
"""A decorator class to add commandlet plugins.
|
|
|
|
Used as a decorator to classes to add them to
|
|
the internal plugin interface. Plugins added
|
|
with @generator() are automatically added to
|
|
the command line.
|
|
|
|
For instance, to add a new generator
|
|
called foo and have it added just do this:
|
|
|
|
@generator("foo")
|
|
class FooGen(object):
|
|
...
|
|
"""
|
|
_generators = {}
|
|
|
|
def __init__(self, gen):
|
|
"""
|
|
Args:
|
|
gen (str): The name of the generator to add.
|
|
|
|
Raises:
|
|
ValueError: If there is a similarly named generator already added.
|
|
|
|
"""
|
|
self._gen = gen
|
|
|
|
if gen in generator._generators:
|
|
raise ValueError('Duplicate generator name: ' + gen)
|
|
|
|
generator._generators[gen] = None
|
|
|
|
def __call__(self, cls):
|
|
|
|
generator._generators[self._gen] = cls()
|
|
return cls
|
|
|
|
@staticmethod
|
|
def get():
|
|
"""Gets the list of generators.
|
|
|
|
Returns:
|
|
The list of registered generators.
|
|
"""
|
|
return generator._generators
|
|
|
|
|
|
class Utils(object):
|
|
"""Various assorted static utilities."""
|
|
|
|
@staticmethod
|
|
def in_any_range(value, ranges):
|
|
"""Tests if a value is in a list of given closed range tuples.
|
|
|
|
A range tuple is a closed range. That means it's inclusive of its
|
|
start and ending values.
|
|
|
|
Args:
|
|
value (int): The value to test.
|
|
range [(int, int)]: The closed range list to test value within.
|
|
|
|
Returns:
|
|
True if value is within the closed range, false otherwise.
|
|
"""
|
|
|
|
return any(lower <= value <= upper for (lower, upper) in ranges)
|
|
|
|
@staticmethod
|
|
def get_login_and_uid_cleansed(aid):
|
|
"""Returns a passwd/group file safe logon and uid.
|
|
|
|
This checks that the logon and uid of the AID do not
|
|
contain the delimiter ":" for a passwd/group file.
|
|
|
|
Args:
|
|
aid (AID): The aid to check
|
|
|
|
Returns:
|
|
logon, uid of the AID after checking its safe.
|
|
|
|
Raises:
|
|
ValueError: If there is a delimiter charcter found.
|
|
"""
|
|
logon = aid.friendly
|
|
uid = aid.normalized_value
|
|
if ':' in uid:
|
|
raise ValueError(
|
|
'Cannot specify delimiter character ":" in uid: "%s"' % uid)
|
|
if ':' in logon:
|
|
raise ValueError(
|
|
'Cannot specify delimiter character ":" in logon: "%s"' %
|
|
logon)
|
|
return logon, uid
|
|
|
|
|
|
class AID(object):
|
|
"""This class represents an Android ID or an AID.
|
|
|
|
Attributes:
|
|
identifier (str): The identifier name for a #define.
|
|
value (str) The User Id (uid) of the associate define.
|
|
found (str) The file it was found in, can be None.
|
|
normalized_value (str): Same as value, but base 10.
|
|
friendly (str): The friendly name of aid.
|
|
"""
|
|
|
|
PREFIX = 'AID_'
|
|
|
|
# Some of the AIDS like AID_MEDIA_EX had names like mediaex
|
|
# list a map of things to fixup until we can correct these
|
|
# at a later date.
|
|
_FIXUPS = {
|
|
'media_drm': 'mediadrm',
|
|
'media_ex': 'mediaex',
|
|
'media_codec': 'mediacodec'
|
|
}
|
|
|
|
def __init__(self, identifier, value, found, login_shell):
|
|
"""
|
|
Args:
|
|
identifier: The identifier name for a #define <identifier>.
|
|
value: The value of the AID, aka the uid.
|
|
found (str): The file found in, not required to be specified.
|
|
login_shell (str): The shell field per man (5) passwd file.
|
|
Raises:
|
|
ValueError: if the friendly name is longer than 31 characters as
|
|
that is bionic's internal buffer size for name.
|
|
ValueError: if value is not a valid string number as processed by
|
|
int(x, 0)
|
|
"""
|
|
self.identifier = identifier
|
|
self.value = value
|
|
self.found = found
|
|
self.login_shell = login_shell
|
|
|
|
try:
|
|
self.normalized_value = str(int(value, 0))
|
|
except ValueError:
|
|
raise ValueError(
|
|
'Invalid "value", not aid number, got: \"%s\"' % value)
|
|
|
|
# Where we calculate the friendly name
|
|
friendly = identifier[len(AID.PREFIX):].lower()
|
|
self.friendly = AID._fixup_friendly(friendly)
|
|
|
|
if len(self.friendly) > 31:
|
|
raise ValueError(
|
|
'AID names must be under 32 characters "%s"' % self.friendly)
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.identifier == other.identifier \
|
|
and self.value == other.value and self.found == other.found \
|
|
and self.normalized_value == other.normalized_value \
|
|
and self.login_shell == other.login_shell
|
|
|
|
@staticmethod
|
|
def is_friendly(name):
|
|
"""Determines if an AID is a freindly name or C define.
|
|
|
|
For example if name is AID_SYSTEM it returns false, if name
|
|
was system, it would return true.
|
|
|
|
Returns:
|
|
True if name is a friendly name False otherwise.
|
|
"""
|
|
|
|
return not name.startswith(AID.PREFIX)
|
|
|
|
@staticmethod
|
|
def _fixup_friendly(friendly):
|
|
"""Fixup friendly names that historically don't follow the convention.
|
|
|
|
Args:
|
|
friendly (str): The friendly name.
|
|
|
|
Returns:
|
|
The fixedup friendly name as a str.
|
|
"""
|
|
|
|
if friendly in AID._FIXUPS:
|
|
return AID._FIXUPS[friendly]
|
|
|
|
return friendly
|
|
|
|
|
|
class FSConfig(object):
|
|
"""Represents a filesystem config array entry.
|
|
|
|
Represents a file system configuration entry for specifying
|
|
file system capabilities.
|
|
|
|
Attributes:
|
|
mode (str): The mode of the file or directory.
|
|
user (str): The uid or #define identifier (AID_SYSTEM)
|
|
group (str): The gid or #define identifier (AID_SYSTEM)
|
|
caps (str): The capability set.
|
|
path (str): The path of the file or directory.
|
|
filename (str): The file it was found in.
|
|
"""
|
|
|
|
def __init__(self, mode, user, group, caps, path, filename):
|
|
"""
|
|
Args:
|
|
mode (str): The mode of the file or directory.
|
|
user (str): The uid or #define identifier (AID_SYSTEM)
|
|
group (str): The gid or #define identifier (AID_SYSTEM)
|
|
caps (str): The capability set as a list.
|
|
path (str): The path of the file or directory.
|
|
filename (str): The file it was found in.
|
|
"""
|
|
self.mode = mode
|
|
self.user = user
|
|
self.group = group
|
|
self.caps = caps
|
|
self.path = path
|
|
self.filename = filename
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.mode == other.mode and self.user == other.user \
|
|
and self.group == other.group and self.caps == other.caps \
|
|
and self.path == other.path and self.filename == other.filename
|
|
|
|
def __repr__(self):
|
|
return 'FSConfig(%r, %r, %r, %r, %r, %r)' % (self.mode, self.user,
|
|
self.group, self.caps,
|
|
self.path, self.filename)
|
|
|
|
|
|
class CapabilityHeaderParser(object):
|
|
"""Parses capability.h file
|
|
|
|
Parses a C header file and extracts lines starting with #define CAP_<name>.
|
|
"""
|
|
|
|
_CAP_DEFINE = re.compile(r'\s*#define\s+(CAP_\S+)\s+(\S+)')
|
|
_SKIP_CAPS = ['CAP_LAST_CAP', 'CAP_TO_INDEX(x)', 'CAP_TO_MASK(x)']
|
|
|
|
def __init__(self, capability_header):
|
|
"""
|
|
Args:
|
|
capability_header (str): file name for the header file containing AID entries.
|
|
"""
|
|
|
|
self.caps = {}
|
|
with open(capability_header) as open_file:
|
|
self._parse(open_file)
|
|
|
|
def _parse(self, capability_file):
|
|
"""Parses a capability header file. Internal use only.
|
|
|
|
Args:
|
|
capability_file (file): The open capability header file to parse.
|
|
"""
|
|
|
|
for line in capability_file:
|
|
match = CapabilityHeaderParser._CAP_DEFINE.match(line)
|
|
if match:
|
|
cap = match.group(1)
|
|
value = match.group(2)
|
|
|
|
if not cap in self._SKIP_CAPS:
|
|
try:
|
|
self.caps[cap] = int(value, 0)
|
|
except ValueError:
|
|
sys.exit('Could not parse capability define "%s":"%s"'
|
|
% (cap, value))
|
|
|
|
|
|
class AIDHeaderParser(object):
|
|
"""Parses an android_filesystem_config.h file.
|
|
|
|
Parses a C header file and extracts lines starting with #define AID_<name>
|
|
while capturing the OEM defined ranges and ignoring other ranges. It also
|
|
skips some hardcoded AIDs it doesn't need to generate a mapping for.
|
|
It provides some basic sanity checks. The information extracted from this
|
|
file can later be used to sanity check other things (like oem ranges) as
|
|
well as generating a mapping of names to uids. It was primarily designed to
|
|
parse the private/android_filesystem_config.h, but any C header should
|
|
work.
|
|
"""
|
|
|
|
_SKIP_AIDS = [
|
|
re.compile(r'%sUNUSED[0-9].*' % AID.PREFIX),
|
|
re.compile(r'%sAPP' % AID.PREFIX),
|
|
re.compile(r'%sUSER' % AID.PREFIX)
|
|
]
|
|
_AID_DEFINE = re.compile(r'\s*#define\s+%s.*' % AID.PREFIX)
|
|
_RESERVED_RANGE = re.compile(
|
|
r'#define AID_(.+)_RESERVED_\d*_*(START|END)\s+(\d+)')
|
|
|
|
# AID lines cannot end with _START or _END, ie AID_FOO is OK
|
|
# but AID_FOO_START is skiped. Note that AID_FOOSTART is NOT skipped.
|
|
_AID_SKIP_RANGE = ['_START', '_END']
|
|
_COLLISION_OK = ['AID_APP', 'AID_APP_START', 'AID_USER', 'AID_USER_OFFSET']
|
|
|
|
def __init__(self, aid_header):
|
|
"""
|
|
Args:
|
|
aid_header (str): file name for the header
|
|
file containing AID entries.
|
|
"""
|
|
self._aid_header = aid_header
|
|
self._aid_name_to_value = {}
|
|
self._aid_value_to_name = {}
|
|
self._ranges = {}
|
|
|
|
with open(aid_header) as open_file:
|
|
self._parse(open_file)
|
|
|
|
try:
|
|
self._process_and_check()
|
|
except ValueError as exception:
|
|
sys.exit('Error processing parsed data: "%s"' % (str(exception)))
|
|
|
|
def _parse(self, aid_file):
|
|
"""Parses an AID header file. Internal use only.
|
|
|
|
Args:
|
|
aid_file (file): The open AID header file to parse.
|
|
"""
|
|
|
|
for lineno, line in enumerate(aid_file):
|
|
|
|
def error_message(msg):
|
|
"""Creates an error message with the current parsing state."""
|
|
# pylint: disable=cell-var-from-loop
|
|
return 'Error "{}" in file: "{}" on line: {}'.format(
|
|
msg, self._aid_header, str(lineno))
|
|
|
|
range_match = self._RESERVED_RANGE.match(line)
|
|
if range_match:
|
|
partition = range_match.group(1).lower()
|
|
value = int(range_match.group(3), 0)
|
|
|
|
if partition == 'oem':
|
|
partition = 'vendor'
|
|
|
|
if partition in self._ranges:
|
|
if isinstance(self._ranges[partition][-1], int):
|
|
self._ranges[partition][-1] = (
|
|
self._ranges[partition][-1], value)
|
|
else:
|
|
self._ranges[partition].append(value)
|
|
else:
|
|
self._ranges[partition] = [value]
|
|
|
|
if AIDHeaderParser._AID_DEFINE.match(line):
|
|
chunks = line.split()
|
|
identifier = chunks[1]
|
|
value = chunks[2]
|
|
|
|
if any(
|
|
x.match(identifier)
|
|
for x in AIDHeaderParser._SKIP_AIDS):
|
|
continue
|
|
|
|
try:
|
|
if not any(
|
|
identifier.endswith(x)
|
|
for x in AIDHeaderParser._AID_SKIP_RANGE):
|
|
self._handle_aid(identifier, value)
|
|
except ValueError as exception:
|
|
sys.exit(
|
|
error_message('{} for "{}"'.format(
|
|
exception, identifier)))
|
|
|
|
def _handle_aid(self, identifier, value):
|
|
"""Handle an AID C #define.
|
|
|
|
Handles an AID, sanity checking, generating the friendly name and
|
|
adding it to the internal maps. Internal use only.
|
|
|
|
Args:
|
|
identifier (str): The name of the #define identifier. ie AID_FOO.
|
|
value (str): The value associated with the identifier.
|
|
|
|
Raises:
|
|
ValueError: With message set to indicate the error.
|
|
"""
|
|
|
|
aid = AID(identifier, value, self._aid_header, '/system/bin/sh')
|
|
|
|
# duplicate name
|
|
if aid.friendly in self._aid_name_to_value:
|
|
raise ValueError('Duplicate aid "%s"' % identifier)
|
|
|
|
if value in self._aid_value_to_name and aid.identifier not in AIDHeaderParser._COLLISION_OK:
|
|
raise ValueError(
|
|
'Duplicate aid value "%s" for %s' % (value, identifier))
|
|
|
|
self._aid_name_to_value[aid.friendly] = aid
|
|
self._aid_value_to_name[value] = aid.friendly
|
|
|
|
def _process_and_check(self):
|
|
"""Process, check and populate internal data structures.
|
|
|
|
After parsing and generating the internal data structures, this method
|
|
is responsible for sanity checking ALL of the acquired data.
|
|
|
|
Raises:
|
|
ValueError: With the message set to indicate the specific error.
|
|
"""
|
|
|
|
# Check for overlapping ranges
|
|
for ranges in self._ranges.values():
|
|
for i, range1 in enumerate(ranges):
|
|
for range2 in ranges[i + 1:]:
|
|
if AIDHeaderParser._is_overlap(range1, range2):
|
|
raise ValueError(
|
|
"Overlapping OEM Ranges found %s and %s" %
|
|
(str(range1), str(range2)))
|
|
|
|
# No core AIDs should be within any oem range.
|
|
for aid in self._aid_value_to_name:
|
|
for ranges in self._ranges.values():
|
|
if Utils.in_any_range(aid, ranges):
|
|
name = self._aid_value_to_name[aid]
|
|
raise ValueError(
|
|
'AID "%s" value: %u within reserved OEM Range: "%s"' %
|
|
(name, aid, str(ranges)))
|
|
|
|
@property
|
|
def ranges(self):
|
|
"""Retrieves the OEM closed ranges as a list of tuples.
|
|
|
|
Returns:
|
|
A list of closed range tuples: [ (0, 42), (50, 105) ... ]
|
|
"""
|
|
return self._ranges
|
|
|
|
@property
|
|
def aids(self):
|
|
"""Retrieves the list of found AIDs.
|
|
|
|
Returns:
|
|
A list of AID() objects.
|
|
"""
|
|
return self._aid_name_to_value.values()
|
|
|
|
@staticmethod
|
|
def _is_overlap(range_a, range_b):
|
|
"""Calculates the overlap of two range tuples.
|
|
|
|
A range tuple is a closed range. A closed range includes its endpoints.
|
|
Note that python tuples use () notation which collides with the
|
|
mathematical notation for open ranges.
|
|
|
|
Args:
|
|
range_a: The first tuple closed range eg (0, 5).
|
|
range_b: The second tuple closed range eg (3, 7).
|
|
|
|
Returns:
|
|
True if they overlap, False otherwise.
|
|
"""
|
|
|
|
return max(range_a[0], range_b[0]) <= min(range_a[1], range_b[1])
|
|
|
|
|
|
class FSConfigFileParser(object):
|
|
"""Parses a config.fs ini format file.
|
|
|
|
This class is responsible for parsing the config.fs ini format files.
|
|
It collects and checks all the data in these files and makes it available
|
|
for consumption post processed.
|
|
"""
|
|
|
|
# These _AID vars work together to ensure that an AID section name
|
|
# cannot contain invalid characters for a C define or a passwd/group file.
|
|
# Since _AID_PREFIX is within the set of _AID_MATCH the error logic only
|
|
# checks end, if you change this, you may have to update the error
|
|
# detection code.
|
|
_AID_MATCH = re.compile('%s[A-Z0-9_]+' % AID.PREFIX)
|
|
_AID_ERR_MSG = 'Expecting upper case, a number or underscore'
|
|
|
|
# list of handler to required options, used to identify the
|
|
# parsing section
|
|
_SECTIONS = [('_handle_aid', ('value', )),
|
|
('_handle_path', ('mode', 'user', 'group', 'caps'))]
|
|
|
|
def __init__(self, config_files, ranges):
|
|
"""
|
|
Args:
|
|
config_files ([str]): The list of config.fs files to parse.
|
|
Note the filename is not important.
|
|
ranges ({str,[()]): Dictionary of partitions and a list of tuples that correspond to their ranges
|
|
"""
|
|
|
|
self._files = []
|
|
self._dirs = []
|
|
self._aids = []
|
|
|
|
self._seen_paths = {}
|
|
# (name to file, value to aid)
|
|
self._seen_aids = ({}, {})
|
|
|
|
self._ranges = ranges
|
|
|
|
self._config_files = config_files
|
|
|
|
for config_file in self._config_files:
|
|
self._parse(config_file)
|
|
|
|
def _parse(self, file_name):
|
|
"""Parses and verifies config.fs files. Internal use only.
|
|
|
|
Args:
|
|
file_name (str): The config.fs (PythonConfigParser file format)
|
|
file to parse.
|
|
|
|
Raises:
|
|
Anything raised by ConfigParser.read()
|
|
"""
|
|
|
|
# Separate config parsers for each file found. If you use
|
|
# read(filenames...) later files can override earlier files which is
|
|
# not what we want. Track state across files and enforce with
|
|
# _handle_dup(). Note, strict ConfigParser is set to true in
|
|
# Python >= 3.2, so in previous versions same file sections can
|
|
# override previous
|
|
# sections.
|
|
|
|
config = ConfigParser.ConfigParser()
|
|
config.read(file_name)
|
|
|
|
for section in config.sections():
|
|
|
|
found = False
|
|
|
|
for test in FSConfigFileParser._SECTIONS:
|
|
handler = test[0]
|
|
options = test[1]
|
|
|
|
if all([config.has_option(section, item) for item in options]):
|
|
handler = getattr(self, handler)
|
|
handler(file_name, section, config)
|
|
found = True
|
|
break
|
|
|
|
if not found:
|
|
sys.exit('Invalid section "%s" in file: "%s"' % (section,
|
|
file_name))
|
|
|
|
# sort entries:
|
|
# * specified path before prefix match
|
|
# ** ie foo before f*
|
|
# * lexicographical less than before other
|
|
# ** ie boo before foo
|
|
# Given these paths:
|
|
# paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*']
|
|
# The sort order would be:
|
|
# paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*']
|
|
# Thus the fs_config tools will match on specified paths before
|
|
# attempting prefix, and match on the longest matching prefix.
|
|
self._files.sort(key=FSConfigFileParser._file_key)
|
|
|
|
# sort on value of (file_name, name, value, strvalue)
|
|
# This is only cosmetic so AIDS are arranged in ascending order
|
|
# within the generated file.
|
|
self._aids.sort(key=lambda item: item.normalized_value)
|
|
|
|
def _verify_valid_range(self, aid):
|
|
"""Verified an AID entry is in a valid range"""
|
|
|
|
ranges = None
|
|
|
|
partitions = self._ranges.keys()
|
|
partitions.sort(key=len, reverse=True)
|
|
for partition in partitions:
|
|
if aid.friendly.startswith(partition):
|
|
ranges = self._ranges[partition]
|
|
break
|
|
|
|
if ranges is None:
|
|
sys.exit('AID "%s" must be prefixed with a partition name' %
|
|
aid.friendly)
|
|
|
|
if not Utils.in_any_range(int(aid.value, 0), ranges):
|
|
emsg = '"value" for aid "%s" not in valid range %s, got: %s'
|
|
emsg = emsg % (aid.friendly, str(ranges), aid.value)
|
|
sys.exit(emsg)
|
|
|
|
def _handle_aid(self, file_name, section_name, config):
|
|
"""Verifies an AID entry and adds it to the aid list.
|
|
|
|
Calls sys.exit() with a descriptive message of the failure.
|
|
|
|
Args:
|
|
file_name (str): The filename of the config file being parsed.
|
|
section_name (str): The section name currently being parsed.
|
|
config (ConfigParser): The ConfigParser section being parsed that
|
|
the option values will come from.
|
|
"""
|
|
|
|
def error_message(msg):
|
|
"""Creates an error message with current parsing state."""
|
|
return '{} for: "{}" file: "{}"'.format(msg, section_name,
|
|
file_name)
|
|
|
|
FSConfigFileParser._handle_dup_and_add('AID', file_name, section_name,
|
|
self._seen_aids[0])
|
|
|
|
match = FSConfigFileParser._AID_MATCH.match(section_name)
|
|
invalid = match.end() if match else len(AID.PREFIX)
|
|
if invalid != len(section_name):
|
|
tmp_errmsg = ('Invalid characters in AID section at "%d" for: "%s"'
|
|
% (invalid, FSConfigFileParser._AID_ERR_MSG))
|
|
sys.exit(error_message(tmp_errmsg))
|
|
|
|
value = config.get(section_name, 'value')
|
|
|
|
if not value:
|
|
sys.exit(error_message('Found specified but unset "value"'))
|
|
|
|
try:
|
|
aid = AID(section_name, value, file_name, '/bin/sh')
|
|
except ValueError as exception:
|
|
sys.exit(error_message(exception))
|
|
|
|
self._verify_valid_range(aid)
|
|
|
|
# use the normalized int value in the dict and detect
|
|
# duplicate definitions of the same value
|
|
FSConfigFileParser._handle_dup_and_add(
|
|
'AID', file_name, aid.normalized_value, self._seen_aids[1])
|
|
|
|
# Append aid tuple of (AID_*, base10(value), _path(value))
|
|
# We keep the _path version of value so we can print that out in the
|
|
# generated header so investigating parties can identify parts.
|
|
# We store the base10 value for sorting, so everything is ascending
|
|
# later.
|
|
self._aids.append(aid)
|
|
|
|
def _handle_path(self, file_name, section_name, config):
|
|
"""Add a file capability entry to the internal list.
|
|
|
|
Handles a file capability entry, verifies it, and adds it to
|
|
to the internal dirs or files list based on path. If it ends
|
|
with a / its a dir. Internal use only.
|
|
|
|
Calls sys.exit() on any validation error with message set.
|
|
|
|
Args:
|
|
file_name (str): The current name of the file being parsed.
|
|
section_name (str): The name of the section to parse.
|
|
config (str): The config parser.
|
|
"""
|
|
|
|
FSConfigFileParser._handle_dup_and_add('path', file_name, section_name,
|
|
self._seen_paths)
|
|
|
|
mode = config.get(section_name, 'mode')
|
|
user = config.get(section_name, 'user')
|
|
group = config.get(section_name, 'group')
|
|
caps = config.get(section_name, 'caps')
|
|
|
|
errmsg = ('Found specified but unset option: \"%s" in file: \"' +
|
|
file_name + '\"')
|
|
|
|
if not mode:
|
|
sys.exit(errmsg % 'mode')
|
|
|
|
if not user:
|
|
sys.exit(errmsg % 'user')
|
|
|
|
if not group:
|
|
sys.exit(errmsg % 'group')
|
|
|
|
if not caps:
|
|
sys.exit(errmsg % 'caps')
|
|
|
|
caps = caps.split()
|
|
|
|
tmp = []
|
|
for cap in caps:
|
|
try:
|
|
# test if string is int, if it is, use as is.
|
|
int(cap, 0)
|
|
tmp.append(cap)
|
|
except ValueError:
|
|
tmp.append('CAP_' + cap.upper())
|
|
|
|
caps = tmp
|
|
|
|
if len(mode) == 3:
|
|
mode = '0' + mode
|
|
|
|
try:
|
|
int(mode, 8)
|
|
except ValueError:
|
|
sys.exit('Mode must be octal characters, got: "%s"' % mode)
|
|
|
|
if len(mode) != 4:
|
|
sys.exit('Mode must be 3 or 4 characters, got: "%s"' % mode)
|
|
|
|
caps_str = ','.join(caps)
|
|
|
|
entry = FSConfig(mode, user, group, caps_str, section_name, file_name)
|
|
if section_name[-1] == '/':
|
|
self._dirs.append(entry)
|
|
else:
|
|
self._files.append(entry)
|
|
|
|
@property
|
|
def files(self):
|
|
"""Get the list of FSConfig file entries.
|
|
|
|
Returns:
|
|
a list of FSConfig() objects for file paths.
|
|
"""
|
|
return self._files
|
|
|
|
@property
|
|
def dirs(self):
|
|
"""Get the list of FSConfig dir entries.
|
|
|
|
Returns:
|
|
a list of FSConfig() objects for directory paths.
|
|
"""
|
|
return self._dirs
|
|
|
|
@property
|
|
def aids(self):
|
|
"""Get the list of AID entries.
|
|
|
|
Returns:
|
|
a list of AID() objects.
|
|
"""
|
|
return self._aids
|
|
|
|
@staticmethod
|
|
def _file_key(fs_config):
|
|
"""Used as the key paramter to sort.
|
|
|
|
This is used as a the function to the key parameter of a sort.
|
|
it wraps the string supplied in a class that implements the
|
|
appropriate __lt__ operator for the sort on path strings. See
|
|
StringWrapper class for more details.
|
|
|
|
Args:
|
|
fs_config (FSConfig): A FSConfig entry.
|
|
|
|
Returns:
|
|
A StringWrapper object
|
|
"""
|
|
|
|
# Wrapper class for custom prefix matching strings
|
|
class StringWrapper(object):
|
|
"""Wrapper class used for sorting prefix strings.
|
|
|
|
The algorithm is as follows:
|
|
- specified path before prefix match
|
|
- ie foo before f*
|
|
- lexicographical less than before other
|
|
- ie boo before foo
|
|
|
|
Given these paths:
|
|
paths=['ac', 'a', 'acd', 'an', 'a*', 'aa', 'ac*']
|
|
The sort order would be:
|
|
paths=['a', 'aa', 'ac', 'acd', 'an', 'ac*', 'a*']
|
|
Thus the fs_config tools will match on specified paths before
|
|
attempting prefix, and match on the longest matching prefix.
|
|
"""
|
|
|
|
def __init__(self, path):
|
|
"""
|
|
Args:
|
|
path (str): the path string to wrap.
|
|
"""
|
|
self.is_prefix = path[-1] == '*'
|
|
if self.is_prefix:
|
|
self.path = path[:-1]
|
|
else:
|
|
self.path = path
|
|
|
|
def __lt__(self, other):
|
|
|
|
# if were both suffixed the smallest string
|
|
# is 'bigger'
|
|
if self.is_prefix and other.is_prefix:
|
|
result = len(self.path) > len(other.path)
|
|
# If I am an the suffix match, im bigger
|
|
elif self.is_prefix:
|
|
result = False
|
|
# If other is the suffix match, he's bigger
|
|
elif other.is_prefix:
|
|
result = True
|
|
# Alphabetical
|
|
else:
|
|
result = self.path < other.path
|
|
return result
|
|
|
|
return StringWrapper(fs_config.path)
|
|
|
|
@staticmethod
|
|
def _handle_dup_and_add(name, file_name, section_name, seen):
|
|
"""Tracks and detects duplicates. Internal use only.
|
|
|
|
Calls sys.exit() on a duplicate.
|
|
|
|
Args:
|
|
name (str): The name to use in the error reporting. The pretty
|
|
name for the section.
|
|
file_name (str): The file currently being parsed.
|
|
section_name (str): The name of the section. This would be path
|
|
or identifier depending on what's being parsed.
|
|
seen (dict): The dictionary of seen things to check against.
|
|
"""
|
|
if section_name in seen:
|
|
dups = '"' + seen[section_name] + '" and '
|
|
dups += file_name
|
|
sys.exit('Duplicate %s "%s" found in files: %s' %
|
|
(name, section_name, dups))
|
|
|
|
seen[section_name] = file_name
|
|
|
|
|
|
class BaseGenerator(object):
|
|
"""Interface for Generators.
|
|
|
|
Base class for generators, generators should implement
|
|
these method stubs.
|
|
"""
|
|
|
|
def add_opts(self, opt_group):
|
|
"""Used to add per-generator options to the command line.
|
|
|
|
Args:
|
|
opt_group (argument group object): The argument group to append to.
|
|
See the ArgParse docs for more details.
|
|
"""
|
|
|
|
raise NotImplementedError("Not Implemented")
|
|
|
|
def __call__(self, args):
|
|
"""This is called to do whatever magic the generator does.
|
|
|
|
Args:
|
|
args (dict): The arguments from ArgParse as a dictionary.
|
|
ie if you specified an argument of foo in add_opts, access
|
|
it via args['foo']
|
|
"""
|
|
|
|
raise NotImplementedError("Not Implemented")
|
|
|
|
|
|
@generator('fsconfig')
|
|
class FSConfigGen(BaseGenerator):
|
|
"""Generates the android_filesystem_config.h file.
|
|
|
|
Output is used in generating fs_config_files and fs_config_dirs.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
BaseGenerator.__init__(args, kwargs)
|
|
|
|
self._oem_parser = None
|
|
self._base_parser = None
|
|
self._friendly_to_aid = None
|
|
self._id_to_aid = None
|
|
self._capability_parser = None
|
|
|
|
self._partition = None
|
|
self._all_partitions = None
|
|
self._out_file = None
|
|
self._generate_files = False
|
|
self._generate_dirs = False
|
|
|
|
def add_opts(self, opt_group):
|
|
|
|
opt_group.add_argument(
|
|
'fsconfig', nargs='+', help='The list of fsconfig files to parse')
|
|
|
|
opt_group.add_argument(
|
|
'--aid-header',
|
|
required=True,
|
|
help='An android_filesystem_config.h file'
|
|
' to parse AIDs and OEM Ranges from')
|
|
|
|
opt_group.add_argument(
|
|
'--capability-header',
|
|
required=True,
|
|
help='A capability.h file to parse capability defines from')
|
|
|
|
opt_group.add_argument(
|
|
'--partition',
|
|
required=True,
|
|
help='Partition to generate contents for')
|
|
|
|
opt_group.add_argument(
|
|
'--all-partitions',
|
|
help='Comma separated list of all possible partitions, used to'
|
|
' ignore these partitions when generating the output for the system partition'
|
|
)
|
|
|
|
opt_group.add_argument(
|
|
'--files', action='store_true', help='Output fs_config_files')
|
|
|
|
opt_group.add_argument(
|
|
'--dirs', action='store_true', help='Output fs_config_dirs')
|
|
|
|
opt_group.add_argument('--out_file', required=True, help='Output file')
|
|
|
|
def __call__(self, args):
|
|
|
|
self._capability_parser = CapabilityHeaderParser(
|
|
args['capability_header'])
|
|
self._base_parser = AIDHeaderParser(args['aid_header'])
|
|
self._oem_parser = FSConfigFileParser(args['fsconfig'],
|
|
self._base_parser.ranges)
|
|
|
|
self._partition = args['partition']
|
|
self._all_partitions = args['all_partitions']
|
|
|
|
self._out_file = args['out_file']
|
|
|
|
self._generate_files = args['files']
|
|
self._generate_dirs = args['dirs']
|
|
|
|
if self._generate_files and self._generate_dirs:
|
|
sys.exit('Only one of --files or --dirs can be provided')
|
|
|
|
if not self._generate_files and not self._generate_dirs:
|
|
sys.exit('One of --files or --dirs must be provided')
|
|
|
|
base_aids = self._base_parser.aids
|
|
oem_aids = self._oem_parser.aids
|
|
|
|
# Detect name collisions on AIDs. Since friendly works as the
|
|
# identifier for collision testing and we need friendly later on for
|
|
# name resolution, just calculate and use friendly.
|
|
# {aid.friendly: aid for aid in base_aids}
|
|
base_friendly = {aid.friendly: aid for aid in base_aids}
|
|
oem_friendly = {aid.friendly: aid for aid in oem_aids}
|
|
|
|
base_set = set(base_friendly.keys())
|
|
oem_set = set(oem_friendly.keys())
|
|
|
|
common = base_set & oem_set
|
|
|
|
if common:
|
|
emsg = 'Following AID Collisions detected for: \n'
|
|
for friendly in common:
|
|
base = base_friendly[friendly]
|
|
oem = oem_friendly[friendly]
|
|
emsg += (
|
|
'Identifier: "%s" Friendly Name: "%s" '
|
|
'found in file "%s" and "%s"' %
|
|
(base.identifier, base.friendly, base.found, oem.found))
|
|
sys.exit(emsg)
|
|
|
|
self._friendly_to_aid = oem_friendly
|
|
self._friendly_to_aid.update(base_friendly)
|
|
|
|
self._id_to_aid = {aid.identifier: aid for aid in base_aids}
|
|
self._id_to_aid.update({aid.identifier: aid for aid in oem_aids})
|
|
|
|
self._generate()
|
|
|
|
def _to_fs_entry(self, fs_config, out_file):
|
|
"""Converts an FSConfig entry to an fs entry.
|
|
|
|
Writes the fs_config contents to the output file.
|
|
|
|
Calls sys.exit() on error.
|
|
|
|
Args:
|
|
fs_config (FSConfig): The entry to convert to write to file.
|
|
file (File): The file to write to.
|
|
"""
|
|
|
|
# Get some short names
|
|
mode = fs_config.mode
|
|
user = fs_config.user
|
|
group = fs_config.group
|
|
caps = fs_config.caps
|
|
path = fs_config.path
|
|
|
|
emsg = 'Cannot convert "%s" to identifier!'
|
|
|
|
# convert mode from octal string to integer
|
|
mode = int(mode, 8)
|
|
|
|
# remap names to values
|
|
if AID.is_friendly(user):
|
|
if user not in self._friendly_to_aid:
|
|
sys.exit(emsg % user)
|
|
user = self._friendly_to_aid[user].value
|
|
else:
|
|
if user not in self._id_to_aid:
|
|
sys.exit(emsg % user)
|
|
user = self._id_to_aid[user].value
|
|
|
|
if AID.is_friendly(group):
|
|
if group not in self._friendly_to_aid:
|
|
sys.exit(emsg % group)
|
|
group = self._friendly_to_aid[group].value
|
|
else:
|
|
if group not in self._id_to_aid:
|
|
sys.exit(emsg % group)
|
|
group = self._id_to_aid[group].value
|
|
|
|
caps_dict = self._capability_parser.caps
|
|
|
|
caps_value = 0
|
|
|
|
try:
|
|
# test if caps is an int
|
|
caps_value = int(caps, 0)
|
|
except ValueError:
|
|
caps_split = caps.split(',')
|
|
for cap in caps_split:
|
|
if cap not in caps_dict:
|
|
sys.exit('Unknown cap "%s" found!' % cap)
|
|
caps_value += 1 << caps_dict[cap]
|
|
|
|
path_length_with_null = len(path) + 1
|
|
path_length_aligned_64 = (path_length_with_null + 7) & ~7
|
|
# 16 bytes of header plus the path length with alignment
|
|
length = 16 + path_length_aligned_64
|
|
|
|
length_binary = bytearray(ctypes.c_uint16(length))
|
|
mode_binary = bytearray(ctypes.c_uint16(mode))
|
|
user_binary = bytearray(ctypes.c_uint16(int(user, 0)))
|
|
group_binary = bytearray(ctypes.c_uint16(int(group, 0)))
|
|
caps_binary = bytearray(ctypes.c_uint64(caps_value))
|
|
path_binary = ctypes.create_string_buffer(path,
|
|
path_length_aligned_64).raw
|
|
|
|
out_file.write(length_binary)
|
|
out_file.write(mode_binary)
|
|
out_file.write(user_binary)
|
|
out_file.write(group_binary)
|
|
out_file.write(caps_binary)
|
|
out_file.write(path_binary)
|
|
|
|
def _emit_entry(self, fs_config):
|
|
"""Returns a boolean whether or not to emit the input fs_config"""
|
|
|
|
path = fs_config.path
|
|
|
|
if self._partition == 'system':
|
|
if not self._all_partitions:
|
|
return True
|
|
for skip_partition in self._all_partitions.split(','):
|
|
if path.startswith(skip_partition) or path.startswith(
|
|
'system/' + skip_partition):
|
|
return False
|
|
return True
|
|
else:
|
|
if path.startswith(
|
|
self._partition) or path.startswith('system/' +
|
|
self._partition):
|
|
return True
|
|
return False
|
|
|
|
def _generate(self):
|
|
"""Generates an OEM android_filesystem_config.h header file to stdout.
|
|
|
|
Args:
|
|
files ([FSConfig]): A list of FSConfig objects for file entries.
|
|
dirs ([FSConfig]): A list of FSConfig objects for directory
|
|
entries.
|
|
aids ([AIDS]): A list of AID objects for Android Id entries.
|
|
"""
|
|
dirs = self._oem_parser.dirs
|
|
files = self._oem_parser.files
|
|
|
|
if self._generate_files:
|
|
with open(self._out_file, 'wb') as open_file:
|
|
for fs_config in files:
|
|
if self._emit_entry(fs_config):
|
|
self._to_fs_entry(fs_config, open_file)
|
|
|
|
if self._generate_dirs:
|
|
with open(self._out_file, 'wb') as open_file:
|
|
for dir_entry in dirs:
|
|
if self._emit_entry(dir_entry):
|
|
self._to_fs_entry(dir_entry, open_file)
|
|
|
|
|
|
@generator('aidarray')
|
|
class AIDArrayGen(BaseGenerator):
|
|
"""Generates the android_id static array."""
|
|
|
|
_GENERATED = ('/*\n'
|
|
' * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY!\n'
|
|
' */')
|
|
|
|
_INCLUDE = '#include <private/android_filesystem_config.h>'
|
|
|
|
# Note that the android_id name field is of type 'const char[]' instead of
|
|
# 'const char*'. While this seems less straightforward as we need to
|
|
# calculate the max length of all names, this allows the entire android_ids
|
|
# table to be placed in .rodata section instead of .data.rel.ro section,
|
|
# resulting in less memory pressure.
|
|
_STRUCT_FS_CONFIG = textwrap.dedent("""
|
|
struct android_id_info {
|
|
const char name[%d];
|
|
unsigned aid;
|
|
};""")
|
|
|
|
_OPEN_ID_ARRAY = 'static const struct android_id_info android_ids[] = {'
|
|
|
|
_ID_ENTRY = ' { "%s", %s },'
|
|
|
|
_CLOSE_FILE_STRUCT = '};'
|
|
|
|
_COUNT = ('#define android_id_count \\\n'
|
|
' (sizeof(android_ids) / sizeof(android_ids[0]))')
|
|
|
|
def add_opts(self, opt_group):
|
|
|
|
opt_group.add_argument(
|
|
'hdrfile', help='The android_filesystem_config.h'
|
|
'file to parse')
|
|
|
|
def __call__(self, args):
|
|
|
|
hdr = AIDHeaderParser(args['hdrfile'])
|
|
max_name_length = max(len(aid.friendly) + 1 for aid in hdr.aids)
|
|
|
|
print AIDArrayGen._GENERATED
|
|
print
|
|
print AIDArrayGen._INCLUDE
|
|
print
|
|
print AIDArrayGen._STRUCT_FS_CONFIG % max_name_length
|
|
print
|
|
print AIDArrayGen._OPEN_ID_ARRAY
|
|
|
|
for aid in hdr.aids:
|
|
print AIDArrayGen._ID_ENTRY % (aid.friendly, aid.identifier)
|
|
|
|
print AIDArrayGen._CLOSE_FILE_STRUCT
|
|
print
|
|
print AIDArrayGen._COUNT
|
|
print
|
|
|
|
|
|
@generator('oemaid')
|
|
class OEMAidGen(BaseGenerator):
|
|
"""Generates the OEM AID_<name> value header file."""
|
|
|
|
_GENERATED = ('/*\n'
|
|
' * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY!\n'
|
|
' */')
|
|
|
|
_GENERIC_DEFINE = "#define %s\t%s"
|
|
|
|
_FILE_COMMENT = '// Defined in file: \"%s\"'
|
|
|
|
# Intentional trailing newline for readability.
|
|
_FILE_IFNDEF_DEFINE = ('#ifndef GENERATED_OEM_AIDS_H_\n'
|
|
'#define GENERATED_OEM_AIDS_H_\n')
|
|
|
|
_FILE_ENDIF = '#endif'
|
|
|
|
def __init__(self):
|
|
|
|
self._old_file = None
|
|
|
|
def add_opts(self, opt_group):
|
|
|
|
opt_group.add_argument(
|
|
'fsconfig', nargs='+', help='The list of fsconfig files to parse.')
|
|
|
|
opt_group.add_argument(
|
|
'--aid-header',
|
|
required=True,
|
|
help='An android_filesystem_config.h file'
|
|
'to parse AIDs and OEM Ranges from')
|
|
|
|
def __call__(self, args):
|
|
|
|
hdr_parser = AIDHeaderParser(args['aid_header'])
|
|
|
|
parser = FSConfigFileParser(args['fsconfig'], hdr_parser.ranges)
|
|
|
|
print OEMAidGen._GENERATED
|
|
|
|
print OEMAidGen._FILE_IFNDEF_DEFINE
|
|
|
|
for aid in parser.aids:
|
|
self._print_aid(aid)
|
|
print
|
|
|
|
print OEMAidGen._FILE_ENDIF
|
|
|
|
def _print_aid(self, aid):
|
|
"""Prints a valid #define AID identifier to stdout.
|
|
|
|
Args:
|
|
aid to print
|
|
"""
|
|
|
|
# print the source file location of the AID
|
|
found_file = aid.found
|
|
if found_file != self._old_file:
|
|
print OEMAidGen._FILE_COMMENT % found_file
|
|
self._old_file = found_file
|
|
|
|
print OEMAidGen._GENERIC_DEFINE % (aid.identifier, aid.value)
|
|
|
|
|
|
@generator('passwd')
|
|
class PasswdGen(BaseGenerator):
|
|
"""Generates the /etc/passwd file per man (5) passwd."""
|
|
|
|
def __init__(self):
|
|
|
|
self._old_file = None
|
|
|
|
def add_opts(self, opt_group):
|
|
|
|
opt_group.add_argument(
|
|
'fsconfig', nargs='+', help='The list of fsconfig files to parse.')
|
|
|
|
opt_group.add_argument(
|
|
'--aid-header',
|
|
required=True,
|
|
help='An android_filesystem_config.h file'
|
|
'to parse AIDs and OEM Ranges from')
|
|
|
|
opt_group.add_argument(
|
|
'--partition',
|
|
required=True,
|
|
help=
|
|
'Filter the input file and only output entries for the given partition.'
|
|
)
|
|
|
|
def __call__(self, args):
|
|
|
|
hdr_parser = AIDHeaderParser(args['aid_header'])
|
|
|
|
parser = FSConfigFileParser(args['fsconfig'], hdr_parser.ranges)
|
|
|
|
filter_partition = args['partition']
|
|
|
|
aids = parser.aids
|
|
|
|
# nothing to do if no aids defined
|
|
if not aids:
|
|
return
|
|
|
|
aids_by_partition = {}
|
|
partitions = hdr_parser.ranges.keys()
|
|
partitions.sort(key=len, reverse=True)
|
|
|
|
for aid in aids:
|
|
for partition in partitions:
|
|
if aid.friendly.startswith(partition):
|
|
if partition in aids_by_partition:
|
|
aids_by_partition[partition].append(aid)
|
|
else:
|
|
aids_by_partition[partition] = [aid]
|
|
break
|
|
|
|
if filter_partition in aids_by_partition:
|
|
for aid in aids_by_partition[filter_partition]:
|
|
self._print_formatted_line(aid)
|
|
|
|
def _print_formatted_line(self, aid):
|
|
"""Prints the aid to stdout in the passwd format. Internal use only.
|
|
|
|
Colon delimited:
|
|
login name, friendly name
|
|
encrypted password (optional)
|
|
uid (int)
|
|
gid (int)
|
|
User name or comment field
|
|
home directory
|
|
interpreter (optional)
|
|
|
|
Args:
|
|
aid (AID): The aid to print.
|
|
"""
|
|
if self._old_file != aid.found:
|
|
self._old_file = aid.found
|
|
|
|
try:
|
|
logon, uid = Utils.get_login_and_uid_cleansed(aid)
|
|
except ValueError as exception:
|
|
sys.exit(exception)
|
|
|
|
print "%s::%s:%s::/:%s" % (logon, uid, uid, aid.login_shell)
|
|
|
|
|
|
@generator('group')
|
|
class GroupGen(PasswdGen):
|
|
"""Generates the /etc/group file per man (5) group."""
|
|
|
|
# Overrides parent
|
|
def _print_formatted_line(self, aid):
|
|
"""Prints the aid to stdout in the group format. Internal use only.
|
|
|
|
Formatted (per man 5 group) like:
|
|
group_name:password:GID:user_list
|
|
|
|
Args:
|
|
aid (AID): The aid to print.
|
|
"""
|
|
if self._old_file != aid.found:
|
|
self._old_file = aid.found
|
|
|
|
try:
|
|
logon, uid = Utils.get_login_and_uid_cleansed(aid)
|
|
except ValueError as exception:
|
|
sys.exit(exception)
|
|
|
|
print "%s::%s:" % (logon, uid)
|
|
|
|
|
|
@generator('print')
|
|
class PrintGen(BaseGenerator):
|
|
"""Prints just the constants and values, separated by spaces, in an easy to
|
|
parse format for use by other scripts.
|
|
|
|
Each line is just the identifier and the value, separated by a space.
|
|
"""
|
|
|
|
def add_opts(self, opt_group):
|
|
opt_group.add_argument(
|
|
'aid-header', help='An android_filesystem_config.h file.')
|
|
|
|
def __call__(self, args):
|
|
|
|
hdr_parser = AIDHeaderParser(args['aid-header'])
|
|
aids = hdr_parser.aids
|
|
|
|
aids.sort(key=lambda item: int(item.normalized_value))
|
|
|
|
for aid in aids:
|
|
print '%s %s' % (aid.identifier, aid.normalized_value)
|
|
|
|
|
|
def main():
|
|
"""Main entry point for execution."""
|
|
|
|
opt_parser = argparse.ArgumentParser(
|
|
description='A tool for parsing fsconfig config files and producing' +
|
|
'digestable outputs.')
|
|
subparser = opt_parser.add_subparsers(help='generators')
|
|
|
|
gens = generator.get()
|
|
|
|
# for each gen, instantiate and add them as an option
|
|
for name, gen in gens.iteritems():
|
|
|
|
generator_option_parser = subparser.add_parser(name, help=gen.__doc__)
|
|
generator_option_parser.set_defaults(which=name)
|
|
|
|
opt_group = generator_option_parser.add_argument_group(name +
|
|
' options')
|
|
gen.add_opts(opt_group)
|
|
|
|
args = opt_parser.parse_args()
|
|
|
|
args_as_dict = vars(args)
|
|
which = args_as_dict['which']
|
|
del args_as_dict['which']
|
|
|
|
gens[which](args_as_dict)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|