| Index: tools/checkperms/checkperms.py
|
| diff --git a/tools/checkperms/checkperms.py b/tools/checkperms/checkperms.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..e2807c73c58b394f72f08b714a8840ba022b5132
|
| --- /dev/null
|
| +++ b/tools/checkperms/checkperms.py
|
| @@ -0,0 +1,480 @@
|
| +#!/usr/bin/env python
|
| +# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| +# Use of this source code is governed by a BSD-style license that can be
|
| +# found in the LICENSE file.
|
| +
|
| +"""Makes sure files have the right permissions.
|
| +
|
| +Some developers have broken SCM configurations that flip the executable
|
| +permission on for no good reason. Unix developers who run ls --color will then
|
| +see .cc files in green and get confused.
|
| +
|
| +- For file extensions that must be executable, add it to EXECUTABLE_EXTENSIONS.
|
| +- For file extensions that must not be executable, add it to
|
| + NOT_EXECUTABLE_EXTENSIONS.
|
| +- To ignore all the files inside a directory, add it to IGNORED_PATHS.
|
| +- For file base name with ambiguous state and that should not be checked for
|
| + shebang, add it to IGNORED_FILENAMES.
|
| +
|
| +Any file not matching the above will be opened and looked if it has a shebang
|
| +or an ELF header. If this does not match the executable bit on the file, the
|
| +file will be flagged.
|
| +
|
| +Note that all directory separators must be slashes (Unix-style) and not
|
| +backslashes. All directories should be relative to the source root and all
|
| +file paths should be only lowercase.
|
| +"""
|
| +
|
| +import json
|
| +import logging
|
| +import optparse
|
| +import os
|
| +import stat
|
| +import string
|
| +import subprocess
|
| +import sys
|
| +
|
| +#### USER EDITABLE SECTION STARTS HERE ####
|
| +
|
| +# Files with these extensions must have executable bit set.
|
| +#
|
| +# Case-sensitive.
|
| +EXECUTABLE_EXTENSIONS = (
|
| + 'bat',
|
| + 'dll',
|
| + 'dylib',
|
| + 'exe',
|
| +)
|
| +
|
| +# These files must have executable bit set.
|
| +#
|
| +# Case-insensitive, lower-case only.
|
| +EXECUTABLE_PATHS = (
|
| + 'chrome/test/data/app_shim/app_shim_32_bit.app/contents/'
|
| + 'macos/app_mode_loader',
|
| + 'chrome/test/data/extensions/uitest/plugins/plugin.plugin/contents/'
|
| + 'macos/testnetscapeplugin',
|
| + 'chrome/test/data/extensions/uitest/plugins_private/plugin.plugin/contents/'
|
| + 'macos/testnetscapeplugin',
|
| +)
|
| +
|
| +# These files must not have the executable bit set. This is mainly a performance
|
| +# optimization as these files are not checked for shebang. The list was
|
| +# partially generated from:
|
| +# git ls-files | grep "\\." | sed 's/.*\.//' | sort | uniq -c | sort -b -g
|
| +#
|
| +# Case-sensitive.
|
| +NON_EXECUTABLE_EXTENSIONS = (
|
| + '1',
|
| + '3ds',
|
| + 'S',
|
| + 'am',
|
| + 'applescript',
|
| + 'asm',
|
| + 'c',
|
| + 'cc',
|
| + 'cfg',
|
| + 'chromium',
|
| + 'cpp',
|
| + 'crx',
|
| + 'cs',
|
| + 'css',
|
| + 'cur',
|
| + 'def',
|
| + 'der',
|
| + 'expected',
|
| + 'gif',
|
| + 'grd',
|
| + 'gyp',
|
| + 'gypi',
|
| + 'h',
|
| + 'hh',
|
| + 'htm',
|
| + 'html',
|
| + 'hyph',
|
| + 'ico',
|
| + 'idl',
|
| + 'java',
|
| + 'jpg',
|
| + 'js',
|
| + 'json',
|
| + 'm',
|
| + 'm4',
|
| + 'mm',
|
| + 'mms',
|
| + 'mock-http-headers',
|
| + 'nexe',
|
| + 'nmf',
|
| + 'onc',
|
| + 'pat',
|
| + 'patch',
|
| + 'pdf',
|
| + 'pem',
|
| + 'plist',
|
| + 'png',
|
| + 'proto',
|
| + 'rc',
|
| + 'rfx',
|
| + 'rgs',
|
| + 'rules',
|
| + 'spec',
|
| + 'sql',
|
| + 'srpc',
|
| + 'svg',
|
| + 'tcl',
|
| + 'test',
|
| + 'tga',
|
| + 'txt',
|
| + 'vcproj',
|
| + 'vsprops',
|
| + 'webm',
|
| + 'word',
|
| + 'xib',
|
| + 'xml',
|
| + 'xtb',
|
| + 'zip',
|
| +)
|
| +
|
| +# These files must not have executable bit set.
|
| +#
|
| +# Case-insensitive, lower-case only.
|
| +NON_EXECUTABLE_PATHS = (
|
| + 'build/android/tests/symbolize/liba.so',
|
| + 'build/android/tests/symbolize/libb.so',
|
| + 'chrome/installer/mac/sign_app.sh.in',
|
| + 'chrome/installer/mac/sign_versioned_dir.sh.in',
|
| + 'chrome/test/data/extensions/uitest/plugins/plugin32.so',
|
| + 'chrome/test/data/extensions/uitest/plugins/plugin64.so',
|
| + 'chrome/test/data/extensions/uitest/plugins_private/plugin32.so',
|
| + 'chrome/test/data/extensions/uitest/plugins_private/plugin64.so',
|
| + 'components/test/data/component_updater/ihfokbkgjpifnbbojhneepfflplebdkc/'
|
| + 'ihfokbkgjpifnbbojhneepfflplebdkc_1/a_changing_binary_file',
|
| + 'components/test/data/component_updater/ihfokbkgjpifnbbojhneepfflplebdkc/'
|
| + 'ihfokbkgjpifnbbojhneepfflplebdkc_2/a_changing_binary_file',
|
| + 'courgette/testdata/elf-32-1',
|
| + 'courgette/testdata/elf-32-2',
|
| + 'courgette/testdata/elf-64',
|
| +)
|
| +
|
| +# File names that are always whitelisted. (These are mostly autoconf spew.)
|
| +#
|
| +# Case-sensitive.
|
| +IGNORED_FILENAMES = (
|
| + 'config.guess',
|
| + 'config.sub',
|
| + 'configure',
|
| + 'depcomp',
|
| + 'install-sh',
|
| + 'missing',
|
| + 'mkinstalldirs',
|
| + 'naclsdk',
|
| + 'scons',
|
| +)
|
| +
|
| +# File paths starting with one of these will be ignored as well.
|
| +# Please consider fixing your file permissions, rather than adding to this list.
|
| +#
|
| +# Case-insensitive, lower-case only.
|
| +IGNORED_PATHS = (
|
| + 'native_client_sdk/src/build_tools/sdk_tools/third_party/fancy_urllib/'
|
| + '__init__.py',
|
| + 'out/',
|
| + # TODO(maruel): Fix these.
|
| + 'third_party/android_testrunner/',
|
| + 'third_party/bintrees/',
|
| + 'third_party/closure_linter/',
|
| + 'third_party/devscripts/licensecheck.pl.vanilla',
|
| + 'third_party/hyphen/',
|
| + 'third_party/jemalloc/',
|
| + 'third_party/lcov-1.9/contrib/galaxy/conglomerate_functions.pl',
|
| + 'third_party/lcov-1.9/contrib/galaxy/gen_makefile.sh',
|
| + 'third_party/lcov/contrib/galaxy/conglomerate_functions.pl',
|
| + 'third_party/lcov/contrib/galaxy/gen_makefile.sh',
|
| + 'third_party/libevent/autogen.sh',
|
| + 'third_party/libevent/test/test.sh',
|
| + 'third_party/libxml/linux/xml2-config',
|
| + 'third_party/libxml/src/ltmain.sh',
|
| + 'third_party/mesa/',
|
| + 'third_party/protobuf/',
|
| + 'third_party/python_gflags/gflags.py',
|
| + 'third_party/sqlite/',
|
| + 'third_party/talloc/script/mksyms.sh',
|
| + 'third_party/tcmalloc/',
|
| + 'third_party/tlslite/setup.py',
|
| +)
|
| +
|
| +#### USER EDITABLE SECTION ENDS HERE ####
|
| +
|
| +assert set(EXECUTABLE_EXTENSIONS) & set(NON_EXECUTABLE_EXTENSIONS) == set()
|
| +assert set(EXECUTABLE_PATHS) & set(NON_EXECUTABLE_PATHS) == set()
|
| +
|
| +VALID_CHARS = set(string.ascii_lowercase + string.digits + '/-_.')
|
| +for paths in (EXECUTABLE_PATHS, NON_EXECUTABLE_PATHS, IGNORED_PATHS):
|
| + assert all([set(path).issubset(VALID_CHARS) for path in paths])
|
| +
|
| +
|
| +def capture(cmd, cwd):
|
| + """Returns the output of a command.
|
| +
|
| + Ignores the error code or stderr.
|
| + """
|
| + logging.debug('%s; cwd=%s' % (' '.join(cmd), cwd))
|
| + env = os.environ.copy()
|
| + env['LANGUAGE'] = 'en_US.UTF-8'
|
| + p = subprocess.Popen(
|
| + cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env)
|
| + return p.communicate()[0]
|
| +
|
| +
|
| +def get_git_root(dir_path):
|
| + """Returns the git checkout root or None."""
|
| + root = capture(['git', 'rev-parse', '--show-toplevel'], dir_path).strip()
|
| + if root:
|
| + return root
|
| +
|
| +
|
| +def is_ignored(rel_path):
|
| + """Returns True if rel_path is in our whitelist of files to ignore."""
|
| + rel_path = rel_path.lower()
|
| + return (
|
| + os.path.basename(rel_path) in IGNORED_FILENAMES or
|
| + rel_path.lower().startswith(IGNORED_PATHS))
|
| +
|
| +
|
| +def must_be_executable(rel_path):
|
| + """The file name represents a file type that must have the executable bit
|
| + set.
|
| + """
|
| + return (os.path.splitext(rel_path)[1][1:] in EXECUTABLE_EXTENSIONS or
|
| + rel_path.lower() in EXECUTABLE_PATHS)
|
| +
|
| +
|
| +def must_not_be_executable(rel_path):
|
| + """The file name represents a file type that must not have the executable
|
| + bit set.
|
| + """
|
| + return (os.path.splitext(rel_path)[1][1:] in NON_EXECUTABLE_EXTENSIONS or
|
| + rel_path.lower() in NON_EXECUTABLE_PATHS)
|
| +
|
| +
|
| +def has_executable_bit(full_path):
|
| + """Returns if any executable bit is set."""
|
| + permission = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
| + return bool(permission & os.stat(full_path).st_mode)
|
| +
|
| +
|
| +def has_shebang_or_is_elf(full_path):
|
| + """Returns if the file starts with #!/ or is an ELF binary.
|
| +
|
| + full_path is the absolute path to the file.
|
| + """
|
| + with open(full_path, 'rb') as f:
|
| + data = f.read(4)
|
| + return (data[:3] == '#!/' or data == '#! /', data == '\x7fELF')
|
| +
|
| +
|
| +def check_file(root_path, rel_path):
|
| + """Checks the permissions of the file whose path is root_path + rel_path and
|
| + returns an error if it is inconsistent. Returns None on success.
|
| +
|
| + It is assumed that the file is not ignored by is_ignored().
|
| +
|
| + If the file name is matched with must_be_executable() or
|
| + must_not_be_executable(), only its executable bit is checked.
|
| + Otherwise, the first few bytes of the file are read to verify if it has a
|
| + shebang or ELF header and compares this with the executable bit on the file.
|
| + """
|
| + full_path = os.path.join(root_path, rel_path)
|
| + def result_dict(error):
|
| + return {
|
| + 'error': error,
|
| + 'full_path': full_path,
|
| + 'rel_path': rel_path,
|
| + }
|
| + try:
|
| + bit = has_executable_bit(full_path)
|
| + except OSError:
|
| + # It's faster to catch exception than call os.path.islink(). Chromium
|
| + # tree happens to have invalid symlinks under
|
| + # third_party/openssl/openssl/test/.
|
| + return None
|
| +
|
| + if must_be_executable(rel_path):
|
| + if not bit:
|
| + return result_dict('Must have executable bit set')
|
| + return
|
| + if must_not_be_executable(rel_path):
|
| + if bit:
|
| + return result_dict('Must not have executable bit set')
|
| + return
|
| +
|
| + # For the others, it depends on the file header.
|
| + (shebang, elf) = has_shebang_or_is_elf(full_path)
|
| + if bit != (shebang or elf):
|
| + if bit:
|
| + return result_dict('Has executable bit but not shebang or ELF header')
|
| + if shebang:
|
| + return result_dict('Has shebang but not executable bit')
|
| + return result_dict('Has ELF header but not executable bit')
|
| +
|
| +
|
| +def check_files(root, files):
|
| + gen = (check_file(root, f) for f in files if not is_ignored(f))
|
| + return filter(None, gen)
|
| +
|
| +
|
| +class ApiBase(object):
|
| + def __init__(self, root_dir, bare_output):
|
| + self.root_dir = root_dir
|
| + self.bare_output = bare_output
|
| + self.count = 0
|
| + self.count_read_header = 0
|
| +
|
| + def check_file(self, rel_path):
|
| + logging.debug('check_file(%s)' % rel_path)
|
| + self.count += 1
|
| +
|
| + if (not must_be_executable(rel_path) and
|
| + not must_not_be_executable(rel_path)):
|
| + self.count_read_header += 1
|
| +
|
| + return check_file(self.root_dir, rel_path)
|
| +
|
| + def check_dir(self, rel_path):
|
| + return self.check(rel_path)
|
| +
|
| + def check(self, start_dir):
|
| + """Check the files in start_dir, recursively check its subdirectories."""
|
| + errors = []
|
| + items = self.list_dir(start_dir)
|
| + logging.info('check(%s) -> %d' % (start_dir, len(items)))
|
| + for item in items:
|
| + full_path = os.path.join(self.root_dir, start_dir, item)
|
| + rel_path = full_path[len(self.root_dir) + 1:]
|
| + if is_ignored(rel_path):
|
| + continue
|
| + if os.path.isdir(full_path):
|
| + # Depth first.
|
| + errors.extend(self.check_dir(rel_path))
|
| + else:
|
| + error = self.check_file(rel_path)
|
| + if error:
|
| + errors.append(error)
|
| + return errors
|
| +
|
| + def list_dir(self, start_dir):
|
| + """Lists all the files and directory inside start_dir."""
|
| + return sorted(
|
| + x for x in os.listdir(os.path.join(self.root_dir, start_dir))
|
| + if not x.startswith('.')
|
| + )
|
| +
|
| +
|
| +class ApiAllFilesAtOnceBase(ApiBase):
|
| + _files = None
|
| +
|
| + def list_dir(self, start_dir):
|
| + """Lists all the files and directory inside start_dir."""
|
| + if self._files is None:
|
| + self._files = sorted(self._get_all_files())
|
| + if not self.bare_output:
|
| + print 'Found %s files' % len(self._files)
|
| + start_dir = start_dir[len(self.root_dir) + 1:]
|
| + return [
|
| + x[len(start_dir):] for x in self._files if x.startswith(start_dir)
|
| + ]
|
| +
|
| + def _get_all_files(self):
|
| + """Lists all the files and directory inside self._root_dir."""
|
| + raise NotImplementedError()
|
| +
|
| +
|
| +class ApiGit(ApiAllFilesAtOnceBase):
|
| + def _get_all_files(self):
|
| + return capture(['git', 'ls-files'], cwd=self.root_dir).splitlines()
|
| +
|
| +
|
| +def get_scm(dir_path, bare):
|
| + """Returns a properly configured ApiBase instance."""
|
| + cwd = os.getcwd()
|
| + root = get_git_root(dir_path or cwd)
|
| + if root:
|
| + if not bare:
|
| + print('Found git repository at %s' % root)
|
| + return ApiGit(dir_path or root, bare)
|
| +
|
| + # Returns a non-scm aware checker.
|
| + if not bare:
|
| + print('Failed to determine the SCM for %s' % dir_path)
|
| + return ApiBase(dir_path or cwd, bare)
|
| +
|
| +
|
| +def main():
|
| + usage = """Usage: python %prog [--root <root>] [tocheck]
|
| + tocheck Specifies the directory, relative to root, to check. This defaults
|
| + to "." so it checks everything.
|
| +
|
| +Examples:
|
| + python %prog
|
| + python %prog --root /path/to/source chrome"""
|
| +
|
| + parser = optparse.OptionParser(usage=usage)
|
| + parser.add_option(
|
| + '--root',
|
| + help='Specifies the repository root. This defaults '
|
| + 'to the checkout repository root')
|
| + parser.add_option(
|
| + '-v', '--verbose', action='count', default=0, help='Print debug logging')
|
| + parser.add_option(
|
| + '--bare',
|
| + action='store_true',
|
| + default=False,
|
| + help='Prints the bare filename triggering the checks')
|
| + parser.add_option(
|
| + '--file', action='append', dest='files',
|
| + help='Specifics a list of files to check the permissions of. Only these '
|
| + 'files will be checked')
|
| + parser.add_option('--json', help='Path to JSON output file')
|
| + options, args = parser.parse_args()
|
| +
|
| + levels = [logging.ERROR, logging.INFO, logging.DEBUG]
|
| + logging.basicConfig(level=levels[min(len(levels) - 1, options.verbose)])
|
| +
|
| + if len(args) > 1:
|
| + parser.error('Too many arguments used')
|
| +
|
| + if options.root:
|
| + options.root = os.path.abspath(options.root)
|
| +
|
| + if options.files:
|
| + # --file implies --bare (for PRESUBMIT.py).
|
| + options.bare = True
|
| +
|
| + errors = check_files(options.root, options.files)
|
| + else:
|
| + api = get_scm(options.root, options.bare)
|
| + start_dir = args[0] if args else api.root_dir
|
| + errors = api.check(start_dir)
|
| +
|
| + if not options.bare:
|
| + print('Processed %s files, %d files where tested for shebang/ELF '
|
| + 'header' % (api.count, api.count_read_header))
|
| +
|
| + if options.json:
|
| + with open(options.json, 'w') as f:
|
| + json.dump(errors, f)
|
| +
|
| + if errors:
|
| + if options.bare:
|
| + print '\n'.join(e['full_path'] for e in errors)
|
| + else:
|
| + print '\nFAILED\n'
|
| + print '\n'.join('%s: %s' % (e['full_path'], e['error']) for e in errors)
|
| + return 1
|
| + if not options.bare:
|
| + print '\nSUCCESS\n'
|
| + return 0
|
| +
|
| +
|
| +if '__main__' == __name__:
|
| + sys.exit(main())
|
|
|