diff --git a/dvc/utils/fs.py b/dvc/utils/fs.py index c73a23563e..cb15088eaa 100644 --- a/dvc/utils/fs.py +++ b/dvc/utils/fs.py @@ -134,3 +134,14 @@ def remove(path): except OSError as exc: if exc.errno != errno.ENOENT: raise + + +def path_isin(child, parent): + """Check if given `child` path is inside `parent`.""" + + def normalize_path(path): + return os.path.normpath(fspath(path)) + + parent = os.path.join(normalize_path(parent), "") + child = normalize_path(child) + return child != parent and child.startswith(parent) diff --git a/tests/unit/utils/test_fs.py b/tests/unit/utils/test_fs.py index a8faabb0c1..7b136df7b5 100644 --- a/tests/unit/utils/test_fs.py +++ b/tests/unit/utils/test_fs.py @@ -16,7 +16,7 @@ from dvc.utils.fs import get_inode from dvc.utils.fs import get_mtime_and_size from dvc.utils.fs import move -from dvc.utils.fs import remove +from dvc.utils.fs import path_isin, remove from tests.basic_env import TestDir from tests.utils import spy @@ -164,3 +164,49 @@ def test_remove(repo_dir): remove(path_info) assert not os.path.isfile(path_info.fspath) + + +@pytest.mark.parametrize( + "parent", + [ + (os.path.join("path", "to", "")), + (os.path.join("path", "to")), + (os.path.join("path", "")), + (os.path.join("path")), + ], +) +def test_path_isin_positive(parent): + child = os.path.join("path", "to", "folder") + assert path_isin(child, parent) + + +def test_path_isin_on_same_path(): + path = os.path.join("path", "to", "folder") + path2 = os.path.join(path, "") + + assert not path_isin(path, path) + assert not path_isin(path, path2) + assert not path_isin(path2, path) + assert not path_isin(path2, path2) + + +def test_path_isin_on_common_substring_path(): + path1 = os.path.join("path", "to", "folder1") + path2 = os.path.join("path", "to", "folder") + + assert not path_isin(path1, path2) + + +def test_path_isin_accepts_pathinfo(): + child = os.path.join("path", "to", "folder") + parent = PathInfo(child) / ".." + + assert path_isin(child, parent) + assert not path_isin(parent, child) + + +def test_path_isin_with_absolute_path(): + parent = os.path.abspath("path") + child = os.path.join(parent, "to", "folder") + + assert path_isin(child, parent)