#
# Copyright (c) nexB Inc. and others. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/aboutcode-org/commoncode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#
import ntpath
import posixpath
import re
from os.path import commonprefix
from commoncode.fileutils import as_posixpath
from commoncode.fileutils import as_winpath
from commoncode.fileutils import is_posixpath
from commoncode.text import as_unicode
from commoncode.text import toascii
"""
Various path utilities such as common prefix and suffix functions, conversion
to OS-safe paths and to POSIX paths.
"""
#
# Build OS-portable and safer paths
def safe_path(path, posix=False, preserve_spaces=False, posix_only=False):
"""
Convert `path` to a safe and portable POSIX path usable on multiple OSes.
The returned path is an ASCII-only byte string, resolved for relative
segments and itself relative.
The `path` is treated as a POSIX path if `posix` is True or as a Windows
path with blackslash separators otherwise.
If `preserve_spaces` is True, then the spaces in `path` will not be replaced.
"""
# if the path is UTF, try to use unicode instead
if not isinstance(path, str):
path = as_unicode(path)
path = path.strip()
if not is_posixpath(path):
path = as_winpath(path)
posix = False
path = resolve(path, posix)
_pathmod, path_sep = path_handlers(path, posix)
segments = [s.strip() for s in path.split(path_sep) if s.strip()]
segments = [
portable_filename(s, preserve_spaces=preserve_spaces, posix_only=posix_only)
for s in segments
]
if not segments:
return "_"
# always return posix
path = "/".join(segments)
return as_posixpath(path)
def path_handlers(path, posix=True):
"""
Return a path module and path separator to use for handling (e.g. split and
join) `path` using either POSIX or Windows conventions depending on the
`path` content. Force usage of POSIX conventions if `posix` is True.
"""
# determine if we use posix or windows path handling
is_posix = is_posixpath(path)
use_posix = posix or is_posix
pathmod = use_posix and posixpath or ntpath
path_sep = "/" if use_posix else "\\"
return pathmod, path_sep
def resolve(path, posix=True):
"""
Return a resolved relative POSIX path from `path` where extra slashes
including leading and trailing slashes are removed, dot '.' and dotdot '..'
path segments have been removed or resolved as possible. When a dotdot path
segment cannot be further resolved and would be "escaping" from the provided
path "tree", it is replaced by the string 'dotdot'.
The `path` is treated as a POSIX path if `posix` is True (default) or as a
Windows path with blackslash separators otherwise.
"""
if not path:
return "."
path = path.strip()
if not path:
return "."
if not is_posixpath(path):
path = as_winpath(path)
posix = False
pathmod, path_sep = path_handlers(path, posix)
path = path.strip(path_sep)
segments = [s.strip() for s in path.split(path_sep) if s.strip()]
# remove empty (// or ///) or blank (space only) or single dot segments
segments = [s for s in segments if s and s != "."]
path = path_sep.join(segments)
# resolves . dot, .. dotdot
path = pathmod.normpath(path)
segments = path.split(path_sep)
# remove empty or blank segments
segments = [s.strip() for s in segments if s and s.strip()]
# is this a windows absolute path? if yes strip the colon to make this relative
if segments and len(segments[0]) == 2 and segments[0].endswith(":"):
segments[0] = segments[0][:-1]
# replace any remaining (usually leading) .. segment with a literal "dotdot"
dotdot = "dotdot"
dd = ".."
segments = [dotdot if s == dd else s for s in segments if s]
if segments:
path = path_sep.join(segments)
else:
path = "."
path = as_posixpath(path)
return path
legal_punctuation = r"!\#$%&\(\)\+,\-\.;\=@\[\]_\{\}\~"
legal_spaces = r" "
legal_alphanumeric = r"A-Za-z0-9"
legal_chars = legal_alphanumeric + legal_punctuation
legal_chars_inc_spaces = legal_chars + legal_spaces
illegal_chars_re = r"[^" + legal_chars + r"]"
illegal_chars_exc_spaces_re = r"[^" + legal_chars_inc_spaces + r"]"
replace_illegal_chars = re.compile(illegal_chars_re).sub
replace_illegal_chars_exc_spaces = re.compile(illegal_chars_exc_spaces_re).sub
posix_legal_punctuation = r'