mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 16:49:41 +00:00
1232 lines
42 KiB
Bash
Executable File
1232 lines
42 KiB
Bash
Executable File
#!/bin/bash
|
|
# Mole - Application Protection
|
|
# System critical and data-protected application lists
|
|
|
|
set -euo pipefail
|
|
|
|
if [[ -n "${MOLE_APP_PROTECTION_LOADED:-}" ]]; then
|
|
return 0
|
|
fi
|
|
readonly MOLE_APP_PROTECTION_LOADED=1
|
|
|
|
_MOLE_CORE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
[[ -z "${MOLE_BASE_LOADED:-}" ]] && source "$_MOLE_CORE_DIR/base.sh"
|
|
|
|
# Declare WHITELIST_PATTERNS if not already set (used by is_path_whitelisted)
|
|
if ! declare -p WHITELIST_PATTERNS &> /dev/null; then
|
|
declare -a WHITELIST_PATTERNS=()
|
|
fi
|
|
|
|
# Application Management
|
|
|
|
# Critical system components protected from uninstallation
|
|
# Note: We explicitly list system components instead of using "com.apple.*" wildcard
|
|
# to allow uninstallation of user-installed Apple apps (Xcode, Final Cut Pro, etc.)
|
|
readonly SYSTEM_CRITICAL_BUNDLES=(
|
|
# Core system applications (in /System/Applications/)
|
|
"com.apple.finder"
|
|
"com.apple.dock"
|
|
"com.apple.Safari"
|
|
"com.apple.mail"
|
|
"com.apple.systempreferences"
|
|
"com.apple.SystemSettings"
|
|
"com.apple.Settings*"
|
|
"com.apple.controlcenter*"
|
|
"com.apple.Spotlight"
|
|
"com.apple.notificationcenterui"
|
|
"com.apple.loginwindow"
|
|
"com.apple.Preview"
|
|
"com.apple.TextEdit"
|
|
"com.apple.Notes"
|
|
"com.apple.reminders"
|
|
"com.apple.iCal"
|
|
"com.apple.AddressBook"
|
|
"com.apple.Photos"
|
|
"com.apple.AppStore"
|
|
"com.apple.calculator"
|
|
"com.apple.Dictionary"
|
|
"com.apple.ScreenSharing"
|
|
"com.apple.ActivityMonitor"
|
|
"com.apple.Console"
|
|
"com.apple.DiskUtility"
|
|
"com.apple.KeychainAccess"
|
|
"com.apple.DigitalColorMeter"
|
|
"com.apple.grapher"
|
|
"com.apple.Terminal"
|
|
"com.apple.ScriptEditor2"
|
|
"com.apple.VoiceOverUtility"
|
|
"com.apple.BluetoothFileExchange"
|
|
"com.apple.print.PrinterProxy"
|
|
"com.apple.systempreferences*"
|
|
"com.apple.SystemProfiler"
|
|
"com.apple.FontBook"
|
|
"com.apple.ColorSyncUtility"
|
|
"com.apple.audio.AudioMIDISetup"
|
|
"com.apple.DirectoryUtility"
|
|
"com.apple.NetworkUtility"
|
|
"com.apple.exposelauncher"
|
|
"com.apple.MigrateAssistant"
|
|
"com.apple.RAIDUtility"
|
|
"com.apple.BootCampAssistant"
|
|
|
|
# System services and daemons
|
|
"com.apple.SecurityAgent"
|
|
"com.apple.CoreServices*"
|
|
"com.apple.SystemUIServer"
|
|
"com.apple.backgroundtaskmanagement*"
|
|
"com.apple.loginitems*"
|
|
"com.apple.sharedfilelist*"
|
|
"com.apple.sfl*"
|
|
"com.apple.coreservices*"
|
|
"com.apple.metadata*"
|
|
"com.apple.MobileSoftwareUpdate*"
|
|
"com.apple.SoftwareUpdate*"
|
|
"com.apple.installer*"
|
|
"com.apple.frameworks*"
|
|
"com.apple.security*"
|
|
"com.apple.keychain*"
|
|
"com.apple.trustd*"
|
|
"com.apple.securityd*"
|
|
"com.apple.cloudd*"
|
|
"com.apple.iCloud*"
|
|
"com.apple.WiFi*"
|
|
"com.apple.airport*"
|
|
"com.apple.Bluetooth*"
|
|
|
|
# Input methods (system built-in)
|
|
"com.apple.inputmethod.*"
|
|
"com.apple.inputsource*"
|
|
"com.apple.TextInput*"
|
|
"com.apple.CharacterPicker*"
|
|
"com.apple.PressAndHold*"
|
|
|
|
# Legacy pattern-based entries (non com.apple.*)
|
|
"loginwindow"
|
|
"dock"
|
|
"systempreferences"
|
|
"finder"
|
|
"safari"
|
|
"backgroundtaskmanagementagent"
|
|
"keychain*"
|
|
"security*"
|
|
"bluetooth*"
|
|
"wifi*"
|
|
"network*"
|
|
"tcc"
|
|
"notification*"
|
|
"accessibility*"
|
|
"universalaccess*"
|
|
"HIToolbox*"
|
|
"textinput*"
|
|
"TextInput*"
|
|
"keyboard*"
|
|
"Keyboard*"
|
|
"inputsource*"
|
|
"InputSource*"
|
|
"keylayout*"
|
|
"KeyLayout*"
|
|
"GlobalPreferences"
|
|
".GlobalPreferences"
|
|
"org.pqrs.Karabiner*"
|
|
)
|
|
|
|
# Apple apps that CAN be uninstalled (from App Store or developer.apple.com)
|
|
readonly APPLE_UNINSTALLABLE_APPS=(
|
|
"com.apple.dt.*" # Xcode, Instruments, FileMerge
|
|
"com.apple.FinalCut*" # Final Cut Pro
|
|
"com.apple.Motion"
|
|
"com.apple.Compressor"
|
|
"com.apple.logic*" # Logic Pro
|
|
"com.apple.garageband*" # GarageBand
|
|
"com.apple.iMovie"
|
|
"com.apple.iWork.*" # Pages, Numbers, Keynote
|
|
"com.apple.MainStage*"
|
|
"com.apple.server.*" # macOS Server
|
|
"com.apple.Playgrounds" # Swift Playgrounds
|
|
)
|
|
|
|
# Applications with sensitive data; protected during cleanup but removable
|
|
readonly DATA_PROTECTED_BUNDLES=(
|
|
# Input Methods (protected during cleanup, uninstall allowed)
|
|
"com.tencent.inputmethod.QQInput"
|
|
"com.sogou.inputmethod.*"
|
|
"com.baidu.inputmethod.*"
|
|
"com.googlecode.rimeime.*"
|
|
"im.rime.*"
|
|
"*.inputmethod"
|
|
"*.InputMethod"
|
|
"*IME"
|
|
|
|
# System Utilities & Cleanup
|
|
"com.nektony.*"
|
|
"com.macpaw.*"
|
|
"com.freemacsoft.AppCleaner"
|
|
"com.omnigroup.omnidisksweeper"
|
|
"com.daisydiskapp.*"
|
|
"com.tunabellysoftware.*"
|
|
"com.grandperspectiv.*"
|
|
"com.binaryfruit.*"
|
|
|
|
# Password Managers
|
|
"com.1password.*"
|
|
"com.agilebits.*"
|
|
"com.lastpass.*"
|
|
"com.dashlane.*"
|
|
"com.bitwarden.*"
|
|
"com.keepassx.*"
|
|
"org.keepassx.*"
|
|
"org.keepassxc.*"
|
|
"com.authy.*"
|
|
"com.yubico.*"
|
|
|
|
# IDEs & Editors
|
|
"com.jetbrains.*"
|
|
"JetBrains*"
|
|
"com.microsoft.VSCode"
|
|
"com.visualstudio.code.*"
|
|
"com.sublimetext.*"
|
|
"com.sublimehq.*"
|
|
"com.microsoft.VSCodeInsiders"
|
|
"com.apple.dt.Xcode"
|
|
"com.coteditor.CotEditor"
|
|
"com.macromates.TextMate"
|
|
"com.panic.Nova"
|
|
"abnerworks.Typora"
|
|
"com.uranusjr.macdown"
|
|
|
|
# AI & LLM Tools
|
|
"com.todesktop.*"
|
|
"Cursor"
|
|
"com.anthropic.claude*"
|
|
"Claude"
|
|
"com.openai.chat*"
|
|
"ChatGPT"
|
|
"com.ollama.ollama"
|
|
"Ollama"
|
|
"com.lmstudio.lmstudio"
|
|
"LM Studio"
|
|
"co.supertool.chatbox"
|
|
"page.jan.jan"
|
|
"com.huggingface.huggingchat"
|
|
"Gemini"
|
|
"com.perplexity.Perplexity"
|
|
"com.drawthings.DrawThings"
|
|
"com.divamgupta.diffusionbee"
|
|
"com.exafunction.windsurf"
|
|
"com.quora.poe.electron"
|
|
"chat.openai.com.*"
|
|
|
|
# Database Clients
|
|
"com.sequelpro.*"
|
|
"com.sequel-ace.*"
|
|
"com.tinyapp.*"
|
|
"com.dbeaver.*"
|
|
"com.navicat.*"
|
|
"com.mongodb.compass"
|
|
"com.redis.RedisInsight"
|
|
"com.pgadmin.pgadmin4"
|
|
"com.eggerapps.Sequel-Pro"
|
|
"com.valentina-db.Valentina-Studio"
|
|
"com.dbvis.DbVisualizer"
|
|
|
|
# API & Network Tools
|
|
"com.postmanlabs.mac"
|
|
"com.konghq.insomnia"
|
|
"com.CharlesProxy.*"
|
|
"com.proxyman.*"
|
|
"com.getpaw.*"
|
|
"com.luckymarmot.Paw"
|
|
"com.charlesproxy.charles"
|
|
"com.telerik.Fiddler"
|
|
"com.usebruno.app"
|
|
|
|
# Network Proxy & VPN Tools
|
|
"*clash*"
|
|
"*Clash*"
|
|
"com.nssurge.surge-mac"
|
|
"*surge*"
|
|
"*Surge*"
|
|
"mihomo*"
|
|
"*openvpn*"
|
|
"*OpenVPN*"
|
|
"net.openvpn.*"
|
|
|
|
# Proxy Clients
|
|
"*ShadowsocksX-NG*"
|
|
"com.qiuyuzhou.*"
|
|
"*v2ray*"
|
|
"*V2Ray*"
|
|
"*v2box*"
|
|
"*V2Box*"
|
|
"*nekoray*"
|
|
"*sing-box*"
|
|
"*OneBox*"
|
|
"*hiddify*"
|
|
"*Hiddify*"
|
|
"*loon*"
|
|
"*Loon*"
|
|
"*quantumult*"
|
|
|
|
# Mesh & Corporate VPNs
|
|
"*tailscale*"
|
|
"io.tailscale.*"
|
|
"*zerotier*"
|
|
"com.zerotier.*"
|
|
"*1dot1dot1dot1*" # Cloudflare WARP
|
|
"*cloudflare*warp*"
|
|
|
|
# Commercial VPNs
|
|
"*nordvpn*"
|
|
"*expressvpn*"
|
|
"*protonvpn*"
|
|
"*surfshark*"
|
|
"*windscribe*"
|
|
"*mullvad*"
|
|
"*privateinternetaccess*"
|
|
|
|
# Screensaver & Wallpaper
|
|
"*Aerial*"
|
|
"*aerial*"
|
|
"*Fliqlo*"
|
|
"*fliqlo*"
|
|
|
|
# Git & Version Control
|
|
"com.github.GitHubDesktop"
|
|
"com.sublimemerge"
|
|
"com.torusknot.SourceTreeNotMAS"
|
|
"com.git-tower.Tower*"
|
|
"com.gitfox.GitFox"
|
|
"com.github.Gitify"
|
|
"com.fork.Fork"
|
|
"com.axosoft.gitkraken"
|
|
|
|
# Terminal & Shell
|
|
"com.googlecode.iterm2"
|
|
"net.kovidgoyal.kitty"
|
|
"io.alacritty"
|
|
"com.github.wez.wezterm"
|
|
"com.hyper.Hyper"
|
|
"com.mizage.divvy"
|
|
"com.fig.Fig"
|
|
"dev.warp.Warp-Stable"
|
|
"com.termius-dmg"
|
|
|
|
# Docker & Virtualization
|
|
"com.docker.docker"
|
|
"com.getutm.UTM"
|
|
"com.vmware.fusion"
|
|
"com.parallels.desktop.*"
|
|
"org.virtualbox.app.VirtualBox"
|
|
"com.vagrant.*"
|
|
"com.orbstack.OrbStack"
|
|
|
|
# System Monitoring
|
|
"com.bjango.istatmenus*"
|
|
"eu.exelban.Stats"
|
|
"com.monitorcontrol.*"
|
|
"com.bresink.system-toolkit.*"
|
|
"com.mediaatelier.MenuMeters"
|
|
"com.activity-indicator.app"
|
|
"net.cindori.sensei"
|
|
|
|
# Window Management
|
|
"com.macitbetter.*" # BetterTouchTool, BetterSnapTool
|
|
"com.hegenberg.*"
|
|
"com.manytricks.*" # Moom, Witch, etc.
|
|
"com.divisiblebyzero.*"
|
|
"com.koingdev.*"
|
|
"com.if.Amphetamine"
|
|
"com.lwouis.alt-tab-macos"
|
|
"net.matthewpalmer.Vanilla"
|
|
"com.lightheadsw.Caffeine"
|
|
"com.contextual.Contexts"
|
|
"com.amethyst.Amethyst"
|
|
"com.knollsoft.Rectangle"
|
|
"com.knollsoft.Hookshot"
|
|
"com.surteesstudios.Bartender"
|
|
"com.gaosun.eul"
|
|
"com.pointum.hazeover"
|
|
|
|
# Launcher & Automation
|
|
"com.runningwithcrayons.Alfred"
|
|
"com.raycast.macos"
|
|
"com.blacktree.Quicksilver"
|
|
"com.stairways.keyboardmaestro.*"
|
|
"com.manytricks.Butler"
|
|
"com.happenapps.Quitter"
|
|
"com.pilotmoon.scroll-reverser"
|
|
"org.pqrs.Karabiner-Elements"
|
|
"com.apple.Automator"
|
|
|
|
# Note-Taking
|
|
"com.bear-writer.*"
|
|
"com.typora.*"
|
|
"com.ulyssesapp.*"
|
|
"com.literatureandlatte.*"
|
|
"com.dayoneapp.*"
|
|
"notion.id"
|
|
"md.obsidian"
|
|
"com.logseq.logseq"
|
|
"com.evernote.Evernote"
|
|
"com.onenote.mac"
|
|
"com.omnigroup.OmniOutliner*"
|
|
"net.shinyfrog.bear"
|
|
"com.goodnotes.GoodNotes"
|
|
"com.marginnote.MarginNote*"
|
|
"com.roamresearch.*"
|
|
"com.reflect.ReflectApp"
|
|
"com.inkdrop.*"
|
|
|
|
# Design & Creative
|
|
"com.adobe.*"
|
|
"com.bohemiancoding.*"
|
|
"com.figma.*"
|
|
"com.framerx.*"
|
|
"com.zeplin.*"
|
|
"com.invisionapp.*"
|
|
"com.principle.*"
|
|
"com.pixelmatorteam.*"
|
|
"com.affinitydesigner.*"
|
|
"com.affinityphoto.*"
|
|
"com.affinitypublisher.*"
|
|
"com.linearity.curve"
|
|
"com.canva.CanvaDesktop"
|
|
"com.maxon.cinema4d"
|
|
"com.autodesk.*"
|
|
"com.sketchup.*"
|
|
|
|
# Communication
|
|
"com.tencent.xinWeChat"
|
|
"com.tencent.qq"
|
|
"com.alibaba.DingTalkMac"
|
|
"com.alibaba.AliLang.osx"
|
|
"com.alibaba.alilang3.osx.ShipIt"
|
|
"com.alibaba.AlilangMgr.QueryNetworkInfo"
|
|
"us.zoom.xos"
|
|
"com.microsoft.teams*"
|
|
"com.slack.Slack"
|
|
"com.hnc.Discord"
|
|
"app.legcord.Legcord"
|
|
"org.telegram.desktop"
|
|
"ru.keepcoder.Telegram"
|
|
"net.whatsapp.WhatsApp"
|
|
"com.skype.skype"
|
|
"com.cisco.webexmeetings"
|
|
"com.ringcentral.RingCentral"
|
|
"com.readdle.smartemail-Mac"
|
|
"com.airmail.*"
|
|
"com.postbox-inc.postbox"
|
|
"com.tinyspeck.slackmacgap"
|
|
|
|
# Task Management
|
|
"com.omnigroup.OmniFocus*"
|
|
"com.culturedcode.*"
|
|
"com.todoist.*"
|
|
"com.any.do.*"
|
|
"com.ticktick.*"
|
|
"com.microsoft.to-do"
|
|
"com.trello.trello"
|
|
"com.asana.nativeapp"
|
|
"com.clickup.*"
|
|
"com.monday.desktop"
|
|
"com.airtable.airtable"
|
|
"com.notion.id"
|
|
"com.linear.linear"
|
|
|
|
# File Transfer & Sync
|
|
"com.panic.transmit*"
|
|
"com.binarynights.ForkLift*"
|
|
"com.noodlesoft.Hazel"
|
|
"com.cyberduck.Cyberduck"
|
|
"io.filezilla.FileZilla"
|
|
"com.apple.Xcode.CloudDocuments"
|
|
"com.synology.*"
|
|
|
|
# Cloud Storage & Backup
|
|
"com.dropbox.*"
|
|
"com.getdropbox.*"
|
|
"*dropbox*"
|
|
"ws.agile.*"
|
|
"com.backblaze.*"
|
|
"*backblaze*"
|
|
"com.box.desktop*"
|
|
"*box.desktop*"
|
|
"com.microsoft.OneDrive*"
|
|
"com.microsoft.SyncReporter"
|
|
"*OneDrive*"
|
|
"com.google.GoogleDrive"
|
|
"com.google.keystone*"
|
|
"*GoogleDrive*"
|
|
"com.amazon.drive"
|
|
"com.apple.bird"
|
|
"com.apple.CloudDocs*"
|
|
"com.displaylink.*"
|
|
"com.fujitsu.pfu.ScanSnap*"
|
|
"com.citrix.*"
|
|
"org.xquartz.*"
|
|
"us.zoom.updater*"
|
|
"com.DigiDNA.iMazing*"
|
|
"com.shirtpocket.*"
|
|
"homebrew.mxcl.*"
|
|
|
|
# Screenshot & Recording
|
|
"com.cleanshot.*"
|
|
"com.xnipapp.xnip"
|
|
"com.reincubate.camo"
|
|
"com.tunabellysoftware.ScreenFloat"
|
|
"net.telestream.screenflow*"
|
|
"com.techsmith.snagit*"
|
|
"com.techsmith.camtasia*"
|
|
"com.obsidianapp.screenrecorder"
|
|
"com.kap.Kap"
|
|
"com.getkap.*"
|
|
"com.linebreak.CloudApp"
|
|
"com.droplr.droplr-mac"
|
|
|
|
# Media & Entertainment
|
|
"com.spotify.client"
|
|
"com.apple.Music"
|
|
"com.apple.podcasts"
|
|
"com.apple.BKAgentService"
|
|
"com.apple.iBooksX"
|
|
"com.apple.iBooks"
|
|
"com.blackmagic-design.*"
|
|
"com.colliderli.iina"
|
|
"org.videolan.vlc"
|
|
"io.mpv"
|
|
"tv.plex.player.desktop"
|
|
"com.netease.163music"
|
|
|
|
# Web Browsers
|
|
"Firefox"
|
|
"org.mozilla.*"
|
|
|
|
# License & App Stores
|
|
"com.paddle.Paddle*"
|
|
"com.setapp.DesktopClient"
|
|
"com.devmate.*"
|
|
"org.sparkle-project.Sparkle"
|
|
)
|
|
|
|
# Centralized check for critical system components (case-insensitive)
|
|
is_critical_system_component() {
|
|
local token="$1"
|
|
[[ -z "$token" ]] && return 1
|
|
|
|
local lower
|
|
lower=$(echo "$token" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
|
|
|
case "$lower" in
|
|
*backgroundtaskmanagement* | *loginitems* | *systempreferences* | *systemsettings* | *settings* | *preferences* | *controlcenter* | *biometrickit* | *sfl* | *tcc*)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Legacy function - preserved for backward compatibility
|
|
# Use should_protect_from_uninstall() or should_protect_data() instead
|
|
readonly PRESERVED_BUNDLE_PATTERNS=("${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}")
|
|
|
|
# Check if bundle ID matches pattern (glob support)
|
|
bundle_matches_pattern() {
|
|
local bundle_id="$1"
|
|
local pattern="$2"
|
|
|
|
[[ -z "$pattern" ]] && return 1
|
|
|
|
# Use bash [[ ]] for glob pattern matching (works with variables in bash 3.2+)
|
|
# shellcheck disable=SC2053 # allow glob pattern matching
|
|
if [[ "$bundle_id" == $pattern ]]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Check if application is a protected system component
|
|
should_protect_from_uninstall() {
|
|
local bundle_id="$1"
|
|
|
|
# First check if it's an uninstallable Apple app
|
|
# These apps have com.apple.* bundle IDs but are NOT system-critical
|
|
for pattern in "${APPLE_UNINSTALLABLE_APPS[@]}"; do
|
|
if bundle_matches_pattern "$bundle_id" "$pattern"; then
|
|
return 1 # Can be uninstalled
|
|
fi
|
|
done
|
|
|
|
# Then check system-critical components
|
|
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do
|
|
if bundle_matches_pattern "$bundle_id" "$pattern"; then
|
|
return 0 # Protected
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Check if application data should be protected during cleanup
|
|
should_protect_data() {
|
|
local bundle_id="$1"
|
|
# Protect both system critical and data protected bundles during cleanup
|
|
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do
|
|
if bundle_matches_pattern "$bundle_id" "$pattern"; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Check if a path is protected from deletion
|
|
# Centralized logic to protect system settings, control center, and critical apps
|
|
#
|
|
# In uninstall mode (MOLE_UNINSTALL_MODE=1), only system-critical components are protected.
|
|
# Data-protected apps (VPNs, dev tools, etc.) can be uninstalled when user explicitly chooses to.
|
|
#
|
|
# Args: $1 - path to check
|
|
# Returns: 0 if protected, 1 if safe to delete
|
|
should_protect_path() {
|
|
local path="$1"
|
|
[[ -z "$path" ]] && return 1
|
|
|
|
local path_lower
|
|
path_lower=$(echo "$path" | LC_ALL=C tr '[:upper:]' '[:lower:]')
|
|
|
|
# 1. Keyword-based matching for system components
|
|
# Protect System Settings, Preferences, Control Center, and related XPC services
|
|
# Also protect "Settings" (used in macOS Sequoia) and savedState files
|
|
if [[ "$path_lower" =~ systemsettings || "$path_lower" =~ systempreferences || "$path_lower" =~ controlcenter ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Additional check for com.apple.Settings (macOS Sequoia System Settings)
|
|
if [[ "$path_lower" =~ com\.apple\.settings ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Protect Notes cache (search index issues)
|
|
if [[ "$path_lower" =~ com\.apple\.notes ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# 2. Protect caches critical for system UI rendering
|
|
# These caches are essential for modern macOS (Sonoma/Sequoia) system UI rendering
|
|
case "$path" in
|
|
# System Settings and Control Center caches (CRITICAL - prevents blank panel bug)
|
|
*com.apple.systempreferences.cache* | *com.apple.Settings.cache* | *com.apple.controlcenter.cache*)
|
|
return 0
|
|
;;
|
|
# Finder and Dock (system essential)
|
|
*com.apple.finder.cache* | *com.apple.dock.cache*)
|
|
return 0
|
|
;;
|
|
# System XPC services and sandboxed containers
|
|
*/Library/Containers/com.apple.Settings* | */Library/Containers/com.apple.SystemSettings* | */Library/Containers/com.apple.controlcenter*)
|
|
return 0
|
|
;;
|
|
*/Library/Group\ Containers/com.apple.systempreferences* | */Library/Group\ Containers/com.apple.Settings*)
|
|
return 0
|
|
;;
|
|
# Shared file lists for System Settings (macOS Sequoia) - Issue #136
|
|
*/com.apple.sharedfilelist/*com.apple.Settings* | */com.apple.sharedfilelist/*com.apple.SystemSettings* | */com.apple.sharedfilelist/*systempreferences*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# 3. Extract bundle ID from sandbox paths
|
|
# Matches: .../Library/Containers/bundle.id/...
|
|
# Matches: .../Library/Group Containers/group.id/...
|
|
if [[ "$path" =~ /Library/Containers/([^/]+) ]] || [[ "$path" =~ /Library/Group\ Containers/([^/]+) ]]; then
|
|
local bundle_id="${BASH_REMATCH[1]}"
|
|
if should_protect_data "$bundle_id"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
# 4. Check for specific hardcoded critical patterns
|
|
case "$path" in
|
|
*com.apple.Settings* | *com.apple.SystemSettings* | *com.apple.controlcenter* | *com.apple.finder* | *com.apple.dock*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# 5. Protect critical preference files and user data
|
|
case "$path" in
|
|
*/Library/Preferences/com.apple.dock.plist | */Library/Preferences/com.apple.finder.plist)
|
|
return 0
|
|
;;
|
|
# Bluetooth and WiFi configurations
|
|
*/ByHost/com.apple.bluetooth.* | */ByHost/com.apple.wifi.*)
|
|
return 0
|
|
;;
|
|
# iCloud Drive - protect user's cloud synced data
|
|
*/Library/Mobile\ Documents* | */Mobile\ Documents*)
|
|
return 0
|
|
;;
|
|
esac
|
|
|
|
# 6. Match full path against protected patterns
|
|
# This catches things like /Users/tw93/Library/Caches/Claude when pattern is *Claude*
|
|
# In uninstall mode, only check system-critical bundles (user explicitly chose to uninstall)
|
|
if [[ "${MOLE_UNINSTALL_MODE:-0}" == "1" ]]; then
|
|
# Uninstall mode: first check if it's an uninstallable Apple app
|
|
for pattern in "${APPLE_UNINSTALLABLE_APPS[@]}"; do
|
|
if bundle_matches_pattern "$path" "$pattern"; then
|
|
return 1 # Can be uninstalled
|
|
fi
|
|
done
|
|
# Then check system-critical components
|
|
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do
|
|
if bundle_matches_pattern "$path" "$pattern"; then
|
|
return 0
|
|
fi
|
|
done
|
|
else
|
|
# Normal mode (cleanup): protect both system-critical and data-protected bundles
|
|
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do
|
|
if bundle_matches_pattern "$path" "$pattern"; then
|
|
return 0
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# 7. Check if the filename itself matches any protected patterns
|
|
# Skip in uninstall mode - user explicitly chose to remove this app
|
|
if [[ "${MOLE_UNINSTALL_MODE:-0}" != "1" ]]; then
|
|
local filename
|
|
filename=$(basename "$path")
|
|
if should_protect_data "$filename"; then
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
return 1
|
|
}
|
|
|
|
# Check if a path is protected by whitelist patterns
|
|
# Args: $1 - path to check
|
|
# Returns: 0 if whitelisted, 1 if not
|
|
is_path_whitelisted() {
|
|
local target_path="$1"
|
|
[[ -z "$target_path" ]] && return 1
|
|
|
|
# Normalize path (remove trailing slash)
|
|
local normalized_target="${target_path%/}"
|
|
|
|
# Empty whitelist means nothing is protected
|
|
[[ ${#WHITELIST_PATTERNS[@]} -eq 0 ]] && return 1
|
|
|
|
for pattern in "${WHITELIST_PATTERNS[@]}"; do
|
|
# Pattern is already expanded/normalized in bin/clean.sh
|
|
local check_pattern="${pattern%/}"
|
|
local has_glob="false"
|
|
case "$check_pattern" in
|
|
*\** | *\?* | *\[*)
|
|
has_glob="true"
|
|
;;
|
|
esac
|
|
|
|
# Check for exact match or glob pattern match
|
|
# shellcheck disable=SC2053
|
|
if [[ "$normalized_target" == "$check_pattern" ]] ||
|
|
[[ "$normalized_target" == $check_pattern ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Check if target is a parent directory of a whitelisted path
|
|
# e.g., if pattern is /path/to/dir/subdir and target is /path/to/dir,
|
|
# the target should be protected to preserve its whitelisted children
|
|
if [[ "$check_pattern" == "$normalized_target"/* ]]; then
|
|
return 0
|
|
fi
|
|
|
|
# Check if target is a child of a whitelisted directory path
|
|
if [[ "$has_glob" == "false" && "$normalized_target" == "$check_pattern"/* ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Locate files associated with an application
|
|
find_app_files() {
|
|
local bundle_id="$1"
|
|
local app_name="$2"
|
|
|
|
# Early validation: require at least one valid identifier
|
|
# Skip scanning if both bundle_id and app_name are invalid
|
|
if [[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] &&
|
|
[[ -z "$app_name" || ${#app_name} -lt 2 ]]; then
|
|
return 0 # Silent return to avoid invalid scanning
|
|
fi
|
|
|
|
local -a files_to_clean=()
|
|
|
|
# Normalize app name for matching - generate all common naming variants
|
|
# Apps use inconsistent naming: "Maestro Studio" vs "maestro-studio" vs "MaestroStudio"
|
|
# Note: Using tr for lowercase conversion (Bash 3.2 compatible, no ${var,,} support)
|
|
local nospace_name="${app_name// /}" # "Maestro Studio" -> "MaestroStudio"
|
|
local underscore_name="${app_name// /_}" # "Maestro Studio" -> "Maestro_Studio"
|
|
local hyphen_name="${app_name// /-}" # "Maestro Studio" -> "Maestro-Studio"
|
|
local lowercase_name=$(echo "$app_name" | tr '[:upper:]' '[:lower:]') # "Zed Nightly" -> "zed nightly"
|
|
local lowercase_nospace=$(echo "$nospace_name" | tr '[:upper:]' '[:lower:]') # "MaestroStudio" -> "maestrostudio"
|
|
local lowercase_hyphen=$(echo "$hyphen_name" | tr '[:upper:]' '[:lower:]') # "Maestro-Studio" -> "maestro-studio"
|
|
local lowercase_underscore=$(echo "$underscore_name" | tr '[:upper:]' '[:lower:]') # "Maestro_Studio" -> "maestro_studio"
|
|
|
|
# Extract base name by removing common version/channel suffixes
|
|
# "Zed Nightly" -> "Zed", "Firefox Developer Edition" -> "Firefox"
|
|
local base_name="$app_name"
|
|
local version_suffixes="Nightly|Beta|Alpha|Dev|Canary|Preview|Insider|Edge|Stable|Release|RC|LTS"
|
|
version_suffixes+="|Developer Edition|Technology Preview"
|
|
if [[ "$app_name" =~ ^(.+)[[:space:]]+(${version_suffixes})$ ]]; then
|
|
base_name="${BASH_REMATCH[1]}"
|
|
fi
|
|
local base_lowercase=$(echo "$base_name" | tr '[:upper:]' '[:lower:]') # "Zed" -> "zed"
|
|
|
|
# Standard path patterns for user-level files
|
|
local -a user_patterns=(
|
|
"$HOME/Library/Application Support/$app_name"
|
|
"$HOME/Library/Application Support/$bundle_id"
|
|
"$HOME/Library/Caches/$bundle_id"
|
|
"$HOME/Library/Caches/$app_name"
|
|
"$HOME/Library/Logs/$app_name"
|
|
"$HOME/Library/Logs/$bundle_id"
|
|
"$HOME/Library/Application Support/CrashReporter/$app_name"
|
|
"$HOME/Library/Saved Application State/$bundle_id.savedState"
|
|
"$HOME/Library/Containers/$bundle_id"
|
|
"$HOME/Library/WebKit/$bundle_id"
|
|
"$HOME/Library/WebKit/com.apple.WebKit.WebContent/$bundle_id"
|
|
"$HOME/Library/HTTPStorages/$bundle_id"
|
|
"$HOME/Library/Cookies/$bundle_id.binarycookies"
|
|
"$HOME/Library/LaunchAgents/$bundle_id.plist"
|
|
"$HOME/Library/Application Scripts/$bundle_id"
|
|
"$HOME/Library/Services/$app_name.workflow"
|
|
"$HOME/Library/QuickLook/$app_name.qlgenerator"
|
|
"$HOME/Library/Internet Plug-Ins/$app_name.plugin"
|
|
"$HOME/Library/Audio/Plug-Ins/Components/$app_name.component"
|
|
"$HOME/Library/Audio/Plug-Ins/VST/$app_name.vst"
|
|
"$HOME/Library/Audio/Plug-Ins/VST3/$app_name.vst3"
|
|
"$HOME/Library/Audio/Plug-Ins/Digidesign/$app_name.dpm"
|
|
"$HOME/Library/PreferencePanes/$app_name.prefPane"
|
|
"$HOME/Library/Input Methods/$app_name.app"
|
|
"$HOME/Library/Input Methods/$bundle_id.app"
|
|
"$HOME/Library/Screen Savers/$app_name.saver"
|
|
"$HOME/Library/Frameworks/$app_name.framework"
|
|
"$HOME/Library/Autosave Information/$bundle_id"
|
|
"$HOME/Library/Contextual Menu Items/$app_name.plugin"
|
|
"$HOME/Library/Spotlight/$app_name.mdimporter"
|
|
"$HOME/Library/ColorPickers/$app_name.colorPicker"
|
|
"$HOME/Library/Workflows/$app_name.workflow"
|
|
"$HOME/.config/$app_name"
|
|
"$HOME/.local/share/$app_name"
|
|
"$HOME/.$app_name"
|
|
"$HOME/.$app_name"rc
|
|
)
|
|
|
|
# Add all naming variants to cover inconsistent app directory naming
|
|
# Issue #377: Apps create directories with various naming conventions
|
|
if [[ ${#app_name} -gt 3 && "$app_name" =~ [[:space:]] ]]; then
|
|
user_patterns+=(
|
|
# Compound naming (MaestroStudio, Maestro_Studio, Maestro-Studio)
|
|
"$HOME/Library/Application Support/$nospace_name"
|
|
"$HOME/Library/Caches/$nospace_name"
|
|
"$HOME/Library/Logs/$nospace_name"
|
|
"$HOME/Library/Application Support/$underscore_name"
|
|
"$HOME/Library/Application Support/$hyphen_name"
|
|
# Lowercase variants (maestrostudio, maestro-studio, maestro_studio)
|
|
"$HOME/.config/$lowercase_nospace"
|
|
"$HOME/.config/$lowercase_hyphen"
|
|
"$HOME/.config/$lowercase_underscore"
|
|
"$HOME/.local/share/$lowercase_nospace"
|
|
"$HOME/.local/share/$lowercase_hyphen"
|
|
"$HOME/.local/share/$lowercase_underscore"
|
|
)
|
|
fi
|
|
|
|
# Add base name variants for versioned apps (e.g., "Zed Nightly" -> check for "zed")
|
|
if [[ "$base_name" != "$app_name" && ${#base_name} -gt 2 ]]; then
|
|
user_patterns+=(
|
|
"$HOME/Library/Application Support/$base_name"
|
|
"$HOME/Library/Caches/$base_name"
|
|
"$HOME/Library/Logs/$base_name"
|
|
"$HOME/.config/$base_lowercase"
|
|
"$HOME/.local/share/$base_lowercase"
|
|
"$HOME/.$base_lowercase"
|
|
)
|
|
fi
|
|
|
|
# Process standard patterns
|
|
for p in "${user_patterns[@]}"; do
|
|
local expanded_path="${p/#\~/$HOME}"
|
|
# Skip if path doesn't exist
|
|
[[ ! -e "$expanded_path" ]] && continue
|
|
|
|
# Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id)
|
|
# This prevents deletion of entire Library subdirectories when bundle_id is empty
|
|
case "$expanded_path" in
|
|
*/Library/Application\ Support | */Library/Application\ Support/ | \
|
|
*/Library/Caches | */Library/Caches/ | \
|
|
*/Library/Logs | */Library/Logs/ | \
|
|
*/Library/Containers | */Library/Containers/ | \
|
|
*/Library/WebKit | */Library/WebKit/ | \
|
|
*/Library/HTTPStorages | */Library/HTTPStorages/ | \
|
|
*/Library/Application\ Scripts | */Library/Application\ Scripts/ | \
|
|
*/Library/Autosave\ Information | */Library/Autosave\ Information/ | \
|
|
*/Library/Group\ Containers | */Library/Group\ Containers/)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
files_to_clean+=("$expanded_path")
|
|
done
|
|
|
|
# Handle Preferences and ByHost variants (only if bundle_id is valid)
|
|
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" && ${#bundle_id} -gt 3 ]]; then
|
|
[[ -f ~/Library/Preferences/"$bundle_id".plist ]] && files_to_clean+=("$HOME/Library/Preferences/$bundle_id.plist")
|
|
[[ -d ~/Library/Preferences/ByHost ]] && while IFS= read -r -d '' pref; do
|
|
files_to_clean+=("$pref")
|
|
done < <(command find ~/Library/Preferences/ByHost -maxdepth 1 \( -name "$bundle_id*.plist" \) -print0 2> /dev/null)
|
|
|
|
# Group Containers (special handling)
|
|
if [[ -d ~/Library/Group\ Containers ]]; then
|
|
while IFS= read -r -d '' container; do
|
|
files_to_clean+=("$container")
|
|
done < <(command find ~/Library/Group\ Containers -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
|
fi
|
|
fi
|
|
|
|
# Launch Agents by name (special handling)
|
|
# Note: LaunchDaemons are system-level and handled in find_app_system_files()
|
|
# Minimum 5-char threshold prevents false positives (e.g., "Time" matching system agents)
|
|
# Short-name apps (e.g., Zoom, Arc) are still cleaned via bundle_id matching above
|
|
# Security: Common words are excluded to prevent matching unrelated plist files
|
|
if [[ ${#app_name} -ge 5 ]] && [[ -d ~/Library/LaunchAgents ]]; then
|
|
# Skip common words that could match many unrelated LaunchAgents
|
|
# These are either generic terms or names that overlap with system/common utilities
|
|
local common_words="Music|Notes|Photos|Finder|Safari|Preview|Calendar|Contacts|Messages|Reminders|Clock|Weather|Stocks|Books|News|Podcasts|Voice|Files|Store|System|Helper|Agent|Daemon|Service|Update|Sync|Backup|Cloud|Manager|Monitor|Server|Client|Worker|Runner|Launcher|Driver|Plugin|Extension|Widget|Utility"
|
|
if [[ "$app_name" =~ ^($common_words)$ ]]; then
|
|
debug_log "Skipping LaunchAgent name search for common word: $app_name"
|
|
else
|
|
while IFS= read -r -d '' plist; do
|
|
local plist_name=$(basename "$plist")
|
|
# Skip Apple's LaunchAgents
|
|
if [[ "$plist_name" =~ ^com\.apple\. ]]; then
|
|
continue
|
|
fi
|
|
files_to_clean+=("$plist")
|
|
done < <(command find ~/Library/LaunchAgents -maxdepth 1 -name "*$app_name*.plist" -print0 2> /dev/null)
|
|
fi
|
|
fi
|
|
|
|
# Handle specialized toolchains and development environments
|
|
# 1. DevEco-Studio (Huawei)
|
|
if [[ "$app_name" =~ DevEco|deveco ]] || [[ "$bundle_id" =~ huawei.*deveco ]]; then
|
|
for d in ~/DevEcoStudioProjects ~/DevEco-Studio ~/Library/Application\ Support/Huawei ~/Library/Caches/Huawei ~/Library/Logs/Huawei ~/Library/Huawei ~/Huawei ~/HarmonyOS ~/.huawei ~/.ohos; do
|
|
[[ -d "$d" ]] && files_to_clean+=("$d")
|
|
done
|
|
fi
|
|
|
|
# 2. Android Studio (Google)
|
|
if [[ "$app_name" =~ Android.*Studio|android.*studio ]] || [[ "$bundle_id" =~ google.*android.*studio|jetbrains.*android ]]; then
|
|
for d in ~/AndroidStudioProjects ~/Library/Android ~/.android; do
|
|
[[ -d "$d" ]] && files_to_clean+=("$d")
|
|
done
|
|
[[ -d ~/Library/Application\ Support/Google ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find ~/Library/Application\ Support/Google -maxdepth 1 -name "AndroidStudio*" -print0 2> /dev/null)
|
|
fi
|
|
|
|
# 3. Xcode (Apple)
|
|
if [[ "$app_name" =~ Xcode|xcode ]] || [[ "$bundle_id" =~ apple.*xcode ]]; then
|
|
[[ -d ~/Library/Developer ]] && files_to_clean+=("$HOME/Library/Developer")
|
|
[[ -d ~/.Xcode ]] && files_to_clean+=("$HOME/.Xcode")
|
|
fi
|
|
|
|
# 4. JetBrains (IDE settings)
|
|
if [[ "$bundle_id" =~ jetbrains ]] || [[ "$app_name" =~ IntelliJ|PyCharm|WebStorm|GoLand|RubyMine|PhpStorm|CLion|DataGrip|Rider ]]; then
|
|
for base in ~/Library/Application\ Support/JetBrains ~/Library/Caches/JetBrains ~/Library/Logs/JetBrains; do
|
|
[[ -d "$base" ]] && while IFS= read -r -d '' d; do files_to_clean+=("$d"); done < <(command find "$base" -maxdepth 1 -name "${app_name}*" -print0 2> /dev/null)
|
|
done
|
|
fi
|
|
|
|
# 5. Unity / Unreal / Godot
|
|
[[ "$app_name" =~ Unity|unity ]] && [[ -d ~/Library/Unity ]] && files_to_clean+=("$HOME/Library/Unity")
|
|
[[ "$app_name" =~ Unreal|unreal ]] && [[ -d ~/Library/Application\ Support/Epic ]] && files_to_clean+=("$HOME/Library/Application Support/Epic")
|
|
[[ "$app_name" =~ Godot|godot ]] && [[ -d ~/Library/Application\ Support/Godot ]] && files_to_clean+=("$HOME/Library/Application Support/Godot")
|
|
|
|
# 6. Tools
|
|
[[ "$bundle_id" =~ microsoft.*vscode ]] && [[ -d ~/.vscode ]] && files_to_clean+=("$HOME/.vscode")
|
|
[[ "$app_name" =~ Docker ]] && [[ -d ~/.docker ]] && files_to_clean+=("$HOME/.docker")
|
|
|
|
# Output results
|
|
if [[ ${#files_to_clean[@]} -gt 0 ]]; then
|
|
printf '%s\n' "${files_to_clean[@]}"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Locate system-level application files
|
|
find_app_system_files() {
|
|
local bundle_id="$1"
|
|
local app_name="$2"
|
|
local -a system_files=()
|
|
|
|
# Generate all naming variants (same as find_app_files for consistency)
|
|
local nospace_name="${app_name// /}"
|
|
local underscore_name="${app_name// /_}"
|
|
local hyphen_name="${app_name// /-}"
|
|
local lowercase_hyphen=$(echo "$hyphen_name" | tr '[:upper:]' '[:lower:]')
|
|
|
|
# Standard system path patterns
|
|
local -a system_patterns=(
|
|
"/Library/Application Support/$app_name"
|
|
"/Library/Application Support/$bundle_id"
|
|
"/Library/LaunchAgents/$bundle_id.plist"
|
|
"/Library/LaunchDaemons/$bundle_id.plist"
|
|
"/Library/Preferences/$bundle_id.plist"
|
|
"/Library/Receipts/$bundle_id.bom"
|
|
"/Library/Receipts/$bundle_id.plist"
|
|
"/Library/Frameworks/$app_name.framework"
|
|
"/Library/Internet Plug-Ins/$app_name.plugin"
|
|
"/Library/Input Methods/$app_name.app"
|
|
"/Library/Input Methods/$bundle_id.app"
|
|
"/Library/Audio/Plug-Ins/Components/$app_name.component"
|
|
"/Library/Audio/Plug-Ins/VST/$app_name.vst"
|
|
"/Library/Audio/Plug-Ins/VST3/$app_name.vst3"
|
|
"/Library/Audio/Plug-Ins/Digidesign/$app_name.dpm"
|
|
"/Library/QuickLook/$app_name.qlgenerator"
|
|
"/Library/PreferencePanes/$app_name.prefPane"
|
|
"/Library/Screen Savers/$app_name.saver"
|
|
"/Library/Caches/$bundle_id"
|
|
"/Library/Caches/$app_name"
|
|
)
|
|
|
|
# Add all naming variants for apps with spaces in name
|
|
if [[ ${#app_name} -gt 3 && "$app_name" =~ [[:space:]] ]]; then
|
|
system_patterns+=(
|
|
"/Library/Application Support/$nospace_name"
|
|
"/Library/Caches/$nospace_name"
|
|
"/Library/Logs/$nospace_name"
|
|
"/Library/Application Support/$underscore_name"
|
|
"/Library/Application Support/$hyphen_name"
|
|
"/Library/Caches/$hyphen_name"
|
|
"/Library/Caches/$lowercase_hyphen"
|
|
)
|
|
fi
|
|
|
|
# Process patterns
|
|
for p in "${system_patterns[@]}"; do
|
|
[[ ! -e "$p" ]] && continue
|
|
|
|
# Safety check: Skip if path ends with a common directory name (indicates empty app_name/bundle_id)
|
|
case "$p" in
|
|
/Library/Application\ Support | /Library/Application\ Support/ | \
|
|
/Library/Caches | /Library/Caches/ | \
|
|
/Library/Logs | /Library/Logs/)
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
system_files+=("$p")
|
|
done
|
|
|
|
# System LaunchAgents/LaunchDaemons by name
|
|
if [[ ${#app_name} -gt 3 ]]; then
|
|
for base in /Library/LaunchAgents /Library/LaunchDaemons; do
|
|
[[ -d "$base" ]] && while IFS= read -r -d '' plist; do
|
|
system_files+=("$plist")
|
|
done < <(command find "$base" -maxdepth 1 \( -name "*$app_name*.plist" \) -print0 2> /dev/null)
|
|
done
|
|
fi
|
|
|
|
# Privileged Helper Tools and Receipts (special handling)
|
|
# Only search with bundle_id if it's valid (not empty and not "unknown")
|
|
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" && ${#bundle_id} -gt 3 ]]; then
|
|
[[ -d /Library/PrivilegedHelperTools ]] && while IFS= read -r -d '' helper; do
|
|
system_files+=("$helper")
|
|
done < <(command find /Library/PrivilegedHelperTools -maxdepth 1 \( -name "$bundle_id*" \) -print0 2> /dev/null)
|
|
|
|
[[ -d /private/var/db/receipts ]] && while IFS= read -r -d '' receipt; do
|
|
system_files+=("$receipt")
|
|
done < <(command find /private/var/db/receipts -maxdepth 1 \( -name "*$bundle_id*" \) -print0 2> /dev/null)
|
|
fi
|
|
|
|
local receipt_files=""
|
|
receipt_files=$(find_app_receipt_files "$bundle_id")
|
|
|
|
local combined_files=""
|
|
if [[ ${#system_files[@]} -gt 0 ]]; then
|
|
combined_files=$(printf '%s\n' "${system_files[@]}")
|
|
fi
|
|
|
|
if [[ -n "$receipt_files" ]]; then
|
|
if [[ -n "$combined_files" ]]; then
|
|
combined_files+=$'\n'
|
|
fi
|
|
combined_files+="$receipt_files"
|
|
fi
|
|
|
|
if [[ -n "$combined_files" ]]; then
|
|
printf '%s\n' "$combined_files" | sort -u
|
|
fi
|
|
}
|
|
|
|
# Locate files using installation receipts (BOM)
|
|
find_app_receipt_files() {
|
|
local bundle_id="$1"
|
|
|
|
# Skip if no bundle ID
|
|
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
|
|
|
|
# Validate bundle_id format to prevent wildcard injection
|
|
# Only allow alphanumeric characters, dots, hyphens, and underscores
|
|
if [[ ! "$bundle_id" =~ ^[a-zA-Z0-9._-]+$ ]]; then
|
|
debug_log "Invalid bundle_id format: $bundle_id"
|
|
return 0
|
|
fi
|
|
|
|
local -a receipt_files=()
|
|
local -a bom_files=()
|
|
|
|
# Find receipts matching the bundle ID
|
|
# Usually in /var/db/receipts/
|
|
if [[ -d /private/var/db/receipts ]]; then
|
|
while IFS= read -r -d '' bom; do
|
|
bom_files+=("$bom")
|
|
done < <(find /private/var/db/receipts -maxdepth 1 -name "${bundle_id}*.bom" -print0 2> /dev/null)
|
|
fi
|
|
|
|
# Process bom files if any found
|
|
if [[ ${#bom_files[@]} -gt 0 ]]; then
|
|
for bom_file in "${bom_files[@]}"; do
|
|
[[ ! -f "$bom_file" ]] && continue
|
|
|
|
# Parse bom file
|
|
# lsbom -f: file paths only
|
|
# -s: suppress output (convert to text)
|
|
local bom_content
|
|
bom_content=$(lsbom -f -s "$bom_file" 2> /dev/null)
|
|
|
|
while IFS= read -r file_path; do
|
|
# Standardize path (remove leading dot)
|
|
local clean_path="${file_path#.}"
|
|
|
|
# Ensure absolute path
|
|
if [[ "$clean_path" != /* ]]; then
|
|
clean_path="/$clean_path"
|
|
fi
|
|
|
|
# Path traversal protection: reject paths containing ..
|
|
if [[ "$clean_path" =~ \.\. ]]; then
|
|
debug_log "Rejected path traversal in BOM: $clean_path"
|
|
continue
|
|
fi
|
|
|
|
# Normalize path (remove duplicate slashes)
|
|
clean_path=$(tr -s "/" <<< "$clean_path")
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Safety check: restrict removal to trusted paths
|
|
# ------------------------------------------------------------------------
|
|
local is_safe=false
|
|
|
|
# Whitelisted prefixes (exclude /Users, /usr, /opt)
|
|
case "$clean_path" in
|
|
/Applications/*) is_safe=true ;;
|
|
/Library/Application\ Support/*) is_safe=true ;;
|
|
/Library/Caches/*) is_safe=true ;;
|
|
/Library/Logs/*) is_safe=true ;;
|
|
/Library/Preferences/*) is_safe=true ;;
|
|
/Library/LaunchAgents/*) is_safe=true ;;
|
|
/Library/LaunchDaemons/*) is_safe=true ;;
|
|
/Library/PrivilegedHelperTools/*) is_safe=true ;;
|
|
/Library/Extensions/*) is_safe=false ;;
|
|
*) is_safe=false ;;
|
|
esac
|
|
|
|
# Hard blocks
|
|
case "$clean_path" in
|
|
/System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/*) is_safe=false ;;
|
|
esac
|
|
|
|
if [[ "$is_safe" == "true" && -e "$clean_path" ]]; then
|
|
# Skip top-level directories
|
|
if [[ "$clean_path" == "/Applications" || "$clean_path" == "/Library" ]]; then
|
|
continue
|
|
fi
|
|
|
|
if declare -f should_protect_path > /dev/null 2>&1; then
|
|
if should_protect_path "$clean_path"; then
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
receipt_files+=("$clean_path")
|
|
fi
|
|
|
|
done <<< "$bom_content"
|
|
done
|
|
fi
|
|
if [[ ${#receipt_files[@]} -gt 0 ]]; then
|
|
printf '%s\n' "${receipt_files[@]}"
|
|
fi
|
|
}
|
|
|
|
# Terminate a running application
|
|
force_kill_app() {
|
|
# Gracefully terminates or force-kills an application
|
|
local app_name="$1"
|
|
local app_path="${2:-""}"
|
|
|
|
# Get the executable name from bundle if app_path is provided
|
|
local exec_name=""
|
|
if [[ -n "$app_path" && -e "$app_path/Contents/Info.plist" ]]; then
|
|
exec_name=$(defaults read "$app_path/Contents/Info.plist" CFBundleExecutable 2> /dev/null || echo "")
|
|
fi
|
|
|
|
# Use executable name for precise matching, fallback to app name
|
|
local match_pattern="${exec_name:-$app_name}"
|
|
|
|
# Check if process is running using exact match only
|
|
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
# Try graceful termination first
|
|
pkill -x "$match_pattern" 2> /dev/null || true
|
|
sleep 2
|
|
|
|
# Check again after graceful kill
|
|
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
|
|
# Force kill if still running
|
|
pkill -9 -x "$match_pattern" 2> /dev/null || true
|
|
sleep 2
|
|
|
|
# If still running and sudo is available, try with sudo
|
|
if pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
|
if sudo -n true 2> /dev/null; then
|
|
sudo pkill -9 -x "$match_pattern" 2> /dev/null || true
|
|
sleep 2
|
|
fi
|
|
fi
|
|
|
|
# Final check with longer timeout for stubborn processes
|
|
local retries=3
|
|
while [[ $retries -gt 0 ]]; do
|
|
if ! pgrep -x "$match_pattern" > /dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
((retries--))
|
|
done
|
|
|
|
# Still running after all attempts
|
|
pgrep -x "$match_pattern" > /dev/null 2>&1 && return 1 || return 0
|
|
}
|
|
|
|
# Note: calculate_total_size() is defined in lib/core/file_ops.sh
|