Files
Bitsy 876122ff98 Audit fixes: security hardening, index correctness, performance, UX
Security
- Elevated copies: root only reads the original; the copy is written by
  the user (sudo -u tee), so root never chowns / chmods a user-controlled
  path. Staging folder forced to 0700.
- Service logs moved from fixed /tmp names to /Library/Logs/Allofit
  (root-owned) and ~/Library/Logs/Allofit; Diagnostics reveals instead of
  opening the log.
- Index files are owner-only (0600; the root daemon's belongs to the
  installing user), set on the temp file before an atomic rename.
- Root install passes the plist inline (base64, plutil -lint) instead of a
  user-writable temp file; the binary comes from Bundle.main.
- Cache loader caps and checks the declared payload size; each save uses
  its own temp file.
- Release action pinned to a commit; non-system LC_RPATHs stripped.

Correctness
- Move to Trash removes the files from the index (the watcher ignores the
  app's own operations) and registers Undo (Put Back); trashed folders are
  matched with the original URLs; failures are shown.
- The saved event id stays below pending subtree walks (GUI and service).
- Roots / exclusions changes restart the watcher from the snapshot's id.
- Case-only renames no longer leave a ghost entry.
- Service mode is saved only after a successful install; a saved but
  missing service falls back to the in-process indexer.

Performance
- New folders are merged without the O(n) removal pass.
- Size / date sorts use a compact key array (539 -> 47 ms for 630k).
- Selection, preview and actions use the selected records directly.
- Service saves at most every 15 s; reader reloads pause while hidden and
  are deferred instead of dropped; window close saves only when dirty.

Usability
- Results appear during the first index; empty-list explanations.
- Down arrow moves to the results, Up on the first row back; history on
  Up / Option-Up / Option-Down.
- Search syntax popover and Help menu; shortcuts shown in the context menu;
  confirmations for Clear Cache and Uninstall; privacy usage strings;
  Group Containers excluded by default; wording, VoiceOver labels, plural.
2026-10-02 14:44:49 +02:00

206 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Removes every trace of Allofit from the system: launchd services, cache
# files, user preferences, build artifacts and log files. Useful for a
# clean reinstall test or to fully uninstall. Can be called from anywhere -
# paths are resolved relative to the script's own location.
#
# Usage:
# ./scripts/clean.sh # from project root
# scripts/clean.sh --keep-build # leave .build/ and outputs/ in place
# ./clean.sh --keep-prefs # from scripts/, keep user prefs
# ./clean.sh --dry-run # print what would happen, change nothing
set -uo pipefail
# ==================
# MARK: Constants
# ==================
kBundleId="com.bitsycore.allofit"
kServiceLabel="${kBundleId}.service"
kServicePlistName="${kServiceLabel}.plist"
kUserAgentPlist="$HOME/Library/LaunchAgents/${kServicePlistName}"
kSystemDaemonPlist="/Library/LaunchDaemons/${kServicePlistName}"
kUserCacheDir="$HOME/Library/Application Support/Allofit"
kSystemCacheDir="/Library/Application Support/Allofit"
kUserPrefsPlist="$HOME/Library/Preferences/${kBundleId}.plist"
# old /tmp log names (versions up to 1.0.9) and the current log folders
kServiceLogStdout="/tmp/allofit-service.log"
kServiceLogStderr="/tmp/allofit-service.err"
kUserLogDir="$HOME/Library/Logs/Allofit"
kSystemLogDir="/Library/Logs/Allofit"
# ==================
# MARK: Args
# ==================
# Resolve the project root from the script's own location (scripts/..),
# so this script works regardless of the caller's CWD.
vProjectRoot="$(cd "$(dirname "$0")/.." && pwd)"
vKeepBuild=0
vKeepPrefs=0
vDryRun=0
for vArg in "$@"; do
case "$vArg" in
--keep-build) vKeepBuild=1 ;;
--keep-prefs) vKeepPrefs=1 ;;
--dry-run|-n) vDryRun=1 ;;
-h|--help)
# print the comment block at the top of this file
sed -n '2,/^set /p' "$0" | sed -E 's/^#( |$)//;/^set /d'
exit 0
;;
*)
echo "Unknown argument: $vArg" >&2
exit 1
;;
esac
done
# ==================
# MARK: Helpers
# ==================
# runs a command, or just prints it under --dry-run
vRun() {
if [[ "$vDryRun" -eq 1 ]]; then
echo " [dry-run] $*"
else
"$@"
fi
}
# caches a sudo session once so later sudo calls don't re-prompt
vGotSudo=0
vNeedsSudo() {
if [[ "$vGotSudo" -eq 0 ]]; then
if [[ "$vDryRun" -eq 1 ]]; then
echo " [dry-run] sudo -v"
else
echo "==> Acquiring sudo (for /Library cleanup)"
sudo -v
fi
vGotSudo=1
fi
}
# stops a launchd job by plist path then deletes the plist
# inDomain inPlist [inSudo]
vRemoveLaunchd() {
local inDomain="$1"
local inPlist="$2"
local inSudo="${3:-}"
if [[ ! -f "$inPlist" ]]; then return 0; fi
if [[ -n "$inSudo" ]]; then vNeedsSudo; fi
vRun $inSudo launchctl bootout "$inDomain" "$inPlist" >/dev/null 2>&1 || true
vRun $inSudo launchctl unload -w "$inPlist" >/dev/null 2>&1 || true
vRun $inSudo rm -f "$inPlist"
}
# ==================
# MARK: launchd services
# ==================
echo "==> Stopping & removing user LaunchAgent (if present)"
vRemoveLaunchd "gui/$(id -u)" "$kUserAgentPlist"
if [[ -f "$kSystemDaemonPlist" ]]; then
echo "==> Stopping & removing system LaunchDaemon"
vRemoveLaunchd "system" "$kSystemDaemonPlist" "sudo"
fi
echo "==> Killing any leftover daemon processes"
# `pkill -f` matches against the full command line; the daemon binary is
# now installed as ".../Allofit service" (renamed copy), so the literal
# "Allofit --service" no longer appears - use a regex that handles both
# the legacy and the renamed binary by anchoring on "Allofit...--service"
vRun pkill -f "Allofit.*--service" >/dev/null 2>&1 || true
# ==================
# MARK: Cache files
# ==================
if [[ -d "$kUserCacheDir" ]]; then
echo "==> Removing user cache directory"
vRun rm -rf "$kUserCacheDir"
fi
if [[ -d "$kSystemCacheDir" ]]; then
echo "==> Removing system cache directory"
vNeedsSudo
vRun sudo rm -rf "$kSystemCacheDir"
fi
# ==================
# MARK: Preferences
# ==================
if [[ "$vKeepPrefs" -eq 0 ]]; then
echo "==> Removing user preferences"
# defaults handles cfprefsd's cached copy in addition to the on-disk plist
vRun defaults delete "$kBundleId" >/dev/null 2>&1 || true
# also clear the SwiftPM "swift run" domain, which may differ from the
# bundled .app's domain depending on how the binary was launched
vRun defaults delete "Allofit" >/dev/null 2>&1 || true
vRun rm -f "$kUserPrefsPlist"
# wipe ByHost variants if any
shopt -s nullglob
for vByHost in "$HOME/Library/Preferences/ByHost/${kBundleId}".*.plist; do
vRun rm -f "$vByHost"
done
shopt -u nullglob
fi
# ==================
# MARK: Logs
# ==================
if [[ -f "$kServiceLogStdout" || -f "$kServiceLogStderr" ]]; then
echo "==> Removing service log files"
# /tmp has the sticky bit, so only the file's owner can rm it. Logs
# left over from a previous *root* daemon run are owned by root - we
# need sudo to delete them. Plain rm for user-owned logs (user agent
# mode), sudo rm for root-owned ones.
for vLog in "$kServiceLogStdout" "$kServiceLogStderr"; do
if [[ -f "$vLog" ]]; then
if [[ -O "$vLog" ]]; then
vRun rm -f "$vLog"
else
vNeedsSudo
vRun sudo rm -f "$vLog"
fi
fi
done
fi
if [[ -d "$kUserLogDir" ]]; then
echo "==> Removing user service logs"
vRun rm -rf "$kUserLogDir"
fi
if [[ -d "$kSystemLogDir" ]]; then
echo "==> Removing system service logs (needs sudo)"
vNeedsSudo
vRun sudo rm -rf "$kSystemLogDir"
fi
# ==================
# MARK: Build artifacts
# ==================
if [[ "$vKeepBuild" -eq 0 ]]; then
if [[ -d "${vProjectRoot}/.build" ]]; then
echo "==> Removing SwiftPM .build directory"
vRun rm -rf "${vProjectRoot}/.build"
fi
if [[ -d "${vProjectRoot}/outputs" ]]; then
echo "==> Removing outputs/ directory (built .app and .dmg)"
vRun rm -rf "${vProjectRoot}/outputs"
fi
fi
echo
echo "Clean complete."
echo
echo "Note: any Full Disk Access grant in System Settings → Privacy & Security"
echo "still points at the previous binary location. Remove it manually if you"
echo "intend to install Allofit at a different path."