SPS · log · files · refs · tags · releases · people · languages · GitHub · file
bin/pkdel
13807 bytes at 2909919 · GitHub
#!/bin/sh
sps_prog=pkdel
sps_bin_dir=$(CDPATH= cd -P "$(dirname "$0")" 2>/dev/null && pwd) || exit 1
if [ -z "${SPS_LIBDIR+x}" ]; then
if [ -r "$sps_bin_dir/../lib/common.sh" ]; then
SPS_LIBDIR=$sps_bin_dir/../lib
else
SPS_LIBDIR=$sps_bin_dir/../lib/sps
fi
fi
if ! . "$SPS_LIBDIR/common.sh"; then
printf '%s\n' "pkdel: cannot load $SPS_LIBDIR/common.sh" >&2
exit 1
fi
sps_load_config || exit $?
sps_usage()
{
printf '%s\n' 'usage: pkdel [--plan] PACKAGE'
}
sps_fail()
{
sps_code=$1
shift
printf '%s: %s\n' "$sps_prog" "$*" >&2
exit "$sps_code"
}
sps_exists()
{
[ -f "$1" ] || [ -d "$1" ] || [ -L "$1" ]
}
sps_target_path()
{
case $SPS_ROOT in
/) sps_target=/$1 ;;
*/) sps_target=$SPS_ROOT$1 ;;
*) sps_target=$SPS_ROOT/$1 ;;
esac
}
sps_safe_parents()
{
sps_parent_rel=${1%/*}
[ "$sps_parent_rel" != "$1" ] || return 0
sps_remainder=$sps_parent_rel
sps_prefix=
while [ -n "$sps_remainder" ]; do
case $sps_remainder in
*/*) sps_component=${sps_remainder%%/*}; sps_remainder=${sps_remainder#*/} ;;
*) sps_component=$sps_remainder; sps_remainder= ;;
esac
if [ -n "$sps_prefix" ]; then
sps_prefix=$sps_prefix/$sps_component
else
sps_prefix=$sps_component
fi
sps_target_path "$sps_prefix"
if [ -L "$sps_target" ] && ! sps_is_shared_dir_link "$sps_prefix"; then
printf '%s: refusing unsafe target path /%s\n' "$sps_prog" "$sps_prefix" >&2
return 1
fi
if sps_exists "$sps_target" && [ ! -d "$sps_target" ]; then
printf '%s: refusing unsafe target path /%s\n' "$sps_prog" "$sps_prefix" >&2
return 1
fi
done
return 0
}
sps_work=
sps_backup_dir=
sps_owners_new=
sps_world_new=
sps_lock=
sps_lock_owned=0
sps_commit_complete=0
sps_database_commit_started=0
sps_cleanup()
{
sps_status=$?
trap - 0 1 2 3 15
if [ "$sps_commit_complete" -eq 0 ] && [ "$sps_database_commit_started" -eq 1 ] &&
[ -n "$sps_backup_dir" ] && [ -d "$sps_backup_dir" ]; then
if [ -n "${sps_record-}" ]; then
rm -rf "$sps_record"
mv "$sps_backup_dir/record" "$sps_record" 2>/dev/null || :
fi
cp "$sps_backup_dir/owners" "$SPS_DB/.owners-restore.$$" 2>/dev/null &&
mv -f "$SPS_DB/.owners-restore.$$" "$SPS_DB/owners" 2>/dev/null || :
cp "$sps_backup_dir/world" "$SPS_DB/.world-restore.$$" 2>/dev/null &&
mv -f "$SPS_DB/.world-restore.$$" "$SPS_DB/world" 2>/dev/null || :
sps_database_commit_started=0
fi
[ -n "$sps_owners_new" ] && rm -f "$sps_owners_new"
[ -n "$sps_world_new" ] && rm -f "$sps_world_new"
if [ -n "$sps_backup_dir" ] && [ -d "$sps_backup_dir" ]; then
rm -rf "$sps_backup_dir"
fi
if [ -n "$sps_work" ] && [ -d "$sps_work" ]; then
rm -rf "$sps_work"
fi
sps_lock_release
exit "$sps_status"
}
trap sps_cleanup 0
trap 'exit 130' 1 2 3 15
sps_plan=0
case ${1-} in
--plan) sps_plan=1; shift ;;
-h|--help) sps_usage; exit 0 ;;
esac
[ "$#" -eq 1 ] || { sps_usage >&2; exit 2; }
sps_name=$1
if ! sps_validate_package_name "$sps_name"; then
sps_fail 2 "invalid package name '$sps_name'"
fi
sps_installed=$SPS_DB/installed
[ -d "$sps_installed" ] && [ ! -L "$sps_installed" ] ||
sps_fail 11 "installed database is missing or is not a real directory"
sps_record=$SPS_DB/installed/$sps_name
if [ -L "$sps_record" ]; then
sps_fail 11 "installed record for '$sps_name' is not a real directory"
fi
[ -d "$sps_record" ] || sps_fail 3 "package '$sps_name' is not installed"
if [ "$sps_plan" -eq 0 ]; then
if ! sps_lock_acquire "$SPS_DB"; then
sps_fail 11 "package database is locked or unavailable: '$SPS_DB/.lock'"
fi
[ -d "$sps_installed" ] && [ ! -L "$sps_installed" ] &&
[ -d "$sps_record" ] && [ ! -L "$sps_record" ] ||
sps_fail 11 "installed database changed while acquiring the lock"
fi
for sps_file in meta files hashes; do
[ -f "$sps_record/$sps_file" ] && [ ! -L "$sps_record/$sps_file" ] ||
sps_fail 11 "installed record for '$sps_name' is missing $sps_file"
done
if [ -e "$sps_record/hooks" ] || [ -L "$sps_record/hooks" ]; then
[ -d "$sps_record/hooks" ] && [ ! -L "$sps_record/hooks" ] ||
sps_fail 11 "installed hooks for '$sps_name' are unsafe"
for sps_hook in pre-install post-install pre-remove post-remove; do
[ ! -e "$sps_record/hooks/$sps_hook" ] && [ ! -L "$sps_record/hooks/$sps_hook" ] && continue
[ -f "$sps_record/hooks/$sps_hook" ] && [ ! -L "$sps_record/hooks/$sps_hook" ] ||
sps_fail 11 "installed hook '$sps_hook' for '$sps_name' is unsafe"
done
fi
if ! awk -v sps_action=validate-meta -v sps_program=pkdel \
-f "$SPS_LIBDIR/package.awk" "$sps_record/meta"; then
sps_fail 11 "installed metadata for '$sps_name' is malformed"
fi
if ! awk -v sps_action=validate-paths -v sps_program=pkdel \
-f "$SPS_LIBDIR/package.awk" "$sps_record/files"; then
sps_fail 11 "installed manifest for '$sps_name' is malformed"
fi
if ! awk -v sps_action=validate-hashes -v sps_program=pkdel \
-f "$SPS_LIBDIR/package.awk" "$sps_record/hashes"; then
sps_fail 11 "installed hashes for '$sps_name' are malformed"
fi
sps_record_name=$(awk -v sps_action=meta-get -v sps_key_name=name \
-v sps_program=pkdel -f "$SPS_LIBDIR/package.awk" "$sps_record/meta") ||
sps_fail 11 "cannot read installed package name"
[ "$sps_record_name" = "$sps_name" ] ||
sps_fail 11 "record name '$sps_record_name' does not match '$sps_name'"
sps_version=$(awk -v sps_action=meta-get -v sps_key_name=version \
-v sps_program=pkdel -f "$SPS_LIBDIR/package.awk" "$sps_record/meta") ||
sps_fail 11 "cannot read installed package version"
for sps_db_control in owners world history; do
[ -f "$SPS_DB/$sps_db_control" ] && [ ! -L "$SPS_DB/$sps_db_control" ] ||
sps_fail 11 "database control '$sps_db_control' is missing or unsafe"
done
if ! awk -v sps_action=owners-validate -v sps_program=pkdel \
-f "$SPS_LIBDIR/db.awk" "$SPS_DB/owners"; then
sps_fail 11 "installed ownership database is malformed"
fi
if ! awk -v sps_action=world-validate -v sps_program=pkdel \
-f "$SPS_LIBDIR/db.awk" "$SPS_DB/world"; then
sps_fail 11 "installed world database is malformed"
fi
sps_tab=$(printf '\tX')
sps_tab=${sps_tab%X}
sps_record_file_modified()
{
sps_preserve_path=$1
sps_preserve_target=$2
sps_is_preserved_path "$sps_preserve_path" || return 1
[ -f "$sps_preserve_target" ] && [ ! -L "$sps_preserve_target" ] || return 1
sps_preserve_expected=$(awk -F "$sps_tab" -v path="$sps_preserve_path" '$3 == path && $1 == "sha256" { print $2; exit }' "$sps_record/hashes")
[ -n "$sps_preserve_expected" ] || return 1
sps_preserve_actual=$(sps_sha256 "$sps_preserve_target") || return 0
[ "$sps_preserve_actual" != "$sps_preserve_expected" ]
}
if [ "$sps_plan" -eq 1 ]; then
printf 'Remove plan\n\n'
printf 'package: %s %s\n' "$sps_name" "$sps_version"
while IFS= read -r sps_entry || [ -n "$sps_entry" ]; do
case $sps_entry in */) continue ;; esac
sps_owner=$(awk -F "$sps_tab" -v path="$sps_entry" '$1 == path { print $2 }' "$SPS_DB/owners")
if [ "$sps_owner" = "$sps_name" ]; then
sps_target_path "$sps_entry"
if sps_record_file_modified "$sps_entry" "$sps_target"; then
printf 'keep: /%s (locally modified protected file)\n' "$sps_entry"
else
printf 'remove: /%s\n' "$sps_entry"
fi
elif [ -n "$sps_owner" ]; then
printf 'keep: /%s (owned by %s)\n' "$sps_entry" "$sps_owner"
else
printf 'keep: /%s (not in owner index)\n' "$sps_entry"
fi
done <"$sps_record/files"
exit 0
fi
mkdir -p "$SPS_BUILD" || sps_fail 8 "cannot create work directory '$SPS_BUILD'"
sps_work=$(sps_mktemp_dir "$SPS_BUILD" remove) ||
sps_fail 8 "cannot create a secure removal workspace"
if [ -d "$sps_record/hooks" ]; then
mkdir "$sps_work/hooks" || sps_fail 8 "cannot stage removal hooks"
cp "$sps_record/hooks"/* "$sps_work/hooks/" 2>/dev/null || :
fi
# Join the manifest and owner index once instead of looking up every path
# separately.
awk -F "$sps_tab" 'NR == FNR { owner[$1]=$2; next }
$0 !~ /\/$/ { print $0 FS owner[$0] }' \
"$SPS_DB/owners" "$sps_record/files" >"$sps_work/manifest.owners" ||
sps_fail 11 "cannot map package ownership"
sps_run_remove_hook()
{
sps_hook_name=$1
sps_hook_file=$sps_work/hooks/$sps_hook_name
[ -f "$sps_hook_file" ] || return 0
printf '%s: running %s hook for %s\n' "$sps_prog" "$sps_hook_name" "$sps_name" >&2
SPS_ACTION=remove
SPS_PACKAGE=$sps_name
SPS_VERSION=$sps_version
export SPS_ACTION SPS_PACKAGE SPS_VERSION
sh -eu "$sps_hook_file"
}
sps_run_remove_hook pre-remove ||
sps_fail 10 "pre-remove hook failed for '$sps_name'"
# Remove a file only if the owner index still says it belongs to this package.
while IFS="$sps_tab" read -r sps_entry sps_owner || [ -n "$sps_entry" ]; do
if [ "$sps_owner" != "$sps_name" ]; then
if [ -n "$sps_owner" ]; then
sps_warn "/$sps_entry is now owned by $sps_owner; keeping it"
else
sps_warn "/$sps_entry has no owner record; keeping it"
fi
continue
fi
sps_safe_parents "$sps_entry" || sps_fail 7 "unsafe target path /$sps_entry"
sps_target_path "$sps_entry"
if [ -d "$sps_target" ] && [ ! -L "$sps_target" ]; then
sps_warn "/$sps_entry is now a directory; keeping it"
elif sps_record_file_modified "$sps_entry" "$sps_target"; then
sps_warn "/$sps_entry is locally modified; keeping it unowned"
elif sps_exists "$sps_target"; then
rm -f "$sps_target" || sps_fail 8 "cannot remove /$sps_entry"
fi
done <"$sps_work/manifest.owners"
# Directories are shared resources, not exclusively owned by any one package.
# A directory may only be removed once no other installed package still claims
# it in its manifest. Collect every directory referenced by the remaining
# installed packages so we never delete a directory the system still needs.
: >"$sps_work/other-dirs"
for sps_other_record in "$sps_installed"/*; do
[ -d "$sps_other_record" ] && [ ! -L "$sps_other_record" ] || continue
sps_other_name=${sps_other_record##*/}
case $sps_other_name in .|..) continue ;; esac
[ "$sps_other_name" = "$sps_name" ] && continue
[ -f "$sps_other_record/files" ] && [ ! -L "$sps_other_record/files" ] || continue
awk '/\/$/ { print }' "$sps_other_record/files" >>"$sps_work/other-dirs"
done
LC_ALL=C sort -u "$sps_work/other-dirs" -o "$sps_work/other-dirs"
# Reverse sort puts children before parents; rmdir leaves shared/non-empty
# directories alone.
LC_ALL=C sort -r "$sps_record/files" >"$sps_work/files.reverse"
while IFS= read -r sps_entry || [ -n "$sps_entry" ]; do
case $sps_entry in
*/)
sps_path=${sps_entry%/}
if awk -v wanted="$sps_entry" '$0 == wanted { found=1 } END { exit !found }' \
"$sps_work/other-dirs"; then
continue
fi
sps_safe_parents "$sps_path" || continue
sps_target_path "$sps_path"
if [ -d "$sps_target" ] && [ ! -L "$sps_target" ]; then
rmdir "$sps_target" 2>/dev/null || :
fi
;;
esac
done <"$sps_work/files.reverse"
sps_owners_new=$(mktemp "$SPS_DB/.owners-new.XXXXXX") || sps_fail 11 "cannot stage owner index"
sps_world_new=$(mktemp "$SPS_DB/.world-new.XXXXXX") || sps_fail 11 "cannot stage world database"
if ! awk -v sps_action=owners-remove -v sps_package="$sps_name" -v sps_program=pkdel \
-f "$SPS_LIBDIR/db.awk" "$SPS_DB/owners" >"$sps_owners_new"; then
sps_fail 11 "installed ownership database is malformed"
fi
if ! awk -v sps_action=world-remove -v sps_package="$sps_name" -v sps_program=pkdel \
-f "$SPS_LIBDIR/db.awk" "$SPS_DB/world" >"$sps_world_new"; then
sps_fail 11 "installed world database is malformed"
fi
sps_backup_dir=$(sps_mktemp_dir "$SPS_DB/installed" backup) ||
sps_fail 11 "cannot create database rollback area"
cp -p "$SPS_DB/owners" "$sps_backup_dir/owners" &&
cp -p "$SPS_DB/world" "$sps_backup_dir/world" ||
sps_fail 11 "cannot preserve database state"
mv "$sps_record" "$sps_backup_dir/record" || sps_fail 11 "cannot preserve package record"
sps_database_commit_started=1
sps_commit_failed=0
mv -f "$sps_owners_new" "$SPS_DB/owners" || sps_commit_failed=1
if [ "$sps_commit_failed" -eq 0 ]; then
mv -f "$sps_world_new" "$SPS_DB/world" || sps_commit_failed=1
fi
if [ "$sps_commit_failed" -ne 0 ]; then
mv "$sps_backup_dir/record" "$sps_record" 2>/dev/null || :
cp "$sps_backup_dir/owners" "$SPS_DB/.owners-restore.$$" 2>/dev/null &&
mv -f "$SPS_DB/.owners-restore.$$" "$SPS_DB/owners" 2>/dev/null || :
cp "$sps_backup_dir/world" "$SPS_DB/.world-restore.$$" 2>/dev/null &&
mv -f "$SPS_DB/.world-restore.$$" "$SPS_DB/world" 2>/dev/null || :
sps_database_commit_started=0
sps_fail 11 "database commit failed; previous metadata was restored"
fi
sps_commit_complete=1
sps_date=$(date '+%Y-%m-%d' 2>/dev/null || printf '%s' unknown)
printf '%s remove %s %s\n' "$sps_date" "$sps_name" "$sps_version" >>"$SPS_DB/history" ||
sps_warn "could not append package history"
if ! sps_run_remove_hook post-remove; then
sps_warn "package removal completed, but post-remove hook failed for '$sps_name'"
exit 10
fi
printf '%s: removed %s %s\n' "$sps_prog" "$sps_name" "$sps_version"
exit 0