refactor(ports): nine columns is the floor; refuse the legacy layouts

The parser accepted five shapes. Three of them (9, 10, 11/12) differ only
by trailing columns that have sane defaults, and those are worth keeping:
39 of the catalogue's descriptors stop at nine because they are
non-Traefik ports — DNS, SMTP, WireGuard UDP — with no subdomain to
state. A short row there is a complete row.

The other two were different animals. The 8-column legacy layout has no
login column and the 7-column one has no parent either, so they SHIFT
every position rather than omitting a tail: whenever the length was
misread, each field after the shift silently took its neighbour's value —
a port's access type reading from its protocol, and so on. That is the
same class of fault the word-splitting bug in this file just caused, and
it is invisible when it happens.

Nothing needs them. All 74 descriptors in the catalogue carry nine or
more, as does every one on this install. So they are refused now, with a
notice naming the offending key: a skipped port is visible, a mis-parsed
one is not.

Checked that skipping a row cannot misalign the parallel arrays —
port_config_data and port_config_vars are appended before the branch, but
neither is ever indexed alongside the others; the former is only tested
for emptiness.

Verified across every shape: 9, 10, 11 and 12 parse with the right
defaults, a label containing spaces survives intact next to an empty
trailing column, and both legacy layouts are refused rather than guessed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
librelad 2026-08-20 01:19:03 +01:00
parent 934193d901
commit 45ee27a0d6

View File

@ -66,10 +66,16 @@ initializeAppVariables()
port_config_data+=("$port_config_value") port_config_data+=("$port_config_value")
# 12-col: parent|name|ext:int|access|proto|login|traefik|webui|label|url_path|subdomain|recommended # 12-col: parent|name|ext:int|access|proto|login|traefik|webui|label|url_path|subdomain|recommended
# 10-col: parent|name|ext:int|access|proto|login|traefik|webui|label|url_path # 11-col: ... |subdomain (recommended defaults to the webui flag)
# 10-col: ... |url_path (subdomain empty -> app-name default)
# 9-col: parent|name|ext:int|access|proto|login|traefik|webui|label # 9-col: parent|name|ext:int|access|proto|login|traefik|webui|label
# Legacy 8-col: parent|name|ext:int|access|proto|traefik|webui|label (login defaults to false) #
# Legacy 7-col: name|ext:int|access|proto|traefik|webui|label (no parent, login defaults to false) # Nine is the floor. The trailing three are genuinely optional and
# have sane defaults, so a 9/10/11-column row is a complete row --
# 39 of the catalogue's descriptors stop at nine because they are
# non-Traefik ports (DNS, SMTP, WireGuard UDP) with no subdomain to
# state. Anything shorter is not a shorter row, it is a DIFFERENT
# layout, and is refused below.
# IFS-split, NOT ${v//|/ } with word-splitting. That idiom broke the # IFS-split, NOT ${v//|/ } with word-splitting. That idiom broke the
# format in two ways at once: a label containing a space became # format in two ways at once: a label containing a space became
# several fields ("Web Interface" -> label "Web", url_path # several fields ("Web Interface" -> label "Web", url_path
@ -105,40 +111,20 @@ initializeAppVariables()
else else
port_recommendeds+=("${parts[7]}") port_recommendeds+=("${parts[7]}")
fi fi
elif [[ ${#parts[@]} -ge 8 ]]; then else
local external_port="${parts[2]%%:*}" # The 8- and 7-column legacy shapes used to be accepted here.
local internal_port="${parts[2]##*:}" # They do not merely omit trailing fields -- they SHIFT every
port_parent_services+=("${parts[0]}") # position (8-col has no login column, 7-col has no parent
port_service_names+=("${parts[1]}") # either), so whenever the length was misread every field after
port_data_tags+=("PORTS_TAG_$i") # the shift silently took its neighbour's value. That is exactly
port_external_ports+=("$external_port") # the class of fault this parser was just fixed for, and nothing
port_internal_ports+=("$internal_port") # ships in those shapes any more: all 74 descriptors in the
port_access_types+=("${parts[3]}") # catalogue, and every one on a live install, carry nine or more.
port_protocols+=("${parts[4]}") #
port_traefik_managed+=("${parts[5]}") # So refuse and say which row, rather than guess a layout and
port_url_accessibles+=("${parts[6]}") # hand back a port whose access or protocol came from the wrong
port_login_requireds+=("false") # column. A skipped row is visible; a mis-parsed one is not.
port_labels+=("${parts[7]}") isNotice "Port config $port_config_var has ${#parts[@]} column(s); at least 9 are required (parent|name|ext:int|access|proto|login|traefik|webui|label). Skipping this port."
port_url_paths+=("")
port_subdomains+=("")
port_recommendeds+=("${parts[6]}")
elif [[ ${#parts[@]} -ge 7 ]]; then
local external_port="${parts[1]%%:*}"
local internal_port="${parts[1]##*:}"
port_parent_services+=("")
port_service_names+=("${parts[0]}")
port_data_tags+=("PORTS_TAG_$i")
port_external_ports+=("$external_port")
port_internal_ports+=("$internal_port")
port_access_types+=("${parts[2]}")
port_protocols+=("${parts[3]}")
port_traefik_managed+=("${parts[4]}")
port_url_accessibles+=("${parts[5]}")
port_login_requireds+=("false")
port_labels+=("${parts[6]}")
port_url_paths+=("")
port_subdomains+=("")
port_recommendeds+=("${parts[5]}")
fi fi
fi fi
done done