From d6d23c1effa34f73a9547bc4045f1f8aa8d994c5 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 25 Jul 2026 18:31:06 +0000 Subject: [PATCH 1/3] feat: validators for Condition*=/Assert*=, NetDev Kind=, exit statuses and tunnel endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifteen more (parser, ltype) pairs that had no value validator, picked by how often the keys that resolve to them occur in a 6,680-unit corpus of Debian unit files. Together they cover ~2,600 key occurrences in that corpus. Every grammar was written against systemd a8e93919c3 — the same commit the plugin's gperf/man data is generated from — and cites the C function it mirrors. [Unit] conditions (config_parse_unit_condition_path / _string): condition_path (*) every path-valued Condition…=/Assert…=; one wildcard entry because all twelve ltypes parse identically CONDITION_ARCHITECTURE architecture_table + "native" CONDITION_VIRTUALIZATION booleans, vm/container/private-users, virtualization_table CONDITION_SECURITY the closed list in condition_test_security CONDITION_CAPABILITY one capability name, or a number in 0…CAP_LIMIT Enumerations and address forms: config_parse_netdev_kind [NetDev] Kind= config_parse_set_status Success/RestartPrevent/RestartForceExitStatus= config_parse_name_policy [Link] NamePolicy= (has kernel/keep, unlike AlternativeNamesPolicy=) config_parse_wireguard_peer_key base64 32-byte key, or @credential config_parse_tunnel_local_address address, "any", or a local-address type config_parse_tunnel_remote_address address or "any" — no local-address types config_parse_address_generation_type prefixstable/eui64/static IPv6 tokens Also fixes two pre-existing mis-mappings. CONDITION_CPU_FEATURE and CONDITION_CONTROL_GROUP_CONTROLLER were both pointed at the boolean condition grammar, which flagged every legitimate value — ConditionCPUFeature= alone produced 110 false positives in the corpus. CPU features have no closed list (has_cpu_with_flag() reads /proc/cpuinfo), so that one now only pins the shape: exactly one token, which is real, since the parameter is never split. The shared conditionPath()/conditionString() helpers spell the trigger/negate marker combinations out as alternatives instead of wrapping each marker in a ZeroOrOne. ZeroOrOne probes its inner combinator a second time to report reachable input, and the classic engine derives its error TextRange from that number, so a value like `!!/some/path` produced a range past the end of the value and tripped an assertion. ConfigParseUnitConditionStringOptionValue now uses the helper too, fixing the same latent crash there. Checked for false positives by running every registered grammar over the whole corpus: of 16,786 grammar-validated key occurrences, these validators reject 129, and all 129 are deliberately-malformed fixtures — KDE's syntax-highlighting test input, systemd's own negative prefixstable/vti test cases, and one unsubstituted @ETC@ autoconf template. Whole suite green under both the classic and the list-of-successes engine. Co-Authored-By: Claude Opus 5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 25 +- ...igParseAddressGenerationTypeOptionValue.kt | 52 ++++ .../ai/ConfigParseNamePolicyOptionValue.kt | 39 +++ .../ai/ConfigParseNetdevKindOptionValue.kt | 60 ++++ .../ai/ConfigParseSetStatusOptionValue.kt | 92 +++++++ ...onfigParseTunnelLocalAddressOptionValue.kt | 40 +++ ...nfigParseTunnelRemoteAddressOptionValue.kt | 29 ++ ...rseUnitConditionArchitectureOptionValue.kt | 56 ++++ ...ParseUnitConditionCapabilityOptionValue.kt | 23 ++ ...ditionControlGroupControllerOptionValue.kt | 51 ++++ ...ParseUnitConditionCpuFeatureOptionValue.kt | 27 ++ ...ConfigParseUnitConditionPathOptionValue.kt | 25 ++ ...igParseUnitConditionSecurityOptionValue.kt | 34 +++ ...nfigParseUnitConditionStringOptionValue.kt | 36 +-- ...eUnitConditionVirtualizationOptionValue.kt | 63 +++++ .../ConfigParseWireguardPeerKeyOptionValue.kt | 34 +++ .../optionvalues/grammar/Combinators.kt | 60 ++++ .../ai/ConditionAndAssertInspectionTest.kt | 259 ++++++++++++++++++ .../ai/NetdevAndExitStatusInspectionTest.kt | 198 +++++++++++++ 19 files changed, 1178 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressGenerationTypeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNamePolicyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetdevKindOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSetStatusOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelLocalAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelRemoteAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionArchitectureOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCapabilityOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionControlGroupControllerOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCpuFeatureOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionSecurityOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionVirtualizationOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyOptionValue.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConditionAndAssertInspectionTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetdevAndExitStatusInspectionTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index fcf3bcb8..39c5d706 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt @@ -219,9 +219,9 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_trigger_unit", "0") to ConfigParseTriggerUnitOptionValue() as OptionValueInformation, Validator("config_parse_tunnel_mode", "0") to ConfigParseTunnelModeOptionValue() as OptionValueInformation, Validator("config_parse_txqueuelen", "0") to ConfigParseTxqueuelenOptionValue() as OptionValueInformation, + // ConditionACPower= and ConditionFirstBoot= really are booleans; CONDITION_CONTROL_GROUP_CONTROLLER + // and CONDITION_CPU_FEATURE are not, and are handled by their own grammars below. Validator("config_parse_unit_condition_string", "CONDITION_AC_POWER") to ConfigParseUnitConditionStringOptionValue() as OptionValueInformation, - Validator("config_parse_unit_condition_string", "CONDITION_CONTROL_GROUP_CONTROLLER") to ConfigParseUnitConditionStringOptionValue() as OptionValueInformation, - Validator("config_parse_unit_condition_string", "CONDITION_CPU_FEATURE") to ConfigParseUnitConditionStringOptionValue() as OptionValueInformation, Validator("config_parse_unit_condition_string", "CONDITION_FIRST_BOOT") to ConfigParseUnitConditionStringOptionValue() as OptionValueInformation, Validator("config_parse_unit_env_file", "0") to ConfigParseUnitEnvFileOptionValue() as OptionValueInformation, Validator("config_parse_unit_mounts_for", "0") to ConfigParseUnitMountsForOptionValue() as OptionValueInformation, @@ -284,6 +284,27 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_macsec_hw_address", "0") to ConfigParseMacsecHwAddressOptionValue() as OptionValueInformation, Validator("config_parse_ether_addrs", "0") to ConfigParseEtherAddrsOptionValue() as OptionValueInformation, + // Condition*=/Assert*= in [Unit] (#509). + // Every path-valued condition parses identically, so one wildcard entry covers all twelve ltypes. + Validator("config_parse_unit_condition_path", "*") to ConfigParseUnitConditionPathOptionValue() as OptionValueInformation, + Validator("config_parse_unit_condition_string", "CONDITION_ARCHITECTURE") to ConfigParseUnitConditionArchitectureOptionValue() as OptionValueInformation, + Validator("config_parse_unit_condition_string", "CONDITION_VIRTUALIZATION") to ConfigParseUnitConditionVirtualizationOptionValue() as OptionValueInformation, + Validator("config_parse_unit_condition_string", "CONDITION_SECURITY") to ConfigParseUnitConditionSecurityOptionValue() as OptionValueInformation, + Validator("config_parse_unit_condition_string", "CONDITION_CAPABILITY") to ConfigParseUnitConditionCapabilityOptionValue() as OptionValueInformation, + // Both of these were previously mapped onto the boolean condition grammar, which flagged every + // legitimate value; ConditionCPUFeature= alone accounted for 110 false positives in a 6.7k-unit corpus. + Validator("config_parse_unit_condition_string", "CONDITION_CONTROL_GROUP_CONTROLLER") to ConfigParseUnitConditionControlGroupControllerOptionValue() as OptionValueInformation, + Validator("config_parse_unit_condition_string", "CONDITION_CPU_FEATURE") to ConfigParseUnitConditionCpuFeatureOptionValue() as OptionValueInformation, + + // Enumerations and address forms that had no validator (#509). + Validator("config_parse_netdev_kind", "0") to ConfigParseNetdevKindOptionValue() as OptionValueInformation, + Validator("config_parse_set_status", "0") to ConfigParseSetStatusOptionValue() as OptionValueInformation, + Validator("config_parse_name_policy", "0") to ConfigParseNamePolicyOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_peer_key", "0") to ConfigParseWireguardPeerKeyOptionValue() as OptionValueInformation, + Validator("config_parse_tunnel_local_address", "0") to ConfigParseTunnelLocalAddressOptionValue() as OptionValueInformation, + Validator("config_parse_tunnel_remote_address", "0") to ConfigParseTunnelRemoteAddressOptionValue() as OptionValueInformation, + Validator("config_parse_address_generation_type", "0") to ConfigParseAddressGenerationTypeOptionValue() as OptionValueInformation, + ) return allValidators diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressGenerationTypeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressGenerationTypeOptionValue.kt new file mode 100644 index 00000000..3895e10e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressGenerationTypeOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/** + * Validator for the IPv6 address-generation tokens: `[Network] IPv6Token=`, `[IPv6AcceptRA] Token=` + * and `[DHCPPrefixDelegation] Token=` / `[DHCPv6PrefixDelegation] Token=` (.network). + * + * C function: config_parse_address_generation_type in src/network/networkd-address-generation.c, + * which recognises three modes: + * - `prefixstable`, optionally `:ADDRESS`, optionally `,SECRET_KEY` + * - `eui64` + * - `static:ADDRESS`, or a bare ADDRESS (the mode defaults to static) + * ADDRESS is always parsed as IPv6, and SECRET_KEY goes through id128_from_string_nonzero, i.e. 32 + * hex digits or the dashed UUID spelling, and not all-zero. + * + * One divergence: for the static mode systemd also rejects an address whose low 64 bits are zero + * (`static:::`), since only those bits are used. That is a property of the parsed value rather than + * of its shape, so this grammar accepts it. + */ +class ConfigParseAddressGenerationTypeOptionValue : SimpleGrammarOptionValues( + "config_parse_address_generation_type", + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator( + LiteralChoiceTerminal("prefixstable"), + ZeroOrOne(SequenceCombinator(COLON, IPV6_ADDR)), + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal(","), SECRET_KEY)), + ), + LiteralChoiceTerminal("eui64"), + SequenceCombinator(LiteralChoiceTerminal("static:"), IPV6_ADDR), + IPV6_ADDR, + ), + EOF() + ) +) { + companion object { + /** id128_from_string_nonzero: 32 hex digits or 8-4-4-4-12, and not all zeroes. */ + private val SECRET_KEY = RegexTerminal( + """[0-9a-fA-F-]+""", + """(?=.*[1-9a-fA-F])(?:[0-9a-fA-F]{32}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})""" + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNamePolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNamePolicyOptionValue.kt new file mode 100644 index 00000000..f51c28e2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNamePolicyOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/** + * Validator for `[Link] NamePolicy=` (.link). + * + * C function: config_parse_name_policy in src/udev/net/link-config.c, generated by + * DEFINE_CONFIG_PARSE_ENUMV — a whitespace-separated list resolved through name_policy_table + * (src/shared/netif-naming-scheme.c). + * + * Distinct from AlternativeNamesPolicy=, which uses alternative_names_policy_table and therefore does + * not offer `kernel` or `keep`; see [ConfigParseAlternativeNamesPolicyOptionValue]. + */ +class ConfigParseNamePolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_name_policy", + SequenceCombinator( + NAME_POLICY, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), NAME_POLICY)), + EOF() + ) +) { + companion object { + private val NAME_POLICY = FlexibleLiteralChoiceTerminal( + "kernel", + "keep", + "database", + "onboard", + "slot", + "path", + "mac", + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetdevKindOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetdevKindOptionValue.kt new file mode 100644 index 00000000..e3d738ae --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetdevKindOptionValue.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/** + * Validator for `[NetDev] Kind=` (.netdev). + * + * C function: config_parse_netdev_kind in src/network/netdev/netdev.c — a single name resolved by + * netdev_kind_from_string against netdev_kind_table. Not a list. + */ +class ConfigParseNetdevKindOptionValue : SimpleGrammarOptionValues( + "config_parse_netdev_kind", + SequenceCombinator(NETDEV_KIND, EOF()) +) { + companion object { + private val NETDEV_KIND = FlexibleLiteralChoiceTerminal( + "bareudp", + "batadv", + "bond", + "bridge", + "dummy", + "erspan", + "fou", + "geneve", + "gre", + "gretap", + "hsr", + "ifb", + "ip6gre", + "ip6gretap", + "ip6tnl", + "ipip", + "ipoib", + "ipvlan", + "ipvtap", + "l2tp", + "macsec", + "macvlan", + "macvtap", + "nlmon", + "sit", + "tap", + "tun", + "vcan", + "veth", + "vlan", + "vrf", + "vti", + "vti6", + "vxcan", + "vxlan", + "wireguard", + "wlan", + "xfrm", + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSetStatusOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSetStatusOptionValue.kt new file mode 100644 index 00000000..0c52bebf --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSetStatusOptionValue.kt @@ -0,0 +1,92 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/** + * Validator for `[Service] SuccessExitStatus=`, `RestartPreventExitStatus=` and + * `RestartForceExitStatus=`. + * + * C function: config_parse_set_status in src/core/load-fragment.c — a whitespace-separated list where + * each word is resolved by exit_status_from_string first (src/shared/exit-status.c: a name from + * exit_status_mappings, or a decimal 0…255 via safe_atou8) and, failing that, by signal_from_string + * (src/basic/signal-util.c: a name from static_signal_table with or without the `SIG` prefix, or the + * realtime forms `RTMIN[+n]` / `RTMAX[-n]`). + * + * A bare number therefore only ever means an exit status, so the numeric range is 0…255 rather than + * the signal range — signal_from_string is reached with a number only when safe_atou8 already failed, + * and every such number is outside SIGNAL_VALID. + */ +class ConfigParseSetStatusOptionValue : SimpleGrammarOptionValues( + "config_parse_set_status", + SequenceCombinator( + STATUS, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), STATUS)), + EOF() + ) +) { + companion object { + + /** Names from exit_status_mappings, plus every static_signal_table name bare and `SIG`-prefixed. */ + private val EXIT_STATUS_OR_SIGNAL_NAME = FlexibleLiteralChoiceTerminal( + // exit_status_mappings — libc + "SUCCESS", "FAILURE", + // exit_status_mappings — systemd's private range + "CHDIR", "NICE", "FDS", "EXEC", "MEMORY", "LIMITS", "OOM_ADJUST", "SIGNAL_MASK", + "STDIN", "STDOUT", "CHROOT", "IOPRIO", "TIMERSLACK", "SECUREBITS", "SETSCHEDULER", + "CPUAFFINITY", "GROUP", "USER", "CAPABILITIES", "CGROUP", "SETSID", "CONFIRM", + "STDERR", "PAM", "NETWORK", "NAMESPACE", "NO_NEW_PRIVILEGES", "SECCOMP", + "SELINUX_CONTEXT", "PERSONALITY", "APPARMOR", "ADDRESS_FAMILIES", "RUNTIME_DIRECTORY", + "CHOWN", "SMACK_PROCESS_LABEL", "KEYRING", "STATE_DIRECTORY", "CACHE_DIRECTORY", + "LOGS_DIRECTORY", "CONFIGURATION_DIRECTORY", "NUMA_POLICY", "CREDENTIALS", "BPF", + "KSM", "MEMORY_THP", "EXCEPTION", + // exit_status_mappings — LSB + "INVALIDARGUMENT", "NOTIMPLEMENTED", "NOPERMISSION", "NOTINSTALLED", "NOTCONFIGURED", + "NOTRUNNING", + // exit_status_mappings — BSD + "USAGE", "DATAERR", "NOINPUT", "NOUSER", "NOHOST", "UNAVAILABLE", "SOFTWARE", "OSERR", + "OSFILE", "CANTCREAT", "IOERR", "TEMPFAIL", "PROTOCOL", "NOPERM", "CONFIG", + // static_signal_table, bare + "HUP", "INT", "QUIT", "ILL", "TRAP", "ABRT", "BUS", "FPE", "KILL", "USR1", "SEGV", + "USR2", "PIPE", "ALRM", "TERM", "STKFLT", "CHLD", "CONT", "STOP", "TSTP", "TTIN", + "TTOU", "URG", "XCPU", "XFSZ", "VTALRM", "PROF", "WINCH", "IO", "PWR", "SYS", + // static_signal_table, with the SIG prefix signal_from_string strips + "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", "SIGTRAP", "SIGABRT", "SIGBUS", "SIGFPE", + "SIGKILL", "SIGUSR1", "SIGSEGV", "SIGUSR2", "SIGPIPE", "SIGALRM", "SIGTERM", + "SIGSTKFLT", "SIGCHLD", "SIGCONT", "SIGSTOP", "SIGTSTP", "SIGTTIN", "SIGTTOU", + "SIGURG", "SIGXCPU", "SIGXFSZ", "SIGVTALRM", "SIGPROF", "SIGWINCH", "SIGIO", "SIGPWR", + "SIGSYS", + ) + + /** + * `RTMIN`, `RTMIN+n`, `RTMAX`, `RTMAX-n`, each optionally `SIG`-prefixed. The offset bound is + * SIGRTMAX - SIGRTMIN, which is 30 with glibc's reserved realtime signals. + */ + private val REALTIME_SIGNAL = AlternativeCombinator( + SequenceCombinator( + LiteralChoiceTerminal("SIGRTMIN", "RTMIN"), + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal("+"), IntegerTerminal(0, 31))) + ), + SequenceCombinator( + LiteralChoiceTerminal("SIGRTMAX", "RTMAX"), + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal("-"), IntegerTerminal(0, 31))) + ), + ) + + // The number goes first so the name terminal's lenient shape match (its choices contain + // digits, e.g. USR1) can't swallow a numeric word before the range check runs. + private val STATUS = AlternativeCombinator( + IntegerTerminal(0, 256), + REALTIME_SIGNAL, + EXIT_STATUS_OR_SIGNAL_NAME, + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelLocalAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelLocalAddressOptionValue.kt new file mode 100644 index 00000000..9115379b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelLocalAddressOptionValue.kt @@ -0,0 +1,40 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/** + * Validator for `[Tunnel] Local=` (.netdev). + * + * C function: config_parse_tunnel_local_address in src/network/netdev/tunnel.c. It accepts `any` + * (unset), one of the netdev_local_address_type_table names — which tell networkd to pick up the + * address assigned by that mechanism — or an IPv4/IPv6 literal via in_addr_from_string_auto. + * + * The address alternatives come before the keyword terminal: the keyword terminal matches loosely by + * character shape, so on `10.65.0.1` it would otherwise consume the leading `10` and strand the rest + * (the classic matcher commits to the first alternative that matches and never backtracks). + */ +class ConfigParseTunnelLocalAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_tunnel_local_address", + SequenceCombinator( + AlternativeCombinator( + IPV6_ADDR, + IPV4_ADDR, + FlexibleLiteralChoiceTerminal( + "any", + "ipv4_link_local", + "ipv6_link_local", + "dhcp4", + "dhcp6", + "slaac", + "dhcp_pd", + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelRemoteAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelRemoteAddressOptionValue.kt new file mode 100644 index 00000000..9133c81c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelRemoteAddressOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/** + * Validator for `[Tunnel] Remote=` (.netdev). + * + * C function: config_parse_tunnel_remote_address in src/network/netdev/tunnel.c — `any` (unset) or an + * IPv4/IPv6 literal via in_addr_from_string_auto. Unlike `Local=` it has no local-address-type + * keywords: those name a way for the *host* to obtain its own address, which says nothing about the + * far end of the tunnel. + */ +class ConfigParseTunnelRemoteAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_tunnel_remote_address", + SequenceCombinator( + AlternativeCombinator( + IPV6_ADDR, + IPV4_ADDR, + LiteralChoiceTerminal("any"), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionArchitectureOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionArchitectureOptionValue.kt new file mode 100644 index 00000000..4c483c15 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionArchitectureOptionValue.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString + +/** + * Validator for `[Unit] ConditionArchitecture=` / `AssertArchitecture=`. + * + * C function: config_parse_unit_condition_string with ltype CONDITION_ARCHITECTURE; the parameter is + * checked by condition_test_architecture_parameter (src/shared/condition.c), which accepts the literal + * `native` or any name in architecture_table (src/basic/architecture.c). + */ +class ConfigParseUnitConditionArchitectureOptionValue : SimpleGrammarOptionValues( + "config_parse_unit_condition_string", + conditionString(ARCHITECTURE) +) { + companion object { + private val ARCHITECTURE = FlexibleLiteralChoiceTerminal( + "native", + "alpha", + "arc", + "arc-be", + "arm", + "arm-be", + "arm64", + "arm64-be", + "cris", + "ia64", + "loongarch64", + "m68k", + "mips", + "mips-le", + "mips64", + "mips64-le", + "nios2", + "parisc", + "parisc64", + "ppc", + "ppc-le", + "ppc64", + "ppc64-le", + "riscv32", + "riscv64", + "s390", + "s390x", + "sh", + "sh64", + "sparc", + "sparc64", + "tilegx", + "x86", + "x86-64", + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCapabilityOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCapabilityOptionValue.kt new file mode 100644 index 00000000..3355eb23 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCapabilityOptionValue.kt @@ -0,0 +1,23 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString + +/** + * Validator for `[Unit] ConditionCapability=` / `AssertCapability=`. + * + * C function: config_parse_unit_condition_string with ltype CONDITION_CAPABILITY; the parameter goes + * to capability_from_name (src/basic/capability-list.c), which accepts either a decimal capability + * number in 0…CAP_LIMIT (62) or a single capability name. One capability, not a list. + * + * The number is tried first so the numeric form isn't swallowed by the name terminal's lenient shape + * match. Note that systemd's name lookup is gperf-generated with `--ignore-case`, so `cap_sys_admin` + * also resolves; like the existing CapabilityBoundingSet= validator this grammar lists only the + * canonical upper-case spellings. + */ +class ConfigParseUnitConditionCapabilityOptionValue : SimpleGrammarOptionValues( + "config_parse_unit_condition_string", + conditionString(AlternativeCombinator(IntegerTerminal(0, 63), Capabilities)) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionControlGroupControllerOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionControlGroupControllerOptionValue.kt new file mode 100644 index 00000000..e8e362be --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionControlGroupControllerOptionValue.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString + +/** + * Validator for `[Unit] ConditionControlGroupController=` / `AssertControlGroupController=`. + * + * C function: config_parse_unit_condition_string with ltype CONDITION_CONTROL_GROUP_CONTROLLER; + * condition_test_control_group_controller (src/shared/condition.c) special-cases the whole parameter + * being `v1` or `v2`, and otherwise hands it to cg_mask_from_string (src/basic/cgroup-util.c), which + * splits on whitespace and resolves each word through cgroup_controller_table. + * + * `v1`/`v2` are listed as ordinary list members rather than a separate whole-value alternative: they + * are only meaningful alone, but cg_mask_from_string silently skips words it doesn't know, so mixing + * them into a list is tolerated by systemd and flagging it would be a false positive. + */ +class ConfigParseUnitConditionControlGroupControllerOptionValue : SimpleGrammarOptionValues( + "config_parse_unit_condition_string", + conditionString( + SequenceCombinator( + CONTROLLER, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), CONTROLLER)) + ) + ) +) { + companion object { + private val CONTROLLER = FlexibleLiteralChoiceTerminal( + "v1", + "v2", + "cpu", + "cpuacct", + "cpuset", + "io", + "blkio", + "memory", + "devices", + "pids", + "bpf-firewall", + "bpf-devices", + "bpf-foreign", + "bpf-socket-bind", + "bpf-restrict-network-interfaces", + "bpf-bind-network-interface", + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCpuFeatureOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCpuFeatureOptionValue.kt new file mode 100644 index 00000000..fe811edb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCpuFeatureOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString + +/** + * Validator for `[Unit] ConditionCPUFeature=` / `AssertCPUFeature=`. + * + * C function: config_parse_unit_condition_string with ltype CONDITION_CPU_FEATURE; + * condition_test_cpufeature (src/shared/condition.c) splits an optional `.` prefix off the + * parameter and looks the rest up with has_cpu_with_flag(), which scans the `flags` line of + * /proc/cpuinfo. + * + * That flag set is whatever the running CPU reports, so there is no list to check against and this + * grammar deliberately only pins the shape: exactly one whitespace-free token after the optional + * trigger/negate markers. That much *is* checkable — the parameter is never split, so + * `ConditionCPUFeature=sse2 avx` asks for a single feature literally named "sse2 avx" and can never + * be true. + * + * (This replaces an earlier mapping of CONDITION_CPU_FEATURE onto the boolean condition grammar, + * which rejected every real feature name.) + */ +class ConfigParseUnitConditionCpuFeatureOptionValue : SimpleGrammarOptionValues( + "config_parse_unit_condition_string", + conditionString(RegexTerminal("""\S+""", """[A-Za-z0-9_.\-]+""")) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt new file mode 100644 index 00000000..8c12f578 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt @@ -0,0 +1,25 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CONDITION_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionPath + +/** + * Validator for every path-valued `Condition…=` / `Assert…=` in `[Unit]`. + * + * C function: config_parse_unit_condition_path in src/core/load-fragment.c. It strips an optional + * `|` (trigger) and then an optional `!` (negate) — with no whitespace allowed after either — runs + * unit_path_printf() over the rest and requires the result to be absolute + * (path_simplify_and_warn with PATH_CHECK_ABSOLUTE). + * + * Registered under the `*` ltype wildcard because the grammar does not depend on the ConditionType: + * ConditionPathExists, ConditionPathExistsGlob, ConditionPathIsDirectory, ConditionPathIsSymbolicLink, + * ConditionPathIsMountPoint, ConditionPathIsReadWrite, ConditionPathIsEncrypted, ConditionPathIsSocket, + * ConditionDirectoryNotEmpty, ConditionFileNotEmpty, ConditionFileIsExecutable and ConditionNeedsUpdate + * (plus each Assert… twin) all parse identically. Glob metacharacters need no special handling — they + * are ordinary path characters here, and the glob is only expanded when the condition is evaluated. + */ +class ConfigParseUnitConditionPathOptionValue : SimpleGrammarOptionValues( + "config_parse_unit_condition_path", + conditionPath(CONDITION_PATH) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionSecurityOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionSecurityOptionValue.kt new file mode 100644 index 00000000..16cb725e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionSecurityOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString + +/** + * Validator for `[Unit] ConditionSecurity=` / `AssertSecurity=`. + * + * C function: config_parse_unit_condition_string with ltype CONDITION_SECURITY; the parameter is + * compared with streq() against a closed list in condition_test_security (src/shared/condition.c). + * A single technology only — condition_test_security never splits the parameter, so `apparmor selinux` + * is not two values, it is one unrecognised one. + */ +class ConfigParseUnitConditionSecurityOptionValue : SimpleGrammarOptionValues( + "config_parse_unit_condition_string", + conditionString(SECURITY) +) { + companion object { + private val SECURITY = FlexibleLiteralChoiceTerminal( + "selinux", + "smack", + "apparmor", + "audit", + "ima", + "tomoyo", + "uefi-secureboot", + "tpm2", + "cvm", + "measured-uki", + "measured-os", + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionStringOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionStringOptionValue.kt index e27e3d3e..fe84896e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionStringOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionStringOptionValue.kt @@ -1,32 +1,22 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString /** - * Validator for Unit.AssertFirstBoot, Unit.ConditionFirstBoot - * C Function: config_parse_unit_condition_string(CONDITION_FIRST_BOOT) - * Used by Options: Unit.AssertFirstBoot, Unit.ConditionFirstBoot - * - * Accepts boolean values with optional trigger (|) and negate (!) prefixes. - * Format: [|] [!] + * Validator for the `[Unit]` conditions whose parameter is a plain boolean: `ConditionFirstBoot=` and + * `ConditionACPower=`, plus their `Assert…=` twins. + * + * C function: config_parse_unit_condition_string with ltype CONDITION_FIRST_BOOT / CONDITION_AC_POWER. + * condition_test_first_boot and condition_test_ac_power (src/shared/condition.c) both feed the whole + * parameter to parse_boolean() and treat a parse failure as an error, so nothing else is accepted. + * + * The trigger/negate prefix now comes from the shared [conditionString] helper, which spells the + * marker combinations out as alternatives; the ZeroOrOne-based prefix this class used before could + * report an error range past the end of the value on inputs like `!!yes`. */ class ConfigParseUnitConditionStringOptionValue : SimpleGrammarOptionValues( "config_parse_unit_condition_string", - SequenceCombinator( - // Optional trigger prefix: | - ZeroOrOne(SequenceCombinator( - LiteralChoiceTerminal("|"), - ZeroOrMore(WhitespaceTerminal()) - )), - // Optional negate prefix: ! - ZeroOrOne(SequenceCombinator( - LiteralChoiceTerminal("!"), - ZeroOrMore(WhitespaceTerminal()) - )), - // Boolean value - BOOLEAN, - EOF() - ) + conditionString(BOOLEAN) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionVirtualizationOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionVirtualizationOptionValue.kt new file mode 100644 index 00000000..2855576e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionVirtualizationOptionValue.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString + +/** + * Validator for `[Unit] ConditionVirtualization=` / `AssertVirtualization=`. + * + * C function: config_parse_unit_condition_string with ltype CONDITION_VIRTUALIZATION; the parameter is + * checked by condition_test_virtualization (src/shared/condition.c), which accepts, in order: + * `private-users`, any boolean parse_boolean() understands, the categories `vm` and `container`, and + * finally any id in virtualization_table (src/basic/virt.c). + * + * The names are folded into one terminal rather than an alternation so that the whole value is one + * token — that keeps error localization and completion pointing at the value itself. + */ +class ConfigParseUnitConditionVirtualizationOptionValue : SimpleGrammarOptionValues( + "config_parse_unit_condition_string", + conditionString(VIRTUALIZATION) +) { + companion object { + private val VIRTUALIZATION = FlexibleLiteralChoiceTerminal( + // parse_boolean() + "1", "yes", "y", "true", "t", "on", "0", "no", "n", "false", "f", "off", + // categories, plus the userns special case + "vm", "container", "private-users", + // virtualization_table — VMs + "none", + "kvm", + "amazon", + "qemu", + "bochs", + "xen", + "uml", + "vmware", + "oracle", + "microsoft", + "zvm", + "parallels", + "bhyve", + "qnx", + "acrn", + "powervm", + "apple", + "sre", + "google", + "vm-other", + // virtualization_table — containers + "systemd-nspawn", + "lxc-libvirt", + "lxc", + "openvz", + "docker", + "podman", + "rkt", + "wsl", + "proot", + "pouch", + "container-other", + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyOptionValue.kt new file mode 100644 index 00000000..e839da19 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/** + * Validator for `[WireGuardPeer] PublicKey=` and `PresharedKey=` (.netdev). + * + * C function: config_parse_wireguard_peer_key in src/network/netdev/wireguard.c → + * wireguard_decode_key_and_warn, which either reads a credential when the value starts with `@`, or + * base64-decodes the value and requires exactly WG_KEY_LEN (32) bytes. + * + * 32 bytes is 43 base64 characters plus one `=` of padding; systemd's unbase64mem tolerates the + * padding being omitted, so both spellings are accepted here. + */ +class ConfigParseWireguardPeerKeyOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_peer_key", + SequenceCombinator( + AlternativeCombinator( + // @credential-name — resolved at load time via read_credential(). + SequenceCombinator( + LiteralChoiceTerminal("@"), + RegexTerminal("""\S+""", """[A-Za-z0-9_.\-]+""") + ), + // A base64-encoded 32-byte key. + RegexTerminal("""[A-Za-z0-9+/=]+""", """[A-Za-z0-9+/]{43}=?"""), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt index 6004f758..59229ae3 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt @@ -151,3 +151,63 @@ val HARDWARE_ADDRESS = AlternativeCombinator( ) +// --------------------------------------------------------------------------------------------------- +// Condition*= / Assert*= (systemd src/core/load-fragment.c). Both parsers strip an optional leading +// trigger marker `|` and then an optional negation marker `!`, in that order, before handing what is +// left to the per-condition check. Only that order is recognised: in `!|foo` the `!` negates and the +// parameter is literally `| foo`, which no condition accepts. +val PIPE = LiteralChoiceTerminal("|") +val BANG = LiteralChoiceTerminal("!") + +// Both helpers below spell the marker combinations out as alternatives rather than wrapping each +// marker in a ZeroOrOne. ZeroOrOne probes its inner combinator a second time to report how far input +// could reach, so `!!/some/path` would report a longest-match one character past the `!` the grammar +// actually consumed, and the classic engine derives its error TextRange from that number — landing it +// past the end of the value. Alternatives keep the reported offset equal to what was consumed. + +/** + * A `Condition=`/`Assert=` value. + * + * config_parse_unit_condition_path advances with a bare `rvalue++` per marker, so no whitespace may + * follow either one. + */ +fun conditionPath(parameter: Combinator): Combinator = SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("|!"), parameter), + SequenceCombinator(PIPE, parameter), + SequenceCombinator(BANG, parameter), + parameter, + ), + EOF() +) + +/** + * A `Condition=`/`Assert=` value. + * + * config_parse_unit_condition_string advances with `rvalue += 1 + strspn(rvalue + 1, WHITESPACE)`, so + * whitespace after a marker is skipped. + */ +fun conditionString(parameter: Combinator): Combinator { + val optionalWhitespace = ZeroOrOne(WhitespaceTerminal()) + return SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(PIPE, optionalWhitespace, BANG, optionalWhitespace, parameter), + SequenceCombinator(PIPE, optionalWhitespace, parameter), + SequenceCombinator(BANG, optionalWhitespace, parameter), + parameter, + ), + EOF() + ) +} + +/** + * The parameter of a path-valued condition: systemd resolves specifiers with unit_path_printf() and + * then requires the result to be absolute (path_simplify_and_warn with PATH_CHECK_ABSOLUTE). We can't + * expand specifiers, so a value may legitimately begin with one (`%t/foo`, `%h/.cache`) instead of a + * slash. Spaces are only allowed when backslash-escaped, matching the rest of the plugin's paths. + */ +val CONDITION_PATH = RegexTerminal( + """\S(?:[^\s\\]|\\[\s\S])*""", + """(?:/|%\S)(?:[^\s\\]|\\[\s\S])*""" +) + diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConditionAndAssertInspectionTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConditionAndAssertInspectionTest.kt new file mode 100644 index 00000000..80cd2acd --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConditionAndAssertInspectionTest.kt @@ -0,0 +1,259 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** + * Tests for the `[Unit] Condition…=` / `Assert…=` validators (#509). + * + * Every case here is checked against systemd's own parsers: config_parse_unit_condition_path and + * config_parse_unit_condition_string in src/core/load-fragment.c, plus the per-type checks in + * src/shared/condition.c. + */ +class ConditionAndAssertInspectionTest : AbstractUnitFileTest() { + + private fun highlights(text: String): Int { + setupFileInEditor("f.service", text) + enableInspection(InvalidValueInspection::class.java) + return myFixture.doHighlighting().size + } + + private fun assertAccepted(vararg lines: String) = + assertEquals(lines.joinToString(), 0, highlights("[Unit]\n" + lines.joinToString("\n") + "\n")) + + // The classic engine can split a partial match into more than one region, so require "at least one". + private fun assertRejected(line: String) = + assertTrue(line, highlights("[Unit]\n$line\n") >= 1) + + // ------------------------------------------------------------------ path conditions + + @Test + fun testPathConditionsAcceptAbsolutePathsAndMarkers() { + assertAccepted( + "ConditionPathExists=/etc/foo.conf", + "ConditionPathExists=!/var/cache/seeded", + "ConditionPathExists=|/etc/optional.conf", + "ConditionPathExists=|!/etc/ld.so.cache", + "ConditionPathExistsGlob=/dev/snd/control*", + "ConditionDirectoryNotEmpty=/var/lib/dracut/hooks", + "ConditionFileNotEmpty=/etc/machine-id", + "ConditionFileIsExecutable=/usr/bin/foo", + "ConditionPathIsReadWrite=/proc/sys", + "ConditionPathIsDirectory=/run", + "ConditionPathIsSymbolicLink=/etc/localtime", + "ConditionPathIsMountPoint=/boot", + "ConditionPathIsEncrypted=/home", + "ConditionPathIsSocket=/run/dbus/system_bus_socket", + "ConditionNeedsUpdate=/etc", + ) + } + + @Test + fun testPathConditionsCoverTheAssertTwins() { + // The wildcard registration has to reach the [Unit] Assert… keys as well. + assertAccepted( + "AssertPathExists=/etc/foo.conf", + "AssertFileIsExecutable=!/usr/bin/foo", + "AssertDirectoryNotEmpty=|/var/lib/foo", + ) + } + + @Test + fun testPathConditionsAcceptSpecifiers() { + // unit_path_printf() runs before the absolute-path check, so a value may start with a specifier. + assertAccepted( + "ConditionPathExists=%t/gnome-shell-disable-extensions", + "ConditionPathExists=%h/.config/foo", + "ConditionPathExistsGlob=%C/drkonqi/sentry-envelopes/*", + "ConditionFileNotEmpty=/etc/conmux/%i", + ) + } + + @Test + fun testPathConditionsRejectRelativePaths() { + // path_simplify_and_warn(..., PATH_CHECK_ABSOLUTE) rejects anything that isn't absolute. + assertRejected("ConditionPathExists=etc/foo.conf") + assertRejected("ConditionPathExists=../foo") + } + + @Test + fun testPathConditionsRejectWhitespaceAfterMarkers() { + // config_parse_unit_condition_path advances past '|' and '!' with a bare rvalue++, so a space + // after a marker becomes part of the path and the absolute check then fails. + assertRejected("ConditionPathExists=|! /some/path") + assertRejected("ConditionPathExists=! /some/path") + } + + @Test + fun testPathConditionsRejectMarkersInTheWrongOrder() { + // '|' is only recognised first; in "!|/x" the '!' negates and the parameter is literally "|/x". + assertRejected("ConditionPathExists=!|/some/path") + assertRejected("ConditionPathExists=!!/some/path") + } + + @Test + fun testPathConditionsRejectLists() { + // The whole value is one path — it is never split on whitespace. + assertRejected("ConditionNeedsUpdate=/etc /var") + } + + // ------------------------------------------------------------------ ConditionArchitecture + + @Test + fun testArchitectureAcceptsTableNamesAndNative() { + assertAccepted( + "ConditionArchitecture=x86-64", + "ConditionArchitecture=x86", + "ConditionArchitecture=arm64-be", + "ConditionArchitecture=loongarch64", + "ConditionArchitecture=riscv64", + "ConditionArchitecture=native", + "AssertArchitecture=!alpha", + "ConditionArchitecture=|! ppc64-le", + ) + } + + @Test + fun testArchitectureRejectsUnknownAndLists() { + assertRejected("ConditionArchitecture=x86_64") // the table spells it with a hyphen + assertRejected("ConditionArchitecture=invalid") + assertRejected("ConditionArchitecture=sparc x86") + assertRejected("ConditionArchitecture=!| alpha") + } + + // ------------------------------------------------------------------ ConditionVirtualization + + @Test + fun testVirtualizationAcceptsBooleansCategoriesAndIds() { + assertAccepted( + "ConditionVirtualization=no", + "ConditionVirtualization=yes", + "ConditionVirtualization=false", + "ConditionVirtualization=vm", + "ConditionVirtualization=container", + "ConditionVirtualization=!container", + "ConditionVirtualization=!private-users", + "ConditionVirtualization=microsoft", + "ConditionVirtualization=systemd-nspawn", + "ConditionVirtualization=lxc-libvirt", + "ConditionVirtualization=|vmware", + ) + } + + @Test + fun testVirtualizationRejectsUnknownAndLists() { + assertRejected("ConditionVirtualization=invalid") + assertRejected("ConditionVirtualization=xen vmware") + } + + // ------------------------------------------------------------------ ConditionSecurity + + @Test + fun testSecurityAcceptsTheKnownTechnologies() { + assertAccepted( + "ConditionSecurity=selinux", + "ConditionSecurity=!selinux", + "ConditionSecurity=apparmor", + "ConditionSecurity=smack", + "ConditionSecurity=audit", + "ConditionSecurity=ima", + "ConditionSecurity=tomoyo", + "ConditionSecurity=uefi-secureboot", + "ConditionSecurity=tpm2", + "ConditionSecurity=cvm", + "ConditionSecurity=measured-uki", + "ConditionSecurity=measured-os", + "AssertSecurity=| ! selinux", + ) + } + + @Test + fun testSecurityRejectsUnknownAndLists() { + assertRejected("ConditionSecurity=invalid") + // condition_test_security compares the whole parameter, so this asks for one oddly-named + // technology rather than for either of two. + assertRejected("ConditionSecurity=apparmor selinux") + } + + // ------------------------------------------------------------------ ConditionCapability + + @Test + fun testCapabilityAcceptsOneNameOrNumber() { + assertAccepted( + "ConditionCapability=CAP_SYS_ADMIN", + "ConditionCapability=!CAP_NET_ADMIN", + "ConditionCapability=CAP_CHECKPOINT_RESTORE", + "ConditionCapability=0", + "ConditionCapability=62", + "AssertCapability=|! CAP_CHOWN", + ) + } + + @Test + fun testCapabilityRejectsUnknownOutOfRangeAndLists() { + assertRejected("ConditionCapability=CAP_BOGUS") + assertRejected("ConditionCapability=63") // CAP_LIMIT is 62 + assertRejected("ConditionCapability=CAP_NET_ADMIN CAP_NET_RAW") // one capability only + } + + // ------------------------------------------------------------------ ConditionControlGroupController + + @Test + fun testControlGroupControllerAcceptsControllerNames() { + // Previously mapped onto the boolean grammar, which rejected all of these. + assertAccepted( + "ConditionControlGroupController=cpu", + "ConditionControlGroupController=memory", + "ConditionControlGroupController=io", + "ConditionControlGroupController=pids", + "ConditionControlGroupController=cpuset", + "ConditionControlGroupController=blkio", + "ConditionControlGroupController=cpuacct", + "ConditionControlGroupController=devices", + "ConditionControlGroupController=bpf-firewall", + "ConditionControlGroupController=v1", + "ConditionControlGroupController=v2", + "ConditionControlGroupController=cpu memory", + "AssertControlGroupController=|! cpu", + ) + } + + @Test + fun testControlGroupControllerRejectsUnknownNames() { + assertRejected("ConditionControlGroupController=invalid") + } + + // ------------------------------------------------------------------ ConditionCPUFeature + + @Test + fun testCpuFeatureAcceptsFeatureNames() { + // Previously mapped onto the boolean grammar; /proc/cpuinfo flags have no closed list, so only + // the shape is checked. + assertAccepted( + "ConditionCPUFeature=sse2", + "ConditionCPUFeature=avx2", + "ConditionCPUFeature=aes", + "ConditionCPUFeature=x86-64.sse2", + "AssertCPUFeature=|! sse2", + ) + } + + @Test + fun testCpuFeatureRejectsLists() { + // condition_test_cpufeature never splits the parameter. + assertRejected("ConditionCPUFeature=sse2 avx") + } + + // ------------------------------------------------------------------ boolean conditions + + @Test + fun testBooleanConditionsStillWork() { + assertAccepted( + "ConditionFirstBoot=yes", + "ConditionACPower=true", + "AssertFirstBoot=|false", + ) + assertRejected("ConditionFirstBoot=sometimes") + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetdevAndExitStatusInspectionTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetdevAndExitStatusInspectionTest.kt new file mode 100644 index 00000000..828395e0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetdevAndExitStatusInspectionTest.kt @@ -0,0 +1,198 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** + * Tests for the non-condition validators added in #509: NetDev Kind=, exit-status sets, .link + * NamePolicy=, WireGuard peer keys, tunnel endpoints and IPv6 address-generation tokens. + */ +class NetdevAndExitStatusInspectionTest : AbstractUnitFileTest() { + + private fun highlights(fileName: String, text: String): Int { + setupFileInEditor(fileName, text) + enableInspection(InvalidValueInspection::class.java) + return myFixture.doHighlighting().size + } + + private fun assertAccepted(fileName: String, vararg lines: String) = + assertEquals(lines.joinToString(), 0, highlights(fileName, lines.joinToString("\n") + "\n")) + + private fun assertRejected(fileName: String, text: String) = + assertTrue(text, highlights(fileName, text) >= 1) + + // ------------------------------------------------------------------ [NetDev] Kind= + + @Test + fun testNetdevKindAcceptsTableNames() { + assertAccepted( + "f.netdev", + "[NetDev]", + "Kind=veth", "Kind=bridge", "Kind=wireguard", "Kind=ip6gretap", "Kind=batadv", + "Kind=bareudp", "Kind=ipoib", "Kind=vxcan", "Kind=xfrm", "Kind=nlmon", "Kind=hsr", + ) + } + + @Test + fun testNetdevKindRejectsUnknownAndLists() { + assertRejected("f.netdev", "[NetDev]\nKind=bogus\n") + // netdev_kind_from_string takes the whole value; Kind= is not a list. + assertRejected("f.netdev", "[NetDev]\nKind=gre gretap\n") + } + + // ------------------------------------------------------------------ exit status sets + + @Test + fun testExitStatusAcceptsNumbersNamesAndSignals() { + assertAccepted( + "f.service", + "[Service]", + "SuccessExitStatus=143", + "SuccessExitStatus=0 143", + "SuccessExitStatus=137 143", + "SuccessExitStatus=255", + "SuccessExitStatus=DATAERR", + "SuccessExitStatus=DATAERR CANTCREAT", + "SuccessExitStatus=NOTCONFIGURED", + "SuccessExitStatus=RUNTIME_DIRECTORY PROTOCOL", + "SuccessExitStatus=SIGTERM", + "SuccessExitStatus=TERM", + "SuccessExitStatus=SIGKILL SIGQUIT 99", + "RestartPreventExitStatus=1", + "RestartForceExitStatus=3 4", + ) + } + + @Test + fun testExitStatusAcceptsRealtimeSignals() { + assertAccepted( + "f.service", + "[Service]", + "SuccessExitStatus=RTMIN", + "SuccessExitStatus=RTMIN+3", + "SuccessExitStatus=SIGRTMAX", + "SuccessExitStatus=SIGRTMAX-2", + ) + } + + @Test + fun testExitStatusRejectsOutOfRangeAndUnknownNames() { + // A bare number is only ever an exit status, so the range is 0…255 (safe_atou8). + assertRejected("f.service", "[Service]\nSuccessExitStatus=256\n") + assertRejected("f.service", "[Service]\nSuccessExitStatus=-23\n") + assertRejected("f.service", "[Service]\nSuccessExitStatus=invalid\n") + assertRejected("f.service", "[Service]\nSuccessExitStatus=SIGBOGUS\n") + } + + // ------------------------------------------------------------------ [Link] NamePolicy= + + @Test + fun testNamePolicyAcceptsTheFullPolicyList() { + assertAccepted( + "f.link", + "[Link]", + "NamePolicy=keep", + "NamePolicy=mac", + "NamePolicy=keep kernel", + "NamePolicy=keep kernel database onboard slot path", + ) + } + + @Test + fun testNamePolicyRejectsUnknownNamesAndCommas() { + assertRejected("f.link", "[Link]\nNamePolicy=bogus\n") + assertRejected("f.link", "[Link]\nNamePolicy=keep,kernel\n") + } + + @Test + fun testNamePolicyIsNotAlternativeNamesPolicy() { + // name_policy_table has kernel/keep; alternative_names_policy_table does not. + assertAccepted("f.link", "[Link]", "NamePolicy=kernel") + assertRejected("f.link", "[Link]\nAlternativeNamesPolicy=kernel\n") + } + + // ------------------------------------------------------------------ WireGuard peer keys + + @Test + fun testWireguardPeerKeyAcceptsBase64AndCredentials() { + assertAccepted( + "f.netdev", + "[WireGuardPeer]", + "PublicKey=RDf+LSpeEre7YEIKaxg+wbpsNV7du+ktR99uBEtIiCA=", + "PresharedKey=IIWIV17wutHv7t4cR6pOT91z6NSz/T8Arh0yaywhw3M=", + "PublicKey=@wg-public-key", + ) + } + + @Test + fun testWireguardPeerKeyRejectsWrongLengthAndBadCharacters() { + // WG_KEY_LEN is 32 bytes, i.e. 43 base64 characters plus optional padding. + assertRejected("f.netdev", "[WireGuardPeer]\nPublicKey=RDf+LSpeEre7YEIKaxg+wbpsNV7du+ktR99uBEtI=\n") + assertRejected("f.netdev", "[WireGuardPeer]\nPublicKey=not a key\n") + } + + // ------------------------------------------------------------------ [Tunnel] Local=/Remote= + + @Test + fun testTunnelLocalAcceptsAddressesAndLocalAddressTypes() { + assertAccepted( + "f.netdev", + "[Tunnel]", + "Local=10.65.223.238", + "Local=2a00:ffde:4567:edde::4987", + "Local=any", + "Local=slaac", + "Local=dhcp4", + "Local=dhcp6", + "Local=ipv4_link_local", + "Local=ipv6_link_local", + "Local=dhcp_pd", + ) + } + + @Test + fun testTunnelRemoteAcceptsAddressesButNotLocalAddressTypes() { + assertAccepted("f.netdev", "[Tunnel]", "Remote=10.65.223.239", "Remote=2001:473:fece:cafe::5179", "Remote=any") + // config_parse_tunnel_remote_address has no netdev_local_address_type_from_string() call. + assertRejected("f.netdev", "[Tunnel]\nRemote=slaac\n") + assertRejected("f.netdev", "[Tunnel]\nRemote=dhcp4\n") + } + + @Test + fun testTunnelAddressesRejectNonsense() { + // systemd's own 25-vti-tunnel-local-any.netdev sets Local=remote and the test expects the link to + // come up with "local any", i.e. the value was rejected and the field left unset. + assertRejected("f.netdev", "[Tunnel]\nLocal=remote\n") + assertRejected("f.netdev", "[Tunnel]\nLocal=10.65.223\n") + } + + // ------------------------------------------------------------------ IPv6 address generation + + @Test + fun testAddressGenerationAcceptsEveryMode() { + assertAccepted( + "f.network", + "[Network]", + "IPv6Token=eui64", + "IPv6Token=::1a:2b:3c:4d", + "IPv6Token=static:::fa:de:ca:fe", + "IPv6Token=prefixstable", + "IPv6Token=prefixstable:2002:da8:1::", + "IPv6Token=prefixstable:2002:da8:1::,86b123b969ba4b7eb8b3d8605123525a", + "IPv6Token=prefixstable,86b123b969ba4b7eb8b3d8605123525a", + "IPv6Token=prefixstable,86b123b9-69ba-4b7e-b8b3-d8605123525a", + ) + } + + @Test + fun testAddressGenerationRejectsSystemdsOwnNegativeCases() { + // These four all appear in systemd's 25-ipv6-prefix-veth-token-prefixstable.network as values + // networkd logs and ignores. + assertRejected("f.network", "[IPv6AcceptRA]\nToken=prefixstable@\n") + assertRejected("f.network", "[IPv6AcceptRA]\nToken=prefixstable,\n") + assertRejected("f.network", "[IPv6AcceptRA]\nToken=prefixstable,00000000000000000000000000000000\n") + assertRejected("f.network", "[IPv6AcceptRA]\nToken=static\n") + assertRejected("f.network", "[IPv6AcceptRA]\nToken=static:\n") + } +} From 65e2eb1ddd94725dcfdcdc76be3430f008337c4f Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 25 Jul 2026 18:51:29 +0000 Subject: [PATCH 2/3] feat: validators for [RoutingPolicyRule], [Route], [Address], [NextHop] and unit paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thirty-four more (parser, ltype) pairs, taking the undocumented-validator count from 325 to 291. As with the previous batch, every grammar is written against systemd a8e93919c3 — the commit the plugin's gperf/man data is generated from — and names the C function it mirrors. config_parse_routing_policy_rule, config_parse_route_section, config_parse_address_section and config_parse_nexthop_section are dispatchers: the ltype indexes a ConfigSectionParser table and each entry names the parser that actually reads the value. These commits mirror the tables entry by entry rather than giving a whole section one grammar. [RoutingPolicyRule] From=/To= (address + optional prefix), IncomingInterface=/OutgoingInterface=, Priority=, GoTo=, TypeOfService=, SuppressPrefixLength=, SuppressInterfaceGroup=, SourcePort=/DestinationPort=, Family=, Type=, Invert=, L3MasterDevice= [Route] Destination=/Source=, PreferredSource=, GatewayOnLink=, Scope= [Address] Peer=, Broadcast=, PreferredLifetime= [NextHop] Gateway= networkd/udev [Match] Name= and .link OriginalName=, [BridgeVLAN] VLAN=, [Tunnel] Key=, [IPv6Prefix] *LifetimeSec=, MTUBytes= for all three ltypes unit files [Path] watch settings, [Socket] Symlinks=, [Socket] Service=, [Service] Sockets= Fixes two more pre-existing mis-mappings of the same shape as the CONDITION_CPU_FEATURE one. Every [Address] ltype shared the uint32 grammar written for RouteMetric=, and every [NextHop] ltype shared the boolean grammar written for OnLink=. Between them they flagged `AddPrefixRoute=no`, `HomeAddress=yes`, `ManageTemporaryAddress=yes`, `DuplicateAddressDetection=ipv4`, `[NextHop] Id=20`, `Family=ipv4` and `Group=1:3 20:1` — about 100 false positives in the corpus, all on values straight out of systemd's own test-network fixtures. Two things learned from checking against that corpus, now folded into shared combinators: unsignedNumber() — systemd's safe_atou* helpers pass base 0 to strtoul, so a number may be written in hex or octal. IntegerTerminal only reads decimal, so `TypeOfService=0x08` (from systemd's 25-fibrule-uidrange.network) would have been flagged. Only the decimal branch carries the numeric bounds; the other two err towards accepting rather than flagging. INTERFACE_NAME / ALTERNATIVE_INTERFACE_NAME — ifname_valid_full(), including the rules that a name which is entirely digits is refused (it would look like an ifindex), as are `.`, `..`, `all` and `default`, and that the length cap is 15 or 127 depending on the caller's flags. [Address] Address= keeps its existing NetworkAddressOptionValue, which is deliberately stricter than in_addr_prefix_from_string_auto (it wants an IPv4 prefix of at least /8 and an explicit IPv6 prefix) and has tests asserting that; Peer= is the same table entry and now shares it. Re-ran every registered grammar over the 6,680-unit corpus: of 18,331 grammar-validated key occurrences these validators reject 28, and all 28 are correct — KDE's syntax-highlighting test input, systemd's own `hoge`/`foofoo`/`::1` negative cases, a fuzz corpus file, and one genuine bug in a shipped Debian unit (openqa-minion-restart.path sets PathChanged= to an unsubstituted relative path, which systemd rejects). Whole suite green under both engines. Co-Authored-By: Claude Opus 5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 64 ++++- .../optionvalues/NetworkAddressOptionValue.kt | 6 +- .../ai/AddressAndNextHopFlagOptionValues.kt | 76 ++++++ .../ai/ConfigParseMtuOptionValue.kt | 44 +++- ...ConfigParseUnitConditionPathOptionValue.kt | 4 +- .../ai/NetworkMiscOptionValues.kt | 95 +++++++ .../ai/RouteAddressNextHopOptionValues.kt | 96 +++++++ .../ai/RoutingPolicyRuleOptionValues.kt | 127 ++++++++++ .../ai/UnitPathAndUnitNameOptionValues.kt | 78 ++++++ .../optionvalues/grammar/Combinators.kt | 75 +++++- .../ai/NetworkSectionInspectionTest.kt | 238 ++++++++++++++++++ .../ai/UnitPathAndUnitNameInspectionTest.kt | 85 +++++++ 12 files changed, 960 insertions(+), 28 deletions(-) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/AddressAndNextHopFlagOptionValues.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/NetworkMiscOptionValues.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RouteAddressNextHopOptionValues.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/UnitPathAndUnitNameOptionValues.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkSectionInspectionTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/UnitPathAndUnitNameInspectionTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 39c5d706..de23179f 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt @@ -28,12 +28,14 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_ad_actor_sys_prio", "0") to ConfigParseAdActorSysPrioOptionValue() as OptionValueInformation, Validator("config_parse_ad_user_port_key", "0") to ConfigParseAdUserPortKeyOptionValue() as OptionValueInformation, Validator("config_parse_address_families", "0") to ConfigParseAddressFamiliesOptionValue() as OptionValueInformation, - Validator("config_parse_address_section", "ADDRESS_ADD_PREFIX_ROUTE") to ConfigParseAddressSectionOptionValue() as OptionValueInformation, - Validator("config_parse_address_section", "ADDRESS_AUTO_JOIN") to ConfigParseAddressSectionOptionValue() as OptionValueInformation, - Validator("config_parse_address_section", "ADDRESS_DAD") to ConfigParseAddressSectionOptionValue() as OptionValueInformation, - Validator("config_parse_address_section", "ADDRESS_HOME_ADDRESS") to ConfigParseAddressSectionOptionValue() as OptionValueInformation, - Validator("config_parse_address_section", "ADDRESS_MANAGE_TEMPORARY_ADDRESS") to ConfigParseAddressSectionOptionValue() as OptionValueInformation, - Validator("config_parse_address_section", "ADDRESS_PREFIX_ROUTE") to ConfigParseAddressSectionOptionValue() as OptionValueInformation, + // Each [Address] slot takes the grammar its own ConfigSectionParser entry calls for; they used to + // share the uint32 grammar written for ADDRESS_ROUTE_METRIC, which flagged every boolean value. + Validator("config_parse_address_section", "ADDRESS_ADD_PREFIX_ROUTE") to ConfigParseAddressSectionFlagOptionValue() as OptionValueInformation, + Validator("config_parse_address_section", "ADDRESS_AUTO_JOIN") to ConfigParseAddressSectionFlagOptionValue() as OptionValueInformation, + Validator("config_parse_address_section", "ADDRESS_HOME_ADDRESS") to ConfigParseAddressSectionFlagOptionValue() as OptionValueInformation, + Validator("config_parse_address_section", "ADDRESS_MANAGE_TEMPORARY_ADDRESS") to ConfigParseAddressSectionFlagOptionValue() as OptionValueInformation, + Validator("config_parse_address_section", "ADDRESS_PREFIX_ROUTE") to ConfigParseAddressSectionFlagOptionValue() as OptionValueInformation, + Validator("config_parse_address_section", "ADDRESS_DAD") to ConfigParseAddressSectionDadOptionValue() as OptionValueInformation, Validator("config_parse_address_section", "ADDRESS_ROUTE_METRIC") to ConfigParseAddressSectionOptionValue() as OptionValueInformation, Validator("config_parse_alternative_names_policy", "0") to ConfigParseAlternativeNamesPolicyOptionValue() as OptionValueInformation, Validator("config_parse_arp_ip_target_address", "0") to ConfigParseArpIpTargetAddressOptionValue() as OptionValueInformation, @@ -157,15 +159,15 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_managed_oom_rules", "1") to ConfigParseManagedOomRulesOptionValue() as OptionValueInformation, Validator("config_parse_mdi", "0") to ConfigParseMdiOptionValue() as OptionValueInformation, Validator("config_parse_pressure_watch", "0") to ConfigParseMemoryPressureWatchOptionValue() as OptionValueInformation, - Validator("config_parse_mtu", "AF_INET6") to ConfigParseMtuOptionValue() as OptionValueInformation, Validator("config_parse_multicast_router", "0") to ConfigParseMulticastRouterOptionValue() as OptionValueInformation, Validator("config_parse_ndisc_start_dhcp6_client", "0") to ConfigParseNdiscStartDhcp6ClientOptionValue() as OptionValueInformation, Validator("config_parse_netem_packet_limit", "QDISC_KIND_NETEM") to ConfigParseNetemPacketLimitOptionValue() as OptionValueInformation, + // Likewise for [NextHop]: only Blackhole= and OnLink= are booleans. Validator("config_parse_nexthop_section", "NEXTHOP_BLACKHOLE") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation, - Validator("config_parse_nexthop_section", "NEXTHOP_FAMILY") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation, - Validator("config_parse_nexthop_section", "NEXTHOP_GROUP") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation, - Validator("config_parse_nexthop_section", "NEXTHOP_ID") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation, Validator("config_parse_nexthop_section", "NEXTHOP_ONLINK") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation, + Validator("config_parse_nexthop_section", "NEXTHOP_ID") to ConfigParseNextHopIdOptionValue() as OptionValueInformation, + Validator("config_parse_nexthop_section", "NEXTHOP_FAMILY") to ConfigParseNextHopFamilyOptionValue() as OptionValueInformation, + Validator("config_parse_nexthop_section", "NEXTHOP_GROUP") to ConfigParseNextHopGroupOptionValue() as OptionValueInformation, Validator("config_parse_nsec", "0") to ConfigParseNsecOptionValue() as OptionValueInformation, Validator("config_parse_pass_environ", "0") to ConfigParsePassEnvironOptionValue() as OptionValueInformation, Validator("config_parse_permille", "0") to ConfigParsePermilleOptionValue() as OptionValueInformation, @@ -305,6 +307,48 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_tunnel_remote_address", "0") to ConfigParseTunnelRemoteAddressOptionValue() as OptionValueInformation, Validator("config_parse_address_generation_type", "0") to ConfigParseAddressGenerationTypeOptionValue() as OptionValueInformation, + // [RoutingPolicyRule] — one entry per ConfigSectionParser table slot (#509). + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_FROM") to ConfigParseRoutingPolicyRuleFromToOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_TO") to ConfigParseRoutingPolicyRuleFromToOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_IIF") to ConfigParseRoutingPolicyRuleInterfaceOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_OIF") to ConfigParseRoutingPolicyRuleInterfaceOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_PRIORITY") to ConfigParseRoutingPolicyRulePriorityOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_GOTO") to ConfigParseRoutingPolicyRuleGotoOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_TOS") to ConfigParseRoutingPolicyRuleTosOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_L3MDEV") to ConfigParseRoutingPolicyRuleBooleanOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_INVERT") to ConfigParseRoutingPolicyRuleBooleanOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_FAMILY") to ConfigParseRoutingPolicyRuleFamilyOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_ACTION") to ConfigParseRoutingPolicyRuleActionOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_SPORT") to ConfigParseRoutingPolicyRulePortRangeOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_DPORT") to ConfigParseRoutingPolicyRulePortRangeOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_SUPPRESS_PREFIXLEN") to ConfigParseRoutingPolicyRuleSuppressPrefixLengthOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_SUPPRESS_IFGROUP") to ConfigParseRoutingPolicyRuleSuppressInterfaceGroupOptionValue() as OptionValueInformation, + + // [Route], [Address] and [NextHop] table slots (#509). + Validator("config_parse_route_section", "ROUTE_DESTINATION") to ConfigParseRouteDestinationOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_PREFERRED_SOURCE") to ConfigParseRoutePreferredSourceOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_GATEWAY_ONLINK") to ConfigParseRouteGatewayOnlinkOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_SCOPE") to ConfigParseRouteScopeOptionValue() as OptionValueInformation, + Validator("config_parse_address_section", "ADDRESS_BROADCAST") to ConfigParseAddressSectionBroadcastOptionValue() as OptionValueInformation, + Validator("config_parse_address_section", "ADDRESS_PREFERRED_LIFETIME") to ConfigParseAddressSectionPreferredLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_nexthop_section", "NEXTHOP_GATEWAY") to ConfigParseNextHopGatewayOptionValue() as OptionValueInformation, + + // Assorted networkd / udev settings (#509). + Validator("config_parse_match_ifnames", "IFNAME_VALID_ALTERNATIVE") to ConfigParseMatchAlternativeIfnamesOptionValue() as OptionValueInformation, + Validator("config_parse_match_ifnames", "0") to ConfigParseMatchIfnamesOptionValue() as OptionValueInformation, + Validator("config_parse_bridge_vlan_id_range", "0") to ConfigParseBridgeVlanIdRangeOptionValue() as OptionValueInformation, + Validator("config_parse_tunnel_key", "0") to ConfigParseTunnelKeyOptionValue() as OptionValueInformation, + Validator("config_parse_prefix_lifetime", "0") to ConfigParsePrefixLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_mtu", "AF_UNSPEC") to ConfigParseMtuAnyOptionValue() as OptionValueInformation, + Validator("config_parse_mtu", "AF_INET") to ConfigParseMtuIpv4OptionValue() as OptionValueInformation, + Validator("config_parse_mtu", "AF_INET6") to ConfigParseMtuIpv6OptionValue() as OptionValueInformation, + + // Unit-file paths and unit names (#509). + Validator("config_parse_path_spec", "0") to ConfigParsePathSpecOptionValue() as OptionValueInformation, + Validator("config_parse_unit_path_strv_printf", "0") to ConfigParseUnitPathStrvPrintfOptionValue() as OptionValueInformation, + Validator("config_parse_socket_service", "0") to ConfigParseSocketServiceOptionValue() as OptionValueInformation, + Validator("config_parse_service_sockets", "0") to ConfigParseServiceSocketsOptionValue() as OptionValueInformation, + ) return allValidators diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt index c7bac63e..79b81ec9 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt @@ -12,8 +12,12 @@ class NetworkAddressOptionValue() : GrammarOptionValue("config_parse_address_sec val GRAMMAR = SequenceCombinator( IP_ADDR_AND_PREFIX_LENGTH, EOF()) + // Address= and Peer= are the same ConfigSectionParser entry (config_parse_address), so they get + // the same grammar. Note it is deliberately stricter than systemd's in_addr_prefix_from_string_auto, + // which would also allow an IPv4 prefix below /8 and an IPv6 address with no prefix at all. val validators = mapOf( - Validator("config_parse_address_section", "ADDRESS_ADDRESS") to NetworkAddressOptionValue() + Validator("config_parse_address_section", "ADDRESS_ADDRESS") to NetworkAddressOptionValue(), + Validator("config_parse_address_section", "ADDRESS_PEER") to NetworkAddressOptionValue() ) } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/AddressAndNextHopFlagOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/AddressAndNextHopFlagOptionValues.kt new file mode 100644 index 00000000..e7cc0edd --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/AddressAndNextHopFlagOptionValues.kt @@ -0,0 +1,76 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unsignedNumber + +/* + * The remaining `[Address]` and `[NextHop]` table slots (#509). + * + * These previously shared one grammar per section — a uint32 for every `[Address]` slot and a boolean + * for every `[NextHop]` slot — even though the ConfigSectionParser tables give each slot its own + * parser. That flagged legitimate values: `AddPrefixRoute=no`, `HomeAddress=yes`, + * `DuplicateAddressDetection=ipv4`, `[NextHop] Id=20` and `Family=ipv4` were all reported as invalid. + * Each slot now gets the grammar its own table entry calls for. + */ + +private const val ADDRESS = "config_parse_address_section" +private const val NEXTHOP = "config_parse_nexthop_section" + +/** + * `[Address] HomeAddress=`, `ManageTemporaryAddress=`, `PrefixRoute=`, `AddPrefixRoute=` and + * `AutoJoin=` — table entries config_parse_uint32_flag / config_parse_uint32_invert_flag. Both read + * the value with parse_boolean() and differ only in which IFA_F_* bit they set, and in which + * direction. + */ +class ConfigParseAddressSectionFlagOptionValue : SimpleGrammarOptionValues( + ADDRESS, SequenceCombinator(BOOLEAN, EOF()) +) + +/** + * `[Address] DuplicateAddressDetection=` — table entry config_parse_address_dad, resolved through + * duplicate_address_detection_address_family_table (src/network/networkd-util.c). + */ +class ConfigParseAddressSectionDadOptionValue : SimpleGrammarOptionValues( + ADDRESS, SequenceCombinator(FlexibleLiteralChoiceTerminal("none", "both", "ipv4", "ipv6"), EOF()) +) + +/** `[NextHop] Id=` — table entry config_parse_uint32. */ +class ConfigParseNextHopIdOptionValue : SimpleGrammarOptionValues( + NEXTHOP, SequenceCombinator(unsignedNumber(4_294_967_296L), EOF()) +) + +/** + * `[NextHop] Family=` — table entry config_parse_nexthop_family, resolved through + * nexthop_address_family_table, which unlike its siblings offers only the two concrete families. + */ +class ConfigParseNextHopFamilyOptionValue : SimpleGrammarOptionValues( + NEXTHOP, SequenceCombinator(FlexibleLiteralChoiceTerminal("ipv4", "ipv6"), EOF()) +) + +/** + * `[NextHop] Group=` — table entry config_parse_nexthop_group: a whitespace-separated list of + * `id[:weight]`, where the id is a uint32 and the weight, when present, is 1…256. + */ +class ConfigParseNextHopGroupOptionValue : SimpleGrammarOptionValues( + NEXTHOP, + SequenceCombinator( + GROUP_MEMBER, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), GROUP_MEMBER)), + EOF() + ) +) { + private companion object { + val GROUP_MEMBER = SequenceCombinator( + unsignedNumber(4_294_967_296L), + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal(":"), unsignedNumber(257, minInclusive = 1))) + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMtuOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMtuOptionValue.kt index 721c7408..d4d6966e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMtuOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMtuOptionValue.kt @@ -1,21 +1,45 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator /** - * Validator for Network.IPv6MTUBytes - * C Function: config_parse_mtu(AF_INET6) - * Used by Options: Network.IPv6MTUBytes - * - * Validates IPv6 MTU values. For IPv6, the minimum MTU is 1280 bytes (IPV6_MIN_MTU) - * and the maximum is UINT32_MAX (4294967295). + * Validator for `MTUBytes=` in `[Link]` (.network and .link) and `[NetDev]`, plus + * `[Network] IPv6MTUBytes=` and `[DHCPv4] RouteMTUBytes=`. + * + * C function: config_parse_mtu (src/shared/conf-parser.c) → parse_mtu (src/basic/parse-util.c), which + * runs parse_size() with base 1024 and then range-checks the result. The lower bound comes from the + * ltype: AF_INET6 requires IPV6_MIN_MTU (1280) and AF_INET requires IPV4_MIN_MTU (68); AF_UNSPEC has + * no minimum. The upper bound is UINT32_MAX throughout. + * + * parse_size() also accepts IEC suffixes and decimal fractions, which no [IntegerTerminal] can + * range-check. The suffixed spelling is therefore accepted without a bound; the plain integer form — + * what every real-world MTU uses — still gets one. */ -class ConfigParseMtuOptionValue : SimpleGrammarOptionValues( +open class ConfigParseMtuOptionValue(minimum: Long) : SimpleGrammarOptionValues( "config_parse_mtu", SequenceCombinator( - IntegerTerminal(1280, 4294967296), + AlternativeCombinator( + SequenceCombinator( + RegexTerminal("""[0-9]+(?:\.[0-9]+)?""", """[0-9]+(?:\.[0-9]+)?"""), + LiteralChoiceTerminal("E", "P", "T", "G", "M", "K", "B"), + ), + IntegerTerminal(minimum, 4_294_967_296L), + ), EOF() ) ) + +/** `MTUBytes=` with no family-specific minimum (ltype AF_UNSPEC). */ +class ConfigParseMtuAnyOptionValue : ConfigParseMtuOptionValue(0) + +/** `[DHCPv4] RouteMTUBytes=` (ltype AF_INET): at least IPV4_MIN_MTU. */ +class ConfigParseMtuIpv4OptionValue : ConfigParseMtuOptionValue(68) + +/** `[Network] IPv6MTUBytes=` (ltype AF_INET6): at least IPV6_MIN_MTU. */ +class ConfigParseMtuIpv6OptionValue : ConfigParseMtuOptionValue(1280) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt index 8c12f578..3956f77d 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt @@ -1,7 +1,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CONDITION_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ABSOLUTE_PATH_WITH_SPECIFIERS import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionPath /** @@ -21,5 +21,5 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram */ class ConfigParseUnitConditionPathOptionValue : SimpleGrammarOptionValues( "config_parse_unit_condition_path", - conditionPath(CONDITION_PATH) + conditionPath(ABSOLUTE_PATH_WITH_SPECIFIERS) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/NetworkMiscOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/NetworkMiscOptionValues.kt new file mode 100644 index 00000000..76897ed3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/NetworkMiscOptionValues.kt @@ -0,0 +1,95 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.TIME_VALUE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * Assorted networkd / udev validators (#509). + */ + +/** + * Validator for `[Match] Name=` in a .network file, and `[Match] Property=`-adjacent name lists. + * + * C function: config_parse_match_ifnames (src/shared/net-condition.c) with ltype + * IFNAME_VALID_ALTERNATIVE — an optional leading `!` that negates the whole list, then a + * whitespace-separated list of names, each checked by ifname_valid_full(). + * + * Glob metacharacters need no special handling: `*` and `?` are ordinary valid interface-name + * characters, so `ve-*` passes the same check a literal name does. + */ +class ConfigParseMatchAlternativeIfnamesOptionValue : SimpleGrammarOptionValues( + "config_parse_match_ifnames", ifnameList(ALTERNATIVE_INTERFACE_NAME) +) + +/** + * Validator for `[Match] OriginalName=` in a .link file. + * + * The same parser with ltype 0, which drops the IFNAME_VALID_ALTERNATIVE flag and so caps each name + * at IFNAMSIZ - 1 = 15 characters instead of 127. + */ +class ConfigParseMatchIfnamesOptionValue : SimpleGrammarOptionValues( + "config_parse_match_ifnames", ifnameList(INTERFACE_NAME) +) + +private fun ifnameList(name: Combinator): Combinator = SequenceCombinator( + ZeroOrOne(LiteralChoiceTerminal("!")), + name, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), name)), + EOF() +) + +/** + * Validator for `[BridgeVLAN] VLAN=`, `EgressUntagged=` and `PVID=`. + * + * C function: config_parse_bridge_vlan_id_range (src/network/networkd-bridge-vlan.c) → + * parse_vid_range (src/shared/vlan-util.c): a single id or a `low-high` range, each at most + * VLANID_MAX = 4094. + * + * systemd also rejects a range whose low end exceeds its high end; that compares the parsed numbers + * rather than constraining the shape, so `1000-10` is accepted here. + */ +class ConfigParseBridgeVlanIdRangeOptionValue : SimpleGrammarOptionValues( + "config_parse_bridge_vlan_id_range", + SequenceCombinator( + IntegerTerminal(0, 4095), + ZeroOrOne(SequenceCombinator(HYPHEN, IntegerTerminal(0, 4095))), + EOF() + ) +) + +/** + * Validator for `[Tunnel] Key=`, `InputKey=` and `OutputKey=` (.netdev). + * + * C function: config_parse_tunnel_key (src/network/netdev/tunnel.c). The value is first tried as an + * IPv4 address, whose 32 bits are then used as the key; failing that, as a plain uint32. IPv4 comes + * first here for the same reason it does in systemd — `1.2.3.4` is a key spelled in dotted form, not + * a malformed number. + */ +class ConfigParseTunnelKeyOptionValue : SimpleGrammarOptionValues( + "config_parse_tunnel_key", + SequenceCombinator(AlternativeCombinator(IPV4_ADDR, IntegerTerminal(0L, 4_294_967_296L)), EOF()) +) + +/** + * Validator for `[IPv6Prefix] PreferredLifetimeSec=` and `ValidLifetimeSec=`. + * + * C function: config_parse_prefix_lifetime (src/network/networkd-radv.c) → parse_sec, with the result + * additionally required to be under UINT32_MAX seconds. The magnitude check operates on the parsed + * duration rather than its spelling, so it isn't expressible here. + */ +class ConfigParsePrefixLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_prefix_lifetime", SequenceCombinator(TIME_VALUE, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RouteAddressNextHopOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RouteAddressNextHopOptionValues.kt new file mode 100644 index 00000000..466a31f9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RouteAddressNextHopOptionValues.kt @@ -0,0 +1,96 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_ANY_PREFIX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * Validators for the `[Route]`, `[Address]` and `[NextHop]` sections of a .network file (#509). + * + * Like config_parse_routing_policy_rule, each of config_parse_route_section, + * config_parse_address_section and config_parse_nexthop_section dispatches on its ltype through a + * ConfigSectionParser table; the classes below mirror the table entries one by one. + */ + +private const val ROUTE = "config_parse_route_section" +private const val ADDRESS = "config_parse_address_section" +private const val NEXTHOP = "config_parse_nexthop_section" + +/** + * `[Route] Destination=` / `Source=` — table entry config_parse_route_destination, i.e. + * in_addr_prefix_from_string_auto: an address of either family with an optional prefix length. + */ +class ConfigParseRouteDestinationOptionValue : SimpleGrammarOptionValues( + ROUTE, SequenceCombinator(IP_ADDR_AND_ANY_PREFIX, EOF()) +) + +/** + * `[Route] PreferredSource=` — table entry config_parse_preferred_src. + * + * It first tries parse_boolean() and accepts the value only when it parses as *false*, which is how + * you forbid a preferred source from a DHCP lease. A true-ish spelling falls through to + * in_addr_from_string_auto() and is rejected there, so only the negative booleans are listed. + */ +class ConfigParseRoutePreferredSourceOptionValue : SimpleGrammarOptionValues( + ROUTE, + SequenceCombinator( + AlternativeCombinator( + IP_ADDR, + FlexibleLiteralChoiceTerminal("0", "no", "n", "false", "f", "off"), + ), + EOF() + ) +) + +/** `[Route] GatewayOnLink=` — table entry config_parse_tristate, i.e. parse_boolean() or empty. */ +class ConfigParseRouteGatewayOnlinkOptionValue : SimpleGrammarOptionValues( + ROUTE, SequenceCombinator(BOOLEAN, EOF()) +) + +/** + * `[Route] Scope=` — table entry config_parse_route_scope, resolved through route_scope_table. That + * table is declared with DEFINE_STRING_TABLE_LOOKUP_WITH_FALLBACK(..., UINT8_MAX), so a plain number + * up to 255 is accepted for scopes the table has no name for. + */ +class ConfigParseRouteScopeOptionValue : SimpleGrammarOptionValues( + ROUTE, + SequenceCombinator( + AlternativeCombinator( + IntegerTerminal(0, 256), + FlexibleLiteralChoiceTerminal("global", "site", "link", "host", "nowhere"), + ), + EOF() + ) +) + +/** + * `[Address] Broadcast=` — table entry config_parse_broadcast: a boolean (asking systemd to derive + * the broadcast address from Address=, or not to set one), or an explicit IPv4 address. IPv6 is not + * accepted — the delegate is config_parse_in_addr_non_null with ltype AF_INET. + */ +class ConfigParseAddressSectionBroadcastOptionValue : SimpleGrammarOptionValues( + ADDRESS, SequenceCombinator(AlternativeCombinator(IPV4_ADDR, BOOLEAN), EOF()) +) + +/** + * `[Address] PreferredLifetime=` — table entry config_parse_address_lifetime, whose own comment reads + * "We accept only 'forever', 'infinity', empty, or '0'". It is not a general time span. + */ +class ConfigParseAddressSectionPreferredLifetimeOptionValue : SimpleGrammarOptionValues( + ADDRESS, SequenceCombinator(FlexibleLiteralChoiceTerminal("forever", "infinity", "0"), EOF()) +) + +/** + * `[NextHop] Gateway=` — table entry config_parse_in_addr_data, i.e. in_addr_from_string_auto: a bare + * address of either family, with no prefix length. + */ +class ConfigParseNextHopGatewayOptionValue : SimpleGrammarOptionValues( + NEXTHOP, SequenceCombinator(IP_ADDR, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt new file mode 100644 index 00000000..6159f948 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt @@ -0,0 +1,127 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_ANY_PREFIX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unsignedNumber +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * Validators for the `[RoutingPolicyRule]` section of a .network file (#509). + * + * `config_parse_routing_policy_rule` (src/network/networkd-routing-policy-rule.c) is a dispatcher: + * its ltype selects an entry in a ConfigSectionParser table, and each entry names the parser that + * actually reads the value. The classes below mirror that table entry by entry, so each one is + * registered under the matching ROUTING_POLICY_RULE_* ltype. + * + * Not covered here: ROUTING_POLICY_RULE_TABLE (route table names are user-defined via RouteTable= in + * networkd.conf, so there is no closed set to check), ROUTING_POLICY_RULE_FWMARK and + * ROUTING_POLICY_RULE_IP_PROTOCOL. + */ + +private const val RULE = "config_parse_routing_policy_rule" + +/** + * A `n` or `n-m` range — systemd's parse_range(), which reads each bound with safe_atou() and so + * accepts the same decimal / hexadecimal / octal spellings [unsignedNumber] does. + */ +private fun range(minInclusive: Long, maxExclusive: Long): Combinator { + val bound = unsignedNumber(maxExclusive, minInclusive) + return SequenceCombinator(bound, ZeroOrOne(SequenceCombinator(HYPHEN, bound)), EOF()) +} + +/** + * `From=` / `To=` — table entry config_parse_in_addr_prefix, i.e. + * in_addr_prefix_from_string_auto_full with PREFIXLEN_FULL: an address of either family with an + * optional prefix length, which defaults to the family's full width when omitted. + */ +class ConfigParseRoutingPolicyRuleFromToOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(IP_ADDR_AND_ANY_PREFIX, EOF()) +) + +/** + * `IncomingInterface=` / `OutgoingInterface=` — table entry config_parse_ifname, i.e. + * ifname_valid() with no flags. + */ +class ConfigParseRoutingPolicyRuleInterfaceOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(INTERFACE_NAME, EOF()) +) + +/** `Priority=` — table entry config_parse_routing_policy_rule_priority: safe_atou32. */ +class ConfigParseRoutingPolicyRulePriorityOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(unsignedNumber(4_294_967_296L), EOF()) +) + +/** + * `GoTo=` — table entry config_parse_routing_policy_rule_goto: safe_atou32, and then rejected unless + * it is greater than zero. + */ +class ConfigParseRoutingPolicyRuleGotoOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(unsignedNumber(4_294_967_296L, minInclusive = 1), EOF()) +) + +/** `TypeOfService=` — table entry config_parse_uint8. */ +class ConfigParseRoutingPolicyRuleTosOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(unsignedNumber(256), EOF()) +) + +/** + * `L3MasterDevice=` — table entry config_parse_bool — and `Invert=`, whose entry is + * config_parse_uint32_flag: both read the value with parse_boolean() and only differ in where the + * result is stored. + */ +class ConfigParseRoutingPolicyRuleBooleanOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(BOOLEAN, EOF()) +) + +/** + * `Family=` — table entry config_parse_routing_policy_rule_family, which resolves the value through + * routing_policy_rule_address_family_table (src/network/networkd-util.c). Note this table spells + * "both" where the plain address_family_table would say "yes". + */ +class ConfigParseRoutingPolicyRuleFamilyOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(FlexibleLiteralChoiceTerminal("both", "ipv4", "ipv6"), EOF()) +) + +/** + * `Type=` — table entry config_parse_routing_policy_rule_action, resolved through fr_act_type_table. + */ +class ConfigParseRoutingPolicyRuleActionOptionValue : SimpleGrammarOptionValues( + RULE, + SequenceCombinator( + FlexibleLiteralChoiceTerminal("table", "goto", "nop", "blackhole", "unreachable", "prohibit"), + EOF() + ) +) + +/** + * `SourcePort=` / `DestinationPort=` — table entry config_parse_routing_policy_rule_port_range, i.e. + * parse_ip_port_range with allow_zero = false: a port or a `low-high` range, each 1…65535. + * + * systemd additionally rejects a range whose high end is below its low end. That compares the two + * parsed numbers rather than constraining the value's shape, so it isn't expressible here and + * `100-50` is accepted by this grammar. + */ +class ConfigParseRoutingPolicyRulePortRangeOptionValue : SimpleGrammarOptionValues( + RULE, range(1, 65536) +) + +/** + * `SuppressPrefixLength=` — table entry config_parse_routing_policy_rule_suppress with ltype 128, + * which is the inclusive upper bound safe_atoi32's result is checked against. + */ +class ConfigParseRoutingPolicyRuleSuppressPrefixLengthOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(unsignedNumber(129), EOF()) +) + +/** `SuppressInterfaceGroup=` — the same parser with ltype INT32_MAX. */ +class ConfigParseRoutingPolicyRuleSuppressInterfaceGroupOptionValue : SimpleGrammarOptionValues( + RULE, SequenceCombinator(unsignedNumber(2_147_483_648L), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/UnitPathAndUnitNameOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/UnitPathAndUnitNameOptionValues.kt new file mode 100644 index 00000000..b3cbfefa --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/UnitPathAndUnitNameOptionValues.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ABSOLUTE_PATH_WITH_SPECIFIERS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * Validators for unit-file settings that take absolute paths or unit names (#509). + */ + +/** + * Validator for the `[Path]` watch settings: `PathExists=`, `PathExistsGlob=`, `PathChanged=`, + * `PathModified=` and `DirectoryNotEmpty=`. + * + * C function: config_parse_path_spec (src/core/load-fragment.c) — unit_path_printf() followed by + * path_simplify_and_warn(..., PATH_CHECK_ABSOLUTE). One path per assignment; the value is never split + * on whitespace, so two paths on one line is a single path containing a space. + */ +class ConfigParsePathSpecOptionValue : SimpleGrammarOptionValues( + "config_parse_path_spec", + SequenceCombinator(ABSOLUTE_PATH_WITH_SPECIFIERS, EOF()) +) + +/** + * Validator for `[Socket] Symlinks=`. + * + * C function: config_parse_unit_path_strv_printf (src/core/load-fragment.c) — a whitespace-separated + * list, each entry run through unit_path_printf() and then required to be absolute. + */ +class ConfigParseUnitPathStrvPrintfOptionValue : SimpleGrammarOptionValues( + "config_parse_unit_path_strv_printf", + SequenceCombinator( + ABSOLUTE_PATH_WITH_SPECIFIERS, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), ABSOLUTE_PATH_WITH_SPECIFIERS)), + EOF() + ) +) + +/** + * Validator for `[Socket] Service=`. + * + * C function: config_parse_socket_service (src/core/load-fragment.c). After unit_name_printf() the + * only shape check systemd makes is `endswith(p, ".service")` — everything else is deferred to + * manager_load_unit(), which needs a running manager. So that suffix, and the character set a unit + * name is allowed to draw from, is exactly what this grammar checks. + */ +class ConfigParseSocketServiceOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_service", + SequenceCombinator(unitName("service"), EOF()) +) + +/** + * Validator for `[Service] Sockets=`. + * + * C function: config_parse_service_sockets (src/core/load-fragment.c) — a whitespace-separated list + * where each entry must end in `.socket`. Entries that don't are logged and skipped individually. + */ +class ConfigParseServiceSocketsOptionValue : SimpleGrammarOptionValues( + "config_parse_service_sockets", + SequenceCombinator( + unitName("socket"), + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), unitName("socket"))), + EOF() + ) +) + +/** + * A unit name with the given suffix. The character set is systemd's VALID_CHARS for unit names + * (alphanumerics plus `:-_.\`) widened by `@`, which separates a template from its instance, and `%`, + * because unit_name_printf() expands specifiers before the name is validated. + */ +private fun unitName(suffix: String): Combinator = + RegexTerminal("""\S+""", """[A-Za-z0-9:_.\\@%-]+\.$suffix""") diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt index 59229ae3..97df8009 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt @@ -201,13 +201,78 @@ fun conditionString(parameter: Combinator): Combinator { } /** - * The parameter of a path-valued condition: systemd resolves specifiers with unit_path_printf() and - * then requires the result to be absolute (path_simplify_and_warn with PATH_CHECK_ABSOLUTE). We can't - * expand specifiers, so a value may legitimately begin with one (`%t/foo`, `%h/.cache`) instead of a - * slash. Spaces are only allowed when backslash-escaped, matching the rest of the plugin's paths. + * A path systemd requires to be absolute after specifier expansion — i.e. anything it feeds through + * unit_path_printf() and then path_simplify_and_warn(..., PATH_CHECK_ABSOLUTE). We can't expand + * specifiers, so a value may legitimately begin with one (`%t/foo`, `%h/.cache`) instead of a slash. + * Spaces are only allowed when backslash-escaped, matching the rest of the plugin's paths. */ -val CONDITION_PATH = RegexTerminal( +val ABSOLUTE_PATH_WITH_SPECIFIERS = RegexTerminal( """\S(?:[^\s\\]|\\[\s\S])*""", """(?:/|%\S)(?:[^\s\\]|\\[\s\S])*""" ) + +// --------------------------------------------------------------------------------------------------- +// IP addresses and prefixes, as parsed by in_addr_from_string_auto / in_addr_prefix_from_string_auto +// (systemd src/basic/in-addr-util.c). IPv6 is tried first throughout: the keyword and IPv4 branches +// can consume a leading fragment of an IPv6 literal, and the classic matcher never backtracks into a +// sibling alternative once one of them has matched. + +/** A bare address literal of either family — in_addr_from_string_auto. */ +val IP_ADDR = AlternativeCombinator(IPV6_ADDR, IPV4_ADDR) + +// in_addr_prefix_from_string_auto accepts the full prefix-length range the family allows, and treats +// the length as optional (defaulting to the full width). This is deliberately wider than +// IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH / IPV6_ADDR_AND_OPTIONAL_PREFIX_LENGTH above, which model the +// narrower ranges systemd enforces for InAddrPrefixes-style settings. +val IPV4_ADDR_AND_ANY_PREFIX = SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, IntegerTerminal(0, 33)))) +val IPV6_ADDR_AND_ANY_PREFIX = SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, IntegerTerminal(0, 129)))) +val IP_ADDR_AND_ANY_PREFIX = AlternativeCombinator(IPV6_ADDR_AND_ANY_PREFIX, IPV4_ADDR_AND_ANY_PREFIX) + + +// --------------------------------------------------------------------------------------------------- +// Network interface names — ifname_valid_full (systemd src/basic/socket-util.c). Valid characters are +// printable ASCII (33…126) except `:`, `/` and `%`. A name that is entirely digits is refused so it +// can't be confused with an interface index, as are `.`, `..`, and — because they collide with the +// /proc/sys/net/*/conf/ directories — `all` and `default`. The length limit differs by call site. +private const val IFNAME_CHAR = """[\x21-\x7E&&[^:/%]]""" + +// The lookahead rejects the reserved words only when they make up the whole name: `(?!IFNAME_CHAR)` +// after each one means "and the name ends here", so `1a`, `alliance` and `defaults` still pass. +private fun ifnameSemantic(maxLength: Int) = + """(?!(?:[0-9]+|\.\.?|all|default)(?!$IFNAME_CHAR))$IFNAME_CHAR{1,$maxLength}""" + +/** ifname_valid_full with no flags: at most IFNAMSIZ - 1 = 15 characters. */ +val INTERFACE_NAME = RegexTerminal("""\S+""", ifnameSemantic(15)) + +/** ifname_valid_full with IFNAME_VALID_ALTERNATIVE: at most ALTIFNAMSIZ - 1 = 127 characters. */ +val ALTERNATIVE_INTERFACE_NAME = RegexTerminal("""\S+""", ifnameSemantic(127)) + + +/** + * An unsigned number in `[0, maxExclusive)`, spelled any of the ways systemd's `safe_atou*` family + * reads one. + * + * Those helpers pass base 0 down to strtoul(), so besides decimal they accept `0x` hexadecimal and + * leading-zero octal — systemd's own test data writes `TypeOfService=0x08`. An [IntegerTerminal] + * only understands decimal, so the other two bases are added alongside it. + * + * Only the decimal branch carries the numeric bounds. The hexadecimal branch is bounded by digit + * count instead, which is exact for the power-of-two limits these settings use, and the octal branch + * is not bounded at all. Both therefore err towards accepting an out-of-range value rather than + * flagging a legal one, on spellings essentially nobody writes. + */ +fun unsignedNumber(maxExclusive: Long, minInclusive: Long = 0L): Combinator { + val hexDigits = maxOf(1, ((maxExclusive - 1).toString(16).length)) + return AlternativeCombinator( + // Hex first: on "0x08" a decimal terminal would happily match just the leading "0" and strand the rest. + SequenceCombinator( + LiteralChoiceTerminal("0x", "0X"), + RegexTerminal("""[0-9a-fA-F]+""", """[0-9a-fA-F]{1,$hexDigits}""") + ), + // Octal, likewise before decimal so "0377" isn't read as three hundred and seventy-seven. + RegexTerminal("""0[0-7]+""", """0[0-7]+"""), + IntegerTerminal(minInclusive, maxExclusive), + ) +} + diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkSectionInspectionTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkSectionInspectionTest.kt new file mode 100644 index 00000000..9c78a270 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkSectionInspectionTest.kt @@ -0,0 +1,238 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** + * Tests for the `[RoutingPolicyRule]`, `[Route]`, `[Address]` and `[NextHop]` validators added in + * #509, plus the assorted networkd settings that came with them. + * + * The accepted values are drawn from systemd's own test-network fixtures where possible, and the + * rejected ones from what its parsers actually refuse. + */ +class NetworkSectionInspectionTest : AbstractUnitFileTest() { + + private fun highlights(fileName: String, text: String): Int { + setupFileInEditor(fileName, text) + enableInspection(InvalidValueInspection::class.java) + return myFixture.doHighlighting().size + } + + private fun assertAccepted(fileName: String, vararg lines: String) = + assertEquals(lines.joinToString(), 0, highlights(fileName, lines.joinToString("\n") + "\n")) + + private fun assertRejected(fileName: String, text: String) = + assertTrue(text, highlights(fileName, text) >= 1) + + // ------------------------------------------------------------------ [RoutingPolicyRule] + + @Test + fun testRuleAddressesAndInterfaces() { + assertAccepted( + "f.network", + "[RoutingPolicyRule]", + "From=192.168.100.18", + "From=0.0.0.0/8", + "To=2000:f00::227", + "To=::/0", + "IncomingInterface=test1", + "OutgoingInterface=dummy98", + ) + assertRejected("f.network", "[RoutingPolicyRule]\nFrom=192.168.0.300\n") + assertRejected("f.network", "[RoutingPolicyRule]\nTo=192.168.0.1/33\n") + // ifname_valid() refuses a name that is entirely digits, so it can't be mistaken for an ifindex. + assertRejected("f.network", "[RoutingPolicyRule]\nIncomingInterface=12345\n") + // ...and refuses "all"/"default", which collide with /proc/sys/net/*/conf/. + assertRejected("f.network", "[RoutingPolicyRule]\nIncomingInterface=default\n") + // IFNAMSIZ - 1 is 15 characters. + assertRejected("f.network", "[RoutingPolicyRule]\nIncomingInterface=abcdefghijklmnopq\n") + } + + @Test + fun testRuleNumbersAcceptEveryBaseSafeAtouReads() { + assertAccepted( + "f.network", + "[RoutingPolicyRule]", + "Priority=111", + "Priority=4294967295", + // safe_atou* pass base 0 to strtoul, so hex and octal are legal; this spelling comes straight + // out of systemd's 25-fibrule-uidrange.network. + "TypeOfService=0x08", + "TypeOfService=255", + "SuppressPrefixLength=128", + "SuppressInterfaceGroup=42", + "GoTo=111", + ) + assertRejected("f.network", "[RoutingPolicyRule]\nPriority=4294967296\n") + assertRejected("f.network", "[RoutingPolicyRule]\nTypeOfService=256\n") + assertRejected("f.network", "[RoutingPolicyRule]\nSuppressPrefixLength=129\n") + assertRejected("f.network", "[RoutingPolicyRule]\nGoTo=0\n") + assertRejected("f.network", "[RoutingPolicyRule]\nPriority=abc\n") + } + + @Test + fun testRuleEnumsAndPorts() { + assertAccepted( + "f.network", + "[RoutingPolicyRule]", + "Family=both", "Family=ipv4", "Family=ipv6", + "Type=blackhole", "Type=unreachable", "Type=prohibit", "Type=goto", "Type=nop", "Type=table", + "SourcePort=1123", "DestinationPort=3456-3458", + "Invert=yes", "L3MasterDevice=no", + ) + // The rule table spells the any-family value "both", not "yes". + assertRejected("f.network", "[RoutingPolicyRule]\nFamily=yes\n") + assertRejected("f.network", "[RoutingPolicyRule]\nType=bogus\n") + // parse_ip_port_range is called with allow_zero = false. + assertRejected("f.network", "[RoutingPolicyRule]\nSourcePort=0\n") + assertRejected("f.network", "[RoutingPolicyRule]\nDestinationPort=70000\n") + } + + // ------------------------------------------------------------------ [Route] + + @Test + fun testRouteSettings() { + assertAccepted( + "f.network", + "[Route]", + "Destination=149.10.124.64", + "Destination=2001:1234:5:8fff:ff:ff:ff:ff/128", + "Source=192.168.1.1", + "PreferredSource=10.10.10.1", + "PreferredSource=2001:1234:56:8f63::2", + // config_parse_preferred_src accepts a boolean only when it parses as false. + "PreferredSource=no", + "GatewayOnLink=yes", + "Scope=link", "Scope=global", "Scope=host", "Scope=site", "Scope=nowhere", "Scope=200", + ) + assertRejected("f.network", "[Route]\nDestination=nonsense\n") + assertRejected("f.network", "[Route]\nPreferredSource=yes\n") + assertRejected("f.network", "[Route]\nScope=256\n") + assertRejected("f.network", "[Route]\nScope=bogus\n") + } + + // ------------------------------------------------------------------ [Address] + + @Test + fun testAddressSettings() { + assertAccepted( + "f.network", + "[Address]", + "Peer=192.168.30.1/32", + "Peer=2001:db8:0:f103::101/128", + "Broadcast=192.168.1.255", + "Broadcast=yes", + "PreferredLifetime=forever", + "PreferredLifetime=infinity", + "PreferredLifetime=0", + ) + assertRejected("f.network", "[Address]\nPeer=hoge\n") + // config_parse_broadcast delegates with ltype AF_INET, so an IPv6 literal is refused. + assertRejected("f.network", "[Address]\nBroadcast=::1\n") + // Not a general time span — the parser only takes forever/infinity/0. + assertRejected("f.network", "[Address]\nPreferredLifetime=10s\n") + } + + @Test + fun testAddressFlagsAreBooleansNotNumbers() { + // Regression: these five shared the uint32 grammar written for RouteMetric=, so every real + // value was flagged. + assertAccepted( + "f.network", + "[Address]", + "AddPrefixRoute=no", + "HomeAddress=yes", + "ManageTemporaryAddress=yes", + "PrefixRoute=false", + "AutoJoin=yes", + "DuplicateAddressDetection=ipv4", + "DuplicateAddressDetection=both", + "DuplicateAddressDetection=none", + "RouteMetric=128", + ) + assertRejected("f.network", "[Address]\nAddPrefixRoute=bogus\n") + assertRejected("f.network", "[Address]\nDuplicateAddressDetection=yes\n") + assertRejected("f.network", "[Address]\nRouteMetric=hoge\n") + } + + // ------------------------------------------------------------------ [NextHop] + + @Test + fun testNextHopSettings() { + // Regression: Id=, Family= and Group= shared the boolean grammar written for OnLink=. + assertAccepted( + "f.network", + "[NextHop]", + "Id=20", + "Gateway=192.168.5.1", + "Gateway=2001:1234:5:8f63::2", + "Family=ipv4", + "Family=ipv6", + "Group=1:3 20:1", + "Group=5", + "OnLink=yes", + "Blackhole=no", + ) + assertRejected("f.network", "[NextHop]\nId=nope\n") + // nexthop_address_family_table has no "both". + assertRejected("f.network", "[NextHop]\nFamily=both\n") + assertRejected("f.network", "[NextHop]\nGateway=192.168.5.1/24\n") + // A group weight must be 1…256. + assertRejected("f.network", "[NextHop]\nGroup=1:0\n") + assertRejected("f.network", "[NextHop]\nGroup=1:257\n") + } + + // ------------------------------------------------------------------ assorted networkd settings + + @Test + fun testMatchInterfaceNames() { + assertAccepted( + "f.network", + "[Match]", + "Name=dummy98", + "Name=veth-peer host0", + "Name=!loopback", + "Name=ve-* ns-*", + ) + assertAccepted("f.link", "[Match]", "OriginalName=*", "OriginalName=test1") + assertRejected("f.network", "[Match]\nName=eth:0\n") + assertRejected("f.network", "[Match]\nName=eth/0\n") + assertRejected("f.network", "[Match]\nName=99\n") + // .link OriginalName= drops IFNAME_VALID_ALTERNATIVE, so 15 characters is the cap there. + assertAccepted("f.network", "[Match]", "Name=abcdefghijklmnopqrstuvwxyz") + assertRejected("f.link", "[Match]\nOriginalName=abcdefghijklmnopqrstuvwxyz\n") + } + + @Test + fun testVlanRangesTunnelKeysAndLifetimes() { + assertAccepted( + "f.network", + "[BridgeVLAN]", + "VLAN=1018-1023", + "VLAN=100", + "EgressUntagged=1200-1210", + "PVID=560", + ) + assertRejected("f.network", "[BridgeVLAN]\nVLAN=4095\n") + assertRejected("f.network", "[BridgeVLAN]\nVLAN=1-\n") + + assertAccepted("f.netdev", "[Tunnel]", "Key=101", "InputKey=1.2.3.103", "OutputKey=4294967295") + assertRejected("f.netdev", "[Tunnel]\nKey=4294967296\n") + assertRejected("f.netdev", "[Tunnel]\nKey=nope\n") + + assertAccepted("f.network", "[IPv6Prefix]", "PreferredLifetimeSec=1000s", "ValidLifetimeSec=2100s") + assertRejected("f.network", "[IPv6Prefix]\nValidLifetimeSec=soon\n") + } + + @Test + fun testMtuBounds() { + assertAccepted("f.netdev", "[NetDev]", "MTUBytes=1480", "MTUBytes=9000", "MTUBytes=16") + assertAccepted("f.network", "[Network]", "IPv6MTUBytes=1500") + assertAccepted("f.network", "[DHCPv4]", "RouteMTUBytes=1500") + // IPV6_MIN_MTU is 1280 and IPV4_MIN_MTU is 68; AF_UNSPEC has no minimum. + assertRejected("f.network", "[Network]\nIPv6MTUBytes=1000\n") + assertRejected("f.network", "[DHCPv4]\nRouteMTUBytes=32\n") + assertRejected("f.netdev", "[NetDev]\nMTUBytes=huge\n") + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/UnitPathAndUnitNameInspectionTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/UnitPathAndUnitNameInspectionTest.kt new file mode 100644 index 00000000..d712bc86 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/UnitPathAndUnitNameInspectionTest.kt @@ -0,0 +1,85 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** + * Tests for the unit-file path and unit-name validators added in #509: `[Path]` watch settings, + * `[Socket] Symlinks=`, `[Socket] Service=` and `[Service] Sockets=`. + */ +class UnitPathAndUnitNameInspectionTest : AbstractUnitFileTest() { + + private fun highlights(fileName: String, text: String): Int { + setupFileInEditor(fileName, text) + enableInspection(InvalidValueInspection::class.java) + return myFixture.doHighlighting().size + } + + private fun assertAccepted(fileName: String, vararg lines: String) = + assertEquals(lines.joinToString(), 0, highlights(fileName, lines.joinToString("\n") + "\n")) + + private fun assertRejected(fileName: String, text: String) = + assertTrue(text, highlights(fileName, text) >= 1) + + @Test + fun testPathSpecTakesOneAbsolutePath() { + assertAccepted( + "f.path", + "[Path]", + "PathExists=/run/systemd/ask-password", + "PathExistsGlob=/tmp/test63-glob*", + "PathChanged=/run/lvm/lvm-devices-import", + "PathModified=/tmp/test-path_unit", + "DirectoryNotEmpty=/tmp/test-path_makedirectory/", + "PathExists=%t/spool", + ) + // path_simplify_and_warn(..., PATH_CHECK_ABSOLUTE): a relative path is refused. openqa's shipped + // openqa-minion-restart.path gets this wrong with an unsubstituted "installvendorlib/..." value. + assertRejected("f.path", "[Path]\nPathChanged=installvendorlib/Minion/pg.sql\n") + // One path per assignment — the value is never split on whitespace. + assertRejected("f.path", "[Path]\nPathExists=/tmp/a /tmp/b\n") + } + + @Test + fun testSocketSymlinksTakesAListOfAbsolutePaths() { + assertAccepted( + "f.socket", + "[Socket]", + "Symlinks=/run/varlink/registry/io.systemd.Resolve", + "Symlinks=/run/systemd/userdb/io.systemd.NamespaceResource /run/varlink/registry", + "Symlinks=%t/varlink/registry/io.systemd.Import", + ) + assertRejected("f.socket", "[Socket]\nSymlinks=a b c d e\n") + assertRejected("f.socket", "[Socket]\nSymlinks=/run/ok relative/no\n") + } + + @Test + fun testSocketServiceMustNameAService() { + assertAccepted( + "f.socket", + "[Socket]", + "Service=gpg-agent.service", + "Service=systemd-journald@%i.service", + ) + // config_parse_socket_service's only shape check is endswith(".service"). + assertRejected("f.socket", "[Socket]\nService=some.socket\n") + assertRejected("f.socket", "[Socket]\nService=some.target\n") + assertRejected("f.socket", "[Socket]\nService=some.invalid\n") + // Not a list. + assertRejected("f.socket", "[Socket]\nService=some.service other.service\n") + } + + @Test + fun testServiceSocketsMustNameSockets() { + assertAccepted( + "f.service", + "[Service]", + "Sockets=some.socket", + "Sockets=udev-control.socket udev-kernel.socket", + "Sockets=some.socket some@instance.socket", + ) + assertRejected("f.service", "[Service]\nSockets=some.service\n") + assertRejected("f.service", "[Service]\nSockets=some.service some.socket\n") + } +} From cfa069596e50197e0e689f7e8a678991301eb01f Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 25 Jul 2026 22:06:31 +0000 Subject: [PATCH 3/3] fix: correct the validators from #509 against the C parsers, and cite sources per file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A code review found ten confirmed defects in the two preceding commits. Six were false positives — values systemd accepts that the plugin flagged — which is the failure mode that matters most here, since a spurious red squiggle on a working unit file is worse than no validation at all. The root cause was method, not carelessness: the grammars were derived from the config_parse_* entry points but the reading often stopped there instead of following through to the leaf helper, and the corpus of real Debian units was allowed to stand in as evidence of correctness. A corpus can show that a grammar rejects something real; it cannot show that a grammar accepts everything legal, because legal-but-unused spellings simply aren't in it. Every fix below is derived from the C at a8e93919c3 — the commit systemd-build/build/last_commit_hash pins — with the man page as a cross-check. False positives fixed: paths config_parse_unit_condition_path and config_parse_path_spec hand the parser the whole rvalue: no splitting, no unquoting, no unescaping. path_is_valid_full() restricts no characters, so `/mnt/My Data` is one perfectly good path. The old grammar stopped at the first unescaped space (a convention copied from the plugin's own PathOptionValue, not from systemd) and wrongly honoured backslash escapes. Split into UNIT_PATH and QUOTABLE_UNIT_PATH, since [Socket] Symlinks= IS split, with EXTRACT_UNQUOTE, and so does take quotes and escapes. numbers safe_atou* pass base 0 to strtoul, so hex and octal are legal wherever they are used. New UnsignedNumberTerminal parses in the actual base, so the range check survives the change of spelling — the previous alternation could only bound its decimal branch and silently let `TypeOfService=0400` (256) through. MTU parse_mtu bounds the value, not the spelling: `IPv6MTUBytes=1K` is 1024 bytes and under IPV6_MIN_MTU. New ByteSizeTerminal evaluates the suffix. Peer= takes the faithful grammar rather than inheriting Address='s stricter one; a bare `Peer=2001:db8::2` is accepted by systemd. DAD config_parse_address_dad tries parse_boolean() FIRST and accepts it with a "For historical reasons" warning. Booleans are now accepted and marked deprecated, which repeats systemd's own advice instead of erroring. The man page documents only the four family names — the source wins. Match Name= `!` is consumed with a bare p += invert and the following extract_first_word skips whitespace, so `Name=! eth0` is valid. capabilities the gperf table is built with --ignore-case and capability_to_name() renders lower case, so `cap_sys_admin` is as canonical as the upper-case form. FlexibleLiteralChoiceTerminal grew an ignoreCase flag rather than duplicating 41 names. Beyond the ten findings, [Address] Address= had the same class of bug and predates this work: it demanded /8…/32 for IPv4 and /64…/128 for IPv6, so it flagged `Address=2600::1/0` — a line from systemd's own test/test-network/conf/25-veth-peer.network. The prefix ranges are now the full family ranges. Requiring an explicit IPv6 prefix is kept, because systemd warns about that itself. InvalidValueForNetworkAddressesTest is updated accordingly (the `/7` case it asserted was wrong). Every file the two commits touched now opens with a reference block naming the man page, the parsing function, and the table or helper that decides the accepted set, all as links pinned to a8e93919c3 so they stay valid. Combinators.kt and AiGenerated.kt additionally record the two traps that produced most of these defects: AlternativeCombinator is first-full-match with no backtracking under the classic engine, and a duplicate key in AiGenerated.kt's mapOf silently wins. Whole suite green under both engines. Re-running every registered grammar over the 6,680-unit corpus, the validators from these commits now reject only values that are genuinely invalid: deliberately-malformed fixtures, plus an unsubstituted @ETC@ template and one relative PathChanged= in shipped Debian units. Co-Authored-By: Claude Opus 5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 24 ++++ .../optionvalues/NetworkAddressOptionValue.kt | 48 +++++-- .../ai/AddressAndNextHopFlagOptionValues.kt | 49 +++++++- ...igParseAddressGenerationTypeOptionValue.kt | 12 ++ .../ai/ConfigParseMtuOptionValue.kt | 53 ++++---- .../ai/ConfigParseNamePolicyOptionValue.kt | 11 ++ .../ai/ConfigParseNetdevKindOptionValue.kt | 8 ++ .../ai/ConfigParseSetStatusOptionValue.kt | 13 +- ...onfigParseTunnelLocalAddressOptionValue.kt | 8 ++ ...nfigParseTunnelRemoteAddressOptionValue.kt | 7 ++ ...rseUnitConditionArchitectureOptionValue.kt | 9 ++ ...ParseUnitConditionCapabilityOptionValue.kt | 29 +++-- ...ditionControlGroupControllerOptionValue.kt | 9 ++ ...ParseUnitConditionCpuFeatureOptionValue.kt | 12 ++ ...ConfigParseUnitConditionPathOptionValue.kt | 18 ++- ...igParseUnitConditionSecurityOptionValue.kt | 11 ++ ...nfigParseUnitConditionStringOptionValue.kt | 8 ++ ...eUnitConditionVirtualizationOptionValue.kt | 12 ++ .../ConfigParseWireguardPeerKeyOptionValue.kt | 8 ++ .../ai/NetworkMiscOptionValues.kt | 25 +++- .../ai/RouteAddressNextHopOptionValues.kt | 30 ++++- .../ai/RoutingPolicyRuleOptionValues.kt | 12 ++ .../ai/UnitPathAndUnitNameOptionValues.kt | 23 +++- .../optionvalues/grammar/ByteSizeTerminal.kt | 78 ++++++++++++ .../optionvalues/grammar/Coloring.kt | 2 + .../optionvalues/grammar/Combinators.kt | 117 +++++++++++++----- .../grammar/FlexibleLiteralChoiceTerminal.kt | 31 +++-- .../grammar/UnsignedNumberTerminal.kt | 70 +++++++++++ .../InvalidValueForNetworkAddressesTest.kt | 9 +- .../ai/ConditionAndAssertInspectionTest.kt | 45 ++++++- .../ai/NetdevAndExitStatusInspectionTest.kt | 18 +++ .../ai/NetworkSectionInspectionTest.kt | 59 ++++++++- .../ai/UnitPathAndUnitNameInspectionTest.kt | 31 ++++- 33 files changed, 794 insertions(+), 105 deletions(-) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ByteSizeTerminal.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/UnsignedNumberTerminal.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index de23179f..1320d6ac 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt @@ -3,6 +3,29 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.* +/* + * Registration table mapping systemd's (config_parse_* function, ltype) pairs to value validators. + * + * The pairs come from the gperf files under systemd-build/build/, which are generated from systemd + * a8e93919c3 (https://github.com/systemd/systemd/blob/a8e93919c3) — grep those rather than guessing at a + * key's parser or its ltype. + * + * Two hazards specific to this file: + * + * - It is one big mapOf(), so a duplicate Validator key is not an error: the LAST occurrence silently + * wins. Check for an existing entry before adding one. + * + * - Registering one grammar across several ltypes of the same parser is only correct when those slots + * really do share a value type. Several dispatching parsers (config_parse_address_section, + * config_parse_nexthop_section, config_parse_routing_policy_rule, config_parse_route_section) give + * each ltype its own leaf parser via a ConfigSectionParser table, and blanket-registering them has + * twice produced grammars that rejected every legitimate value. + * + * A `*` ltype is a wildcard: SemanticDataRepository.getValidatorForSectionAndKey falls back to it when + * the exact ltype is absent, which is the right tool when every ltype of a parser genuinely does share + * a grammar (config_parse_unit_condition_path). + */ + /** * This file is auto-generated by the install.sh script. * It contains all AI-generated validators with their registration mappings. @@ -329,6 +352,7 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_route_section", "ROUTE_PREFERRED_SOURCE") to ConfigParseRoutePreferredSourceOptionValue() as OptionValueInformation, Validator("config_parse_route_section", "ROUTE_GATEWAY_ONLINK") to ConfigParseRouteGatewayOnlinkOptionValue() as OptionValueInformation, Validator("config_parse_route_section", "ROUTE_SCOPE") to ConfigParseRouteScopeOptionValue() as OptionValueInformation, + Validator("config_parse_address_section", "ADDRESS_PEER") to ConfigParseAddressSectionPeerOptionValue() as OptionValueInformation, Validator("config_parse_address_section", "ADDRESS_BROADCAST") to ConfigParseAddressSectionBroadcastOptionValue() as OptionValueInformation, Validator("config_parse_address_section", "ADDRESS_PREFERRED_LIFETIME") to ConfigParseAddressSectionPreferredLifetimeOptionValue() as OptionValueInformation, Validator("config_parse_nexthop_section", "NEXTHOP_GATEWAY") to ConfigParseNextHopGatewayOptionValue() as OptionValueInformation, diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt index 79b81ec9..13a22825 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt @@ -1,24 +1,56 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_PREFIX_LENGTH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +/* + * [Address] Address= (and the [Network] Address= shorthand) in a .network file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Address= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-address.c config_parse_address + * https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/conf-parser.c config_parse_in_addr_prefix + */ + +/** + * Validator for `Address=`, the ADDRESS_ADDRESS slot of config_parse_address_section. + * + * config_parse_address delegates to config_parse_in_addr_prefix asking for PREFIXLEN_REFUSE. When the + * prefix length is missing that returns -ENOANO, and the caller retries with + * in_addr_prefix_from_string_auto and logs: + * + * > Address=… is specified without prefix length. Assuming the prefix length is N. + * > Please specify the prefix length explicitly. + * + * Requiring the prefix on IPv6 therefore matches systemd's own advice rather than over-reaching, and + * that behaviour is kept. What is *not* faithful is bounding the prefix: in_addr_prefix_from_string + * only rejects a length above the family's address width, so the full 0…32 and 0…128 ranges are + * legal. The previous grammar demanded /8…/32 and /64…/128, which flagged `Address=2600::1/0` — a + * line out of systemd's own test/test-network/conf/25-veth-peer.network — and `Address=…/7`. + * + * `Peer=` is the same ConfigSectionParser slot but takes the fully faithful grammar, since it has no + * comparable history; see ConfigParseAddressSectionPeerOptionValue. + */ class NetworkAddressOptionValue() : GrammarOptionValue("config_parse_address_section", GRAMMAR) { companion object { val GRAMMAR = SequenceCombinator( - IP_ADDR_AND_PREFIX_LENGTH, EOF()) + AlternativeCombinator( + SequenceCombinator(IPV6_ADDR, CIDR_SEPARATOR, IntegerTerminal(0, 129)), + SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, IntegerTerminal(0, 33)))), + ), + EOF() + ) - // Address= and Peer= are the same ConfigSectionParser entry (config_parse_address), so they get - // the same grammar. Note it is deliberately stricter than systemd's in_addr_prefix_from_string_auto, - // which would also allow an IPv4 prefix below /8 and an IPv6 address with no prefix at all. val validators = mapOf( - Validator("config_parse_address_section", "ADDRESS_ADDRESS") to NetworkAddressOptionValue(), - Validator("config_parse_address_section", "ADDRESS_PEER") to NetworkAddressOptionValue() + Validator("config_parse_address_section", "ADDRESS_ADDRESS") to NetworkAddressOptionValue() ) } } - diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/AddressAndNextHopFlagOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/AddressAndNextHopFlagOptionValues.kt index e7cc0edd..5393ad2e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/AddressAndNextHopFlagOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/AddressAndNextHopFlagOptionValues.kt @@ -11,6 +11,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unsignedNumber +/* + * The remaining [Address] and [NextHop] table slots of a .network file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BAddress%5D%20Section%20Options + * parsers https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-address.c config_parse_address_dad, config_parse_uint32_flag + * https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-nexthop.c config_parse_nexthop_family, config_parse_nexthop_group + * + * These slots previously shared one grammar per section — a uint32 for every [Address] entry and a + * boolean for every [NextHop] entry — which flagged legitimate values such as AddPrefixRoute=no, + * DuplicateAddressDetection=ipv4, Id=20 and Family=ipv4. + */ + /* * The remaining `[Address]` and `[NextHop]` table slots (#509). * @@ -35,12 +47,41 @@ class ConfigParseAddressSectionFlagOptionValue : SimpleGrammarOptionValues( ) /** - * `[Address] DuplicateAddressDetection=` — table entry config_parse_address_dad, resolved through - * duplicate_address_detection_address_family_table (src/network/networkd-util.c). + * `[Address] DuplicateAddressDetection=` — table entry config_parse_address_dad. + * + * The man page documents only the four family names, but the parser tries parse_boolean() *first* and + * accepts a boolean with nothing worse than a warning: + * + * ```c + * r = parse_boolean(rvalue); + * if (r >= 0) { + * log_syntax(unit, LOG_WARNING, filename, line, 0, + * "For historical reasons, %s=%s means %s=%s. " + * "Please use 'both', 'ipv4', 'ipv6' or 'none' instead.", …); + * ``` + * + * So the booleans are accepted here too, but marked deprecated so the editor repeats systemd's advice + * instead of reporting an error on a line networkd honours. This is a case where the C source and the + * man page disagree and the source wins. */ class ConfigParseAddressSectionDadOptionValue : SimpleGrammarOptionValues( - ADDRESS, SequenceCombinator(FlexibleLiteralChoiceTerminal("none", "both", "ipv4", "ipv6"), EOF()) -) + ADDRESS, SequenceCombinator(DAD, EOF()) +) { + private companion object { + private const val HISTORICAL = + "For historical reasons a boolean here means the opposite of what it looks like: " + + "yes means none and no means both. Please use 'both', 'ipv4', 'ipv6' or 'none' instead." + + val DAD = FlexibleLiteralChoiceTerminal( + "none", "both", "ipv4", "ipv6", + // parse_boolean() spellings, all deprecated. + "1", "yes", "y", "true", "t", "on", "0", "no", "n", "false", "f", "off", + ).deprecating( + listOf("1", "yes", "y", "true", "t", "on", "0", "no", "n", "false", "f", "off") + .associateWith { HISTORICAL } + ) + } +} /** `[NextHop] Id=` — table entry config_parse_uint32. */ class ConfigParseNextHopIdOptionValue : SimpleGrammarOptionValues( diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressGenerationTypeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressGenerationTypeOptionValue.kt index 3895e10e..ca4d3e11 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressGenerationTypeOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressGenerationTypeOptionValue.kt @@ -10,6 +10,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +/* + * The IPv6 address-generation tokens: [Network] IPv6Token=, [IPv6AcceptRA] Token= and + * [DHCPPrefixDelegation] / [DHCPv6PrefixDelegation] Token=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6Token= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-address-generation.c config_parse_address_generation_type + * secret https://github.com/systemd/systemd/blob/a8e93919c3/src/libsystemd/sd-id128/id128-util.c id128_from_string_nonzero + * + * systemd's own test/test-network/conf/25-ipv6-prefix-veth-token-prefixstable.network doubles as the + * negative-case list for this setting. + */ + /** * Validator for the IPv6 address-generation tokens: `[Network] IPv6Token=`, `[IPv6AcceptRA] Token=` * and `[DHCPPrefixDelegation] Token=` / `[DHCPv6PrefixDelegation] Token=` (.network). diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMtuOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMtuOptionValue.kt index d4d6966e..8795e445 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMtuOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMtuOptionValue.kt @@ -1,40 +1,51 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ByteSizeTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +/* + * MTUBytes= and its family-specific siblings. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#MTUBytes= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6MTUBytes= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/conf-parser.c config_parse_mtu + * https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/parse-util.c parse_mtu, parse_size + */ + /** * Validator for `MTUBytes=` in `[Link]` (.network and .link) and `[NetDev]`, plus * `[Network] IPv6MTUBytes=` and `[DHCPv4] RouteMTUBytes=`. * - * C function: config_parse_mtu (src/shared/conf-parser.c) → parse_mtu (src/basic/parse-util.c), which - * runs parse_size() with base 1024 and then range-checks the result. The lower bound comes from the - * ltype: AF_INET6 requires IPV6_MIN_MTU (1280) and AF_INET requires IPV4_MIN_MTU (68); AF_UNSPEC has - * no minimum. The upper bound is UINT32_MAX throughout. + * config_parse_mtu hands the value to parse_mtu, which runs parse_size() with base 1024 and then + * range-checks the number of bytes it denotes: * - * parse_size() also accepts IEC suffixes and decimal fractions, which no [IntegerTerminal] can - * range-check. The suffixed spelling is therefore accepted without a bound; the plain integer form — - * what every real-world MTU uses — still gets one. + * ```c + * r = parse_size(s, 1024, &u); + * … + * if (u > UINT32_MAX) return -ERANGE; + * switch (family) { + * case AF_INET: m = IPV4_MIN_MTU; break; // 68 + * case AF_INET6: m = IPV6_MIN_MTU; break; // 1280 + * default: m = 0; + * } + * if (u < m) return -ERANGE; + * ``` + * + * The bound is therefore on the *value*, not on how it is written: `IPv6MTUBytes=1K` is 1024 bytes + * and is rejected for being under IPV6_MIN_MTU even though nothing about its spelling looks wrong. + * [ByteSizeTerminal] evaluates the suffix so the minimum applies to every form. + * + * @param minimum the family's IPV*_MIN_MTU, in bytes */ open class ConfigParseMtuOptionValue(minimum: Long) : SimpleGrammarOptionValues( "config_parse_mtu", - SequenceCombinator( - AlternativeCombinator( - SequenceCombinator( - RegexTerminal("""[0-9]+(?:\.[0-9]+)?""", """[0-9]+(?:\.[0-9]+)?"""), - LiteralChoiceTerminal("E", "P", "T", "G", "M", "K", "B"), - ), - IntegerTerminal(minimum, 4_294_967_296L), - ), - EOF() - ) + SequenceCombinator(ByteSizeTerminal(minimum, UINT32_MAX), EOF()) ) +private const val UINT32_MAX = 4_294_967_295L + /** `MTUBytes=` with no family-specific minimum (ltype AF_UNSPEC). */ class ConfigParseMtuAnyOptionValue : ConfigParseMtuOptionValue(0) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNamePolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNamePolicyOptionValue.kt index f51c28e2..35f8be50 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNamePolicyOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNamePolicyOptionValue.kt @@ -7,6 +7,17 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +/* + * [Link] NamePolicy= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#NamePolicy= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/udev/net/link-config.c config_parse_name_policy (DEFINE_CONFIG_PARSE_ENUMV) + * values https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/netif-naming-scheme.c name_policy_table + * + * Distinct from AlternativeNamesPolicy=, which resolves against alternative_names_policy_table in the + * same file and so offers neither "kernel" nor "keep". + */ + /** * Validator for `[Link] NamePolicy=` (.link). * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetdevKindOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetdevKindOptionValue.kt index e3d738ae..5faf4725 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetdevKindOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetdevKindOptionValue.kt @@ -5,6 +5,14 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +/* + * [NetDev] Kind= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Kind= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/network/netdev/netdev.c config_parse_netdev_kind + * values https://github.com/systemd/systemd/blob/a8e93919c3/src/network/netdev/netdev.c netdev_kind_table + */ + /** * Validator for `[NetDev] Kind=` (.netdev). * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSetStatusOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSetStatusOptionValue.kt index 0c52bebf..107031e1 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSetStatusOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSetStatusOptionValue.kt @@ -5,12 +5,23 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unsignedNumber import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +/* + * [Service] SuccessExitStatus=, RestartPreventExitStatus= and RestartForceExitStatus=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#SuccessExitStatus= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_set_status + * names https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/exit-status.c exit_status_mappings, exit_status_from_string + * https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/signal-util.c static_signal_table, signal_from_string + * bases https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/parse-util.h safe_atou8 passes base 0 to strtoul + */ + /** * Validator for `[Service] SuccessExitStatus=`, `RestartPreventExitStatus=` and * `RestartForceExitStatus=`. @@ -84,7 +95,7 @@ class ConfigParseSetStatusOptionValue : SimpleGrammarOptionValues( // The number goes first so the name terminal's lenient shape match (its choices contain // digits, e.g. USR1) can't swallow a numeric word before the range check runs. private val STATUS = AlternativeCombinator( - IntegerTerminal(0, 256), + unsignedNumber(256), REALTIME_SIGNAL, EXIT_STATUS_OR_SIGNAL_NAME, ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelLocalAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelLocalAddressOptionValue.kt index 9115379b..3b84dbb4 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelLocalAddressOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelLocalAddressOptionValue.kt @@ -8,6 +8,14 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +/* + * [Tunnel] Local= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Local= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/network/netdev/tunnel.c config_parse_tunnel_local_address + * values https://github.com/systemd/systemd/blob/a8e93919c3/src/network/netdev/netdev-util.c netdev_local_address_type_table + */ + /** * Validator for `[Tunnel] Local=` (.netdev). * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelRemoteAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelRemoteAddressOptionValue.kt index 9133c81c..4d0bde7c 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelRemoteAddressOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTunnelRemoteAddressOptionValue.kt @@ -8,6 +8,13 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +/* + * [Tunnel] Remote= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Remote= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/network/netdev/tunnel.c config_parse_tunnel_remote_address + */ + /** * Validator for `[Tunnel] Remote=` (.netdev). * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionArchitectureOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionArchitectureOptionValue.kt index 4c483c15..7acdc157 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionArchitectureOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionArchitectureOptionValue.kt @@ -4,6 +4,15 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString +/* + * [Unit] ConditionArchitecture= / AssertArchitecture=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#ConditionArchitecture= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_unit_condition_string (CONDITION_ARCHITECTURE) + * check https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/condition.c condition_test_architecture_parameter + * values https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/architecture.c architecture_table + */ + /** * Validator for `[Unit] ConditionArchitecture=` / `AssertArchitecture=`. * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCapabilityOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCapabilityOptionValue.kt index 3355eb23..cfdeceff 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCapabilityOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCapabilityOptionValue.kt @@ -2,22 +2,37 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CAPABILITY_NAME import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unsignedNumber + +/* + * [Unit] ConditionCapability= / AssertCapability=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#ConditionCapability= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_unit_condition_string (CONDITION_CAPABILITY) + * check https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/condition.c condition_test_capability + * names https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/capability-list.c capability_from_name + * https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/meson.build the gperf table is built with --ignore-case + */ /** * Validator for `[Unit] ConditionCapability=` / `AssertCapability=`. * * C function: config_parse_unit_condition_string with ltype CONDITION_CAPABILITY; the parameter goes - * to capability_from_name (src/basic/capability-list.c), which accepts either a decimal capability - * number in 0…CAP_LIMIT (62) or a single capability name. One capability, not a list. + * to capability_from_name (src/basic/capability-list.c), which accepts either a capability number in + * 0…CAP_LIMIT (62) or a single capability name. One capability, not a list. * * The number is tried first so the numeric form isn't swallowed by the name terminal's lenient shape - * match. Note that systemd's name lookup is gperf-generated with `--ignore-case`, so `cap_sys_admin` - * also resolves; like the existing CapabilityBoundingSet= validator this grammar lists only the - * canonical upper-case spellings. + * match, and it is an [unsignedNumber] because capability_from_name reads it with safe_atoi(). + * + * Case: the name lookup is gperf-generated with `--ignore-case` + * (src/basic/meson.build: `gperf … '--ignore-case'`), and capability_to_name() actually renders the + * canonical form in *lower* case, so `cap_sys_admin` is every bit as valid as `CAP_SYS_ADMIN`. + * [CAPABILITY_NAME] therefore accepts either, while keeping the upper-case list as the terminal that + * supplies quick-fix suggestions. */ class ConfigParseUnitConditionCapabilityOptionValue : SimpleGrammarOptionValues( "config_parse_unit_condition_string", - conditionString(AlternativeCombinator(IntegerTerminal(0, 63), Capabilities)) + conditionString(AlternativeCombinator(unsignedNumber(63), CAPABILITY_NAME)) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionControlGroupControllerOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionControlGroupControllerOptionValue.kt index e8e362be..7a50bda4 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionControlGroupControllerOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionControlGroupControllerOptionValue.kt @@ -7,6 +7,15 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString +/* + * [Unit] ConditionControlGroupController= / AssertControlGroupController=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#ConditionControlGroupController= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_unit_condition_string (CONDITION_CONTROL_GROUP_CONTROLLER) + * check https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/condition.c condition_test_control_group_controller + * values https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/cgroup-util.c cgroup_controller_table, cg_mask_from_string + */ + /** * Validator for `[Unit] ConditionControlGroupController=` / `AssertControlGroupController=`. * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCpuFeatureOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCpuFeatureOptionValue.kt index fe811edb..211698b0 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCpuFeatureOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionCpuFeatureOptionValue.kt @@ -4,6 +4,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString +/* + * [Unit] ConditionCPUFeature= / AssertCPUFeature=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#ConditionCPUFeature= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_unit_condition_string (CONDITION_CPU_FEATURE) + * check https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/condition.c condition_test_cpufeature -> has_cpu_with_flag + * + * The flag set comes from /proc/cpuinfo at evaluation time, so there is nothing to enumerate; only the + * shape is checkable. This slot previously pointed at the boolean condition grammar, which rejected + * every real feature name. + */ + /** * Validator for `[Unit] ConditionCPUFeature=` / `AssertCPUFeature=`. * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt index 3956f77d..ff310d3a 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionPathOptionValue.kt @@ -1,9 +1,23 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ABSOLUTE_PATH_WITH_SPECIFIERS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UNIT_PATH import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionPath +/* + * The path-valued Condition*= / Assert*= settings of a unit's [Unit] section. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#ConditionPathExists= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_unit_condition_path + * checks https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/parse-helpers.c path_simplify_and_warn + * https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/path-util.c path_is_valid_full, path_is_normalized + * keys systemd-build/build/load-fragment-gperf.gperf (the Unit.Condition… and Unit.Assert… keys) + * + * The parser hands the whole rvalue to unit_path_printf() and then to path_simplify_and_warn() with + * PATH_CHECK_ABSOLUTE. It never splits on whitespace and never unescapes, so the only shape rules are + * "absolute after specifier expansion" and "no surviving `..` component" — see UNIT_PATH. + */ + /** * Validator for every path-valued `Condition…=` / `Assert…=` in `[Unit]`. * @@ -21,5 +35,5 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram */ class ConfigParseUnitConditionPathOptionValue : SimpleGrammarOptionValues( "config_parse_unit_condition_path", - conditionPath(ABSOLUTE_PATH_WITH_SPECIFIERS) + conditionPath(UNIT_PATH) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionSecurityOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionSecurityOptionValue.kt index 16cb725e..75df3f4b 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionSecurityOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionSecurityOptionValue.kt @@ -4,6 +4,17 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString +/* + * [Unit] ConditionSecurity= / AssertSecurity=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#ConditionSecurity= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_unit_condition_string (CONDITION_SECURITY) + * check https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/condition.c condition_test_security + * + * There is no lookup table for this one: condition_test_security is a chain of streq() calls against + * the whole parameter, which is why it takes exactly one technology and never a list. + */ + /** * Validator for `[Unit] ConditionSecurity=` / `AssertSecurity=`. * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionStringOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionStringOptionValue.kt index fe84896e..d62a3c37 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionStringOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionStringOptionValue.kt @@ -4,6 +4,14 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString +/* + * The boolean-valued [Unit] conditions: ConditionFirstBoot=, ConditionACPower= and their Assert twins. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#ConditionFirstBoot= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_unit_condition_string + * checks https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/condition.c condition_test_first_boot, condition_test_ac_power + */ + /** * Validator for the `[Unit]` conditions whose parameter is a plain boolean: `ConditionFirstBoot=` and * `ConditionACPower=`, plus their `Assert…=` twins. diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionVirtualizationOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionVirtualizationOptionValue.kt index 2855576e..eaf0e044 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionVirtualizationOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUnitConditionVirtualizationOptionValue.kt @@ -4,6 +4,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.conditionString +/* + * [Unit] ConditionVirtualization= / AssertVirtualization=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#ConditionVirtualization= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_unit_condition_string (CONDITION_VIRTUALIZATION) + * check https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/condition.c condition_test_virtualization + * values https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/virt.c virtualization_table + * + * The check tries, in order: the literal "private-users", parse_boolean(), the categories "vm" and + * "container", and finally an exact id from the table — so all four groups are one choice set here. + */ + /** * Validator for `[Unit] ConditionVirtualization=` / `AssertVirtualization=`. * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyOptionValue.kt index e839da19..43bc51fd 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyOptionValue.kt @@ -7,6 +7,14 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +/* + * [WireGuardPeer] PublicKey= and PresharedKey= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PublicKey= + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/network/netdev/wireguard.c config_parse_wireguard_peer_key + * -> wireguard_decode_key_and_warn + */ + /** * Validator for `[WireGuardPeer] PublicKey=` and `PresharedKey=` (.netdev). * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/NetworkMiscOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/NetworkMiscOptionValues.kt index 76897ed3..e92e5d73 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/NetworkMiscOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/NetworkMiscOptionValues.kt @@ -8,7 +8,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unsignedNumber import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.TIME_VALUE @@ -16,6 +16,19 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +/* + * Assorted networkd and udev settings. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Name= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Key= + * parsers https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/net-condition.c config_parse_match_ifnames + * https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-bridge-vlan.c config_parse_bridge_vlan_id_range + * https://github.com/systemd/systemd/blob/a8e93919c3/src/network/netdev/tunnel.c config_parse_tunnel_key + * https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-radv.c config_parse_prefix_lifetime + * names https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/socket-util.c ifname_valid_full, ifname_valid_char + * ranges https://github.com/systemd/systemd/blob/a8e93919c3/src/shared/vlan-util.c parse_vid_range (VLANID_MAX) + */ + /* * Assorted networkd / udev validators (#509). */ @@ -44,8 +57,10 @@ class ConfigParseMatchIfnamesOptionValue : SimpleGrammarOptionValues( "config_parse_match_ifnames", ifnameList(INTERFACE_NAME) ) +// The `!` is consumed with a bare `p += invert`, and the extract_first_word() that follows skips +// leading whitespace, so `Name=! eth0` is the same inverted match as `Name=!eth0`. private fun ifnameList(name: Combinator): Combinator = SequenceCombinator( - ZeroOrOne(LiteralChoiceTerminal("!")), + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal("!"), ZeroOrOne(WhitespaceTerminal()))), name, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), name)), EOF() @@ -64,8 +79,8 @@ private fun ifnameList(name: Combinator): Combinator = SequenceCombinator( class ConfigParseBridgeVlanIdRangeOptionValue : SimpleGrammarOptionValues( "config_parse_bridge_vlan_id_range", SequenceCombinator( - IntegerTerminal(0, 4095), - ZeroOrOne(SequenceCombinator(HYPHEN, IntegerTerminal(0, 4095))), + unsignedNumber(4095), + ZeroOrOne(SequenceCombinator(HYPHEN, unsignedNumber(4095))), EOF() ) ) @@ -80,7 +95,7 @@ class ConfigParseBridgeVlanIdRangeOptionValue : SimpleGrammarOptionValues( */ class ConfigParseTunnelKeyOptionValue : SimpleGrammarOptionValues( "config_parse_tunnel_key", - SequenceCombinator(AlternativeCombinator(IPV4_ADDR, IntegerTerminal(0L, 4_294_967_296L)), EOF()) + SequenceCombinator(AlternativeCombinator(IPV4_ADDR, unsignedNumber(4_294_967_296L)), EOF()) ) /** diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RouteAddressNextHopOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RouteAddressNextHopOptionValues.kt index 466a31f9..808865a5 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RouteAddressNextHopOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RouteAddressNextHopOptionValues.kt @@ -8,9 +8,22 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_ANY_PREFIX -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unsignedNumber import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +/* + * The [Route], [Address] and [NextHop] sections of a .network file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BRoute%5D%20Section%20Options + * parsers https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-route.c config_parse_route_section + * https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-address.c config_parse_address_section + * https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-nexthop.c config_parse_nexthop_section + * keys systemd-build/build/networkd-network-gperf.gperf + * + * Each of these is a dispatcher over a ConfigSectionParser table keyed by ltype; the classes below + * mirror the table entries one by one. + */ + /* * Validators for the `[Route]`, `[Address]` and `[NextHop]` sections of a .network file (#509). * @@ -63,13 +76,26 @@ class ConfigParseRouteScopeOptionValue : SimpleGrammarOptionValues( ROUTE, SequenceCombinator( AlternativeCombinator( - IntegerTerminal(0, 256), + unsignedNumber(256), FlexibleLiteralChoiceTerminal("global", "site", "link", "host", "nowhere"), ), EOF() ) ) +/** + * `[Address] Peer=` — table entry config_parse_address, i.e. config_parse_in_addr_prefix. It is asked + * for PREFIXLEN_REFUSE, but when that reports a missing prefix length it retries with + * in_addr_prefix_from_string_auto and only logs a hint, so a bare address is accepted too; the prefix, + * when present, may be anything the family allows. + * + * `Address=` is the same table entry but keeps the older, stricter [NetworkAddressOptionValue] because + * that behaviour is pinned by existing tests — see the note there. + */ +class ConfigParseAddressSectionPeerOptionValue : SimpleGrammarOptionValues( + ADDRESS, SequenceCombinator(IP_ADDR_AND_ANY_PREFIX, EOF()) +) + /** * `[Address] Broadcast=` — table entry config_parse_broadcast: a boolean (asking systemd to derive * the broadcast address from Address=, or not to set one), or an explicit IPv4 address. IPv6 is not diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt index 6159f948..1aa294f2 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt @@ -13,6 +13,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +/* + * The [RoutingPolicyRule] section of a .network file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BRoutingPolicyRule%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/a8e93919c3/src/network/networkd-routing-policy-rule.c config_parse_routing_policy_rule + * keys systemd-build/build/networkd-network-gperf.gperf + * + * config_parse_routing_policy_rule is a dispatcher: the ltype indexes a ConfigSectionParser table and + * each entry names the parser that actually reads the value, so this file mirrors the table entry by + * entry rather than giving the whole section one grammar. + */ + /* * Validators for the `[RoutingPolicyRule]` section of a .network file (#509). * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/UnitPathAndUnitNameOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/UnitPathAndUnitNameOptionValues.kt index b3cbfefa..0baf17ab 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/UnitPathAndUnitNameOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/UnitPathAndUnitNameOptionValues.kt @@ -1,7 +1,8 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ABSOLUTE_PATH_WITH_SPECIFIERS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.QUOTABLE_UNIT_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UNIT_PATH import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal @@ -9,6 +10,20 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +/* + * Unit-file settings that take absolute paths or unit names. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.path.html#PathExists= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#Symlinks= + * parsers https://github.com/systemd/systemd/blob/a8e93919c3/src/core/load-fragment.c config_parse_path_spec, config_parse_unit_path_strv_printf, + * config_parse_socket_service, config_parse_service_sockets + * words https://github.com/systemd/systemd/blob/a8e93919c3/src/basic/extract-word.c extract_first_word + * + * Note the split between the two path shapes: config_parse_path_spec takes the whole rvalue, while + * Symlinks= is a list built with extract_first_word(..., EXTRACT_UNQUOTE), which honours quotes and + * drops backslashes. See UNIT_PATH and QUOTABLE_UNIT_PATH. + */ + /* * Validators for unit-file settings that take absolute paths or unit names (#509). */ @@ -23,7 +38,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram */ class ConfigParsePathSpecOptionValue : SimpleGrammarOptionValues( "config_parse_path_spec", - SequenceCombinator(ABSOLUTE_PATH_WITH_SPECIFIERS, EOF()) + SequenceCombinator(UNIT_PATH, EOF()) ) /** @@ -35,8 +50,8 @@ class ConfigParsePathSpecOptionValue : SimpleGrammarOptionValues( class ConfigParseUnitPathStrvPrintfOptionValue : SimpleGrammarOptionValues( "config_parse_unit_path_strv_printf", SequenceCombinator( - ABSOLUTE_PATH_WITH_SPECIFIERS, - ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), ABSOLUTE_PATH_WITH_SPECIFIERS)), + QUOTABLE_UNIT_PATH, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), QUOTABLE_UNIT_PATH)), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ByteSizeTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ByteSizeTerminal.kt new file mode 100644 index 00000000..ca94690b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ByteSizeTerminal.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigDecimal +import java.math.BigInteger + +/** + * A byte size as read by systemd's `parse_size()`, range-checked on the value it denotes rather than + * on how it is spelled. + * + * `parse_size(s, base)` accepts a run of `number[suffix]` elements, where the suffix selects a power + * of `base` from the table for that base (`B`/`K`/`M`/`G`/`T`/`P`/`E` for base 1024) and a bare + * number means bytes. A fractional part is allowed on all but the last element. Every setting here + * uses base 1024. + * + * Bounding the spelling instead of the value is not good enough: `IPv6MTUBytes=1K` is 1024, which is + * below IPV6_MIN_MTU, and no digit-count or character-class check can see that. This terminal + * evaluates the sum and compares it, so the family minimum applies to every spelling. + * + * Only the single-element form is matched, which is what a size setting is written as in practice; + * the multi-element form `parse_size()` also accepts (`1G 512M`) is not recognised, so it is reported + * as malformed rather than silently mis-valued. + * + * @param minInclusive lowest accepted value in bytes + * @param maxInclusive highest accepted value in bytes + * @see parse-util.c, parse_size() + */ +class ByteSizeTerminal( + private val minInclusive: BigInteger, + private val maxInclusive: BigInteger, +) : TerminalCombinator { + + constructor(minInclusive: Long, maxInclusive: Long) : + this(BigInteger.valueOf(minInclusive), BigInteger.valueOf(maxInclusive)) + + private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value + + private fun inRange(text: String): Boolean { + val match = SHAPE.matchEntire(text) ?: return false + val digits = match.groupValues[1] + val suffix = match.groupValues[2] + val multiplier = MULTIPLIERS[suffix] ?: return false + // parse_size() truncates towards zero, so "1.5K" is 1536 and "0.5B" is 0. + val bytes = BigDecimal(digits).multiply(BigDecimal(multiplier)).toBigInteger() + return bytes >= minInclusive && bytes <= maxInclusive + } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + if (!inRange(text)) return NoMatch.copy(longestMatch = offset) + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun parse(value: String, offset: Int): Sequence { + val text = span(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, inRange(text))))) + } + + override fun toString(): String = "Size($minInclusive,$maxInclusive)" + + private companion object { + val SHAPE = Regex("""([0-9]+(?:\.[0-9]+)?)([EPTGMKB]?)""") + + /** table_1024 in parse_size(); the empty suffix is a plain byte count. */ + val MULTIPLIERS: Map = buildMap { + var scale = BigInteger.ONE + for (suffix in listOf("B", "K", "M", "G", "T", "P", "E")) { + put(suffix, scale) + put("", BigInteger.ONE) + scale = scale.multiply(BigInteger.valueOf(1024)) + } + } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Coloring.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Coloring.kt index d5a27280..03a8a56e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Coloring.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Coloring.kt @@ -36,6 +36,8 @@ data class Region(val start: Int, val end: Int, val role: Role, val tag: Semanti */ fun defaultRole(terminal: TerminalCombinator): Role? = when (terminal) { is IntegerTerminal -> Role.LITERAL + is UnsignedNumberTerminal -> Role.LITERAL + is ByteSizeTerminal -> Role.LITERAL is LiteralChoiceTerminal -> if (terminal.choices.allPunctuation()) Role.OPERATOR else Role.ENUM is FlexibleLiteralChoiceTerminal -> if (terminal.choices.allPunctuation()) Role.OPERATOR else Role.ENUM // RegexTerminal (free-form names/strings: Description=, interface names, ...) and whitespace stay diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt index 97df8009..5d10819b 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt @@ -1,5 +1,28 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues + +/* + * Shared value grammars, each modelled on the systemd routine that actually parses that kind of value. + * + * Everything here is pinned to systemd a8e93919c3, which is the commit recorded in + * systemd-build/build/last_commit_hash and therefore the one the plugin's own gperf and man data are + * generated from. Browse it at https://github.com/systemd/systemd/blob/a8e93919c3. + * + * Two things worth knowing before adding to this file: + * + * - Under the classic SyntacticMatch/SemanticMatch engine, AlternativeCombinator is first-full-match + * with NO backtracking: once a branch matches at an offset the enclosing sequence continues from + * there and never reconsiders. So order alternatives longest/most-specific first, and put a lenient + * shape matcher (a FlexibleLiteralChoiceTerminal, say) after the precise ones it could shadow. + * + * - Reach for the C source rather than the man page when they disagree. The man page documents intent + * and is sometimes narrower than the parser: DuplicateAddressDetection= is documented as four family + * names but config_parse_address_dad accepts booleans too, and safe_atou* silently accept hex and + * octal because they pass base 0 to strtoul. The gperf files under systemd-build/build/ are the + * authority for which (parser, ltype) pairs exist and which keys use them. + */ + val BOOLEAN = FlexibleLiteralChoiceTerminal("1", "yes", "y", "true", "t", "on", "0", "no", "n", "false", "f", "off") val BYTES = RegexTerminal("[0-9]+[a-zA-Z]*\\s*", "[0-9]+[KMGT]?\\s*") val DEVICE = RegexTerminal("\\S+\\s*", "/[^\\u0000. ]+\\s*") @@ -200,15 +223,45 @@ fun conditionString(parameter: Combinator): Combinator { ) } -/** - * A path systemd requires to be absolute after specifier expansion — i.e. anything it feeds through - * unit_path_printf() and then path_simplify_and_warn(..., PATH_CHECK_ABSOLUTE). We can't expand - * specifiers, so a value may legitimately begin with one (`%t/foo`, `%h/.cache`) instead of a slash. - * Spaces are only allowed when backslash-escaped, matching the rest of the plugin's paths. - */ -val ABSOLUTE_PATH_WITH_SPECIFIERS = RegexTerminal( - """\S(?:[^\s\\]|\\[\s\S])*""", - """(?:/|%\S)(?:[^\s\\]|\\[\s\S])*""" +// --------------------------------------------------------------------------------------------------- +// Paths that systemd requires to be absolute — anything it feeds through unit_path_printf() and then +// path_simplify_and_warn(..., PATH_CHECK_ABSOLUTE). +// +// That helper (src/shared/parse-helpers.c) checks, in order: valid UTF-8, absolute, then — after +// path_simplify() has collapsed `//`, `./` and any trailing slash — a length under PATH_MAX and +// path_is_normalized(), which is what rejects a surviving `..` component. Nothing restricts which +// characters a component may contain, so a path may perfectly well contain spaces. +// +// Whether a space *ends* the value depends on the caller, not on the path rules, which is why there +// are two terminals below: +// +// UNIT_PATH the setting takes one path and hands the parser the whole rvalue verbatim — +// no word splitting, no unquoting, no unescaping. Condition*=/Assert*= and the +// [Path] watch settings work this way, so `/mnt/My Data` is a single path and a +// backslash in it is an ordinary character. +// QUOTABLE_UNIT_PATH the setting takes a list and splits it with extract_first_word(). That drops +// backslashes and, with EXTRACT_UNQUOTE, honours '…' and "…", so a path with a +// space has to be escaped or quoted to survive splitting. +// +// Specifiers are resolved before the absolute check, so a value may legitimately begin with one +// (`%t/foo`, `%h/.cache`) rather than with a slash. + +private const val PATH_START = """(?:/|%\S)""" + +// Rejects a `..` component anywhere: the optional `(?:[\s\S]*/)` swallows any leading directories, so +// the lookahead fires on `/a/../b` and `/a/..` but not on `/a/..b` or `/a/b..`. +private const val NO_DOT_DOT = """(?!(?:[\s\S]*/)?\.\.(?:/|$))""" + +/** One whole-value absolute path: everything from here to the end of the value belongs to it. */ +val UNIT_PATH = RegexTerminal( + """[\s\S]+""", + """$NO_DOT_DOT$PATH_START[\s\S]*""" +) + +/** One element of a whitespace-separated path list, optionally quoted or backslash-escaped. */ +val QUOTABLE_UNIT_PATH = RegexTerminal( + """"[^"]*"|'[^']*'|(?:[^\s\\]|\\[\s\S])+""", + """"$PATH_START(?:[^"\\]|\\[\s\S])*"|'$PATH_START(?:[^'\\]|\\[\s\S])*'|$PATH_START(?:[^\s\\]|\\[\s\S])*""" ) @@ -250,29 +303,27 @@ val ALTERNATIVE_INTERFACE_NAME = RegexTerminal("""\S+""", ifnameSemantic(127)) /** - * An unsigned number in `[0, maxExclusive)`, spelled any of the ways systemd's `safe_atou*` family - * reads one. - * - * Those helpers pass base 0 down to strtoul(), so besides decimal they accept `0x` hexadecimal and - * leading-zero octal — systemd's own test data writes `TypeOfService=0x08`. An [IntegerTerminal] - * only understands decimal, so the other two bases are added alongside it. - * - * Only the decimal branch carries the numeric bounds. The hexadecimal branch is bounded by digit - * count instead, which is exact for the power-of-two limits these settings use, and the octal branch - * is not bounded at all. Both therefore err towards accepting an out-of-range value rather than - * flagging a legal one, on spellings essentially nobody writes. + * An unsigned number in `[minInclusive, maxExclusive)`, spelled any of the ways systemd's `safe_atou*` + * family reads one — decimal, `0x` hexadecimal or leading-zero octal, since those helpers pass base 0 + * to strtoul(). See [UnsignedNumberTerminal], which parses in the actual base so the bounds hold for + * every spelling. */ -fun unsignedNumber(maxExclusive: Long, minInclusive: Long = 0L): Combinator { - val hexDigits = maxOf(1, ((maxExclusive - 1).toString(16).length)) - return AlternativeCombinator( - // Hex first: on "0x08" a decimal terminal would happily match just the leading "0" and strand the rest. - SequenceCombinator( - LiteralChoiceTerminal("0x", "0X"), - RegexTerminal("""[0-9a-fA-F]+""", """[0-9a-fA-F]{1,$hexDigits}""") - ), - // Octal, likewise before decimal so "0377" isn't read as three hundred and seventy-seven. - RegexTerminal("""0[0-7]+""", """0[0-7]+"""), - IntegerTerminal(minInclusive, maxExclusive), - ) -} +fun unsignedNumber(maxExclusive: Long, minInclusive: Long = 0L): Combinator = + UnsignedNumberTerminal(minInclusive, maxExclusive) + + +// --------------------------------------------------------------------------------------------------- +// Capability names — capability_from_name (systemd src/basic/capability-list.c). +// +// The lookup table is gperf-generated with `--ignore-case` (src/basic/meson.build), and the reverse +// mapping capability_to_name() renders names in lower case (src/basic/capability-to-name.awk uses +// `tolower`), so both `CAP_SYS_ADMIN` and `cap_sys_admin` resolve. The upper-case +// FlexibleLiteralChoiceTerminal is kept as the first alternative because it is what supplies the +// quick-fix suggestions on a misspelled name; the regex behind it accepts any other casing. +// +// The name list is read back off that terminal rather than duplicated, so the two can't drift. +val CAPABILITY_NAME = FlexibleLiteralChoiceTerminal( + *SimpleGrammarOptionValues.Capabilities.choices, + ignoreCase = true, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt index b1c5e234..70884c90 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt @@ -2,12 +2,23 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gra import kotlin.math.max -class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombinator { +/** + * A choice from a fixed set of words that matches loosely for syntax — so a wrong value still gets + * located and highlighted — but requires an exact choice to be semantically valid. + * + * [ignoreCase] models systemd lookup tables generated by gperf with `--ignore-case`, where the spelling + * in the table is canonical but any casing resolves. The choices are still stored in their canonical + * form, so quick-fixes suggest that spelling. + */ +class FlexibleLiteralChoiceTerminal(vararg val choices: String, val ignoreCase: Boolean = false) : TerminalCombinator { init { choices.sortBy { -it.length } } + /** The choice [text] denotes, or null if it is none of them. */ + private fun choiceFor(text: String): String? = choices.firstOrNull { it.equals(text, ignoreCase) } + private var deprecations: Map = emptyMap() /** Mark some choices as valid-but-deprecated (choice -> reason). Returns this for chaining. */ @@ -16,7 +27,7 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin return this } - override fun deprecationFor(token: String): String? = deprecations[token] + override fun deprecationFor(token: String): String? = choiceFor(token)?.let { deprecations[it] } val syntaticMatch: Regex @@ -58,11 +69,11 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin var regexClass = "" - if (lowerCase) { + if (lowerCase || (ignoreCase && upperCase)) { regexClass += "a-z" } - if (upperCase) { + if (upperCase || (ignoreCase && lowerCase)) { regexClass += "A-Z" } @@ -82,8 +93,9 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin override fun SyntacticMatch(value: String, offset: Int): MatchResult { for (choice in choices) { - if (value.substring(offset).startsWith(choice)) { - return MatchResult(listOf(choice), offset + choice.length, listOf(this), offset + choice.length) + if (value.startsWith(choice, offset, ignoreCase)) { + val text = value.substring(offset, offset + choice.length) + return MatchResult(listOf(text), offset + choice.length, listOf(this), offset + choice.length) } } @@ -94,8 +106,9 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin override fun SemanticMatch(value: String, offset: Int): MatchResult { for (choice in choices) { - if (value.substring(offset).startsWith(choice)) { - return MatchResult(listOf(choice), offset + choice.length, listOf(this), offset + choice.length) + if (value.startsWith(choice, offset, ignoreCase)) { + val text = value.substring(offset, offset + choice.length) + return MatchResult(listOf(text), offset + choice.length, listOf(this), offset + choice.length) } } return NoMatch.copy(longestMatch = offset) @@ -106,7 +119,7 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin // valid only if the matched text is one of the exact choices. val m = syntaticMatch.matchAt(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) val text = m.value - val valid = choices.any { it == text } + val valid = choiceFor(text) != null return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, valid)))) } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/UnsignedNumberTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/UnsignedNumberTerminal.kt new file mode 100644 index 00000000..f7f24fed --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/UnsignedNumberTerminal.kt @@ -0,0 +1,70 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +/** + * An unsigned integer written the way systemd's `safe_atou*` family reads one, range-checked exactly. + * + * Those helpers are thin wrappers that pass base `0` to `strtoul()`: + * + * ```c + * static inline int safe_atou8(const char *s, uint8_t *ret) { + * return safe_atou8_full(s, 0, ret); + * } + * ``` + * + * (src/basic/parse-util.h; the same shape holds for safe_atou, safe_atou16 and safe_atou32.) Base 0 + * means C literal syntax, so `255`, `0xFF` and `0377` are all the same number and all valid input to + * any setting parsed this way — systemd's own test data writes `TypeOfService=0x08`. + * + * [IntegerTerminal] only understands decimal, and an alternation of decimal / hex / octal branches + * can only bound the decimal one, which silently drops the range check for the other two. This + * terminal parses in the actual base instead, so `[0, maxExclusive)` is enforced however the number + * is spelled. + * + * Note this is deliberately *not* used for values systemd reads with `parse_size()` (see + * [ByteSizeTerminal]) or with a plain `strtoul(..., 10)`. + * + * @param minInclusive lowest accepted value + * @param maxExclusive one past the highest accepted value + * @see parse-util.h + */ +class UnsignedNumberTerminal( + private val minInclusive: Long, + private val maxExclusive: Long, +) : TerminalCombinator { + + private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value + + /** The numeric value of [text], or null when it doesn't fit a Long (and so is out of range anyway). */ + private fun valueOf(text: String): Long? = when { + text.length > 2 && (text.startsWith("0x") || text.startsWith("0X")) -> text.drop(2).toLongOrNull(16) + text.length > 1 && text.startsWith("0") -> text.drop(1).toLongOrNull(8) + else -> text.toLongOrNull() + } + + private fun inRange(text: String): Boolean = valueOf(text)?.let { it >= minInclusive && it < maxExclusive } ?: false + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + if (!inRange(text)) return NoMatch.copy(longestMatch = offset) + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun parse(value: String, offset: Int): Sequence { + val text = span(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + // Lenient: any number-shaped run matches so it can be located and highlighted; valid only in range. + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, inRange(text))))) + } + + override fun toString(): String = "UInt($minInclusive,$maxExclusive)" + + private companion object { + // Hex before octal before decimal, so "0x10" isn't read as the single digit 0 and "0377" isn't + // read as three hundred and seventy-seven. + val SHAPE = Regex("""0[xX][0-9a-fA-F]+|0[0-7]+|[0-9]+""") + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt index 0f3656d0..6ea9c32d 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt @@ -10,6 +10,10 @@ class InvalidValueForNetworkAddressesTest : AbstractUnitFileTest() { val file=""" [Network] Address=244.178.44.111/32 + # in_addr_prefix_from_string only rejects a prefix wider than the address, so the whole 0..32 + # range is legal -- systemd's own 25-veth-peer.network uses Address=2600::1/0. + Address=244.25.2.1/7 + Address=10.0.0.1/0 """.trimIndent() // Execute SUT @@ -34,8 +38,6 @@ class InvalidValueForNetworkAddressesTest : AbstractUnitFileTest() { Address=1.2.3.4.5/8 # Invalid Prefix Length Address=244.25.2.1/33 - # Invalid Prefix Length - Address=244.25.2.1/7 """.trimIndent() // Execute SUT @@ -44,7 +46,7 @@ class InvalidValueForNetworkAddressesTest : AbstractUnitFileTest() { val highlights = myFixture.doHighlighting() // Verification - assertSize(5, highlights) + assertSize(4, highlights) } @@ -73,6 +75,7 @@ class InvalidValueForNetworkAddressesTest : AbstractUnitFileTest() { Address=::1/127 Address=2001:db8::/65 Address=ff02::1/128 + Address=2600::1/0 # Honestly I don't know what matches this Address=2001:0db8:85a3:0000:0000:8a2e:192.168.0.1/96 """.trimIndent() diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConditionAndAssertInspectionTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConditionAndAssertInspectionTest.kt index 80cd2acd..84e346d9 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConditionAndAssertInspectionTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConditionAndAssertInspectionTest.kt @@ -4,6 +4,16 @@ import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection import org.junit.Test +/* + * Expectations here are derived from systemd's C parsers at a8e93919c3 (https://github.com/systemd/systemd/blob/a8e93919c3), + * the commit systemd-build/build/last_commit_hash pins, and NOT from what happens to appear in + * real-world unit files. Where a case is subtle the individual test says which routine decides it. + * + * Several of the rejection cases are lifted from systemd's own negative fixtures under + * test/test-network/conf/ and from KDE's syntax-highlighting test input, both of which deliberately + * contain malformed values. + */ + /** * Tests for the `[Unit] Condition…=` / `Assert…=` validators (#509). * @@ -93,9 +103,21 @@ class ConditionAndAssertInspectionTest : AbstractUnitFileTest() { } @Test - fun testPathConditionsRejectLists() { - // The whole value is one path — it is never split on whitespace. - assertRejected("ConditionNeedsUpdate=/etc /var") + fun testPathConditionsTakeTheWholeValueAsOnePath() { + // The value is never split on whitespace and never unescaped, so a space is just a path + // character: `/etc /var` is one directory whose name contains a space, not two paths. + assertAccepted( + "ConditionNeedsUpdate=/etc /var", + "ConditionPathExists=/mnt/My Data/.stamp", + "ConditionPathExists=!/srv/My Share/spool", + ) + } + + @Test + fun testPathConditionsRejectUnnormalizedPaths() { + // path_simplify() collapses `.` and `//` but leaves `..`, which path_is_normalized() then refuses. + assertRejected("ConditionPathExists=/etc/../var") + assertRejected("ConditionPathExists=/etc/..") } // ------------------------------------------------------------------ ConditionArchitecture @@ -190,6 +212,23 @@ class ConditionAndAssertInspectionTest : AbstractUnitFileTest() { ) } + @Test + fun testCapabilityNamesAreCaseInsensitive() { + // capability_from_name() looks the name up in a gperf table built with --ignore-case, and + // capability_to_name() actually renders the canonical form in lower case. + assertAccepted( + "ConditionCapability=cap_sys_admin", + "ConditionCapability=Cap_Net_Admin", + "AssertCapability=!cap_chown", + ) + } + + @Test + fun testCapabilityNumberAcceptsEveryBase() { + assertAccepted("ConditionCapability=0x1e", "ConditionCapability=016") + assertRejected("ConditionCapability=0x3F") + } + @Test fun testCapabilityRejectsUnknownOutOfRangeAndLists() { assertRejected("ConditionCapability=CAP_BOGUS") diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetdevAndExitStatusInspectionTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetdevAndExitStatusInspectionTest.kt index 828395e0..0b919958 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetdevAndExitStatusInspectionTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetdevAndExitStatusInspectionTest.kt @@ -4,6 +4,16 @@ import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection import org.junit.Test +/* + * Expectations here are derived from systemd's C parsers at a8e93919c3 (https://github.com/systemd/systemd/blob/a8e93919c3), + * the commit systemd-build/build/last_commit_hash pins, and NOT from what happens to appear in + * real-world unit files. Where a case is subtle the individual test says which routine decides it. + * + * Several of the rejection cases are lifted from systemd's own negative fixtures under + * test/test-network/conf/ and from KDE's syntax-highlighting test input, both of which deliberately + * contain malformed values. + */ + /** * Tests for the non-condition validators added in #509: NetDev Kind=, exit-status sets, .link * NamePolicy=, WireGuard peer keys, tunnel endpoints and IPv6 address-generation tokens. @@ -76,6 +86,14 @@ class NetdevAndExitStatusInspectionTest : AbstractUnitFileTest() { ) } + @Test + fun testExitStatusNumbersAcceptEveryBase() { + // exit_status_from_string() -> safe_atou8() -> strtoul with base 0. + assertAccepted("f.service", "[Service]", "SuccessExitStatus=0x10", "RestartPreventExitStatus=0377") + assertRejected("f.service", "[Service]\nSuccessExitStatus=0x100\n") + assertRejected("f.service", "[Service]\nSuccessExitStatus=0400\n") + } + @Test fun testExitStatusRejectsOutOfRangeAndUnknownNames() { // A bare number is only ever an exit status, so the range is 0…255 (safe_atou8). diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkSectionInspectionTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkSectionInspectionTest.kt index 9c78a270..345dc144 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkSectionInspectionTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkSectionInspectionTest.kt @@ -4,6 +4,16 @@ import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection import org.junit.Test +/* + * Expectations here are derived from systemd's C parsers at a8e93919c3 (https://github.com/systemd/systemd/blob/a8e93919c3), + * the commit systemd-build/build/last_commit_hash pins, and NOT from what happens to appear in + * real-world unit files. Where a case is subtle the individual test says which routine decides it. + * + * Several of the rejection cases are lifted from systemd's own negative fixtures under + * test/test-network/conf/ and from KDE's syntax-highlighting test input, both of which deliberately + * contain malformed values. + */ + /** * Tests for the `[RoutingPolicyRule]`, `[Route]`, `[Address]` and `[NextHop]` validators added in * #509, plus the assorted networkd settings that came with them. @@ -149,10 +159,14 @@ class NetworkSectionInspectionTest : AbstractUnitFileTest() { "DuplicateAddressDetection=ipv4", "DuplicateAddressDetection=both", "DuplicateAddressDetection=none", + // config_parse_address_dad tries parse_boolean() first and accepts it with a warning, so these + // are legal (if confusing) rather than invalid. + "DuplicateAddressDetection=yes", + "DuplicateAddressDetection=no", + "DuplicateAddressDetection=0", "RouteMetric=128", ) assertRejected("f.network", "[Address]\nAddPrefixRoute=bogus\n") - assertRejected("f.network", "[Address]\nDuplicateAddressDetection=yes\n") assertRejected("f.network", "[Address]\nRouteMetric=hoge\n") } @@ -225,6 +239,44 @@ class NetworkSectionInspectionTest : AbstractUnitFileTest() { assertRejected("f.network", "[IPv6Prefix]\nValidLifetimeSec=soon\n") } + @Test + fun testMatchIfnamesAllowsSpaceAfterTheInversionMarker() { + // `invert = *p == '!'; p += invert;` and then extract_first_word, which skips leading whitespace. + assertAccepted("f.network", "[Match]", "Name=! eth0", "Name=! veth-peer host0") + assertAccepted("f.link", "[Match]", "OriginalName=! enp0s1") + } + + @Test + fun testPeerFollowsTheCParserNotTheStricterAddressGrammar() { + // config_parse_in_addr_prefix retries without a prefix and allows the whole 0…128 range. + assertAccepted( + "f.network", + "[Address]", + "Peer=2001:db8::2", + "Peer=fd00::2/56", + "Peer=10.0.0.2", + "Peer=10.0.0.2/4", + ) + } + + @Test + fun testNumbersAcceptEveryBaseAndStillRangeCheck() { + // safe_atou* run strtoul with base 0, so hex and octal are legal spellings... + assertAccepted( + "f.netdev", + "[Tunnel]", + "Key=0x11223344", + "InputKey=0755", + ) + assertAccepted("f.network", "[BridgeVLAN]", "VLAN=0x10", "EgressUntagged=0777") + // ...and the range check has to survive the change of base. + assertRejected("f.network", "[RoutingPolicyRule]\nTypeOfService=0x1FF\n") + assertRejected("f.network", "[RoutingPolicyRule]\nTypeOfService=0400\n") + assertRejected("f.network", "[RoutingPolicyRule]\nGoTo=00\n") + assertRejected("f.network", "[BridgeVLAN]\nVLAN=0xFFF\n") + assertRejected("f.network", "[NextHop]\nGroup=5:0401\n") + } + @Test fun testMtuBounds() { assertAccepted("f.netdev", "[NetDev]", "MTUBytes=1480", "MTUBytes=9000", "MTUBytes=16") @@ -234,5 +286,10 @@ class NetworkSectionInspectionTest : AbstractUnitFileTest() { assertRejected("f.network", "[Network]\nIPv6MTUBytes=1000\n") assertRejected("f.network", "[DHCPv4]\nRouteMTUBytes=32\n") assertRejected("f.netdev", "[NetDev]\nMTUBytes=huge\n") + // parse_size() bounds the value, not the spelling: 1K is 1024 bytes, under IPV6_MIN_MTU. + assertAccepted("f.network", "[Network]", "IPv6MTUBytes=2K") + assertRejected("f.network", "[Network]\nIPv6MTUBytes=1K\n") + assertRejected("f.network", "[DHCPv4]\nRouteMTUBytes=8B\n") + assertRejected("f.netdev", "[NetDev]\nMTUBytes=8E\n") } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/UnitPathAndUnitNameInspectionTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/UnitPathAndUnitNameInspectionTest.kt index d712bc86..74a5d92c 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/UnitPathAndUnitNameInspectionTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/UnitPathAndUnitNameInspectionTest.kt @@ -4,6 +4,16 @@ import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection import org.junit.Test +/* + * Expectations here are derived from systemd's C parsers at a8e93919c3 (https://github.com/systemd/systemd/blob/a8e93919c3), + * the commit systemd-build/build/last_commit_hash pins, and NOT from what happens to appear in + * real-world unit files. Where a case is subtle the individual test says which routine decides it. + * + * Several of the rejection cases are lifted from systemd's own negative fixtures under + * test/test-network/conf/ and from KDE's syntax-highlighting test input, both of which deliberately + * contain malformed values. + */ + /** * Tests for the unit-file path and unit-name validators added in #509: `[Path]` watch settings, * `[Socket] Symlinks=`, `[Socket] Service=` and `[Service] Sockets=`. @@ -37,8 +47,9 @@ class UnitPathAndUnitNameInspectionTest : AbstractUnitFileTest() { // path_simplify_and_warn(..., PATH_CHECK_ABSOLUTE): a relative path is refused. openqa's shipped // openqa-minion-restart.path gets this wrong with an unsubstituted "installvendorlib/..." value. assertRejected("f.path", "[Path]\nPathChanged=installvendorlib/Minion/pg.sql\n") - // One path per assignment — the value is never split on whitespace. - assertRejected("f.path", "[Path]\nPathExists=/tmp/a /tmp/b\n") + // config_parse_path_spec takes the whole rvalue, so a space is a path character rather than a + // separator: this is one directory called "a /tmp/b" under /tmp, and systemd accepts it. + assertAccepted("f.path", "[Path]", "PathExists=/tmp/a /tmp/b") } @Test @@ -54,6 +65,22 @@ class UnitPathAndUnitNameInspectionTest : AbstractUnitFileTest() { assertRejected("f.socket", "[Socket]\nSymlinks=/run/ok relative/no\n") } + @Test + fun testSocketSymlinksHonoursQuotingAndEscapes() { + // Unlike the [Path] settings, this list IS split with extract_first_word(..., EXTRACT_UNQUOTE), + // which honours quotes and drops backslashes — so a path with a space can survive splitting. + assertAccepted( + "f.socket", + "[Socket]", + "Symlinks=\"/run/my socket\"", + "Symlinks=\"/run/my socket\" /run/other", + "Symlinks='/run/my socket'", + "Symlinks=/run/my\\ socket", + ) + // Quoted or not, each entry still has to be absolute. + assertRejected("f.socket", "[Socket]\nSymlinks=\"relative/no\"\n") + } + @Test fun testSocketServiceMustNameAService() { assertAccepted(