1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5"""
6Like :py:mod:`os.path`, with a reduced set of functions, and with normalized path
7separators (always use forward slashes).
8Also contains a few additional utilities not found in :py:mod:`os.path`.
9"""
10
11# Imported from
12# https://searchfox.org/mozilla-central/rev/c3ebaf6de2d481c262c04bb9657eaf76bf47e2ac/python/mozbuild/mozpack/path.py
13
14
15import posixpath
16import os
17import re
18
19
20def normsep(path):
21    """
22    Normalize path separators, by using forward slashes instead of whatever
23    :py:const:`os.sep` is.
24    """
25    if os.sep != "/":
26        path = path.replace(os.sep, "/")
27    if os.altsep and os.altsep != "/":
28        path = path.replace(os.altsep, "/")
29    return path
30
31
32def relpath(path, start):
33    rel = normsep(os.path.relpath(path, start))
34    return "" if rel == "." else rel
35
36
37def realpath(path):
38    return normsep(os.path.realpath(path))
39
40
41def abspath(path):
42    return normsep(os.path.abspath(path))
43
44
45def join(*paths):
46    return normsep(os.path.join(*paths))
47
48
49def normpath(path):
50    return posixpath.normpath(normsep(path))
51
52
53def dirname(path):
54    return posixpath.dirname(normsep(path))
55
56
57def commonprefix(paths):
58    return posixpath.commonprefix([normsep(path) for path in paths])
59
60
61def basename(path):
62    return os.path.basename(path)
63
64
65def splitext(path):
66    return posixpath.splitext(normsep(path))
67
68
69def split(path):
70    """
71    Return the normalized path as a list of its components.
72
73        ``split('foo/bar/baz')`` returns ``['foo', 'bar', 'baz']``
74    """
75    return normsep(path).split("/")
76
77
78def basedir(path, bases):
79    """
80    Given a list of directories (`bases`), return which one contains the given
81    path. If several matches are found, the deepest base directory is returned.
82
83        ``basedir('foo/bar/baz', ['foo', 'baz', 'foo/bar'])`` returns ``'foo/bar'``
84        (`'foo'` and `'foo/bar'` both match, but `'foo/bar'` is the deepest match)
85    """
86    path = normsep(path)
87    bases = [normsep(b) for b in bases]
88    if path in bases:
89        return path
90    for b in sorted(bases, reverse=True):
91        if b == "" or path.startswith(b + "/"):
92            return b
93
94
95re_cache = {}
96# Python versions < 3.7 return r'\/' for re.escape('/').
97if re.escape("/") == "/":
98    MATCH_STAR_STAR_RE = re.compile(r"(^|/)\\\*\\\*/")
99    MATCH_STAR_STAR_END_RE = re.compile(r"(^|/)\\\*\\\*$")
100else:
101    MATCH_STAR_STAR_RE = re.compile(r"(^|\\\/)\\\*\\\*\\\/")
102    MATCH_STAR_STAR_END_RE = re.compile(r"(^|\\\/)\\\*\\\*$")
103
104
105def match(path, pattern):
106    """
107    Return whether the given path matches the given pattern.
108    An asterisk can be used to match any string, including the null string, in
109    one part of the path:
110
111        ``foo`` matches ``*``, ``f*`` or ``fo*o``
112
113    However, an asterisk matching a subdirectory may not match the null string:
114
115        ``foo/bar`` does *not* match ``foo/*/bar``
116
117    If the pattern matches one of the ancestor directories of the path, the
118    patch is considered matching:
119
120        ``foo/bar`` matches ``foo``
121
122    Two adjacent asterisks can be used to match files and zero or more
123    directories and subdirectories.
124
125        ``foo/bar`` matches ``foo/**/bar``, or ``**/bar``
126    """
127    if not pattern:
128        return True
129    if pattern not in re_cache:
130        p = re.escape(pattern)
131        p = MATCH_STAR_STAR_RE.sub(r"\1(?:.+/)?", p)
132        p = MATCH_STAR_STAR_END_RE.sub(r"(?:\1.+)?", p)
133        p = p.replace(r"\*", "[^/]*") + "(?:/.*)?$"
134        re_cache[pattern] = re.compile(p)
135    return re_cache[pattern].match(path) is not None
136
137
138def rebase(oldbase, base, relativepath):
139    """
140    Return `relativepath` relative to `base` instead of `oldbase`.
141    """
142    if base == oldbase:
143        return relativepath
144    if len(base) < len(oldbase):
145        assert basedir(oldbase, [base]) == base
146        relbase = relpath(oldbase, base)
147        result = join(relbase, relativepath)
148    else:
149        assert basedir(base, [oldbase]) == oldbase
150        relbase = relpath(base, oldbase)
151        result = relpath(relativepath, relbase)
152    result = normpath(result)
153    if relativepath.endswith("/") and not result.endswith("/"):
154        result += "/"
155    return result
156
157
158def ancestors(path):
159    """Emit the parent directories of a path.
160
161    Args:
162        path (str): Path to emit parents of.
163
164    Yields:
165        str: Path of parent directory.
166    """
167    while path:
168        yield path
169        newpath = os.path.dirname(path)
170        if newpath == path:
171            break
172        path = newpath
173