diff --git a/bin/clean.sh b/bin/clean.sh index 319a51e..208b91e 100755 --- a/bin/clean.sh +++ b/bin/clean.sh @@ -25,11 +25,6 @@ DRY_RUN=false PROTECT_FINDER_METADATA=false IS_M_SERIES=$([[ "$(uname -m)" == "arm64" ]] && echo "true" || echo "false") -# Constants -readonly MAX_PARALLEL_JOBS=15 # Maximum parallel background jobs -readonly TEMP_FILE_AGE_DAYS=7 # Age threshold for temp file cleanup -readonly ORPHAN_AGE_DAYS=60 # Age threshold for orphaned data - # Protected Service Worker domains (web-based editing tools) readonly PROTECTED_SW_DOMAINS=( "capcut.com" @@ -64,6 +59,12 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then # Expand tilde to home directory [[ "$line" == ~* ]] && line="${line/#~/$HOME}" + # Security: reject path traversal attempts + if [[ "$line" =~ \.\. ]]; then + WHITELIST_WARNINGS+=("Path traversal not allowed: $line") + continue + fi + # Path validation with support for spaces and wildcards # Allow: letters, numbers, /, _, ., -, @, spaces, and * anywhere in path if [[ ! "$line" =~ ^[a-zA-Z0-9/_.@\ *-]+$ ]]; then @@ -71,6 +72,12 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then continue fi + # Require absolute paths (must start with /) + if [[ "$line" != /* ]]; then + WHITELIST_WARNINGS+=("Must be absolute path: $line") + continue + fi + # Reject paths with consecutive slashes (e.g., //) if [[ "$line" =~ // ]]; then WHITELIST_WARNINGS+=("Consecutive slashes: $line") @@ -79,7 +86,7 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then # Prevent critical system directories case "$line" in - /System/* | /bin/* | /sbin/* | /usr/bin/* | /usr/sbin/* | /etc/* | /var/db/*) + / | /System | /System/* | /bin | /bin/* | /sbin | /sbin/* | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /etc | /etc/* | /var/db | /var/db/*) WHITELIST_WARNINGS+=("Protected system path: $line") continue ;; @@ -322,7 +329,7 @@ safe_clean() { pids+=($!) ((idx++)) - if ((${#pids[@]} >= MAX_PARALLEL_JOBS)); then + if ((${#pids[@]} >= MOLE_MAX_PARALLEL_JOBS)); then wait "${pids[0]}" 2> /dev/null || true pids=("${pids[@]:1}") ((completed++)) @@ -351,7 +358,7 @@ safe_clean() { if [[ -L "$path" ]]; then rm "$path" 2> /dev/null || true else - rm -rf "$path" 2> /dev/null || true + safe_remove "$path" true || true fi fi ((total_size_bytes += size)) @@ -380,7 +387,7 @@ safe_clean() { if [[ -L "$path" ]]; then rm "$path" 2> /dev/null || true else - rm -rf "$path" 2> /dev/null || true + safe_remove "$path" true || true fi fi ((total_size_bytes += size_bytes)) @@ -606,9 +613,9 @@ perform_cleanup() { clean_virtualization_tools end_section - # ===== 11. Application Support logs cleanup ===== - start_section "Application Support logs" - # Application Support logs cleanup (delegated to clean_user_data module) + # ===== 11. Application Support logs and caches cleanup ===== + start_section "Application Support" + # Clean logs, Service Worker caches, Code Cache, Crashpad, stale updates, Group Containers clean_application_support_logs end_section diff --git a/bin/optimize.sh b/bin/optimize.sh index e9aa4a7..cb09fcc 100755 --- a/bin/optimize.sh +++ b/bin/optimize.sh @@ -190,10 +190,10 @@ cleanup_path() { fi local removed=false - if rm -rf "$expanded_path" 2> /dev/null; then + if safe_remove "$expanded_path" true; then removed=true elif request_sudo_access "Removing $label requires admin access"; then - if sudo rm -rf "$expanded_path" 2> /dev/null; then + if safe_sudo_remove "$expanded_path"; then removed=true fi fi diff --git a/bin/uninstall.sh b/bin/uninstall.sh index ac7cecc..1b0f488 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -522,7 +522,7 @@ uninstall_applications() { done # Remove the application - if rm -rf "$app_path" 2> /dev/null; then + if safe_remove "$app_path" true; then echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed application" else echo -e " ${RED}${ICON_ERROR}${NC} Failed to remove $app_path" @@ -538,7 +538,7 @@ uninstall_applications() { echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(echo "$file" | sed "s|$HOME|~|" | xargs basename)" fi else - if rm -rf "$file" 2> /dev/null; then + if safe_remove "$file" true; then echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(echo "$file" | sed "s|$HOME|~|" | xargs basename)" fi fi @@ -558,7 +558,7 @@ uninstall_applications() { echo -e " ${YELLOW}${ICON_ERROR}${NC} Failed to remove: $file" fi else - if sudo rm -rf "$file" 2> /dev/null; then + if safe_sudo_remove "$file"; then echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(basename "$file")" else echo -e " ${YELLOW}${ICON_ERROR}${NC} Failed to remove: $file" diff --git a/lib/clean_caches.sh b/lib/clean_caches.sh index 4531408..46170e8 100644 --- a/lib/clean_caches.sh +++ b/lib/clean_caches.sh @@ -90,7 +90,7 @@ clean_service_worker_cache() { # Clean if not protected if [[ "$is_protected" == "false" ]]; then if [[ "$DRY_RUN" != "true" ]]; then - rm -rf "$cache_dir" 2> /dev/null || true + safe_remove "$cache_dir" true || true fi cleaned_size=$((cleaned_size + size)) fi diff --git a/lib/clean_user_data.sh b/lib/clean_user_data.sh index 8fe57bc..16c9fdb 100644 --- a/lib/clean_user_data.sh +++ b/lib/clean_user_data.sh @@ -1,11 +1,9 @@ #!/bin/bash # User Data Cleanup Module -# Essential user caches, browsers, cloud storage, office apps set -euo pipefail # Clean user essentials (caches, logs, trash, crash reports) -# Env: DRY_RUN clean_user_essentials() { safe_clean ~/Library/Caches/* "User app cache" safe_clean ~/Library/Logs/* "User app logs" @@ -22,10 +20,13 @@ clean_user_essentials() { nfs | smbfs | afpfs | cifs | webdav) continue ;; esac - # Verify volume is mounted - if mount | grep -q "on $volume "; then + # Verify volume is mounted and not a symlink + if mount | grep -q "on $volume " && [[ ! -L "$volume/.Trashes" ]]; then if [[ "$DRY_RUN" != "true" ]]; then - find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -exec rm -rf {} \; 2> /dev/null || true + # Safely iterate and remove each item + while IFS= read -r -d '' item; do + safe_remove "$item" true || true + done < <(find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -print0 2> /dev/null) fi fi done @@ -52,7 +53,6 @@ clean_user_essentials() { } # Clean Finder metadata (.DS_Store files) -# Env: PROTECT_FINDER_METADATA clean_finder_metadata() { if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then note_activity @@ -170,7 +170,7 @@ clean_virtualization_tools() { safe_clean ~/.vagrant.d/tmp/* "Vagrant temporary files" } -# Clean Application Support logs +# Clean Application Support logs and caches clean_application_support_logs() { # Check permission if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" > /dev/null 2>&1; then @@ -179,7 +179,7 @@ clean_application_support_logs() { return 0 fi - # Clean log directories with iteration limit to prevent hanging + # Clean log directories and cache patterns with iteration limit local iteration_count=0 local max_iterations=200 @@ -201,7 +201,7 @@ clean_application_support_logs() { ;; esac - # Clean common log directories (only if they exist and are accessible) + # Clean log directories if [[ -d "$app_dir/log" ]] && ls "$app_dir/log" > /dev/null 2>&1; then safe_clean "$app_dir/log"/* "App logs: $app_name" fi @@ -211,7 +211,44 @@ clean_application_support_logs() { if [[ -d "$app_dir/activitylog" ]] && ls "$app_dir/activitylog" > /dev/null 2>&1; then safe_clean "$app_dir/activitylog"/* "Activity logs: $app_name" fi + + # Clean common cache patterns (Service Worker, Code Cache, Crashpad) + if [[ -d "$app_dir/Cache/Cache_Data" ]] && ls "$app_dir/Cache/Cache_Data" > /dev/null 2>&1; then + safe_clean "$app_dir/Cache/Cache_Data" "Cache data: $app_name" + fi + if [[ -d "$app_dir/Code Cache/js" ]] && ls "$app_dir/Code Cache/js" > /dev/null 2>&1; then + safe_clean "$app_dir/Code Cache/js"/* "Code cache: $app_name" + fi + if [[ -d "$app_dir/Crashpad/completed" ]] && ls "$app_dir/Crashpad/completed" > /dev/null 2>&1; then + safe_clean "$app_dir/Crashpad/completed"/* "Crash reports: $app_name" + fi + + # Clean Service Worker caches (CacheStorage and ScriptCache) + while IFS= read -r -d '' sw_cache; do + local profile_path=$(dirname "$(dirname "$sw_cache")") + local profile_name=$(basename "$profile_path") + [[ "$profile_name" == "User Data" ]] && profile_name=$(basename "$(dirname "$profile_path")") + clean_service_worker_cache "$app_name ($profile_name)" "$sw_cache" + done < <(find "$app_dir" -maxdepth 4 -type d \( -name "CacheStorage" -o -name "ScriptCache" \) -path "*/Service Worker/*" 2> /dev/null || true) + + # Clean stale update downloads (older than 7 days) + if [[ -d "$app_dir/update" ]] && ls "$app_dir/update" > /dev/null 2>&1; then + while IFS= read -r update_dir; do + local dir_age_days=$(( ($(date +%s) - $(get_file_mtime "$update_dir")) / 86400 )) + if [[ $dir_age_days -ge $MOLE_TEMP_FILE_AGE_DAYS ]]; then + safe_clean "$update_dir" "Stale update: $app_name" + fi + done < <(find "$app_dir/update" -mindepth 1 -maxdepth 1 -type d 2> /dev/null || true) + fi done + + # Clean Group Containers logs + if [[ -d "$HOME/Library/Group Containers" ]]; then + while IFS= read -r logs_dir; do + local container_name=$(basename "$(dirname "$logs_dir")") + safe_clean "$logs_dir"/* "Group container logs: $container_name" + done < <(find "$HOME/Library/Group Containers" -maxdepth 2 -type d -name "Logs" 2> /dev/null || true) + fi } # Check and show iOS device backup info diff --git a/lib/common.sh b/lib/common.sh index 9a4a974..7fcfe00 100755 --- a/lib/common.sh +++ b/lib/common.sh @@ -20,30 +20,39 @@ readonly RED="${ESC}[0;31m" readonly GRAY="${ESC}[0;90m" readonly NC="${ESC}[0m" -# Icon definitions (shared across modules) -readonly ICON_CONFIRM="◎" # Confirm operation / spinner text -readonly ICON_ADMIN="⚙" # Gear indicator for admin/settings/system info -readonly ICON_SUCCESS="✓" # Success mark -readonly ICON_ERROR="☻" # Error / warning mark -readonly ICON_EMPTY="○" # Hollow circle (empty state / unchecked) -readonly ICON_SOLID="●" # Solid circle (selected / system marker) -readonly ICON_LIST="•" # Basic list bullet -readonly ICON_ARROW="➤" # Pointer / prompt indicator -readonly ICON_WARNING="☻" # Warning marker (shares glyph with error) -readonly ICON_NAV_UP="↑" # Navigation up -readonly ICON_NAV_DOWN="↓" # Navigation down -readonly ICON_NAV_LEFT="←" # Navigation left -readonly ICON_NAV_RIGHT="→" # Navigation right +# Icon definitions +readonly ICON_CONFIRM="◎" +readonly ICON_ADMIN="⚙" +readonly ICON_SUCCESS="✓" +readonly ICON_ERROR="☻" +readonly ICON_EMPTY="○" +readonly ICON_SOLID="●" +readonly ICON_LIST="•" +readonly ICON_ARROW="➤" +readonly ICON_WARNING="☻" +readonly ICON_NAV_UP="↑" +readonly ICON_NAV_DOWN="↓" +readonly ICON_NAV_LEFT="←" +readonly ICON_NAV_RIGHT="→" -# Get spinner characters (ASCII by default, overridable via MO_SPINNER_CHARS env) +# Global configuration constants +readonly MOLE_TEMP_FILE_AGE_DAYS=7 # Temp file cleanup threshold +readonly MOLE_ORPHAN_AGE_DAYS=60 # Orphaned data threshold +readonly MOLE_MAX_PARALLEL_JOBS=15 # Parallel job limit +readonly MOLE_MAIL_DOWNLOADS_MIN_KB=5120 # Mail attachments size threshold (~5MB) +readonly MOLE_LOG_AGE_DAYS=30 # System log retention +readonly MOLE_CRASH_REPORT_AGE_DAYS=30 # Crash report retention +readonly MOLE_SAVED_STATE_AGE_DAYS=7 # App saved state retention +readonly MOLE_TM_BACKUP_SAFE_HOURS=48 # Time Machine failed backup safety window + +# Get spinner characters (overridable via MO_SPINNER_CHARS) mo_spinner_chars() { local chars="${MO_SPINNER_CHARS:-|/-\\}" [[ -z "$chars" ]] && chars='|/-\\' printf "%s" "$chars" } -# BSD stat compatibility (for users with GNU CoreUtils installed) -# Always use system BSD stat instead of potentially overridden GNU version +# BSD stat compatibility readonly STAT_BSD="/usr/bin/stat" # Get file size in bytes using BSD stat @@ -66,20 +75,7 @@ get_file_owner() { # Security and Path Validation Functions -# Validates a path for safe deletion -# -# Security checks: -# - Rejects empty paths -# - Requires absolute paths (must start with /) -# - Blocks control characters and newlines -# - Protects critical system directories -# -# Args: -# $1 - Path to validate -# -# Returns: -# 0 if path is safe to delete -# 1 if path fails any validation check +# Validates path for deletion (absolute, no control chars, not system dir) validate_path_for_deletion() { local path="$1" @@ -113,21 +109,8 @@ validate_path_for_deletion() { return 0 } -# Safe wrapper around rm -rf with validation and logging -# -# Provides a secure alternative to direct rm -rf calls with: -# - Path validation (absolute paths, no control characters) -# - System directory protection -# - Logging of all operations -# - Silent mode for non-critical failures -# -# Usage: -# safe_remove "/path/to/file" # Normal mode with logging -# safe_remove "/path/to/file" true # Silent mode -# -# Returns: -# 0 on success or if path doesn't exist -# 1 on validation failure or deletion error +# Safe wrapper around rm -rf with path validation and logging +# Usage: safe_remove "/path" [silent] safe_remove() { local path="$1" local silent="${2:-false}" @@ -139,26 +122,121 @@ safe_remove() { # Check if path exists if [[ ! -e "$path" ]]; then - [[ "$silent" != "true" ]] && log_warning "Path does not exist, skipping: $path" return 0 fi - # Log what we're about to delete - if [[ -d "$path" ]]; then - log_info "Removing directory: $path" - else - log_info "Removing file: $path" - fi - - # Perform the deletion + # Perform the deletion (log only on error) if rm -rf "$path" 2> /dev/null; then return 0 else - log_error "Failed to remove: $path" + [[ "$silent" != "true" ]] && log_error "Failed to remove: $path" return 1 fi } +# Safe sudo remove with validation (rejects symlinks) +# Usage: safe_sudo_remove "/path" +safe_sudo_remove() { + local path="$1" + + # Validate path + if ! validate_path_for_deletion "$path"; then + log_error "Path validation failed for sudo remove: $path" + return 1 + fi + + # Check if path exists + if [[ ! -e "$path" ]]; then + return 0 + fi + + # Additional check: reject symlinks for sudo operations + if [[ -L "$path" ]]; then + log_error "Refusing to sudo remove symlink: $path" + return 1 + fi + + # Perform the deletion (log only on error) + if sudo rm -rf "$path" 2> /dev/null; then + return 0 + else + log_error "Failed to remove (sudo): $path" + return 1 + fi +} + +# Safe find delete with depth limit and validation +# Usage: safe_find_delete "/dir" "pattern" age_days "f|d" +safe_find_delete() { + local base_dir="$1" + local pattern="$2" + local age_days="${3:-7}" + local type_filter="${4:-f}" + + # Validate base directory exists and is not a symlink + if [[ ! -d "$base_dir" ]]; then + log_warning "Base directory does not exist: $base_dir" + return 1 + fi + + if [[ -L "$base_dir" ]]; then + log_error "Refusing to search symlinked directory: $base_dir" + return 1 + fi + + # Validate type filter + if [[ "$type_filter" != "f" && "$type_filter" != "d" ]]; then + log_error "Invalid type filter: $type_filter (must be 'f' or 'd')" + return 1 + fi + + # Execute find with safety limits + find "$base_dir" \ + -maxdepth 3 \ + -name "$pattern" \ + -type "$type_filter" \ + -mtime "+$age_days" \ + -delete 2> /dev/null || true + + return 0 +} + +# Safe sudo find delete (same as safe_find_delete with sudo) +# Usage: safe_sudo_find_delete "/dir" "pattern" age_days "f|d" +safe_sudo_find_delete() { + local base_dir="$1" + local pattern="$2" + local age_days="${3:-7}" + local type_filter="${4:-f}" + + # Validate base directory exists and is not a symlink + if [[ ! -d "$base_dir" ]]; then + log_warning "Base directory does not exist: $base_dir" + return 1 + fi + + if [[ -L "$base_dir" ]]; then + log_error "Refusing to search symlinked directory: $base_dir" + return 1 + fi + + # Validate type filter + if [[ "$type_filter" != "f" && "$type_filter" != "d" ]]; then + log_error "Invalid type filter: $type_filter (must be 'f' or 'd')" + return 1 + fi + + # Execute find with safety limits + sudo find "$base_dir" \ + -maxdepth 3 \ + -name "$pattern" \ + -type "$type_filter" \ + -mtime "+$age_days" \ + -delete 2> /dev/null || true + + return 0 +} + # Logging configuration readonly LOG_FILE="${HOME}/.config/mole/mole.log" readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB @@ -200,6 +278,22 @@ log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE" 2> /dev/null || true } +# Run command with optional error handling +# Usage: run_silent command args... # Ignore errors +# run_logged command args... # Log errors but continue +run_silent() { + "$@" > /dev/null 2>&1 || true +} + +run_logged() { + local cmd="$1" + if ! "$@" 2>&1 | tee -a "$LOG_FILE" > /dev/null; then + log_warning "Command failed: $cmd" + return 1 + fi + return 0 +} + # Call rotation check once when common.sh is sourced rotate_log_once @@ -261,8 +355,7 @@ show_cursor() { } # Read single keypress and return normalized key name -# Returns: ENTER, SPACE, UP, DOWN, LEFT, RIGHT, QUIT, DELETE, CHAR:, etc. -# Env: MOLE_READ_KEY_FORCE_CHAR=1 for filter mode +# Returns: ENTER, SPACE, UP, DOWN, LEFT, RIGHT, QUIT, DELETE, CHAR: read_key() { local key rest read_status diff --git a/lib/optimization_tasks.sh b/lib/optimization_tasks.sh index 1d27f19..9e4202a 100644 --- a/lib/optimization_tasks.sh +++ b/lib/optimization_tasks.sh @@ -1,11 +1,8 @@ #!/bin/bash # Optimization Tasks -# Individual optimization operations extracted from execute_optimization set -euo pipefail -readonly MAIL_DOWNLOADS_MIN_KB=5120 # ~5MB threshold - _opt_get_dir_size_kb() { local path="$1" [[ -e "$path" ]] || { @@ -145,8 +142,8 @@ opt_log_cleanup() { done if [[ -d "/Library/Logs/DiagnosticReports" ]]; then - sudo find /Library/Logs/DiagnosticReports -type f -name "*.crash" -delete 2> /dev/null || true - sudo find /Library/Logs/DiagnosticReports -type f -name "*.panic" -delete 2> /dev/null || true + safe_sudo_find_delete "/Library/Logs/DiagnosticReports" "*.crash" 0 "f" + safe_sudo_find_delete "/Library/Logs/DiagnosticReports" "*.panic" 0 "f" echo -e "${GREEN}${ICON_SUCCESS}${NC} System diagnostic logs cleared" else echo -e "${GRAY}-${NC} No system diagnostic logs found" @@ -158,7 +155,7 @@ opt_recent_items() { echo -e "${BLUE}${ICON_ARROW}${NC} Clearing recent items lists..." local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist" if [[ -d "$shared_dir" ]]; then - find "$shared_dir" -name "*.sfl2" -type f -delete 2> /dev/null || true + safe_find_delete "$shared_dir" "*.sfl2" 0 "f" echo -e "${GREEN}${ICON_SUCCESS}${NC} Shared file lists cleared" fi @@ -211,16 +208,20 @@ opt_mail_downloads() { total_kb=$((total_kb + $(_opt_get_dir_size_kb "$target_path"))) done - if [[ $total_kb -lt $MAIL_DOWNLOADS_MIN_KB ]]; then + if [[ $total_kb -lt $MOLE_MAIL_DOWNLOADS_MIN_KB ]]; then echo -e "${GRAY}-${NC} Only $(bytes_to_human $((total_kb * 1024))) detected, skipping cleanup" return fi - # Only delete files older than 30 days (safer) + # Only delete old attachments (safety window) local deleted=0 for target_path in "${mail_dirs[@]}"; do if [[ -d "$target_path" ]]; then - deleted=$((deleted + $(find "$target_path" -type f -mtime +30 -delete -print 2> /dev/null | wc -l | tr -d ' '))) + local file_count=$(find "$target_path" -type f -mtime "+$MOLE_LOG_AGE_DAYS" 2> /dev/null | wc -l | tr -d ' ') + if [[ "$file_count" -gt 0 ]]; then + safe_find_delete "$target_path" "*" "$MOLE_LOG_AGE_DAYS" "f" + deleted=$((deleted + file_count)) + fi fi done @@ -233,7 +234,7 @@ opt_mail_downloads() { # Saved state: remove OLD app saved states (7+ days) opt_saved_state_cleanup() { - echo -e "${BLUE}${ICON_ARROW}${NC} Removing old saved application states (7+ days)..." + echo -e "${BLUE}${ICON_ARROW}${NC} Removing old saved application states (${MOLE_SAVED_STATE_AGE_DAYS}+ days)..." local state_dir="$HOME/Library/Saved Application State" if [[ ! -d "$state_dir" ]]; then @@ -241,9 +242,13 @@ opt_saved_state_cleanup() { return fi - # Only delete states older than 7 days (safer - won't lose recent work) + # Only delete old saved states (safety window) local deleted=0 - deleted=$(find "$state_dir" -type d -name "*.savedState" -mtime +7 -exec rm -rf {} \; -print 2> /dev/null | wc -l | tr -d ' ') + while IFS= read -r -d '' state_path; do + if safe_remove "$state_path" true; then + ((deleted++)) + fi + done < <(find "$state_dir" -type d -name "*.savedState" -mtime "+$MOLE_SAVED_STATE_AGE_DAYS" -print0 2> /dev/null) if [[ $deleted -gt 0 ]]; then echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed $deleted old saved state(s)" diff --git a/lib/uninstall_batch.sh b/lib/uninstall_batch.sh index db643d0..cc108cb 100755 --- a/lib/uninstall_batch.sh +++ b/lib/uninstall_batch.sh @@ -199,16 +199,16 @@ batch_uninstall_applications() { fi if [[ -z "$reason" ]]; then if [[ "$needs_sudo" == true ]]; then - sudo rm -rf "$app_path" 2> /dev/null || reason="remove failed" + safe_sudo_remove "$app_path" || reason="remove failed" else - rm -rf "$app_path" 2> /dev/null || reason="remove failed" + safe_remove "$app_path" true || reason="remove failed" fi fi if [[ -z "$reason" ]]; then local files_removed=0 while IFS= read -r file; do [[ -n "$file" && -e "$file" ]] || continue - rm -rf "$file" 2> /dev/null && ((files_removed++)) || true + safe_remove "$file" true && ((files_removed++)) || true done <<< "$related_files" ((total_size_freed += total_kb)) ((success_count++))