From e588ebaead0b4f9e077ade7d146a2210dd392ebd Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 23 Oct 2026 18:26:46 -0700 Subject: [PATCH 1/8] feat: add grammar validators for netdev and link options (netdev-link batch) Adds 34 value validators for systemd-networkd .netdev and udev .link settings that had none (WireGuard keys/endpoints/allowed IPs/route tables, VXLAN/GENEVE/L2TP addresses, VLAN protocol and QoS maps, MACsec key ids and key files, CPU sets and RPS masks, ethtool advertise modes, udev Property=, and more), plus shared helpers in BatchNetdevLinkCombinators.kt (safe_atou numbers incl. 0b/0o/+, parse_size SI chains, parse_nsec durations, CPU sets, key-file paths, route tables). All grammars are derived from the C parsers at systemd 0f0682b773. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 41 +++ .../ai/ConfigParseAdvertiseOptionValue.kt | 75 +++++ .../ConfigParseBadadvBandwidthOptionValue.kt | 33 ++ .../ai/ConfigParseCoalesceNsecOptionValue.kt | 32 ++ .../ai/ConfigParseCpuSetOptionValue.kt | 34 +++ ...ConfigParseDefaultPortVlanidOptionValue.kt | 36 +++ .../ai/ConfigParseGeneveAddressOptionValue.kt | 27 ++ .../ai/ConfigParseIfaliasOptionValue.kt | 27 ++ .../ai/ConfigParseIfnamesOptionValue.kt | 37 +++ .../ai/ConfigParseIpoibPkeyOptionValue.kt | 32 ++ .../ai/ConfigParseIpv6FlowlabelOptionValue.kt | 32 ++ .../ConfigParseIrqAffinityNumaOptionValue.kt | 34 +++ ...ConfigParseIrqAffinityPolicyOptionValue.kt | 26 ++ .../ConfigParseL2tpSessionNameOptionValue.kt | 27 ++ ...gParseL2tpTunnelLocalAddressOptionValue.kt | 48 +++ ...ParseL2tpTunnelRemoteAddressOptionValue.kt | 28 ++ .../ConfigParseMacAddressPolicyOptionValue.kt | 26 ++ .../ai/ConfigParseMacsecKeyFileOptionValue.kt | 30 ++ .../ai/ConfigParseMacsecKeyIdOptionValue.kt | 44 +++ .../ai/ConfigParsePortOptionValue.kt | 26 ++ .../ai/ConfigParsePortRangeOptionValue.kt | 30 ++ .../ai/ConfigParseRpsCpuMaskOptionValue.kt | 33 ++ .../ai/ConfigParseUdevPropertyOptionValue.kt | 90 ++++++ .../ai/ConfigParseVlanQosMapsOptionValue.kt | 39 +++ .../ai/ConfigParseVlanprotocolOptionValue.kt | 26 ++ .../ai/ConfigParseVrfTableOptionValue.kt | 30 ++ .../ai/ConfigParseVxlanAddressOptionValue.kt | 45 +++ ...nfigParseWireguardAllowedIpsOptionValue.kt | 42 +++ ...ConfigParseWireguardEndpointOptionValue.kt | 70 +++++ ...figParseWireguardPeerKeyFileOptionValue.kt | 30 ++ ...ParseWireguardPeerRouteTableOptionValue.kt | 40 +++ ...ParseWireguardPrivateKeyFileOptionValue.kt | 30 ++ ...nfigParseWireguardPrivateKeyOptionValue.kt | 34 +++ ...nfigParseWireguardRouteTableOptionValue.kt | 40 +++ .../ai/ConfigParseWolPasswordOptionValue.kt | 35 +++ .../grammar/BatchNetdevLinkCombinators.kt | 287 ++++++++++++++++++ .../ai/ConfigParseAdvertiseOptionValueTest.kt | 44 +++ ...nfigParseBadadvBandwidthOptionValueTest.kt | 59 ++++ .../ConfigParseCoalesceNsecOptionValueTest.kt | 52 ++++ .../ai/ConfigParseCpuSetOptionValueTest.kt | 52 ++++ ...igParseDefaultPortVlanidOptionValueTest.kt | 46 +++ ...ConfigParseGeneveAddressOptionValueTest.kt | 41 +++ .../ai/ConfigParseIfaliasOptionValueTest.kt | 42 +++ .../ai/ConfigParseIfnamesOptionValueTest.kt | 44 +++ .../ai/ConfigParseIpoibPkeyOptionValueTest.kt | 47 +++ ...ConfigParseIpv6FlowlabelOptionValueTest.kt | 45 +++ ...nfigParseIrqAffinityNumaOptionValueTest.kt | 45 +++ ...igParseIrqAffinityPolicyOptionValueTest.kt | 40 +++ ...nfigParseL2tpSessionNameOptionValueTest.kt | 45 +++ ...seL2tpTunnelLocalAddressOptionValueTest.kt | 52 ++++ ...eL2tpTunnelRemoteAddressOptionValueTest.kt | 41 +++ ...figParseMacAddressPolicyOptionValueTest.kt | 42 +++ ...ConfigParseMacsecKeyFileOptionValueTest.kt | 43 +++ .../ConfigParseMacsecKeyIdOptionValueTest.kt | 51 ++++ .../ai/ConfigParsePortOptionValueTest.kt | 44 +++ .../ai/ConfigParsePortRangeOptionValueTest.kt | 45 +++ .../ConfigParseRpsCpuMaskOptionValueTest.kt | 46 +++ .../ConfigParseUdevPropertyOptionValueTest.kt | 53 ++++ .../ConfigParseVlanQosMapsOptionValueTest.kt | 48 +++ .../ConfigParseVlanprotocolOptionValueTest.kt | 48 +++ .../ai/ConfigParseVrfTableOptionValueTest.kt | 48 +++ .../ConfigParseVxlanAddressOptionValueTest.kt | 53 ++++ ...ParseWireguardAllowedIpsOptionValueTest.kt | 49 +++ ...igParseWireguardEndpointOptionValueTest.kt | 51 ++++ ...arseWireguardPeerKeyFileOptionValueTest.kt | 47 +++ ...eWireguardPeerRouteTableOptionValueTest.kt | 52 ++++ ...eWireguardPrivateKeyFileOptionValueTest.kt | 41 +++ ...ParseWireguardPrivateKeyOptionValueTest.kt | 46 +++ ...ParseWireguardRouteTableOptionValueTest.kt | 52 ++++ .../ConfigParseWolPasswordOptionValueTest.kt | 45 +++ 70 files changed, 3195 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.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 36704512..8cafbacf 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 @@ -413,6 +413,47 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_socket_service", "0") to ConfigParseSocketServiceOptionValue() as OptionValueInformation, Validator("config_parse_service_sockets", "0") to ConfigParseServiceSocketsOptionValue() as OptionValueInformation, + // Batch 2026-10: netdev-link + // systemd-networkd .netdev (src/network/netdev/) and udev .link (src/udev/net/) settings, pinned to + // systemd 0f0682b773. Every key using each (parser, ltype) pair was checked in the gperf files; where + // a parser branches on the key name (vxlan_address) the grammar is the union and says so. + Validator("config_parse_badadv_bandwidth", "0") to ConfigParseBadadvBandwidthOptionValue() as OptionValueInformation, + Validator("config_parse_vxlan_address", "0") to ConfigParseVxlanAddressOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_peer_key_file", "0") to ConfigParseWireguardPeerKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_vlanprotocol", "0") to ConfigParseVlanprotocolOptionValue() as OptionValueInformation, + Validator("config_parse_vlan_qos_maps", "0") to ConfigParseVlanQosMapsOptionValue() as OptionValueInformation, + Validator("config_parse_macsec_key_id", "0") to ConfigParseMacsecKeyIdOptionValue() as OptionValueInformation, + Validator("config_parse_macsec_key_file", "0") to ConfigParseMacsecKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_cpu_set", "0") to ConfigParseCpuSetOptionValue() as OptionValueInformation, + Validator("config_parse_wol_password", "0") to ConfigParseWolPasswordOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_route_table", "0") to ConfigParseWireguardRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_private_key_file", "0") to ConfigParseWireguardPrivateKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_private_key", "0") to ConfigParseWireguardPrivateKeyOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_peer_route_table", "0") to ConfigParseWireguardPeerRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_endpoint", "0") to ConfigParseWireguardEndpointOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_allowed_ips", "0") to ConfigParseWireguardAllowedIpsOptionValue() as OptionValueInformation, + Validator("config_parse_vrf_table", "0") to ConfigParseVrfTableOptionValue() as OptionValueInformation, + Validator("config_parse_udev_property", "0") to ConfigParseUdevPropertyOptionValue() as OptionValueInformation, + Validator("config_parse_rps_cpu_mask", "0") to ConfigParseRpsCpuMaskOptionValue() as OptionValueInformation, + Validator("config_parse_port_range", "0") to ConfigParsePortRangeOptionValue() as OptionValueInformation, + Validator("config_parse_port", "0") to ConfigParsePortOptionValue() as OptionValueInformation, + Validator("config_parse_mac_address_policy", "0") to ConfigParseMacAddressPolicyOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_tunnel_remote_address", "0") to ConfigParseL2tpTunnelRemoteAddressOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_tunnel_local_address", "0") to ConfigParseL2tpTunnelLocalAddressOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_session_name", "0") to ConfigParseL2tpSessionNameOptionValue() as OptionValueInformation, + Validator("config_parse_irq_affinity_policy", "0") to ConfigParseIrqAffinityPolicyOptionValue() as OptionValueInformation, + Validator("config_parse_irq_affinity_numa", "0") to ConfigParseIrqAffinityNumaOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_flowlabel", "0") to ConfigParseIpv6FlowlabelOptionValue() as OptionValueInformation, + Validator("config_parse_ipoib_pkey", "0") to ConfigParseIpoibPkeyOptionValue() as OptionValueInformation, + Validator("config_parse_ifnames", "IFNAME_VALID_ALTERNATIVE") to ConfigParseIfnamesOptionValue() as OptionValueInformation, + Validator("config_parse_ifalias", "0") to ConfigParseIfaliasOptionValue() as OptionValueInformation, + Validator("config_parse_geneve_address", "0") to ConfigParseGeneveAddressOptionValue() as OptionValueInformation, + Validator("config_parse_default_port_vlanid", "0") to ConfigParseDefaultPortVlanidOptionValue() as OptionValueInformation, + Validator("config_parse_advertise", "0") to ConfigParseAdvertiseOptionValue() as OptionValueInformation, + // RxCoalesceCQENSec= registers config_parse_coalesce_sec with ltype `true` (parse_nsec instead of + // parse_sec); the `0` entry above (ConfigParseCoalesceSecOptionValue) covers the parse_sec keys. + Validator("config_parse_coalesce_sec", "true") to ConfigParseCoalesceNsecOptionValue() as OptionValueInformation, + ) return allValidators diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt new file mode 100644 index 00000000..d5d929ee --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt @@ -0,0 +1,75 @@ +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 + +/* + * [Link] Advertise= and [EnergyEfficientEthernet] LinkMode= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Advertise= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L1125-L1177 config_parse_advertise (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L162-L168 ethtool_link_mode_bit_table (DEFINE_STRING_TABLE_LOOKUP) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-link-mode.py (generates the table from enum ethtool_link_mode_bit_indices) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/ethtool.h#L1966-L2109 enum ethtool_link_mode_bit_indices + * linux https://man7.org/linux/man-pages/man8/ethtool.8.html + */ + +/** + * The link-mode names ethtool-link-mode.py derives from the vendored kernel header: each + * `ETHTOOL_LINK_MODE__BIT` lower-cased with `_` turned into `-`, and `autoneg` renamed + * `autonegotiation`. 125 names at 0f0682b773. + */ +private val LINK_MODE = FlexibleLiteralChoiceTerminal( + "10baset-half", "10baset-full", "100baset-half", "100baset-full", "1000baset-half", + "1000baset-full", "autonegotiation", "tp", "aui", "mii", "fibre", "bnc", "10000baset-full", + "pause", "asym-pause", "2500basex-full", "backplane", "1000basekx-full", "10000basekx4-full", + "10000basekr-full", "10000baser-fec", "20000basemld2-full", "20000basekr2-full", + "40000basekr4-full", "40000basecr4-full", "40000basesr4-full", "40000baselr4-full", + "56000basekr4-full", "56000basecr4-full", "56000basesr4-full", "56000baselr4-full", + "25000basecr-full", "25000basekr-full", "25000basesr-full", "50000basecr2-full", + "50000basekr2-full", "100000basekr4-full", "100000basesr4-full", "100000basecr4-full", + "100000baselr4-er4-full", "50000basesr2-full", "1000basex-full", "10000basecr-full", + "10000basesr-full", "10000baselr-full", "10000baselrm-full", "10000baseer-full", + "2500baset-full", "5000baset-full", "fec-none", "fec-rs", "fec-baser", "50000basekr-full", + "50000basesr-full", "50000basecr-full", "50000baselr-er-fr-full", "50000basedr-full", + "100000basekr2-full", "100000basesr2-full", "100000basecr2-full", "100000baselr2-er2-fr2-full", + "100000basedr2-full", "200000basekr4-full", "200000basesr4-full", "200000baselr4-er4-fr4-full", + "200000basedr4-full", "200000basecr4-full", "100baset1-full", "1000baset1-full", + "400000basekr8-full", "400000basesr8-full", "400000baselr8-er8-fr8-full", "400000basedr8-full", + "400000basecr8-full", "fec-llrs", "100000basekr-full", "100000basesr-full", + "100000baselr-er-fr-full", "100000basecr-full", "100000basedr-full", "200000basekr2-full", + "200000basesr2-full", "200000baselr2-er2-fr2-full", "200000basedr2-full", "200000basecr2-full", + "400000basekr4-full", "400000basesr4-full", "400000baselr4-er4-fr4-full", "400000basedr4-full", + "400000basecr4-full", "100basefx-half", "100basefx-full", "10baset1l-full", "800000basecr8-full", + "800000basekr8-full", "800000basedr8-full", "800000basedr8-2-full", "800000basesr8-full", + "800000basevr8-full", "10baset1s-full", "10baset1s-half", "10baset1s-p2mp-half", + "10baset1brr-full", "200000basecr-full", "200000basekr-full", "200000basedr-full", + "200000basedr-2-full", "200000basesr-full", "200000basevr-full", "400000basecr2-full", + "400000basekr2-full", "400000basedr2-full", "400000basedr2-2-full", "400000basesr2-full", + "400000basevr2-full", "800000basecr4-full", "800000basekr4-full", "800000basedr4-full", + "800000basedr4-2-full", "800000basesr4-full", "800000basevr4-full", "1600000basecr8-full", + "1600000basekr8-full", "1600000basedr8-full", "1600000basedr8-2-full" +) + +/** + * Validator for `[Link] Advertise=` and `[EnergyEfficientEthernet] LinkMode=` (.link). + * + * C function: config_parse_advertise. A whitespace-separated list (extract_first_word, no unquoting) of + * ethtool link-mode names, each looked up exactly (case-sensitive) in ethtool_link_mode_bit_table — + * speed/duplex modes like `1000baset-full` as well as the non-speed bits (`autonegotiation`, `pause`, + * `asym-pause`, `tp`, `fibre`, `fec-rs`, …). + * + * The table is generated at build time from the kernel's enum ethtool_link_mode_bit_indices in + * systemd's vendored copy of linux/ethtool.h (src/include/uapi, which meson puts first on the include + * path). That is wider than the ethtool-link-mode.xml table in the plugin's generated man data, which + * lists 102 names; this follows the header the C is built against. An unknown name is logged and + * skipped by udev; an empty assignment clears the list. + */ +class ConfigParseAdvertiseOptionValue : SimpleGrammarOptionValues( + "config_parse_advertise", + SequenceCombinator(LINK_MODE, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), LINK_MODE)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt new file mode 100644 index 00000000..9cd52a24 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt @@ -0,0 +1,33 @@ +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.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.siSize + +/* + * [BatmanAdvanced] GatewayBandwidthDown= / GatewayBandwidthUp= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#GatewayBandwidthDown= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/batadv.c#L53-L89 config_parse_badadv_bandwidth (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1000) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-batadv.netdev#L14-L15 + */ + +/** + * Validator for `[BatmanAdvanced] GatewayBandwidthDown=` and `GatewayBandwidthUp=` (.netdev), plus the + * misspelled `GatewayBandwithDown=` / `GatewayBandwithUp=` that systemd still accepts as aliases. + * + * C function: config_parse_badadv_bandwidth, which hands the whole value to parse_size(rvalue, 1000, …): + * a number with an optional case-sensitive SI suffix (`K`, `M`, `G`, `T`, `P`, `E`, `B`), a + * fraction allowed, and several elements with strictly descending suffixes (`1G 500M`) summed. See + * [siSize]. + * + * The parser only *warns* when the result divided by 100 kbit exceeds UINT32_MAX and still stores it, + * so no magnitude bound is applied here. An invalid value is logged and ignored by systemd. + */ +class ConfigParseBadadvBandwidthOptionValue : SimpleGrammarOptionValues( + "config_parse_badadv_bandwidth", + SequenceCombinator(siSize(), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt new file mode 100644 index 00000000..5eaf1d1d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt @@ -0,0 +1,32 @@ +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.NSEC_DURATION_FINITE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] RxCoalesceCQENSec= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#RxCoalesceCQENSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L1370-L1415 config_parse_coalesce_sec (ltype "true", i.e. nsec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1374-L1466 parse_nsec + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L141 TEST(parse_nsec) + */ + +/** + * Validator for `[Link] RxCoalesceCQENSec=` (.link) — the one key that registers config_parse_coalesce_sec + * with ltype `true`, which switches the parser from parse_sec() to parse_nsec(). + * + * parse_nsec reads one or more `number[unit]` elements, a bare number meaning nanoseconds (`500`, + * `1us`, `2.5`, `1s500ms`, `.7`). See [NSEC_DURATION_FINITE]. + * + * `infinity` parses, but config_parse_coalesce_sec then refuses any result above UINT32_MAX ("Too large + * … value"), and NSEC_INFINITY is UINT64_MAX — so `infinity` is never a usable value here and is + * rejected. The same UINT32_MAX ceiling (about 4.29 seconds in nanoseconds) applies to every finite value + * too, but that compares the computed sum and is not modelled. An invalid value is logged and ignored. + */ +class ConfigParseCoalesceNsecOptionValue : SimpleGrammarOptionValues( + "config_parse_coalesce_sec", + SequenceCombinator(NSEC_DURATION_FINITE, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt new file mode 100644 index 00000000..1e2210b0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.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.CPU_SET +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] IRQAffinity= (.link) and [Exec] CPUAffinity= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinity= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#CPUAffinity= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L220-L307 config_parse_cpu_set (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L186-L206 cpu_set_add_range (CPU_SET_MAX_NCPU) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-cpu-set-util.c#L36 TEST(parse_cpu_set) + * linux https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html + */ + +/** + * Validator for `[Link] IRQAffinity=` (.link) and `[Exec] CPUAffinity=` (.nspawn). + * + * C function: config_parse_cpu_set. CPU indices and `N-M` ranges, separated by any mix of whitespace + * and commas (separators coalesce; leading/trailing ones are fine), each word optionally quoted. Each + * bound is a safe_atou() number (so hex works) below CPU_SET_MAX_NCPU = 8192. See [CPU_SET]. + * + * Lenient on one point: a reversed range such as `3-0` is rejected by systemd after parsing (it + * compares the numbers); that is not modelled. An empty assignment clears the set. + */ +class ConfigParseCpuSetOptionValue : SimpleGrammarOptionValues( + "config_parse_cpu_set", + SequenceCombinator(CPU_SET, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt new file mode 100644 index 00000000..de967d48 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt @@ -0,0 +1,36 @@ +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.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou + +/* + * [Bridge] DefaultPVID= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#DefaultPVID= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.c#L44-L67 config_parse_default_port_vlanid (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.c#L8-L23 parse_vlanid (safe_atou16, vlanid_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.h#L7-L13 VLANID_MAX (4094) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-bridge.netdev#L13 DefaultPVID=9 + */ + +/** + * Validator for `[Bridge] DefaultPVID=` (.netdev). + * + * C function: config_parse_default_port_vlanid. Either `none` (no PVID) or a VLAN id read by + * parse_vlanid (safe_atou16, so hex works) and checked by vlanid_is_valid, i.e. 0…4094. + * + * The man page says "an integer in the range 1…4094", but vlanid_is_valid only checks `id <= 4094`, so + * `0` is accepted (and stored as 0, the same as `none`). This follows the C. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseDefaultPortVlanidOptionValue : SimpleGrammarOptionValues( + "config_parse_default_port_vlanid", + SequenceCombinator( + AlternativeCombinator(FlexibleLiteralChoiceTerminal("none"), safeAtou(4095L)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt new file mode 100644 index 00000000..a5756c53 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [GENEVE] 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/0f0682b773/src/network/netdev/geneve.c#L124-L163 config_parse_geneve_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[GENEVE] Remote=` (.netdev). + * + * C function: config_parse_geneve_address: an IPv4 or IPv6 literal (in_addr_from_string_auto → + * inet_pton). Not modelled: systemd also refuses a multicast address, which depends on the parsed value. + * An invalid value is logged and ignored by systemd. + */ +class ConfigParseGeneveAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_geneve_address", + SequenceCombinator(IP_ADDR, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt new file mode 100644 index 00000000..b4b05b86 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] Alias= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Alias= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L1944-L1980 config_parse_ifalias (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/if.h#L33 IFALIASZ (256) + */ + +/** + * Validator for `[Link] Alias=` (.link). + * + * C function: config_parse_ifalias. The value is free text, but it must be ASCII (ascii_is_valid) and + * shorter than IFALIASZ = 256 bytes, so at most 255 characters. Anything containing a non-ASCII + * character (`Café uplink`) or longer than that is logged and ignored by udev. An empty assignment + * clears the alias. + */ +class ConfigParseIfaliasOptionValue : SimpleGrammarOptionValues( + "config_parse_ifalias", + SequenceCombinator(RegexTerminal("[\\s\\S]+", "[\\x00-\\x7F]{1,255}"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt new file mode 100644 index 00000000..a3f6805b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt @@ -0,0 +1,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.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +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 + +/* + * [Link] AlternativeName= (.link) and [HSR] Ports= (.netdev). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#AlternativeName= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Ports= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1651-L1702 config_parse_ifnames (IFNAME_VALID_ALTERNATIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-hsr.netdev#L7 Ports=test1 dummy98 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-util.c#L29 TEST(ifname_valid) + */ + +/** + * Validator for `[Link] AlternativeName=` (.link) and `[HSR] Ports=` (.netdev). + * + * C function: config_parse_ifnames with ltype IFNAME_VALID_ALTERNATIVE: a whitespace-separated list + * (extract_first_word, no unquoting) of interface names, each checked by ifname_valid_full with + * IFNAME_VALID_ALTERNATIVE — up to 127 printable ASCII characters other than `:`, `/` and `%`, not + * entirely digits, and not `.`, `..`, `all` or `default`. See [ALTERNATIVE_INTERFACE_NAME]. An + * invalid name is logged and skipped; an empty assignment clears the list. + */ +class ConfigParseIfnamesOptionValue : SimpleGrammarOptionValues( + "config_parse_ifnames", + SequenceCombinator( + ALTERNATIVE_INTERFACE_NAME, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), ALTERNATIVE_INTERFACE_NAME)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt new file mode 100644 index 00000000..f8dbf778 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt @@ -0,0 +1,32 @@ +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.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT16_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou + +/* + * [IPoIB] PartitionKey= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PartitionKey= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/ipoib.c#L100-L140 config_parse_ipoib_pkey (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L599-L613 safe_atou16_full (base 0) + */ + +/** + * Validator for `[IPoIB] PartitionKey=` (.netdev). + * + * C function: config_parse_ipoib_pkey. A uint16 read with safe_atou16() (so `0x8001` works, which is + * how partition keys are usually written) in 1…0xffff, except 0x8000 (the full-membership bit with an + * empty key). Spelled as two ranges either side of 0x8000. An empty assignment unsets it; an invalid + * value is logged and ignored by systemd. + */ +class ConfigParseIpoibPkeyOptionValue : SimpleGrammarOptionValues( + "config_parse_ipoib_pkey", + SequenceCombinator( + AlternativeCombinator(safeAtou(0x8000L, 1), safeAtou(UINT16_LIMIT, 0x8001L)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt new file mode 100644 index 00000000..952f80fe --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt @@ -0,0 +1,32 @@ +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.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou + +/* + * [Tunnel] IPv6FlowLabel= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#IPv6FlowLabel= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/tunnel.c#L907-L942 config_parse_ipv6_flowlabel (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2151-L2182 config_parse_unsigned_bounded → safe_atou_bounded + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Tunnel] IPv6FlowLabel=` (.netdev). + * + * C function: config_parse_ipv6_flowlabel. Either `inherit` (copy the inner packet's flow label) or a + * 20-bit flow label 0…0xFFFFF read by config_parse_uint32_bounded (safe_atou, so hex works, as the man + * page's own range notation suggests). An out-of-range value is logged and ignored by systemd. + */ +class ConfigParseIpv6FlowlabelOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_flowlabel", + SequenceCombinator( + AlternativeCombinator(FlexibleLiteralChoiceTerminal("inherit"), safeAtou(0x100000L)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt new file mode 100644 index 00000000..dcecd885 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.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.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou + +/* + * [Link] IRQAffinityNUMA= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinityNUMA= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2157-L2201 config_parse_irq_affinity_numa (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.h#L35-L36 IRQ_AFFINITY_NUMA_UNSET / IRQ_AFFINITY_NUMA_LOCAL + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Link] IRQAffinityNUMA=` (.link). + * + * C function: config_parse_irq_affinity_numa. Either `local` (the device's own NUMA node) or a NUMA + * node number read with safe_atou() (so hex works). UINT_MAX and UINT_MAX - 1 are reserved as the + * "unset" and "local" markers, so the number must be below 4294967294. An empty assignment unsets it. + * An out-of-range number is logged and ignored by udev. + */ +class ConfigParseIrqAffinityNumaOptionValue : SimpleGrammarOptionValues( + "config_parse_irq_affinity_numa", + SequenceCombinator( + AlternativeCombinator(FlexibleLiteralChoiceTerminal("local"), safeAtou(UINT32_LIMIT - 2)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt new file mode 100644 index 00000000..726caac6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt @@ -0,0 +1,26 @@ +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 + +/* + * [Link] IRQAffinityPolicy= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinityPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2209-L2213 config_parse_irq_affinity_policy (0), DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2203-L2206 irq_affinity_policy_table + */ + +/** + * Validator for `[Link] IRQAffinityPolicy=` (.link). + * + * C function: config_parse_irq_affinity_policy, generated by DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT over + * irq_affinity_policy_table: exactly `single` or `spread` (case-sensitive). An empty assignment unsets + * it. An invalid value is logged and ignored by udev. + */ +class ConfigParseIrqAffinityPolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_irq_affinity_policy", + SequenceCombinator(FlexibleLiteralChoiceTerminal("single", "spread"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt new file mode 100644 index 00000000..e2a373de --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [L2TPSession] Name= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Name= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L727-L765 config_parse_l2tp_session_name (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0, via ifname_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-util.c#L29 TEST(ifname_valid) + */ + +/** + * Validator for `[L2TPSession] Name=` (.netdev). + * + * C function: config_parse_l2tp_session_name, which requires ifname_valid(): 1…15 printable ASCII + * characters other than `:`, `/` and `%`, not entirely digits, and not `.`, `..`, `all` or + * `default`. See [INTERFACE_NAME]. An invalid value is logged and ignored by systemd. + */ +class ConfigParseL2tpSessionNameOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_session_name", + SequenceCombinator(INTERFACE_NAME, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt new file mode 100644 index 00000000..d1a04c4c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt @@ -0,0 +1,48 @@ +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.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.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [L2TP] 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/0f0682b773/src/network/netdev/l2tp-tunnel.c#L477-L569 config_parse_l2tp_tunnel_local_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L38-L42 l2tp_local_address_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("@", no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + */ + +/** + * Validator for `[L2TP] Local=` (.netdev). + * + * C function: config_parse_l2tp_tunnel_local_address. `ADDRESS[@INTERFACE]`, where ADDRESS is one of + * the l2tp_local_address_type_table names `auto`, `static`, `dynamic` or an IPv4/IPv6 literal, and the + * optional interface (everything after the first `@`) must pass ifname_valid_full with + * IFNAME_VALID_ALTERNATIVE (up to 127 characters). + * + * The `@` is cut with extract_first_word(…, "@", 0), which coalesces a run of separators, so `auto@@eth0` + * and even a trailing `auto@` (no interface) are accepted. Not modelled: systemd refuses the null + * address and an address family that conflicts with `Remote=`. An invalid value is logged and ignored. + */ +class ConfigParseL2tpTunnelLocalAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_tunnel_local_address", + SequenceCombinator( + AlternativeCombinator( + IPV6_ADDR, + IPV4_ADDR, + FlexibleLiteralChoiceTerminal("auto", "static", "dynamic"), + ), + ZeroOrOne(SequenceCombinator(RegexTerminal("@+", "@+"), ZeroOrOne(ALTERNATIVE_INTERFACE_NAME))), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt new file mode 100644 index 00000000..97aa2f17 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt @@ -0,0 +1,28 @@ +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.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [L2TP] 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/0f0682b773/src/network/netdev/l2tp-tunnel.c#L571-L623 config_parse_l2tp_tunnel_remote_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[L2TP] Remote=` (.netdev). + * + * C function: config_parse_l2tp_tunnel_remote_address: an IPv4 or IPv6 literal (in_addr_from_string_auto + * → inet_pton). Not modelled: systemd also refuses the null address (`0.0.0.0`, `::`) and an address + * whose family differs from an earlier `Local=`; both depend on the parsed value or on other keys. An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseL2tpTunnelRemoteAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_tunnel_remote_address", + SequenceCombinator(IP_ADDR, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt new file mode 100644 index 00000000..d96abc78 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt @@ -0,0 +1,26 @@ +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 + +/* + * [Link] MACAddressPolicy= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#MACAddressPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2145-L2149 config_parse_mac_address_policy (0), DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2138-L2142 mac_address_policy_table + */ + +/** + * Validator for `[Link] MACAddressPolicy=` (.link). + * + * C function: config_parse_mac_address_policy, generated by DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT over + * mac_address_policy_table: exactly one of `persistent`, `random` or `none` (case-sensitive). An empty + * assignment selects the default, `none`. An invalid value is logged and ignored by udev. + */ +class ConfigParseMacAddressPolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_mac_address_policy", + SequenceCombinator(FlexibleLiteralChoiceTerminal("persistent", "random", "none"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt new file mode 100644 index 00000000..9d999f8c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +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.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [MACsecTransmitAssociation] / [MACsecReceiveAssociation] KeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#KeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/macsec.c#L770-L821 config_parse_macsec_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `KeyFile=` in both `[MACsecTransmitAssociation]` and `[MACsecReceiveAssociation]` (.netdev). + * + * C function: config_parse_macsec_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseMacsecKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_macsec_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt new file mode 100644 index 00000000..8b0af0d6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt @@ -0,0 +1,44 @@ +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.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Repeat +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.ZeroOrOne + +/* + * [MACsecTransmitAssociation] / [MACsecReceiveAssociation] KeyId= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#KeyId= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/macsec.c#L823-L879 config_parse_macsec_key_id (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L109-L158 unhexmem_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/if_macsec.h#L23 MACSEC_KEYID_LEN (16) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-macsec.netdev#L12 KeyId=01 … KeyId=0203040506070809 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-hexdecoct.c#L110 TEST(unhexmem) + */ + +// One byte: two hex digits. unhex_next() skips whitespace around every digit, so a space may even +// split a byte. +private val KEY_ID_BYTE = RegexTerminal("[0-9a-fA-F][ \\t]*[0-9a-fA-F]", "[0-9a-fA-F][ \\t]*[0-9a-fA-F]") + +/** + * Validator for `KeyId=` in `[MACsecTransmitAssociation]` and `[MACsecReceiveAssociation]` (.netdev). + * + * C function: config_parse_macsec_key_id, which decodes the value with unhexmem(): an even number of + * hex digits (case-insensitive, whitespace between digits ignored) encoding at most MACSEC_KEYID_LEN = + * 16 bytes, i.e. 2 to 32 digits. Shorter ids are zero-padded. + * + * The man page still says "Takes a number between 0-255", but the parser is hex-only: `KeyId=1` (odd + * length) and `KeyId=255` are refused, while systemd's own fixtures write `KeyId=01` and + * `KeyId=0203040506070809`. This follows the C. An invalid value is logged and ignored by systemd. + */ +class ConfigParseMacsecKeyIdOptionValue : SimpleGrammarOptionValues( + "config_parse_macsec_key_id", + SequenceCombinator( + KEY_ID_BYTE, + Repeat(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), KEY_ID_BYTE), 0, 15), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt new file mode 100644 index 00000000..9c1bf25f --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt @@ -0,0 +1,26 @@ +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 + +/* + * [Link] Port= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Port= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L84 config_parse_port (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L75-L81 port_table + */ + +/** + * Validator for `[Link] Port=` (.link). + * + * C function: config_parse_port, generated by DEFINE_CONFIG_PARSE_ENUM over port_table: exactly one of + * `tp`, `aui`, `mii`, `fibre` or `bnc` (case-sensitive string-table lookup). An invalid value is + * logged and ignored by udev. + */ +class ConfigParsePortOptionValue : SimpleGrammarOptionValues( + "config_parse_port", + SequenceCombinator(FlexibleLiteralChoiceTerminal("tp", "aui", "mii", "fibre", "bnc"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt new file mode 100644 index 00000000..7ed65c15 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt @@ -0,0 +1,30 @@ +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.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT16_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseRange +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou + +/* + * [VXLAN] PortRange= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PortRange= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vxlan.c#L299-L324 config_parse_port_range (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L736-L760 parse_ip_port_range (allow_zero=false) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + */ + +/** + * Validator for `[VXLAN] PortRange=` (.netdev). + * + * C function: config_parse_port_range → parse_ip_port_range(…, allow_zero = false): a single port or a + * `low-high` range, each bound a safe_atou() number (so hex works) in 1…65535. A range with + * `high < low` is refused by systemd after parsing; that compares numbers and is not modelled. An + * invalid value is logged and ignored by systemd. + */ +class ConfigParsePortRangeOptionValue : SimpleGrammarOptionValues( + "config_parse_port_range", + SequenceCombinator(parseRange(safeAtou(UINT16_LIMIT, 1)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt new file mode 100644 index 00000000..ad9921de --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt @@ -0,0 +1,33 @@ +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.CPU_SET +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 + +/* + * [Link] ReceivePacketSteeringCPUMask= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#ReceivePacketSteeringCPUMask= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2095-L2136 config_parse_rps_cpu_mask (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L220-L307 config_parse_cpu_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-rps-cpu-multi.link#L6-L7 ReceivePacketSteeringCPUMask=0 1 / =2,3 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/24-rps-cpu-invalid.link#L6 ReceivePacketSteeringCPUMask=0 3 8-invalid (deliberately invalid) + */ + +/** + * Validator for `[Link] ReceivePacketSteeringCPUMask=` (.link). + * + * C function: config_parse_rps_cpu_mask. The whole value `disable` (an empty mask) or `all` (every + * online CPU), compared with streq, or else a CPU set exactly as config_parse_cpu_set reads it — see + * [CPU_SET]. An empty assignment clears the mask. An invalid word is logged and skipped by systemd. + */ +class ConfigParseRpsCpuMaskOptionValue : SimpleGrammarOptionValues( + "config_parse_rps_cpu_mask", + AlternativeCombinator( + SequenceCombinator(FlexibleLiteralChoiceTerminal("disable", "all"), EOF()), + SequenceCombinator(CPU_SET, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt new file mode 100644 index 00000000..de92c7e8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt @@ -0,0 +1,90 @@ +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 +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 + +/* + * [Link] Property= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Property= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L1810-L1878 config_parse_udev_property (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/specifier.c#L33-L105 specifier_printf (link_specifier_table) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L81-L102 env_assignment_is_valid (via udev_property_assignment_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-device/device-util.c#L219-L237 device_property_can_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/85-unmanaged.link#L18 Property=ID_NET_MANAGED_BY=io.systemd.Network + */ + +// The properties udev reserves for itself (device_property_can_set), refused as assignment targets, plus +// the kernel's SYNTH_ARG_ prefix. The trailing lookahead makes each reserved word match only as the whole +// name, so `DEVPATHX` or `TAGS_EXTRA` are still fine. +private const val RESERVED_PROPERTY = + """(?!(?:ACTION|SEQNUM|SYNTH_UUID|DEVPATH|DEVPATH_OLD|SUBSYSTEM|DEVTYPE|DRIVER|MODALIAS|DEVNAME|DEVMODE|""" + + """DEVUID|DEVGID|MAJOR|MINOR|DISKSEQ|PARTN|IFINDEX|INTERFACE|INTERFACE_OLD|DEVLINKS|TAGS|CURRENT_TAGS|""" + + """USEC_INITIALIZED|UDEV_DATABASE_VERSION)(?![A-Za-z0-9_%]))(?!SYNTH_ARG_)""" + +// A link_specifier_table specifier (COMMON_SYSTEM_SPECIFIERS + COMMON_TMP_SPECIFIERS). `%%` is left out on +// purpose: it expands to a literal `%`, which no property name may contain. +private const val SPECIFIER = """%[aAbBHlqmMovwWTV]""" + +// The property name: env_name_is_valid — bash-style, i.e. letters, digits and `_`, not starting with a +// digit. Specifiers are expanded before the check, so one may appear in the name too. +private val PROPERTY_NAME = RegexTerminal( + """[A-Za-z0-9_%]+""", + """$RESERVED_PROPERTY(?:[A-Za-z_]|$SPECIFIER)(?:[A-Za-z0-9_]|$SPECIFIER)*""", +) + +private val EQUALS = LiteralChoiceTerminal("=") + +// The value, in its raw (still-quoted, still-escaped) form. env_value_is_valid only requires valid UTF-8, +// so any character is fine; whitespace ends the word unless it is quoted or backslash-escaped. +private val RAW_VALUE = RegexTerminal( + """(?:[^\s"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""", + """(?:[^\s"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""", +) +private val DOUBLE_QUOTED_INNER = RegexTerminal("""(?:[^"\\]|\\[\s\S])+""", """(?:[^"\\]|\\[\s\S])+""") +private val SINGLE_QUOTED_INNER = RegexTerminal("""(?:[^'\\]|\\[\s\S])+""", """(?:[^'\\]|\\[\s\S])+""") + +private fun quotedAssignment(quote: String, inner: RegexTerminal) = SequenceCombinator( + LiteralChoiceTerminal(quote), PROPERTY_NAME, EQUALS, ZeroOrOne(inner), LiteralChoiceTerminal(quote), ZeroOrOne(RAW_VALUE), +) + +// One word: NAME=VALUE, either plain (the value may still contain quoted runs) or with the whole +// assignment quoted from the start of the name. +private val PROPERTY_ASSIGNMENT = AlternativeCombinator( + quotedAssignment("\"", DOUBLE_QUOTED_INNER), + quotedAssignment("'", SINGLE_QUOTED_INNER), + SequenceCombinator(PROPERTY_NAME, EQUALS, ZeroOrOne(RAW_VALUE)), +) + +/** + * Validator for `[Link] Property=` (.link). + * + * C function: config_parse_udev_property. A whitespace-separated list of `NAME=VALUE` udev property + * assignments. Each word is split with extract_first_word(…, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE), so it may + * be quoted (`"FOO=bar baz"` or `FOO="bar baz"`) or contain C escapes; specifiers from the link specifier + * table (`%H`, `%v`, …) are expanded; then the assignment must pass env_assignment_is_valid — a + * bash-style name (letters, digits, `_`, not starting with a digit), `=`, and any UTF-8 value, which may be + * empty — and the name must not be one udev reserves (device_property_can_set: `ACTION`, `DEVPATH`, + * `INTERFACE`, `TAGS`, …, or anything starting `SYNTH_ARG_`). + * + * Lenient corners: the escapes inside a value are not checked against cunescape_one's list (an unknown + * escape such as `\q` makes systemd ignore the whole assignment), quotes that open in the middle of a + * name are not modelled, and an unknown `%` specifier is not flagged. An invalid word is logged and + * skipped by udev; an empty assignment clears the list. + */ +class ConfigParseUdevPropertyOptionValue : SimpleGrammarOptionValues( + "config_parse_udev_property", + SequenceCombinator( + PROPERTY_ASSIGNMENT, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), PROPERTY_ASSIGNMENT)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt new file mode 100644 index 00000000..66d23a8f --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.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.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +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.optionallyQuoted +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseRange +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou + +/* + * [VLAN] EgressQOSMaps= / IngressQOSMaps= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#EgressQOSMaps= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vlan.c#L120-L180 config_parse_vlan_qos_maps (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +private val QOS_MAPPING = optionallyQuoted(parseRange(safeAtou(UINT32_LIMIT))) + +/** + * Validator for `[VLAN] EgressQOSMaps=` and `IngressQOSMaps=` (.netdev). + * + * C function: config_parse_vlan_qos_maps. A whitespace-separated list of words (each may be quoted); + * every word goes to parse_range(), so it is `from-to` or a single number meaning `from == to`, each + * side an unsigned int as safe_atou() reads it (decimal, `0x` hex, octal, `0b`/`0o`). + * + * The man page says each integer is in 1…4294967294 and that `from` must be >= `to`; the parser + * enforces neither (it stores whatever parse_range returns), so this follows the C and accepts the full + * unsigned range and any order. A malformed word is logged and skipped by systemd. + */ +class ConfigParseVlanQosMapsOptionValue : SimpleGrammarOptionValues( + "config_parse_vlan_qos_maps", + SequenceCombinator(QOS_MAPPING, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), QOS_MAPPING)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt new file mode 100644 index 00000000..1eafd4bc --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt @@ -0,0 +1,26 @@ +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 + +/* + * [VLAN] Protocol= and [Bridge] VLANProtocol= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Protocol= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1832-L1862 config_parse_vlanprotocol (0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-bridge.netdev#L17 + */ + +/** + * Validator for `[VLAN] Protocol=` and `[Bridge] VLANProtocol=` (.netdev). + * + * C function: config_parse_vlanprotocol, which compares the value with STR_IN_SET against exactly four + * spellings: `802.1q` / `802.1Q` (ETH_P_8021Q) and `802.1ad` / `802.1AD` (ETH_P_8021AD). Mixed case + * such as `802.1Ad` is refused. An empty assignment resets to the kernel default. + */ +class ConfigParseVlanprotocolOptionValue : SimpleGrammarOptionValues( + "config_parse_vlanprotocol", + SequenceCombinator(FlexibleLiteralChoiceTerminal("802.1q", "802.1Q", "802.1ad", "802.1AD"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt new file mode 100644 index 00000000..068ffd88 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt @@ -0,0 +1,30 @@ +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.ROUTE_TABLE + +/* + * [VRF] Table= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Table= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vrf.c#L10-L39 config_parse_vrf_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + */ + +/** + * Validator for `[VRF] Table=` (.netdev). + * + * C function: config_parse_vrf_table → manager_get_route_table_from_string: `default`, `main`, + * `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 (safe_atou32, so hex + * works; `0` is refused with ERANGE). See [ROUTE_TABLE]. + * + * Because table names are user-defined, any word without whitespace or `:` that is not all digits is + * accepted as a potential name. What is flagged: `0` and out-of-range numbers, values containing + * whitespace, and `name:number` pairs. An invalid value is logged and ignored by systemd. + */ +class ConfigParseVrfTableOptionValue : SimpleGrammarOptionValues( + "config_parse_vrf_table", + ROUTE_TABLE +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt new file mode 100644 index 00000000..a2616521 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt @@ -0,0 +1,45 @@ +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 + +/* + * [VXLAN] Group= / Local= / Remote= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Local= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vxlan.c#L220-L297 config_parse_vxlan_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/netdev-util.c#L9-L16 netdev_local_address_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[VXLAN] Group=`, `Local=` and `Remote=` (.netdev). + * + * C function: config_parse_vxlan_address. The value is an IPv4 or IPv6 literal + * (in_addr_from_string_auto → inet_pton). For `Local=` only, one of the netdev_local_address_type_table + * names (`ipv4_link_local`, `ipv6_link_local`, `dhcp4`, `dhcp6`, `slaac`, `dhcp_pd`) is accepted + * first, telling networkd to pick up the address that mechanism assigned. + * + * One grammar serves all three keys because validators are keyed by (parser, ltype), not by key, so + * this is deliberately lenient in two ways that depend on the key name: the local-address type names + * are accepted for `Group=`/`Remote=` too, and the multicast checks (`Group=` must be multicast, + * `Local=`/`Remote=` must not be) are not modelled. Addresses come before the names so the + * name terminal's loose shape cannot swallow a leading fragment of an address. + */ +class ConfigParseVxlanAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_vxlan_address", + SequenceCombinator( + AlternativeCombinator( + IPV6_ADDR, + IPV4_ADDR, + FlexibleLiteralChoiceTerminal("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/ConfigParseWireguardAllowedIpsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt new file mode 100644 index 00000000..117711c1 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt @@ -0,0 +1,42 @@ +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.IP_ADDR_AND_ANY_PREFIX +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.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [WireGuardPeer] AllowedIPs= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#AllowedIPs= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L673-L749 config_parse_wireguard_allowed_ips (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("," WHITESPACE, no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L845-L897 in_addr_prefix_from_string_auto_full + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L15 AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24 + */ + +// One extract_first_word() separator run; "," and whitespace both separate and runs coalesce. +private val ALLOWED_IPS_SEPARATOR = RegexTerminal("[\\s,]+", "[\\s,]+") + +/** + * Validator for `[WireGuardPeer] AllowedIPs=` (.netdev). + * + * C function: config_parse_wireguard_allowed_ips. A list of IPv4/IPv6 addresses with optional prefix + * lengths (in_addr_prefix_from_string_auto: a missing length means the full width, `/0` is allowed), + * separated by commas and/or whitespace. Separators coalesce, and leading or trailing ones are harmless. + * An address that is not properly masked (`192.168.26.3/24`) is accepted and masked with a warning. + * An invalid entry is logged and skipped by systemd; an empty assignment clears the list. + */ +class ConfigParseWireguardAllowedIpsOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_allowed_ips", + SequenceCombinator( + ZeroOrOne(ALLOWED_IPS_SEPARATOR), + IP_ADDR_AND_ANY_PREFIX, + ZeroOrMore(SequenceCombinator(ALLOWED_IPS_SEPARATOR, IP_ADDR_AND_ANY_PREFIX)), + ZeroOrOne(ALLOWED_IPS_SEPARATOR), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt new file mode 100644 index 00000000..5369be46 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt @@ -0,0 +1,70 @@ +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.CREDENTIAL_REFERENCE +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.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT16_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou + +/* + * [WireGuardPeer] Endpoint= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Endpoint= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L751-L862 config_parse_wireguard_endpoint (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L220-L354 in_addr_port_ifindex_name_from_string_auto (no ifindex, no server name) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L718-L734 parse_ip_port + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape (via dns_name_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L156-L180 read_credential (credential_name_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-netlink.c#L356 TEST(in_addr_port_ifindex_name_from_string_auto) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L17 Endpoint=@network.wireguard.peer0.endpoint + */ + +// parse_ip_port: a safe_atou16 number (base 0), not zero. +private val ENDPOINT_PORT = safeAtou(UINT16_LIMIT, 1) + +// The hostname fallback: everything up to the LAST colon (strrchr), checked with dns_name_is_valid(): +// dot-separated labels of at most 63 characters, each character printable (>= ' ', not DEL) or one of +// the escapes `\.`, `\\` or `\DDD`. A single trailing dot is allowed, and dns_name_is_valid() also +// accepts the root name — `.` or nothing at all — so `:51820` passes the parser too. +private const val DNS_LABEL = """(?:[^.\\\x00-\x1F\x7F]|\\[.\\]|\\[0-9]{3}){1,63}""" +private val ENDPOINT_HOST = RegexTerminal( + """[^\x00-\x1F\x7F]+(?=:[^:]*$)""", + """(?:$DNS_LABEL(?:\.$DNS_LABEL)*\.?|\.)(?=:[^:]*$)""", +) + +/** + * Validator for `[WireGuardPeer] Endpoint=` (.netdev). + * + * C function: config_parse_wireguard_endpoint. In the order systemd tries them: + * + * - `@name` reads the endpoint from a credential; + * - in_addr_port_ifindex_name_from_string_auto with no interface and no server name: `[IPv6]:port`, + * `IPv4:port`, or — because that helper makes the port optional — a bare IPv4 or IPv6 address (the + * man page says a port is required; the parser stores port 0); + * - otherwise `host:port`, split at the last colon, where the host only has to pass + * dns_name_is_valid(), which is permissive (any printable characters, labels up to 63 bytes). + * + * The port is a parse_ip_port() number, 1…65535 (safe_atou16 base 0, so hex works). Because the host + * fallback is so permissive, something like `[fe80::1%eth0]:51820` is accepted (as a hostname), as it + * is by systemd at parse time. What is flagged: a missing or invalid port, empty labels, over-long labels + * and control characters. An invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardEndpointOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_endpoint", + AlternativeCombinator( + SequenceCombinator(CREDENTIAL_REFERENCE, EOF()), + SequenceCombinator(LiteralChoiceTerminal("["), IPV6_ADDR, LiteralChoiceTerminal("]"), COLON, ENDPOINT_PORT, EOF()), + SequenceCombinator(IPV4_ADDR, COLON, ENDPOINT_PORT, EOF()), + SequenceCombinator(IPV6_ADDR, EOF()), + SequenceCombinator(IPV4_ADDR, EOF()), + SequenceCombinator(ZeroOrOne(ENDPOINT_HOST), COLON, ENDPOINT_PORT, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt new file mode 100644 index 00000000..daf299e9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +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.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuardPeer] PublicKeyFile= / PresharedKeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PublicKeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L623-L671 config_parse_wireguard_peer_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `[WireGuardPeer] PublicKeyFile=` and `PresharedKeyFile=` (.netdev). + * + * C function: config_parse_wireguard_peer_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseWireguardPeerKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_peer_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt new file mode 100644 index 00000000..b5d35631 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.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.BOOLEAN_FALSE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuardPeer] RouteTable= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#RouteTable= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L941-L990 config_parse_wireguard_peer_route_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L10 + */ + +/** + * Validator for `[WireGuardPeer] RouteTable=` (.netdev). + * + * C function: config_parse_wireguard_peer_route_table. A value that parse_boolean() reads as false (`no`, `off`, `0`, …) + * disables route configuration; anything else is a route table for manager_get_route_table_from_string: + * `default`, `main`, `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 + * (safe_atou32, so hex works). See [ROUTE_TABLE]. An empty assignment falls back to the `[WireGuard]` section's table. + * + * Because table names are user-defined, any word without whitespace or `:` that is not all digits is + * accepted as a potential name — including `yes`, which is only valid if networkd.conf defines it. + * What is flagged: out-of-range numbers, values containing whitespace, and `name:number` pairs (that is + * networkd.conf syntax, not a table reference). An invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardPeerRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_peer_route_table", + AlternativeCombinator( + SequenceCombinator(BOOLEAN_FALSE, EOF()), + ROUTE_TABLE, + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt new file mode 100644 index 00000000..4e818fc7 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +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.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuard] PrivateKeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PrivateKeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L563-L591 config_parse_wireguard_private_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `[WireGuard] PrivateKeyFile=` (.netdev). + * + * C function: config_parse_wireguard_private_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseWireguardPrivateKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_private_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt new file mode 100644 index 00000000..ea68413d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.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.CREDENTIAL_REFERENCE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WIREGUARD_BASE64_KEY + +/* + * [WireGuard] PrivateKey= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PrivateKey= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L546-L561 config_parse_wireguard_private_key (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L488-L544 wireguard_decode_key_and_warn + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755-L851 unbase64mem_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L528-L555 unbase64char (also accepts the URL-safe '-' and '_') + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L156-L180 read_credential (credential_name_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L12 + */ + +/** + * Validator for `[WireGuard] PrivateKey=` (.netdev). + * + * C function: config_parse_wireguard_private_key → wireguard_decode_key_and_warn. Either `@name`, + * which reads the key from the named credential (the name must pass credential_name_valid), or the key + * itself in base64, which must decode to exactly 32 bytes: 43 characters plus one mandatory `=` of + * padding, the 43rd character restricted to those whose low two bits are clear. See + * [WIREGUARD_BASE64_KEY]. An invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardPrivateKeyOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_private_key", + SequenceCombinator(AlternativeCombinator(CREDENTIAL_REFERENCE, WIREGUARD_BASE64_KEY), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt new file mode 100644 index 00000000..7ab5147f --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.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.BOOLEAN_FALSE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuard] RouteTable= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#RouteTable= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L905-L939 config_parse_wireguard_route_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L10 + */ + +/** + * Validator for `[WireGuard] RouteTable=` (.netdev). + * + * C function: config_parse_wireguard_route_table. A value that parse_boolean() reads as false (`no`, `off`, `0`, …) + * disables route configuration; anything else is a route table for manager_get_route_table_from_string: + * `default`, `main`, `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 + * (safe_atou32, so hex works). See [ROUTE_TABLE]. An empty assignment also disables it. + * + * Because table names are user-defined, any word without whitespace or `:` that is not all digits is + * accepted as a potential name — including `yes`, which is only valid if networkd.conf defines it. + * What is flagged: out-of-range numbers, values containing whitespace, and `name:number` pairs (that is + * networkd.conf syntax, not a table reference). An invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_route_table", + AlternativeCombinator( + SequenceCombinator(BOOLEAN_FALSE, EOF()), + ROUTE_TABLE, + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt new file mode 100644 index 00000000..761800da --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt @@ -0,0 +1,35 @@ +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.MAC_ADDRESS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SAFE_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] WakeOnLanPassword= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#WakeOnLanPassword= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2050-L2093 config_parse_wol_password (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L134-L155 link_parse_wol_password → parse_ether_addr + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/ether-addr-util.c#L282-L295 parse_ether_addr (parse_hw_addr_full, ETH_ALEN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.h#L152-L154 path_is_safe + */ + +/** + * Validator for `[Link] WakeOnLanPassword=` (.link). + * + * C function: config_parse_wol_password. If the value is an absolute path that path_is_safe() accepts + * (no `..` component) it names a file to read the password from; otherwise it must be the 6-byte + * SecureOn password written like an Ethernet address (parse_ether_addr: `00:11:22:33:44:55`, + * `00-11-22-33-44-55` or `0011.2233.4455`). The path is taken verbatim (no specifiers, spaces are + * ordinary characters). An invalid value is logged and ignored by systemd. + */ +class ConfigParseWolPasswordOptionValue : SimpleGrammarOptionValues( + "config_parse_wol_password", + AlternativeCombinator( + SequenceCombinator(MAC_ADDRESS, EOF()), + SequenceCombinator(SAFE_ABSOLUTE_PATH, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt new file mode 100644 index 00000000..b72dc3fc --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -0,0 +1,287 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigInteger + +/* + * Shared grammars for the systemd-networkd .netdev (src/network/netdev/) and udev .link (src/udev/net/) + * validators added in the 2026-10 netdev-link batch. + * + * Everything here is pinned to systemd 0f0682b773, the commit recorded in + * systemd-build/build/last_commit_hash. Browse it at https://github.com/systemd/systemd/blob/0f0682b773. + * + * These live in their own file (rather than Combinators.kt) only because several batches were written + * in parallel. Several are general-purpose and are good candidates for promotion — see the batch's + * RECOMMENDATIONS.md. + */ + +// --------------------------------------------------------------------------------------------------- +// safe_atou() and friends — src/basic/parse-util.c (safe_atou_full, mangle_base). + +/** + * An unsigned number exactly as systemd's `safe_atou*()` helpers read it with base 0. + * + * Compared with [UnsignedNumberTerminal] this also accepts the two spellings that helper family adds + * on top of strtoul(): the Python-style `0b`/`0o` prefixes that mangle_base() strips before calling + * strtoul (so `0b101` and `0o17` are legal), and a leading `+`, which strtoul accepts and + * safe_atou_full only refuses when SAFE_ATO_REFUSE_PLUS_MINUS is set (none of the callers here set it). + * + * Hex (`0x`/`0X`) and C octal (leading `0`) are accepted as for [UnsignedNumberTerminal]; a digit + * string like `089` is number-shaped but not a valid octal literal, so it matches and is then rejected + * — which is what strtoul()+`*x != 0` does too. + * + * Not accepted: `-0`. safe_atou_full lets a negative zero through (`l != 0 && s[0] == '-'` is the only + * minus check), but no real configuration spells zero that way and accepting a leading `-` would blur + * the error on every genuinely negative value. + * + * @param minInclusive lowest accepted value + * @param maxExclusive one past the highest accepted value + */ +class SafeAtouTerminal( + 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 is not a valid literal in its base. */ + private fun valueOf(text: String): BigInteger? { + // mangle_base() only recognises 0b/0o at the very start, so they never carry a sign. + if (text.length > 2 && (text.startsWith("0b") || text.startsWith("0B"))) return text.drop(2).toBigIntegerOrNull(2) + if (text.length > 2 && (text.startsWith("0o") || text.startsWith("0O"))) return text.drop(2).toBigIntegerOrNull(8) + val digits = text.removePrefix("+") + return when { + digits.length > 2 && (digits.startsWith("0x") || digits.startsWith("0X")) -> digits.drop(2).toBigIntegerOrNull(16) + digits.length > 1 && digits.startsWith("0") -> digits.drop(1).toBigIntegerOrNull(8) + else -> digits.toBigIntegerOrNull() + } + } + + private fun inRange(text: String): Boolean = + valueOf(text)?.let { it >= BigInteger.valueOf(minInclusive) && it < BigInteger.valueOf(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))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, inRange(text))))) + } + + override fun toString(): String = "SafeAtou($minInclusive,$maxExclusive)" + + private companion object { + // 0b/0o first: as plain digits they would otherwise be read as "0" followed by junk. Within the + // signed group, hex before octal before decimal for the same reason. + val SHAPE = Regex("""0[bB][01]+|0[oO][0-7]+|\+?(?:0[xX][0-9a-fA-F]+|0[0-7]+|[0-9]+)""") + } +} + +/** Shorthand for a [SafeAtouTerminal] over `[minInclusive, maxExclusive)`. */ +fun safeAtou(maxExclusive: Long, minInclusive: Long = 0L): Combinator = SafeAtouTerminal(minInclusive, maxExclusive) + +/** Exclusive upper bound of a uint16_t. */ +const val UINT16_LIMIT = 65_536L + +/** Exclusive upper bound of a uint32_t / unsigned. */ +const val UINT32_LIMIT = 4_294_967_296L + +/** + * `N` or `N-M` as parse_range() (src/basic/parse-util.c) reads it: the value is split on the first + * `-` (EXTRACT_DONT_COALESCE_SEPARATORS, so `-5` and `5-` both fail) and each side goes through + * safe_atou(). The `lower <= upper` checks that callers make afterwards compare parsed numbers and are + * not modelled. + */ +fun parseRange(bound: Combinator): Combinator = SequenceCombinator(bound, ZeroOrOne(SequenceCombinator(HYPHEN, bound))) + +/** + * [inner] optionally wrapped in a matching pair of `"…"` or `'…'`, the form a word takes after + * extract_first_word(…, EXTRACT_UNQUOTE) has removed its quotes (systemd's own test-cpu-set-util.c + * writes `8 '9' 10 "11"`). Only whole-word quoting is modelled; quotes that start mid-word are not. + */ +fun optionallyQuoted(inner: Combinator): Combinator = AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), inner, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), inner, LiteralChoiceTerminal("'")), + inner, +) + + +// --------------------------------------------------------------------------------------------------- +// CPU sets — config_parse_cpu_set (src/shared/cpu-set-util.c). + +/** The largest CPU index cpu_set_add_range() accepts is CPU_SET_MAX_NCPU - 1 = 8191. */ +private const val CPU_SET_MAX_NCPU = 8192L + +// One extract_first_word() separator run: any mix of whitespace and commas (separators coalesce). +private val CPU_SET_SEPARATOR = RegexTerminal("""[\s,]+""", """[\s,]+""") + +private val CPU_SET_ITEM = optionallyQuoted(parseRange(safeAtou(CPU_SET_MAX_NCPU))) + +/** + * A CPU set: CPU indices and `N-M` ranges separated by whitespace and/or commas. + * + * config_parse_cpu_set splits with extract_first_word(…, WHITESPACE ",", EXTRACT_UNQUOTE), so separators + * coalesce, leading and trailing separators are harmless (`0, 1, 2, 3, 4, 5, 6, 7, 63, ` is in + * test-cpu-set-util.c), and a word may be quoted. Each word goes to parse_range(), whose bounds are + * safe_atou() numbers — so hex works — and cpu_set_add_range() refuses anything at or above + * CPU_SET_MAX_NCPU (8192). A range whose lower end exceeds its upper end (`3-0`) is rejected by systemd + * after parsing; that compares numbers and is not modelled. + */ +val CPU_SET: Combinator = SequenceCombinator( + ZeroOrOne(CPU_SET_SEPARATOR), + CPU_SET_ITEM, + ZeroOrMore(SequenceCombinator(CPU_SET_SEPARATOR, CPU_SET_ITEM)), + ZeroOrOne(CPU_SET_SEPARATOR), +) + + +// --------------------------------------------------------------------------------------------------- +// Sizes — parse_size() (src/basic/parse-util.c). + +// strtoull(…, 10) after parse_size has skipped whitespace: an optional `+` (a `-` is refused), digits, +// then an optional `.` and fraction. parse_size steps over the `.` unconditionally and ignores a +// missing fraction ("E.g. 10.M is valid"), so `10.` and `10.M` are both legal. +private val SIZE_NUMBER = RegexTerminal("""\+?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""") + +/** + * A parse_size() value with base 1000 (the SI table: `E`, `P`, `T`, `G`, `M`, `K`, `B` and bare). + * + * parse_size accepts a run of `number[suffix]` elements whose suffixes strictly descend through the + * table (`start_pos = i + 1`), with optional whitespace before each number and between a number and its + * suffix. The bare (suffix-less) element is the last table entry, so it can only come last. That + * ordering is spelled out structurally: [sizeChain] for table index `i` offers every suffix from `i` + * onwards, each optionally followed by a chain starting after it. + * + * Suffixes are case-sensitive: `1k` is not a size. + */ +fun siSize(): Combinator = sizeChain(listOf("E", "P", "T", "G", "M", "K", "B")) + +private fun sizeChain(suffixes: List): Combinator { + val ws = ZeroOrOne(WhitespaceTerminal()) + // Built from the last suffix backwards so each chain reuses the shorter one after it. + var chainAfter: Combinator? = null + val chains = ArrayList() + // The bare element (no suffix) is always the final alternative of every chain. + val bare: Combinator = SequenceCombinator(ws, SIZE_NUMBER) + for (i in suffixes.indices.reversed()) { + val element = SequenceCombinator(ws, SIZE_NUMBER, ws, LiteralChoiceTerminal(suffixes[i])) + val withRest = if (chainAfter == null) SequenceCombinator(element, ZeroOrOne(bare)) + else SequenceCombinator(element, ZeroOrOne(chainAfter)) + chains.add(0, withRest) + chainAfter = AlternativeCombinator(*chains.toTypedArray(), bare) + } + return chainAfter ?: bare +} + + +// --------------------------------------------------------------------------------------------------- +// Durations — parse_nsec() (src/basic/time-util.c). + +// strtoll(…, 10) as parse_nsec drives it: an optional `+` (a leading `-` is refused), then digits with +// an optional fraction — or a bare fraction like `.5`, which parse_nsec accepts because it checks for +// `.` before it checks whether strtoll consumed anything. A trailing `.` with no digits (`3.`) is refused. +private val DURATION_NUMBER = RegexTerminal("""\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""", """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""") + +/** extract_nsec_multiplier's table (minus its empty default entry, which is the bare-number case). */ +private val NSEC_SUFFIX = LiteralChoiceTerminal( + "seconds", "second", "sec", "s", "minutes", "minute", "min", "months", "month", "M", "msec", "ms", "m", + "hours", "hour", "hr", "h", "days", "day", "d", "weeks", "week", "w", "years", "year", "y", + "usec", "us", "μs", "µs", "nsec", "ns", +) + +/** + * A parse_nsec() duration WITHOUT the `infinity` spelling: one or more `number[unit]` elements, an + * element without a unit meaning nanoseconds. Whitespace may separate elements and may sit between a + * number and its unit; an element with a unit may be followed directly by the next one (`1s500ms`). + * + * Lenient on one corner: parse_nsec refuses a bare number followed immediately by a `.` fraction + * (`12.34.56`), which this grammar would read as `12.34` + `.56`. + */ +val NSEC_DURATION_FINITE: Combinator = run { + val ws = ZeroOrOne(WhitespaceTerminal()) + val element = AlternativeCombinator( + SequenceCombinator(DURATION_NUMBER, ws, NSEC_SUFFIX), + DURATION_NUMBER, + ) + SequenceCombinator(element, ZeroOrMore(SequenceCombinator(ws, element))) +} + + +// --------------------------------------------------------------------------------------------------- +// Paths. + +// Rejects a `..` component anywhere (see NO_DOT_DOT in Combinators.kt). +private const val NO_DOT_DOT_COMPONENT = """(?!(?:[\s\S]*/)?\.\.(?:/|$))""" + +/** + * An absolute path checked by path_simplify_and_warn(…, PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * (src/shared/parse-helpers.c), taken verbatim — no specifier expansion, no unquoting — as the netdev + * `*KeyFile=` settings do. After path_simplify() the path must be normalized (so no `..` component) and + * must not be at or below one of the API VFS mounts `/dev`, `/sys`, `/proc` (path_below_api_vfs, which + * compares whole components, so `/devices` is fine). Spaces are ordinary path characters. + */ +val NON_API_VFS_ABSOLUTE_PATH = RegexTerminal( + """[\s\S]+""", + """(?!/+(?:dev|sys|proc)(?:/|$))$NO_DOT_DOT_COMPONENT/[\s\S]*""", +) + +/** + * An absolute path accepted by path_is_absolute() && path_is_safe() (src/basic/path-util.h): it starts + * with `/` and has no `..` component. Taken verbatim. + */ +val SAFE_ABSOLUTE_PATH = RegexTerminal( + """/[\s\S]*""", + """$NO_DOT_DOT_COMPONENT/[\s\S]*""", +) + + +// --------------------------------------------------------------------------------------------------- +// WireGuard keys and credentials — wireguard_decode_key_and_warn (src/network/netdev/wireguard.c) → +// unbase64mem_full (src/basic/hexdecoct.c). + +/** + * A 32-byte WireGuard key in base64, as unbase64mem_full() decodes it. + * + * 32 bytes is ten full 4-character groups plus a final 3-character group padded with one `=`; the + * padding is mandatory — without it unbase64_next() runs out of input mid-group and fails. The third + * character of a padded group must have its low two bits clear (`c & 3`), which limits the 43rd + * character to `AEIMQUYcgkosw048`. unbase64char() accepts the URL-safe alphabet too (`-` for `+`, + * `_` for `/`), and unbase64_next() skips whitespace anywhere in the input. + */ +val WIREGUARD_BASE64_KEY = RegexTerminal( + """[A-Za-z0-9+/_=\-]+(?:[ \t]+[A-Za-z0-9+/_=\-]+)*""", + """(?:[A-Za-z0-9+/_\-][ \t]*){42}[AEIMQUYcgkosw048][ \t]*=""", +) + +/** + * `@name` — a credential reference resolved by read_credential(), which refuses any name that fails + * credential_name_valid() (src/shared/creds-util.c). [CONDITION_CREDENTIAL] models that check. + */ +val CREDENTIAL_REFERENCE: Combinator = SequenceCombinator(LiteralChoiceTerminal("@"), CONDITION_CREDENTIAL) + + +// --------------------------------------------------------------------------------------------------- +// Route tables — manager_get_route_table_from_string (src/network/networkd-route-util.c). + +// Names an administrator can define with RouteTable=name:number in networkd.conf. That parser splits +// words on whitespace, cuts each at the first `:`, and refuses an empty or all-digit name; any other +// spelling is a potential name, so a non-numeric word here cannot be checked against a closed set. +private val ROUTE_TABLE_NAME = RegexTerminal("""[^\s:]+""", """(?![0-9]+$)[^\s:]+""") + +/** + * A route table: one of the predefined names `default`, `main`, `local`, a name defined in + * networkd.conf, or a non-zero uint32 (safe_atou32, so hex works). Each branch carries its own EOF so + * the classic first-match engine can fall back from the numeric branch to the name branch on `1abc`. + */ +val ROUTE_TABLE: Combinator = AlternativeCombinator( + SequenceCombinator(FlexibleLiteralChoiceTerminal("default", "main", "local"), EOF()), + SequenceCombinator(safeAtou(UINT32_LIMIT, 1), EOF()), + SequenceCombinator(ROUTE_TABLE_NAME, EOF()), +) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt new file mode 100644 index 00000000..e8af0df8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt @@ -0,0 +1,44 @@ +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 + +class ConfigParseAdvertiseOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Advertise=1000baset-full + Advertise=10baset-half 100baset-full autonegotiation pause asym-pause + Advertise=10000baser-fec fec-rs + Advertise=1600000basedr8-2-full + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Advertise=hoge foo + Advertise=1000BaseT-Full + Advertise=autoneg + Advertise=1000baset-full,100baset-full + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt new file mode 100644 index 00000000..ee8e4a8a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt @@ -0,0 +1,59 @@ +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 + +class ConfigParseBadadvBandwidthOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [BatmanAdvanced] + GatewayBandwidthDown=205M + GatewayBandwidthUp=2G + GatewayBandwidthDown=1000 + GatewayBandwidthUp=1.5G + GatewayBandwidthDown=10.M + GatewayBandwidthUp=3. K + GatewayBandwidthDown=4 M 11.5K + GatewayBandwidthUp=3.5G3B + GatewayBandwidthDown=4T3G3B + GatewayBandwidthUp=+5K + GatewayBandwidthDown=12P + GatewayBandwidthUp=3E 2P + GatewayBandwithDown=100K + GatewayBandwithUp=1T + GatewayBandwidthDown=111.4 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BatmanAdvanced] + GatewayBandwidthDown=5k + GatewayBandwidthUp=-1 + GatewayBandwidthDown=12X + GatewayBandwidthUp=3B3.5G + GatewayBandwidthDown=12P12P + GatewayBandwidthUp=3. 0 K + GatewayBandwidthDown=fast + GatewayBandwithUp=1 Mbit + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt new file mode 100644 index 00000000..e22f59a9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt @@ -0,0 +1,52 @@ +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 + +class ConfigParseCoalesceNsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RxCoalesceCQENSec=500 + RxCoalesceCQENSec=1us + RxCoalesceCQENSec=2.5 + RxCoalesceCQENSec=1s500ms + RxCoalesceCQENSec=.7 + RxCoalesceCQENSec=3.1 sec .2 sec + RxCoalesceCQENSec=+3.1s + RxCoalesceCQENSec=3.1s.2 + RxCoalesceCQENSec=100 ns + RxCoalesceCQENSec=1µs + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RxCoalesceCQENSec=infinity + RxCoalesceCQENSec=-5 + RxCoalesceCQENSec=5. + RxCoalesceCQENSec=3.s + RxCoalesceCQENSec=xyz + RxCoalesceCQENSec=12..34 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt new file mode 100644 index 00000000..eebb35d6 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt @@ -0,0 +1,52 @@ +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 + +class ConfigParseCpuSetOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinity= + IRQAffinity=0 + IRQAffinity=1 2 4 + IRQAffinity=0-3,8-11 + IRQAffinity=0, 1, 2, 3, 4, 5, 6, 7, 63, + IRQAffinity=8 '9' 10 "11" + IRQAffinity=0-3 8-11, + IRQAffinity=8000-8191 + IRQAffinity=0x0-0x3 + IRQAffinity=,0 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinity=0 1 2 3 garbage + IRQAffinity=0-3 8-garbage + IRQAffinity=8192 + IRQAffinity=1- + IRQAffinity=-1 + IRQAffinity=0 1 2 3 "4 5 6 7 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt new file mode 100644 index 00000000..5baaf66f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt @@ -0,0 +1,46 @@ +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 + +class ConfigParseDefaultPortVlanidOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Bridge] + DefaultPVID=9 + DefaultPVID=none + DefaultPVID=1 + DefaultPVID=4094 + DefaultPVID=0 + DefaultPVID=0xFFE + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Bridge] + DefaultPVID=4095 + DefaultPVID=None + DefaultPVID=-1 + DefaultPVID=1-10 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt new file mode 100644 index 00000000..1b857168 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt @@ -0,0 +1,41 @@ +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 + +class ConfigParseGeneveAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [GENEVE] + Remote=10.0.0.2 + Remote=2001:db8::1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [GENEVE] + Remote=10.0.0 + Remote=geneve.example.com + Remote=10.0.0.2/24 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt new file mode 100644 index 00000000..4fb1e872 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt @@ -0,0 +1,42 @@ +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 + +class ConfigParseIfaliasOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Alias= + Alias=uplink to core switch + Alias=eth0 (management) + Alias=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Alias=Café uplink + Alias=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt new file mode 100644 index 00000000..8cf16f95 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt @@ -0,0 +1,44 @@ +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 + +class ConfigParseIfnamesOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + AlternativeName=dummy98-2-altname + AlternativeName=dummyalt hogehogehogehogehogehoge + AlternativeName=a-very-long-alternative-interface-name-that-is-fine + AlternativeName=sim99 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + AlternativeName=eth:0 + AlternativeName=all + AlternativeName=123 + AlternativeName=ok bad/name + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt new file mode 100644 index 00000000..2bd3cdbb --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt @@ -0,0 +1,47 @@ +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 + +class ConfigParseIpoibPkeyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPoIB] + PartitionKey=1 + PartitionKey=0x8001 + PartitionKey=0xffff + PartitionKey=0x7fff + PartitionKey=32769 + PartitionKey=0b1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [IPoIB] + PartitionKey=0 + PartitionKey=0x8000 + PartitionKey=32768 + PartitionKey=0x10000 + PartitionKey=default + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt new file mode 100644 index 00000000..028cff76 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt @@ -0,0 +1,45 @@ +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 + +class ConfigParseIpv6FlowlabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Tunnel] + IPv6FlowLabel=inherit + IPv6FlowLabel=0 + IPv6FlowLabel=1048575 + IPv6FlowLabel=0xFFFFF + IPv6FlowLabel=0x12345 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Tunnel] + IPv6FlowLabel=1048576 + IPv6FlowLabel=0x100000 + IPv6FlowLabel=Inherit + IPv6FlowLabel=-1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt new file mode 100644 index 00000000..33b41a64 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt @@ -0,0 +1,45 @@ +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 + +class ConfigParseIrqAffinityNumaOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityNUMA=local + IRQAffinityNUMA=0 + IRQAffinityNUMA=3 + IRQAffinityNUMA=0x1 + IRQAffinityNUMA=4294967293 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityNUMA=4294967294 + IRQAffinityNUMA=-1 + IRQAffinityNUMA=remote + IRQAffinityNUMA=1.5 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt new file mode 100644 index 00000000..32344a7a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt @@ -0,0 +1,40 @@ +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 + +class ConfigParseIrqAffinityPolicyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityPolicy=single + IRQAffinityPolicy=spread + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityPolicy=Single + IRQAffinityPolicy=balanced + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt new file mode 100644 index 00000000..651d76a0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt @@ -0,0 +1,45 @@ +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 + +class ConfigParseL2tpSessionNameOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TPSession] + Name=l2tp-ses1 + Name=sess.a + Name=1a + Name=abcdefghijklmno + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TPSession] + Name=abcdefghijklmnop + Name=123 + Name=all + Name=a:b + Name=a/b + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt new file mode 100644 index 00000000..cc46f9ac --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt @@ -0,0 +1,52 @@ +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 + +class ConfigParseL2tpTunnelLocalAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Local=192.168.30.100 + Local=2001:db8::1 + Local=auto + Local=static + Local=dynamic + Local=auto@eth0 + Local=dynamic@test1 + Local=192.168.30.100@eth0 + Local=auto@@eth0 + Local=auto@ + Local=static@a-very-long-alternative-interface-name + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Local=any + Local=auto@all + Local=auto@eth/0 + Local=192.168.30.256 + Local=auto@123 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt new file mode 100644 index 00000000..6d7d654d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt @@ -0,0 +1,41 @@ +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 + +class ConfigParseL2tpTunnelRemoteAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Remote=192.168.30.101 + Remote=2001:db8::2 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Remote=auto + Remote=192.168.30 + Remote=peer.example.com + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt new file mode 100644 index 00000000..54f08a4c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt @@ -0,0 +1,42 @@ +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 + +class ConfigParseMacAddressPolicyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + MACAddressPolicy=persistent + MACAddressPolicy=random + MACAddressPolicy=none + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + MACAddressPolicy=Persistent + MACAddressPolicy=keep + MACAddressPolicy=yes + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt new file mode 100644 index 00000000..97c1be19 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt @@ -0,0 +1,43 @@ +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 + +class ConfigParseMacsecKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyFile=/etc/systemd/network/macsec.key + [MACsecReceiveAssociation] + KeyFile=/run/keys/rx key.hex + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyFile=macsec.key + KeyFile=/sys/kernel/key + [MACsecReceiveAssociation] + KeyFile=/etc/keys/../secret + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt new file mode 100644 index 00000000..8c1f9874 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt @@ -0,0 +1,51 @@ +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 + +class ConfigParseMacsecKeyIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyId=01 + KeyId=0203 + KeyId=0203040506070809 + KeyId=ABCDEF + KeyId=00112233445566778899aabbccddeeff + KeyId=01 02 03 + [MACsecReceiveAssociation] + KeyId=020304 + KeyId=ff + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyId=1 + KeyId=255 + KeyId=0x01 + KeyId=00112233445566778899aabbccddeeff00 + [MACsecReceiveAssociation] + KeyId=zz + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt new file mode 100644 index 00000000..0be26b67 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt @@ -0,0 +1,44 @@ +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 + +class ConfigParsePortOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Port=tp + Port=aui + Port=mii + Port=fibre + Port=bnc + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Port=fiber + Port=TP + Port=da + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt new file mode 100644 index 00000000..76c58578 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt @@ -0,0 +1,45 @@ +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 + +class ConfigParsePortRangeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + PortRange=1-65535 + PortRange=4789 + PortRange=0x100-0x200 + PortRange=32768-61000 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + PortRange=0-100 + PortRange=1-65536 + PortRange=100:200 + PortRange=-5 + PortRange=5- + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt new file mode 100644 index 00000000..09e39cdf --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt @@ -0,0 +1,46 @@ +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 + +class ConfigParseRpsCpuMaskOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ReceivePacketSteeringCPUMask=0 1 + ReceivePacketSteeringCPUMask=2,3 + ReceivePacketSteeringCPUMask=all + ReceivePacketSteeringCPUMask=disable + ReceivePacketSteeringCPUMask=0-7 + ReceivePacketSteeringCPUMask=1 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ReceivePacketSteeringCPUMask=invalid + ReceivePacketSteeringCPUMask=0 3 8-invalid + ReceivePacketSteeringCPUMask=all 1 + ReceivePacketSteeringCPUMask=none + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt new file mode 100644 index 00000000..bd93c4ea --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt @@ -0,0 +1,53 @@ +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 + +class ConfigParseUdevPropertyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Property=ID_NET_MANAGED_BY=io.systemd.Network + Property=ID_NET_NAME_MAC=* + Property=FOO=bar BAZ=qux + Property=FOO= + Property="FOO=bar baz" + Property=FOO="bar baz" + Property='FOO=bar baz' X=1 + Property=HOST_%H=1 + Property=_X1=a=b + Property=DEVPATHX=1 + Property=TAGS_EXTRA=1 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Property=FOO + Property=1FOO=bar + Property=INTERFACE=eth0 + Property=SYNTH_ARG_X=1 + Property=FOO-BAR=1 + Property=ACTION=add + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt new file mode 100644 index 00000000..ad447103 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt @@ -0,0 +1,48 @@ +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 + +class ConfigParseVlanQosMapsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + EgressQOSMaps=21-7 45-5 + IngressQOSMaps=1-1 + EgressQOSMaps=5 + IngressQOSMaps=0x10-0x3 "7-2" '8-1' + EgressQOSMaps=4294967295-7 + IngressQOSMaps=1-7 2-6 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + EgressQOSMaps=21:7 + IngressQOSMaps=1- + EgressQOSMaps=-5 + IngressQOSMaps=1-2-3 + EgressQOSMaps=4294967296-1 + IngressQOSMaps=a-b + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt new file mode 100644 index 00000000..10f5f748 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt @@ -0,0 +1,48 @@ +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 + +class ConfigParseVlanprotocolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + Protocol=802.1q + Protocol=802.1Q + Protocol=802.1ad + Protocol=802.1AD + [Bridge] + VLANProtocol=802.1ad + VLANProtocol=802.1Q + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + Protocol=802.1Ad + Protocol=8021q + Protocol=qinq + [Bridge] + VLANProtocol=802.1x + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt new file mode 100644 index 00000000..388d0cf5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt @@ -0,0 +1,48 @@ +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 + +class ConfigParseVrfTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VRF] + Table=42 + Table=main + Table=local + Table=default + Table=0x2a + Table=custom-table + Table=4294967295 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VRF] + Table=0 + Table=4294967296 + Table=main 42 + Table=vrf:42 + Table=1 2 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt new file mode 100644 index 00000000..b37ef77a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt @@ -0,0 +1,53 @@ +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 + +class ConfigParseVxlanAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + Group=239.1.1.1 + Group=ff02::42 + Local=192.168.1.1 + Local=fe80::1 + Local=dhcp4 + Local=ipv6_link_local + Local=slaac + Local=dhcp_pd + Remote=10.0.0.2 + Remote=2001:db8::1 + Remote=::ffff:10.0.0.2 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + Group=239.1.1 + Local=dhcp + Remote=example.com + Remote=10.0.0.256 + Local=192.168.1.1/24 + Group=fe80::1::2 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt new file mode 100644 index 00000000..047cb7a9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt @@ -0,0 +1,49 @@ +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 + +class ConfigParseWireguardAllowedIpsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + AllowedIPs= + AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24 + AllowedIPs=0.0.0.0/0 + AllowedIPs=::/0 + AllowedIPs=10.0.0.1 + AllowedIPs=fd8d:4d6d:3ccb:0500:900c:d437:ec27:8822/128 + AllowedIPs=10.0.0.0/8, 192.168.0.0/16 + AllowedIPs=10.0.0.0/8 fd00::/8, + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + AllowedIPs=10.0.0.0/33 + AllowedIPs=fd00::/129 + AllowedIPs=10.0.0.0/8;192.168.0.0/16 + AllowedIPs=any + AllowedIPs=10.0.0/8 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt new file mode 100644 index 00000000..4f86f85b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt @@ -0,0 +1,51 @@ +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 + +class ConfigParseWireguardEndpointOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + Endpoint=192.168.27.3:51820 + Endpoint=[2001:db8::1]:51820 + Endpoint=wg.example.com:51820 + Endpoint=@network.wireguard.peer0.endpoint + Endpoint=192.168.27.3 + Endpoint=fe80::1 + Endpoint=vpn.example.org.:0x1F90 + Endpoint=[fe80::1%eth0]:51820 + Endpoint=host:65535 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + Endpoint=wg.example.com + Endpoint=192.168.27.3:0 + Endpoint=192.168.27.3:65536 + Endpoint=wg..example.com:51820 + Endpoint=[2001:db8::1]: + Endpoint=wg.example.com:port + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt new file mode 100644 index 00000000..65f151e7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt @@ -0,0 +1,47 @@ +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 + +class ConfigParseWireguardPeerKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + PublicKeyFile=/run/systemd/network/25-wireguard-public-key.txt + PresharedKeyFile=/etc/systemd/network/wg psk.key + PresharedKeyFile=/etc//systemd/./network/psk/ + PublicKeyFile=/devices/key + PublicKeyFile=/run/credentials/%i.key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + PublicKeyFile=relative/key + PresharedKeyFile=/etc/../root/psk + PublicKeyFile=/proc/self/environ + PresharedKeyFile=/dev/stdin + PublicKeyFile=%d/key + PresharedKeyFile=//sys/key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt new file mode 100644 index 00000000..9fb18e7f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt @@ -0,0 +1,52 @@ +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 + +class ConfigParseWireguardPeerRouteTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + RouteTable=off + RouteTable=no + RouteTable=0 + RouteTable=false + RouteTable=1234 + RouteTable=main + RouteTable=local + RouteTable=default + RouteTable=0x10 + RouteTable=4294967295 + RouteTable=custom + RouteTable=vpn-table + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + RouteTable=4294967296 + RouteTable=main table + RouteTable=custom:100 + RouteTable=00 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt new file mode 100644 index 00000000..54d549aa --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt @@ -0,0 +1,41 @@ +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 + +class ConfigParseWireguardPrivateKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKeyFile=/run/systemd/network/25-wireguard-private-key.txt + PrivateKeyFile=/etc/wireguard/my key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKeyFile=wg.key + PrivateKeyFile=/proc/1/environ + PrivateKeyFile=/etc/wireguard/../../shadow + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt new file mode 100644 index 00000000..85839cff --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt @@ -0,0 +1,46 @@ +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 + +class ConfigParseWireguardPrivateKeyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= + PrivateKey=Eor9QBsIoUG6C3ZKsKdqm4Vkt0n2N7qpSh2LzphQeho= + PrivateKey=@network.wireguard.private + PrivateKey=EEGlnEPYJV__kbvvIqxKkQwOiS-UENyPncC4bF46ong= + PrivateKey=EEGlnEPYJV//kbvvIqxK kQwOiS+UENyPncC4bF46ong= + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46onh= + PrivateKey=short= + PrivateKey=@bad/name + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong== + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt new file mode 100644 index 00000000..3b84706e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt @@ -0,0 +1,52 @@ +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 + +class ConfigParseWireguardRouteTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + RouteTable=off + RouteTable=no + RouteTable=0 + RouteTable=false + RouteTable=1234 + RouteTable=main + RouteTable=local + RouteTable=default + RouteTable=0x10 + RouteTable=4294967295 + RouteTable=custom + RouteTable=vpn-table + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + RouteTable=4294967296 + RouteTable=main table + RouteTable=custom:100 + RouteTable=00 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt new file mode 100644 index 00000000..78a342df --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt @@ -0,0 +1,45 @@ +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 + +class ConfigParseWolPasswordOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + WakeOnLanPassword=00:11:22:33:44:55 + WakeOnLanPassword=00-11-22-33-44-55 + WakeOnLanPassword=0011.2233.4455 + WakeOnLanPassword=/etc/systemd/network/wol.pass + WakeOnLanPassword=/run/credentials/wol pass + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + WakeOnLanPassword=00:11:22:33:44 + WakeOnLanPassword=secret + WakeOnLanPassword=/etc/../wol.pass + WakeOnLanPassword=00:11:22:33:44:55:66:77 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} From 70a633abc55ebfbd9395c6cd3bd678ca6e41ba44 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 23 Oct 2026 18:33:13 -0700 Subject: [PATCH 2/8] test: cover netdev-link batch combinators on both engines; colour safe_atou numbers Adds grammar-level tests for the shared helpers (safe_atou bases, parse_size SI chains, parse_nsec durations, CPU sets, key-file paths, WireGuard keys, route tables) using systemd's own unit-test cases, checked against both the classic and the list-of-successes engine. Numbers from the new terminals are now coloured as literals, and AllowedIPs= reads prefix lengths the way in_addr_parse_prefixlen does (safe_atou8, so /0x18 is legal). Co-Authored-By: Claude Opus 5.5 (1M context) --- ...nfigParseWireguardAllowedIpsOptionValue.kt | 12 +-- .../grammar/BatchNetdevLinkCombinators.kt | 32 ++++++- ...ParseWireguardAllowedIpsOptionValueTest.kt | 4 +- .../grammar/BatchNetdevLinkCombinatorsTest.kt | 86 +++++++++++++++++++ 4 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt index 117711c1..eb33a27d 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt @@ -2,7 +2,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.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_ANY_PREFIX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_SAFE_PREFIX 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.ZeroOrMore @@ -15,6 +15,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L673-L749 config_parse_wireguard_allowed_ips (0) * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("," WHITESPACE, no flags) * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L845-L897 in_addr_prefix_from_string_auto_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L779-L797 in_addr_parse_prefixlen (safe_atou8) * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L15 AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24 */ @@ -25,8 +26,9 @@ private val ALLOWED_IPS_SEPARATOR = RegexTerminal("[\\s,]+", "[\\s,]+") * Validator for `[WireGuardPeer] AllowedIPs=` (.netdev). * * C function: config_parse_wireguard_allowed_ips. A list of IPv4/IPv6 addresses with optional prefix - * lengths (in_addr_prefix_from_string_auto: a missing length means the full width, `/0` is allowed), - * separated by commas and/or whitespace. Separators coalesce, and leading or trailing ones are harmless. + * lengths, separated by commas and/or whitespace. Each entry goes to in_addr_prefix_from_string_auto: a + * missing length means the full width, `/0` is allowed, and the length is a safe_atou8 number, so `/0x18` + * works (see [IP_ADDR_AND_SAFE_PREFIX]). Separators coalesce, and leading or trailing ones are harmless. * An address that is not properly masked (`192.168.26.3/24`) is accepted and masked with a warning. * An invalid entry is logged and skipped by systemd; an empty assignment clears the list. */ @@ -34,8 +36,8 @@ class ConfigParseWireguardAllowedIpsOptionValue : SimpleGrammarOptionValues( "config_parse_wireguard_allowed_ips", SequenceCombinator( ZeroOrOne(ALLOWED_IPS_SEPARATOR), - IP_ADDR_AND_ANY_PREFIX, - ZeroOrMore(SequenceCombinator(ALLOWED_IPS_SEPARATOR, IP_ADDR_AND_ANY_PREFIX)), + IP_ADDR_AND_SAFE_PREFIX, + ZeroOrMore(SequenceCombinator(ALLOWED_IPS_SEPARATOR, IP_ADDR_AND_SAFE_PREFIX)), ZeroOrOne(ALLOWED_IPS_SEPARATOR), EOF() ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt index b72dc3fc..977181cb 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -84,8 +84,13 @@ class SafeAtouTerminal( } } -/** Shorthand for a [SafeAtouTerminal] over `[minInclusive, maxExclusive)`. */ -fun safeAtou(maxExclusive: Long, minInclusive: Long = 0L): Combinator = SafeAtouTerminal(minInclusive, maxExclusive) +/** + * A [SafeAtouTerminal] over `[minInclusive, maxExclusive)`, coloured as a literal. The [Labeled] wrapper + * is only there because [defaultRole] does not know this terminal class yet (it is transparent to + * matching); once the terminal is promoted next to [UnsignedNumberTerminal] it can go. + */ +fun safeAtou(maxExclusive: Long, minInclusive: Long = 0L): Combinator = + Labeled(Role.LITERAL, SafeAtouTerminal(minInclusive, maxExclusive)) /** Exclusive upper bound of a uint16_t. */ const val UINT16_LIMIT = 65_536L @@ -148,7 +153,8 @@ val CPU_SET: Combinator = SequenceCombinator( // strtoull(…, 10) after parse_size has skipped whitespace: an optional `+` (a `-` is refused), digits, // then an optional `.` and fraction. parse_size steps over the `.` unconditionally and ignores a // missing fraction ("E.g. 10.M is valid"), so `10.` and `10.M` are both legal. -private val SIZE_NUMBER = RegexTerminal("""\+?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""") +// Labeled so the number colours as a literal, like ByteSizeTerminal does. +private val SIZE_NUMBER = Labeled(Role.LITERAL, RegexTerminal("""\+?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""")) /** * A parse_size() value with base 1000 (the SI table: `E`, `P`, `T`, `G`, `M`, `K`, `B` and bare). @@ -187,7 +193,10 @@ private fun sizeChain(suffixes: List): Combinator { // strtoll(…, 10) as parse_nsec drives it: an optional `+` (a leading `-` is refused), then digits with // an optional fraction — or a bare fraction like `.5`, which parse_nsec accepts because it checks for // `.` before it checks whether strtoll consumed anything. A trailing `.` with no digits (`3.`) is refused. -private val DURATION_NUMBER = RegexTerminal("""\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""", """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""") +private val DURATION_NUMBER = Labeled( + Role.LITERAL, + RegexTerminal("""\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""", """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+"""), +) /** extract_nsec_multiplier's table (minus its empty default entry, which is the bare-number case). */ private val NSEC_SUFFIX = LiteralChoiceTerminal( @@ -285,3 +294,18 @@ val ROUTE_TABLE: Combinator = AlternativeCombinator( SequenceCombinator(safeAtou(UINT32_LIMIT, 1), EOF()), SequenceCombinator(ROUTE_TABLE_NAME, EOF()), ) + + +// --------------------------------------------------------------------------------------------------- +// Address prefixes — in_addr_prefix_from_string_auto_full (src/basic/in-addr-util.c). + +/** + * An IPv4 or IPv6 address with an optional `/prefixlen`, as in_addr_prefix_from_string_auto (mode + * PREFIXLEN_FULL) reads it. Like [IP_ADDR_AND_ANY_PREFIX], but the length goes through + * in_addr_parse_prefixlen → safe_atou8, so it is a base-0 number (`/0x18` is 24) up to the family width. + * IPv6 is tried first so the IPv4 branch never strands part of an IPv6 literal. + */ +val IP_ADDR_AND_SAFE_PREFIX: Combinator = AlternativeCombinator( + SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, safeAtou(129)))), + SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, safeAtou(33)))), +) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt index 047cb7a9..3e76d8e0 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt @@ -19,6 +19,7 @@ class ConfigParseWireguardAllowedIpsOptionValueTest : AbstractUnitFileTest() { AllowedIPs=fd8d:4d6d:3ccb:0500:900c:d437:ec27:8822/128 AllowedIPs=10.0.0.0/8, 192.168.0.0/16 AllowedIPs=10.0.0.0/8 fd00::/8, + AllowedIPs=10.0.0.0/0x8 fd00::/0100 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -38,12 +39,13 @@ class ConfigParseWireguardAllowedIpsOptionValueTest : AbstractUnitFileTest() { AllowedIPs=10.0.0.0/8;192.168.0.0/16 AllowedIPs=any AllowedIPs=10.0.0/8 + AllowedIPs=10.0.0.0/ """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(5, highlights) + assertSize(6, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt new file mode 100644 index 00000000..56aad8ba --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt @@ -0,0 +1,86 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Grammar-level tests for the shared helpers in BatchNetdevLinkCombinators.kt, run against BOTH matching + * engines: the classic SyntacticMatch/SemanticMatch pair and the list-of-successes [validate]. The cases + * come from systemd's own unit tests at 0f0682b773 (test-parse-util.c, test-time-util.c, + * test-cpu-set-util.c) where one exists. + */ +class BatchNetdevLinkCombinatorsTest { + + private fun classicAccepts(grammar: Combinator, value: String): Boolean = + grammar.SyntacticMatch(value, 0).matchResult == value.length && grammar.SemanticMatch(value, 0).matchResult == value.length + + private fun assertAccepts(grammar: Combinator, expected: Boolean, vararg values: String) { + val whole = SequenceCombinator(grammar, EOF()) + for (value in values) { + assertEquals("classic engine on '$value'", expected, classicAccepts(whole, value)) + assertEquals("parse engine on '$value'", expected, whole.validate(value) == ParseOutcome.Valid) + } + } + + @Test + fun testSafeAtouAcceptsEveryBaseSystemdDoes() { + val u16 = safeAtou(UINT16_LIMIT) + assertAccepts(u16, true, "0", "65535", "0xffff", "0XFF", "0177777", "0b101", "0B1", "0o17", "0O17", "+5", "+0x10") + assertAccepts(u16, false, "65536", "0x10000", "089", "0b2", "0o8", "-1", "+0b1", "0x", "abc", "1.5") + } + + @Test + fun testSafeAtouHonoursTheLowerBound() { + assertAccepts(safeAtou(UINT32_LIMIT, 1), true, "1", "4294967295") + assertAccepts(safeAtou(UINT32_LIMIT, 1), false, "0", "00", "0x0", "4294967296") + } + + @Test + fun testSiSizeMatchesParseSizeSiTests() { + // TEST(parse_size_si) successes. + assertAccepts(siSize(), true, "111", "111.4", "112 B", "112.6 B", "3.5 K", "3. K", "3.0 K", "4 M 11.5K", + "3.5G3B", "3.5G 4B", "4T3G3B", "4 T 3 G 3 B", "12P", "3E 2P", "1000E", "10.M", "+5K") + // TEST(parse_size_si) failures (bar 1000E, an overflow this grammar does not compute). + assertAccepts(siSize(), false, "3. 0 K", "3B3.5G", "3B3G4T", "12P12P", "12X", "12.5X", "12.5e3", "-1", "-1000E", + "-10B 20K", "1k", "1 2") + } + + @Test + fun testNsecDurationMatchesParseNsecTests() { + // TEST(parse_nsec) successes, minus the two "infinity" spellings this grammar deliberately excludes. + assertAccepts(NSEC_DURATION_FINITE, true, "5s", "5s500ms", "5s 500ms", "5.5s", "5.5s 0.5ms", ".22s", ".50y", + "2.5", ".7", "+3.1s", "3.1s.2", "3.1 .2s", "3.1 sec .2 sec", "3.1 sec 1.2 sec", "1.111111111111y", "100 ns", "1µs") + assertAccepts(NSEC_DURATION_FINITE, false, "xyz", ".", "5.", ".s", "-5s", "-0.3s", "0.-0s", "3.-0s", "3.+1s", + "3. 1s", "3.s", "12..34", "..1234", "1234..", "infinity") + } + + @Test + fun testCpuSetMatchesParseCpuSetTests() { + // TEST(parse_cpu_set) successes. + assertAccepts(CPU_SET, true, "0", "1 2 4", "0 1 2 3 8 9 10 11", "8 '9' 10 \"11\"", "0,1,2,3 8,9,10,11", + "0, 1, 2, 3, 4, 5, 6, 7, 63, ", "0-3,8-11", "36-39,44-47", "64-71", "0-3 8-11, ", "0-7 4-11", "0,2 4-11", + "8000-8191", "0x0-0x3", ",0") + // TEST(parse_cpu_set) failures (bar "3-0", a reversed range this grammar does not compare). + assertAccepts(CPU_SET, false, "0 1 2 3 garbage", "0-3 8-garbage", "0 1 2 3 \"4 5 6 7 ", "8192", "1-", "-1") + } + + @Test + fun testKeyFilePathRefusesApiVfsAndDotDot() { + assertAccepts(NON_API_VFS_ABSOLUTE_PATH, true, "/etc/key", "/etc/my key", "/devices/x", "/sysroot/key", "/etc//a/./b/") + assertAccepts(NON_API_VFS_ABSOLUTE_PATH, false, "key", "/dev", "/dev/null", "//sys/x", "/proc/1/environ", "/a/../b", "/a/..") + } + + @Test + fun testWireguardKeyNeedsPaddingAndAValidLastCharacter() { + assertAccepts(WIREGUARD_BASE64_KEY, true, "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong=", + "EEGlnEPYJV__kbvvIqxKkQwOiS-UENyPncC4bF46ong=", "EEGlnEPYJV//kbvvIqxK kQwOiS+UENyPncC4bF46ong =") + assertAccepts(WIREGUARD_BASE64_KEY, false, "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong", + "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46onh=", "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong==", "AAAA") + } + + @Test + fun testRouteTableAcceptsNamesAndNonZeroNumbers() { + assertAccepts(ROUTE_TABLE, true, "main", "local", "default", "1", "0x10", "4294967295", "custom", "1abc", "-1") + assertAccepts(ROUTE_TABLE, false, "0", "00", "4294967296", "a b", "name:1") + } +} From 8cf0258857f2116f302cc562dc91816e4ba94176 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 23 Oct 2026 18:35:10 -0700 Subject: [PATCH 3/8] fix: accept negative zero in safe_atou numbers (netdev-link batch) safe_atou_full only refuses a leading '-' when the parsed value is non-zero, so '-0' and '-0x0' are legal. Negative numbers are now number-shaped and reported as out of range instead of as unrecognised characters. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../grammar/BatchNetdevLinkCombinators.kt | 14 ++++++++------ .../grammar/BatchNetdevLinkCombinatorsTest.kt | 4 ++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt index 977181cb..bb779791 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -29,9 +29,9 @@ import java.math.BigInteger * string like `089` is number-shaped but not a valid octal literal, so it matches and is then rejected * — which is what strtoul()+`*x != 0` does too. * - * Not accepted: `-0`. safe_atou_full lets a negative zero through (`l != 0 && s[0] == '-'` is the only - * minus check), but no real configuration spells zero that way and accepting a leading `-` would blur - * the error on every genuinely negative value. + * A leading `-` is number-shaped (so `-1` is reported as an out-of-range number rather than as junk) + * and valid only when the value is zero: safe_atou_full's only minus check is `l != 0 && s[0] == '-'`, + * so `-0` (and `-0x0`) really are accepted. * * @param minInclusive lowest accepted value * @param maxExclusive one past the highest accepted value @@ -48,12 +48,14 @@ class SafeAtouTerminal( // mangle_base() only recognises 0b/0o at the very start, so they never carry a sign. if (text.length > 2 && (text.startsWith("0b") || text.startsWith("0B"))) return text.drop(2).toBigIntegerOrNull(2) if (text.length > 2 && (text.startsWith("0o") || text.startsWith("0O"))) return text.drop(2).toBigIntegerOrNull(8) - val digits = text.removePrefix("+") - return when { + val digits = text.removePrefix("+").removePrefix("-") + val magnitude = when { digits.length > 2 && (digits.startsWith("0x") || digits.startsWith("0X")) -> digits.drop(2).toBigIntegerOrNull(16) digits.length > 1 && digits.startsWith("0") -> digits.drop(1).toBigIntegerOrNull(8) else -> digits.toBigIntegerOrNull() } + // strtoul() negates a `-` value; safe_atou_full then refuses it unless it is zero. + return if (text.startsWith("-") && magnitude != BigInteger.ZERO) null else magnitude } private fun inRange(text: String): Boolean = @@ -80,7 +82,7 @@ class SafeAtouTerminal( private companion object { // 0b/0o first: as plain digits they would otherwise be read as "0" followed by junk. Within the // signed group, hex before octal before decimal for the same reason. - val SHAPE = Regex("""0[bB][01]+|0[oO][0-7]+|\+?(?:0[xX][0-9a-fA-F]+|0[0-7]+|[0-9]+)""") + val SHAPE = Regex("""0[bB][01]+|0[oO][0-7]+|[+-]?(?:0[xX][0-9a-fA-F]+|0[0-7]+|[0-9]+)""") } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt index 56aad8ba..2a1d42de 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt @@ -25,8 +25,8 @@ class BatchNetdevLinkCombinatorsTest { @Test fun testSafeAtouAcceptsEveryBaseSystemdDoes() { val u16 = safeAtou(UINT16_LIMIT) - assertAccepts(u16, true, "0", "65535", "0xffff", "0XFF", "0177777", "0b101", "0B1", "0o17", "0O17", "+5", "+0x10") - assertAccepts(u16, false, "65536", "0x10000", "089", "0b2", "0o8", "-1", "+0b1", "0x", "abc", "1.5") + assertAccepts(u16, true, "0", "65535", "0xffff", "0XFF", "0177777", "0b101", "0B1", "0o17", "0O17", "+5", "+0x10", "-0", "-0x0") + assertAccepts(u16, false, "65536", "0x10000", "089", "0b2", "0o8", "-1", "-0x1", "-0b0", "+0b1", "0x", "abc", "1.5") } @Test From d3bf3e90d2b3d9f15d0e5dece97b308b8733318c Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 23 Oct 2026 19:02:28 -0700 Subject: [PATCH 4/8] fix: address review findings for netdev-link batch validators - Accept quotes/escapes anywhere in an extract_first_word word (CPU sets, RPS masks, VLAN QoS maps, udev Property= names) via a fallback ExtractedWordTerminal that unquotes/unescapes before checking. - parse_range: refuse -0 as the lower bound; allow whitespace after the dash for PortRange= (safe_atou skips it). - L2TP Local=: allow leading '@' separators. - Key-file paths: catch /dev, /sys, /proc reached via '.' components. - WireGuard Endpoint=: restrict \DDD escapes to 000-255. - Fix PrivateKey= test fixture link; document empty-value handling. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ConfigParseBadadvBandwidthOptionValue.kt | 3 + ...ConfigParseDefaultPortVlanidOptionValue.kt | 3 + .../ConfigParseL2tpSessionNameOptionValue.kt | 3 + ...gParseL2tpTunnelLocalAddressOptionValue.kt | 7 +- .../ai/ConfigParsePortRangeOptionValue.kt | 12 +- .../ai/ConfigParseUdevPropertyOptionValue.kt | 39 ++- .../ai/ConfigParseVlanQosMapsOptionValue.kt | 9 +- .../ai/ConfigParseVrfTableOptionValue.kt | 3 + ...ConfigParseWireguardEndpointOptionValue.kt | 8 +- ...nfigParseWireguardPrivateKeyOptionValue.kt | 3 +- .../grammar/BatchNetdevLinkCombinators.kt | 266 +++++++++++++++--- .../ai/ConfigParseCpuSetOptionValueTest.kt | 6 +- ...seL2tpTunnelLocalAddressOptionValueTest.kt | 5 +- .../ai/ConfigParsePortRangeOptionValueTest.kt | 4 +- .../ConfigParseRpsCpuMaskOptionValueTest.kt | 4 +- .../ConfigParseUdevPropertyOptionValueTest.kt | 9 +- .../ConfigParseVlanQosMapsOptionValueTest.kt | 6 +- ...igParseWireguardEndpointOptionValueTest.kt | 4 +- ...eWireguardPrivateKeyFileOptionValueTest.kt | 6 +- .../grammar/BatchNetdevLinkCombinatorsTest.kt | 36 ++- 20 files changed, 371 insertions(+), 65 deletions(-) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt index 9cd52a24..ac5bad90 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt @@ -26,6 +26,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * * The parser only *warns* when the result divided by 100 kbit exceeds UINT32_MAX and still stores it, * so no magnitude bound is applied here. An invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `GatewayBandwidthDown=` or `GatewayBandwidthUp=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. */ class ConfigParseBadadvBandwidthOptionValue : SimpleGrammarOptionValues( "config_parse_badadv_bandwidth", diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt index de967d48..5382424f 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt @@ -26,6 +26,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * The man page says "an integer in the range 1…4094", but vlanid_is_valid only checks `id <= 4094`, so * `0` is accepted (and stored as 0, the same as `none`). This follows the C. An invalid value is logged * and ignored by systemd. + * + * There is no isempty() branch, so an empty `DefaultPVID=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. */ class ConfigParseDefaultPortVlanidOptionValue : SimpleGrammarOptionValues( "config_parse_default_port_vlanid", diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt index e2a373de..c063fc5f 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt @@ -20,6 +20,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * C function: config_parse_l2tp_session_name, which requires ifname_valid(): 1…15 printable ASCII * characters other than `:`, `/` and `%`, not entirely digits, and not `.`, `..`, `all` or * `default`. See [INTERFACE_NAME]. An invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `Name=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. */ class ConfigParseL2tpSessionNameOptionValue : SimpleGrammarOptionValues( "config_parse_l2tp_session_name", diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt index d1a04c4c..94ce597e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt @@ -17,7 +17,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Local= * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L477-L569 config_parse_l2tp_tunnel_local_address (0) * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L38-L42 l2tp_local_address_type_table - * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("@", no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("@", no flags; leading-separator skip at #L39-L56) * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto */ @@ -31,12 +31,15 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * IFNAME_VALID_ALTERNATIVE (up to 127 characters). * * The `@` is cut with extract_first_word(…, "@", 0), which coalesces a run of separators, so `auto@@eth0` - * and even a trailing `auto@` (no interface) are accepted. Not modelled: systemd refuses the null + * and even a trailing `auto@` (no interface) are accepted; it also skips leading separators, so `@auto` + * is `auto` (a lone `@` is an empty word and refused). Not modelled: systemd refuses the null * address and an address family that conflicts with `Remote=`. An invalid value is logged and ignored. */ class ConfigParseL2tpTunnelLocalAddressOptionValue : SimpleGrammarOptionValues( "config_parse_l2tp_tunnel_local_address", SequenceCombinator( + // extract_first_word() skips leading separators, so `@auto` reads as `auto`. + ZeroOrOne(RegexTerminal("@+", "@+")), AlternativeCombinator( IPV6_ADDR, IPV4_ADDR, diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt index 7ed65c15..d9caabc3 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt @@ -5,7 +5,6 @@ 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.UINT16_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseRange -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou /* * [VXLAN] PortRange= in a .netdev file. @@ -20,11 +19,14 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Validator for `[VXLAN] PortRange=` (.netdev). * * C function: config_parse_port_range → parse_ip_port_range(…, allow_zero = false): a single port or a - * `low-high` range, each bound a safe_atou() number (so hex works) in 1…65535. A range with - * `high < low` is refused by systemd after parsing; that compares numbers and is not modelled. An - * invalid value is logged and ignored by systemd. + * `low-high` range, each bound a safe_atou() number (so hex works) in 1…65535. safe_atou() skips leading + * whitespace, so `1000- 2000` is legal (but `1000 -2000` is not: the lower word keeps its trailing space). + * A range with `high < low` is refused by systemd after parsing; that compares numbers and is not + * modelled. An invalid value is logged and ignored by systemd. There is no isempty() branch, so `PortRange=` + * is a parse error in systemd rather than a reset; the plugin never runs a grammar on an empty value, so it + * is not flagged here. */ class ConfigParsePortRangeOptionValue : SimpleGrammarOptionValues( "config_parse_port_range", - SequenceCombinator(parseRange(safeAtou(UINT16_LIMIT, 1)), EOF()) + SequenceCombinator(parseRange(UINT16_LIMIT, 1, spaceAfterDash = true), EOF()) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt index de92c7e8..81994454 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt @@ -3,10 +3,12 @@ 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.ExtractedWordTerminal 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.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WordEnd import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne @@ -56,12 +58,30 @@ private fun quotedAssignment(quote: String, inner: RegexTerminal) = SequenceComb LiteralChoiceTerminal(quote), PROPERTY_NAME, EQUALS, ZeroOrOne(inner), LiteralChoiceTerminal(quote), ZeroOrOne(RAW_VALUE), ) +// The name check of PROPERTY_NAME, applied to an already unquoted and unescaped word. +private val UNQUOTED_PROPERTY_NAME = Regex("""$RESERVED_PROPERTY(?:[A-Za-z_]|$SPECIFIER)(?:[A-Za-z0-9_]|$SPECIFIER)*""") + +private fun assignmentNameIsValid(word: String): Boolean { + val eq = word.indexOf('=') + return eq > 0 && UNQUOTED_PROPERTY_NAME.matches(word.substring(0, eq)) +} + // One word: NAME=VALUE, either plain (the value may still contain quoted runs) or with the whole -// assignment quoted from the start of the name. +// assignment quoted from the start of the name. Anything else extract_first_word could turn into a valid +// assignment — a quote or C escape inside the name (`F"OO"=bar`, `"FOO"=bar`, `FOO\x41=1`) — falls +// through to the ExtractedWordTerminal, which unquotes and unescapes the word first and then applies the +// same name check. WordEnd stops the classic engine committing to a structured branch that only matched +// a prefix of the word. private val PROPERTY_ASSIGNMENT = AlternativeCombinator( - quotedAssignment("\"", DOUBLE_QUOTED_INNER), - quotedAssignment("'", SINGLE_QUOTED_INNER), - SequenceCombinator(PROPERTY_NAME, EQUALS, ZeroOrOne(RAW_VALUE)), + SequenceCombinator( + AlternativeCombinator( + quotedAssignment("\"", DOUBLE_QUOTED_INNER), + quotedAssignment("'", SINGLE_QUOTED_INNER), + SequenceCombinator(PROPERTY_NAME, EQUALS, ZeroOrOne(RAW_VALUE)), + ), + WordEnd("""\s"""), + ), + ExtractedWordTerminal("""\s""", cunescape = true, "udev property assignment", ::assignmentNameIsValid), ) /** @@ -75,9 +95,14 @@ private val PROPERTY_ASSIGNMENT = AlternativeCombinator( * empty — and the name must not be one udev reserves (device_property_can_set: `ACTION`, `DEVPATH`, * `INTERFACE`, `TAGS`, …, or anything starting `SYNTH_ARG_`). * - * Lenient corners: the escapes inside a value are not checked against cunescape_one's list (an unknown - * escape such as `\q` makes systemd ignore the whole assignment), quotes that open in the middle of a - * name are not modelled, and an unknown `%` specifier is not flagged. An invalid word is logged and + * Quotes and C escapes may appear anywhere in the word, including inside the name (`F"OO"=bar`, + * `"FOO"=bar`, `FOO\x41=1` are all `FOO…=…` after extract_first_word); such words are unquoted and + * unescaped before the name is checked. + * + * Lenient corners: in the common `NAME=VALUE` spelling the escapes inside the value are not checked + * against cunescape_one's list (an unknown escape such as `\q` makes systemd ignore the whole + * assignment), the value's UTF-8 validity is not checked, and an unknown `%` specifier in the value is + * not flagged. An invalid word is logged and * skipped by udev; an empty assignment clears the list. */ class ConfigParseUdevPropertyOptionValue : SimpleGrammarOptionValues( diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt index 66d23a8f..554fa40a 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt @@ -5,10 +5,8 @@ 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.UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.extractedRangeWord import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.optionallyQuoted -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseRange -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou /* * [VLAN] EgressQOSMaps= / IngressQOSMaps= in a .netdev file. @@ -20,12 +18,13 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) */ -private val QOS_MAPPING = optionallyQuoted(parseRange(safeAtou(UINT32_LIMIT))) +private val QOS_MAPPING = extractedRangeWord(UINT32_LIMIT, 0L, """\s""", cunescape = true) /** * Validator for `[VLAN] EgressQOSMaps=` and `IngressQOSMaps=` (.netdev). * - * C function: config_parse_vlan_qos_maps. A whitespace-separated list of words (each may be quoted); + * C function: config_parse_vlan_qos_maps. A whitespace-separated list of words (each may be quoted, and + * may contain C escapes — the quotes and escapes are removed before parsing, see [extractedRangeWord]); * every word goes to parse_range(), so it is `from-to` or a single number meaning `from == to`, each * side an unsigned int as safe_atou() reads it (decimal, `0x` hex, octal, `0b`/`0o`). * diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt index 068ffd88..ef19bed6 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt @@ -23,6 +23,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Because table names are user-defined, any word without whitespace or `:` that is not all digits is * accepted as a potential name. What is flagged: `0` and out-of-range numbers, values containing * whitespace, and `name:number` pairs. An invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `Table=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. */ class ConfigParseVrfTableOptionValue : SimpleGrammarOptionValues( "config_parse_vrf_table", diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt index 5369be46..5a5cf704 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt @@ -32,9 +32,9 @@ private val ENDPOINT_PORT = safeAtou(UINT16_LIMIT, 1) // The hostname fallback: everything up to the LAST colon (strrchr), checked with dns_name_is_valid(): // dot-separated labels of at most 63 characters, each character printable (>= ' ', not DEL) or one of -// the escapes `\.`, `\\` or `\DDD`. A single trailing dot is allowed, and dns_name_is_valid() also +// the escapes `\.`, `\\` or `\DDD` with DDD at most 255 (dns_label_unescape refuses `k > 255`). A single trailing dot is allowed, and dns_name_is_valid() also // accepts the root name — `.` or nothing at all — so `:51820` passes the parser too. -private const val DNS_LABEL = """(?:[^.\\\x00-\x1F\x7F]|\\[.\\]|\\[0-9]{3}){1,63}""" +private const val DNS_LABEL = """(?:[^.\\\x00-\x1F\x7F]|\\[.\\]|\\(?:[01][0-9]{2}|2[0-4][0-9]|25[0-5])){1,63}""" private val ENDPOINT_HOST = RegexTerminal( """[^\x00-\x1F\x7F]+(?=:[^:]*$)""", """(?:$DNS_LABEL(?:\.$DNS_LABEL)*\.?|\.)(?=:[^:]*$)""", @@ -55,7 +55,9 @@ private val ENDPOINT_HOST = RegexTerminal( * The port is a parse_ip_port() number, 1…65535 (safe_atou16 base 0, so hex works). Because the host * fallback is so permissive, something like `[fe80::1%eth0]:51820` is accepted (as a hostname), as it * is by systemd at parse time. What is flagged: a missing or invalid port, empty labels, over-long labels - * and control characters. An invalid value is logged and ignored by systemd. + * and control characters. Lenient: the overall name length (DNS_HOSTNAME_MAX, enforced by + * dns_name_concat) and the 63-byte label limit, which is counted here in characters, not UTF-8 bytes. An + * invalid value is logged and ignored by systemd. */ class ConfigParseWireguardEndpointOptionValue : SimpleGrammarOptionValues( "config_parse_wireguard_endpoint", diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt index ea68413d..db993546 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt @@ -16,7 +16,8 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755-L851 unbase64mem_full * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L528-L555 unbase64char (also accepts the URL-safe '-' and '_') * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L156-L180 read_credential (credential_name_valid) - * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L12 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard-23-peers.netdev#L12 PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev.d/private-key.conf#L2 (same key, drop-in) */ /** diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt index bb779791..ac7efeb1 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -35,31 +35,18 @@ import java.math.BigInteger * * @param minInclusive lowest accepted value * @param maxExclusive one past the highest accepted value + * @param allowMinus false for a position where a `-` cannot reach safe_atou at all — the lower bound + * of a parse_range(), which is cut at the first `-`, so `-0` there is an empty word and EINVAL. */ class SafeAtouTerminal( private val minInclusive: Long, private val maxExclusive: Long, + private val allowMinus: Boolean = true, ) : TerminalCombinator { - private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value - - /** The numeric value of [text], or null when it is not a valid literal in its base. */ - private fun valueOf(text: String): BigInteger? { - // mangle_base() only recognises 0b/0o at the very start, so they never carry a sign. - if (text.length > 2 && (text.startsWith("0b") || text.startsWith("0B"))) return text.drop(2).toBigIntegerOrNull(2) - if (text.length > 2 && (text.startsWith("0o") || text.startsWith("0O"))) return text.drop(2).toBigIntegerOrNull(8) - val digits = text.removePrefix("+").removePrefix("-") - val magnitude = when { - digits.length > 2 && (digits.startsWith("0x") || digits.startsWith("0X")) -> digits.drop(2).toBigIntegerOrNull(16) - digits.length > 1 && digits.startsWith("0") -> digits.drop(1).toBigIntegerOrNull(8) - else -> digits.toBigIntegerOrNull() - } - // strtoul() negates a `-` value; safe_atou_full then refuses it unless it is zero. - return if (text.startsWith("-") && magnitude != BigInteger.ZERO) null else magnitude - } + private fun span(value: String, offset: Int): String? = (if (allowMinus) SHAPE else SHAPE_NO_MINUS).matchAt(value, offset)?.value - private fun inRange(text: String): Boolean = - valueOf(text)?.let { it >= BigInteger.valueOf(minInclusive) && it < BigInteger.valueOf(maxExclusive) } ?: false + private fun inRange(text: String): Boolean = inRange(valueOf(text), minInclusive, maxExclusive) override fun SyntacticMatch(value: String, offset: Int): MatchResult { val text = span(value, offset) ?: return NoMatch @@ -77,12 +64,40 @@ class SafeAtouTerminal( return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, inRange(text))))) } - override fun toString(): String = "SafeAtou($minInclusive,$maxExclusive)" + override fun toString(): String = "SafeAtou($minInclusive,$maxExclusive${if (allowMinus) "" else ",no-minus"})" - private companion object { + internal companion object { // 0b/0o first: as plain digits they would otherwise be read as "0" followed by junk. Within the // signed group, hex before octal before decimal for the same reason. val SHAPE = Regex("""0[bB][01]+|0[oO][0-7]+|[+-]?(?:0[xX][0-9a-fA-F]+|0[0-7]+|[0-9]+)""") + val SHAPE_NO_MINUS = Regex("""0[bB][01]+|0[oO][0-7]+|\+?(?:0[xX][0-9a-fA-F]+|0[0-7]+|[0-9]+)""") + + fun inRange(value: BigInteger?, minInclusive: Long, maxExclusive: Long): Boolean = + value != null && value >= BigInteger.valueOf(minInclusive) && value < BigInteger.valueOf(maxExclusive) + + /** + * What safe_atou() makes of the whole string [s], or null when it refuses it: leading whitespace is + * skipped (`s += strspn(s, WHITESPACE)`), then the rest must be exactly one [SHAPE] literal. + */ + fun safeAtouValue(s: String): BigInteger? { + val text = s.trimStart(' ', '\t', '\n', '\r') + return if (SHAPE.matches(text)) valueOf(text) else null + } + + /** The numeric value of [text], or null when it is not a valid literal in its base. */ + fun valueOf(text: String): BigInteger? { + // mangle_base() only recognises 0b/0o at the very start, so they never carry a sign. + if (text.length > 2 && (text.startsWith("0b") || text.startsWith("0B"))) return text.drop(2).toBigIntegerOrNull(2) + if (text.length > 2 && (text.startsWith("0o") || text.startsWith("0O"))) return text.drop(2).toBigIntegerOrNull(8) + val digits = text.removePrefix("+").removePrefix("-") + val magnitude = when { + digits.length > 2 && (digits.startsWith("0x") || digits.startsWith("0X")) -> digits.drop(2).toBigIntegerOrNull(16) + digits.length > 1 && digits.startsWith("0") -> digits.drop(1).toBigIntegerOrNull(8) + else -> digits.toBigIntegerOrNull() + } + // strtoul() negates a `-` value; safe_atou_full then refuses it unless it is zero. + return if (text.startsWith("-") && magnitude != BigInteger.ZERO) null else magnitude + } } } @@ -91,8 +106,8 @@ class SafeAtouTerminal( * is only there because [defaultRole] does not know this terminal class yet (it is transparent to * matching); once the terminal is promoted next to [UnsignedNumberTerminal] it can go. */ -fun safeAtou(maxExclusive: Long, minInclusive: Long = 0L): Combinator = - Labeled(Role.LITERAL, SafeAtouTerminal(minInclusive, maxExclusive)) +fun safeAtou(maxExclusive: Long, minInclusive: Long = 0L, allowMinus: Boolean = true): Combinator = + Labeled(Role.LITERAL, SafeAtouTerminal(minInclusive, maxExclusive, allowMinus)) /** Exclusive upper bound of a uint16_t. */ const val UINT16_LIMIT = 65_536L @@ -101,17 +116,43 @@ const val UINT16_LIMIT = 65_536L const val UINT32_LIMIT = 4_294_967_296L /** - * `N` or `N-M` as parse_range() (src/basic/parse-util.c) reads it: the value is split on the first - * `-` (EXTRACT_DONT_COALESCE_SEPARATORS, so `-5` and `5-` both fail) and each side goes through - * safe_atou(). The `lower <= upper` checks that callers make afterwards compare parsed numbers and are - * not modelled. + * `N` or `N-M` as parse_range() (src/basic/parse-util.c) reads it, each bound a safe_atou() number in + * `[minInclusive, maxExclusive)`. The value is split on the first `-` (EXTRACT_DONT_COALESCE_SEPARATORS, + * so `-5` and `5-` both fail); the lower part goes to safe_atou() and so can never start with `-` + * (`-0` is an empty lower word), while the upper part is handed to safe_atou() as is, so `5--0` is legal. + * safe_atou() skips leading whitespace, so whitespace may follow the dash — but only where the caller's + * own word splitting cannot cut there, hence [spaceAfterDash] (true for a whole-value range such as + * PortRange=; false inside a whitespace-separated list, where `0- 3` is two words). The `lower <= upper` + * checks that callers make afterwards compare parsed numbers and are not modelled. */ -fun parseRange(bound: Combinator): Combinator = SequenceCombinator(bound, ZeroOrOne(SequenceCombinator(HYPHEN, bound))) +fun parseRange(maxExclusive: Long, minInclusive: Long = 0L, spaceAfterDash: Boolean = false): Combinator = SequenceCombinator( + safeAtou(maxExclusive, minInclusive, allowMinus = false), + ZeroOrOne( + if (spaceAfterDash) SequenceCombinator(HYPHEN, ZeroOrOne(WhitespaceTerminal()), safeAtou(maxExclusive, minInclusive)) + else SequenceCombinator(HYPHEN, safeAtou(maxExclusive, minInclusive)) + ), +) + +/** + * Whether parse_range() accepts the already-extracted [word] with both bounds in + * `[minInclusive, maxExclusive)` — the same check as [parseRange], done on a string rather than as a + * grammar, for words whose quotes or escapes have to be removed first (see [ExtractedWordTerminal]). + */ +fun parseRangeAccepts(word: String, minInclusive: Long, maxExclusive: Long): Boolean { + val dash = word.indexOf('-') + val lower = if (dash < 0) word else word.substring(0, dash) + if (lower.isEmpty()) return false + if (!SafeAtouTerminal.inRange(SafeAtouTerminal.safeAtouValue(lower), minInclusive, maxExclusive)) return false + if (dash < 0) return true + val upper = word.substring(dash + 1) + return upper.isNotEmpty() && SafeAtouTerminal.inRange(SafeAtouTerminal.safeAtouValue(upper), minInclusive, maxExclusive) +} /** * [inner] optionally wrapped in a matching pair of `"…"` or `'…'`, the form a word takes after * extract_first_word(…, EXTRACT_UNQUOTE) has removed its quotes (systemd's own test-cpu-set-util.c - * writes `8 '9' 10 "11"`). Only whole-word quoting is modelled; quotes that start mid-word are not. + * writes `8 '9' 10 "11"`). Only whole-word quoting is modelled here; pair it with an + * [ExtractedWordTerminal] fallback (as [extractedRangeWord] does) for quotes that start mid-word. */ fun optionallyQuoted(inner: Combinator): Combinator = AlternativeCombinator( SequenceCombinator(LiteralChoiceTerminal("\""), inner, LiteralChoiceTerminal("\"")), @@ -120,6 +161,164 @@ fun optionallyQuoted(inner: Combinator): Combinator = AlternativeCombinator( ) +// --------------------------------------------------------------------------------------------------- +// Words as extract_first_word() (src/basic/extract-word.c) returns them. + +/** + * A zero-width check that the input ends here or continues with one of [separators] (a regex character + * class body such as `\s,`). Put after a structured word so that, under the classic first-match engine, + * an [AlternativeCombinator] does not commit to a branch that matched only a prefix of the word. + */ +class WordEnd(separators: String) : Combinator { + private val separator = Regex("[$separators]") + + private fun at(value: String, offset: Int): Boolean = + offset == value.length || separator.matchesAt(value, offset) + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = + if (at(value, offset)) MatchResult(emptyList(), offset, emptyList(), offset) else NoMatch + + override fun SemanticMatch(value: String, offset: Int): MatchResult = SyntacticMatch(value, offset) + + override fun parse(value: String, offset: Int): Sequence = + if (at(value, offset)) sequenceOf(Parse(offset, emptyList())) else sequenceOf(Stuck(offset, setOf(this))) + + override fun toStringIndented(indent: Int): String = "WordEnd" +} + +/** + * One raw word of an extract_first_word(…, separators, EXTRACT_UNQUOTE[|EXTRACT_CUNESCAPE]) list, checked + * the way systemd checks it: AFTER the quotes (which may open and close anywhere in the word, as in + * `0-"3"`) and backslash escapes are removed. + * + * It matches a run of non-separator characters, backslash escapes and balanced quoted runs (quoted runs + * may contain separators) — but ONLY when that word contains a quote or backslash, [extractWord] succeeds + * on it and [check] accepts the result. Use it as the last alternative after a structured grammar for the + * common spelling: the common spelling keeps its highlighting and error localization, and this only + * rescues the exotic quoting and escaping systemd also accepts. + * + * It deliberately has no "syntactically right but semantically wrong" state. Under the classic engine + * GrammarOptionValue locates a semantic error by lining the semantic match's longestMatch up against the + * syntactic tokens; a whole-word token that only fails semantically would be misaligned with the + * structured branch's partial progress and produce an out-of-range highlight. So an invalid word simply + * does not match, and the value is reported as not matching the expected format. + * + * @param separators a regex character-class body for the separator set, e.g. `\s` or `\s,` + * @param cunescape whether the caller passes EXTRACT_CUNESCAPE + */ +class ExtractedWordTerminal( + separators: String, + private val cunescape: Boolean, + private val description: String, + private val check: (String) -> Boolean, +) : TerminalCombinator { + + private val shape = Regex("""(?:[^$separators"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""") + + /** The word at [offset] if it is one this terminal accepts, else null. */ + private fun word(value: String, offset: Int): String? { + val text = shape.matchAt(value, offset)?.value ?: return null + if (text.none { it == '"' || it == '\'' || it == '\\' }) return null + return text.takeIf { extractWord(it, cunescape)?.let(check) == true } + } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = word(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult = SyntacticMatch(value, offset) + + override fun parse(value: String, offset: Int): Sequence { + val text = word(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, true)))) + } + + override fun toString(): String = "ExtractedWord($description)" +} + +/** + * The text extract_first_word() returns for the single raw word [raw] under EXTRACT_UNQUOTE (plus + * EXTRACT_CUNESCAPE when [cunescape]), or null where it fails: an unbalanced quote, a trailing + * backslash, or — with [cunescape] — an escape cunescape_one() (src/basic/escape.c) refuses. Without + * [cunescape], a backslash simply makes the next character literal. + */ +fun extractWord(raw: String, cunescape: Boolean): String? { + val out = StringBuilder() + var quote: Char? = null + var i = 0 + while (i < raw.length) { + val c = raw[i] + if (c == '\\') { + if (i + 1 >= raw.length) return null + if (cunescape) { + val (text, length) = cunescapeOne(raw, i + 1) ?: return null + out.append(text) + i += 1 + length + } else { + out.append(raw[i + 1]) + i += 2 + } + continue + } + when { + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> out.append(c) + } + i++ + } + return if (quote == null) out.toString() else null +} + +/** cunescape_one(…, accept_nul = false) on the escape starting at [p]: the decoded text and its length. */ +private fun cunescapeOne(s: String, p: Int): Pair? { + fun digits(count: Int, radix: Int): Int? = + if (p + 1 + count > s.length) null else s.substring(p + 1, p + 1 + count).let { d -> if (d.all { Character.digit(it, radix) >= 0 }) d.toInt(radix) else null } + + return when (val c = s[p]) { + 'a' -> "\u0007" to 1 + 'b' -> "\b" to 1 + 'f' -> "\u000C" to 1 + 'n' -> "\n" to 1 + 'r' -> "\r" to 1 + 't' -> "\t" to 1 + 'v' -> "\u000B" to 1 + '\\', '"', '\'' -> c.toString() to 1 + 's' -> " " to 1 + 'x' -> digits(2, 16)?.takeIf { it != 0 }?.let { it.toChar().toString() to 3 } + 'u' -> digits(4, 16)?.takeIf { it != 0 && it !in 0xD800..0xDFFF }?.let { it.toChar().toString() to 5 } + 'U' -> { + if (p + 9 > s.length) return null + val v = s.substring(p + 1, p + 9).toLongOrNull(16) ?: return null + if (v == 0L || v > 0x10FFFF || v in 0xD800..0xDFFF) return null + String(Character.toChars(v.toInt())) to 9 + } + in '0'..'7' -> { + if (p + 3 > s.length) return null + val d = s.substring(p, p + 3) + if (!d.all { it in '0'..'7' }) return null + val v = d.toInt(8) + if (v == 0 || v > 255) null else v.toChar().toString() to 3 + } + else -> null + } +} + +/** + * One parse_range() word of a separator-split list: the structured `N[-M]` grammar (optionally quoted + * as a whole) for highlighting, with an [ExtractedWordTerminal] fallback that accepts the same numbers + * however extract_first_word's quoting and escaping spells them (`0-"3"`, `'0'-'3'`). + */ +fun extractedRangeWord(maxExclusive: Long, minInclusive: Long, separators: String, cunescape: Boolean): Combinator = + AlternativeCombinator( + SequenceCombinator(optionallyQuoted(parseRange(maxExclusive, minInclusive)), WordEnd(separators)), + ExtractedWordTerminal(separators, cunescape, "range $minInclusive..${maxExclusive - 1}") { + parseRangeAccepts(it, minInclusive, maxExclusive) + }, + ) + + // --------------------------------------------------------------------------------------------------- // CPU sets — config_parse_cpu_set (src/shared/cpu-set-util.c). @@ -129,14 +328,15 @@ private const val CPU_SET_MAX_NCPU = 8192L // One extract_first_word() separator run: any mix of whitespace and commas (separators coalesce). private val CPU_SET_SEPARATOR = RegexTerminal("""[\s,]+""", """[\s,]+""") -private val CPU_SET_ITEM = optionallyQuoted(parseRange(safeAtou(CPU_SET_MAX_NCPU))) +private val CPU_SET_ITEM = extractedRangeWord(CPU_SET_MAX_NCPU, 0L, """\s,""", cunescape = false) /** * A CPU set: CPU indices and `N-M` ranges separated by whitespace and/or commas. * * config_parse_cpu_set splits with extract_first_word(…, WHITESPACE ",", EXTRACT_UNQUOTE), so separators * coalesce, leading and trailing separators are harmless (`0, 1, 2, 3, 4, 5, 6, 7, 63, ` is in - * test-cpu-set-util.c), and a word may be quoted. Each word goes to parse_range(), whose bounds are + * test-cpu-set-util.c), and a word may be quoted — anywhere in the word, not only around it (`0-"3"` is + * `0-3`; see [extractedRangeWord]). Each word goes to parse_range(), whose bounds are * safe_atou() numbers — so hex works — and cpu_set_add_range() refuses anything at or above * CPU_SET_MAX_NCPU (8192). A range whose lower end exceeds its upper end (`3-0`) is rejected by systemd * after parsing; that compares numbers and is not modelled. @@ -236,11 +436,13 @@ private const val NO_DOT_DOT_COMPONENT = """(?!(?:[\s\S]*/)?\.\.(?:/|$))""" * (src/shared/parse-helpers.c), taken verbatim — no specifier expansion, no unquoting — as the netdev * `*KeyFile=` settings do. After path_simplify() the path must be normalized (so no `..` component) and * must not be at or below one of the API VFS mounts `/dev`, `/sys`, `/proc` (path_below_api_vfs, which - * compares whole components, so `/devices` is fine). Spaces are ordinary path characters. + * compares whole components, so `/devices` is fine). Because path_simplify() runs first, `.` components + * and repeated slashes in front of those names do not hide them (`/./dev/shm/key`, `//sys`). Spaces + * are ordinary path characters. Lenient: path_is_valid()'s NAME_MAX/PATH_MAX length limits. */ val NON_API_VFS_ABSOLUTE_PATH = RegexTerminal( """[\s\S]+""", - """(?!/+(?:dev|sys|proc)(?:/|$))$NO_DOT_DOT_COMPONENT/[\s\S]*""", + """(?!/(?:\.?/)*(?:dev|sys|proc)(?:/|$))$NO_DOT_DOT_COMPONENT/[\s\S]*""", ) /** diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt index eebb35d6..e92da505 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt @@ -21,6 +21,8 @@ class ConfigParseCpuSetOptionValueTest : AbstractUnitFileTest() { IRQAffinity=8000-8191 IRQAffinity=0x0-0x3 IRQAffinity=,0 + IRQAffinity=0-"3" + IRQAffinity='0'-'3', 4 """.trimIndent() setupFileInEditor("file.link", file) @@ -41,12 +43,14 @@ class ConfigParseCpuSetOptionValueTest : AbstractUnitFileTest() { IRQAffinity=1- IRQAffinity=-1 IRQAffinity=0 1 2 3 "4 5 6 7 + IRQAffinity=-0 + IRQAffinity=0-"9000" """.trimIndent() setupFileInEditor("file.link", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(6, highlights) + assertSize(8, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt index cc46f9ac..1fcec3c5 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt @@ -22,6 +22,8 @@ class ConfigParseL2tpTunnelLocalAddressOptionValueTest : AbstractUnitFileTest() Local=auto@@eth0 Local=auto@ Local=static@a-very-long-alternative-interface-name + Local=@auto + Local=@@192.168.30.100@eth0 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -41,12 +43,13 @@ class ConfigParseL2tpTunnelLocalAddressOptionValueTest : AbstractUnitFileTest() Local=auto@eth/0 Local=192.168.30.256 Local=auto@123 + Local=@ """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(5, highlights) + assertSize(6, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt index 76c58578..2122f65d 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt @@ -15,6 +15,7 @@ class ConfigParsePortRangeOptionValueTest : AbstractUnitFileTest() { PortRange=4789 PortRange=0x100-0x200 PortRange=32768-61000 + PortRange=1000- 2000 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -34,12 +35,13 @@ class ConfigParsePortRangeOptionValueTest : AbstractUnitFileTest() { PortRange=100:200 PortRange=-5 PortRange=5- + PortRange=1000 -2000 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(5, highlights) + assertSize(6, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt index 09e39cdf..2aca8447 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt @@ -17,6 +17,7 @@ class ConfigParseRpsCpuMaskOptionValueTest : AbstractUnitFileTest() { ReceivePacketSteeringCPUMask=disable ReceivePacketSteeringCPUMask=0-7 ReceivePacketSteeringCPUMask=1 + ReceivePacketSteeringCPUMask=0-"3" """.trimIndent() setupFileInEditor("file.link", file) @@ -35,12 +36,13 @@ class ConfigParseRpsCpuMaskOptionValueTest : AbstractUnitFileTest() { ReceivePacketSteeringCPUMask=0 3 8-invalid ReceivePacketSteeringCPUMask=all 1 ReceivePacketSteeringCPUMask=none + ReceivePacketSteeringCPUMask=-0 """.trimIndent() setupFileInEditor("file.link", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(4, highlights) + assertSize(5, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt index bd93c4ea..3e701359 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt @@ -22,6 +22,10 @@ class ConfigParseUdevPropertyOptionValueTest : AbstractUnitFileTest() { Property=_X1=a=b Property=DEVPATHX=1 Property=TAGS_EXTRA=1 + Property="FOO"=bar + Property=F"OO"=bar + Property=FOO\x41=1 + Property='ID_X'=1 "Y"=2 """.trimIndent() setupFileInEditor("file.link", file) @@ -42,12 +46,15 @@ class ConfigParseUdevPropertyOptionValueTest : AbstractUnitFileTest() { Property=SYNTH_ARG_X=1 Property=FOO-BAR=1 Property=ACTION=add + Property="ACTION"=add + Property=FOO\q=1 + Property=F"OO=bar """.trimIndent() setupFileInEditor("file.link", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(6, highlights) + assertSize(9, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt index ad447103..14e143dd 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt @@ -17,6 +17,8 @@ class ConfigParseVlanQosMapsOptionValueTest : AbstractUnitFileTest() { IngressQOSMaps=0x10-0x3 "7-2" '8-1' EgressQOSMaps=4294967295-7 IngressQOSMaps=1-7 2-6 + EgressQOSMaps=1-"2" "1"-2 3 + IngressQOSMaps=5--0 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -37,12 +39,14 @@ class ConfigParseVlanQosMapsOptionValueTest : AbstractUnitFileTest() { IngressQOSMaps=1-2-3 EgressQOSMaps=4294967296-1 IngressQOSMaps=a-b + EgressQOSMaps=-0 + IngressQOSMaps=1-"2 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(6, highlights) + assertSize(8, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt index 4f86f85b..b43ad1f0 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt @@ -20,6 +20,7 @@ class ConfigParseWireguardEndpointOptionValueTest : AbstractUnitFileTest() { Endpoint=vpn.example.org.:0x1F90 Endpoint=[fe80::1%eth0]:51820 Endpoint=host:65535 + Endpoint=a\255b.example.com:51820 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -40,12 +41,13 @@ class ConfigParseWireguardEndpointOptionValueTest : AbstractUnitFileTest() { Endpoint=wg..example.com:51820 Endpoint=[2001:db8::1]: Endpoint=wg.example.com:port + Endpoint=a\999b:51820 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(6, highlights) + assertSize(7, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt index 54d549aa..68830390 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt @@ -13,6 +13,8 @@ class ConfigParseWireguardPrivateKeyFileOptionValueTest : AbstractUnitFileTest() [WireGuard] PrivateKeyFile=/run/systemd/network/25-wireguard-private-key.txt PrivateKeyFile=/etc/wireguard/my key + PrivateKeyFile=/./devices/key + PrivateKeyFile=/.dev/key """.trimIndent() setupFileInEditor("file.netdev", file) @@ -30,12 +32,14 @@ class ConfigParseWireguardPrivateKeyFileOptionValueTest : AbstractUnitFileTest() PrivateKeyFile=wg.key PrivateKeyFile=/proc/1/environ PrivateKeyFile=/etc/wireguard/../../shadow + PrivateKeyFile=/./dev/shm/key + PrivateKeyFile=//sys/key """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(3, highlights) + assertSize(5, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt index 2a1d42de..86d67c00 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt @@ -62,12 +62,44 @@ class BatchNetdevLinkCombinatorsTest { "8000-8191", "0x0-0x3", ",0") // TEST(parse_cpu_set) failures (bar "3-0", a reversed range this grammar does not compare). assertAccepts(CPU_SET, false, "0 1 2 3 garbage", "0-3 8-garbage", "0 1 2 3 \"4 5 6 7 ", "8192", "1-", "-1") + // EXTRACT_UNQUOTE removes quotes anywhere in a word, and without EXTRACT_CUNESCAPE a backslash just + // makes the next character literal. + assertAccepts(CPU_SET, true, "0-\"3\"", "'0'-'3'", "\"0\"-3, 4", "0-\\3", "\" 3\"", "1-\"0x3\"") + // The lower bound of a range is cut at the first `-`, so `-0` is an empty word there. + assertAccepts(CPU_SET, false, "-0", "-0-3", "0-\"9000\"", "\"0-3", "\"3 \"", "0-\"\"", "0-", "0- 3") + } + + @Test + fun testParseRangeBounds() { + assertAccepts(parseRange(100), true, "5", "5-7", "5--0", "0x5-+7", "0") + assertAccepts(parseRange(100), false, "-0", "-5", "5-", "5- 7", "5 -7", "100", "5-100") + assertAccepts(parseRange(UINT16_LIMIT, 1, spaceAfterDash = true), true, "1000- 2000", "1000-2000") + assertAccepts(parseRange(UINT16_LIMIT, 1, spaceAfterDash = true), false, "1000 -2000", "0-5", "5--0") + } + + @Test + fun testExtractWordUnquotesAndUnescapes() { + assertEquals("FOO=bar baz", extractWord("\"FOO=bar baz\"", true)) + assertEquals("FOO=bar", extractWord("F\"OO\"=bar", true)) + assertEquals("FOOA=1", extractWord("FOO\\x41=1", true)) + assertEquals("a b", extractWord("a\\sb", true)) + assertEquals("a\u00e9", extractWord("a\\u00e9", true)) + assertEquals("a\u0001", extractWord("a\\001", true)) + assertEquals(null, extractWord("FOO\\q", true)) + assertEquals(null, extractWord("a\\x00", true)) + assertEquals(null, extractWord("a\\400", true)) + assertEquals(null, extractWord("a\\uD800", true)) + assertEquals(null, extractWord("\"abc", true)) + assertEquals(null, extractWord("abc\\", false)) + assertEquals("aq", extractWord("a\\q", false)) } @Test fun testKeyFilePathRefusesApiVfsAndDotDot() { - assertAccepts(NON_API_VFS_ABSOLUTE_PATH, true, "/etc/key", "/etc/my key", "/devices/x", "/sysroot/key", "/etc//a/./b/") - assertAccepts(NON_API_VFS_ABSOLUTE_PATH, false, "key", "/dev", "/dev/null", "//sys/x", "/proc/1/environ", "/a/../b", "/a/..") + assertAccepts(NON_API_VFS_ABSOLUTE_PATH, true, "/etc/key", "/etc/my key", "/devices/x", "/sysroot/key", "/etc//a/./b/", + "/./devices/x", "/.dev/x", "/..dev/x") + assertAccepts(NON_API_VFS_ABSOLUTE_PATH, false, "key", "/dev", "/dev/null", "//sys/x", "/proc/1/environ", "/a/../b", "/a/..", + "/./dev/shm/key", "/.//./proc", "//./sys/", "/dev/") } @Test From aefddb16b7d38b5bff58df81e6fe64ba6f8a245d Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 23 Oct 2026 19:07:10 -0700 Subject: [PATCH 5/8] refactor: give netdev-link batch grammar helpers family-unique names Several batch branches declared public helpers with the same name in the shared grammar package (SafeAtouTerminal, UINT32_LIMIT, safeAtou, SafeAtoNumberTerminal, BOOLEAN_ANY_CASE, COALESCED_COLONS). Each branch compiled alone but any two merged together would not. Prefix them per family so the batch PRs can merge in any order; consolidating them into Combinators.kt is left for a follow-up. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ConfigParseDefaultPortVlanidOptionValue.kt | 4 +-- .../ai/ConfigParseIpoibPkeyOptionValue.kt | 4 +-- .../ai/ConfigParseIpv6FlowlabelOptionValue.kt | 4 +-- .../ConfigParseIrqAffinityNumaOptionValue.kt | 6 ++--- .../ai/ConfigParseVlanQosMapsOptionValue.kt | 4 +-- ...ConfigParseWireguardEndpointOptionValue.kt | 4 +-- .../grammar/BatchNetdevLinkCombinators.kt | 26 +++++++++---------- .../grammar/BatchNetdevLinkCombinatorsTest.kt | 6 ++--- 8 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt index 5382424f..15b71745 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt @@ -5,7 +5,7 @@ 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.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [Bridge] DefaultPVID= in a .netdev file. @@ -33,7 +33,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram class ConfigParseDefaultPortVlanidOptionValue : SimpleGrammarOptionValues( "config_parse_default_port_vlanid", SequenceCombinator( - AlternativeCombinator(FlexibleLiteralChoiceTerminal("none"), safeAtou(4095L)), + AlternativeCombinator(FlexibleLiteralChoiceTerminal("none"), netdevLinkSafeAtou(4095L)), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt index f8dbf778..acb390d1 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt @@ -5,7 +5,7 @@ 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.SequenceCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT16_LIMIT -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [IPoIB] PartitionKey= in a .netdev file. @@ -26,7 +26,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram class ConfigParseIpoibPkeyOptionValue : SimpleGrammarOptionValues( "config_parse_ipoib_pkey", SequenceCombinator( - AlternativeCombinator(safeAtou(0x8000L, 1), safeAtou(UINT16_LIMIT, 0x8001L)), + AlternativeCombinator(netdevLinkSafeAtou(0x8000L, 1), netdevLinkSafeAtou(UINT16_LIMIT, 0x8001L)), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt index 952f80fe..f50ffb85 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt @@ -5,7 +5,7 @@ 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.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [Tunnel] IPv6FlowLabel= in a .netdev file. @@ -26,7 +26,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram class ConfigParseIpv6FlowlabelOptionValue : SimpleGrammarOptionValues( "config_parse_ipv6_flowlabel", SequenceCombinator( - AlternativeCombinator(FlexibleLiteralChoiceTerminal("inherit"), safeAtou(0x100000L)), + AlternativeCombinator(FlexibleLiteralChoiceTerminal("inherit"), netdevLinkSafeAtou(0x100000L)), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt index dcecd885..7b961a75 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt @@ -5,8 +5,8 @@ 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.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETDEV_LINK_UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [Link] IRQAffinityNUMA= in a .link file. @@ -28,7 +28,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram class ConfigParseIrqAffinityNumaOptionValue : SimpleGrammarOptionValues( "config_parse_irq_affinity_numa", SequenceCombinator( - AlternativeCombinator(FlexibleLiteralChoiceTerminal("local"), safeAtou(UINT32_LIMIT - 2)), + AlternativeCombinator(FlexibleLiteralChoiceTerminal("local"), netdevLinkSafeAtou(NETDEV_LINK_UINT32_LIMIT - 2)), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt index 554fa40a..6f173bf9 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt @@ -3,7 +3,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.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETDEV_LINK_UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.extractedRangeWord import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore @@ -18,7 +18,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) */ -private val QOS_MAPPING = extractedRangeWord(UINT32_LIMIT, 0L, """\s""", cunescape = true) +private val QOS_MAPPING = extractedRangeWord(NETDEV_LINK_UINT32_LIMIT, 0L, """\s""", cunescape = true) /** * Validator for `[VLAN] EgressQOSMaps=` and `IngressQOSMaps=` (.netdev). diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt index 5a5cf704..ac1022e8 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt @@ -12,7 +12,7 @@ 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.UINT16_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtou +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [WireGuardPeer] Endpoint= in a .netdev file. @@ -28,7 +28,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram */ // parse_ip_port: a safe_atou16 number (base 0), not zero. -private val ENDPOINT_PORT = safeAtou(UINT16_LIMIT, 1) +private val ENDPOINT_PORT = netdevLinkSafeAtou(UINT16_LIMIT, 1) // The hostname fallback: everything up to the LAST colon (strrchr), checked with dns_name_is_valid(): // dot-separated labels of at most 63 characters, each character printable (>= ' ', not DEL) or one of diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt index ac7efeb1..5f39e63f 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -38,7 +38,7 @@ import java.math.BigInteger * @param allowMinus false for a position where a `-` cannot reach safe_atou at all — the lower bound * of a parse_range(), which is cut at the first `-`, so `-0` there is an empty word and EINVAL. */ -class SafeAtouTerminal( +class NetdevLinkSafeAtouTerminal( private val minInclusive: Long, private val maxExclusive: Long, private val allowMinus: Boolean = true, @@ -102,18 +102,18 @@ class SafeAtouTerminal( } /** - * A [SafeAtouTerminal] over `[minInclusive, maxExclusive)`, coloured as a literal. The [Labeled] wrapper + * A [NetdevLinkSafeAtouTerminal] over `[minInclusive, maxExclusive)`, coloured as a literal. The [Labeled] wrapper * is only there because [defaultRole] does not know this terminal class yet (it is transparent to * matching); once the terminal is promoted next to [UnsignedNumberTerminal] it can go. */ -fun safeAtou(maxExclusive: Long, minInclusive: Long = 0L, allowMinus: Boolean = true): Combinator = - Labeled(Role.LITERAL, SafeAtouTerminal(minInclusive, maxExclusive, allowMinus)) +fun netdevLinkSafeAtou(maxExclusive: Long, minInclusive: Long = 0L, allowMinus: Boolean = true): Combinator = + Labeled(Role.LITERAL, NetdevLinkSafeAtouTerminal(minInclusive, maxExclusive, allowMinus)) /** Exclusive upper bound of a uint16_t. */ const val UINT16_LIMIT = 65_536L /** Exclusive upper bound of a uint32_t / unsigned. */ -const val UINT32_LIMIT = 4_294_967_296L +const val NETDEV_LINK_UINT32_LIMIT = 4_294_967_296L /** * `N` or `N-M` as parse_range() (src/basic/parse-util.c) reads it, each bound a safe_atou() number in @@ -126,10 +126,10 @@ const val UINT32_LIMIT = 4_294_967_296L * checks that callers make afterwards compare parsed numbers and are not modelled. */ fun parseRange(maxExclusive: Long, minInclusive: Long = 0L, spaceAfterDash: Boolean = false): Combinator = SequenceCombinator( - safeAtou(maxExclusive, minInclusive, allowMinus = false), + netdevLinkSafeAtou(maxExclusive, minInclusive, allowMinus = false), ZeroOrOne( - if (spaceAfterDash) SequenceCombinator(HYPHEN, ZeroOrOne(WhitespaceTerminal()), safeAtou(maxExclusive, minInclusive)) - else SequenceCombinator(HYPHEN, safeAtou(maxExclusive, minInclusive)) + if (spaceAfterDash) SequenceCombinator(HYPHEN, ZeroOrOne(WhitespaceTerminal()), netdevLinkSafeAtou(maxExclusive, minInclusive)) + else SequenceCombinator(HYPHEN, netdevLinkSafeAtou(maxExclusive, minInclusive)) ), ) @@ -142,10 +142,10 @@ fun parseRangeAccepts(word: String, minInclusive: Long, maxExclusive: Long): Boo val dash = word.indexOf('-') val lower = if (dash < 0) word else word.substring(0, dash) if (lower.isEmpty()) return false - if (!SafeAtouTerminal.inRange(SafeAtouTerminal.safeAtouValue(lower), minInclusive, maxExclusive)) return false + if (!NetdevLinkSafeAtouTerminal.inRange(NetdevLinkSafeAtouTerminal.safeAtouValue(lower), minInclusive, maxExclusive)) return false if (dash < 0) return true val upper = word.substring(dash + 1) - return upper.isNotEmpty() && SafeAtouTerminal.inRange(SafeAtouTerminal.safeAtouValue(upper), minInclusive, maxExclusive) + return upper.isNotEmpty() && NetdevLinkSafeAtouTerminal.inRange(NetdevLinkSafeAtouTerminal.safeAtouValue(upper), minInclusive, maxExclusive) } /** @@ -495,7 +495,7 @@ private val ROUTE_TABLE_NAME = RegexTerminal("""[^\s:]+""", """(?![0-9]+$)[^\s:] */ val ROUTE_TABLE: Combinator = AlternativeCombinator( SequenceCombinator(FlexibleLiteralChoiceTerminal("default", "main", "local"), EOF()), - SequenceCombinator(safeAtou(UINT32_LIMIT, 1), EOF()), + SequenceCombinator(netdevLinkSafeAtou(NETDEV_LINK_UINT32_LIMIT, 1), EOF()), SequenceCombinator(ROUTE_TABLE_NAME, EOF()), ) @@ -510,6 +510,6 @@ val ROUTE_TABLE: Combinator = AlternativeCombinator( * IPv6 is tried first so the IPv4 branch never strands part of an IPv6 literal. */ val IP_ADDR_AND_SAFE_PREFIX: Combinator = AlternativeCombinator( - SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, safeAtou(129)))), - SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, safeAtou(33)))), + SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, netdevLinkSafeAtou(129)))), + SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, netdevLinkSafeAtou(33)))), ) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt index 86d67c00..9e57ff29 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt @@ -24,15 +24,15 @@ class BatchNetdevLinkCombinatorsTest { @Test fun testSafeAtouAcceptsEveryBaseSystemdDoes() { - val u16 = safeAtou(UINT16_LIMIT) + val u16 = netdevLinkSafeAtou(UINT16_LIMIT) assertAccepts(u16, true, "0", "65535", "0xffff", "0XFF", "0177777", "0b101", "0B1", "0o17", "0O17", "+5", "+0x10", "-0", "-0x0") assertAccepts(u16, false, "65536", "0x10000", "089", "0b2", "0o8", "-1", "-0x1", "-0b0", "+0b1", "0x", "abc", "1.5") } @Test fun testSafeAtouHonoursTheLowerBound() { - assertAccepts(safeAtou(UINT32_LIMIT, 1), true, "1", "4294967295") - assertAccepts(safeAtou(UINT32_LIMIT, 1), false, "0", "00", "0x0", "4294967296") + assertAccepts(netdevLinkSafeAtou(NETDEV_LINK_UINT32_LIMIT, 1), true, "1", "4294967295") + assertAccepts(netdevLinkSafeAtou(NETDEV_LINK_UINT32_LIMIT, 1), false, "0", "00", "0x0", "4294967296") } @Test From 6baaf372928de9a860fdfef059189d9c6d4fc3f0 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 06:47:43 -0700 Subject: [PATCH 6/8] fix: align netdev-link validators with systemd's parsers Checked against systemd's real .netdev/.link parsers built at 0f0682b773. - BatmanAdvanced GatewayBandwidth*=: flag totals of 429496729600000 or more (12P, "3E 2P", 20E); systemd warns and stores a truncated uint32. - VXLAN Group=/Local=/Remote=: apply the key-specific rules (local address type names only for Local=, Group= must be multicast, Local= and Remote= must not be) by picking a per-key grammar. - GENEVE Remote=: refuse multicast addresses. - Add NetdevLinkCheckedSpan, netdevLinkIsMulticast and netdevLinkParseSizeSi for checks on a parsed value. - Tests: oracle-verified values for the above and for the questions in the batch notes (MACsec KeyId=0, QoS maps, Endpoint=:51820, route table names, PortRange=+0b1-2). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ConfigParseBadadvBandwidthOptionValue.kt | 22 +++++- .../ai/ConfigParseGeneveAddressOptionValue.kt | 9 ++- .../ai/ConfigParseVxlanAddressOptionValue.kt | 67 +++++++++++----- .../grammar/BatchNetdevLinkCombinators.kt | 78 +++++++++++++++++++ ...nfigParseBadadvBandwidthOptionValueTest.kt | 14 +++- ...ConfigParseGeneveAddressOptionValueTest.kt | 7 +- .../ConfigParseMacsecKeyIdOptionValueTest.kt | 3 +- .../ai/ConfigParsePortRangeOptionValueTest.kt | 6 +- .../ConfigParseVlanQosMapsOptionValueTest.kt | 3 + .../ai/ConfigParseVrfTableOptionValueTest.kt | 3 + .../ConfigParseVxlanAddressOptionValueTest.kt | 19 ++++- ...igParseWireguardEndpointOptionValueTest.kt | 1 + ...ParseWireguardRouteTableOptionValueTest.kt | 1 + .../grammar/BatchNetdevLinkCombinatorsTest.kt | 18 +++++ 14 files changed, 219 insertions(+), 32 deletions(-) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt index ac5bad90..4d6c601d 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt @@ -2,14 +2,18 @@ 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.NetdevLinkCheckedSpan import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkParseSizeSi import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.siSize +import java.math.BigInteger /* * [BatmanAdvanced] GatewayBandwidthDown= / GatewayBandwidthUp= in a .netdev file. * * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#GatewayBandwidthDown= * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/batadv.c#L53-L89 config_parse_badadv_bandwidth (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/batadv.c#L81-L84 (the 100 kbit uint32_t range warning) * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1000) * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178 TEST(parse_size_si) * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-batadv.netdev#L14-L15 @@ -24,13 +28,25 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * fraction allowed, and several elements with strictly descending suffixes (`1G 500M`) summed. See * [siSize]. * - * The parser only *warns* when the result divided by 100 kbit exceeds UINT32_MAX and still stores it, - * so no magnitude bound is applied here. An invalid value is logged and ignored by systemd. + * The total is stored in units of 100 kbit in a uint32_t, so a total of 4294967296 × 100000 + * (429496729600000, e.g. `12P` or `3E 2P`) or more is flagged. systemd logs that case ("is outside of + * 0...429496729500000 range") and then stores the total truncated to 32 bits, which is never what was + * meant, so it is treated as invalid here. Totals that overflow parse_size itself (`20E`) are refused + * outright. Confirmed against systemd's parser built at 0f0682b773: `429496729599999` is silent, + * `429496729600000` warns. Any other invalid value is logged and ignored by systemd. * * There is no isempty() branch, so an empty `GatewayBandwidthDown=` or `GatewayBandwidthUp=` is a parse error in systemd (logged and ignored), not * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. */ class ConfigParseBadadvBandwidthOptionValue : SimpleGrammarOptionValues( "config_parse_badadv_bandwidth", - SequenceCombinator(siSize(), EOF()) + SequenceCombinator( + NetdevLinkCheckedSpan(siSize(), "total < 429496729600000") { text -> + netdevLinkParseSizeSi(text)?.let { it < BATADV_BANDWIDTH_LIMIT } == true + }, + EOF() + ) ) + +/** `k/1000/100 > UINT32_MAX` (batadv.c#L81) first holds at this total. */ +private val BATADV_BANDWIDTH_LIMIT: BigInteger = BigInteger.valueOf(4_294_967_296L) * BigInteger.valueOf(100_000L) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt index a5756c53..941564c0 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt @@ -3,7 +3,9 @@ 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.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetdevLinkCheckedSpan import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkIsMulticast /* * [GENEVE] Remote= in a .netdev file. @@ -11,6 +13,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Remote= * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/geneve.c#L124-L163 config_parse_geneve_address (0) * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L103-L113 in_addr_is_multicast * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html */ @@ -18,10 +21,10 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Validator for `[GENEVE] Remote=` (.netdev). * * C function: config_parse_geneve_address: an IPv4 or IPv6 literal (in_addr_from_string_auto → - * inet_pton). Not modelled: systemd also refuses a multicast address, which depends on the parsed value. - * An invalid value is logged and ignored by systemd. + * inet_pton) that is not a multicast address (224.0.0.0/4, ff00::/8). An invalid value is logged and + * ignored by systemd. */ class ConfigParseGeneveAddressOptionValue : SimpleGrammarOptionValues( "config_parse_geneve_address", - SequenceCombinator(IP_ADDR, EOF()) + SequenceCombinator(NetdevLinkCheckedSpan(IP_ADDR, "not multicast") { !netdevLinkIsMulticast(it) }, EOF()) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt index a2616521..f70ff004 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt @@ -1,12 +1,18 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai +import com.intellij.codeInspection.ProblemsHolder +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType 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.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.GrammarOptionValue 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.NetdevLinkCheckedSpan import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkIsMulticast /* * [VXLAN] Group= / Local= / Remote= in a .netdev file. @@ -15,31 +21,54 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vxlan.c#L220-L297 config_parse_vxlan_address (0) * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/netdev-util.c#L9-L16 netdev_local_address_type_table * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L103-L113 in_addr_is_multicast * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html */ +private const val VXLAN_VALIDATOR = "config_parse_vxlan_address" + +private val VXLAN_LOCAL_TYPE = FlexibleLiteralChoiceTerminal("ipv4_link_local", "ipv6_link_local", "dhcp4", "dhcp6", "slaac", "dhcp_pd") + +/** An address literal that is (or, with [multicast] false, is not) a multicast address. */ +private fun vxlanAddress(multicast: Boolean): Combinator = AlternativeCombinator( + NetdevLinkCheckedSpan(IPV6_ADDR, if (multicast) "multicast" else "not multicast") { netdevLinkIsMulticast(it) == multicast }, + NetdevLinkCheckedSpan(IPV4_ADDR, if (multicast) "multicast" else "not multicast") { netdevLinkIsMulticast(it) == multicast }, +) + +/** `Group=`: a multicast address. */ +private val VXLAN_GROUP = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(vxlanAddress(multicast = true), EOF())) + +/** `Remote=`: a unicast address. */ +private val VXLAN_REMOTE = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(vxlanAddress(multicast = false), EOF())) + +/** `Local=`: a local-address type name, or a unicast address. Addresses first, so the name terminal cannot take a prefix. */ +private val VXLAN_LOCAL = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(AlternativeCombinator(vxlanAddress(multicast = false), VXLAN_LOCAL_TYPE), EOF())) + /** * Validator for `[VXLAN] Group=`, `Local=` and `Remote=` (.netdev). * - * C function: config_parse_vxlan_address. The value is an IPv4 or IPv6 literal - * (in_addr_from_string_auto → inet_pton). For `Local=` only, one of the netdev_local_address_type_table - * names (`ipv4_link_local`, `ipv6_link_local`, `dhcp4`, `dhcp6`, `slaac`, `dhcp_pd`) is accepted - * first, telling networkd to pick up the address that mechanism assigned. + * C function: config_parse_vxlan_address, which branches on the key name. The value is an IPv4 or IPv6 + * literal (in_addr_from_string_auto → inet_pton). For `Local=` only, one of the + * netdev_local_address_type_table names (`ipv4_link_local`, `ipv6_link_local`, `dhcp4`, `dhcp6`, `slaac`, + * `dhcp_pd`) is accepted first, telling networkd to pick up the address that mechanism assigned. `Group=` + * must be a multicast address; `Local=` and `Remote=` must not be. * - * One grammar serves all three keys because validators are keyed by (parser, ltype), not by key, so - * this is deliberately lenient in two ways that depend on the key name: the local-address type names - * are accepted for `Group=`/`Remote=` too, and the multicast checks (`Group=` must be multicast, - * `Local=`/`Remote=` must not be) are not modelled. Addresses come before the names so the - * name terminal's loose shape cannot swallow a leading fragment of an address. + * Validators are looked up by (parser, ltype), not by key, so the key-specific rules are applied by + * choosing a per-key grammar in [generateProblemDescriptors]. [combinator] (used for colouring and + * completion) stays the union of the three. All of this was confirmed against systemd's parser built at + * 0f0682b773: `Group=dhcp4`, `Remote=slaac`, `Group=192.168.1.1`, `Remote=239.1.1.1` and + * `Local=239.1.1.1` are each logged and ignored. */ class ConfigParseVxlanAddressOptionValue : SimpleGrammarOptionValues( - "config_parse_vxlan_address", - SequenceCombinator( - AlternativeCombinator( - IPV6_ADDR, - IPV4_ADDR, - FlexibleLiteralChoiceTerminal("ipv4_link_local", "ipv6_link_local", "dhcp4", "dhcp6", "slaac", "dhcp_pd"), - ), - EOF() - ) -) + VXLAN_VALIDATOR, + SequenceCombinator(AlternativeCombinator(IPV6_ADDR, IPV4_ADDR, VXLAN_LOCAL_TYPE), EOF()) +) { + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + when (property.key) { + "Group" -> VXLAN_GROUP.generateProblemDescriptors(property, holder) + "Remote" -> VXLAN_REMOTE.generateProblemDescriptors(property, holder) + "Local" -> VXLAN_LOCAL.generateProblemDescriptors(property, holder) + else -> super.generateProblemDescriptors(property, holder) + } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt index 5f39e63f..81ac64ee 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -513,3 +513,81 @@ val IP_ADDR_AND_SAFE_PREFIX: Combinator = AlternativeCombinator( SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, netdevLinkSafeAtou(129)))), SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, netdevLinkSafeAtou(33)))), ) + + +// --------------------------------------------------------------------------------------------------- +// Checks on a whole matched span — for rules systemd applies to the parsed value rather than its shape. + +/** + * Matches exactly what [inner] matches, but the match is only semantically valid when [check] accepts + * the matched text. For limits systemd applies after parsing, which a grammar cannot spell out: a + * magnitude bound on a parse_size() total, or "must (not) be a multicast address". + * + * Under the parse engine a rejected span marks its first token invalid; under the classic engine the + * semantic match fails with no progress at the span's start. Either way the first token of the span is + * highlighted, and the value keeps the structured grammar's colouring. + */ +class NetdevLinkCheckedSpan( + private val inner: Combinator, + private val description: String, + private val check: (String) -> Boolean, +) : Combinator { + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val match = inner.SemanticMatch(value, offset) + if (match.matchResult == -1 || check(value.substring(offset, match.matchResult))) return match + return NoMatch.copy(longestMatch = offset) + } + + override fun parse(value: String, offset: Int): Sequence = + inner.parse(value, offset).map { step -> + if (step is Parse && step.tokens.isNotEmpty() && !check(value.substring(offset, step.end))) { + step.copy(tokens = listOf(step.tokens.first().copy(valid = false)) + step.tokens.drop(1)) + } else { + step + } + } + + override fun toString(): String = "Checked($description)" + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "Checked($description,\n" + inner.toStringIndented(indent + 1) + ")" +} + +/** + * in_addr_is_multicast() (src/basic/in-addr-util.c) on an address literal [IP_ADDR] has matched: IPv4 + * in 224.0.0.0/4 (IN_MULTICAST), IPv6 in ff00::/8 (IN6_IS_ADDR_MULTICAST). The first IPv6 byte is 0xff + * only when the first group is written with four hex digits starting `ff` — `ff::1` is 0x00ff::1, and an + * address starting `::` (including IPv4-mapped `::ffff:239.1.1.1`) has a zero first byte. + */ +fun netdevLinkIsMulticast(address: String): Boolean { + if (':' in address) { + val first = address.substringBefore(':') + return first.length == 4 && first.startsWith("ff", ignoreCase = true) + } + return address.substringBefore('.').toIntOrNull() in 224..239 +} + +/** + * The total parse_size(text, 1000, …) (src/basic/parse-util.c) computes for a value [siSize] has + * matched: each element contributes `l * factor + (unsigned long long) (frac * factor)`. Exact decimal + * arithmetic stands in for the C double, which only matters within a unit of the truncated fraction. + * Null if [text] does not have [siSize]'s shape. + */ +fun netdevLinkParseSizeSi(text: String): BigInteger? { + val factors = mapOf("E" to 18, "P" to 15, "T" to 12, "G" to 9, "M" to 6, "K" to 3, "B" to 0, "" to 0) + val element = Regex("""\s*\+?([0-9]+)(?:\.([0-9]*))?\s*([EPTGMKB]?)""") + var total = BigInteger.ZERO + var pos = 0 + while (pos < text.length) { + val m = element.matchAt(text, pos) ?: return null + if (m.value.isEmpty()) return null + val factor = BigInteger.TEN.pow(factors.getValue(m.groupValues[3])) + val fraction = m.groupValues[2].takeIf { it.isNotEmpty() } + ?.let { java.math.BigDecimal("0.$it").multiply(java.math.BigDecimal(factor)).toBigInteger() } ?: BigInteger.ZERO + total += BigInteger(m.groupValues[1]) * factor + fraction + pos = m.range.last + 1 + } + return total +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt index ee8e4a8a..8b5eef20 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt @@ -21,8 +21,9 @@ class ConfigParseBadadvBandwidthOptionValueTest : AbstractUnitFileTest() { GatewayBandwidthUp=3.5G3B GatewayBandwidthDown=4T3G3B GatewayBandwidthUp=+5K - GatewayBandwidthDown=12P - GatewayBandwidthUp=3E 2P + GatewayBandwidthDown=429496729599999 + GatewayBandwidthUp=429T 496G 729M 599K 999 + GatewayBandwidthDown=429.496729599999T GatewayBandwithDown=100K GatewayBandwithUp=1T GatewayBandwidthDown=111.4 @@ -48,12 +49,19 @@ class ConfigParseBadadvBandwidthOptionValueTest : AbstractUnitFileTest() { GatewayBandwidthUp=3. 0 K GatewayBandwidthDown=fast GatewayBandwithUp=1 Mbit + GatewayBandwidthDown=12P + GatewayBandwidthUp=3E 2P + GatewayBandwidthDown=429496729600000 + GatewayBandwidthUp=429.4967296T + GatewayBandwidthDown=0.5P + GatewayBandwidthUp=20E + GatewayBandwidthDown=18446744073709551616 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(8, highlights) + assertSize(15, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt index 1b857168..124aa131 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt @@ -13,6 +13,9 @@ class ConfigParseGeneveAddressOptionValueTest : AbstractUnitFileTest() { [GENEVE] Remote=10.0.0.2 Remote=2001:db8::1 + Remote=223.255.255.255 + Remote=240.0.0.1 + Remote=ff::1 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -30,12 +33,14 @@ class ConfigParseGeneveAddressOptionValueTest : AbstractUnitFileTest() { Remote=10.0.0 Remote=geneve.example.com Remote=10.0.0.2/24 + Remote=239.1.1.1 + Remote=ff02::1 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(3, highlights) + assertSize(5, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt index 8c1f9874..7be9e45d 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt @@ -36,6 +36,7 @@ class ConfigParseMacsecKeyIdOptionValueTest : AbstractUnitFileTest() { [MACsecTransmitAssociation] KeyId=1 KeyId=255 + KeyId=0 KeyId=0x01 KeyId=00112233445566778899aabbccddeeff00 [MACsecReceiveAssociation] @@ -46,6 +47,6 @@ class ConfigParseMacsecKeyIdOptionValueTest : AbstractUnitFileTest() { enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(5, highlights) + assertSize(6, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt index 2122f65d..285514e7 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt @@ -36,12 +36,16 @@ class ConfigParsePortRangeOptionValueTest : AbstractUnitFileTest() { PortRange=-5 PortRange=5- PortRange=1000 -2000 + PortRange=+0b1-2 + PortRange=-0b0-2 + PortRange=5--0 + PortRange=-0-5 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(6, highlights) + assertSize(10, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt index 14e143dd..281b283e 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt @@ -19,6 +19,9 @@ class ConfigParseVlanQosMapsOptionValueTest : AbstractUnitFileTest() { IngressQOSMaps=1-7 2-6 EgressQOSMaps=1-"2" "1"-2 3 IngressQOSMaps=5--0 + EgressQOSMaps=0-7 + IngressQOSMaps=1-5 + EgressQOSMaps=4294967295-1 """.trimIndent() setupFileInEditor("file.netdev", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt index 388d0cf5..69c55363 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt @@ -18,6 +18,9 @@ class ConfigParseVrfTableOptionValueTest : AbstractUnitFileTest() { Table=0x2a Table=custom-table Table=4294967295 + Table=yes + Table=0x0 + Table=mian """.trimIndent() setupFileInEditor("file.netdev", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt index b37ef77a..e3d255a6 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt @@ -22,6 +22,13 @@ class ConfigParseVxlanAddressOptionValueTest : AbstractUnitFileTest() { Remote=10.0.0.2 Remote=2001:db8::1 Remote=::ffff:10.0.0.2 + Group=FF05::2 + Group=224.0.0.1 + Group=239.255.255.255 + Remote=223.255.255.255 + Remote=fe80::1 + Local=::ffff:239.1.1.1 + Local=dhcp6 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -42,12 +49,22 @@ class ConfigParseVxlanAddressOptionValueTest : AbstractUnitFileTest() { Remote=10.0.0.256 Local=192.168.1.1/24 Group=fe80::1::2 + Group=dhcp4 + Remote=slaac + Group=192.168.1.1 + Group=240.0.0.1 + Group=ff::1 + Group=::ffff:239.1.1.1 + Remote=239.1.1.1 + Remote=ff02::1 + Local=239.1.1.1 + Local=ff02::1 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(6, highlights) + assertSize(16, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt index b43ad1f0..7744f10b 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt @@ -21,6 +21,7 @@ class ConfigParseWireguardEndpointOptionValueTest : AbstractUnitFileTest() { Endpoint=[fe80::1%eth0]:51820 Endpoint=host:65535 Endpoint=a\255b.example.com:51820 + Endpoint=:51820 """.trimIndent() setupFileInEditor("file.netdev", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt index 3b84706e..daa1e581 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt @@ -23,6 +23,7 @@ class ConfigParseWireguardRouteTableOptionValueTest : AbstractUnitFileTest() { RouteTable=4294967295 RouteTable=custom RouteTable=vpn-table + RouteTable=yes """.trimIndent() setupFileInEditor("file.netdev", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt index 9e57ff29..1df5472e 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt @@ -115,4 +115,22 @@ class BatchNetdevLinkCombinatorsTest { assertAccepts(ROUTE_TABLE, true, "main", "local", "default", "1", "0x10", "4294967295", "custom", "1abc", "-1") assertAccepts(ROUTE_TABLE, false, "0", "00", "4294967296", "a b", "name:1") } + + @Test + fun testCheckedSpanRejectsWhatItsCheckRefusesOnBothEngines() { + val multicast = NetdevLinkCheckedSpan(IP_ADDR, "multicast") { netdevLinkIsMulticast(it) } + assertAccepts(multicast, true, "224.0.0.1", "239.255.255.255", "ff02::1", "FF05::2") + assertAccepts(multicast, false, "223.255.255.255", "240.0.0.1", "ff::1", "::ffff:239.1.1.1", "10.0.0.1", "239.1.1") + } + + @Test + fun testParseSizeSiTotals() { + assertEquals(java.math.BigInteger("12000000000000000"), netdevLinkParseSizeSi("12P")) + assertEquals(java.math.BigInteger("3002000000000000000"), netdevLinkParseSizeSi("3E 2P")) + assertEquals(java.math.BigInteger("4011500"), netdevLinkParseSizeSi("4 M 11.5K")) + assertEquals(java.math.BigInteger("10000000"), netdevLinkParseSizeSi("10.M")) + assertEquals(java.math.BigInteger("111"), netdevLinkParseSizeSi("111.4")) + assertEquals(java.math.BigInteger("5000"), netdevLinkParseSizeSi("+5K")) + assertEquals(null, netdevLinkParseSizeSi("5k")) + } } From 599b93fa95293f64a45141ee56dbea8a6cef0fdb Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 07:40:32 -0700 Subject: [PATCH 7/8] test: cover case handling in netdev-link validators The branch had no private case-insensitive boolean copies or names-or-boolean terminals to restructure; its only boolean use (BOOLEAN_FALSE in the WireGuard RouteTable= validators) now inherits case-insensitivity from 242.x. Lock that in with mixed-case false values, and add mixed-case names that systemd's case-sensitive streq/string-table lookups reject (verified with sd-check at 0f0682b773) for IRQAffinityNUMA=, L2TP Local=, ReceivePacketSteeringCPUMask= and VXLAN Local=. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ai/ConfigParseIrqAffinityNumaOptionValueTest.kt | 3 ++- .../ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt | 4 +++- .../inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt | 4 +++- .../inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt | 4 +++- .../ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt | 4 ++++ .../ai/ConfigParseWireguardRouteTableOptionValueTest.kt | 4 ++++ 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt index 33b41a64..9cd4aa66 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt @@ -34,12 +34,13 @@ class ConfigParseIrqAffinityNumaOptionValueTest : AbstractUnitFileTest() { IRQAffinityNUMA=-1 IRQAffinityNUMA=remote IRQAffinityNUMA=1.5 + IRQAffinityNUMA=Local """.trimIndent() setupFileInEditor("file.link", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(4, highlights) + assertSize(5, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt index 1fcec3c5..daa73eba 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt @@ -44,12 +44,14 @@ class ConfigParseL2tpTunnelLocalAddressOptionValueTest : AbstractUnitFileTest() Local=192.168.30.256 Local=auto@123 Local=@ + Local=Auto + Local=STATIC@eth0 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(6, highlights) + assertSize(8, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt index 2aca8447..6a8927d8 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt @@ -37,12 +37,14 @@ class ConfigParseRpsCpuMaskOptionValueTest : AbstractUnitFileTest() { ReceivePacketSteeringCPUMask=all 1 ReceivePacketSteeringCPUMask=none ReceivePacketSteeringCPUMask=-0 + ReceivePacketSteeringCPUMask=All + ReceivePacketSteeringCPUMask=Disable """.trimIndent() setupFileInEditor("file.link", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(5, highlights) + assertSize(7, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt index e3d255a6..590ef709 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt @@ -59,12 +59,14 @@ class ConfigParseVxlanAddressOptionValueTest : AbstractUnitFileTest() { Remote=ff02::1 Local=239.1.1.1 Local=ff02::1 + Local=DHCP4 + Local=Slaac """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(16, highlights) + assertSize(18, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt index 9fb18e7f..cc1b4b0b 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt @@ -15,6 +15,10 @@ class ConfigParseWireguardPeerRouteTableOptionValueTest : AbstractUnitFileTest() RouteTable=no RouteTable=0 RouteTable=false + RouteTable=OFF + RouteTable=No + RouteTable=fAlSe + RouteTable=F RouteTable=1234 RouteTable=main RouteTable=local diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt index daa1e581..cb14e3ca 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt @@ -15,6 +15,10 @@ class ConfigParseWireguardRouteTableOptionValueTest : AbstractUnitFileTest() { RouteTable=no RouteTable=0 RouteTable=false + RouteTable=OFF + RouteTable=No + RouteTable=fAlSe + RouteTable=F RouteTable=1234 RouteTable=main RouteTable=local From 2f351fbed50965d70f281883b3963fd27860d366 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 11:38:15 -0700 Subject: [PATCH 8/8] refactor: use the shared SafeAtoTerminal in netdev-link validators (#563) Replace the branch's own safe_atou copy (NetdevLinkSafeAtouTerminal, netdevLinkSafeAtou, UINT16_LIMIT, NETDEV_LINK_UINT32_LIMIT) and the parseRange/parseRangeAccepts/extractedRangeWord/optionallyQuoted helpers with SafeAtoTerminal written out at each call site, and switch [VRF] Table=, [WireGuard] RouteTable= and [WireGuardPeer] RouteTable= to the shared ROUTE_TABLE_FROM_STRING (deleting the local ROUTE_TABLE). The WireGuard endpoint port now models parse_ip_port's SAFE_ATO_REFUSE_LEADING_WHITESPACE. Words from whitespace-split lists (IRQAffinity=, *QOSMaps=, AllowedIPs=) use acceptsWhitespace = false. Verdict changes, all checked with sd-check at 0f0682b773: - now accepted: `0b+1`, `0b 1`, `0o 7`, `0b -0` and `0b-0` in the whole-value numbers (DefaultPVID=, PartitionKey=, IPv6FlowLabel=, IRQAffinityNUMA=, PortRange= bounds, Endpoint= port); `0b+1` and `0-0b-0` inside IRQAffinity=/QOSMaps= words; `/0b+1000` in AllowedIPs=; route-table values containing whitespace (possible networkd.conf names) and `0b 1`. - still flagged: `Endpoint=1.2.3.4: 80`, `IRQAffinity=0b 1`, `AllowedIPs=10.0.0.0/0b 1`. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...ConfigParseDefaultPortVlanidOptionValue.kt | 17 +- .../ai/ConfigParseIpoibPkeyOptionValue.kt | 24 +- .../ai/ConfigParseIpv6FlowlabelOptionValue.kt | 15 +- .../ConfigParseIrqAffinityNumaOptionValue.kt | 18 +- .../ai/ConfigParsePortRangeOptionValue.kt | 39 ++- .../ai/ConfigParseVlanQosMapsOptionValue.kt | 75 ++++- .../ai/ConfigParseVrfTableOptionValue.kt | 15 +- ...ConfigParseWireguardEndpointOptionValue.kt | 19 +- ...ParseWireguardPeerRouteTableOptionValue.kt | 15 +- ...nfigParseWireguardRouteTableOptionValue.kt | 15 +- .../grammar/BatchNetdevLinkCombinators.kt | 290 +++++++----------- .../ai/ConfigParseCpuSetOptionValueTest.kt | 9 +- ...igParseDefaultPortVlanidOptionValueTest.kt | 4 + .../ai/ConfigParseIpoibPkeyOptionValueTest.kt | 2 + ...ConfigParseIpv6FlowlabelOptionValueTest.kt | 2 + ...nfigParseIrqAffinityNumaOptionValueTest.kt | 2 + .../ai/ConfigParsePortRangeOptionValueTest.kt | 7 +- .../ConfigParseVlanQosMapsOptionValueTest.kt | 6 +- .../ai/ConfigParseVrfTableOptionValueTest.kt | 13 +- ...ParseWireguardAllowedIpsOptionValueTest.kt | 6 +- ...igParseWireguardEndpointOptionValueTest.kt | 9 +- ...eWireguardPeerRouteTableOptionValueTest.kt | 9 +- ...ParseWireguardRouteTableOptionValueTest.kt | 9 +- .../grammar/BatchNetdevLinkCombinatorsTest.kt | 61 ++-- 24 files changed, 419 insertions(+), 262 deletions(-) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt index 15b71745..4d75d13e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt @@ -4,8 +4,8 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp 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.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [Bridge] DefaultPVID= in a .netdev file. @@ -21,7 +21,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Validator for `[Bridge] DefaultPVID=` (.netdev). * * C function: config_parse_default_port_vlanid. Either `none` (no PVID) or a VLAN id read by - * parse_vlanid (safe_atou16, so hex works) and checked by vlanid_is_valid, i.e. 0…4094. + * parse_vlanid (safe_atou16, so hex, octal and `0b`/`0o` work) and checked by vlanid_is_valid, i.e. 0…4094. * * The man page says "an integer in the range 1…4094", but vlanid_is_valid only checks `id <= 4094`, so * `0` is accepted (and stored as 0, the same as `none`). This follows the C. An invalid value is logged @@ -33,7 +33,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram class ConfigParseDefaultPortVlanidOptionValue : SimpleGrammarOptionValues( "config_parse_default_port_vlanid", SequenceCombinator( - AlternativeCombinator(FlexibleLiteralChoiceTerminal("none"), netdevLinkSafeAtou(4095L)), + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("none"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4094L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt index acb390d1..b75098ef 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt @@ -3,9 +3,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.AlternativeCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT16_LIMIT -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [IPoIB] PartitionKey= in a .netdev file. @@ -26,7 +25,26 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram class ConfigParseIpoibPkeyOptionValue : SimpleGrammarOptionValues( "config_parse_ipoib_pkey", SequenceCombinator( - AlternativeCombinator(netdevLinkSafeAtou(0x8000L, 1), netdevLinkSafeAtou(UINT16_LIMIT, 0x8001L)), + AlternativeCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 0x7fffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0x8001L, + maxInclusive = 0xffffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt index f50ffb85..2b7c51d3 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt @@ -4,8 +4,8 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp 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.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [Tunnel] IPv6FlowLabel= in a .netdev file. @@ -26,7 +26,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram class ConfigParseIpv6FlowlabelOptionValue : SimpleGrammarOptionValues( "config_parse_ipv6_flowlabel", SequenceCombinator( - AlternativeCombinator(FlexibleLiteralChoiceTerminal("inherit"), netdevLinkSafeAtou(0x100000L)), + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("inherit"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 0xfffffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt index 7b961a75..ac5afd41 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt @@ -4,9 +4,8 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp 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.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETDEV_LINK_UINT32_LIMIT -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [Link] IRQAffinityNUMA= in a .link file. @@ -22,13 +21,24 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * * C function: config_parse_irq_affinity_numa. Either `local` (the device's own NUMA node) or a NUMA * node number read with safe_atou() (so hex works). UINT_MAX and UINT_MAX - 1 are reserved as the - * "unset" and "local" markers, so the number must be below 4294967294. An empty assignment unsets it. + * "unset" and "local" markers, so the number must be at most 4294967293. An empty assignment unsets it. * An out-of-range number is logged and ignored by udev. */ class ConfigParseIrqAffinityNumaOptionValue : SimpleGrammarOptionValues( "config_parse_irq_affinity_numa", SequenceCombinator( - AlternativeCombinator(FlexibleLiteralChoiceTerminal("local"), netdevLinkSafeAtou(NETDEV_LINK_UINT32_LIMIT - 2)), + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("local"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_293L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), EOF() ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt index d9caabc3..ec880400 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt @@ -2,9 +2,10 @@ 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.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT16_LIMIT -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseRange +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne /* * [VXLAN] PortRange= in a .netdev file. @@ -19,8 +20,11 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Validator for `[VXLAN] PortRange=` (.netdev). * * C function: config_parse_port_range → parse_ip_port_range(…, allow_zero = false): a single port or a - * `low-high` range, each bound a safe_atou() number (so hex works) in 1…65535. safe_atou() skips leading - * whitespace, so `1000- 2000` is legal (but `1000 -2000` is not: the lower word keeps its trailing space). + * `low-high` range, each bound a safe_atou() number (hex, octal and `0b`/`0o` work) in 1…65535. + * parse_range() cuts the whole value at the first `-`, so the lower bound never sees a minus (`-0` is an empty + * word) and the upper bound is the rest. Whitespace inside the value reaches safe_atou(), which skips it before + * the number, so `1000- 2000`, `0b 1-5` and `1-0b 1` are legal (but `1000 -2000` is not: the lower word keeps + * its trailing space). * A range with `high < low` is refused by systemd after parsing; that compares numbers and is not * modelled. An invalid value is logged and ignored by systemd. There is no isempty() branch, so `PortRange=` * is a parse error in systemd rather than a reset; the plugin never runs a grammar on an empty value, so it @@ -28,5 +32,30 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram */ class ConfigParsePortRangeOptionValue : SimpleGrammarOptionValues( "config_parse_port_range", - SequenceCombinator(parseRange(UINT16_LIMIT, 1, spaceAfterDash = true), EOF()) + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = false, + ), + ZeroOrOne( + SequenceCombinator( + HYPHEN, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + ), + EOF(), + ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt index 6f173bf9..66fccd7e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt @@ -1,12 +1,19 @@ 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.ExtractedWordTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETDEV_LINK_UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.extractedRangeWord +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WordEnd 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.netdevLinkParseRangeAccepts +import java.math.BigInteger /* * [VLAN] EgressQOSMaps= / IngressQOSMaps= in a .netdev file. @@ -18,15 +25,73 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) */ -private val QOS_MAPPING = extractedRangeWord(NETDEV_LINK_UINT32_LIMIT, 0L, """\s""", cunescape = true) +// parse_range() bounds as safe_atou() reads them (any unsigned int). The word was split on whitespace, so no +// whitespace reaches safe_atou(). The lower bound is cut at the first `-`, so it never sees a minus; the upper +// bound is the rest of the word, which may (`1--0`). +private val QOS_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = false, + acceptsMinus = false, +) +private val QOS_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = false, + acceptsMinus = true, +) +private val QOS_RANGE = SequenceCombinator(QOS_LOWER, ZeroOrOne(SequenceCombinator(HYPHEN, QOS_UPPER))) + +// The same bounds for a word whose quotes and escapes have been removed: quoted or escaped whitespace does reach +// safe_atou(). +private val QOS_UNQUOTED_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = false, +) +private val QOS_UNQUOTED_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// One word: the structured `from[-to]` grammar, bare or quoted as a whole, for highlighting; then an +// [ExtractedWordTerminal] fallback for quotes and C escapes anywhere in the word. +private val QOS_MAPPING = AlternativeCombinator( + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), QOS_RANGE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), QOS_RANGE, LiteralChoiceTerminal("'")), + QOS_RANGE, + ), + WordEnd("""\s"""), + ), + ExtractedWordTerminal("""\s""", cunescape = true, "range 0..4294967295") { word -> + netdevLinkParseRangeAccepts(word, QOS_UNQUOTED_LOWER, QOS_UNQUOTED_UPPER) + }, +) /** * Validator for `[VLAN] EgressQOSMaps=` and `IngressQOSMaps=` (.netdev). * * C function: config_parse_vlan_qos_maps. A whitespace-separated list of words (each may be quoted, and - * may contain C escapes — the quotes and escapes are removed before parsing, see [extractedRangeWord]); + * may contain C escapes — the quotes and escapes are removed before parsing, see [ExtractedWordTerminal]); * every word goes to parse_range(), so it is `from-to` or a single number meaning `from == to`, each - * side an unsigned int as safe_atou() reads it (decimal, `0x` hex, octal, `0b`/`0o`). + * side an unsigned int as safe_atou() reads it ([SafeAtoTerminal]: decimal, `0x` hex, octal, `0b`/`0o`). * * The man page says each integer is in 1…4294967294 and that `from` must be >= `to`; the parser * enforces neither (it stores whatever parse_range returns), so this follows the C and accepts the full diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt index ef19bed6..fbb9ea6a 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.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.ROUTE_TABLE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING /* * [VRF] Table= in a .netdev file. @@ -17,17 +17,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Validator for `[VRF] Table=` (.netdev). * * C function: config_parse_vrf_table → manager_get_route_table_from_string: `default`, `main`, - * `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 (safe_atou32, so hex - * works; `0` is refused with ERANGE). See [ROUTE_TABLE]. + * `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 (safe_atou32, so hex, + * octal and `0b`/`0o` work; `0` is refused with ERANGE). See [ROUTE_TABLE_FROM_STRING]. * - * Because table names are user-defined, any word without whitespace or `:` that is not all digits is - * accepted as a potential name. What is flagged: `0` and out-of-range numbers, values containing - * whitespace, and `name:number` pairs. An invalid value is logged and ignored by systemd. + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name. What is flagged: `0`, out-of-range or + * malformed all-digit numbers (`00`, `08`), and anything containing `:` (`name:number` is networkd.conf + * syntax, not a table reference). An invalid value is logged and ignored by systemd. * * There is no isempty() branch, so an empty `Table=` is a parse error in systemd (logged and ignored), not * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. */ class ConfigParseVrfTableOptionValue : SimpleGrammarOptionValues( "config_parse_vrf_table", - ROUTE_TABLE + ROUTE_TABLE_FROM_STRING ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt index ac1022e8..2526f660 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt @@ -9,10 +9,9 @@ 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.LiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT16_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkSafeAtou /* * [WireGuardPeer] Endpoint= in a .netdev file. @@ -27,8 +26,17 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L17 Endpoint=@network.wireguard.peer0.endpoint */ -// parse_ip_port: a safe_atou16 number (base 0), not zero. -private val ENDPOINT_PORT = netdevLinkSafeAtou(UINT16_LIMIT, 1) +// parse_ip_port: safe_atou16_full(…, SAFE_ATO_REFUSE_LEADING_WHITESPACE) (base 0), then 0 is refused. The port is +// everything after the last `:`, so whitespace can reach the parser: `\v80` and `0b 1` are accepted, ` 80` is not. +private val ENDPOINT_PORT = SafeAtoTerminal( + signed = false, + base = 0, + flags = setOf(SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, +) // The hostname fallback: everything up to the LAST colon (strrchr), checked with dns_name_is_valid(): // dot-separated labels of at most 63 characters, each character printable (>= ' ', not DEL) or one of @@ -52,7 +60,8 @@ private val ENDPOINT_HOST = RegexTerminal( * - otherwise `host:port`, split at the last colon, where the host only has to pass * dns_name_is_valid(), which is permissive (any printable characters, labels up to 63 bytes). * - * The port is a parse_ip_port() number, 1…65535 (safe_atou16 base 0, so hex works). Because the host + * The port is a parse_ip_port() number, 1…65535 (safe_atou16 base 0, so hex, octal and `0b`/`0o` work, and + * a leading space is refused while a leading `\v` is not). Because the host * fallback is so permissive, something like `[fe80::1%eth0]:51820` is accepted (as a hostname), as it * is by systemd at parse time. What is flagged: a missing or invalid port, empty labels, over-long labels * and control characters. Lenient: the overall name length (DNS_HOSTNAME_MAX, enforced by diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt index b5d35631..14ab1c47 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt @@ -4,7 +4,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_FALSE import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator /* @@ -24,17 +24,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * C function: config_parse_wireguard_peer_route_table. A value that parse_boolean() reads as false (`no`, `off`, `0`, …) * disables route configuration; anything else is a route table for manager_get_route_table_from_string: * `default`, `main`, `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 - * (safe_atou32, so hex works). See [ROUTE_TABLE]. An empty assignment falls back to the `[WireGuard]` section's table. + * (safe_atou32, so hex, octal and `0b`/`0o` work). See [ROUTE_TABLE_FROM_STRING]. An empty assignment falls back to the `[WireGuard]` section's table. * - * Because table names are user-defined, any word without whitespace or `:` that is not all digits is - * accepted as a potential name — including `yes`, which is only valid if networkd.conf defines it. - * What is flagged: out-of-range numbers, values containing whitespace, and `name:number` pairs (that is - * networkd.conf syntax, not a table reference). An invalid value is logged and ignored by systemd. + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name — including `yes`, which is only valid if + * networkd.conf defines it. What is flagged: out-of-range or malformed all-digit numbers (`4294967296`, + * `08`) and anything containing `:` (`name:number` is networkd.conf syntax, not a table reference). An + * invalid value is logged and ignored by systemd. */ class ConfigParseWireguardPeerRouteTableOptionValue : SimpleGrammarOptionValues( "config_parse_wireguard_peer_route_table", AlternativeCombinator( SequenceCombinator(BOOLEAN_FALSE, EOF()), - ROUTE_TABLE, + ROUTE_TABLE_FROM_STRING, ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt index 7ab5147f..0c1e49e5 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt @@ -4,7 +4,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_FALSE import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator /* @@ -24,17 +24,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * C function: config_parse_wireguard_route_table. A value that parse_boolean() reads as false (`no`, `off`, `0`, …) * disables route configuration; anything else is a route table for manager_get_route_table_from_string: * `default`, `main`, `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 - * (safe_atou32, so hex works). See [ROUTE_TABLE]. An empty assignment also disables it. + * (safe_atou32, so hex, octal and `0b`/`0o` work). See [ROUTE_TABLE_FROM_STRING]. An empty assignment also disables it. * - * Because table names are user-defined, any word without whitespace or `:` that is not all digits is - * accepted as a potential name — including `yes`, which is only valid if networkd.conf defines it. - * What is flagged: out-of-range numbers, values containing whitespace, and `name:number` pairs (that is - * networkd.conf syntax, not a table reference). An invalid value is logged and ignored by systemd. + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name — including `yes`, which is only valid if + * networkd.conf defines it. What is flagged: out-of-range or malformed all-digit numbers (`4294967296`, + * `08`) and anything containing `:` (`name:number` is networkd.conf syntax, not a table reference). An + * invalid value is logged and ignored by systemd. */ class ConfigParseWireguardRouteTableOptionValue : SimpleGrammarOptionValues( "config_parse_wireguard_route_table", AlternativeCombinator( SequenceCombinator(BOOLEAN_FALSE, EOF()), - ROUTE_TABLE, + ROUTE_TABLE_FROM_STRING, ) ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt index 81ac64ee..10ad2f15 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -15,151 +15,21 @@ import java.math.BigInteger */ // --------------------------------------------------------------------------------------------------- -// safe_atou() and friends — src/basic/parse-util.c (safe_atou_full, mangle_base). +// parse_range() — src/basic/parse-util.c. Its bounds are safe_atou() numbers, modelled by [SafeAtoTerminal]. /** - * An unsigned number exactly as systemd's `safe_atou*()` helpers read it with base 0. - * - * Compared with [UnsignedNumberTerminal] this also accepts the two spellings that helper family adds - * on top of strtoul(): the Python-style `0b`/`0o` prefixes that mangle_base() strips before calling - * strtoul (so `0b101` and `0o17` are legal), and a leading `+`, which strtoul accepts and - * safe_atou_full only refuses when SAFE_ATO_REFUSE_PLUS_MINUS is set (none of the callers here set it). - * - * Hex (`0x`/`0X`) and C octal (leading `0`) are accepted as for [UnsignedNumberTerminal]; a digit - * string like `089` is number-shaped but not a valid octal literal, so it matches and is then rejected - * — which is what strtoul()+`*x != 0` does too. - * - * A leading `-` is number-shaped (so `-1` is reported as an out-of-range number rather than as junk) - * and valid only when the value is zero: safe_atou_full's only minus check is `l != 0 && s[0] == '-'`, - * so `-0` (and `-0x0`) really are accepted. - * - * @param minInclusive lowest accepted value - * @param maxExclusive one past the highest accepted value - * @param allowMinus false for a position where a `-` cannot reach safe_atou at all — the lower bound - * of a parse_range(), which is cut at the first `-`, so `-0` there is an empty word and EINVAL. - */ -class NetdevLinkSafeAtouTerminal( - private val minInclusive: Long, - private val maxExclusive: Long, - private val allowMinus: Boolean = true, -) : TerminalCombinator { - - private fun span(value: String, offset: Int): String? = (if (allowMinus) SHAPE else SHAPE_NO_MINUS).matchAt(value, offset)?.value - - private fun inRange(text: String): Boolean = inRange(valueOf(text), minInclusive, maxExclusive) - - 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 = "SafeAtou($minInclusive,$maxExclusive${if (allowMinus) "" else ",no-minus"})" - - internal companion object { - // 0b/0o first: as plain digits they would otherwise be read as "0" followed by junk. Within the - // signed group, hex before octal before decimal for the same reason. - val SHAPE = Regex("""0[bB][01]+|0[oO][0-7]+|[+-]?(?:0[xX][0-9a-fA-F]+|0[0-7]+|[0-9]+)""") - val SHAPE_NO_MINUS = Regex("""0[bB][01]+|0[oO][0-7]+|\+?(?:0[xX][0-9a-fA-F]+|0[0-7]+|[0-9]+)""") - - fun inRange(value: BigInteger?, minInclusive: Long, maxExclusive: Long): Boolean = - value != null && value >= BigInteger.valueOf(minInclusive) && value < BigInteger.valueOf(maxExclusive) - - /** - * What safe_atou() makes of the whole string [s], or null when it refuses it: leading whitespace is - * skipped (`s += strspn(s, WHITESPACE)`), then the rest must be exactly one [SHAPE] literal. - */ - fun safeAtouValue(s: String): BigInteger? { - val text = s.trimStart(' ', '\t', '\n', '\r') - return if (SHAPE.matches(text)) valueOf(text) else null - } - - /** The numeric value of [text], or null when it is not a valid literal in its base. */ - fun valueOf(text: String): BigInteger? { - // mangle_base() only recognises 0b/0o at the very start, so they never carry a sign. - if (text.length > 2 && (text.startsWith("0b") || text.startsWith("0B"))) return text.drop(2).toBigIntegerOrNull(2) - if (text.length > 2 && (text.startsWith("0o") || text.startsWith("0O"))) return text.drop(2).toBigIntegerOrNull(8) - val digits = text.removePrefix("+").removePrefix("-") - val magnitude = when { - digits.length > 2 && (digits.startsWith("0x") || digits.startsWith("0X")) -> digits.drop(2).toBigIntegerOrNull(16) - digits.length > 1 && digits.startsWith("0") -> digits.drop(1).toBigIntegerOrNull(8) - else -> digits.toBigIntegerOrNull() - } - // strtoul() negates a `-` value; safe_atou_full then refuses it unless it is zero. - return if (text.startsWith("-") && magnitude != BigInteger.ZERO) null else magnitude - } - } -} - -/** - * A [NetdevLinkSafeAtouTerminal] over `[minInclusive, maxExclusive)`, coloured as a literal. The [Labeled] wrapper - * is only there because [defaultRole] does not know this terminal class yet (it is transparent to - * matching); once the terminal is promoted next to [UnsignedNumberTerminal] it can go. - */ -fun netdevLinkSafeAtou(maxExclusive: Long, minInclusive: Long = 0L, allowMinus: Boolean = true): Combinator = - Labeled(Role.LITERAL, NetdevLinkSafeAtouTerminal(minInclusive, maxExclusive, allowMinus)) - -/** Exclusive upper bound of a uint16_t. */ -const val UINT16_LIMIT = 65_536L - -/** Exclusive upper bound of a uint32_t / unsigned. */ -const val NETDEV_LINK_UINT32_LIMIT = 4_294_967_296L - -/** - * `N` or `N-M` as parse_range() (src/basic/parse-util.c) reads it, each bound a safe_atou() number in - * `[minInclusive, maxExclusive)`. The value is split on the first `-` (EXTRACT_DONT_COALESCE_SEPARATORS, - * so `-5` and `5-` both fail); the lower part goes to safe_atou() and so can never start with `-` - * (`-0` is an empty lower word), while the upper part is handed to safe_atou() as is, so `5--0` is legal. - * safe_atou() skips leading whitespace, so whitespace may follow the dash — but only where the caller's - * own word splitting cannot cut there, hence [spaceAfterDash] (true for a whole-value range such as - * PortRange=; false inside a whitespace-separated list, where `0- 3` is two words). The `lower <= upper` - * checks that callers make afterwards compare parsed numbers and are not modelled. + * Whether parse_range() (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340) + * accepts the already-extracted [word]: the text before the first `-` must satisfy [lower] and, if there is a `-`, + * the non-empty rest must satisfy [upper]. A predicate for an [ExtractedWordTerminal], whose words have had their + * quotes and escapes removed first; the structured grammar spells the same check out with the terminals. */ -fun parseRange(maxExclusive: Long, minInclusive: Long = 0L, spaceAfterDash: Boolean = false): Combinator = SequenceCombinator( - netdevLinkSafeAtou(maxExclusive, minInclusive, allowMinus = false), - ZeroOrOne( - if (spaceAfterDash) SequenceCombinator(HYPHEN, ZeroOrOne(WhitespaceTerminal()), netdevLinkSafeAtou(maxExclusive, minInclusive)) - else SequenceCombinator(HYPHEN, netdevLinkSafeAtou(maxExclusive, minInclusive)) - ), -) - -/** - * Whether parse_range() accepts the already-extracted [word] with both bounds in - * `[minInclusive, maxExclusive)` — the same check as [parseRange], done on a string rather than as a - * grammar, for words whose quotes or escapes have to be removed first (see [ExtractedWordTerminal]). - */ -fun parseRangeAccepts(word: String, minInclusive: Long, maxExclusive: Long): Boolean { +fun netdevLinkParseRangeAccepts(word: String, lower: SafeAtoTerminal, upper: SafeAtoTerminal): Boolean { val dash = word.indexOf('-') - val lower = if (dash < 0) word else word.substring(0, dash) - if (lower.isEmpty()) return false - if (!NetdevLinkSafeAtouTerminal.inRange(NetdevLinkSafeAtouTerminal.safeAtouValue(lower), minInclusive, maxExclusive)) return false - if (dash < 0) return true - val upper = word.substring(dash + 1) - return upper.isNotEmpty() && NetdevLinkSafeAtouTerminal.inRange(NetdevLinkSafeAtouTerminal.safeAtouValue(upper), minInclusive, maxExclusive) + if (dash < 0) return lower.valueOf(word) != null + val upperText = word.substring(dash + 1) + return lower.valueOf(word.substring(0, dash)) != null && upperText.isNotEmpty() && upper.valueOf(upperText) != null } -/** - * [inner] optionally wrapped in a matching pair of `"…"` or `'…'`, the form a word takes after - * extract_first_word(…, EXTRACT_UNQUOTE) has removed its quotes (systemd's own test-cpu-set-util.c - * writes `8 '9' 10 "11"`). Only whole-word quoting is modelled here; pair it with an - * [ExtractedWordTerminal] fallback (as [extractedRangeWord] does) for quotes that start mid-word. - */ -fun optionallyQuoted(inner: Combinator): Combinator = AlternativeCombinator( - SequenceCombinator(LiteralChoiceTerminal("\""), inner, LiteralChoiceTerminal("\"")), - SequenceCombinator(LiteralChoiceTerminal("'"), inner, LiteralChoiceTerminal("'")), - inner, -) - // --------------------------------------------------------------------------------------------------- // Words as extract_first_word() (src/basic/extract-word.c) returns them. @@ -305,30 +175,70 @@ private fun cunescapeOne(s: String, p: Int): Pair? { } } -/** - * One parse_range() word of a separator-split list: the structured `N[-M]` grammar (optionally quoted - * as a whole) for highlighting, with an [ExtractedWordTerminal] fallback that accepts the same numbers - * however extract_first_word's quoting and escaping spells them (`0-"3"`, `'0'-'3'`). - */ -fun extractedRangeWord(maxExclusive: Long, minInclusive: Long, separators: String, cunescape: Boolean): Combinator = - AlternativeCombinator( - SequenceCombinator(optionallyQuoted(parseRange(maxExclusive, minInclusive)), WordEnd(separators)), - ExtractedWordTerminal(separators, cunescape, "range $minInclusive..${maxExclusive - 1}") { - parseRangeAccepts(it, minInclusive, maxExclusive) - }, - ) - - // --------------------------------------------------------------------------------------------------- // CPU sets — config_parse_cpu_set (src/shared/cpu-set-util.c). -/** The largest CPU index cpu_set_add_range() accepts is CPU_SET_MAX_NCPU - 1 = 8191. */ -private const val CPU_SET_MAX_NCPU = 8192L - // One extract_first_word() separator run: any mix of whitespace and commas (separators coalesce). private val CPU_SET_SEPARATOR = RegexTerminal("""[\s,]+""", """[\s,]+""") -private val CPU_SET_ITEM = extractedRangeWord(CPU_SET_MAX_NCPU, 0L, """\s,""", cunescape = false) +// parse_range() bounds as safe_atou() reads them, below CPU_SET_MAX_NCPU (8192, cpu_set_add_range). The word was +// split on whitespace and commas, so no whitespace can reach safe_atou(). The lower bound is cut at the first `-` +// by parse_range(), so it never sees a minus; the upper bound is the rest of the word, which may (`0--0`). +private val CPU_SET_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = false, +) +private val CPU_SET_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = true, +) +private val CPU_SET_RANGE = SequenceCombinator(CPU_SET_LOWER, ZeroOrOne(SequenceCombinator(HYPHEN, CPU_SET_UPPER))) + +// The same bounds for a word whose quotes have been removed: quoted whitespace does reach safe_atou() (`" 3"`). +private val CPU_SET_UNQUOTED_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = true, + acceptsMinus = false, +) +private val CPU_SET_UNQUOTED_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// One word: the structured `N[-M]` grammar, bare or quoted as a whole, for highlighting; then an +// [ExtractedWordTerminal] fallback for quotes and escapes anywhere in the word (`0-"3"`, `'0'-'3'`). +private val CPU_SET_ITEM = AlternativeCombinator( + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), CPU_SET_RANGE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), CPU_SET_RANGE, LiteralChoiceTerminal("'")), + CPU_SET_RANGE, + ), + WordEnd("""\s,"""), + ), + ExtractedWordTerminal("""\s,""", cunescape = false, "range 0..8191") { word -> + netdevLinkParseRangeAccepts(word, CPU_SET_UNQUOTED_LOWER, CPU_SET_UNQUOTED_UPPER) + }, +) /** * A CPU set: CPU indices and `N-M` ranges separated by whitespace and/or commas. @@ -336,8 +246,8 @@ private val CPU_SET_ITEM = extractedRangeWord(CPU_SET_MAX_NCPU, 0L, """\s,""", c * config_parse_cpu_set splits with extract_first_word(…, WHITESPACE ",", EXTRACT_UNQUOTE), so separators * coalesce, leading and trailing separators are harmless (`0, 1, 2, 3, 4, 5, 6, 7, 63, ` is in * test-cpu-set-util.c), and a word may be quoted — anywhere in the word, not only around it (`0-"3"` is - * `0-3`; see [extractedRangeWord]). Each word goes to parse_range(), whose bounds are - * safe_atou() numbers — so hex works — and cpu_set_add_range() refuses anything at or above + * `0-3`; the [ExtractedWordTerminal] fallback handles that). Each word goes to parse_range(), whose bounds are + * safe_atou() numbers ([SafeAtoTerminal]: hex, octal, `0b`/`0o`) — and cpu_set_add_range() refuses anything at or above * CPU_SET_MAX_NCPU (8192). A range whose lower end exceeds its upper end (`3-0`) is rejected by systemd * after parsing; that compares numbers and is not modelled. */ @@ -480,38 +390,52 @@ val WIREGUARD_BASE64_KEY = RegexTerminal( val CREDENTIAL_REFERENCE: Combinator = SequenceCombinator(LiteralChoiceTerminal("@"), CONDITION_CREDENTIAL) -// --------------------------------------------------------------------------------------------------- -// Route tables — manager_get_route_table_from_string (src/network/networkd-route-util.c). - -// Names an administrator can define with RouteTable=name:number in networkd.conf. That parser splits -// words on whitespace, cuts each at the first `:`, and refuses an empty or all-digit name; any other -// spelling is a potential name, so a non-numeric word here cannot be checked against a closed set. -private val ROUTE_TABLE_NAME = RegexTerminal("""[^\s:]+""", """(?![0-9]+$)[^\s:]+""") - -/** - * A route table: one of the predefined names `default`, `main`, `local`, a name defined in - * networkd.conf, or a non-zero uint32 (safe_atou32, so hex works). Each branch carries its own EOF so - * the classic first-match engine can fall back from the numeric branch to the name branch on `1abc`. - */ -val ROUTE_TABLE: Combinator = AlternativeCombinator( - SequenceCombinator(FlexibleLiteralChoiceTerminal("default", "main", "local"), EOF()), - SequenceCombinator(netdevLinkSafeAtou(NETDEV_LINK_UINT32_LIMIT, 1), EOF()), - SequenceCombinator(ROUTE_TABLE_NAME, EOF()), -) - - // --------------------------------------------------------------------------------------------------- // Address prefixes — in_addr_prefix_from_string_auto_full (src/basic/in-addr-util.c). /** * An IPv4 or IPv6 address with an optional `/prefixlen`, as in_addr_prefix_from_string_auto (mode * PREFIXLEN_FULL) reads it. Like [IP_ADDR_AND_ANY_PREFIX], but the length goes through - * in_addr_parse_prefixlen → safe_atou8, so it is a base-0 number (`/0x18` is 24) up to the family width. - * IPv6 is tried first so the IPv4 branch never strands part of an IPv6 literal. + * in_addr_parse_prefixlen → safe_atou8, so it is a base-0 number (`/0x18` is 24, `/0b+1000` is 8) up to the + * family width. Its only user, AllowedIPs=, splits the list on whitespace first, so no whitespace reaches + * safe_atou8 (`/0b 1` is the two words `…/0b` and `1`). IPv6 is tried first so the IPv4 branch never strands + * part of an IPv6 literal. */ val IP_ADDR_AND_SAFE_PREFIX: Combinator = AlternativeCombinator( - SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, netdevLinkSafeAtou(129)))), - SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, netdevLinkSafeAtou(33)))), + SequenceCombinator( + IPV6_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + ), + ), + SequenceCombinator( + IPV4_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 32L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + ), + ), ) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt index e92da505..06a3074c 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt @@ -23,6 +23,10 @@ class ConfigParseCpuSetOptionValueTest : AbstractUnitFileTest() { IRQAffinity=,0 IRQAffinity=0-"3" IRQAffinity='0'-'3', 4 + IRQAffinity=0b+1 + IRQAffinity=0-0b+1 + IRQAffinity=0-0b-0 + IRQAffinity="0b 1" """.trimIndent() setupFileInEditor("file.link", file) @@ -45,12 +49,15 @@ class ConfigParseCpuSetOptionValueTest : AbstractUnitFileTest() { IRQAffinity=0 1 2 3 "4 5 6 7 IRQAffinity=-0 IRQAffinity=0-"9000" + IRQAffinity=0b-0 + IRQAffinity=0b 1 + IRQAffinity=0-0b 1 """.trimIndent() setupFileInEditor("file.link", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(8, highlights) + assertSize(11, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt index 5baaf66f..44b4c1b6 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt @@ -17,6 +17,10 @@ class ConfigParseDefaultPortVlanidOptionValueTest : AbstractUnitFileTest() { DefaultPVID=4094 DefaultPVID=0 DefaultPVID=0xFFE + DefaultPVID=0b 1 + DefaultPVID=0b+1 + DefaultPVID=0o 7 + DefaultPVID=0b -0 """.trimIndent() setupFileInEditor("file.netdev", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt index 2bd3cdbb..7b252817 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt @@ -17,6 +17,8 @@ class ConfigParseIpoibPkeyOptionValueTest : AbstractUnitFileTest() { PartitionKey=0x7fff PartitionKey=32769 PartitionKey=0b1 + PartitionKey=0b 1 + PartitionKey=0b+1 """.trimIndent() setupFileInEditor("file.netdev", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt index 028cff76..e115519f 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt @@ -16,6 +16,8 @@ class ConfigParseIpv6FlowlabelOptionValueTest : AbstractUnitFileTest() { IPv6FlowLabel=1048575 IPv6FlowLabel=0xFFFFF IPv6FlowLabel=0x12345 + IPv6FlowLabel=0b 1 + IPv6FlowLabel=0b-0 """.trimIndent() setupFileInEditor("file.netdev", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt index 9cd4aa66..2b368796 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt @@ -16,6 +16,8 @@ class ConfigParseIrqAffinityNumaOptionValueTest : AbstractUnitFileTest() { IRQAffinityNUMA=3 IRQAffinityNUMA=0x1 IRQAffinityNUMA=4294967293 + IRQAffinityNUMA=0b 1 + IRQAffinityNUMA=0b+1 """.trimIndent() setupFileInEditor("file.link", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt index 285514e7..96e428c8 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt @@ -16,6 +16,9 @@ class ConfigParsePortRangeOptionValueTest : AbstractUnitFileTest() { PortRange=0x100-0x200 PortRange=32768-61000 PortRange=1000- 2000 + PortRange=0b 1-5 + PortRange=0b+1-5 + PortRange=1-0b 1 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -40,12 +43,14 @@ class ConfigParsePortRangeOptionValueTest : AbstractUnitFileTest() { PortRange=-0b0-2 PortRange=5--0 PortRange=-0-5 + PortRange=0b-0 + PortRange=1-0b -1 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(10, highlights) + assertSize(12, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt index 281b283e..bd29db52 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt @@ -22,6 +22,8 @@ class ConfigParseVlanQosMapsOptionValueTest : AbstractUnitFileTest() { EgressQOSMaps=0-7 IngressQOSMaps=1-5 EgressQOSMaps=4294967295-1 + EgressQOSMaps=0b+1-2 + EgressQOSMaps=1-0b+10 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -44,12 +46,14 @@ class ConfigParseVlanQosMapsOptionValueTest : AbstractUnitFileTest() { IngressQOSMaps=a-b EgressQOSMaps=-0 IngressQOSMaps=1-"2 + EgressQOSMaps=0b 1-2 + EgressQOSMaps=0b-0 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(8, highlights) + assertSize(10, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt index 69c55363..db886ccc 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt @@ -6,6 +6,9 @@ import org.junit.Test class ConfigParseVrfTableOptionValueTest : AbstractUnitFileTest() { + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). @Test fun testValidValues() { // language="unit file (systemd)" @@ -21,6 +24,12 @@ class ConfigParseVrfTableOptionValueTest : AbstractUnitFileTest() { Table=yes Table=0x0 Table=mian + Table=main 42 + Table=1 2 + Table=my table + Table=0b 1 + Table=0b+1 + Table=0o 7 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -37,9 +46,9 @@ class ConfigParseVrfTableOptionValueTest : AbstractUnitFileTest() { [VRF] Table=0 Table=4294967296 - Table=main 42 + Table=08 Table=vrf:42 - Table=1 2 + Table=040000000000 """.trimIndent() setupFileInEditor("file.netdev", file) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt index 3e76d8e0..497fd9dc 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt @@ -20,6 +20,8 @@ class ConfigParseWireguardAllowedIpsOptionValueTest : AbstractUnitFileTest() { AllowedIPs=10.0.0.0/8, 192.168.0.0/16 AllowedIPs=10.0.0.0/8 fd00::/8, AllowedIPs=10.0.0.0/0x8 fd00::/0100 + AllowedIPs=10.0.0.0/0b+1000 + AllowedIPs=10.0.0.0/0o10 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -40,12 +42,14 @@ class ConfigParseWireguardAllowedIpsOptionValueTest : AbstractUnitFileTest() { AllowedIPs=any AllowedIPs=10.0.0/8 AllowedIPs=10.0.0.0/ + AllowedIPs=10.0.0.0/0b 1 + AllowedIPs=10.0.0.0/ 8 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(6, highlights) + assertSize(8, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt index 7744f10b..fe53bfba 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt @@ -22,6 +22,10 @@ class ConfigParseWireguardEndpointOptionValueTest : AbstractUnitFileTest() { Endpoint=host:65535 Endpoint=a\255b.example.com:51820 Endpoint=:51820 + Endpoint=192.168.27.3:0b+1 + Endpoint=192.168.27.3:0b 1 + Endpoint=[2001:db8::1]:0o 7 + Endpoint=host.example:0b 1 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -43,12 +47,15 @@ class ConfigParseWireguardEndpointOptionValueTest : AbstractUnitFileTest() { Endpoint=[2001:db8::1]: Endpoint=wg.example.com:port Endpoint=a\999b:51820 + Endpoint=192.168.27.3: 80 + Endpoint=host.example: 80 + Endpoint=192.168.27.3:0b -0 """.trimIndent() setupFileInEditor("file.netdev", file) enableInspection(InvalidValueInspection::class.java) val highlights = myFixture.doHighlighting() - assertSize(7, highlights) + assertSize(10, highlights) } } diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt index cc1b4b0b..bbc40d13 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt @@ -6,6 +6,9 @@ import org.junit.Test class ConfigParseWireguardPeerRouteTableOptionValueTest : AbstractUnitFileTest() { + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). @Test fun testValidValues() { // language="unit file (systemd)" @@ -27,6 +30,10 @@ class ConfigParseWireguardPeerRouteTableOptionValueTest : AbstractUnitFileTest() RouteTable=4294967295 RouteTable=custom RouteTable=vpn-table + RouteTable=main table + RouteTable=0b 1 + RouteTable=0b+1 + RouteTable=0o 7 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -42,7 +49,7 @@ class ConfigParseWireguardPeerRouteTableOptionValueTest : AbstractUnitFileTest() val file = """ [WireGuardPeer] RouteTable=4294967296 - RouteTable=main table + RouteTable=08 RouteTable=custom:100 RouteTable=00 """.trimIndent() diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt index cb14e3ca..b8212f90 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt @@ -6,6 +6,9 @@ import org.junit.Test class ConfigParseWireguardRouteTableOptionValueTest : AbstractUnitFileTest() { + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). @Test fun testValidValues() { // language="unit file (systemd)" @@ -28,6 +31,10 @@ class ConfigParseWireguardRouteTableOptionValueTest : AbstractUnitFileTest() { RouteTable=custom RouteTable=vpn-table RouteTable=yes + RouteTable=main table + RouteTable=0b 1 + RouteTable=0b+1 + RouteTable=0o 7 """.trimIndent() setupFileInEditor("file.netdev", file) @@ -43,7 +50,7 @@ class ConfigParseWireguardRouteTableOptionValueTest : AbstractUnitFileTest() { val file = """ [WireGuard] RouteTable=4294967296 - RouteTable=main table + RouteTable=08 RouteTable=custom:100 RouteTable=00 """.trimIndent() diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt index 1df5472e..5148c1f5 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt @@ -7,7 +7,8 @@ import org.junit.Test * Grammar-level tests for the shared helpers in BatchNetdevLinkCombinators.kt, run against BOTH matching * engines: the classic SyntacticMatch/SemanticMatch pair and the list-of-successes [validate]. The cases * come from systemd's own unit tests at 0f0682b773 (test-parse-util.c, test-time-util.c, - * test-cpu-set-util.c) where one exists. + * test-cpu-set-util.c) where one exists, otherwise from sd-check. The integer and route-table terminals + * themselves are covered by SafeAtoTerminalTest and RouteTableTest. */ class BatchNetdevLinkCombinatorsTest { @@ -22,19 +23,6 @@ class BatchNetdevLinkCombinatorsTest { } } - @Test - fun testSafeAtouAcceptsEveryBaseSystemdDoes() { - val u16 = netdevLinkSafeAtou(UINT16_LIMIT) - assertAccepts(u16, true, "0", "65535", "0xffff", "0XFF", "0177777", "0b101", "0B1", "0o17", "0O17", "+5", "+0x10", "-0", "-0x0") - assertAccepts(u16, false, "65536", "0x10000", "089", "0b2", "0o8", "-1", "-0x1", "-0b0", "+0b1", "0x", "abc", "1.5") - } - - @Test - fun testSafeAtouHonoursTheLowerBound() { - assertAccepts(netdevLinkSafeAtou(NETDEV_LINK_UINT32_LIMIT, 1), true, "1", "4294967295") - assertAccepts(netdevLinkSafeAtou(NETDEV_LINK_UINT32_LIMIT, 1), false, "0", "00", "0x0", "4294967296") - } - @Test fun testSiSizeMatchesParseSizeSiTests() { // TEST(parse_size_si) successes. @@ -67,14 +55,39 @@ class BatchNetdevLinkCombinatorsTest { assertAccepts(CPU_SET, true, "0-\"3\"", "'0'-'3'", "\"0\"-3, 4", "0-\\3", "\" 3\"", "1-\"0x3\"") // The lower bound of a range is cut at the first `-`, so `-0` is an empty word there. assertAccepts(CPU_SET, false, "-0", "-0-3", "0-\"9000\"", "\"0-3", "\"3 \"", "0-\"\"", "0-", "0- 3") + + // The bounds are SafeAtoTerminal numbers in a word split on whitespace and commas (sd-check, IRQAffinity=): + // a sign after 0b/0o is fine, but `0b 1` is two words and the lower bound of `0b-0` is the bare word `0b`. + assertAccepts(CPU_SET, true, "0b+1", "0-0b+1", "0-0b-0", "0o17", "\"0b 1\"") + assertAccepts(CPU_SET, false, "0b-0", "0b 1", "0-0b 1", "0b-1-3") } @Test - fun testParseRangeBounds() { - assertAccepts(parseRange(100), true, "5", "5-7", "5--0", "0x5-+7", "0") - assertAccepts(parseRange(100), false, "-0", "-5", "5-", "5- 7", "5 -7", "100", "5-100") - assertAccepts(parseRange(UINT16_LIMIT, 1, spaceAfterDash = true), true, "1000- 2000", "1000-2000") - assertAccepts(parseRange(UINT16_LIMIT, 1, spaceAfterDash = true), false, "1000 -2000", "0-5", "5--0") + fun testParseRangeAcceptsSplitsAtTheFirstDash() { + val lower = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 99L, + acceptsWhitespace = true, + acceptsMinus = false, + ) + val upper = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 99L, + acceptsWhitespace = true, + acceptsMinus = true, + ) + for (word in listOf("5", "5-7", "5--0", "0x5-+7", "0", " 5", "5- 7", "0b 1-0b+1")) { + assertEquals("parse_range accepts <$word>", true, netdevLinkParseRangeAccepts(word, lower, upper)) + } + for (word in listOf("", "-0", "-5", "5-", "5 -7", "100", "5-100", "5-0b-1", "0b-0")) { + assertEquals("parse_range refuses <$word>", false, netdevLinkParseRangeAccepts(word, lower, upper)) + } } @Test @@ -111,9 +124,13 @@ class BatchNetdevLinkCombinatorsTest { } @Test - fun testRouteTableAcceptsNamesAndNonZeroNumbers() { - assertAccepts(ROUTE_TABLE, true, "main", "local", "default", "1", "0x10", "4294967295", "custom", "1abc", "-1") - assertAccepts(ROUTE_TABLE, false, "0", "00", "4294967296", "a b", "name:1") + fun testSafePrefixLengthIsASafeAtou8WithinTheFamilyWidth() { + // sd-check (AllowedIPs=): `/0b+1000`, `/0o10` and `0.0.0.0/0b-0` are accepted; `/0b-1` is refused, and so are + // `/0b 1` and `/ 8`, which the list splits into two words. + assertAccepts(IP_ADDR_AND_SAFE_PREFIX, true, "10.0.0.0/8", "10.0.0.0/0x8", "10.0.0.0/0b+1000", "10.0.0.0/0o10", + "0.0.0.0/0b-0", "10.0.0.1", "fd00::/0x40", "::/128") + assertAccepts(IP_ADDR_AND_SAFE_PREFIX, false, "10.0.0.0/33", "10.0.0.0/0b-1", "10.0.0.0/0b 1", "10.0.0.0/ 8", + "fd00::/129", "10.0.0.0/") } @Test