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..e06fda43 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 @@ -412,6 +412,61 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_unit_path_strv_printf", "0") to ConfigParseUnitPathStrvPrintfOptionValue() as OptionValueInformation, Validator("config_parse_socket_service", "0") to ConfigParseSocketServiceOptionValue() as OptionValueInformation, Validator("config_parse_service_sockets", "0") to ConfigParseServiceSocketsOptionValue() as OptionValueInformation, + // Batch 2026-10: network-route-addr + // Generic word-list / string / match parsers. + Validator("config_parse_match_strv", "0") to ConfigParseMatchStrvOptionValue() as OptionValueInformation, + Validator("config_parse_strv", "0") to ConfigParseStrvOptionValue() as OptionValueInformation, + Validator("config_parse_match_property", "0") to ConfigParseMatchPropertyOptionValue() as OptionValueInformation, + Validator("config_parse_string", "CONFIG_PARSE_STRING_SAFE_AND_ASCII") to ConfigParseStringSafeAsciiOptionValue() as OptionValueInformation, + Validator("config_parse_nft_set", "NFT_SET_PARSE_NETWORK") to ConfigParseNftSetNetworkOptionValue() as OptionValueInformation, + Validator("config_parse_timezone", "0") to ConfigParseTimezoneOptionValue() as OptionValueInformation, + Validator("config_parse_in_addr_non_null", "AF_INET6") to ConfigParseInAddrNonNullIpv6OptionValue() as OptionValueInformation, + Validator("config_parse_neighbor_proxy_address", "0") to ConfigParseNeighborProxyAddressOptionValue() as OptionValueInformation, + // [Network] stacked netdevs: the ltype only records the expected kind; every slot takes netdev names. + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPOIB") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPVLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPVTAP") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACSEC") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACVLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACVTAP") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_VLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_VXLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_XFRM") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "_NETDEV_KIND_TUNNEL") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + // [IPv6SendRA] / [IPv6Prefix] / [IPv6RoutePrefix] / [IPv6PREF64Prefix] / [IPv6AcceptRA]. + Validator("config_parse_router_prefix_delegation", "0") to ConfigParseRouterPrefixDelegationOptionValue() as OptionValueInformation, + Validator("config_parse_router_uint32_msec_usec", "0") to ConfigParseRouterUint32MsecUsecOptionValue() as OptionValueInformation, + Validator("config_parse_router_lifetime", "0") to ConfigParseRouterLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_router_home_agent_lifetime", "0") to ConfigParseRouterHomeAgentLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_route_prefix", "0") to ConfigParseRoutePrefixOptionValue() as OptionValueInformation, + Validator("config_parse_route_prefix_lifetime", "0") to ConfigParseRoutePrefixLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_pref64_prefix_lifetime", "0") to ConfigParsePref64PrefixLifetimeOptionValue() as OptionValueInformation, + // config_parse_prefix_token hands its value straight to config_parse_address_generation_type. + Validator("config_parse_prefix_token", "0") to ConfigParseAddressGenerationTypeOptionValue() as OptionValueInformation, + Validator("config_parse_ndisc_route_metric", "0") to ConfigParseNdiscRouteMetricOptionValue() as OptionValueInformation, + Validator("config_parse_ndisc_route_metric", "AF_INET6") to ConfigParseNdiscRouteMetricOptionValue() as OptionValueInformation, + Validator("config_parse_can_restart_usec", "0") to ConfigParseCanRestartUsecOptionValue() as OptionValueInformation, + // [Route] table slots (each ltype mirrors its own ConfigSectionParser entry). + Validator("config_parse_route_section", "ROUTE_TABLE") to ConfigParseRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_MULTIPATH") to ConfigParseRouteMultiPathOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_MTU") to ConfigParseRouteMetricMtuOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_ADVMSS") to ConfigParseRouteMetricAdvMssOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_INITCWND") to ConfigParseRouteMetricInitCwndOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_RTO_MIN") to ConfigParseRouteMetricTcpRtoOptionValue() as OptionValueInformation, + // [RoutingPolicyRule] table slots. + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_TABLE") to ConfigParseRouteTableOptionValue("config_parse_routing_policy_rule") as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_FWMARK") to ConfigParseRoutingPolicyRuleFirewallMarkOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_IP_PROTOCOL") to ConfigParseRoutingPolicyRuleIpProtocolOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_UID_RANGE") to ConfigParseRoutingPolicyRuleUidRangeOptionValue() as OptionValueInformation, + // [Address] / [Neighbor] / [IPv6AddressLabel] table slots. + Validator("config_parse_address_section", "ADDRESS_LABEL") to ConfigParseAddressLabelOptionValue() as OptionValueInformation, + // ADDRESS_NET_LABEL's table entry is config_parse_string with CONFIG_PARSE_STRING_SAFE. + Validator("config_parse_address_section", "ADDRESS_NET_LABEL") to ConfigParseStringOptionValue() as OptionValueInformation, + // ADDRESS_NFT_SET's table entry is config_parse_nft_set with NFT_SET_PARSE_NETWORK. + Validator("config_parse_address_section", "ADDRESS_NFT_SET") to ConfigParseNftSetNetworkOptionValue() as OptionValueInformation, + Validator("config_parse_neighbor_section", "NEIGHBOR_DESTINATION_ADDRESS") to ConfigParseNeighborDestinationAddressOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_address_label_section", "IPV6_ADDRESS_LABEL_PREFIX") to ConfigParseIpv6AddressLabelPrefixOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_address_label_section", "IPV6_ADDRESS_LABEL") to ConfigParseIpv6AddressLabelOptionValue() as OptionValueInformation, ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt new file mode 100644 index 00000000..c05602ad --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.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 + +/* + * [Address] Label= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Label= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2298-L2363 config_parse_address_section (ADDRESS_LABEL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2174-L2200 config_parse_address_label + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L832-L852 address_label_valid + */ + +/** + * Validator for `[Address] Label=` (.network) — the ADDRESS_LABEL slot, whose table entry is + * config_parse_address_label → address_label_valid(). + * + * One to 15 characters (shorter than IFNAMSIZ), each a byte in 32…126 — printable ASCII, space + * included. A longer or non-ASCII label is ignored with "Interface label is too long or invalid". + */ +class ConfigParseAddressLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_address_section", + SequenceCombinator(RegexTerminal("""[\s\S]+""", """[\x20-\x7E]{1,15}"""), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt new file mode 100644 index 00000000..c0f64684 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [CAN] RestartSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RestartSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-can.c#L210-L245 config_parse_can_restart_usec (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[CAN] RestartSec=` (.network). + * + * config_parse_can_restart_usec reads a time span with parse_sec() and ignores it unless it is + * `infinity` or rounds up to at most UINT32_MAX milliseconds (the kernel's IFLA_CAN_RESTART_MS is a + * u32). `0` (disable automatic restart) is accepted. Note this parser has no isempty() branch of its + * own; an empty assignment is not something this grammar is asked about. + */ +class ConfigParseCanRestartUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_can_restart_usec", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || divRoundUp(it, USEC_PER_MSEC) <= UINT32_MAX }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt new file mode 100644 index 00000000..0dd53b0c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.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.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsNull +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes + +/* + * [Network] IPv6StableSecretAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6StableSecretAddress= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2036-L2082 config_parse_in_addr_non_null (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for config_parse_in_addr_non_null with ltype AF_INET6 — `[Network] + * IPv6StableSecretAddress=` (.network). + * + * The value goes through in_addr_from_string(AF_INET6), i.e. inet_pton(AF_INET6): an IPv6 literal, no + * prefix length and no `%zone`. An IPv4 literal is not accepted. The parser then refuses the + * all-zero address (`::`, `0:0:0:0:0:0:0:0`, …) with "cannot be the ANY address", which is flagged + * here as well. + */ +class ConfigParseInAddrNonNullIpv6OptionValue : SimpleGrammarOptionValues( + "config_parse_in_addr_non_null", + SequenceCombinator( + Checked(IPV6_ADDR) { text -> ipLiteralBytes(text)?.let { !ipIsNull(it) } ?: false }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt new file mode 100644 index 00000000..258aac7d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [IPv6AddressLabel] Label= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Label= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L419-L461 config_parse_ipv6_address_label_section (IPV6_ADDRESS_LABEL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L386-L417 config_parse_ipv6_address_label + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[IPv6AddressLabel] Label=` (.network): safe_atou32() — decimal, `0x` hex, `0o`/leading + * zero octal, `0b` binary — in 0…4294967294. 4294967295 (0xffffffff) is reserved and refused. The value is + * whole, so whitespace after a `0b`/`0o` prefix (`0b 1`) and a leading `\v` reach safe_atou32 and are fine. + */ +class ConfigParseIpv6AddressLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_address_label_section", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt new file mode 100644 index 00000000..f0470d82 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt @@ -0,0 +1,55 @@ +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.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_PREFIX_SAFE_LEN +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.ipLiteralBytes + +/* + * [IPv6AddressLabel] Prefix= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6AddressLabel%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L419-L461 config_parse_ipv6_address_label_section (IPV6_ADDRESS_LABEL_PREFIX) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L346-L384 config_parse_ipv6_address_label_prefix + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L799-L843 in_addr_prefix_from_string (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L165-L171 in6_addr_is_ipv4_mapped_address + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html (kernel net/ipv6/addrlabel.c ip6addrlbl_alloc) + */ + +/** True for an IPv4-mapped address (`::ffff:a.b.c.d`) whose prefix length exceeds 96. */ +private fun isMappedTooLong(text: String): Boolean { + val slash = text.indexOf('/') + val bytes = ipLiteralBytes(if (slash >= 0) text.substring(0, slash) else text) ?: return false + val mapped = (0 until 10).all { bytes[it].toInt() == 0 } && bytes[10].toInt() == -1 && bytes[11].toInt() == -1 + // The same safe_atou8 reading as the length in IPV6_PREFIX_SAFE_LEN, so `/0b1100001` counts as 97. + val length = if (slash >= 0) { + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(text.substring(slash + 1))?.toInt() ?: return false + } else { + 128 + } + return mapped && length > 96 +} + +/** + * Validator for `[IPv6AddressLabel] Prefix=` (.network). + * + * An IPv6 prefix as in_addr_prefix_from_string(AF_INET6) reads it — an IPv6 literal with an optional + * `/LEN` in 0…128 (no slash means /128). config_parse_ipv6_address_label_prefix then refuses an + * IPv4-mapped address (`::ffff:0:0/96`) with a prefix length above 96, mirroring the kernel's + * ip6addrlbl_alloc(); note that this includes an IPv4-mapped address written without a slash. + */ +class ConfigParseIpv6AddressLabelPrefixOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_address_label_section", + SequenceCombinator(Checked(IPV6_PREFIX_SAFE_LEN) { !isMappedTooLong(it) }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt index cf7679ba..4b338096 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt @@ -7,7 +7,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Validator for ManagedOOMMemoryPressureDurationSec=. * * C function: config_parse_managed_oom_mem_pressure_duration_sec in src/core/load-fragment.c. - * Empty resets to USEC_INFINITY. Otherwise calls parse_sec(rvalue) and additionally requires + * Empty resets to NETWORK_ROUTE_ADDR_USEC_INFINITY. Otherwise calls parse_sec(rvalue) and additionally requires * the result to be at least 1s and strictly less than infinity. * * The grammar matches parse_sec syntax (which is the existing TIME_VALUE: "infinity" or a diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt new file mode 100644 index 00000000..fcac5eeb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt @@ -0,0 +1,67 @@ +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.BANG +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CUNESCAPE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +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 +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unquoteWord + +/* + * [Match] Property= (.network and .link). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Property= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Property= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/net-condition.c#L344-L403 config_parse_match_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/basic/escape.c#L104-L296 cunescape_one + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L81-L102 env_assignment_is_valid + */ + +// env_name_is_valid_n: one or more of VALID_BASH_ENV_NAME_CHARS (letters, digits, `_`), not starting +// with a digit. +private val ENV_ASSIGNMENT = Regex("""[A-Za-z_][A-Za-z0-9_]*=[\s\S]*""") + +/** + * Validator for `[Match] Property=` in .network and .link files. + * + * config_parse_match_property strips one optional leading `!` (inverting the match) and splits the rest + * with extract_first_word(EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE): quotes group, and a backslash starts a C + * escape (`\"`, `\\`, `\n`, `\x41`, …) both inside and outside quotes — an unknown escape such as `\q`, + * or an unterminated quote, makes the whole assignment "Invalid syntax, ignoring". Each resulting word + * must then satisfy env_assignment_is_valid(): `NAME=VALUE` where NAME is letters, digits and `_` not + * starting with a digit, and VALUE is anything (including empty). A word that fails is logged and + * skipped — flagged here. + * + * The man page's own example is accepted as written: + * `Property=ID_MODEL_ID=9999 "ID_VENDOR_FROM_DATABASE=vendor name" "KEY=with \"quotation\""`. + * + * The NAME check runs on the unquoted word, so quoting the pair, quoting just the value, or even + * quoting the name are all judged exactly as systemd judges them. Lenient corners: `\x00`/`\u0000` + * (refused by cunescape_one as NUL) and UTF-16 surrogates in `\u` escapes are not singled out, and + * VALUE's UTF-8 validity / ARG_MAX length limits are not checked. + */ +class ConfigParseMatchPropertyOptionValue : SimpleGrammarOptionValues( + "config_parse_match_property", + AlternativeCombinator( + // `p += invert` and then extract_first_word() skips leading whitespace, so `! A=b` == `!A=b`. + SequenceCombinator(BANG, ZeroOrOne(WhitespaceTerminal()), PROPERTY_LIST), + // A lone `!` yields no words at all, which is accepted. + SequenceCombinator(BANG, EOF()), + PROPERTY_LIST, + ) +) { + private companion object { + val PROPERTY_WORD = Checked(CUNESCAPE_WORD) { ENV_ASSIGNMENT.matches(unquoteWord(it)) } + val PROPERTY_LIST = SequenceCombinator( + PROPERTY_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), PROPERTY_WORD)), + EOF(), + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt new file mode 100644 index 00000000..dba2916a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.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.RETAIN_ESCAPE_WORD_LIST + +/* + * [Match] Path= / Driver= / Type= / Kind= (.network and .link), WLANInterfaceType= / SSID= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Path= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Path= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/net-condition.c#L228-L281 config_parse_match_strv (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) + */ + +/** + * Validator for the glob-list `[Match]` settings: `Path=`, `Driver=`, `Type=`, `Kind=` (.network and + * .link) and `WLANInterfaceType=`, `SSID=` (.network). + * + * config_parse_match_strv strips one optional leading `!` (inverting the whole list) and then splits the + * rest with extract_first_word(EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE). Each word is an fnmatch() glob + * stored verbatim — the kind, type and driver names are open-ended (the man page itself says the lists of + * examples are "not comprehensive"), so no word is checked against a list. + * + * What *is* checked is the one thing systemd rejects: an unterminated `"` or `'` quote makes + * extract_first_word fail, and the parser then logs "Invalid syntax, ignoring" and drops the whole + * assignment. Quoted pieces may be glued to bare text (`"pci-0000 00"*`), and because of + * EXTRACT_RETAIN_ESCAPE a backslash is an ordinary character, even inside quotes. The `!` needs no + * special handling: it is an ordinary first character of the first word, and `!` on its own is accepted + * (it yields an empty list). + */ +class ConfigParseMatchStrvOptionValue : SimpleGrammarOptionValues( + "config_parse_match_strv", + RETAIN_ESCAPE_WORD_LIST, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt new file mode 100644 index 00000000..5c06ad71 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt @@ -0,0 +1,54 @@ +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.Checked +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.ZeroOrOne + +/* + * [IPv6AcceptRA] RouteMetric= and the legacy [DHCPv6] RouteMetric= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RouteMetric= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L451-L507 config_parse_ndisc_route_metric (0, AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/systemd-networkd-tests.py#L8124 (RouteMetric=100:200:300) + */ + +// safe_atou32() on the whole value, or on one field of the `:`-separated triple. Either way whitespace +// inside the text reaches safe_atou32, which skips it at the start (`100: 200: 300`) and after a +// `0b`/`0o` prefix (`0b 1`). +private val METRIC = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// extract_many_words(…, EXTRACT_DONT_COALESCE_SEPARATORS) leaves p at "" after a single trailing `:`, +// which passes the isempty(p) check, so `100:200:300:` is accepted too. +// high < medium < low, compared on the parsed numbers. +private val METRIC_TRIPLE = Checked(SequenceCombinator(METRIC, COLON, METRIC, COLON, METRIC, ZeroOrOne(COLON))) { text -> + val v = text.removeSuffix(":").split(':').map { METRIC.valueOf(it) } + v.size == 3 && v.all { it != null } && v[0]!! < v[1]!! && v[1]!! < v[2]!! +} + +/** + * Validator for config_parse_ndisc_route_metric: `[IPv6AcceptRA] RouteMetric=` (ltype 0) and the legacy + * `[DHCPv6] RouteMetric=` alias (ltype AF_INET6; the function ignores its ltype). + * + * Either one safe_atou32() metric used for all three router preferences, or `HIGH:MEDIUM:LOW` — exactly + * three safe_atou32() fields split on `:` — which systemd only accepts when strictly increasing + * (HIGH < MEDIUM < LOW, e.g. the default `512:1024:2048`). An empty field, a fourth field or a + * non-increasing triple is ignored with a warning and flagged here. + */ +class ConfigParseNdiscRouteMetricOptionValue : SimpleGrammarOptionValues( + "config_parse_ndisc_route_metric", + SequenceCombinator(AlternativeCombinator(METRIC_TRIPLE, METRIC), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt new file mode 100644 index 00000000..33ad8a2a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Neighbor] Address= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BNeighbor%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-neighbor.c#L747-L781 config_parse_neighbor_section (NEIGHBOR_DESTINATION_ADDRESS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2084-L2112 config_parse_in_addr_data + * 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 `[Neighbor] Address=` (.network) — the NEIGHBOR_DESTINATION_ADDRESS slot of + * config_parse_neighbor_section, whose table entry is config_parse_in_addr_data. + * + * That reads one address of either family with in_addr_from_string_auto (inet_pton(AF_INET), then + * inet_pton(AF_INET6)): no prefix length and no `%zone`. Unlike some address settings it does not + * refuse the ANY address at parse time, so neither does this grammar. + */ +class ConfigParseNeighborDestinationAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_neighbor_section", + SequenceCombinator(IP_ADDR, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt new file mode 100644 index 00000000..dc867038 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt @@ -0,0 +1,41 @@ +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.Checked +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 +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsMulticast +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsNull +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes + +/* + * [Network] ProxyNeighbor= and its deprecated alias IPv6ProxyNDPAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ProxyNeighbor= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-neighbor-proxy.c#L157-L216 config_parse_neighbor_proxy_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 `[Network] ProxyNeighbor=` and the deprecated `IPv6ProxyNDPAddress=` (.network). + * + * config_parse_neighbor_proxy_address reads one address of either family with in_addr_from_string_auto + * (inet_pton(AF_INET) then inet_pton(AF_INET6) — no prefix length, no zone index) and then ignores it + * with a warning if it is + * - the ANY address (`0.0.0.0`, `::`), + * - multicast (224.0.0.0/4 or ff00::/8), or + * - the IPv4 limited broadcast address `255.255.255.255`, + * because the kernel would refuse such a proxy entry. All three are flagged here. + */ +class ConfigParseNeighborProxyAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_neighbor_proxy_address", + SequenceCombinator( + Checked(IP_ADDR) { text -> + val bytes = ipLiteralBytes(text) + bytes != null && !ipIsNull(bytes) && !ipIsMulticast(bytes) && !(bytes.size == 4 && bytes.all { it.toInt() == -1 }) + }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt new file mode 100644 index 00000000..7fbb1d93 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt @@ -0,0 +1,131 @@ +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.Checked +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.OneOrMore +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 + +/* + * NFTSet= in [Address], [DHCPv4], [DHCPv6], [IPv6AcceptRA] and [DHCPPrefixDelegation] (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#NFTSet=source:family:table:set + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1207-L1295 config_parse_nft_set (NFT_SET_PARSE_NETWORK) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2298-L2363 config_parse_address_section (ADDRESS_NFT_SET → config_parse_nft_set, NFT_SET_PARSE_NETWORK) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1125-L1145 nfproto_table, nft_set_source_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L824-L835 nft_identifier_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (leading separators skipped L39-L56, coalescing L145-L159) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240-L281 extract_many_words_internal + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv4-only.network + * linux https://www.netfilter.org/projects/nftables/manpage.html (NFT_NAME_MAXLEN = 256, include/uapi/linux/netfilter/nf_tables.h) + */ + +// nft_identifier_valid: a letter, then letters, digits, `/`, `\`, `_` and `.`; shorter than +// NFT_NAME_MAXLEN (256). The tuple is split with EXTRACT_CUNESCAPE, so a backslash in the file starts a C +// escape; any escaped character is accepted here rather than decoding it. +private val NFT_IDENTIFIER = RegexTerminal( + """[^\s:"']+""", + """[A-Za-z](?:[A-Za-z0-9/_.]|\\[\s\S]){0,254}""" +) + +private val NFT_SOURCES = arrayOf("address", "prefix", "ifindex") +private val NFT_FAMILIES = arrayOf("arp", "bridge", "inet", "ip", "ip6", "netdev") +private val NFT_IDENTIFIER_REGEX = Regex("""[A-Za-z](?:[A-Za-z0-9/_.]|\\[\s\S]){0,254}""") + +// extract_many_words(":") is called without EXTRACT_DONT_COALESCE_SEPARATORS, so a run of colons is one +// separator, and leading / trailing colons are skipped. +private val COLONS = OneOrMore(COLON) + +/** + * One unquoted `source:family:table:set` tuple. `source` is restricted by NFT_SET_PARSE_NETWORK to + * `address`, `prefix` or `ifindex` (the cgroup/user/group sources are for unit files). + */ +private val NFT_TUPLE = SequenceCombinator( + ZeroOrMore(COLON), + FlexibleLiteralChoiceTerminal(*NFT_SOURCES), + COLONS, + FlexibleLiteralChoiceTerminal(*NFT_FAMILIES), + COLONS, + NFT_IDENTIFIER, + COLONS, + NFT_IDENTIFIER, + ZeroOrMore(COLON), +) + +/** + * The tuple text after the first-level EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE split: quotes removed + * wherever they are, backslashes kept. + */ +private fun stripQuotesRetainEscape(text: String): String { + val sb = StringBuilder() + var quote: Char? = null + for (c in text) { + when { + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> sb.append(c) + } + } + return sb.toString() +} + +/** Whether the (already unquoted) tuple is valid: four non-empty `:`-separated fields, each acceptable. */ +private fun nftTupleValid(tuple: String): Boolean { + val fields = tuple.split(':').filter { it.isNotEmpty() } + return fields.size == 4 && + fields[0] in NFT_SOURCES && + fields[1] in NFT_FAMILIES && + NFT_IDENTIFIER_REGEX.matches(fields[2]) && + NFT_IDENTIFIER_REGEX.matches(fields[3]) +} + +// A word that contains a quote somewhere (`"address:inet:t:s"`, `address:"inet":t:s`, ...). The first +// split removes the quotes wherever they are, so the word is judged as a whole after unquoting; it is a +// single token because the quotes may cut across the tuple's fields. An unterminated quote does not +// match here and is then flagged by the unquoted branch. +private val QUOTED_NFT_WORD = Checked( + RegexTerminal( + """(?=[^\s"']*["'])(?:[^\s"']+|"[^"]*"|'[^']*')+""", + """(?=[^\s"']*["'])(?:[^\s"']+|"[^"]*"|'[^']*')+""" + ) +) { nftTupleValid(stripQuotesRetainEscape(it)) } + +// The quoted form first: it requires a quote, so it never shadows a plain tuple. +private val NFT_WORD = AlternativeCombinator( + QUOTED_NFT_WORD, + NFT_TUPLE, +) + +/** + * Validator for the .network `NFTSet=` settings: `[Address]`, `[DHCPv4]`, `[DHCPv6]`, + * `[IPv6AcceptRA]` and `[DHCPPrefixDelegation]` — config_parse_nft_set with NFT_SET_PARSE_NETWORK, + * reached either directly or through config_parse_address_section's ADDRESS_NFT_SET table slot. + * + * The value is a whitespace-separated list (split with EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE, so quotes + * anywhere in a word are removed) of `source:family:table:set` tuples, each split on `:` into exactly + * four fields (extract_many_words with EXTRACT_CUNESCAPE and without DONT_COALESCE_SEPARATORS: runs of + * `:` count as one separator and leading/trailing `:` are skipped; a fifth field is an error): + * - source: `address`, `prefix` or `ifindex`; + * - family: nfproto_table — `arp`, `bridge`, `inet`, `ip`, `ip6`, `netdev`; + * - table and set: nft_identifier_valid — a letter followed by letters, digits, `/`, `\`, `_`, `.`, + * at most 255 characters. + * Any invalid tuple aborts the rest of the assignment with a warning. + * + * Lenient / strict corners: C escapes inside a table or set name are accepted without being decoded. + * A C escape that spells part of the source or family (`addr\x65ss`) is flagged, although systemd + * would decode it; nobody writes that. + */ +class ConfigParseNftSetNetworkOptionValue : SimpleGrammarOptionValues( + "config_parse_nft_set", + SequenceCombinator( + NFT_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), NFT_WORD)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt new file mode 100644 index 00000000..867cc632 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6PREF64Prefix] LifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6PREF64Prefix%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1240-L1283 config_parse_pref64_prefix_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + */ + +private val EIGHT_SECONDS: BigInteger = BigInteger.valueOf(8).multiply(USEC_PER_SEC) +private val SCALED_LIFETIME_LIMIT: BigInteger = BigInteger.valueOf(1L shl 13) + +/** + * Validator for `[IPv6PREF64Prefix] LifetimeSec=` (.network). + * + * The PREF64 option (RFC 8781) carries the lifetime as a 13-bit count of 8-second units, so + * config_parse_pref64_prefix_lifetime ignores ("PREF64 lifetime is too long") `infinity` and any value + * where DIV_ROUND_UP(usec, 8s) ≥ 8192 — i.e. it accepts parse_sec() values up to 8191 × 8 = 65528 + * seconds (18h 12min 8s). `0` is accepted. + */ +class ConfigParsePref64PrefixLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_pref64_prefix_lifetime", + SequenceCombinator(timeSpan { it != NETWORK_ROUTE_ADDR_USEC_INFINITY && divRoundUp(it, EIGHT_SECONDS) < SCALED_LIFETIME_LIMIT }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt new file mode 100644 index 00000000..9fe3e29c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.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.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SIZE_IEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseSizeIec +import java.math.BigInteger + +/* + * [Route] TCPAdvertisedMaximumSegmentSize= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#TCPAdvertisedMaximumSegmentSize= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_ADVMSS) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_ADVMSS) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L221-L251 config_parse_route_metric_advmss + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-static.network + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] TCPAdvertisedMaximumSegmentSize=` (.network) — ROUTE_METRIC_ADVMSS, which + * config_parse_route_metric dispatches to config_parse_route_metric_advmss. + * + * A parse_size(…, 1024) byte count (`1460`, `1K`, `1.5K`, …) that must be in 1…UINT32_MAX. The man page + * says 1…4294967294; the C bound is `u == 0 || u > UINT32_MAX`, so 4294967295 is accepted here. + */ +class ConfigParseRouteMetricAdvMssOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + Checked(PARSE_SIZE_IEC) { text -> parseSizeIec(text)?.let { it.signum() > 0 && it <= UINT32_MAX } ?: false }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt new file mode 100644 index 00000000..c3fb87b9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt @@ -0,0 +1,41 @@ +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.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Route] InitialCongestionWindow= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#InitialCongestionWindow= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_INITCWND) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_INITCWND) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L284-L313 config_parse_tcp_window + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-static.network#L37 + */ + +/** + * Validator for `[Route] InitialCongestionWindow=` (.network) — ROUTE_METRIC_INITCWND, which + * config_parse_route_metric dispatches to config_parse_tcp_window: safe_atou32() (so `0x10`, `0o20`, + * `020` are numbers too, and so are `0b 1` and `0b+1`, since safe_atou32 sees the whole value) and then 1…1023. + * + * (`InitialAdvertisedReceiveWindow=` uses the same leaf and is already covered by + * ConfigParseRouteSectionInitRwndOptionValue.) + */ +class ConfigParseRouteMetricInitCwndOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 1023L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt new file mode 100644 index 00000000..9f3baa4c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.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.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SIZE_IEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseSizeIec +import java.math.BigInteger + +/* + * [Route] MTUBytes= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MTUBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_MTU) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_MTU) + * https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1737-L1766 config_parse_mtu (ltype 0 = AF_UNSPEC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L126-L155 parse_mtu + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024) + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] MTUBytes=` (.network) — ROUTE_METRIC_MTU, which config_parse_route_metric + * dispatches to config_parse_mtu with ltype 0 (AF_UNSPEC, so no family minimum). + * + * parse_mtu() reads the value with parse_size(…, 1024) — a byte count with optional `K`/`M`/`G`/… + * suffixes (base 1024), fractions and multi-part sums like `1K 512` — and refuses anything above + * UINT32_MAX. With AF_UNSPEC there is no lower bound, so `0` is accepted. + */ +class ConfigParseRouteMetricMtuOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator(Checked(PARSE_SIZE_IEC) { text -> parseSizeIec(text)?.let { it <= UINT32_MAX } ?: false }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt new file mode 100644 index 00000000..c843ec2c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.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.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [Route] TCPRetransmissionTimeoutSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#TCPRetransmissionTimeoutSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_RTO_MIN) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_RTO_MIN) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L315-L346 config_parse_route_metric_tcp_rto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.h#L106-L108 timestamp_is_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-congctl.network + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] TCPRetransmissionTimeoutSec=` (.network) — ROUTE_METRIC_RTO_MIN, which + * config_parse_route_metric dispatches to config_parse_route_metric_tcp_rto. + * + * A parse_sec() time span that must be set (neither `0` nor `infinity`) and must round up to at most + * UINT32_MAX milliseconds, since RTAX_RTO_MIN is a u32 of milliseconds. + */ +class ConfigParseRouteMetricTcpRtoOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + timeSpan { it.signum() > 0 && it != NETWORK_ROUTE_ADDR_USEC_INFINITY && divRoundUp(it, USEC_PER_MSEC) <= UINT32_MAX }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt new file mode 100644 index 00000000..c9473999 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt @@ -0,0 +1,85 @@ +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.IP_ADDR +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.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Route] MultiPathRoute= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MultiPathRoute= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_MULTIPATH) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-nexthop.c#L1041-L1142 config_parse_multipath_route + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex (safe_atoi, > 0) + * 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 + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (weight, base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/network/test-networkd-conf.c#L352-L440 TEST(config_parse_multipath_route) + */ + +private val AT = LiteralChoiceTerminal("@") + +// After `@`: parse_ifindex() (a positive int) or, failing that, an alternative-length interface name. +// The name branch comes first because its lenient shape also matches digits but it only *validates* +// non-numeric names; under the classic matcher a number then falls through to the index branch. +// parse_ifindex() is safe_atoi() refusing anything below 1. The device is part of the first word, which +// extract_first_word() cut at whitespace, so no whitespace may follow the `@`. +private val DEVICE = AlternativeCombinator( + ALTERNATIVE_INTERFACE_NAME, + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = false, + acceptsMinus = true, + ), +) + +/** + * Validator for `[Route] MultiPathRoute=` (.network) — the ROUTE_MULTIPATH slot, whose table entry is + * config_parse_multipath_route. + * + * The first whitespace-separated word is `[GATEWAY][@DEVICE]` with at least one part present: + * - GATEWAY is an IPv4 or IPv6 literal (in_addr_from_string_auto — no prefix, no zone); + * - DEVICE, after the first `@`, is an interface index (parse_ifindex: a positive integer) or an + * interface name valid with IFNAME_VALID_ALTERNATIVE (up to 127 characters, not all digits). + * Anything after that word is the weight, read as a whole with safe_atou32() and accepted in 1…256 — + * a second word, or anything else trailing, is an error. Because the weight is the whole rest of the value, + * `@eth0 0b 1` is weight 1. + * + * Examples from systemd's own unit test: `@wg0 10`, `@1 15`, `10.0.0.1@eth0 20`, + * `2001:db8::1@wg0 15`, `192.168.1.1 100`, `fe80::1`, `@999999 10`. + */ +class ConfigParseRouteMultiPathOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(IP_ADDR, ZeroOrOne(SequenceCombinator(AT, DEVICE))), + SequenceCombinator(AT, DEVICE), + ), + ZeroOrOne( + SequenceCombinator( + WhitespaceTerminal(), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 256L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt new file mode 100644 index 00000000..505dce41 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6RoutePrefix] LifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6RoutePrefix%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1116-L1159 config_parse_route_prefix_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipv6ra-prefix.network#L24-L26 + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[IPv6RoutePrefix] LifetimeSec=` (.network). + * + * config_parse_route_prefix_lifetime reads a time span with parse_sec() and ignores it ("Lifetime is + * too long") unless it is `infinity` or rounds up to fewer than UINT32_MAX whole seconds — the Route + * Information option carries a 32-bit seconds field in which all-ones means infinity. + */ +class ConfigParseRoutePrefixLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_route_prefix_lifetime", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || divRoundUp(it, USEC_PER_SEC) < UINT32_MAX }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt new file mode 100644 index 00000000..0853a577 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.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.IPV6_PREFIX_SAFE_LEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [IPv6RoutePrefix] Route= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Route= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1076-L1114 config_parse_route_prefix (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L799-L843 in_addr_prefix_from_string (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L779-L797 in_addr_parse_prefixlen (safe_atou8, ≤ 128) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipv6ra-prefix.network#L24-L26 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[IPv6RoutePrefix] Route=` (.network). + * + * config_parse_route_prefix reads an IPv6 prefix with in_addr_prefix_from_string(AF_INET6): an IPv6 + * literal (inet_pton — no IPv4, no zone) and an optional `/LEN` in 0…128, LEN read with safe_atou8 so + * hex/octal spellings count too. The man page asks for the slash, but without it the C simply uses a + * /128, so that is accepted. Host bits are masked off silently, so `2001:db8::1/64` is fine. + */ +class ConfigParseRoutePrefixOptionValue : SimpleGrammarOptionValues( + "config_parse_route_prefix", + SequenceCombinator(IPV6_PREFIX_SAFE_LEN, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt new file mode 100644 index 00000000..c99d5808 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.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.ROUTE_TABLE_FROM_STRING + +/* + * [Route] Table= and [RoutingPolicyRule] Table= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Table= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_TABLE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1862-L1886 config_parse_route_table + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_TABLE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1397-L1424 config_parse_routing_policy_rule_table + * 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#L496-L616 config_parse_route_table_names (what a user-defined name may look like) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Route] Table=` and `[RoutingPolicyRule] Table=` (.network). Both table slots hand the whole + * value to manager_get_route_table_from_string(), so they use the shared [ROUTE_TABLE_FROM_STRING] grammar. + * + * That tries, in order: the predefined names `default`, `main`, `local`; the names defined by `RouteTable=` in + * networkd.conf; and finally safe_atou32(), refusing 0. The user-defined names live in a file the plugin cannot + * see, so every value that could be such a name is accepted: anything non-empty without a `:` that is not all + * digits. That includes `0x0`, `-0`, and names with whitespace such as `main table` (networkd.conf can define + * `main\ table:100`). An all-digit value must be a table number in 1…4294967295, so `0`, `00`, `08` and + * `4294967296` are flagged, as is any value containing `:`. + */ +class ConfigParseRouteTableOptionValue(parserName: String = "config_parse_route_section") : SimpleGrammarOptionValues( + parserName, + ROUTE_TABLE_FROM_STRING, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt new file mode 100644 index 00000000..7bc1c829 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6SendRA] HomeAgentLifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#HomeAgentLifetimeSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1583-L1624 config_parse_router_home_agent_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.h#L106-L108 timestamp_is_set + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L72 RADV_HOME_AGENT_MAX_LIFETIME_USEC + */ + +private val RADV_HOME_AGENT_MAX_LIFETIME_USEC: BigInteger = BigInteger.valueOf(65_535L).multiply(USEC_PER_SEC) + +/** + * Validator for `[IPv6SendRA] HomeAgentLifetimeSec=` (.network). + * + * config_parse_router_home_agent_lifetime reads a time span with parse_sec() and ignores it unless + * timestamp_is_set() (neither `0` nor `infinity`) and it is at most RADV_HOME_AGENT_MAX_LIFETIME_USEC = + * 65535 seconds. Sub-second values such as `500ms` pass the C check (the man page's "1…65535" is in + * whole seconds, but the comparison is on microseconds), so they are accepted here too. + */ +class ConfigParseRouterHomeAgentLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_router_home_agent_lifetime", + SequenceCombinator(timeSpan { it.signum() > 0 && it != NETWORK_ROUTE_ADDR_USEC_INFINITY && it <= RADV_HOME_AGENT_MAX_LIFETIME_USEC }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt new file mode 100644 index 00000000..396db662 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan + +/* + * [IPv6SendRA] RouterLifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RouterLifetimeSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1463-L1512 config_parse_router_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L38-L39 RADV_MIN/MAX_ROUTER_LIFETIME_USEC + */ + +/** + * Validator for `[IPv6SendRA] RouterLifetimeSec=` (.network). + * + * config_parse_router_lifetime reads a time span with parse_sec(). Only a parse failure ignores the + * assignment: `0` is kept (the host is not a default router), and any other value outside + * RADV_MIN_ROUTER_LIFETIME_USEC…9000s — `infinity` included — is *clamped* into that range with a + * warning, not rejected. So every parse_sec() spelling is accepted here; the clamping is not flagged. + */ +class ConfigParseRouterLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_router_lifetime", + SequenceCombinator(timeSpan(), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt new file mode 100644 index 00000000..b5b0fd58 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt @@ -0,0 +1,64 @@ +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.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] IPv6SendRA= and the deprecated [Network] IPv6PrefixDelegation= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6SendRA= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1418-L1461 config_parse_router_prefix_delegation (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1406-L1416 radv_prefix_delegation_table (DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (case-insensitive) + */ + +/** + * Validator for config_parse_router_prefix_delegation: `[Network] IPv6SendRA=` and its deprecated + * predecessor `[Network] IPv6PrefixDelegation=`. + * + * The parser branches on the key name. For `IPv6SendRA=` it takes a plain parse_boolean() value. For + * the backward-compatible `IPv6PrefixDelegation=` it uses radv_prefix_delegation_from_string(), a + * string table with boolean fallback: any boolean, or `static` / `dhcpv6`. + * + * Both keys share one (parser, ltype) registration, so the registered grammar is the union — a boolean, + * or `static` / `dhcpv6` — which is exact for `IPv6PrefixDelegation=`. For `IPv6SendRA=` + * [generateProblemDescriptors] branches on the key, as the C does, and checks against a boolean-only + * grammar, so `IPv6SendRA=static` is flagged. + * + * string_table_lookup_from_string_with_boolean() runs parse_boolean() first (STRCASE_IN_SET, so `Yes`, + * `TRUE`, `oFf` are fine), then a case-sensitive streq() lookup, so `Static` is refused. The table's + * `no` / `yes` entries are left out of the names because [BOOLEAN] already covers them in any case. + */ +class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( + "config_parse_router_prefix_delegation", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("static", "dhcpv6"), + BOOLEAN, + ), + EOF() + ) +) { + + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + if (property.key == "IPv6SendRA") { + SEND_RA_BOOLEAN_ONLY.generateProblemDescriptors(property, holder) + } else { + super.generateProblemDescriptors(property, holder) + } + } + + private companion object { + // streq(lvalue, "IPv6SendRA") → parse_boolean() only. + val SEND_RA_BOOLEAN_ONLY = SimpleGrammarOptionValues( + "config_parse_router_prefix_delegation", + SequenceCombinator(BOOLEAN, EOF()) + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt new file mode 100644 index 00000000..3da8d2ea --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6SendRA] ReachableTimeSec= / RetransmitSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ReachableTimeSec= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RetransmitSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1514-L1555 config_parse_router_uint32_msec_usec (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L44 RADV_MAX_UINT32_MSEC_USEC + */ + +private val RADV_MAX_UINT32_MSEC_USEC: BigInteger = BigInteger.valueOf(4_294_967_295L).multiply(USEC_PER_MSEC) + +/** + * Validator for `[IPv6SendRA] ReachableTimeSec=` and `RetransmitSec=` (.network). + * + * config_parse_router_uint32_msec_usec reads a time span with parse_sec() (default unit seconds, every + * suffix extract_multiplier() knows, `infinity`) and ignores it unless it is `infinity` or at most + * RADV_MAX_UINT32_MSEC_USEC = UINT32_MAX milliseconds (~49.7 days), since the value is sent as a 32-bit + * millisecond count. The man page documents only the 0…4294967295 ms range; the C also lets `infinity` + * through, so it is accepted here. + */ +class ConfigParseRouterUint32MsecUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_router_uint32_msec_usec", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || it <= RADV_MAX_UINT32_MSEC_USEC }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt new file mode 100644 index 00000000..8c3fe01b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt @@ -0,0 +1,57 @@ +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.CIDR_SEPARATOR +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.ZeroOrOne + +/* + * [RoutingPolicyRule] FirewallMark= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#FirewallMark= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_FWMARK) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1426-L1450 config_parse_routing_policy_rule_fwmark + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1298-L1330 parse_fwmark_fwmask + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[RoutingPolicyRule] FirewallMark=` (.network) — the ROUTING_POLICY_RULE_FWMARK slot, + * whose table entry is config_parse_routing_policy_rule_fwmark → parse_fwmark_fwmask. + * + * `MARK` or `MARK/MASK`, split at the first `/`, each read with safe_atou32() — so decimal, `0x` hex, + * leading-zero / `0o` octal and `0b` binary are all accepted, up to 4294967295. A missing number on + * either side of the slash is an error. Both parts reach safe_atou32 with any whitespace they contain, so a + * leading `\v` or `0b 1` is fine, but whitespace before the slash (`1 /2`) is trailing junk for the mark. + */ +class ConfigParseRoutingPolicyRuleFirewallMarkOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt new file mode 100644 index 00000000..21f3a51c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt @@ -0,0 +1,77 @@ +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.Checked +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.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [RoutingPolicyRule] IPProtocol= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPProtocol= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_IP_PROTOCOL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2257-L2287 config_parse_ip_protocol (ltype 0 = not relaxed) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ip-protocol-list.c#L36-L70 parse_ip_protocol_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/generate-ip-protocol-list.sh every `#define IPPROTO_*` cpp sees for + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/meson.build#L328-L333 ... run with system_include_args, so is systemd's override + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/override/netinet/in.h#L4-L6 which includes the bundled uapi , + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/in.h#L27-L92 IPPROTO_IP ... IPPROTO_MPTCP (incl. AGGFRAG = 144) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/in6.h#L131-L139 IPPROTO_HOPOPTS ... IPPROTO_MH + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ip-protocol-to-name.awk number -> name table (HOPOPTS skipped; 0 is "ip") + * linux https://man7.org/linux/man-pages/man5/protocols.5.html + */ + +/** + * The IPPROTO_* names the generated gperf table knows, lower-cased. The table is generated from + * systemd's own vendored uapi headers (the override includes and + * , not glibc's), so it is fixed per systemd commit. `smc` (256) and `mptcp` (262) are left + * out because config_parse_ip_protocol refuses anything above 255. + */ +private val IP_PROTOCOL_NAMES = arrayOf( + "ip", "hopopts", "icmp", "igmp", "ipip", "tcp", "egp", "pup", "udp", "idp", "tp", "dccp", "ipv6", + "routing", "fragment", "rsvp", "gre", "esp", "ah", "icmpv6", "none", "dstopts", "mtp", "beetph", + "encap", "pim", "comp", "l2tp", "mh", "sctp", "udplite", "mpls", "ethernet", "aggfrag", "raw", +) + +/** The protocol numbers ip_protocol_to_name() has a name for (and that fit in a byte). */ +private val IP_PROTOCOL_NUMBERS = setOf( + 0, 1, 2, 4, 6, 8, 12, 17, 22, 29, 33, 41, 43, 44, 46, 47, 50, 51, 58, 59, 60, 92, 94, 98, 103, 108, 115, + 132, 135, 136, 137, 143, 144, 255, +) + +// parse_ip_protocol_full reads a number with safe_atoi() and refuses a negative one with -ERANGE; +// config_parse_ip_protocol then refuses anything above UINT8_MAX. The value is whole, so `\v6` and +// `0b 110` reach safe_atoi too. +private val IP_PROTOCOL_NUMBER = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 255L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +/** + * Validator for `[RoutingPolicyRule] IPProtocol=` (.network) — the ROUTING_POLICY_RULE_IP_PROTOCOL slot, + * whose table entry is config_parse_ip_protocol with ltype 0. + * + * parse_ip_protocol_full(…, relaxed = false) accepts an IPPROTO_* name looked up case-insensitively + * (the gperf table is built with `--ignore-case`, and the value is also retried lower-cased), or a + * number read with safe_atoi() (base 0, so `0x11` works) that ip_protocol_to_name() has a name for + * (otherwise -EPROTONOSUPPORT; e.g. `3` is refused). config_parse_ip_protocol then refuses anything + * above 255, because the netlink attribute is one byte. + */ +class ConfigParseRoutingPolicyRuleIpProtocolOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal(*IP_PROTOCOL_NAMES, ignoreCase = true), + Checked(IP_PROTOCOL_NUMBER) { text -> IP_PROTOCOL_NUMBER.valueOf(text)?.toInt() in IP_PROTOCOL_NUMBERS }, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt new file mode 100644 index 00000000..147a8b1c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt @@ -0,0 +1,97 @@ +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.Checked +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.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.ZeroOrOne + +/* + * [RoutingPolicyRule] User= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#User= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_UID_RANGE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1478-L1508 config_parse_routing_policy_rule_uid_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L285-L369 get_user_creds + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L97-L135 parse_uid_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L66-L95 parse_uid (base 10, no sign, no leading zero, no whitespace) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L51-L64 uid_is_valid (not 65535, not 4294967295) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-fibrule-uidrange.network + */ + +// parse_uid: safe_atou32_full(s, 10 | SAFE_ATO_REFUSE_PLUS_MINUS | SAFE_ATO_REFUSE_LEADING_ZERO | +// SAFE_ATO_REFUSE_LEADING_WHITESPACE), so strict decimal, no `+`/`-`, no leading zero (except `0` itself) and +// no leading " \t\n\r" (a leading `\v` still gets through, as in C). 4294967295 is refused by uid_is_valid(). +// The lower bound is the text before the first `-` (extract_first_word with "-"), so it cannot contain one. +private val UID_LOWER = SafeAtoTerminal( + signed = false, + base = 10, + flags = setOf( + SafeAtoTerminal.Flag.REFUSE_PLUS_MINUS, + SafeAtoTerminal.Flag.REFUSE_LEADING_ZERO, + SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE, + ), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = false, +) + +// The upper bound is the rest of the value after that `-`. +private val UID_UPPER = SafeAtoTerminal( + signed = false, + base = 10, + flags = setOf( + SafeAtoTerminal.Flag.REFUSE_PLUS_MINUS, + SafeAtoTerminal.Flag.REFUSE_LEADING_ZERO, + SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE, + ), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// uid_is_valid() also refuses the 16-bit -1, 65535. +private val UID_RANGE = Checked( + SequenceCombinator( + Checked(UID_LOWER) { UID_LOWER.valueOf(it)?.toLong() != 65_535L }, + ZeroOrOne(SequenceCombinator(HYPHEN, Checked(UID_UPPER) { UID_UPPER.valueOf(it)?.toLong() != 65_535L })), + ) +) { text -> + val dash = text.indexOf('-') + val lo = if (dash >= 0) UID_LOWER.valueOf(text.substring(0, dash)) else null + val hi = if (dash >= 0) UID_UPPER.valueOf(text.substring(dash + 1)) else null + dash < 0 || lo == null || hi == null || lo <= hi +} + +// A user name for getpwnam(): NSS is the judge, so anything without whitespace or `:` is accepted, as +// long as it does not look like a UID or UID range (those are judged by the numeric branch). The lenient +// shape stops only at systemd's WHITESPACE (" \t\n\r"), so under the classic engine a range such as +// `5-\v6` (a `\v` that parse_uid lets through) is not cut short at the `\v` and falls through to UID_RANGE. +private val USER_NAME = RegexTerminal("""[^ \t\n\r]+""", """(?![0-9][0-9-]*(?:\s|$))[^\s:]+""") + +/** + * Validator for `[RoutingPolicyRule] User=` (.network) — the ROUTING_POLICY_RULE_UID_RANGE slot. + * + * config_parse_routing_policy_rule_uid_range first tries get_user_creds() (a user name, or a numeric UID + * that exists in the user database), and falls back to parse_uid_range(): `UID` or `UID-UID`, each + * read by parse_uid() — decimal only, no sign, no leading zeros, no whitespace, and not one of the + * invalid UIDs 65535 / 4294967295 — with the lower bound not above the upper. + * + * User names can only be checked at runtime, so any token that is not purely digits and dashes is + * accepted as a possible name. A digits-and-dashes token is judged strictly as a UID or UID range. + */ +class ConfigParseRoutingPolicyRuleUidRangeOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + // Name first: its lenient shape also covers numbers, but it only validates non-numeric names, so + // the classic matcher falls through to the numeric branch for those. + AlternativeCombinator(USER_NAME, UID_RANGE), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt new file mode 100644 index 00000000..49b7cac8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt @@ -0,0 +1,54 @@ +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.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.plainNameList + +/* + * [Network] IPoIB= / IPVLAN= / IPVTAP= / MACsec= / MACVLAN= / MACVTAP= / Tunnel= / VLAN= / VXLAN= / + * Xfrm= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPoIB= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L945-L1008 config_parse_stacked_netdev (NETDEV_KIND_IPOIB, …_IPVLAN, …_IPVTAP, …_MACSEC, …_MACVLAN, …_MACVTAP, …_VLAN, …_VXLAN, …_XFRM, _NETDEV_KIND_TUNNEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0, via ifname_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex (safe_atoi base 0, > 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-macvlan.network#L7 (MACVLAN=macvlan99) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipvlan.network#L7 (IPVLAN=ipvlan99) + */ + +// ifname_valid() first refuses anything parse_ifindex() accepts: safe_atoi() with base 0 (so hex, `0b`, +// `0o` and a leading `+` count) giving a value in 1…INT_MAX. INTERFACE_NAME only refuses all-digit names. +// The name is a whole word handed to safe_atoi, so leading whitespace that survived the split (`\v`) is +// skipped as C does. +private val NOT_AN_IFINDEX_NAME = Checked(INTERFACE_NAME) { name -> + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(name) == null +} + +/** + * Validator for the stacked-netdev settings of a .network `[Network]` section: `IPoIB=`, `IPVLAN=`, + * `IPVTAP=`, `MACsec=`, `MACVLAN=`, `MACVTAP=`, `Tunnel=`, `VLAN=`, `VXLAN=` and `Xfrm=`. All ten use + * config_parse_stacked_netdev; the ltype only records which netdev kind is expected, so they share a + * grammar. + * + * The value is a whitespace-separated list (extract_first_word with no flags: no quote handling) of + * netdev names, each checked with ifname_valid(): 1–15 printable ASCII characters other than `:`, `/` + * and `%`, not all digits, not something parse_ifindex() reads as a positive ifindex (`0x10`, `+5`, + * `0b1`), and not `.`, `..`, `all` or `default`. An invalid name is logged and skipped; + * it is flagged here. An unmatched backslash at the very end of the value would also make + * extract_first_word fail, but is not singled out. + */ +class ConfigParseStackedNetdevOptionValue : SimpleGrammarOptionValues( + "config_parse_stacked_netdev", + plainNameList(NOT_AN_IFINDEX_NAME), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt new file mode 100644 index 00000000..cb64c294 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPServer] BootFilename=, [MobileNetwork] APN= / OperatorId= / User= / Password= / PIN= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#BootFilename= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#APN= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1253-L1300 config_parse_string (CONFIG_PARSE_STRING_SAFE_AND_ASCII) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-util.c#L1154-L1198 string_is_safe (STRING_ALLOW_GLOBS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/utf8.h#L17 ascii_is_valid + */ + +/** + * Validator for config_parse_string with CONFIG_PARSE_STRING_SAFE_AND_ASCII + * (= CONFIG_PARSE_STRING_SAFE | CONFIG_PARSE_STRING_ASCII): `[DHCPServer] BootFilename=` and the + * `[MobileNetwork]` `APN=`, `OperatorId=`, `User=`, `Password=` and `PIN=` settings of .network files. + * + * Both checks run on the raw value. string_is_safe(…, STRING_ALLOW_GLOBS) refuses control characters + * (0x01–0x1F, 0x7F), backslashes and the quote characters `"` and `'` (globs `*?[` are fine); and + * ascii_is_valid() refuses any byte of 0x80 or above. Either failure logs a warning and ignores the + * assignment. What remains is printable ASCII, space included, minus `"`, `'` and `\`. + */ +class ConfigParseStringSafeAsciiOptionValue : SimpleGrammarOptionValues( + "config_parse_string", + SequenceCombinator( + RegexTerminal("""[\s\S]+""", """[\x20-\x7E&&[^"'\\]]+"""), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt new file mode 100644 index 00000000..942eeb56 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.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.RETAIN_ESCAPE_WORD_LIST + +/* + * [Network] BindCarrier= (.network), [Exec] Parameters= / Environment= (.nspawn). + * (logind.conf's KillOnlyUsers= / KillExcludeUsers= share the parser but are not a file type the plugin + * handles.) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#BindCarrier= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Parameters= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Environment= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1416-L1460 config_parse_strv (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) + */ + +/** + * Validator for config_parse_strv with ltype 0: `[Network] BindCarrier=` in .network files and + * `[Exec] Parameters=` / `Environment=` in .nspawn files. + * + * config_parse_strv splits the value with extract_first_word(EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) and + * appends every word verbatim; it validates nothing about the words themselves (BindCarrier= globs are + * matched later, nspawn's Environment= entries are not even checked for an `=`). The only input it + * refuses is one extract_first_word cannot split — an unterminated `"` or `'` quote — and for that it + * logs a parse error and ignores the assignment. That is all this grammar checks; see + * [RETAIN_ESCAPE_WORD_LIST] for the word shape. + */ +class ConfigParseStrvOptionValue : SimpleGrammarOptionValues( + "config_parse_strv", + RETAIN_ESCAPE_WORD_LIST, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt new file mode 100644 index 00000000..2cd08bb1 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt @@ -0,0 +1,38 @@ +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.CIDR_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * [DHCPServer] Timezone= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Timezone= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2222-L2255 config_parse_timezone (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1596-L1655 verify_timezone + * linux https://man7.org/linux/man-pages/man5/tzfile.5.html + */ + +// One path component of a zoneinfo name: verify_timezone allows ASCII letters, digits, `-`, `_`, `+`. +private val TZ_COMPONENT = RegexTerminal("""[^/]+""", """[A-Za-z0-9_+-]+""") + +/** + * Validator for `[DHCPServer] Timezone=` (.network). + * + * config_parse_timezone calls verify_timezone(), which accepts `UTC` unconditionally and otherwise + * requires a relative zoneinfo path: components of ASCII letters, digits, `-`, `_` and `+`, separated by + * single `/`, with no leading or trailing `/` (so no `.` or `..` components either). It then opens + * `/usr/share/zoneinfo/` and checks the TZif magic — that runtime check cannot be done here, so + * a well-formed but non-existent zone such as `Mars/Olympus_Mons` is accepted. + */ +class ConfigParseTimezoneOptionValue : SimpleGrammarOptionValues( + "config_parse_timezone", + SequenceCombinator( + TZ_COMPONENT, + ZeroOrMore(SequenceCombinator(CIDR_SEPARATOR, TZ_COMPONENT)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt index 1aa294f2..a73d2188 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt @@ -33,9 +33,10 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * actually reads the value. The classes below mirror that table entry by entry, so each one is * registered under the matching ROUTING_POLICY_RULE_* ltype. * - * Not covered here: ROUTING_POLICY_RULE_TABLE (route table names are user-defined via RouteTable= in - * networkd.conf, so there is no closed set to check), ROUTING_POLICY_RULE_FWMARK and - * ROUTING_POLICY_RULE_IP_PROTOCOL. + * Covered in their own files instead: ROUTING_POLICY_RULE_TABLE (ConfigParseRouteTableOptionValue), + * ROUTING_POLICY_RULE_FWMARK (ConfigParseRoutingPolicyRuleFirewallMarkOptionValue), + * ROUTING_POLICY_RULE_IP_PROTOCOL (ConfigParseRoutingPolicyRuleIpProtocolOptionValue) and + * ROUTING_POLICY_RULE_UID_RANGE (ConfigParseRoutingPolicyRuleUidRangeOptionValue). */ private const val RULE = "config_parse_routing_policy_rule" diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt new file mode 100644 index 00000000..48154b00 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt @@ -0,0 +1,365 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigInteger + +/* + * Shared helpers for the network-route-addr validator batch (2026-10): [Route], [RoutingPolicyRule], + * [Address], [Neighbor], [IPv6SendRA] / [IPv6Prefix] / [IPv6RoutePrefix] / [IPv6PREF64Prefix] and the + * generic strv / string / match parsers. + * + * 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. + * + * Kept in its own file (rather than Combinators.kt) because several validator batches were written in + * parallel; the candidates for promotion are listed in the batch RECOMMENDATIONS.md. + */ + +// --------------------------------------------------------------------------------------------------- +// Checked — a value-level check on top of a structural grammar. + +/** + * Matches exactly what [inner] matches, but additionally requires [predicate] to hold for the text it + * consumed. + * + * Use it when systemd's acceptance depends on the *value* a well-formed span denotes rather than on its + * shape — a time span that has to fit in a uint32 of milliseconds, a metric triple that has to be + * strictly increasing, an address that may not be the ANY address. The structural grammar still drives + * tokenization and highlighting; only validity changes. + * + * When the predicate fails the first token of the span is reported as the bad one (under both engines), + * since there is no single token to blame for a property of the whole span. + */ +class Checked(private val inner: Combinator, private val predicate: (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) return match + if (!predicate(value.substring(offset, match.matchResult))) return NoMatch.copy(longestMatch = offset) + return match + } + + override fun parse(value: String, offset: Int): Sequence = + inner.parse(value, offset).map { step -> + // The predicate only runs on spans whose tokens are all individually valid, so predicates may assume + // well-formed input (e.g. a digit run that fits its range) and need not be total. + if (step is Parse && step.tokens.isNotEmpty() && step.end > offset && step.tokens.all { it.valid } && + !predicate(value.substring(offset, step.end))) { + step.copy(tokens = listOf(step.tokens[0].copy(valid = false)) + step.tokens.drop(1)) + } else { + step + } + } + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "Checked(\n" + inner.toStringIndented(indent + 1) + "\n" + " ".repeat(indent) + ")" + + override fun toString(): String = toStringIndented(0) +} + + +// --------------------------------------------------------------------------------------------------- +// parse_sec() — parse_time(t, &usec, USEC_PER_SEC) in src/basic/time-util.c. +// +// The grammar mirrors parse_time()'s loop: a run of `NUMBER [whitespace] [UNIT]` elements, where +// - NUMBER is strtoll() base 10 (so an optional `+`) with an optional `.fraction`; `.5` is fine, `5.` +// is not ("Don't allow … 3.sec"); +// - elements may abut (`1h30min`, `5s+3s`) or be separated by whitespace; +// - a number with no unit defaults to seconds and must then be followed by whitespace or the end — +// extract_multiplier() refuses `12.34.56` but accepts `12.34 .56`; +// - `infinity` stands alone. +// Units are matched by a FlexibleLiteralChoiceTerminal: a unit is always followed by a digit, a sign, +// whitespace or the end, so the terminal's greedy letter run lines up with extract_multiplier()'s +// longest-prefix-first table and a misspelt unit is highlighted as one token. + +/** extract_multiplier()'s table, in microseconds. */ +private val TIME_UNIT_USEC: Map = run { + val sec = BigInteger.valueOf(1_000_000L) + val min = sec.multiply(BigInteger.valueOf(60)) + val hour = min.multiply(BigInteger.valueOf(60)) + val day = hour.multiply(BigInteger.valueOf(24)) + mapOf( + "seconds" to sec, "second" to sec, "sec" to sec, "s" to sec, + "minutes" to min, "minute" to min, "min" to min, + "months" to sec.multiply(BigInteger.valueOf(2_629_800L)), "month" to sec.multiply(BigInteger.valueOf(2_629_800L)), + "M" to sec.multiply(BigInteger.valueOf(2_629_800L)), + "msec" to BigInteger.valueOf(1000), "ms" to BigInteger.valueOf(1000), + "m" to min, + "hours" to hour, "hour" to hour, "hr" to hour, "h" to hour, + "days" to day, "day" to day, "d" to day, + "weeks" to day.multiply(BigInteger.valueOf(7)), "week" to day.multiply(BigInteger.valueOf(7)), "w" to day.multiply(BigInteger.valueOf(7)), + "years" to sec.multiply(BigInteger.valueOf(31_557_600L)), "year" to sec.multiply(BigInteger.valueOf(31_557_600L)), + "y" to sec.multiply(BigInteger.valueOf(31_557_600L)), + "usec" to BigInteger.ONE, "us" to BigInteger.ONE, "μs" to BigInteger.ONE, "µs" to BigInteger.ONE, + ) +} + +/** NETWORK_ROUTE_ADDR_USEC_INFINITY, i.e. UINT64_MAX. */ +val NETWORK_ROUTE_ADDR_USEC_INFINITY: BigInteger = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE) +val USEC_PER_SEC: BigInteger = BigInteger.valueOf(1_000_000L) +val USEC_PER_MSEC: BigInteger = BigInteger.valueOf(1_000L) + +// `+.5` is refused: strtoll() reads no digits after the sign, so e == p and *e is not '.'. +private val TIME_NUMBER = RegexTerminal("""[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]*)""", """(?:\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+)""") +private val TIME_UNIT = FlexibleLiteralChoiceTerminal(*TIME_UNIT_USEC.keys.toTypedArray()) +private val TIME_INFINITY = FlexibleLiteralChoiceTerminal("infinity") + +private val TIME_ELEMENT = AlternativeCombinator( + SequenceCombinator(TIME_NUMBER, ZeroOrOne(WhitespaceTerminal()), TIME_UNIT), + SequenceCombinator(TIME_NUMBER, AlternativeCombinator(WhitespaceTerminal(), EOF())), +) + +/** Every spelling parse_sec() accepts, structurally; not range-checked. Not followed by EOF. */ +val NETWORK_ROUTE_ADDR_PARSE_SEC = AlternativeCombinator( + TIME_INFINITY, + OneOrMore(SequenceCombinator(TIME_ELEMENT, ZeroOrOne(WhitespaceTerminal()))), +) + +private val TIME_ELEMENT_REGEX = Regex("""\s*(?:\+(?=[0-9]))?([0-9]*)(?:\.([0-9]+))?\s*(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)?""") + +/** + * The value parse_sec() gives [text] in microseconds — [NETWORK_ROUTE_ADDR_USEC_INFINITY] for `infinity` — or null if + * parse_sec() would fail (including its -ERANGE overflow checks). [text] is expected to be something + * [NETWORK_ROUTE_ADDR_PARSE_SEC] matched. + */ +fun parseSecUsec(text: String): BigInteger? { + val t = text.trim() + if (t == "infinity") return NETWORK_ROUTE_ADDR_USEC_INFINITY + var pos = 0 + var usec = BigInteger.ZERO + var something = false + while (pos < t.length) { + val m = TIME_ELEMENT_REGEX.matchAt(t, pos) ?: return null + if (m.value.isEmpty()) return null + val whole = m.groupValues[1] + val frac = m.groupValues[2] + if (whole.isEmpty() && frac.isEmpty()) return null + val multiplier = TIME_UNIT_USEC[m.groupValues[3]] ?: USEC_PER_SEC + val l = if (whole.isEmpty()) BigInteger.ZERO else BigInteger(whole) + if (l >= NETWORK_ROUTE_ADDR_USEC_INFINITY.divide(multiplier)) return null + usec = usec.add(l.multiply(multiplier)) + var scale = multiplier.divide(BigInteger.TEN) + for (c in frac) { + usec = usec.add(BigInteger.valueOf((c - '0').toLong()).multiply(scale)) + scale = scale.divide(BigInteger.TEN) + } + if (usec >= NETWORK_ROUTE_ADDR_USEC_INFINITY) return null + something = true + pos = m.range.last + 1 + } + return if (something) usec else null +} + +/** + * A parse_sec() value whose microsecond count must satisfy [accept] (which receives [NETWORK_ROUTE_ADDR_USEC_INFINITY] for + * `infinity`). Not followed by EOF. + */ +fun timeSpan(accept: (BigInteger) -> Boolean = { true }): Combinator = + Checked(NETWORK_ROUTE_ADDR_PARSE_SEC) { text -> parseSecUsec(text)?.let(accept) ?: false } + +/** DIV_ROUND_UP(a, b) for non-negative a. */ +fun divRoundUp(a: BigInteger, b: BigInteger): BigInteger = a.add(b).subtract(BigInteger.ONE).divide(b) + + +// --------------------------------------------------------------------------------------------------- +// parse_size(t, 1024, …) — src/basic/parse-util.c. +// +// A run of `NUMBER [whitespace] [SUFFIX]` elements where each suffix must be strictly smaller than the +// previous one (start_pos), a bare number (the "" suffix, bytes) ends the run, and NUMBER is strtoull() +// base 10 with an optional fraction (`10.M` is accepted: "Ignore failure"). + +private val SIZE_NUMBER = RegexTerminal("""[+-]?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""") +private val SIZE_SUFFIX = LiteralChoiceTerminal("E", "P", "T", "G", "M", "K", "B") +private val SIZE_ELEMENT = SequenceCombinator(SIZE_NUMBER, ZeroOrOne(WhitespaceTerminal()), ZeroOrOne(SIZE_SUFFIX)) + +/** Every spelling parse_size() (base 1024) accepts, structurally. Not followed by EOF. */ +val PARSE_SIZE_IEC = SequenceCombinator(SIZE_ELEMENT, ZeroOrMore(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), SIZE_ELEMENT))) + +private val SIZE_ELEMENT_REGEX = Regex("""\s*\+?([0-9]+)(?:\.([0-9]*))?\s*([EPTGMKB]?)""") +private val SIZE_ORDER = listOf("E", "P", "T", "G", "M", "K", "B", "") + +/** The byte count parse_size(text, 1024) yields, or null if it would fail. */ +fun parseSizeIec(text: String): BigInteger? { + val t = text.trim() + var pos = 0 + var total = BigInteger.ZERO + var startPos = 0 + val max = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE) + if (t.isEmpty()) return null + while (pos < t.length) { + val m = SIZE_ELEMENT_REGEX.matchAt(t, pos) ?: return null + val suffix = m.groupValues[3] + val index = SIZE_ORDER.indexOf(suffix) + if (index < startPos) return null + val factor = BigInteger.valueOf(1024).pow(6 - minOf(index, 6)) + val whole = BigInteger(m.groupValues[1]) + val fracDigits = m.groupValues[2] + var bytes = whole.multiply(factor) + if (fracDigits.isNotEmpty()) { + bytes = bytes.add(factor.multiply(BigInteger(fracDigits)).divide(BigInteger.TEN.pow(fracDigits.length))) + } + total = total.add(bytes) + if (total > max) return null + startPos = index + 1 + pos = m.range.last + 1 + if (m.value.isEmpty()) return null + } + return total +} + + +// --------------------------------------------------------------------------------------------------- +// extract_first_word() word lists (src/basic/extract-word.c). + +/** + * One word as extract_first_word(…, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) reads it: runs of bare + * characters and `"…"` / `'…'` quoted pieces, glued together (`a"b c"d` is the single word `ab cd`). + * With EXTRACT_RETAIN_ESCAPE a backslash is an ordinary character, even inside quotes, so the only way + * such a word can fail is an unterminated quote. The quoted pieces match leniently without their + * closing quote so the unterminated piece is the token highlighted. + */ +val RETAIN_ESCAPE_WORD: Combinator = OneOrMore( + AlternativeCombinator( + RegexTerminal("\"[^\"]*\"?", "\"[^\"]*\""), + RegexTerminal("'[^']*'?", "'[^']*'"), + RegexTerminal("[^\\s\"']+", "[^\\s\"']+"), + ) +) + +/** A whitespace-separated list of [RETAIN_ESCAPE_WORD]s, followed by EOF. */ +val RETAIN_ESCAPE_WORD_LIST: Combinator = SequenceCombinator( + RETAIN_ESCAPE_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), RETAIN_ESCAPE_WORD)), + EOF(), +) + +// The C escapes cunescape_one() understands (src/basic/escape.c). NUL and surrogate code points, which +// it refuses, are not singled out. +private const val C_ESCAPE = """\\(?:[abfnrtv\\"'s]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-3][0-7]{2})""" + +/** + * One word as extract_first_word(…, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) reads it: like + * [RETAIN_ESCAPE_WORD], but a backslash starts a C escape both inside and outside quotes, and an + * unknown escape is an error. + */ +val CUNESCAPE_WORD: Combinator = OneOrMore( + AlternativeCombinator( + RegexTerminal("\"(?:[^\"\\\\]|\\\\[\\s\\S])*\"?", "\"(?:[^\"\\\\]|$C_ESCAPE)*\""), + RegexTerminal("'(?:[^'\\\\]|\\\\[\\s\\S])*'?", "'(?:[^'\\\\]|$C_ESCAPE)*'"), + RegexTerminal("(?:[^\\s\"'\\\\]|\\\\[\\s\\S])+", "(?:[^\\s\"'\\\\]|$C_ESCAPE)+"), + ) +) + +/** + * [text] with its quotes removed and its escapes collapsed to the escaped character — enough to look at + * the word's structure (where its `=` is, what precedes it). Escapes are not decoded faithfully. + */ +fun unquoteWord(text: String): String { + val sb = StringBuilder() + var quote: Char? = null + var i = 0 + while (i < text.length) { + val c = text[i] + when { + c == '\\' && i + 1 < text.length -> { + sb.append(text[i + 1]); i += 2; continue + } + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> sb.append(c) + } + i++ + } + return sb.toString() +} + +/** + * A whitespace-separated list of interface names as extract_first_word(…, 0) splits it — no quote + * handling, so a name is simply a run of non-whitespace characters — each checked with [name]. + */ +fun plainNameList(name: Combinator): Combinator = SequenceCombinator( + name, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), name)), + EOF(), +) + + +// --------------------------------------------------------------------------------------------------- +// Address literals, as bytes — for the value checks inet_pton() parsing alone cannot express. + +/** The 4 or 16 bytes an IPv4 / IPv6 literal (as matched by [IP_ADDR]) denotes, or null. */ +fun ipLiteralBytes(text: String): ByteArray? = if (':' in text) ipv6Bytes(text) else ipv4Bytes(text) + +private fun ipv4Bytes(text: String): ByteArray? { + val parts = text.split('.') + if (parts.size != 4) return null + val out = ByteArray(4) + for ((i, p) in parts.withIndex()) { + val v = p.toIntOrNull() ?: return null + if (v !in 0..255) return null + out[i] = v.toByte() + } + return out +} + +private fun ipv6Bytes(text: String): ByteArray? { + var s = text + var tail = ByteArray(0) + // An embedded dotted-quad supplies the last two groups. + val lastColon = s.lastIndexOf(':') + if ('.' in s.substring(lastColon + 1)) { + tail = ipv4Bytes(s.substring(lastColon + 1)) ?: return null + s = s.substring(0, lastColon + 1) + "0:0" + } + val dc = s.indexOf("::") + val groups: List = if (dc >= 0) { + val head = s.substring(0, dc).split(':').filter { it.isNotEmpty() } + val rest = s.substring(dc + 2).split(':').filter { it.isNotEmpty() } + val missing = 8 - head.size - rest.size + if (missing < 1) return null + head + List(missing) { "0" } + rest + } else { + s.split(':') + } + if (groups.size != 8) return null + val out = ByteArray(16) + for ((i, g) in groups.withIndex()) { + if (g.isEmpty() || g.length > 4) return null + val v = g.toIntOrNull(16) ?: return null + out[2 * i] = (v shr 8).toByte() + out[2 * i + 1] = v.toByte() + } + if (tail.isNotEmpty()) tail.copyInto(out, 12) + return out +} + +/** in_addr_is_null(): every byte zero. */ +fun ipIsNull(bytes: ByteArray): Boolean = bytes.all { it.toInt() == 0 } + +/** in_addr_is_multicast(): 224.0.0.0/4 for IPv4, ff00::/8 for IPv6. */ +fun ipIsMulticast(bytes: ByteArray): Boolean = + if (bytes.size == 4) (bytes[0].toInt() and 0xF0) == 0xE0 else (bytes[0].toInt() and 0xFF) == 0xFF + + +// --------------------------------------------------------------------------------------------------- +// IPv6 prefixes — in_addr_prefix_from_string(…, AF_INET6, …) (src/basic/in-addr-util.c): an IPv6 literal +// and an optional `/LEN`, where LEN goes through in_addr_parse_prefixlen → safe_atou8 (base 0) and must +// be ≤ 128. The length is everything after the `/`, so whitespace reaches safe_atou8 (`::/ 64` and +// `::/0b 1` are fine). Without a slash the prefix length is 128. Not followed by EOF. +val IPV6_PREFIX_SAFE_LEN: Combinator = SequenceCombinator( + IPV6_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), +) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt new file mode 100644 index 00000000..ed63fe98 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt @@ -0,0 +1,39 @@ +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 + +/** [Address] Label= (ADDRESS_LABEL). */ +class ConfigParseAddressLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Address] + Label=eth0:1 + Label=with space + Label=123456789012345 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + Label=1234567890123456 + Label=café + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt new file mode 100644 index 00000000..b82bc4a3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.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 + +/** [Address] NetLabel= (ADDRESS_NET_LABEL -> config_parse_string(CONFIG_PARSE_STRING_SAFE)). */ +class ConfigParseAddressNetLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // CONFIG_PARSE_STRING_SAFE uses STRING_ALLOW_GLOBS and does not require ASCII, so globs and UTF-8 are fine + // (systemd oracle check 2026-10-24). + // language="unit file (systemd)" + val file = """ + [Address] + NetLabel=system_u:object_r:root_t:s0 + NetLabel=unconfined_u:object_r:* + NetLabel=foo*? + NetLabel=café + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // systemd logs these at warning level, but it drops the value and with it the whole [Address] section + // (config_section_parse returns 0, so address_unref_or_set_invalidp marks the section invalid). + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + NetLabel="system_u:object_r:root_t:s0" + NetLabel=back\\slash + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt new file mode 100644 index 00000000..6fe8125e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.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 + +/** config_parse_can_restart_usec. */ +class ConfigParseCanRestartUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [CAN] + RestartSec=0 + RestartSec=100ms + RestartSec=1.5s + RestartSec=infinity + RestartSec=49d + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [CAN] + RestartSec=50d + RestartSec=never + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt new file mode 100644 index 00000000..30321237 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.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 + +/** config_parse_in_addr_non_null(AF_INET6). */ +class ConfigParseInAddrNonNullIpv6OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv6StableSecretAddress=2001:db8::1 + IPv6StableSecretAddress=::1 + IPv6StableSecretAddress=fe80::1234:5678 + IPv6StableSecretAddress=::ffff:192.168.1.1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + IPv6StableSecretAddress=:: + IPv6StableSecretAddress=0:0:0:0:0:0:0:0 + IPv6StableSecretAddress=192.168.1.1 + IPv6StableSecretAddress=2001:db8::/64 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt new file mode 100644 index 00000000..dec188ef --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.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 + +/** [IPv6AddressLabel] Label=. */ +class ConfigParseIpv6AddressLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Label=0 + Label=100 + Label=4294967294 + Label=0xfffffffe + Label=0b 1 + Label=0b+1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1 and -0b0 (EINVAL) and accepts 0b 1 and 0b+1. + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Label=4294967295 + Label=0xffffffff + Label=-1 + Label=label + Label=+0b1 + Label=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt new file mode 100644 index 00000000..52452cc4 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.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 + +/** [IPv6AddressLabel] Prefix=. */ +class ConfigParseIpv6AddressLabelPrefixOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Prefix=2001:db8::/32 + Prefix=::/0 + Prefix=::ffff:0:0/96 + Prefix=fe80::1 + Prefix=2001:db8::/ 32 + Prefix=::/0b 1 + Prefix=::ffff:0:0/0b1100000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects ::/+0b1 (EINVAL) and ::ffff:0:0/0b1100001 (97 > 96), and accepts ::/0b 1. + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Prefix=::ffff:0:0/97 + Prefix=::ffff:1.2.3.4 + Prefix=10.0.0.0/8 + Prefix=2001:db8::/200 + Prefix=::/+0b1 + Prefix=::ffff:0:0/0b1100001 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt new file mode 100644 index 00000000..31e08f81 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt @@ -0,0 +1,76 @@ +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 + +/** config_parse_match_property: NAME=VALUE words with CUNESCAPE|UNQUOTE. */ +class ConfigParseMatchPropertyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Match] + Property=ID_MODEL_ID=9999 "ID_VENDOR_FROM_DATABASE=vendor name" "KEY=with \"quotation\"" + Property=HOGE=foo BAR=baz + Property=ID_NET_MANAGED_BY=io.systemd.Network + Property=!ID_BUS=usb + Property=! ID_BUS=usb + Property=! + Property=EMPTY= + Property=KEY="value in quotes" + Property=_UNDERSCORE1=x\x41 + Property=GLOB=foo* + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Property=NOEQUALS + Property=1BAD=x + Property=BAD-NAME=x + Property="KEY=unterminated + Property=KEY=bad\qescape + Property=GOOD=1 !BAD=2 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } + @Test + fun testValidValuesLink() { + // language="unit file (systemd)" + val file = """ + [Match] + Property=ID_PATH=pci-0000:00:1f.6 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesLink() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Property==value + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt new file mode 100644 index 00000000..9d58d26d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt @@ -0,0 +1,78 @@ +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 + +/** config_parse_match_strv: glob lists with only quote balance checked. */ +class ConfigParseMatchStrvOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Match] + Path=pci-0000:02:00.0-* + Driver=dummy + Driver=!e1000e igb + Type=ether wlan + Type=!loopback + Kind=dummy veth + Kind=! + WLANInterfaceType=ap station + SSID="My Home Wifi" 'Other Net' + SSID=Cafe\ Wifi + SSID=pre"fix mid"dle + Path= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + SSID="unterminated + Kind=bond 'veth + Driver=a"b + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } + @Test + fun testValidValuesLink() { + // language="unit file (systemd)" + val file = """ + [Match] + Path=pci-0000:00:1f.6 + Driver=e1000e + Type=ether + Kind=!* + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesLink() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Path="pci-0000 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt new file mode 100644 index 00000000..67dcf434 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt @@ -0,0 +1,55 @@ +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 + +/** config_parse_ndisc_route_metric. */ +class ConfigParseNdiscRouteMetricOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AcceptRA] + RouteMetric=256 + RouteMetric=512:1024:2048 + RouteMetric=100:200:300 + RouteMetric=0 + RouteMetric=0x10:0x20:0x30 + RouteMetric=100: 200: 300 + RouteMetric=100:200:300: + RouteMetric=0b 1 + RouteMetric=0b1:0b 10:0b11 + [DHCPv6] + RouteMetric=1024 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1 and 1:+0b10:3, and accepts 0b 1 and 0b1:0b 10:0b11. + // language="unit file (systemd)" + val file = """ + [IPv6AcceptRA] + RouteMetric=hoge + RouteMetric=300:200:100 + RouteMetric=100:100:200 + RouteMetric=1:2 + RouteMetric=1:2:3:4 + RouteMetric=1::3 + RouteMetric=1:2:3:: + RouteMetric=100 :200:300 + RouteMetric=+0b1 + RouteMetric=1:+0b10:3 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt new file mode 100644 index 00000000..0a98a141 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.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 + +/** [Neighbor] Address= (NEIGHBOR_DESTINATION_ADDRESS). */ +class ConfigParseNeighborDestinationAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Neighbor] + Address=192.168.10.1 + Address=2004:da8:1:0::1 + Address=0.0.0.0 + Address=fe80::1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Neighbor] + Address=192.168.10.1/24 + Address=not-an-address + Address=256.1.1.1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt new file mode 100644 index 00000000..fa88ef74 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.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 + +/** config_parse_neighbor_proxy_address. */ +class ConfigParseNeighborProxyAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + ProxyNeighbor=192.168.1.10 + ProxyNeighbor=2001:db8::1 + ProxyNeighbor=::ffff:10.0.0.1 + ProxyNeighbor=255.255.255.254 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + ProxyNeighbor=0.0.0.0 + ProxyNeighbor=:: + ProxyNeighbor=224.0.0.1 + ProxyNeighbor=ff02::1 + ProxyNeighbor=255.255.255.255 + ProxyNeighbor=192.168.1.0/24 + ProxyNeighbor=fe80::1%eth0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt new file mode 100644 index 00000000..ef45133c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt @@ -0,0 +1,61 @@ +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 + +/** config_parse_nft_set(NFT_SET_PARSE_NETWORK) and [Address] NFTSet=. */ +class ConfigParseNftSetNetworkOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Address] + NFTSet=address:inet:sd_test:addr4 prefix:inet:sd_test:network4 ifindex:inet:sd_test:ifindex + NFTSet=prefix:netdev:filter:eth_ipv4_prefix + NFTSet="address:ip6:my.table:set/one" + [DHCPv4] + NFTSet=address:ip:filter:dhcp4 + [DHCPv6] + NFTSet=prefix:ip6:filter:dhcp6_prefix + [IPv6AcceptRA] + NFTSet=address:inet:t:s + [DHCPPrefixDelegation] + NFTSet=prefix:bridge:t_1:s_2 + NFTSet=address::inet:t:s + NFTSet=:address:inet:t:s + NFTSet=address:inet:t:s: + NFTSet=address:"inet":t:s + NFTSet='address':inet:"t":s prefix:ip:t:s + NFTSet=address:inet:"t:s" + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + NFTSet=cgroup:inet:filter:set + NFTSet=address:ipx:filter:set + NFTSet=address:inet:1table:set + NFTSet=address:inet:table + NFTSet=address:inet:table:set:extra + [DHCPv4] + NFTSet=address:inet:filter:bad-name + NFTSet=address:"inet:t + NFTSet=address:"ipx":t:s + NFTSet="address:inet:t:s:extra" + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt new file mode 100644 index 00000000..b283ebe1 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.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 + +/** config_parse_pref64_prefix_lifetime: at most 8191 * 8s, not infinity. */ +class ConfigParsePref64PrefixLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6PREF64Prefix] + LifetimeSec=1800 + LifetimeSec=0 + LifetimeSec=65528 + LifetimeSec=18h 12min 8s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6PREF64Prefix] + LifetimeSec=65529 + LifetimeSec=infinity + LifetimeSec=1d + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt new file mode 100644 index 00000000..551a212f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.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 + +/** [IPv6Prefix] Token= (config_parse_prefix_token -> config_parse_address_generation_type). */ +class ConfigParsePrefixTokenOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6Prefix] + Token=eui64 + Token=::1a:2b:3c:4d + Token=static:::fa:de:ca:fe + Token=prefixstable + Token=prefixstable:2002:da8:1::,86b123b969ba4b7eb8b3d8605123525a + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6Prefix] + Token=prefixstable@ + Token=prefixstable,00000000000000000000000000000000 + Token=static: + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt new file mode 100644 index 00000000..d728ab06 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.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 + +/** [Route] TCPAdvertisedMaximumSegmentSize=. */ +class ConfigParseRouteMetricAdvMssOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + TCPAdvertisedMaximumSegmentSize=30 + TCPAdvertisedMaximumSegmentSize=1460 + TCPAdvertisedMaximumSegmentSize=1K + TCPAdvertisedMaximumSegmentSize=4294967295 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + TCPAdvertisedMaximumSegmentSize=0 + TCPAdvertisedMaximumSegmentSize=4G + TCPAdvertisedMaximumSegmentSize=big + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt new file mode 100644 index 00000000..50a7ff2b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.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 + +/** [Route] InitialCongestionWindow=. */ +class ConfigParseRouteMetricInitCwndOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // safe_atou32 sees the whole value, so whitespace or a sign after a 0b/0o prefix is fine (sd-check accepts 0b 1, 0b+1). + // language="unit file (systemd)" + val file = """ + [Route] + InitialCongestionWindow=20 + InitialCongestionWindow=1 + InitialCongestionWindow=1023 + InitialCongestionWindow=0x10 + InitialCongestionWindow=0b 1 + InitialCongestionWindow=0b+1 + InitialCongestionWindow=0o 7 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. +0b1 and -0b0 are EINVAL: systemd turns off glibc's C23 0b parsing (sd-check rejects both). + // language="unit file (systemd)" + val file = """ + [Route] + InitialCongestionWindow=0 + InitialCongestionWindow=1024 + InitialCongestionWindow=ten + InitialCongestionWindow=+0b1 + InitialCongestionWindow=-0b0 + InitialCongestionWindow=08 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt new file mode 100644 index 00000000..f94ca474 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.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 + +/** [Route] MTUBytes= (config_parse_mtu, AF_UNSPEC). */ +class ConfigParseRouteMetricMtuOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + MTUBytes=1480 + MTUBytes=9000 + MTUBytes=0 + MTUBytes=1K + MTUBytes=1.5K + MTUBytes=1 K + MTUBytes=1K 512 + MTUBytes=4294967295 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + MTUBytes=4G + MTUBytes=1500X + MTUBytes=512 1K + MTUBytes=-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt new file mode 100644 index 00000000..18841fde --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.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 + +/** [Route] TCPRetransmissionTimeoutSec=. */ +class ConfigParseRouteMetricTcpRtoOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + TCPRetransmissionTimeoutSec=300s + TCPRetransmissionTimeoutSec=200ms + TCPRetransmissionTimeoutSec=1 + TCPRetransmissionTimeoutSec=1us + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + TCPRetransmissionTimeoutSec=0 + TCPRetransmissionTimeoutSec=infinity + TCPRetransmissionTimeoutSec=50d + TCPRetransmissionTimeoutSec=fast + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt new file mode 100644 index 00000000..38143c82 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt @@ -0,0 +1,57 @@ +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 + +/** [Route] MultiPathRoute=. */ +class ConfigParseRouteMultiPathOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + MultiPathRoute=10.0.0.1@eth0 20 + MultiPathRoute=192.168.1.1 50 + MultiPathRoute=@wg0 15 + MultiPathRoute=2001:db8::1@eth0 + MultiPathRoute=2001:1234:5:9fff:ff:ff:ff:ff@dummy98 5 + MultiPathRoute=@1 15 + MultiPathRoute=@999999 10 + MultiPathRoute=10.0.0.1@5 + MultiPathRoute=fe80::1 + MultiPathRoute=@eth0 256 + MultiPathRoute=@eth0 1 + MultiPathRoute=@eth0 0b 1 + MultiPathRoute=@+0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects the weight +0b1 and the empty device in `@ 5`; it accepts the + // weight 0b 1 and the device name +0b1 (parse_ifindex refuses it, ifname_valid accepts it). + // language="unit file (systemd)" + val file = """ + [Route] + MultiPathRoute=@eth0 0 + MultiPathRoute=@eth0 257 + MultiPathRoute=@ + MultiPathRoute=@0 + MultiPathRoute=10.0.0.1/24@eth0 + MultiPathRoute=@eth0 10 20 + MultiPathRoute=eth0 + MultiPathRoute=@eth0 +0b1 + MultiPathRoute=@ 5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt new file mode 100644 index 00000000..69b8e838 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.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 + +/** config_parse_route_prefix_lifetime. */ +class ConfigParseRoutePrefixLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + LifetimeSec=1000 + LifetimeSec=infinity + LifetimeSec=0 + LifetimeSec=4294967294 + LifetimeSec=1w + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + LifetimeSec=4294967295 + LifetimeSec=200y + LifetimeSec=forever + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt new file mode 100644 index 00000000..4f269c92 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.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 + +/** [IPv6RoutePrefix] Route=. */ +class ConfigParseRoutePrefixOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + Route=2001:db0:fff::/64 + Route=2001:db2:fff::/48 + Route=::/0 + Route=2001:db8::1 + Route=2001:db8::/0x40 + Route=2001:db8::/ 64 + Route=2001:db8::/0b 1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + Route=192.168.0.0/24 + Route=2001:db8::/129 + Route=2001:db8::/ + Route=2001:db8::/+0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt new file mode 100644 index 00000000..cce75651 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt @@ -0,0 +1,56 @@ +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 + +/** [Route] Table= and [RoutingPolicyRule] Table=. */ +class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // custom-table, my_table, 0x0 and `main table` are deliberate leniencies: they are legal networkd.conf RouteTable= + // names (only all-digit names and names with `:` are refused; `main\ table:100` defines one with a space), which the + // plugin cannot see. systemd without such a definition rejects them. 0b 1 is safe_atou32 = 1 (sd-check accepts it). + // language="unit file (systemd)" + val file = """ + [Route] + Table=main + Table=local + Table=default + Table=1011 + Table=4294967295 + Table=custom-table + Table=0x0 + [RoutingPolicyRule] + Table=7 + Table=my_table + Table=0x10 + Table=0b 1 + Table=main table + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + Table=0 + Table=4294967296 + Table=a:b + [RoutingPolicyRule] + Table=00 + Table=08 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt new file mode 100644 index 00000000..5c782dde --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.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 + +/** config_parse_router_home_agent_lifetime: 0 < t <= 65535s. */ +class ConfigParseRouterHomeAgentLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + HomeAgentLifetimeSec=1 + HomeAgentLifetimeSec=65535 + HomeAgentLifetimeSec=18h 12min 15s + HomeAgentLifetimeSec=500ms + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + HomeAgentLifetimeSec=0 + HomeAgentLifetimeSec=65536 + HomeAgentLifetimeSec=infinity + HomeAgentLifetimeSec=1day + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt new file mode 100644 index 00000000..afcadc96 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.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 + +/** config_parse_router_lifetime: any parse_sec() value (out-of-range values are clamped). */ +class ConfigParseRouterLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + RouterLifetimeSec=1800 + RouterLifetimeSec=0 + RouterLifetimeSec=30min + RouterLifetimeSec=1s + RouterLifetimeSec=10000 + RouterLifetimeSec=infinity + RouterLifetimeSec=.5 + RouterLifetimeSec=+5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + RouterLifetimeSec=forever + RouterLifetimeSec=30 minuts + RouterLifetimeSec=+.5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt new file mode 100644 index 00000000..80f2a7e9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt @@ -0,0 +1,55 @@ +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 + +/** config_parse_router_prefix_delegation (IPv6SendRA= and deprecated IPv6PrefixDelegation=). */ +class ConfigParseRouterPrefixDelegationOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv6SendRA=yes + IPv6SendRA=no + IPv6SendRA=True + IPv6SendRA=0 + IPv6SendRA=Yes + IPv6SendRA=TRUE + IPv6SendRA=oFf + IPv6PrefixDelegation=static + IPv6PrefixDelegation=dhcpv6 + IPv6PrefixDelegation=yes + IPv6PrefixDelegation=Yes + IPv6PrefixDelegation=TRUE + IPv6PrefixDelegation=oFf + IPv6PrefixDelegation=No + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + IPv6SendRA=maybe + IPv6SendRA=both + IPv6SendRA=static + IPv6SendRA=dhcpv6 + IPv6PrefixDelegation=Static + IPv6PrefixDelegation=DHCPv6 + IPv6PrefixDelegation=maybe + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt new file mode 100644 index 00000000..7dcd2384 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt @@ -0,0 +1,50 @@ +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 + +/** config_parse_router_uint32_msec_usec. */ +class ConfigParseRouterUint32MsecUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + ReachableTimeSec=30s + ReachableTimeSec=0 + ReachableTimeSec=1h30min + ReachableTimeSec=1500ms + ReachableTimeSec=4294967295ms + ReachableTimeSec=infinity + RetransmitSec=1.5 + RetransmitSec=.5s + RetransmitSec=2 min + RetransmitSec=1d 2h + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + ReachableTimeSec=4294967296ms + ReachableTimeSec=50d + RetransmitSec=5x + RetransmitSec=-1s + RetransmitSec=5. + RetransmitSec=12.34.56 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt new file mode 100644 index 00000000..dc724aa2 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.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 + +/** [RoutingPolicyRule] FirewallMark=. */ +class ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + FirewallMark=1011 + FirewallMark=7/255 + FirewallMark=0x10/0xff + FirewallMark=0 + FirewallMark=4294967295/4294967295 + FirewallMark=0b101 + FirewallMark=7/ 255 + FirewallMark=0b 1 + FirewallMark=1/0b 1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1, 1/+0b1 and 1 /2, and accepts 0b 1 and 1/0b 1. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + FirewallMark=4294967296 + FirewallMark=7/ + FirewallMark=/255 + FirewallMark=mark + FirewallMark=-1 + FirewallMark=+0b1 + FirewallMark=1/+0b1 + FirewallMark=1 /2 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(8, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt new file mode 100644 index 00000000..689f742d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt @@ -0,0 +1,62 @@ +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 + +/** [RoutingPolicyRule] IPProtocol=. */ +class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + IPProtocol=tcp + IPProtocol=UDP + IPProtocol=sctp + IPProtocol=icmpv6 + IPProtocol=6 + IPProtocol=17 + IPProtocol=0x11 + IPProtocol=255 + IPProtocol=aggfrag + IPProtocol=AGGFRAG + IPProtocol=144 + IPProtocol=0 + IPProtocol=58 + IPProtocol=TcP + IPProtocol=IcMpV6 + IPProtocol=L2TP + IPProtocol=0b 110 + IPProtocol=-0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b110 (EINVAL) and 0b-1 (-1: ERANGE), and accepts 0b 110 (tcp) and -0. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + IPProtocol=tcpp + IPProtocol=256 + IPProtocol=mptcp + IPProtocol=-6 + IPProtocol=3 + IPProtocol=200 + IPProtocol=TcPx + IPProtocol=Yes + IPProtocol=+0b110 + IPProtocol=0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt new file mode 100644 index 00000000..a9caaa4d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt @@ -0,0 +1,50 @@ +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 + +/** [RoutingPolicyRule] User=. */ +class ConfigParseRoutingPolicyRuleUidRangeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // systemd-network and +1000 are accepted as possible user names: systemd resolves them via NSS at runtime + // (get_user_creds), so they are only rejected on a machine without that user. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + User=100-200 + User=1000 + User=0 + User=root + User=systemd-network + User=1000-1000 + User=+1000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + User=200-100 + User=65535 + User=4294967295 + User=01000 + User=100- + User=1000-99999999999999999999 + User=99999999999999999999-1000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt new file mode 100644 index 00000000..32d3a47e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt @@ -0,0 +1,60 @@ +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 + +/** config_parse_stacked_netdev (all ten kinds). */ +class ConfigParseStackedNetdevOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // +0b1 is not an ifindex (safe_atoi gives EINVAL because systemd turns off glibc's C23 0b parsing), so it is a + // valid name; 0b-1 is the ifindex -1, which is not positive. sd-check accepts both names. + // language="unit file (systemd)" + val file = """ + [Network] + IPoIB=ib0.8001 + IPVLAN=ipvlan99 + IPVTAP=ipvtap99 + MACsec=macsec99 + MACVLAN=macvlan99 macvlan98 + MACVTAP=macvtap99 + Tunnel=sittun99 + VLAN=vlan99 + VLAN=vlan-1 vlan-2 + VXLAN=vxlan-slaac + Xfrm=xfrm98 + VLAN= + VLAN=0x0 + VLAN=-5 + VLAN=0xvlan + VLAN=+0b1 + VLAN=0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + VLAN=vlan:1 + VXLAN=this-name-is-way-too-long + Tunnel=123 + MACVLAN=ok all + IPVLAN=0x10 + VLAN=+5 + VLAN=0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt new file mode 100644 index 00000000..7b22ecc5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.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 + +/** config_parse_string(CONFIG_PARSE_STRING_SAFE_AND_ASCII). */ +class ConfigParseStringSafeAsciiOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + BootFilename=pxelinux.0 + BootFilename=efi/grubx64.efi + BootFilename=boot file with spaces*.efi + BootFilename=p@ss:w0rd!#[x]? + BootFilename=foo*bar[1]? + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + BootFilename=café.efi + BootFilename=back\\slash + BootFilename="quoted" + BootFilename=it's + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt new file mode 100644 index 00000000..c209d662 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt @@ -0,0 +1,68 @@ +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 + +/** config_parse_strv(0): word lists with only quote balance checked. */ +class ConfigParseStrvOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Network] + BindCarrier=eth0 eth1 + BindCarrier=en* + BindCarrier= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + BindCarrier=eth0 "eth1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } + @Test + fun testValidValuesNspawn() { + // language="unit file (systemd)" + val file = """ + [Exec] + Parameters=/bin/sh -c "echo hello world" + Parameters='single quoted' plain + Environment=FOO=bar "BAZ=with space" + Environment=PATH=/usr/bin\:/bin + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNspawn() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Exec] + Parameters=/bin/sh -c "echo + Environment=FOO='bar + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt new file mode 100644 index 00000000..8cf6bfa5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.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 + +/** config_parse_timezone (syntax of verify_timezone). */ +class ConfigParseTimezoneOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Nowhere/Fake is well-formed; systemd only rejects it because /usr/share/zoneinfo/Nowhere/Fake does not exist, + // which depends on the installed tzdata (deliberate leniency). + // language="unit file (systemd)" + val file = """ + [DHCPServer] + Timezone=Europe/Berlin + Timezone=UTC + Timezone=America/Argentina/Buenos_Aires + Timezone=Etc/GMT+5 + Timezone=Etc/GMT-14 + Timezone=Nowhere/Fake + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + Timezone=/Europe/Berlin + Timezone=Europe/ + Timezone=Europe//Berlin + Timezone=../etc/passwd + Timezone=Europe/Berlin Time + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt new file mode 100644 index 00000000..197c61f8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseIpv6AddressLabelOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseIpv6AddressLabelPrefixOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseNdiscRouteMetricOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRouteMetricInitCwndOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRouteMultiPathOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleFirewallMarkOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleIpProtocolOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleUidRangeOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseStackedNetdevOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** + * Values containing a vertical tab (`\v`), which config_parse() does not strip (it only strips " \t\n\r") and + * which strtoul() skips. They are hard to write in a fixture file, so they are checked here on the grammar, for + * both engines. Every verdict was confirmed with steve-local/systemd-tools/bin/sd-check at 0f0682b773, one value + * per .network file. + */ +class NetworkRouteAddrSafeAtoVerdictsTest { + + private fun accepted(optionValue: GrammarOptionValue, value: String) { + assertEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(optionValue.combinator, value)) + assertEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(optionValue.combinator, value)) + } + + private fun rejected(optionValue: GrammarOptionValue, value: String) { + assertNotEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(optionValue.combinator, value)) + assertNotEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(optionValue.combinator, value)) + } + + @Test + fun testLeadingVerticalTabReachesSafeAtou() { + accepted(ConfigParseRouteMetricInitCwndOptionValue(), "\u000B5") + accepted(ConfigParseIpv6AddressLabelOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleFirewallMarkOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleFirewallMarkOptionValue(), "1/\u000B5") + accepted(ConfigParseNdiscRouteMetricOptionValue(), "\u000B5") + accepted(ConfigParseRouteMultiPathOptionValue(), "@eth0 \u000B5") + accepted(ConfigParseIpv6AddressLabelPrefixOptionValue(), "::/\u000B64") + // safe_atoi, through parse_ip_protocol_full: 6 is tcp. + accepted(ConfigParseRoutingPolicyRuleIpProtocolOptionValue(), "\u000B6") + } + + @Test + fun testParseUidLetsVerticalTabThrough() { + // SAFE_ATO_REFUSE_LEADING_WHITESPACE only refuses " \t\n\r"; strtoul then skips the \v. + accepted(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "5-\u000B6") + // A space is refused by the same flag (sd-check: "Invalid uid or uid range '5- 6'"). + rejected(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "5- 6") + } + + @Test + fun testIfindexWithVerticalTabIsNotAName() { + // parse_ifindex("\v5") is 5, so ifname_valid() refuses it (sd-check: "Invalid netdev name in VLAN="). + rejected(ConfigParseStackedNetdevOptionValue(), "\u000B5") + } +}