diff options
Diffstat (limited to 'Tools/c-analyzer/c_common/fsutil.py')
-rw-r--r-- | Tools/c-analyzer/c_common/fsutil.py | 388 |
1 files changed, 388 insertions, 0 deletions
diff --git a/Tools/c-analyzer/c_common/fsutil.py b/Tools/c-analyzer/c_common/fsutil.py new file mode 100644 index 00000000000..56023f33523 --- /dev/null +++ b/Tools/c-analyzer/c_common/fsutil.py @@ -0,0 +1,388 @@ +import fnmatch +import glob +import os +import os.path +import shutil +import stat + +from .iterutil import iter_many + + +C_SOURCE_SUFFIXES = ('.c', '.h') + + +def create_backup(old, backup=None): + if isinstance(old, str): + filename = old + else: + filename = getattr(old, 'name', None) + if not filename: + return None + if not backup or backup is True: + backup = f'{filename}.bak' + try: + shutil.copyfile(filename, backup) + except FileNotFoundError as exc: + if exc.filename != filename: + raise # re-raise + backup = None + return backup + + +################################## +# find files + +def match_glob(filename, pattern): + if fnmatch.fnmatch(filename, pattern): + return True + + # fnmatch doesn't handle ** quite right. It will not match the + # following: + # + # ('x/spam.py', 'x/**/*.py') + # ('spam.py', '**/*.py') + # + # though it *will* match the following: + # + # ('x/y/spam.py', 'x/**/*.py') + # ('x/spam.py', '**/*.py') + + if '**/' not in pattern: + return False + + # We only accommodate the single-"**" case. + return fnmatch.fnmatch(filename, pattern.replace('**/', '', 1)) + + +def iter_filenames(filenames, *, + start=None, + include=None, + exclude=None, + ): + onempty = Exception('no filenames provided') + for filename, solo in iter_many(filenames, onempty): + check, start = _get_check(filename, start, include, exclude) + yield filename, check, solo +# filenames = iter(filenames or ()) +# try: +# first = next(filenames) +# except StopIteration: +# raise Exception('no filenames provided') +# try: +# second = next(filenames) +# except StopIteration: +# check, _ = _get_check(first, start, include, exclude) +# yield first, check, False +# return +# +# check, start = _get_check(first, start, include, exclude) +# yield first, check, True +# check, start = _get_check(second, start, include, exclude) +# yield second, check, True +# for filename in filenames: +# check, start = _get_check(filename, start, include, exclude) +# yield filename, check, True + + +def expand_filenames(filenames): + for filename in filenames: + # XXX Do we need to use glob.escape (a la commit 9355868458, GH-20994)? + if '**/' in filename: + yield from glob.glob(filename.replace('**/', '')) + yield from glob.glob(filename) + + +def _get_check(filename, start, include, exclude): + if start and filename != start: + return (lambda: '<skipped>'), start + else: + def check(): + if _is_excluded(filename, exclude, include): + return '<excluded>' + return None + return check, None + + +def _is_excluded(filename, exclude, include): + if include: + for included in include: + if match_glob(filename, included): + return False + return True + elif exclude: + for excluded in exclude: + if match_glob(filename, excluded): + return True + return False + else: + return False + + +def _walk_tree(root, *, + _walk=os.walk, + ): + # A wrapper around os.walk that resolves the filenames. + for parent, _, names in _walk(root): + for name in names: + yield os.path.join(parent, name) + + +def walk_tree(root, *, + suffix=None, + walk=_walk_tree, + ): + """Yield each file in the tree under the given directory name. + + If "suffix" is provided then only files with that suffix will + be included. + """ + if suffix and not isinstance(suffix, str): + raise ValueError('suffix must be a string') + + for filename in walk(root): + if suffix and not filename.endswith(suffix): + continue + yield filename + + +def glob_tree(root, *, + suffix=None, + _glob=glob.iglob, + ): + """Yield each file in the tree under the given directory name. + + If "suffix" is provided then only files with that suffix will + be included. + """ + suffix = suffix or '' + if not isinstance(suffix, str): + raise ValueError('suffix must be a string') + + for filename in _glob(f'{root}/*{suffix}'): + yield filename + for filename in _glob(f'{root}/**/*{suffix}'): + yield filename + + +def iter_files(root, suffix=None, relparent=None, *, + get_files=os.walk, + _glob=glob_tree, + _walk=walk_tree, + ): + """Yield each file in the tree under the given directory name. + + If "root" is a non-string iterable then do the same for each of + those trees. + + If "suffix" is provided then only files with that suffix will + be included. + + if "relparent" is provided then it is used to resolve each + filename as a relative path. + """ + if not isinstance(root, str): + roots = root + for root in roots: + yield from iter_files(root, suffix, relparent, + get_files=get_files, + _glob=_glob, _walk=_walk) + return + + # Use the right "walk" function. + if get_files in (glob.glob, glob.iglob, glob_tree): + get_files = _glob + else: + _files = _walk_tree if get_files in (os.walk, walk_tree) else get_files + get_files = (lambda *a, **k: _walk(*a, walk=_files, **k)) + + # Handle a single suffix. + if suffix and not isinstance(suffix, str): + filenames = get_files(root) + suffix = tuple(suffix) + else: + filenames = get_files(root, suffix=suffix) + suffix = None + + for filename in filenames: + if suffix and not isinstance(suffix, str): # multiple suffixes + if not filename.endswith(suffix): + continue + if relparent: + filename = os.path.relpath(filename, relparent) + yield filename + + +def iter_files_by_suffix(root, suffixes, relparent=None, *, + walk=walk_tree, + _iter_files=iter_files, + ): + """Yield each file in the tree that has the given suffixes. + + Unlike iter_files(), the results are in the original suffix order. + """ + if isinstance(suffixes, str): + suffixes = [suffixes] + # XXX Ignore repeated suffixes? + for suffix in suffixes: + yield from _iter_files(root, suffix, relparent) + + +################################## +# file info + +# XXX posix-only? + +S_IRANY = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH +S_IWANY = stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH +S_IXANY = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + + +def is_readable(file, *, user=None, check=False): + filename, st, mode = _get_file_info(file) + if check: + try: + okay = _check_file(filename, S_IRANY) + except NotImplementedError: + okay = NotImplemented + if okay is not NotImplemented: + return okay + # Fall back to checking the mode. + return _check_mode(st, mode, S_IRANY, user) + + +def is_writable(file, *, user=None, check=False): + filename, st, mode = _get_file_info(file) + if check: + try: + okay = _check_file(filename, S_IWANY) + except NotImplementedError: + okay = NotImplemented + if okay is not NotImplemented: + return okay + # Fall back to checking the mode. + return _check_mode(st, mode, S_IWANY, user) + + +def is_executable(file, *, user=None, check=False): + filename, st, mode = _get_file_info(file) + if check: + try: + okay = _check_file(filename, S_IXANY) + except NotImplementedError: + okay = NotImplemented + if okay is not NotImplemented: + return okay + # Fall back to checking the mode. + return _check_mode(st, mode, S_IXANY, user) + + +def _get_file_info(file): + filename = st = mode = None + if isinstance(file, int): + mode = file + elif isinstance(file, os.stat_result): + st = file + else: + if isinstance(file, str): + filename = file + elif hasattr(file, 'name') and os.path.exists(file.name): + filename = file.name + else: + raise NotImplementedError(file) + st = os.stat(filename) + return filename, st, mode or st.st_mode + + +def _check_file(filename, check): + if not isinstance(filename, str): + raise Exception(f'filename required to check file, got {filename}') + if check & S_IRANY: + flags = os.O_RDONLY + elif check & S_IWANY: + flags = os.O_WRONLY + elif check & S_IXANY: + # We can worry about S_IXANY later + return NotImplemented + else: + raise NotImplementedError(check) + + try: + fd = os.open(filename, flags) + except PermissionError: + return False + # We do not ignore other exceptions. + else: + os.close(fd) + return True + + +def _get_user_info(user): + import pwd + username = uid = gid = groups = None + if user is None: + uid = os.geteuid() + #username = os.getlogin() + username = pwd.getpwuid(uid)[0] + gid = os.getgid() + groups = os.getgroups() + else: + if isinstance(user, int): + uid = user + entry = pwd.getpwuid(uid) + username = entry.pw_name + elif isinstance(user, str): + username = user + entry = pwd.getpwnam(username) + uid = entry.pw_uid + else: + raise NotImplementedError(user) + gid = entry.pw_gid + os.getgrouplist(username, gid) + return username, uid, gid, groups + + +def _check_mode(st, mode, check, user): + orig = check + _, uid, gid, groups = _get_user_info(user) + if check & S_IRANY: + check -= S_IRANY + matched = False + if mode & stat.S_IRUSR: + if st.st_uid == uid: + matched = True + if mode & stat.S_IRGRP: + if st.st_uid == gid or st.st_uid in groups: + matched = True + if mode & stat.S_IROTH: + matched = True + if not matched: + return False + if check & S_IWANY: + check -= S_IWANY + matched = False + if mode & stat.S_IWUSR: + if st.st_uid == uid: + matched = True + if mode & stat.S_IWGRP: + if st.st_uid == gid or st.st_uid in groups: + matched = True + if mode & stat.S_IWOTH: + matched = True + if not matched: + return False + if check & S_IXANY: + check -= S_IXANY + matched = False + if mode & stat.S_IXUSR: + if st.st_uid == uid: + matched = True + if mode & stat.S_IXGRP: + if st.st_uid == gid or st.st_uid in groups: + matched = True + if mode & stat.S_IXOTH: + matched = True + if not matched: + return False + if check: + raise NotImplementedError((orig, check)) + return True |