diff --git a/pkgs/clan-cli/clan_lib/persist/util.py b/pkgs/clan-cli/clan_lib/persist/util.py index 20a82cf31..8ceb5dce3 100644 --- a/pkgs/clan-cli/clan_lib/persist/util.py +++ b/pkgs/clan-cli/clan_lib/persist/util.py @@ -9,6 +9,33 @@ from typing import Any from clan_lib.errors import ClanError +def path_match(path: list[str], whitelist_paths: list[list[str]]) -> bool: + """ + Returns True if path matches any whitelist path with "*" wildcards. + + I.e.: + whitelist_paths = [["a.b.*"]] + path = ["a", "b", "c"] + path_match(path, whitelist_paths) == True + + + whitelist_paths = ["a.b.c", "a.b.*"] + path = ["a", "b", "d"] + path_match(path, whitelist_paths) == False + """ + for wp in whitelist_paths: + if len(path) != len(wp): + continue + match = True + for p, w in zip(path, wp, strict=False): + if w != "*" and p != w: + match = False + break + if match: + return True + return False + + def flatten_data(data: dict, parent_key: str = "", separator: str = ".") -> dict: """ Recursively flattens a nested dictionary structure where keys are joined by the separator. diff --git a/pkgs/clan-cli/clan_lib/persist/util_test.py b/pkgs/clan-cli/clan_lib/persist/util_test.py index 8c987b0de..9ccec35cc 100644 --- a/pkgs/clan-cli/clan_lib/persist/util_test.py +++ b/pkgs/clan-cli/clan_lib/persist/util_test.py @@ -9,10 +9,58 @@ from clan_lib.persist.util import ( calc_patches, delete_by_path, determine_writeability, + path_match, unmerge_lists, ) +@pytest.mark.parametrize( + ("path", "whitelist", "expected"), + [ + # Exact matches + (["a", "b", "c"], [["a", "b", "c"]], True), + (["a", "b"], [["a", "b"]], True), + ([], [[]], True), + # Wildcard matches + (["a", "b", "c"], [["a", "*", "c"]], True), + (["a", "x", "c"], [["a", "*", "c"]], True), + (["a", "b", "c"], [["*", "b", "c"]], True), + (["a", "b", "c"], [["a", "b", "*"]], True), + (["a", "b", "c"], [["*", "*", "*"]], True), + # Multiple patterns - one matches + (["a", "b", "c"], [["x", "y", "z"], ["a", "*", "c"]], True), + (["x", "y", "z"], [["a", "*", "c"], ["x", "y", "z"]], True), + # Length mismatch + (["a", "b", "c"], [["a", "b"]], False), + (["a", "b"], [["a", "b", "c"]], False), + # Non-matching + (["a", "b", "c"], [["a", "b", "x"]], False), + (["a", "b", "c"], [["a", "x", "x"]], False), + (["a", "b", "c"], [["x", "x", "x"]], False), + # Empty whitelist + (["a"], [], False), + # Wildcards and exact mixed + ( + ["instances", "inst1", "roles", "roleA", "settings"], + [["instances", "*", "roles", "*", "settings"]], + True, + ), + # Partial wildcard - length mismatch should fail + ( + ["instances", "inst1", "roles", "roleA"], + [["instances", "*", "roles", "*", "settings"]], + False, + ), + # Empty path, no patterns + ([], [], False), + ], +) +def test_path_match( + path: list[str], whitelist: list[list[str]], expected: bool +) -> None: + assert path_match(path, whitelist) == expected + + # --------- Patching tests --------- def test_patch_nested() -> None: orig = {"a": 1, "b": {"a": 2.1, "b": 2.2}, "c": 3}