From 8f5b70457e6d2306b89b0acb45385af0d0a5e0cc Mon Sep 17 00:00:00 2001 From: tw93 Date: Sun, 22 Feb 2026 22:06:19 +0800 Subject: [PATCH] fix(purge): normalize search roots for scan filtering (#478) --- lib/clean/project.sh | 23 ++++++++++++++++++++++- tests/purge.bats | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/lib/clean/project.sh b/lib/clean/project.sh index f91d2b8..0ed4a29 100644 --- a/lib/clean/project.sh +++ b/lib/clean/project.sh @@ -254,12 +254,33 @@ is_purge_project_root() { is_safe_project_artifact() { local path="$1" local search_path="$2" + + # Normalize search path to tolerate user config entries with trailing slash. + if [[ "$search_path" != "/" ]]; then + search_path="${search_path%/}" + fi + if [[ "$path" != /* ]]; then return 1 fi if [[ "$path" != "$search_path/"* ]]; then - return 1 + # fd may emit physical/canonical paths (for example /private/var) + # while configured search roots use symlink aliases (for example /var). + # Compare physical paths as a fallback to avoid false negatives. + local physical_path="" + local physical_search_path="" + if [[ -d "$path" && -d "$search_path" ]]; then + physical_path=$(cd "$path" 2> /dev/null && pwd -P || echo "") + physical_search_path=$(cd "$search_path" 2> /dev/null && pwd -P || echo "") + fi + + if [[ -z "$physical_path" || -z "$physical_search_path" || "$physical_path" != "$physical_search_path/"* ]]; then + return 1 + fi + + path="$physical_path" + search_path="$physical_search_path" fi # Must not be a direct child of the search root. diff --git a/tests/purge.bats b/tests/purge.bats index e1e6931..c16e662 100644 --- a/tests/purge.bats +++ b/tests/purge.bats @@ -92,6 +92,23 @@ setup() { [[ "$result" == "ALLOWED" ]] } +@test "is_safe_project_artifact: accepts physical path under symlinked search root" { + mkdir -p "$HOME/www/real/proj/node_modules" + touch "$HOME/www/real/proj/package.json" + ln -s "$HOME/www/real" "$HOME/www/link" + + result=$(bash -c " + source '$PROJECT_ROOT/lib/clean/project.sh' + if is_safe_project_artifact '$HOME/www/real/proj/node_modules' '$HOME/www/link/proj'; then + echo 'ALLOWED' + else + echo 'BLOCKED' + fi + ") + + [[ "$result" == "ALLOWED" ]] +} + @test "filter_nested_artifacts: removes nested node_modules" { mkdir -p "$HOME/www/project/node_modules/package/node_modules" @@ -472,6 +489,28 @@ EOF [[ "$result" == "FOUND" ]] } +@test "scan_purge_targets: supports trailing slash search path in find mode" { + mkdir -p "$HOME/single-project/node_modules" + touch "$HOME/single-project/package.json" + + local scan_output + scan_output="$(mktemp)" + + result=$(bash -c " + source '$PROJECT_ROOT/lib/clean/project.sh' + MO_USE_FIND=1 scan_purge_targets '$HOME/single-project/' '$scan_output' + if grep -q '$HOME/single-project/node_modules' '$scan_output'; then + echo 'FOUND' + else + echo 'MISSING' + fi + ") + + rm -f "$scan_output" + + [[ "$result" == "FOUND" ]] +} + @test "is_recently_modified: detects recent projects" { mkdir -p "$HOME/www/project/node_modules" touch "$HOME/www/project/package.json"