mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 14:26:46 +00:00
Previously, apps matching DATA_PROTECTED_BUNDLES patterns (VPNs, dev tools, etc.) could not be uninstalled because should_protect_path blocked their deletion. Now use MOLE_UNINSTALL_MODE to distinguish between cleanup and explicit uninstall, allowing users to remove these apps when they choose to while still protecting their data during normal cleanup operations. Also allow deletion of installer receipts in /private/var/db/receipts/.
1081 lines
42 KiB
Bash
Executable File
1081 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
|
|
readonly SYSTEM_CRITICAL_BUNDLES=(
|
|
"com.apple.*" # System essentials
|
|
"loginwindow"
|
|
"dock"
|
|
"systempreferences"
|
|
"finder"
|
|
"safari"
|
|
"com.apple.Settings*"
|
|
"com.apple.SystemSettings*"
|
|
"com.apple.controlcenter*"
|
|
"com.apple.backgroundtaskmanagement*"
|
|
"com.apple.loginitems*"
|
|
"com.apple.sharedfilelist*"
|
|
"com.apple.sfl*"
|
|
"backgroundtaskmanagementagent"
|
|
"keychain*"
|
|
"security*"
|
|
"bluetooth*"
|
|
"wifi*"
|
|
"network*"
|
|
"tcc"
|
|
"notification*"
|
|
"accessibility*"
|
|
"universalaccess*"
|
|
"HIToolbox*"
|
|
"textinput*"
|
|
"TextInput*"
|
|
"keyboard*"
|
|
"Keyboard*"
|
|
"inputsource*"
|
|
"InputSource*"
|
|
"keylayout*"
|
|
"KeyLayout*"
|
|
"GlobalPreferences"
|
|
".GlobalPreferences"
|
|
"com.apple.inputmethod.*"
|
|
"org.pqrs.Karabiner*"
|
|
"com.apple.inputsource*"
|
|
"com.apple.TextInputMenuAgent"
|
|
"com.apple.TextInputSwitcher"
|
|
)
|
|
|
|
# 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 Tools
|
|
"com.nektony.*" # App Cleaner & Uninstaller
|
|
"com.macpaw.*" # CleanMyMac, CleanMaster
|
|
"com.freemacsoft.AppCleaner" # AppCleaner
|
|
"com.omnigroup.omnidisksweeper" # OmniDiskSweeper
|
|
"com.daisydiskapp.*" # DaisyDisk
|
|
"com.tunabellysoftware.*" # Disk Utility apps
|
|
"com.grandperspectiv.*" # GrandPerspective
|
|
"com.binaryfruit.*" # FusionCast
|
|
|
|
# Password Managers & Security
|
|
"com.1password.*" # 1Password
|
|
"com.agilebits.*" # 1Password legacy
|
|
"com.lastpass.*" # LastPass
|
|
"com.dashlane.*" # Dashlane
|
|
"com.bitwarden.*" # Bitwarden
|
|
"com.keepassx.*" # KeePassXC (Legacy)
|
|
"org.keepassx.*" # KeePassX
|
|
"org.keepassxc.*" # KeePassXC
|
|
"com.authy.*" # Authy
|
|
"com.yubico.*" # YubiKey Manager
|
|
|
|
# Development Tools - IDEs & Editors
|
|
"com.jetbrains.*" # JetBrains IDEs (IntelliJ, DataGrip, etc.)
|
|
"JetBrains*" # JetBrains Application Support folders
|
|
"com.microsoft.VSCode" # Visual Studio Code
|
|
"com.visualstudio.code.*" # VS Code variants
|
|
"com.sublimetext.*" # Sublime Text
|
|
"com.sublimehq.*" # Sublime Merge
|
|
"com.microsoft.VSCodeInsiders" # VS Code Insiders
|
|
"com.apple.dt.Xcode" # Xcode (keep settings)
|
|
"com.coteditor.CotEditor" # CotEditor
|
|
"com.macromates.TextMate" # TextMate
|
|
"com.panic.Nova" # Nova
|
|
"abnerworks.Typora" # Typora (Markdown editor)
|
|
"com.uranusjr.macdown" # MacDown
|
|
|
|
# AI & LLM Tools
|
|
"com.todesktop.*" # Cursor (often uses generic todesktop ID)
|
|
"Cursor" # Cursor App Support
|
|
"com.anthropic.claude*" # Claude
|
|
"Claude" # Claude App Support
|
|
"com.openai.chat*" # ChatGPT
|
|
"ChatGPT" # ChatGPT App Support
|
|
"com.ollama.ollama" # Ollama
|
|
"Ollama" # Ollama App Support
|
|
"com.lmstudio.lmstudio" # LM Studio
|
|
"LM Studio" # LM Studio App Support
|
|
"co.supertool.chatbox" # Chatbox
|
|
"page.jan.jan" # Jan
|
|
"com.huggingface.huggingchat" # HuggingChat
|
|
"Gemini" # Gemini
|
|
"com.perplexity.Perplexity" # Perplexity
|
|
"com.drawthings.DrawThings" # Draw Things
|
|
"com.divamgupta.diffusionbee" # DiffusionBee
|
|
"com.exafunction.windsurf" # Windsurf
|
|
"com.quora.poe.electron" # Poe
|
|
"chat.openai.com.*" # OpenAI web wrappers
|
|
|
|
# Development Tools - Database Clients
|
|
"com.sequelpro.*" # Sequel Pro
|
|
"com.sequel-ace.*" # Sequel Ace
|
|
"com.tinyapp.*" # TablePlus
|
|
"com.dbeaver.*" # DBeaver
|
|
"com.navicat.*" # Navicat
|
|
"com.mongodb.compass" # MongoDB Compass
|
|
"com.redis.RedisInsight" # Redis Insight
|
|
"com.pgadmin.pgadmin4" # pgAdmin
|
|
"com.eggerapps.Sequel-Pro" # Sequel Pro legacy
|
|
"com.valentina-db.Valentina-Studio" # Valentina Studio
|
|
"com.dbvis.DbVisualizer" # DbVisualizer
|
|
|
|
# Development Tools - API & Network
|
|
"com.postmanlabs.mac" # Postman
|
|
"com.konghq.insomnia" # Insomnia
|
|
"com.CharlesProxy.*" # Charles Proxy
|
|
"com.proxyman.*" # Proxyman
|
|
"com.getpaw.*" # Paw
|
|
"com.luckymarmot.Paw" # Paw legacy
|
|
"com.charlesproxy.charles" # Charles
|
|
"com.telerik.Fiddler" # Fiddler
|
|
"com.usebruno.app" # Bruno (API client)
|
|
|
|
# Network Proxy & VPN Tools (pattern-based protection)
|
|
# Clash variants
|
|
"*clash*" # All Clash variants (ClashX, ClashX Pro, Clash Verge, etc)
|
|
"*Clash*" # Capitalized variants
|
|
"com.nssurge.surge-mac" # Surge
|
|
"*surge*" # Surge variants
|
|
"*Surge*" # Surge variants
|
|
"mihomo*" # Mihomo Party and variants
|
|
"*openvpn*" # OpenVPN Connect and variants
|
|
"*OpenVPN*" # OpenVPN capitalized variants
|
|
"net.openvpn.*" # OpenVPN bundle IDs
|
|
|
|
# Proxy Clients (Shadowsocks, V2Ray, etc)
|
|
"*ShadowsocksX-NG*" # ShadowsocksX-NG
|
|
"com.qiuyuzhou.*" # ShadowsocksX-NG bundle
|
|
"*v2ray*" # V2Ray variants
|
|
"*V2Ray*" # V2Ray variants
|
|
"*v2box*" # V2Box
|
|
"*V2Box*" # V2Box
|
|
"*nekoray*" # Nekoray
|
|
"*sing-box*" # Sing-box
|
|
"*OneBox*" # OneBox
|
|
"*hiddify*" # Hiddify
|
|
"*Hiddify*" # Hiddify
|
|
"*loon*" # Loon
|
|
"*Loon*" # Loon
|
|
"*quantumult*" # Quantumult X
|
|
|
|
# Mesh & Corporate VPNs
|
|
"*tailscale*" # Tailscale
|
|
"io.tailscale.*" # Tailscale bundle
|
|
"*zerotier*" # ZeroTier
|
|
"com.zerotier.*" # ZeroTier bundle
|
|
"*1dot1dot1dot1*" # Cloudflare WARP
|
|
"*cloudflare*warp*" # Cloudflare WARP
|
|
|
|
# Commercial VPNs
|
|
"*nordvpn*" # NordVPN
|
|
"*expressvpn*" # ExpressVPN
|
|
"*protonvpn*" # ProtonVPN
|
|
"*surfshark*" # Surfshark
|
|
"*windscribe*" # Windscribe
|
|
"*mullvad*" # Mullvad
|
|
"*privateinternetaccess*" # PIA
|
|
|
|
# Screensaver & Dynamic Wallpaper
|
|
"*Aerial*" # Aerial screensaver (all case variants)
|
|
"*aerial*" # Aerial lowercase
|
|
"*Fliqlo*" # Fliqlo screensaver (all case variants)
|
|
"*fliqlo*" # Fliqlo lowercase
|
|
|
|
# Development Tools - Git & Version Control
|
|
"com.github.GitHubDesktop" # GitHub Desktop
|
|
"com.sublimemerge" # Sublime Merge
|
|
"com.torusknot.SourceTreeNotMAS" # SourceTree
|
|
"com.git-tower.Tower*" # Tower
|
|
"com.gitfox.GitFox" # GitFox
|
|
"com.github.Gitify" # Gitify
|
|
"com.fork.Fork" # Fork
|
|
"com.axosoft.gitkraken" # GitKraken
|
|
|
|
# Development Tools - Terminal & Shell
|
|
"com.googlecode.iterm2" # iTerm2
|
|
"net.kovidgoyal.kitty" # Kitty
|
|
"io.alacritty" # Alacritty
|
|
"com.github.wez.wezterm" # WezTerm
|
|
"com.hyper.Hyper" # Hyper
|
|
"com.mizage.divvy" # Divvy
|
|
"com.fig.Fig" # Fig (terminal assistant)
|
|
"dev.warp.Warp-Stable" # Warp
|
|
"com.termius-dmg" # Termius (SSH client)
|
|
|
|
# Development Tools - Docker & Virtualization
|
|
"com.docker.docker" # Docker Desktop
|
|
"com.getutm.UTM" # UTM
|
|
"com.vmware.fusion" # VMware Fusion
|
|
"com.parallels.desktop.*" # Parallels Desktop
|
|
"org.virtualbox.app.VirtualBox" # VirtualBox
|
|
"com.vagrant.*" # Vagrant
|
|
"com.orbstack.OrbStack" # OrbStack
|
|
|
|
# System Monitoring & Performance
|
|
"com.bjango.istatmenus*" # iStat Menus
|
|
"eu.exelban.Stats" # Stats
|
|
"com.monitorcontrol.*" # MonitorControl
|
|
"com.bresink.system-toolkit.*" # TinkerTool System
|
|
"com.mediaatelier.MenuMeters" # MenuMeters
|
|
"com.activity-indicator.app" # Activity Indicator
|
|
"net.cindori.sensei" # Sensei
|
|
|
|
# Window Management & Productivity
|
|
"com.macitbetter.*" # BetterTouchTool, BetterSnapTool
|
|
"com.hegenberg.*" # BetterTouchTool legacy
|
|
"com.manytricks.*" # Moom, Witch, Name Mangler, Resolutionator
|
|
"com.divisiblebyzero.*" # Spectacle
|
|
"com.koingdev.*" # Koingg apps
|
|
"com.if.Amphetamine" # Amphetamine
|
|
"com.lwouis.alt-tab-macos" # AltTab
|
|
"net.matthewpalmer.Vanilla" # Vanilla
|
|
"com.lightheadsw.Caffeine" # Caffeine
|
|
"com.contextual.Contexts" # Contexts
|
|
"com.amethyst.Amethyst" # Amethyst
|
|
"com.knollsoft.Rectangle" # Rectangle
|
|
"com.knollsoft.Hookshot" # Hookshot
|
|
"com.surteesstudios.Bartender" # Bartender
|
|
"com.gaosun.eul" # eul (system monitor)
|
|
"com.pointum.hazeover" # HazeOver
|
|
|
|
# Launcher & Automation
|
|
"com.runningwithcrayons.Alfred" # Alfred
|
|
"com.raycast.macos" # Raycast
|
|
"com.blacktree.Quicksilver" # Quicksilver
|
|
"com.stairways.keyboardmaestro.*" # Keyboard Maestro
|
|
"com.manytricks.Butler" # Butler
|
|
"com.happenapps.Quitter" # Quitter
|
|
"com.pilotmoon.scroll-reverser" # Scroll Reverser
|
|
"org.pqrs.Karabiner-Elements" # Karabiner-Elements
|
|
"com.apple.Automator" # Automator (system, but keep user workflows)
|
|
|
|
# Note-Taking & Documentation
|
|
"com.bear-writer.*" # Bear
|
|
"com.typora.*" # Typora
|
|
"com.ulyssesapp.*" # Ulysses
|
|
"com.literatureandlatte.*" # Scrivener
|
|
"com.dayoneapp.*" # Day One
|
|
"notion.id" # Notion
|
|
"md.obsidian" # Obsidian
|
|
"com.logseq.logseq" # Logseq
|
|
"com.evernote.Evernote" # Evernote
|
|
"com.onenote.mac" # OneNote
|
|
"com.omnigroup.OmniOutliner*" # OmniOutliner
|
|
"net.shinyfrog.bear" # Bear legacy
|
|
"com.goodnotes.GoodNotes" # GoodNotes
|
|
"com.marginnote.MarginNote*" # MarginNote
|
|
"com.roamresearch.*" # Roam Research
|
|
"com.reflect.ReflectApp" # Reflect
|
|
"com.inkdrop.*" # Inkdrop
|
|
|
|
# Design & Creative Tools
|
|
"com.adobe.*" # Adobe Creative Suite
|
|
"com.bohemiancoding.*" # Sketch
|
|
"com.figma.*" # Figma
|
|
"com.framerx.*" # Framer
|
|
"com.zeplin.*" # Zeplin
|
|
"com.invisionapp.*" # InVision
|
|
"com.principle.*" # Principle
|
|
"com.pixelmatorteam.*" # Pixelmator
|
|
"com.affinitydesigner.*" # Affinity Designer
|
|
"com.affinityphoto.*" # Affinity Photo
|
|
"com.affinitypublisher.*" # Affinity Publisher
|
|
"com.linearity.curve" # Linearity Curve
|
|
"com.canva.CanvaDesktop" # Canva
|
|
"com.maxon.cinema4d" # Cinema 4D
|
|
"com.autodesk.*" # Autodesk products
|
|
"com.sketchup.*" # SketchUp
|
|
|
|
# Communication & Collaboration
|
|
"com.tencent.xinWeChat" # WeChat (Chinese users)
|
|
"com.tencent.qq" # QQ
|
|
"com.alibaba.DingTalkMac" # DingTalk
|
|
"com.alibaba.AliLang.osx" # AliLang (retain login/config data)
|
|
"com.alibaba.alilang3.osx.ShipIt" # AliLang updater component
|
|
"com.alibaba.AlilangMgr.QueryNetworkInfo" # AliLang network helper
|
|
"us.zoom.xos" # Zoom
|
|
"com.microsoft.teams*" # Microsoft Teams
|
|
"com.slack.Slack" # Slack
|
|
"com.hnc.Discord" # Discord
|
|
"app.legcord.Legcord" # Legcord
|
|
"org.telegram.desktop" # Telegram
|
|
"ru.keepcoder.Telegram" # Telegram legacy
|
|
"net.whatsapp.WhatsApp" # WhatsApp
|
|
"com.skype.skype" # Skype
|
|
"com.cisco.webexmeetings" # Webex
|
|
"com.ringcentral.RingCentral" # RingCentral
|
|
"com.readdle.smartemail-Mac" # Spark Email
|
|
"com.airmail.*" # Airmail
|
|
"com.postbox-inc.postbox" # Postbox
|
|
"com.tinyspeck.slackmacgap" # Slack legacy
|
|
|
|
# Task Management & Productivity
|
|
"com.omnigroup.OmniFocus*" # OmniFocus
|
|
"com.culturedcode.*" # Things
|
|
"com.todoist.*" # Todoist
|
|
"com.any.do.*" # Any.do
|
|
"com.ticktick.*" # TickTick
|
|
"com.microsoft.to-do" # Microsoft To Do
|
|
"com.trello.trello" # Trello
|
|
"com.asana.nativeapp" # Asana
|
|
"com.clickup.*" # ClickUp
|
|
"com.monday.desktop" # Monday.com
|
|
"com.airtable.airtable" # Airtable
|
|
"com.notion.id" # Notion (also note-taking)
|
|
"com.linear.linear" # Linear
|
|
|
|
# File Transfer & Sync
|
|
"com.panic.transmit*" # Transmit (FTP/SFTP)
|
|
"com.binarynights.ForkLift*" # ForkLift
|
|
"com.noodlesoft.Hazel" # Hazel
|
|
"com.cyberduck.Cyberduck" # Cyberduck
|
|
"io.filezilla.FileZilla" # FileZilla
|
|
"com.apple.Xcode.CloudDocuments" # Xcode Cloud Documents
|
|
"com.synology.*" # Synology apps
|
|
|
|
# Cloud Storage & Backup (Issue #204)
|
|
"com.dropbox.*" # Dropbox
|
|
"com.getdropbox.*" # Dropbox legacy
|
|
"*dropbox*" # Dropbox helpers/updaters
|
|
"ws.agile.*" # 1Password sync helpers
|
|
"com.backblaze.*" # Backblaze
|
|
"*backblaze*" # Backblaze helpers
|
|
"com.box.desktop*" # Box
|
|
"*box.desktop*" # Box helpers
|
|
"com.microsoft.OneDrive*" # Microsoft OneDrive
|
|
"com.microsoft.SyncReporter" # OneDrive sync reporter
|
|
"*OneDrive*" # OneDrive helpers/updaters
|
|
"com.google.GoogleDrive" # Google Drive
|
|
"com.google.keystone*" # Google updaters (Drive, Chrome, etc.)
|
|
"*GoogleDrive*" # Google Drive helpers
|
|
"com.amazon.drive" # Amazon Drive
|
|
"com.apple.bird" # iCloud Drive daemon
|
|
"com.apple.CloudDocs*" # iCloud Documents
|
|
"com.displaylink.*" # DisplayLink
|
|
"com.fujitsu.pfu.ScanSnap*" # ScanSnap
|
|
"com.citrix.*" # Citrix Workspace
|
|
"org.xquartz.*" # XQuartz
|
|
"us.zoom.updater*" # Zoom updaters
|
|
"com.DigiDNA.iMazing*" # iMazing
|
|
"com.shirtpocket.*" # SuperDuper backup
|
|
"homebrew.mxcl.*" # Homebrew services
|
|
|
|
# Screenshot & Recording
|
|
"com.cleanshot.*" # CleanShot X
|
|
"com.xnipapp.xnip" # Xnip
|
|
"com.reincubate.camo" # Camo
|
|
"com.tunabellysoftware.ScreenFloat" # ScreenFloat
|
|
"net.telestream.screenflow*" # ScreenFlow
|
|
"com.techsmith.snagit*" # Snagit
|
|
"com.techsmith.camtasia*" # Camtasia
|
|
"com.obsidianapp.screenrecorder" # Screen Recorder
|
|
"com.kap.Kap" # Kap
|
|
"com.getkap.*" # Kap legacy
|
|
"com.linebreak.CloudApp" # CloudApp
|
|
"com.droplr.droplr-mac" # Droplr
|
|
|
|
# Media & Entertainment
|
|
"com.spotify.client" # Spotify
|
|
"com.apple.Music" # Apple Music
|
|
"com.apple.podcasts" # Apple Podcasts
|
|
"com.apple.BKAgentService" # Apple Books (Agent)
|
|
"com.apple.iBooksX" # Apple Books
|
|
"com.apple.iBooks" # Apple Books (Legacy)
|
|
"com.apple.FinalCutPro" # Final Cut Pro
|
|
"com.apple.Motion" # Motion
|
|
"com.apple.Compressor" # Compressor
|
|
"com.blackmagic-design.*" # DaVinci Resolve
|
|
"com.colliderli.iina" # IINA
|
|
"org.videolan.vlc" # VLC
|
|
"io.mpv" # MPV
|
|
"com.noodlesoft.Hazel" # Hazel (automation)
|
|
"tv.plex.player.desktop" # Plex
|
|
"com.netease.163music" # NetEase Music
|
|
|
|
# Web Browsers (protect complex storage like IndexedDB, localStorage)
|
|
"Firefox" # Firefox Application Support
|
|
"org.mozilla.*" # Firefox bundle IDs
|
|
|
|
# License Management & App Stores
|
|
"com.paddle.Paddle*" # Paddle (license management)
|
|
"com.setapp.DesktopClient" # Setapp
|
|
"com.devmate.*" # DevMate (license framework)
|
|
"org.sparkle-project.Sparkle" # Sparkle (update framework)
|
|
)
|
|
|
|
# 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"
|
|
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do
|
|
if bundle_matches_pattern "$bundle_id" "$pattern"; then
|
|
return 0
|
|
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: only protect 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
|
|
local nospace_name="${app_name// /}"
|
|
local underscore_name="${app_name// /_}"
|
|
|
|
# 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 sanitized name variants if unique enough
|
|
if [[ ${#app_name} -gt 3 && "$app_name" =~ [[:space:]] ]]; then
|
|
user_patterns+=(
|
|
"$HOME/Library/Application Support/$nospace_name"
|
|
"$HOME/Library/Caches/$nospace_name"
|
|
"$HOME/Library/Logs/$nospace_name"
|
|
"$HOME/Library/Application Support/$underscore_name"
|
|
)
|
|
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
|
|
if [[ ${#app_name} -ge 5 ]] && [[ -d ~/Library/LaunchAgents ]]; then
|
|
while IFS= read -r -d '' plist; do
|
|
local plist_name=$(basename "$plist")
|
|
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
|
|
|
|
# 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=()
|
|
|
|
# Sanitized App Name (remove spaces)
|
|
local nospace_name="${app_name// /}"
|
|
|
|
# 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"
|
|
)
|
|
|
|
if [[ ${#app_name} -gt 3 && "$app_name" =~ [[:space:]] ]]; then
|
|
system_patterns+=(
|
|
"/Library/Application Support/$nospace_name"
|
|
"/Library/Caches/$nospace_name"
|
|
"/Library/Logs/$nospace_name"
|
|
)
|
|
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
|