platform_system_sepolicy/tests/fc_sort.py
Thiébaud Weksteen f24b457dd0 Migrate tests/ to Python 3
In general, it appears that libselinux and libsepol interpret paths and
contexts as bytes. For instance, selabel_file(5) mentions about the path
field of file_contexts:

  Strings representing paths are processed as bytes (as opposed to
  Unicode), meaning that non-ASCII characters are not matched
  by a single wildcard.

libsepol also uses primitives such as strchr[1], which explicitly
operate at the byte level (see strchr(3)). However, practically, Android
paths and contexts all uses ASCII characters.

Use the str type (i.e., Unicode) for all Python code to avoid a larger
refactoring. Ensure we convert to bytes for inputs and outputs of
libsepolwrap.so. The encoding "ascii" is used, which will raise an error
should a context or type contain non-ASCII characters.

Update headers to match development/docs/copyright-templates.

[1] https://cs.android.com/android/platform/superproject/+/master:external/selinux/libsepol/src/context_record.c;l=224;drc=454466e2e49fd99f36db78396e604962b8682cb4

Bug: 200119288
Test: lunch aosp_bramble-userdebug && m
Test: atest --host fc_sort_test
Test: manually run searchpolicy
Change-Id: I72d41a35f90b2d4112e481cd8d7408764a6c8132
2021-12-01 21:45:13 +00:00

158 lines
4.6 KiB
Python

#!/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)