Stop bash arrays ending at the first ) inside a comment

Both the reader and the writer ran to the first closing paren, which is only the
array's own close if nothing inside contains one. HOST1_WATCHDOG_SCAN_IGNORE has
carried "(exit 127 — bad image)" for weeks: writes spliced into the middle of it
and were refused by bash -n with a bare false, and reads returned the entries
above it — the Rsync tab showed no intermediate scripts at all and 10 of 17
daily. Arrays now close on a ) that starts its own line, as confform already did.
This commit is contained in:
Gmer4Lfe
2026-08-14 11:35:18 -04:00
parent a2debe972f
commit cbcfbfaaad
3 changed files with 35 additions and 4 deletions
+6 -2
View File
@@ -836,10 +836,14 @@ function vv_log_tail(string $path, int $lines): string {
return implode("\n", array_slice($all, -$lines)); return implode("\n", array_slice($all, -$lines));
} }
// See VV_CONF_ARRAY_BODY in config.php for why this cannot stop at the first `)`.
function vv_parse_bash_array(string $raw, string $varName): array { function vv_parse_bash_array(string $raw, string $varName): array {
if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\(([^)]*)\)/ms', $raw, $m)) return []; if (!preg_match('/^\s*' . preg_quote($varName, '/') . '\s*=\s*\(' . VV_CONF_ARRAY_BODY . '/ms',
$raw, $m)) return [];
// Group 2 exists only when the multi-line branch matched; group 1 is the single-line body.
$body = isset($m[2]) ? $m[2] : ($m[1] ?? '');
$items = []; $items = [];
foreach (explode("\n", $m[1]) as $line) { foreach (explode("\n", $body) as $line) {
$line = trim(preg_replace('/#.*$/', '', $line), " \t\"'"); $line = trim(preg_replace('/#.*$/', '', $line), " \t\"'");
if ($line !== '') $items[] = $line; if ($line !== '') $items[] = $line;
} }
+4 -2
View File
@@ -813,16 +813,18 @@ function vv_conf_write_file(string $file, array $fileChanges): bool {
$raw $raw
) ?? $raw; ) ?? $raw;
// VV_CONF_ARRAY_CLOSE, not [^)]*\) — see the constant for what the greedy-to-first-paren
// version did to a list whose comments contain one.
} elseif ($type === 'array') { } elseif ($type === 'array') {
$raw = preg_replace_callback( $raw = preg_replace_callback(
'/^(\s*)(' . $qKey . '\s*=\s*\()[^)]*\)/ms', '/^(\s*)(' . $qKey . '\s*=\s*\()' . VV_CONF_ARRAY_CLOSE . '/ms',
fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")", fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")",
$raw $raw
) ?? $raw; ) ?? $raw;
} elseif ($type === 'assoc_array') { } elseif ($type === 'assoc_array') {
$raw = preg_replace_callback( $raw = preg_replace_callback(
'/^(\s*)(declare\s+-A\s+' . $qKey . '\s*=\s*\()[^)]*\)/ms', '/^(\s*)(declare\s+-A\s+' . $qKey . '\s*=\s*\()' . VV_CONF_ARRAY_CLOSE . '/ms',
fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")", fn($m) => $m[1] . $m[2] . "\n" . $value . "\n" . $m[1] . ")",
$raw $raw
) ?? $raw; ) ?? $raw;
+25
View File
@@ -645,6 +645,31 @@ function vv_format_uptime(int $seconds): string {
return ($d ? "{$d}d " : '') . ($h ? "{$h}h " : '') . "{$m}m"; return ($d ? "{$d}d " : '') . ($h ? "{$h}h " : '') . "{$m}m";
} }
// ── Where a bash array ends ──────────────────────────────────────────────────────────────────
// Both halves of this had the same bug independently, which is why the rule now lives in one
// place: `[^)]*\)` — run to the first closing paren — is only the array's own close if no entry
// or comment contains one. HOST1_WATCHDOG_SCAN_IGNORE has carried
// `# broken test container (exit 127 — bad image)` for weeks, and the two sides failed differently
// against it:
//
// writing vv_conf_write_file() spliced the new body into the middle of that comment and left
// the real `)` stranded below as a stray token. `bash -n` caught it and vv_conf_edit()
// rolled the file back, so no conf was corrupted — but the caller got a bare `false`
// with an empty $rejected, and every future save of that list would have failed the
// same silent way.
// reading vv_parse_bash_array() stopped there and returned the entries above it, so the array
// was quietly short. Only the PHP layer was affected: bash sources the file itself and
// always saw every entry, so the watchdogs behaved correctly while the pages under-
// reported what they were configured with — the failure mode with no symptom.
//
// confform.php's own reader was always right, and this is its rule: a multi-line array closes on
// a `)` that starts its own line. Parens anywhere else are just text.
//
// CLOSE is non-capturing, for splicing a new body in. BODY captures — group 1 when the array is
// written on one line, group 2 when it spans several.
const VV_CONF_ARRAY_CLOSE = '(?:[^)\n]*\)|.*?\n[ \t]*\))';
const VV_CONF_ARRAY_BODY = '(?:([^)\n]*)\)|\n(.*?)^[ \t]*\)[ \t]*$)';
// Parse a scalar value from raw conf text. Matches KEY="value" or KEY=value. // Parse a scalar value from raw conf text. Matches KEY="value" or KEY=value.
// Identical logic was previously duplicated as vv_arr_scalar / vv_wd_scalar / // Identical logic was previously duplicated as vv_arr_scalar / vv_wd_scalar /
// vv_fb_scalar / vv_media_conf_scalar — all reduce to this one regex. // vv_fb_scalar / vv_media_conf_scalar — all reduce to this one regex.