1"""
2Platform independent versions of some os/os.path functions. Gets around PY2's
3lack of support for reading NTFS links.
4"""
5
6
7import logging
8import os
9import posixpath
10import re
11from collections.abc import Iterable
12
13import salt.utils.args
14import salt.utils.platform
15import salt.utils.stringutils
16from salt.exceptions import CommandNotFoundError
17from salt.utils.decorators.jinja import jinja_filter
18
19try:
20    import win32file
21
22    HAS_WIN32FILE = True
23except ImportError:
24    HAS_WIN32FILE = False
25
26log = logging.getLogger(__name__)
27
28
29def islink(path):
30    """
31    Equivalent to os.path.islink()
32    """
33    return os.path.islink(path)
34
35
36def readlink(path):
37    """
38    Equivalent to os.readlink()
39    """
40    base = os.readlink(path)
41    if salt.utils.platform.is_windows():
42        # Python 3.8 added support for directory junctions which prefixes the
43        # return with `\\?\`. We need to strip that off.
44        # https://docs.python.org/3/library/os.html#os.readlink
45        if base.startswith("\\\\?\\"):
46            base = base[4:]
47    return base
48
49
50def _is_reparse_point(path):
51    """
52    Returns True if path is a reparse point; False otherwise.
53    """
54    result = win32file.GetFileAttributesW(path)
55
56    if result == -1:
57        return False
58
59    return True if result & 0x400 else False
60
61
62def _get_reparse_data(path):
63    """
64    Retrieves the reparse point data structure for the given path.
65
66    If the path is not a reparse point, None is returned.
67
68    See http://msdn.microsoft.com/en-us/library/ff552012.aspx for details on the
69    REPARSE_DATA_BUFFER structure returned.
70    """
71    # ensure paths are using the right slashes
72    path = os.path.normpath(path)
73
74    if not _is_reparse_point(path):
75        return None
76
77    fileHandle = None
78    try:
79        fileHandle = win32file.CreateFileW(
80            path,
81            0x80000000,  # GENERIC_READ
82            1,  # share with other readers
83            None,  # no inherit, default security descriptor
84            3,  # OPEN_EXISTING
85            0x00200000
86            | 0x02000000,  # FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS
87        )
88
89        reparseData = win32file.DeviceIoControl(
90            fileHandle,
91            0x900A8,  # FSCTL_GET_REPARSE_POINT
92            None,  # in buffer
93            16384,  # out buffer size (MAXIMUM_REPARSE_DATA_BUFFER_SIZE)
94        )
95
96    finally:
97        if fileHandle:
98            win32file.CloseHandle(fileHandle)
99
100    return reparseData
101
102
103@jinja_filter("which")
104def which(exe=None):
105    """
106    Python clone of /usr/bin/which
107    """
108
109    if not exe:
110        log.error("No executable was passed to be searched by salt.utils.path.which()")
111        return None
112
113    ## define some utilities (we use closures here because our predecessor used them)
114    def is_executable_common(path):
115        """
116        This returns truth if posixy semantics (which python simulates on
117        windows) states that this is executable.
118        """
119        return os.path.isfile(path) and os.access(path, os.X_OK)
120
121    def resolve(path):
122        """
123        This will take a path and recursively follow the link until we get to a
124        real file.
125        """
126        while os.path.islink(path):
127            res = readlink(path)
128
129            # if the link points to a relative target, then convert it to an
130            # absolute path relative to the original path
131            if not os.path.isabs(res):
132                directory, _ = os.path.split(path)
133                res = join(directory, res)
134            path = res
135        return path
136
137    # windows-only
138    def has_executable_ext(path, ext_membership):
139        """
140        Extract the extension from the specified path, lowercase it so we
141        can be insensitive, and then check it against the available exts.
142        """
143        p, ext = os.path.splitext(path)
144        return ext.lower() in ext_membership
145
146    ## prepare related variables from the environment
147    res = salt.utils.stringutils.to_unicode(os.environ.get("PATH", ""))
148    system_path = res.split(os.pathsep)
149
150    # add some reasonable defaults in case someone's PATH is busted
151    if not salt.utils.platform.is_windows():
152        res = set(system_path)
153        extended_path = [
154            "/sbin",
155            "/bin",
156            "/usr/sbin",
157            "/usr/bin",
158            "/usr/local/sbin",
159            "/usr/local/bin",
160        ]
161        system_path.extend([p for p in extended_path if p not in res])
162
163    ## now to define the semantics of what's considered executable on a given platform
164    if salt.utils.platform.is_windows():
165        # executable semantics on windows requires us to search PATHEXT
166        res = salt.utils.stringutils.to_str(os.environ.get("PATHEXT", ".EXE"))
167
168        # generate two variables, one of them for O(n) searches (but ordered)
169        # and another for O(1) searches. the previous guy was trying to use
170        # memoization with a function that has no arguments, this provides
171        # the exact same benefit
172        pathext = res.split(os.pathsep)
173        res = {ext.lower() for ext in pathext}
174
175        # check if our caller already specified a valid extension as then we don't need to match it
176        _, ext = os.path.splitext(exe)
177        if ext.lower() in res:
178            pathext = [""]
179
180            is_executable = is_executable_common
181
182        # The specified extension isn't valid, so we just assume it's part of the
183        # filename and proceed to walk the pathext list
184        else:
185            is_executable = lambda path, membership=res: is_executable_common(
186                path
187            ) and has_executable_ext(path, membership)
188
189    else:
190        # in posix, there's no such thing as file extensions..only zuul
191        pathext = [""]
192
193        # executable semantics are pretty simple on reasonable platforms...
194        is_executable = is_executable_common
195
196    ## search for the executable
197
198    # check to see if the full path was specified as then we don't need
199    # to actually walk the system_path for any reason
200    if is_executable(exe):
201        return exe
202
203    # now to search through our system_path
204    for path in system_path:
205        p = join(path, exe)
206
207        # iterate through all extensions to see which one is executable
208        for ext in pathext:
209            pext = p + ext
210            rp = resolve(pext)
211            if is_executable(rp):
212                return p + ext
213            continue
214        continue
215
216    ## if something was executable, we should've found it already...
217    log.trace(
218        "'%s' could not be found in the following search path: '%s'", exe, system_path
219    )
220    return None
221
222
223def which_bin(exes):
224    """
225    Scan over some possible executables and return the first one that is found
226    """
227    if not isinstance(exes, Iterable):
228        return None
229    for exe in exes:
230        path = which(exe)
231        if not path:
232            continue
233        return path
234    return None
235
236
237@jinja_filter("path_join")
238def join(*parts, **kwargs):
239    """
240    This functions tries to solve some issues when joining multiple absolute
241    paths on both *nix and windows platforms.
242
243    See tests/unit/utils/path_join_test.py for some examples on what's being
244    talked about here.
245
246    The "use_posixpath" kwarg can be be used to force joining using poxixpath,
247    which is useful for Salt fileserver paths on Windows masters.
248    """
249    parts = [salt.utils.stringutils.to_str(part) for part in parts]
250
251    kwargs = salt.utils.args.clean_kwargs(**kwargs)
252    use_posixpath = kwargs.pop("use_posixpath", False)
253    if kwargs:
254        salt.utils.args.invalid_kwargs(kwargs)
255
256    pathlib = posixpath if use_posixpath else os.path
257
258    # Normalize path converting any os.sep as needed
259    parts = [pathlib.normpath(p) for p in parts]
260
261    try:
262        root = parts.pop(0)
263    except IndexError:
264        # No args passed to func
265        return ""
266
267    root = salt.utils.stringutils.to_unicode(root)
268    if not parts:
269        ret = root
270    else:
271        stripped = [p.lstrip(os.sep) for p in parts]
272        ret = pathlib.join(root, *salt.utils.data.decode(stripped))
273    return pathlib.normpath(ret)
274
275
276def check_or_die(command):
277    """
278    Simple convenience function for modules to use for gracefully blowing up
279    if a required tool is not available in the system path.
280
281    Lazily import `salt.modules.cmdmod` to avoid any sort of circular
282    dependencies.
283    """
284    if command is None:
285        raise CommandNotFoundError("'None' is not a valid command.")
286
287    if not which(command):
288        raise CommandNotFoundError("'{}' is not in the path".format(command))
289
290
291def sanitize_win_path(winpath):
292    """
293    Remove illegal path characters for windows
294    """
295    intab = "<>:|?*"
296    if isinstance(winpath, str):
297        winpath = winpath.translate({ord(c): "_" for c in intab})
298    elif isinstance(winpath, str):
299        outtab = "_" * len(intab)
300        trantab = "".maketrans(intab, outtab)
301        winpath = winpath.translate(trantab)
302    return winpath
303
304
305def safe_path(path, allow_path=None):
306    r"""
307    .. versionadded:: 2017.7.3
308
309    Checks that the path is safe for modification by Salt. For example, you
310    wouldn't want to have salt delete the contents of ``C:\Windows``. The
311    following directories are considered unsafe:
312
313    - C:\, D:\, E:\, etc.
314    - \
315    - C:\Windows
316
317    Args:
318
319        path (str): The path to check
320
321        allow_paths (str, list): A directory or list of directories inside of
322            path that may be safe. For example: ``C:\Windows\TEMP``
323
324    Returns:
325        bool: True if safe, otherwise False
326    """
327    # Create regex definitions for directories that may be unsafe to modify
328    system_root = os.environ.get("SystemRoot", "C:\\Windows")
329    deny_paths = (
330        r"[a-z]\:\\$",  # C:\, D:\, etc
331        r"\\$",  # \
332        re.escape(system_root),  # C:\Windows
333    )
334
335    # Make allow_path a list
336    if allow_path and not isinstance(allow_path, list):
337        allow_path = [allow_path]
338
339    # Create regex definition for directories we may want to make exceptions for
340    allow_paths = list()
341    if allow_path:
342        for item in allow_path:
343            allow_paths.append(re.escape(item))
344
345    # Check the path to make sure it's not one of the bad paths
346    good_path = True
347    for d_path in deny_paths:
348        if re.match(d_path, path, flags=re.IGNORECASE) is not None:
349            # Found deny path
350            good_path = False
351
352    # If local_dest is one of the bad paths, check for exceptions
353    if not good_path:
354        for a_path in allow_paths:
355            if re.match(a_path, path, flags=re.IGNORECASE) is not None:
356                # Found exception
357                good_path = True
358
359    return good_path
360
361
362def os_walk(top, *args, **kwargs):
363    """
364    This is a helper than ensures that all paths returned from os.walk are
365    unicode.
366    """
367    top_query = salt.utils.stringutils.to_str(top)
368    for item in os.walk(top_query, *args, **kwargs):
369        yield salt.utils.data.decode(item, preserve_tuples=True)
370