1#!/usr/bin/env python3 2# 3# Copyright 2021 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import argparse 18import os 19import sys 20 21 22META_CHARS = frozenset(['.', '^', '$', '?', '*', '+', '|', '[', '(', '{']) 23ESCAPED_META_CHARS = frozenset([ '\\{}'.format(c) for c in META_CHARS ]) 24 25 26def get_stem_len(path): 27 """Returns the length of the stem.""" 28 stem_len = 0 29 i = 0 30 while i < len(path): 31 if path[i] == "\\": 32 i += 1 33 elif path[i] in META_CHARS: 34 break 35 stem_len += 1 36 i += 1 37 return stem_len 38 39 40def is_meta(path): 41 """Indicates if a path contains any metacharacter.""" 42 meta_char_count = 0 43 escaped_meta_char_count = 0 44 for c in META_CHARS: 45 if c in path: 46 meta_char_count += 1 47 for c in ESCAPED_META_CHARS: 48 if c in path: 49 escaped_meta_char_count += 1 50 return meta_char_count > escaped_meta_char_count 51 52 53class FileContextsNode(object): 54 """An entry in a file_context file.""" 55 56 def __init__(self, path, file_type, context, meta, stem_len, str_len, line): 57 self.path = path 58 self.file_type = file_type 59 self.context = context 60 self.meta = meta 61 self.stem_len = stem_len 62 self.str_len = str_len 63 self.type = context.split(":")[2] 64 self.line = line 65 66 @classmethod 67 def create(cls, line): 68 if (len(line) == 0) or (line[0] == '#'): 69 return None 70 71 split = line.split() 72 path = split[0].strip() 73 context = split[-1].strip() 74 file_type = None 75 if len(split) == 3: 76 file_type = split[1].strip() 77 meta = is_meta(path) 78 stem_len = get_stem_len(path) 79 str_len = len(path.replace("\\", "")) 80 81 return cls(path, file_type, context, meta, stem_len, str_len, line) 82 83 # Comparator function based off fc_sort.c 84 def __lt__(self, other): 85 # The regex without metachars is more specific. 86 if self.meta and not other.meta: 87 return True 88 if other.meta and not self.meta: 89 return False 90 91 # The regex with longer stem_len (regex before any meta characters) is 92 # more specific. 93 if self.stem_len < other.stem_len: 94 return True 95 if other.stem_len < self.stem_len: 96 return False 97 98 # The regex with longer string length is more specific 99 if self.str_len < other.str_len: 100 return True 101 if other.str_len < self.str_len: 102 return False 103 104 # A regex with a file_type defined (e.g. file, dir) is more specific. 105 if self.file_type is None and other.file_type is not None: 106 return True 107 if other.file_type is None and self.file_type is not None: 108 return False 109 110 return False 111 112 113def read_file_contexts(file_descriptor): 114 file_contexts = [] 115 for line in file_descriptor: 116 node = FileContextsNode.create(line.strip()) 117 if node is not None: 118 file_contexts.append(node) 119 return file_contexts 120 121 122def read_multiple_file_contexts(files): 123 file_contexts = [] 124 for filename in files: 125 with open(filename) as fd: 126 file_contexts.extend(read_file_contexts(fd)) 127 return file_contexts 128 129 130def sort(files): 131 for f in files: 132 if not os.path.exists(f): 133 sys.exit("Error: File_contexts file " + f + " does not exist\n") 134 file_contexts = read_multiple_file_contexts(files) 135 file_contexts.sort() 136 return file_contexts 137 138 139def print_fc(fc, out): 140 if not out: 141 f = sys.stdout 142 else: 143 f = open(out, "w") 144 for node in fc: 145 f.write(node.line + "\n") 146 147 148if __name__ == '__main__': 149 parser = argparse.ArgumentParser( 150 description="SELinux file_contexts sorting tool.") 151 parser.add_argument("-i", dest="input", nargs="*", 152 help="Path to the file_contexts file(s).") 153 parser.add_argument("-o", dest="output", help="Path to the output file.") 154 args = parser.parse_args() 155 if not args.input: 156 parser.error("Must include path to policy") 157 158 print_fc(sort(args.input), args.output) 159