103 lines
3.0 KiB
Bash
Executable File
103 lines
3.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# safe-replace-dir.sh — safely replace a directory within a safety-root boundary
|
|
#
|
|
# Provides safe_replace_dir() for sourcing, or run standalone:
|
|
# ./scripts/lib/safe-replace-dir.sh <source> <target> <safety_root>
|
|
#
|
|
# Safety contract (mirrors safe-replace-dir.mjs):
|
|
# - <target> must be a non-empty path.
|
|
# - <target> must be a strict descendant of <safety_root> (not equal to it).
|
|
# - Prints an error and returns/exits 1 if either constraint is violated.
|
|
#
|
|
# Usage (sourced):
|
|
# source "$(dirname "${BASH_SOURCE[0]}")/safe-replace-dir.sh"
|
|
# safe_replace_dir "$source" "$target" "$safety_root"
|
|
#
|
|
# Usage (standalone):
|
|
# ./scripts/lib/safe-replace-dir.sh /path/to/source /safe/root/target /safe/root
|
|
|
|
safe_replace_dir() {
|
|
local source=$1
|
|
local target=$2
|
|
local safety_root=$3
|
|
|
|
if [[ -z "$target" ]]; then
|
|
echo "safe_replace_dir: refusing to replace unsafe target: (empty string)" >&2
|
|
return 1
|
|
fi
|
|
|
|
# Resolve the real (symlink-resolved) safety root.
|
|
local abs_safety
|
|
abs_safety=$(cd "$safety_root" 2>/dev/null && pwd -P) || {
|
|
echo "safe_replace_dir: safety root does not exist: $safety_root" >&2
|
|
return 1
|
|
}
|
|
|
|
# Build an absolute lexical path for target's parent directory.
|
|
local target_parent target_base
|
|
target_base=$(basename "$target")
|
|
target_parent=$(dirname "$target")
|
|
# Make target_parent absolute without relying on cd (target may not exist yet).
|
|
if [[ "$target_parent" != /* ]]; then
|
|
target_parent="${PWD}/${target_parent}"
|
|
fi
|
|
|
|
# Walk up from target_parent to find the deepest existing directory,
|
|
# accumulating the non-existing path suffix as we go.
|
|
local suffix=""
|
|
local walk="$target_parent"
|
|
while [[ ! -d "$walk" ]]; do
|
|
local component
|
|
component=$(basename "$walk")
|
|
if [[ -z "$suffix" ]]; then
|
|
suffix="$component"
|
|
else
|
|
suffix="${component}/${suffix}"
|
|
fi
|
|
local next
|
|
next=$(dirname "$walk")
|
|
if [[ "$next" == "$walk" ]]; then
|
|
echo "safe_replace_dir: could not find existing ancestor for: $target" >&2
|
|
return 1
|
|
fi
|
|
walk="$next"
|
|
done
|
|
|
|
# Resolve the real path of the existing ancestor (follows symlinks).
|
|
local abs_parent
|
|
abs_parent=$(cd "$walk" && pwd -P) || {
|
|
echo "safe_replace_dir: could not resolve parent directory: $walk" >&2
|
|
return 1
|
|
}
|
|
|
|
# Reconstruct the full absolute target path.
|
|
local abs_target
|
|
if [[ -n "$suffix" ]]; then
|
|
abs_target="${abs_parent}/${suffix}/${target_base}"
|
|
else
|
|
abs_target="${abs_parent}/${target_base}"
|
|
fi
|
|
|
|
# Check that abs_target is strictly inside abs_safety
|
|
case "$abs_target" in
|
|
"${abs_safety}/"*) ;;
|
|
*)
|
|
echo "safe_replace_dir: refusing to replace target outside safety root: $target" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
|
|
rm -rf "$abs_target"
|
|
mkdir -p "$abs_target"
|
|
cp -R "${source}/." "$abs_target/"
|
|
}
|
|
|
|
# Allow standalone use
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
if [[ $# -ne 3 ]]; then
|
|
echo "Usage: $0 <source> <target> <safety_root>" >&2
|
|
exit 1
|
|
fi
|
|
safe_replace_dir "$1" "$2" "$3" || exit 1
|
|
fi
|