fs_config: introduce TARGET_FS_CONFIG_GEN

Introduce TARGET_FS_CONFIG_GEN, an option for generating
android_filesystem_config.h files from multiple intermediate
file formats. See the README for more details.

Change-Id: Ibdb2f65702657802e34b0cec18fb43709d7452bb
Signed-off-by: William Roberts <william.c.roberts@intel.com>
diff --git a/tools/fs_config/fs_config_generator.py b/tools/fs_config/fs_config_generator.py
new file mode 100755
index 0000000..e66e295
--- /dev/null
+++ b/tools/fs_config/fs_config_generator.py
@@ -0,0 +1,277 @@
+#!/usr/bin/env python
+
+import ConfigParser
+import re
+import sys
+
+
+GENERATED = '''
+/*
+ * THIS IS AN AUTOGENERATED FILE! DO NOT MODIFY
+ */
+'''
+
+INCLUDE = '#include <private/android_filesystem_config.h>'
+
+DEFINE_NO_DIRS = '#define NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS\n'
+DEFINE_NO_FILES = '#define NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_FILES\n'
+
+DEFAULT_WARNING = '#warning No device-supplied android_filesystem_config.h, using empty default.'
+
+NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS_ENTRY = '{ 00000, AID_ROOT,      AID_ROOT,      0, "system/etc/fs_config_dirs" },'
+NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_FILES_ENTRY = '{ 00000, AID_ROOT,      AID_ROOT,      0, "system/etc/fs_config_files" },'
+
+IFDEF_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS = '#ifdef NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS'
+ENDIF = '#endif'
+
+OPEN_FILE_STRUCT = 'static const struct fs_path_config android_device_files[] = {'
+OPEN_DIR_STRUCT = 'static const struct fs_path_config android_device_dirs[] = {'
+CLOSE_FILE_STRUCT = '};'
+
+GENERIC_DEFINE = "#define %s\t%s"
+
+FILE_COMMENT = '// Defined in file: \"%s\"'
+
+# from system/core/include/private/android_filesystem_config.h
+AID_OEM_RESERVED_START = 2900
+AID_OEM_RESERVED_END = 2999
+
+
+AID_MATCH = re.compile('AID_[a-zA-Z]+')
+
+def handle_aid(file_name, section_name, config, aids, seen_aids):
+    value = config.get(section_name, 'value')
+
+    errmsg = '%s for: \"' + section_name + '" file: \"' + file_name + '\"'
+
+    if not value:
+        raise Exception(errmsg % 'Found specified but unset "value"')
+
+    v = convert_int(value)
+    if not v:
+        raise Exception(errmsg % ('Invalid "value", not a number, got: \"%s\"' % value))
+
+    # Values must be within OEM range
+    if (v < AID_OEM_RESERVED_START) or (v > AID_OEM_RESERVED_END):
+        s = '"value" not in valid range %d - %d, got: %s'
+        s = s % (AID_OEM_RESERVED_START, AID_OEM_RESERVED_END, value)
+        raise Exception(errmsg % s)
+
+    # use the normalized int value in the dict and detect
+    # duplicate definitions of the same vallue
+    v = str(v)
+    if v in seen_aids[1]:
+        # map of value to aid name
+        a = seen_aids[1][v]
+
+        # aid name to file
+        f = seen_aids[0][a]
+
+        s = 'Duplicate AID value "%s" found on AID: "%s".' % (value, seen_aids[1][v])
+        s += ' Previous found in file: "%s."' % f
+        raise Exception(errmsg % s)
+
+    seen_aids[1][v] = section_name
+
+    # Append a tuple of (AID_*, base10(value), str(value))
+    # We keep the str 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.
+    aids.append((file_name, section_name, v, value))
+
+def convert_int(num):
+
+        try:
+            if num.startswith('0x'):
+                return int(num, 16)
+            elif num.startswith('0b'):
+                return int(num, 2)
+            elif num.startswith('0'):
+                return int(num, 8)
+            else:
+                return int(num, 10)
+        except ValueError:
+            pass
+        return None
+
+def handle_path(file_name, section_name, config, files, dirs):
+
+            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:
+                raise Exception(errmsg % 'mode')
+
+            if not user:
+                raise Exception(errmsg % 'user')
+
+            if not group:
+                raise Exception(errmsg % 'group')
+
+            if not caps:
+                raise Exception(errmsg % 'caps')
+
+            caps = caps.split()
+
+            tmp = []
+            for x in caps:
+                if convert_int(x):
+                    tmp.append('(' + x + ')')
+                else:
+                    tmp.append('(1ULL << CAP_' + x.upper() + ')')
+
+            caps = tmp
+
+            path = '"' + section_name + '"'
+
+            if len(mode) == 3:
+                mode = '0' + mode
+
+            try:
+                int(mode, 8)
+            except:
+                raise Exception('Mode must be octal characters, got: "' + mode + '"')
+
+            if len(mode) != 4:
+                raise Exception('Mode must be 3 or 4 characters, got: "' + mode + '"')
+
+
+            caps = '|'.join(caps)
+
+            x = [ mode, user, group, caps, section_name ]
+            if section_name[-1] == '/':
+                dirs.append((file_name, x))
+            else:
+                files.append((file_name, x))
+
+def handle_dup(name, file_name, section_name, seen):
+        if section_name in seen:
+            dups = '"' + seen[section_name] + '" and '
+            dups += file_name
+            raise Exception('Duplicate ' + name + ' "' + section_name + '" found in files: ' + dups)
+
+def parse(file_name, files, dirs, aids, seen_paths, seen_aids):
+
+        config = ConfigParser.ConfigParser()
+        config.read(file_name)
+
+        for s in config.sections():
+
+            if AID_MATCH.match(s) and config.has_option(s, 'value'):
+                handle_dup('AID', file_name, s, seen_aids[0])
+                seen_aids[0][s] = file_name
+                handle_aid(file_name, s, config, aids, seen_aids)
+            else:
+                handle_dup('path', file_name, s, seen_paths)
+                seen_paths[s] = file_name
+                handle_path(file_name, s, config, files, dirs)
+
+def generate(files, dirs, aids):
+    print GENERATED
+    print INCLUDE
+    print
+
+    are_dirs = len(dirs) > 0
+    are_files = len(files) > 0
+    are_aids = len(aids) > 0
+
+    if are_aids:
+        # sort on value of (file_name, name, value, strvalue)
+        aids.sort(key=lambda x: x[2])
+        for a in aids:
+            # use the preserved str value
+            print FILE_COMMENT % a[0]
+            print GENERIC_DEFINE % (a[1], a[2])
+
+        print
+
+    if not are_dirs:
+        print DEFINE_NO_DIRS
+
+    if not are_files:
+        print DEFINE_NO_FILES
+
+    if not are_files and not are_dirs and not are_aids:
+        print DEFAULT_WARNING
+        return
+
+    if are_files:
+        print OPEN_FILE_STRUCT
+        for tup in files:
+            f = tup[0]
+            c = tup[1]
+            c[4] = '"' + c[4] + '"'
+            c = '{ ' + '    ,'.join(c) + ' },'
+            print FILE_COMMENT % f
+            print '    ' + c
+
+        if not are_dirs:
+            print IFDEF_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS
+            print '    ' + NO_ANDROID_FILESYSTEM_CONFIG_DEVICE_DIRS_ENTRY
+            print ENDIF
+        print CLOSE_FILE_STRUCT
+
+    if are_dirs:
+        print OPEN_DIR_STRUCT
+        for d in dirs:
+            f[4] = '"' + f[4] + '"'
+            d = '{ ' + '    ,'.join(d) + ' },'
+            print '    ' + d
+
+        print CLOSE_FILE_STRUCT
+
+def file_key(x):
+
+    # Wrapper class for custom prefix matching strings
+    class S(object):
+        def __init__(self, str):
+
+            self.orig = str
+            self.is_prefix = str[-1] == '*'
+            if self.is_prefix:
+                self.str = str[:-1]
+            else:
+                self.str = str
+
+        def __lt__(self, other):
+
+            # if were both suffixed the smallest string
+            # is 'bigger'
+            if self.is_prefix and other.is_prefix:
+                b = len(self.str) > len(other.str)
+            # If I am an the suffix match, im bigger
+            elif self.is_prefix:
+                b = False
+            # If other is the suffix match, he's bigger
+            elif other.is_prefix:
+                b = True
+            # Alphabetical
+            else:
+                b = self.str < other.str
+            return b
+
+    return S(x[4])
+
+def main():
+
+    files = []
+    dirs = []
+    aids = []
+    seen_paths = {}
+
+    # (name to file, value to aid)
+    seen_aids = ({}, {})
+
+    for x in sys.argv[1:]:
+        parse(x, files, dirs, aids, seen_paths, seen_aids)
+
+    files.sort(key= lambda x: file_key(x[1]))
+    generate(files, dirs, aids)
+
+if __name__ == '__main__':
+    main()