| #!/usr/bin/env python3 |
| # |
| # Copyright 2021 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import os |
| import sys |
| |
| |
| META_CHARS = frozenset(['.', '^', '$', '?', '*', '+', '|', '[', '(', '{']) |
| ESCAPED_META_CHARS = frozenset([ '\\{}'.format(c) for c in META_CHARS ]) |
| |
| |
| def get_stem_len(path): |
| """Returns the length of the stem.""" |
| stem_len = 0 |
| i = 0 |
| while i < len(path): |
| if path[i] == "\\": |
| i += 1 |
| elif path[i] in META_CHARS: |
| break |
| stem_len += 1 |
| i += 1 |
| return stem_len |
| |
| |
| def is_meta(path): |
| """Indicates if a path contains any metacharacter.""" |
| meta_char_count = 0 |
| escaped_meta_char_count = 0 |
| for c in META_CHARS: |
| if c in path: |
| meta_char_count += 1 |
| for c in ESCAPED_META_CHARS: |
| if c in path: |
| escaped_meta_char_count += 1 |
| return meta_char_count > escaped_meta_char_count |
| |
| |
| class FileContextsNode(object): |
| """An entry in a file_context file.""" |
| |
| def __init__(self, path, file_type, context, meta, stem_len, str_len, line): |
| self.path = path |
| self.file_type = file_type |
| self.context = context |
| self.meta = meta |
| self.stem_len = stem_len |
| self.str_len = str_len |
| self.type = context.split(":")[2] |
| self.line = line |
| |
| @classmethod |
| def create(cls, line): |
| if (len(line) == 0) or (line[0] == '#'): |
| return None |
| |
| split = line.split() |
| path = split[0].strip() |
| context = split[-1].strip() |
| file_type = None |
| if len(split) == 3: |
| file_type = split[1].strip() |
| meta = is_meta(path) |
| stem_len = get_stem_len(path) |
| str_len = len(path.replace("\\", "")) |
| |
| return cls(path, file_type, context, meta, stem_len, str_len, line) |
| |
| # Comparator function based off fc_sort.c |
| def __lt__(self, other): |
| # The regex without metachars is more specific. |
| if self.meta and not other.meta: |
| return True |
| if other.meta and not self.meta: |
| return False |
| |
| # The regex with longer stem_len (regex before any meta characters) is |
| # more specific. |
| if self.stem_len < other.stem_len: |
| return True |
| if other.stem_len < self.stem_len: |
| return False |
| |
| # The regex with longer string length is more specific |
| if self.str_len < other.str_len: |
| return True |
| if other.str_len < self.str_len: |
| return False |
| |
| # A regex with a file_type defined (e.g. file, dir) is more specific. |
| if self.file_type is None and other.file_type is not None: |
| return True |
| if other.file_type is None and self.file_type is not None: |
| return False |
| |
| return False |
| |
| |
| def read_file_contexts(file_descriptor): |
| file_contexts = [] |
| for line in file_descriptor: |
| node = FileContextsNode.create(line.strip()) |
| if node is not None: |
| file_contexts.append(node) |
| return file_contexts |
| |
| |
| def read_multiple_file_contexts(files): |
| file_contexts = [] |
| for filename in files: |
| with open(filename) as fd: |
| file_contexts.extend(read_file_contexts(fd)) |
| return file_contexts |
| |
| |
| def sort(files): |
| for f in files: |
| if not os.path.exists(f): |
| sys.exit("Error: File_contexts file " + f + " does not exist\n") |
| file_contexts = read_multiple_file_contexts(files) |
| file_contexts.sort() |
| return file_contexts |
| |
| |
| def print_fc(fc, out): |
| if not out: |
| f = sys.stdout |
| else: |
| f = open(out, "w") |
| for node in fc: |
| f.write(node.line + "\n") |
| |
| |
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser( |
| description="SELinux file_contexts sorting tool.") |
| parser.add_argument("-i", dest="input", nargs="*", |
| help="Path to the file_contexts file(s).") |
| parser.add_argument("-o", dest="output", help="Path to the output file.") |
| args = parser.parse_args() |
| if not args.input: |
| parser.error("Must include path to policy") |
| |
| print_fc(sort(args.input), args.output) |