diff --git a/lib/clean/apps.sh b/lib/clean/apps.sh index ac91ac1..a848880 100644 --- a/lib/clean/apps.sh +++ b/lib/clean/apps.sh @@ -221,7 +221,8 @@ is_bundle_orphaned() { if [[ -n "$bundle_id" ]] && [[ "$bundle_id" =~ ^[a-zA-Z0-9._-]+$ ]] && [[ ${#bundle_id} -ge 5 ]]; then # Initialize cache file if needed if [[ -z "$ORPHAN_MDFIND_CACHE_FILE" ]]; then - ORPHAN_MDFIND_CACHE_FILE=$(mktemp "${TMPDIR:-/tmp}/mole_mdfind_cache.XXXXXX") + ensure_mole_temp_root + ORPHAN_MDFIND_CACHE_FILE=$(mktemp "$MOLE_RESOLVED_TMPDIR/mole_mdfind_cache.XXXXXX") register_temp_file "$ORPHAN_MDFIND_CACHE_FILE" fi @@ -277,7 +278,8 @@ is_claude_vm_bundle_orphaned() { fi if [[ -z "$ORPHAN_MDFIND_CACHE_FILE" ]]; then - ORPHAN_MDFIND_CACHE_FILE=$(mktemp "${TMPDIR:-/tmp}/mole_mdfind_cache.XXXXXX") + ensure_mole_temp_root + ORPHAN_MDFIND_CACHE_FILE=$(mktemp "$MOLE_RESOLVED_TMPDIR/mole_mdfind_cache.XXXXXX") register_temp_file "$ORPHAN_MDFIND_CACHE_FILE" fi @@ -449,7 +451,8 @@ clean_orphaned_system_services() { if [[ -n "$bundle_id" ]] && [[ "$bundle_id" =~ ^[a-zA-Z0-9._-]+$ ]] && [[ ${#bundle_id} -ge 5 ]]; then if [[ -z "$mdfind_cache_file" ]]; then - mdfind_cache_file=$(mktemp "${TMPDIR:-/tmp}/mole_mdfind_cache.XXXXXX") + ensure_mole_temp_root + mdfind_cache_file=$(mktemp "$MOLE_RESOLVED_TMPDIR/mole_mdfind_cache.XXXXXX") register_temp_file "$mdfind_cache_file" fi diff --git a/lib/core/base.sh b/lib/core/base.sh index 9a98175..22e541b 100644 --- a/lib/core/base.sh +++ b/lib/core/base.sh @@ -556,10 +556,93 @@ bytes_to_human_kb() { declare -a MOLE_TEMP_FILES=() declare -a MOLE_TEMP_DIRS=() +normalize_temp_root() { + local path="${1:-}" + [[ -z "$path" ]] && return 1 + + if [[ "$path" == "~"* ]]; then + path="${path/#\~/$HOME}" + fi + + while [[ "$path" != "/" && "$path" == */ ]]; do + path="${path%/}" + done + + [[ -n "$path" ]] || return 1 + printf '%s\n' "$path" +} + +probe_temp_root() { + local raw_path="$1" + local allow_create="${2:-false}" + local path + local probe="" + + path=$(normalize_temp_root "$raw_path") || return 1 + + if [[ "$allow_create" == "true" ]]; then + ensure_user_dir "$path" + fi + + [[ -d "$path" ]] || return 1 + + probe=$(mktemp "$path/mole.probe.XXXXXX" 2> /dev/null) || return 1 + rm -f "$probe" 2> /dev/null || true + + printf '%s\n' "$path" +} + +ensure_mole_temp_root() { + if [[ -n "${MOLE_RESOLVED_TMPDIR:-}" ]]; then + return 0 + fi + + local resolved="" + local candidate="${TMPDIR:-}" + local invoking_home="" + + if [[ -n "$candidate" ]]; then + resolved=$(probe_temp_root "$candidate" false || true) + fi + + if [[ -z "$resolved" ]]; then + invoking_home=$(get_invoking_home) + if [[ -n "$invoking_home" ]]; then + resolved=$(probe_temp_root "$invoking_home/.cache/mole/tmp" true || true) + fi + fi + + if [[ -z "$resolved" ]]; then + resolved=$(probe_temp_root "/tmp" false || true) + fi + + [[ -n "$resolved" ]] || resolved="/tmp" + MOLE_RESOLVED_TMPDIR="$resolved" + export MOLE_RESOLVED_TMPDIR +} + +get_mole_temp_root() { + ensure_mole_temp_root + printf '%s\n' "$MOLE_RESOLVED_TMPDIR" +} + +prepare_mole_tmpdir() { + ensure_mole_temp_root + export TMPDIR="$MOLE_RESOLVED_TMPDIR" + printf '%s\n' "$MOLE_RESOLVED_TMPDIR" +} + +mole_temp_path_template() { + local prefix="${1:-mole}" + ensure_mole_temp_root + printf '%s/%s.XXXXXX\n' "$MOLE_RESOLVED_TMPDIR" "$prefix" +} + # Create tracked temporary file create_temp_file() { local temp - temp=$(mktemp "${TMPDIR:-/tmp}/mole.XXXXXX") || return 1 + ensure_mole_temp_root + temp=$(mktemp "$MOLE_RESOLVED_TMPDIR/mole.XXXXXX") || return 1 register_temp_file "$temp" echo "$temp" } @@ -567,7 +650,8 @@ create_temp_file() { # Create tracked temporary directory create_temp_dir() { local temp - temp=$(mktemp -d "${TMPDIR:-/tmp}/mole.XXXXXX") || return 1 + ensure_mole_temp_root + temp=$(mktemp -d "$MOLE_RESOLVED_TMPDIR/mole.XXXXXX") || return 1 register_temp_dir "$temp" echo "$temp" } @@ -588,9 +672,8 @@ mktemp_file() { local prefix="${1:-mole}" local temp local error_msg - # Use TMPDIR if set, otherwise /tmp # Add .XXXXXX suffix to work with both BSD and GNU mktemp - if ! error_msg=$(mktemp "${TMPDIR:-/tmp}/${prefix}.XXXXXX" 2>&1); then + if ! error_msg=$(mktemp "$(mole_temp_path_template "$prefix")" 2>&1); then echo "Error: Failed to create temporary file: $error_msg" >&2 return 1 fi diff --git a/lib/core/common.sh b/lib/core/common.sh index 1211685..83ab59f 100755 --- a/lib/core/common.sh +++ b/lib/core/common.sh @@ -14,6 +14,7 @@ _MOLE_CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Load core modules source "$_MOLE_CORE_DIR/base.sh" +prepare_mole_tmpdir > /dev/null source "$_MOLE_CORE_DIR/log.sh" source "$_MOLE_CORE_DIR/timeout.sh" diff --git a/lib/core/ui.sh b/lib/core/ui.sh index 510d5a4..a00ab3e 100755 --- a/lib/core/ui.sh +++ b/lib/core/ui.sh @@ -324,7 +324,8 @@ start_inline_spinner() { if [[ -t 1 ]]; then # Create unique stop flag file for this spinner instance - INLINE_SPINNER_STOP_FILE="${TMPDIR:-/tmp}/mole_spinner_$$_$RANDOM.stop" + ensure_mole_temp_root + INLINE_SPINNER_STOP_FILE="$MOLE_RESOLVED_TMPDIR/mole_spinner_$$_$RANDOM.stop" ( local stop_file="$INLINE_SPINNER_STOP_FILE" diff --git a/tests/clean_core.bats b/tests/clean_core.bats index 127b96c..ab05999 100644 --- a/tests/clean_core.bats +++ b/tests/clean_core.bats @@ -91,6 +91,26 @@ run_clean_dry_run() { [[ "$output" == *"full preview"* ]] } +@test "mo clean --dry-run survives an unwritable TMPDIR" { + local blocked_tmp="$HOME/blocked-tmp" + mkdir -p "$blocked_tmp" + chmod 500 "$blocked_tmp" + + set_mock_sudo_uncached + local test_path="$PATH" + if [[ -n "${TEST_MOCK_BIN:-}" ]]; then + test_path="$TEST_MOCK_BIN:$PATH" + fi + + run env HOME="$HOME" TMPDIR="$blocked_tmp" MOLE_TEST_MODE=1 PATH="$test_path" \ + "$PROJECT_ROOT/mole" clean --dry-run + + [ "$status" -eq 0 ] + [[ "$output" != *"mktemp:"* ]] + [[ "$output" != *"Failed to create temporary file"* ]] + [ -d "$HOME/.cache/mole/tmp" ] +} + @test "mo clean --dry-run reports user cache without deleting it" { mkdir -p "$HOME/Library/Caches/TestApp" echo "cache data" > "$HOME/Library/Caches/TestApp/cache.tmp" diff --git a/tests/user_file_ops.bats b/tests/user_file_ops.bats index 086a125..fbad735 100644 --- a/tests/user_file_ops.bats +++ b/tests/user_file_ops.bats @@ -76,6 +76,61 @@ setup() { [ -d "$result" ] } +@test "get_mole_temp_root uses writable TMPDIR when available" { + local writable_tmp="$HOME/custom-tmp" + mkdir -p "$writable_tmp" + + result=$(env HOME="$HOME" TMPDIR="$writable_tmp" bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_mole_temp_root") + [ "$result" = "$writable_tmp" ] +} + +@test "get_mole_temp_root falls back to user cache when TMPDIR is not writable" { + local blocked_tmp="$HOME/blocked-tmp" + mkdir -p "$blocked_tmp" + chmod 500 "$blocked_tmp" + + result=$(env HOME="$HOME" TMPDIR="$blocked_tmp" bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_mole_temp_root") + [ "$result" = "$HOME/.cache/mole/tmp" ] + [ -d "$HOME/.cache/mole/tmp" ] +} + +@test "get_mole_temp_root caches the first resolved directory" { + local first_tmp="$HOME/first-tmp" + local second_tmp="$HOME/second-tmp" + mkdir -p "$first_tmp" "$second_tmp" + + result=$(env HOME="$HOME" TMPDIR="$first_tmp" bash -c " + source '$PROJECT_ROOT/lib/core/base.sh' + ensure_mole_temp_root + first=\$MOLE_RESOLVED_TMPDIR + export TMPDIR='$second_tmp' + ensure_mole_temp_root + second=\$MOLE_RESOLVED_TMPDIR + printf '%s|%s\n' \"\$first\" \"\$second\" + ") + + [ "$result" = "$first_tmp|$first_tmp" ] +} + +@test "get_mole_temp_root falls back to /tmp when TMPDIR and invoking home are unavailable" { + result=$(env HOME="$HOME" TMPDIR="/var/empty" bash -c " + source '$PROJECT_ROOT/lib/core/base.sh' + get_invoking_home() { echo '/var/empty'; } + get_mole_temp_root + ") + + [ "$result" = "/tmp" ] +} + +@test "common.sh exports resolved TMPDIR for runtime callers" { + local blocked_tmp="$HOME/common-blocked-tmp" + mkdir -p "$blocked_tmp" + chmod 500 "$blocked_tmp" + + result=$(env HOME="$HOME" TMPDIR="$blocked_tmp" bash -c "source '$PROJECT_ROOT/lib/core/common.sh'; printf '%s\n' \"\$TMPDIR\"") + [ "$result" = "$HOME/.cache/mole/tmp" ] +} + @test "get_user_home returns home for valid user" { current_user="${USER:-$(whoami)}" result=$(bash -c "source '$PROJECT_ROOT/lib/core/base.sh'; get_user_home '$current_user'")