2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
6 """Makes sure files have the right permissions.
8 Some developers have broken SCM configurations that flip the svn:executable
9 permission on for no good reason. Unix developers who run ls --color will then
10 see .cc files in green and get confused.
12 - For file extensions that must be executable, add it to EXECUTABLE_EXTENSIONS.
13 - For file extensions that must not be executable, add it to
14 NOT_EXECUTABLE_EXTENSIONS.
15 - To ignore all the files inside a directory, add it to IGNORED_PATHS.
16 - For file base name with ambiguous state and that should not be checked for
17 shebang, add it to IGNORED_FILENAMES.
19 Any file not matching the above will be opened and looked if it has a shebang
20 or an ELF header. If this does not match the executable bit on the file, the
23 Note that all directory separators must be slashes (Unix-style) and not
24 backslashes. All directories should be relative to the source root and all
25 file paths should be only lowercase.
37 #### USER EDITABLE SECTION STARTS HERE ####
39 # Files with these extensions must have executable bit set.
42 EXECUTABLE_EXTENSIONS = (
49 # These files must have executable bit set.
51 # Case-insensitive, lower-case only.
53 'chrome/test/data/app_shim/app_shim_32_bit.app/contents/'
54 'macos/app_mode_loader',
55 'chrome/test/data/extensions/uitest/plugins/plugin.plugin/contents/'
56 'macos/testnetscapeplugin',
57 'chrome/test/data/extensions/uitest/plugins_private/plugin.plugin/contents/'
58 'macos/testnetscapeplugin',
61 # These files must not have the executable bit set. This is mainly a performance
62 # optimization as these files are not checked for shebang. The list was
63 # partially generated from:
64 # git ls-files | grep "\\." | sed 's/.*\.//' | sort | uniq -c | sort -b -g
67 NON_EXECUTABLE_EXTENSIONS = (
138 # These files must not have executable bit set.
140 # Case-insensitive, lower-case only.
141 NON_EXECUTABLE_PATHS = (
142 'build/android/tests/symbolize/liba.so',
143 'build/android/tests/symbolize/libb.so',
144 'chrome/installer/mac/sign_app.sh.in',
145 'chrome/installer/mac/sign_versioned_dir.sh.in',
146 'chrome/test/data/components/ihfokbkgjpifnbbojhneepfflplebdkc/'
147 'ihfokbkgjpifnbbojhneepfflplebdkc_1/a_changing_binary_file',
148 'chrome/test/data/components/ihfokbkgjpifnbbojhneepfflplebdkc/'
149 'ihfokbkgjpifnbbojhneepfflplebdkc_2/a_changing_binary_file',
150 'chrome/test/data/extensions/uitest/plugins/plugin32.so',
151 'chrome/test/data/extensions/uitest/plugins/plugin64.so',
152 'chrome/test/data/extensions/uitest/plugins_private/plugin32.so',
153 'chrome/test/data/extensions/uitest/plugins_private/plugin64.so',
154 'courgette/testdata/elf-32-1',
155 'courgette/testdata/elf-32-2',
156 'courgette/testdata/elf-64',
159 # File names that are always whitelisted. (These are mostly autoconf spew.)
162 IGNORED_FILENAMES = (
174 # File paths starting with one of these will be ignored as well.
175 # Please consider fixing your file permissions, rather than adding to this list.
177 # Case-insensitive, lower-case only.
179 'native_client_sdk/src/build_tools/sdk_tools/third_party/fancy_urllib/'
182 # TODO(maruel): Fix these.
183 'third_party/android_testrunner/',
184 'third_party/bintrees/',
185 'third_party/closure_linter/',
186 'third_party/devscripts/licensecheck.pl.vanilla',
187 'third_party/hyphen/',
188 'third_party/jemalloc/',
189 'third_party/lcov-1.9/contrib/galaxy/conglomerate_functions.pl',
190 'third_party/lcov-1.9/contrib/galaxy/gen_makefile.sh',
191 'third_party/lcov/contrib/galaxy/conglomerate_functions.pl',
192 'third_party/lcov/contrib/galaxy/gen_makefile.sh',
193 'third_party/libevent/autogen.sh',
194 'third_party/libevent/test/test.sh',
195 'third_party/libxml/linux/xml2-config',
196 'third_party/libxml/src/ltmain.sh',
198 'third_party/protobuf/',
199 'third_party/python_gflags/gflags.py',
200 'third_party/sqlite/',
201 'third_party/talloc/script/mksyms.sh',
202 'third_party/tcmalloc/',
203 'third_party/tlslite/setup.py',
206 #### USER EDITABLE SECTION ENDS HERE ####
208 assert set(EXECUTABLE_EXTENSIONS) & set(NON_EXECUTABLE_EXTENSIONS) == set()
209 assert set(EXECUTABLE_PATHS) & set(NON_EXECUTABLE_PATHS) == set()
211 VALID_CHARS = set(string.ascii_lowercase + string.digits + '/-_.')
212 for paths in (EXECUTABLE_PATHS, NON_EXECUTABLE_PATHS, IGNORED_PATHS):
213 assert all([set(path).issubset(VALID_CHARS) for path in paths])
216 def capture(cmd, cwd):
217 """Returns the output of a command.
219 Ignores the error code or stderr.
221 logging.debug('%s; cwd=%s' % (' '.join(cmd), cwd))
222 env = os.environ.copy()
223 env['LANGUAGE'] = 'en_US.UTF-8'
224 p = subprocess.Popen(
225 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd, env=env)
226 return p.communicate()[0]
229 def get_svn_info(dir_path):
230 """Returns svn meta-data for a svn checkout."""
231 if not os.path.isdir(dir_path):
233 out = capture(['svn', 'info', '.', '--non-interactive'], dir_path)
234 return dict(l.split(': ', 1) for l in out.splitlines() if l)
237 def get_svn_url(dir_path):
238 return get_svn_info(dir_path).get('URL')
241 def get_svn_root(dir_path):
242 """Returns the svn checkout root or None."""
243 svn_url = get_svn_url(dir_path)
246 logging.info('svn url: %s' % svn_url)
248 parent = os.path.dirname(dir_path)
249 if parent == dir_path:
251 svn_url = svn_url.rsplit('/', 1)[0]
252 if svn_url != get_svn_url(parent):
257 def get_git_root(dir_path):
258 """Returns the git checkout root or None."""
259 root = capture(['git', 'rev-parse', '--show-toplevel'], dir_path).strip()
264 def is_ignored(rel_path):
265 """Returns True if rel_path is in our whitelist of files to ignore."""
266 rel_path = rel_path.lower()
268 os.path.basename(rel_path) in IGNORED_FILENAMES or
269 rel_path.lower().startswith(IGNORED_PATHS))
272 def must_be_executable(rel_path):
273 """The file name represents a file type that must have the executable bit
276 return (os.path.splitext(rel_path)[1][1:] in EXECUTABLE_EXTENSIONS or
277 rel_path.lower() in EXECUTABLE_PATHS)
280 def must_not_be_executable(rel_path):
281 """The file name represents a file type that must not have the executable
284 return (os.path.splitext(rel_path)[1][1:] in NON_EXECUTABLE_EXTENSIONS or
285 rel_path.lower() in NON_EXECUTABLE_PATHS)
288 def has_executable_bit(full_path):
289 """Returns if any executable bit is set."""
290 permission = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
291 return bool(permission & os.stat(full_path).st_mode)
294 def has_shebang_or_is_elf(full_path):
295 """Returns if the file starts with #!/ or is an ELF binary.
297 full_path is the absolute path to the file.
299 with open(full_path, 'rb') as f:
301 return (data[:3] == '#!/', data == '\x7fELF')
304 def check_file(root_path, rel_path):
305 """Checks the permissions of the file whose path is root_path + rel_path and
306 returns an error if it is inconsistent. Returns None on success.
308 It is assumed that the file is not ignored by is_ignored().
310 If the file name is matched with must_be_executable() or
311 must_not_be_executable(), only its executable bit is checked.
312 Otherwise, the first few bytes of the file are read to verify if it has a
313 shebang or ELF header and compares this with the executable bit on the file.
315 full_path = os.path.join(root_path, rel_path)
316 def result_dict(error):
319 'full_path': full_path,
320 'rel_path': rel_path,
323 bit = has_executable_bit(full_path)
325 # It's faster to catch exception than call os.path.islink(). Chromium
326 # tree happens to have invalid symlinks under
327 # third_party/openssl/openssl/test/.
330 if must_be_executable(rel_path):
332 return result_dict('Must have executable bit set')
334 if must_not_be_executable(rel_path):
336 return result_dict('Must not have executable bit set')
339 # For the others, it depends on the file header.
340 (shebang, elf) = has_shebang_or_is_elf(full_path)
341 if bit != (shebang or elf):
343 return result_dict('Has executable bit but not shebang or ELF header')
345 return result_dict('Has shebang but not executable bit')
346 return result_dict('Has ELF header but not executable bit')
349 def check_files(root, files):
350 gen = (check_file(root, f) for f in files if not is_ignored(f))
351 return filter(None, gen)
354 class ApiBase(object):
355 def __init__(self, root_dir, bare_output):
356 self.root_dir = root_dir
357 self.bare_output = bare_output
359 self.count_read_header = 0
361 def check_file(self, rel_path):
362 logging.debug('check_file(%s)' % rel_path)
365 if (not must_be_executable(rel_path) and
366 not must_not_be_executable(rel_path)):
367 self.count_read_header += 1
369 return check_file(self.root_dir, rel_path)
371 def check_dir(self, rel_path):
372 return self.check(rel_path)
374 def check(self, start_dir):
375 """Check the files in start_dir, recursively check its subdirectories."""
377 items = self.list_dir(start_dir)
378 logging.info('check(%s) -> %d' % (start_dir, len(items)))
380 full_path = os.path.join(self.root_dir, start_dir, item)
381 rel_path = full_path[len(self.root_dir) + 1:]
382 if is_ignored(rel_path):
384 if os.path.isdir(full_path):
386 errors.extend(self.check_dir(rel_path))
388 error = self.check_file(rel_path)
393 def list_dir(self, start_dir):
394 """Lists all the files and directory inside start_dir."""
396 x for x in os.listdir(os.path.join(self.root_dir, start_dir))
397 if not x.startswith('.')
401 class ApiSvnQuick(ApiBase):
402 """Returns all files in svn-versioned directories, independent of the fact if
405 Uses svn info in each directory to determine which directories should be
408 def __init__(self, *args):
409 super(ApiSvnQuick, self).__init__(*args)
410 self.url = get_svn_url(self.root_dir)
412 def check_dir(self, rel_path):
413 url = self.url + '/' + rel_path
414 if get_svn_url(os.path.join(self.root_dir, rel_path)) != url:
416 return super(ApiSvnQuick, self).check_dir(rel_path)
419 class ApiAllFilesAtOnceBase(ApiBase):
422 def list_dir(self, start_dir):
423 """Lists all the files and directory inside start_dir."""
424 if self._files is None:
425 self._files = sorted(self._get_all_files())
426 if not self.bare_output:
427 print 'Found %s files' % len(self._files)
428 start_dir = start_dir[len(self.root_dir) + 1:]
430 x[len(start_dir):] for x in self._files if x.startswith(start_dir)
433 def _get_all_files(self):
434 """Lists all the files and directory inside self._root_dir."""
435 raise NotImplementedError()
438 class ApiSvn(ApiAllFilesAtOnceBase):
439 """Returns all the subversion controlled files.
441 Warning: svn ls is abnormally slow.
443 def _get_all_files(self):
444 cmd = ['svn', 'ls', '--non-interactive', '--recursive']
446 x for x in capture(cmd, self.root_dir).splitlines()
447 if not x.endswith(os.path.sep))
450 class ApiGit(ApiAllFilesAtOnceBase):
451 def _get_all_files(self):
452 return capture(['git', 'ls-files'], cwd=self.root_dir).splitlines()
455 def get_scm(dir_path, bare):
456 """Returns a properly configured ApiBase instance."""
458 root = get_svn_root(dir_path or cwd)
461 print('Found subversion checkout at %s' % root)
462 return ApiSvnQuick(dir_path or root, bare)
463 root = get_git_root(dir_path or cwd)
466 print('Found git repository at %s' % root)
467 return ApiGit(dir_path or root, bare)
469 # Returns a non-scm aware checker.
471 print('Failed to determine the SCM for %s' % dir_path)
472 return ApiBase(dir_path or cwd, bare)
476 usage = """Usage: python %prog [--root <root>] [tocheck]
477 tocheck Specifies the directory, relative to root, to check. This defaults
478 to "." so it checks everything.
482 python %prog --root /path/to/source chrome"""
484 parser = optparse.OptionParser(usage=usage)
487 help='Specifies the repository root. This defaults '
488 'to the checkout repository root')
490 '-v', '--verbose', action='count', default=0, help='Print debug logging')
495 help='Prints the bare filename triggering the checks')
497 '--file', action='append', dest='files',
498 help='Specifics a list of files to check the permissions of. Only these '
499 'files will be checked')
500 parser.add_option('--json', help='Path to JSON output file')
501 options, args = parser.parse_args()
503 levels = [logging.ERROR, logging.INFO, logging.DEBUG]
504 logging.basicConfig(level=levels[min(len(levels) - 1, options.verbose)])
507 parser.error('Too many arguments used')
510 options.root = os.path.abspath(options.root)
513 # --file implies --bare (for PRESUBMIT.py).
516 errors = check_files(options.root, options.files)
518 api = get_scm(options.root, options.bare)
519 start_dir = args[0] if args else api.root_dir
520 errors = api.check(start_dir)
523 print('Processed %s files, %d files where tested for shebang/ELF '
524 'header' % (api.count, api.count_read_header))
527 with open(options.json, 'w') as f:
532 print '\n'.join(e['full_path'] for e in errors)
535 print '\n'.join('%s: %s' % (e['full_path'], e['error']) for e in errors)
542 if '__main__' == __name__: