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