summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-28 09:27:56 +0300
committerPaul Buetow <paul@buetow.org>2026-06-28 09:27:56 +0300
commitea9de473e3d6788ea08ca3697722cf89b9d1d107 (patch)
treeb3a3d6d6bc1ab8bc5ca76b9574a7247da19d7bb7
parent86c354932e698d52f23e65a1dafb1b7250f4c1b9 (diff)
Extract destructive --clean path guard into config.clean-guard.source.sh
Move the safety-critical rm -rf guard (resolve_dist_dir_path and validate_clean_dist_dir, including the dangerous-path blocklist) out of config.validate.source.sh into a dedicated module so the policy that gates an unconditional rm -rf lives in one isolated place with its own test surface. This is a pure move refactor: the guard logic, the forbidden list, and the error messages are byte-identical. Register the new module in both LIB_SOURCES lists (Justfile and the src/shuriken.sh marker block) at the same position, right before config.validate.source.sh, keeping the anti-drift invariant green and regenerating bin/shuriken. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
-rw-r--r--Justfile2
-rwxr-xr-xbin/shuriken189
-rw-r--r--src/lib/config.clean-guard.source.sh99
-rw-r--r--src/lib/config.validate.source.sh88
-rwxr-xr-xsrc/shuriken.sh2
5 files changed, 213 insertions, 167 deletions
diff --git a/Justfile b/Justfile
index 738aad2..6fbad93 100644
--- a/Justfile
+++ b/Justfile
@@ -7,7 +7,7 @@ PREFIX := env_var_or_default("PREFIX", "/usr")
BINDIR := env_var_or_default("BINDIR", PREFIX + "/bin")
DATADIR := env_var_or_default("DATADIR", PREFIX + "/share")
SYSCONFDIR := env_var_or_default("SYSCONFDIR", "/etc/default")
-LIB_SOURCES := "src/lib/logging.source.sh src/lib/compat.source.sh src/lib/bootstrap.source.sh src/lib/paths.source.sh src/lib/imagemagick.source.sh src/lib/process.source.sh src/lib/archive.source.sh src/lib/template.source.sh src/lib/job-pool.source.sh src/lib/image.source.sh src/lib/random.source.sh src/lib/photo-list.source.sh src/lib/metadata-label.source.sh src/lib/metadata-cache.source.sh src/lib/image-pipeline.source.sh src/lib/album-metadata.source.sh src/lib/generation-metadata.source.sh src/lib/dry-run.source.sh src/lib/album-tile-layout.source.sh src/lib/album-thumbnail-html.source.sh src/lib/album-photo-select.source.sh src/lib/album-render.source.sh src/lib/album.source.sh src/lib/stats-aggregate.source.sh src/lib/stats-render.source.sh src/lib/stats-filter-album.source.sh src/lib/config.source.sh src/lib/config.print.source.sh src/lib/config.sync.source.sh src/lib/config.staging.source.sh src/lib/config.validate.source.sh src/lib/config.cli.source.sh src/lib/action.source.sh"
+LIB_SOURCES := "src/lib/logging.source.sh src/lib/compat.source.sh src/lib/bootstrap.source.sh src/lib/paths.source.sh src/lib/imagemagick.source.sh src/lib/process.source.sh src/lib/archive.source.sh src/lib/template.source.sh src/lib/job-pool.source.sh src/lib/image.source.sh src/lib/random.source.sh src/lib/photo-list.source.sh src/lib/metadata-label.source.sh src/lib/metadata-cache.source.sh src/lib/image-pipeline.source.sh src/lib/album-metadata.source.sh src/lib/generation-metadata.source.sh src/lib/dry-run.source.sh src/lib/album-tile-layout.source.sh src/lib/album-thumbnail-html.source.sh src/lib/album-photo-select.source.sh src/lib/album-render.source.sh src/lib/album.source.sh src/lib/stats-aggregate.source.sh src/lib/stats-render.source.sh src/lib/stats-filter-album.source.sh src/lib/config.source.sh src/lib/config.print.source.sh src/lib/config.sync.source.sh src/lib/config.staging.source.sh src/lib/config.clean-guard.source.sh src/lib/config.validate.source.sh src/lib/config.cli.source.sh src/lib/action.source.sh"
default: build
diff --git a/bin/shuriken b/bin/shuriken
index 0a4450d..b7f1bfd 100755
--- a/bin/shuriken
+++ b/bin/shuriken
@@ -5980,6 +5980,107 @@ generate_staged() {
clear_generation_staging_traps
}
+# Inlined from src/lib/config.clean-guard.source.sh
+# Destructive --clean path guard. Extracted from config.validate.source.sh
+# (task tr0) so the safety-critical "refuse to rm -rf a dangerous DIST_DIR"
+# policy lives in one isolated module with its own test surface, instead of
+# being buried among the generic config validators.
+#
+# This module owns the single source of truth for the dangerous-path blocklist
+# (the filesystem root, well-known system trees, HOME, and the current working
+# directory) plus the canonicalization needed to apply it safely. It is kept
+# separate because a regression here is catastrophic (it gates an unconditional
+# rm -rf), so the policy and its guard tests deserve a dedicated home.
+#
+# It still leans on the generic helpers in config.validate.source.sh
+# (require_config_var, config_error, validate_dist_dir); all libs are sourced
+# before any code runs, so those call sites resolve at runtime regardless of
+# module order.
+
+# Resolve DIST_DIR to a canonical absolute path so the --clean guard cannot be
+# bypassed via "." / trailing slashes / symlinks / relative paths. The directory
+# itself may not exist yet (cleaning a stale config), so when it is missing we
+# resolve its existing parent and append the basename. Output is printed; the
+# caller captures it. Returns non-zero only if even the parent cannot resolve.
+resolve_dist_dir_path() {
+ local -r target_dir="$1"; shift
+ local parent base
+
+ if [ -d "$target_dir" ]; then
+ ( cd "$target_dir" && pwd -P ) && return
+ return 1
+ fi
+
+ # DIST_DIR does not exist: canonicalize the deepest existing ancestor and
+ # re-attach the remaining path so symlinked parents are still resolved.
+ parent=$(existing_parent_dir "$target_dir")
+ base=${target_dir#"$parent"}
+ base=${base#/}
+ parent=$( cd "$parent" && pwd -P ) || return 1
+ if [ -n "$base" ]; then
+ printf '%s/%s\n' "$parent" "$base"
+ else
+ printf '%s\n' "$parent"
+ fi
+}
+
+# Guard for the destructive --clean action: refuse to "rm -rf" DIST_DIR when it
+# resolves to an empty value or a clearly dangerous location (filesystem root,
+# the user's HOME, the current working directory, or a well-known system tree).
+# This is the single place where the dangerous-path policy lives so it stays in
+# sync. NOTE (ln0): this only guards against deleting the wrong tree; leftover
+# staging artifacts are a separate concern handled by task ln0.
+validate_clean_dist_dir() {
+ local resolved home_resolved cwd_resolved
+ local -a forbidden=(
+ / /home /root /tmp /usr /etc /var /bin /sbin /lib /boot /opt
+ )
+ local entry
+
+ require_config_var DIST_DIR || return
+
+ # Reuse the shared DIST_DIR sanity checks (must be a directory, writable,
+ # parent writable) before applying the destructive-path policy.
+ validate_dist_dir || return
+
+ resolved=$(resolve_dist_dir_path "$DIST_DIR") || {
+ config_error "DIST_DIR $DIST_DIR could not be resolved"
+ return 1
+ }
+
+ if [ -z "$resolved" ]; then
+ config_error 'refusing to clean an empty DIST_DIR'
+ return 1
+ fi
+
+ # Reject the filesystem root and well-known system directories outright.
+ for entry in "${forbidden[@]}"; do
+ if [ "$resolved" = "$entry" ]; then
+ config_error \
+ "refusing to clean DIST_DIR $resolved (dangerous path)"
+ return 1
+ fi
+ done
+
+ # Reject HOME and the current working directory themselves (deleting either
+ # would be catastrophic and is never the intended DIST_DIR).
+ if [ -n "${HOME:-}" ]; then
+ home_resolved=$( cd "$HOME" 2>/dev/null && pwd -P ) || home_resolved=''
+ if [ -n "$home_resolved" ] && [ "$resolved" = "$home_resolved" ]; then
+ config_error \
+ "refusing to clean DIST_DIR $resolved (is HOME)"
+ return 1
+ fi
+ fi
+
+ cwd_resolved=$(pwd -P)
+ if [ "$resolved" = "$cwd_resolved" ]; then
+ config_error \
+ "refusing to clean DIST_DIR $resolved (is current directory)"
+ return 1
+ fi
+}
+
# Inlined from src/lib/config.validate.source.sh
config_error() {
local -r message="$1"; shift
@@ -6070,89 +6171,11 @@ validate_dist_dir() {
fi
}
-# Resolve DIST_DIR to a canonical absolute path so the --clean guard cannot be
-# bypassed via "." / trailing slashes / symlinks / relative paths. The directory
-# itself may not exist yet (cleaning a stale config), so when it is missing we
-# resolve its existing parent and append the basename. Output is printed; the
-# caller captures it. Returns non-zero only if even the parent cannot resolve.
-resolve_dist_dir_path() {
- local -r target_dir="$1"; shift
- local parent base
-
- if [ -d "$target_dir" ]; then
- ( cd "$target_dir" && pwd -P ) && return
- return 1
- fi
-
- # DIST_DIR does not exist: canonicalize the deepest existing ancestor and
- # re-attach the remaining path so symlinked parents are still resolved.
- parent=$(existing_parent_dir "$target_dir")
- base=${target_dir#"$parent"}
- base=${base#/}
- parent=$( cd "$parent" && pwd -P ) || return 1
- if [ -n "$base" ]; then
- printf '%s/%s\n' "$parent" "$base"
- else
- printf '%s\n' "$parent"
- fi
-}
-
-# Guard for the destructive --clean action: refuse to "rm -rf" DIST_DIR when it
-# resolves to an empty value or a clearly dangerous location (filesystem root,
-# the user's HOME, the current working directory, or a well-known system tree).
-# This is the single place where the dangerous-path policy lives so it stays in
-# sync. NOTE (ln0): this only guards against deleting the wrong tree; leftover
-# staging artifacts are a separate concern handled by task ln0.
-validate_clean_dist_dir() {
- local resolved home_resolved cwd_resolved
- local -a forbidden=(
- / /home /root /tmp /usr /etc /var /bin /sbin /lib /boot /opt
- )
- local entry
-
- require_config_var DIST_DIR || return
-
- # Reuse the shared DIST_DIR sanity checks (must be a directory, writable,
- # parent writable) before applying the destructive-path policy.
- validate_dist_dir || return
-
- resolved=$(resolve_dist_dir_path "$DIST_DIR") || {
- config_error "DIST_DIR $DIST_DIR could not be resolved"
- return 1
- }
-
- if [ -z "$resolved" ]; then
- config_error 'refusing to clean an empty DIST_DIR'
- return 1
- fi
-
- # Reject the filesystem root and well-known system directories outright.
- for entry in "${forbidden[@]}"; do
- if [ "$resolved" = "$entry" ]; then
- config_error \
- "refusing to clean DIST_DIR $resolved (dangerous path)"
- return 1
- fi
- done
-
- # Reject HOME and the current working directory themselves (deleting either
- # would be catastrophic and is never the intended DIST_DIR).
- if [ -n "${HOME:-}" ]; then
- home_resolved=$( cd "$HOME" 2>/dev/null && pwd -P ) || home_resolved=''
- if [ -n "$home_resolved" ] && [ "$resolved" = "$home_resolved" ]; then
- config_error \
- "refusing to clean DIST_DIR $resolved (is HOME)"
- return 1
- fi
- fi
-
- cwd_resolved=$(pwd -P)
- if [ "$resolved" = "$cwd_resolved" ]; then
- config_error \
- "refusing to clean DIST_DIR $resolved (is current directory)"
- return 1
- fi
-}
+# The destructive --clean path guard (resolve_dist_dir_path,
+# validate_clean_dist_dir, and the dangerous-path blocklist) lives in
+# config.clean-guard.source.sh (task tr0). It is kept in its own module because
+# it gates an unconditional rm -rf and warrants an isolated test surface; the
+# functions there still reuse the generic validators below at runtime.
validate_template_dir_access() {
if [[ ! -d "$TEMPLATE_DIR" || ! -r "$TEMPLATE_DIR" \
diff --git a/src/lib/config.clean-guard.source.sh b/src/lib/config.clean-guard.source.sh
new file mode 100644
index 0000000..785a1cd
--- /dev/null
+++ b/src/lib/config.clean-guard.source.sh
@@ -0,0 +1,99 @@
+# Destructive --clean path guard. Extracted from config.validate.source.sh
+# (task tr0) so the safety-critical "refuse to rm -rf a dangerous DIST_DIR"
+# policy lives in one isolated module with its own test surface, instead of
+# being buried among the generic config validators.
+#
+# This module owns the single source of truth for the dangerous-path blocklist
+# (the filesystem root, well-known system trees, HOME, and the current working
+# directory) plus the canonicalization needed to apply it safely. It is kept
+# separate because a regression here is catastrophic (it gates an unconditional
+# rm -rf), so the policy and its guard tests deserve a dedicated home.
+#
+# It still leans on the generic helpers in config.validate.source.sh
+# (require_config_var, config_error, validate_dist_dir); all libs are sourced
+# before any code runs, so those call sites resolve at runtime regardless of
+# module order.
+
+# Resolve DIST_DIR to a canonical absolute path so the --clean guard cannot be
+# bypassed via "." / trailing slashes / symlinks / relative paths. The directory
+# itself may not exist yet (cleaning a stale config), so when it is missing we
+# resolve its existing parent and append the basename. Output is printed; the
+# caller captures it. Returns non-zero only if even the parent cannot resolve.
+resolve_dist_dir_path() {
+ local -r target_dir="$1"; shift
+ local parent base
+
+ if [ -d "$target_dir" ]; then
+ ( cd "$target_dir" && pwd -P ) && return
+ return 1
+ fi
+
+ # DIST_DIR does not exist: canonicalize the deepest existing ancestor and
+ # re-attach the remaining path so symlinked parents are still resolved.
+ parent=$(existing_parent_dir "$target_dir")
+ base=${target_dir#"$parent"}
+ base=${base#/}
+ parent=$( cd "$parent" && pwd -P ) || return 1
+ if [ -n "$base" ]; then
+ printf '%s/%s\n' "$parent" "$base"
+ else
+ printf '%s\n' "$parent"
+ fi
+}
+
+# Guard for the destructive --clean action: refuse to "rm -rf" DIST_DIR when it
+# resolves to an empty value or a clearly dangerous location (filesystem root,
+# the user's HOME, the current working directory, or a well-known system tree).
+# This is the single place where the dangerous-path policy lives so it stays in
+# sync. NOTE (ln0): this only guards against deleting the wrong tree; leftover
+# staging artifacts are a separate concern handled by task ln0.
+validate_clean_dist_dir() {
+ local resolved home_resolved cwd_resolved
+ local -a forbidden=(
+ / /home /root /tmp /usr /etc /var /bin /sbin /lib /boot /opt
+ )
+ local entry
+
+ require_config_var DIST_DIR || return
+
+ # Reuse the shared DIST_DIR sanity checks (must be a directory, writable,
+ # parent writable) before applying the destructive-path policy.
+ validate_dist_dir || return
+
+ resolved=$(resolve_dist_dir_path "$DIST_DIR") || {
+ config_error "DIST_DIR $DIST_DIR could not be resolved"
+ return 1
+ }
+
+ if [ -z "$resolved" ]; then
+ config_error 'refusing to clean an empty DIST_DIR'
+ return 1
+ fi
+
+ # Reject the filesystem root and well-known system directories outright.
+ for entry in "${forbidden[@]}"; do
+ if [ "$resolved" = "$entry" ]; then
+ config_error \
+ "refusing to clean DIST_DIR $resolved (dangerous path)"
+ return 1
+ fi
+ done
+
+ # Reject HOME and the current working directory themselves (deleting either
+ # would be catastrophic and is never the intended DIST_DIR).
+ if [ -n "${HOME:-}" ]; then
+ home_resolved=$( cd "$HOME" 2>/dev/null && pwd -P ) || home_resolved=''
+ if [ -n "$home_resolved" ] && [ "$resolved" = "$home_resolved" ]; then
+ config_error \
+ "refusing to clean DIST_DIR $resolved (is HOME)"
+ return 1
+ fi
+ fi
+
+ cwd_resolved=$(pwd -P)
+ if [ "$resolved" = "$cwd_resolved" ]; then
+ config_error \
+ "refusing to clean DIST_DIR $resolved (is current directory)"
+ return 1
+ fi
+}
diff --git a/src/lib/config.validate.source.sh b/src/lib/config.validate.source.sh
index f0b14bf..4d4466e 100644
--- a/src/lib/config.validate.source.sh
+++ b/src/lib/config.validate.source.sh
@@ -87,89 +87,11 @@ validate_dist_dir() {
fi
}
-# Resolve DIST_DIR to a canonical absolute path so the --clean guard cannot be
-# bypassed via "." / trailing slashes / symlinks / relative paths. The directory
-# itself may not exist yet (cleaning a stale config), so when it is missing we
-# resolve its existing parent and append the basename. Output is printed; the
-# caller captures it. Returns non-zero only if even the parent cannot resolve.
-resolve_dist_dir_path() {
- local -r target_dir="$1"; shift
- local parent base
-
- if [ -d "$target_dir" ]; then
- ( cd "$target_dir" && pwd -P ) && return
- return 1
- fi
-
- # DIST_DIR does not exist: canonicalize the deepest existing ancestor and
- # re-attach the remaining path so symlinked parents are still resolved.
- parent=$(existing_parent_dir "$target_dir")
- base=${target_dir#"$parent"}
- base=${base#/}
- parent=$( cd "$parent" && pwd -P ) || return 1
- if [ -n "$base" ]; then
- printf '%s/%s\n' "$parent" "$base"
- else
- printf '%s\n' "$parent"
- fi
-}
-
-# Guard for the destructive --clean action: refuse to "rm -rf" DIST_DIR when it
-# resolves to an empty value or a clearly dangerous location (filesystem root,
-# the user's HOME, the current working directory, or a well-known system tree).
-# This is the single place where the dangerous-path policy lives so it stays in
-# sync. NOTE (ln0): this only guards against deleting the wrong tree; leftover
-# staging artifacts are a separate concern handled by task ln0.
-validate_clean_dist_dir() {
- local resolved home_resolved cwd_resolved
- local -a forbidden=(
- / /home /root /tmp /usr /etc /var /bin /sbin /lib /boot /opt
- )
- local entry
-
- require_config_var DIST_DIR || return
-
- # Reuse the shared DIST_DIR sanity checks (must be a directory, writable,
- # parent writable) before applying the destructive-path policy.
- validate_dist_dir || return
-
- resolved=$(resolve_dist_dir_path "$DIST_DIR") || {
- config_error "DIST_DIR $DIST_DIR could not be resolved"
- return 1
- }
-
- if [ -z "$resolved" ]; then
- config_error 'refusing to clean an empty DIST_DIR'
- return 1
- fi
-
- # Reject the filesystem root and well-known system directories outright.
- for entry in "${forbidden[@]}"; do
- if [ "$resolved" = "$entry" ]; then
- config_error \
- "refusing to clean DIST_DIR $resolved (dangerous path)"
- return 1
- fi
- done
-
- # Reject HOME and the current working directory themselves (deleting either
- # would be catastrophic and is never the intended DIST_DIR).
- if [ -n "${HOME:-}" ]; then
- home_resolved=$( cd "$HOME" 2>/dev/null && pwd -P ) || home_resolved=''
- if [ -n "$home_resolved" ] && [ "$resolved" = "$home_resolved" ]; then
- config_error \
- "refusing to clean DIST_DIR $resolved (is HOME)"
- return 1
- fi
- fi
-
- cwd_resolved=$(pwd -P)
- if [ "$resolved" = "$cwd_resolved" ]; then
- config_error \
- "refusing to clean DIST_DIR $resolved (is current directory)"
- return 1
- fi
-}
+# The destructive --clean path guard (resolve_dist_dir_path,
+# validate_clean_dist_dir, and the dangerous-path blocklist) lives in
+# config.clean-guard.source.sh (task tr0). It is kept in its own module because
+# it gates an unconditional rm -rf and warrants an isolated test surface; the
+# functions there still reuse the generic validators below at runtime.
validate_template_dir_access() {
if [[ ! -d "$TEMPLATE_DIR" || ! -r "$TEMPLATE_DIR" \
diff --git a/src/shuriken.sh b/src/shuriken.sh
index 2308d90..290a074 100755
--- a/src/shuriken.sh
+++ b/src/shuriken.sh
@@ -151,6 +151,8 @@ source "$SHURIKEN_SOURCE_DIR/lib/config.print.source.sh"
source "$SHURIKEN_SOURCE_DIR/lib/config.sync.source.sh"
# shellcheck source=src/lib/config.staging.source.sh
source "$SHURIKEN_SOURCE_DIR/lib/config.staging.source.sh"
+# shellcheck source=src/lib/config.clean-guard.source.sh
+source "$SHURIKEN_SOURCE_DIR/lib/config.clean-guard.source.sh"
# shellcheck source=src/lib/config.validate.source.sh
source "$SHURIKEN_SOURCE_DIR/lib/config.validate.source.sh"
# shellcheck source=src/lib/config.cli.source.sh