From d701c0a66d15c764f05ed9e14f0246050d8d9bbe Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 23 Oct 2026 18:29:11 -0700 Subject: [PATCH 1/6] feat: add grammar validators for networkd routes, rules, addresses and RA settings (network-route-addr batch) Adds C-grounded value grammars (systemd 0f0682b773) for the [Route], [RoutingPolicyRule], [Address], [Neighbor], [IPv6AddressLabel], [IPv6SendRA]/[IPv6RoutePrefix]/[IPv6PREF64Prefix]/[IPv6AcceptRA] and [CAN] settings, plus the generic match/strv/string parsers, with tests for each. Shared helpers live in BatchNetworkRouteAddrCombinators.kt: a Checked value-predicate wrapper, a safe_atou*-accurate number terminal, structural parse_sec()/parse_size() grammars with exact value evaluation, and extract_first_word() word shapes. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 55 +++ .../ai/ConfigParseAddressLabelOptionValue.kt | 27 ++ .../ConfigParseCanRestartUsecOptionValue.kt | 34 ++ ...ConfigParseInAddrNonNullIpv6OptionValue.kt | 35 ++ .../ConfigParseIpv6AddressLabelOptionValue.kt | 24 + ...gParseIpv6AddressLabelPrefixOptionValue.kt | 42 ++ .../ai/ConfigParseMatchPropertyOptionValue.kt | 67 +++ .../ai/ConfigParseMatchStrvOptionValue.kt | 34 ++ .../ConfigParseNdiscRouteMetricOptionValue.kt | 42 ++ ...seNeighborDestinationAddressOptionValue.kt | 29 ++ ...figParseNeighborProxyAddressOptionValue.kt | 41 ++ .../ai/ConfigParseNftSetNetworkOptionValue.kt | 83 ++++ ...figParsePref64PrefixLifetimeOptionValue.kt | 35 ++ ...ConfigParseRouteMetricAdvMssOptionValue.kt | 37 ++ ...nfigParseRouteMetricInitCwndOptionValue.kt | 30 ++ .../ConfigParseRouteMetricMtuOptionValue.kt | 35 ++ ...ConfigParseRouteMetricTcpRtoOptionValue.kt | 39 ++ .../ConfigParseRouteMultiPathOptionValue.kt | 59 +++ ...nfigParseRoutePrefixLifetimeOptionValue.kt | 34 ++ .../ai/ConfigParseRoutePrefixOptionValue.kt | 30 ++ .../ai/ConfigParseRouteTableOptionValue.kt | 50 ++ ...ParseRouterHomeAgentLifetimeOptionValue.kt | 34 ++ .../ConfigParseRouterLifetimeOptionValue.kt | 29 ++ ...gParseRouterPrefixDelegationOptionValue.kt | 41 ++ ...figParseRouterUint32MsecUsecOptionValue.kt | 36 ++ ...outingPolicyRuleFirewallMarkOptionValue.kt | 36 ++ ...eRoutingPolicyRuleIpProtocolOptionValue.kt | 54 +++ ...rseRoutingPolicyRuleUidRangeOptionValue.kt | 63 +++ .../ai/ConfigParseStackedNetdevOptionValue.kt | 33 ++ .../ConfigParseStringSafeAsciiOptionValue.kt | 34 ++ .../ai/ConfigParseStrvOptionValue.kt | 32 ++ .../ai/ConfigParseTimezoneOptionValue.kt | 38 ++ .../BatchNetworkRouteAddrCombinators.kt | 428 ++++++++++++++++++ .../ConfigParseAddressLabelOptionValueTest.kt | 39 ++ ...nfigParseAddressNetLabelOptionValueTest.kt | 38 ++ ...onfigParseCanRestartUsecOptionValueTest.kt | 41 ++ ...igParseInAddrNonNullIpv6OptionValueTest.kt | 42 ++ ...figParseIpv6AddressLabelOptionValueTest.kt | 42 ++ ...seIpv6AddressLabelPrefixOptionValueTest.kt | 42 ++ ...ConfigParseMatchPropertyOptionValueTest.kt | 76 ++++ .../ai/ConfigParseMatchStrvOptionValueTest.kt | 78 ++++ ...figParseNdiscRouteMetricOptionValueTest.kt | 47 ++ ...ighborDestinationAddressOptionValueTest.kt | 41 ++ ...arseNeighborProxyAddressOptionValueTest.kt | 45 ++ ...ConfigParseNftSetNetworkOptionValueTest.kt | 52 +++ ...arsePref64PrefixLifetimeOptionValueTest.kt | 41 ++ .../ConfigParsePrefixTokenOptionValueTest.kt | 42 ++ ...igParseRouteMetricAdvMssOptionValueTest.kt | 41 ++ ...ParseRouteMetricInitCwndOptionValueTest.kt | 41 ++ ...onfigParseRouteMetricMtuOptionValueTest.kt | 46 ++ ...igParseRouteMetricTcpRtoOptionValueTest.kt | 42 ++ ...onfigParseRouteMultiPathOptionValueTest.kt | 52 +++ ...ParseRoutePrefixLifetimeOptionValueTest.kt | 42 ++ .../ConfigParseRoutePrefixOptionValueTest.kt | 42 ++ .../ConfigParseRouteTableOptionValueTest.kt | 50 ++ ...eRouterHomeAgentLifetimeOptionValueTest.kt | 42 ++ ...onfigParseRouterLifetimeOptionValueTest.kt | 42 ++ ...seRouterPrefixDelegationOptionValueTest.kt | 40 ++ ...arseRouterUint32MsecUsecOptionValueTest.kt | 50 ++ ...ngPolicyRuleFirewallMarkOptionValueTest.kt | 45 ++ ...tingPolicyRuleIpProtocolOptionValueTest.kt | 46 ++ ...outingPolicyRuleUidRangeOptionValueTest.kt | 45 ++ ...ConfigParseStackedNetdevOptionValueTest.kt | 50 ++ ...nfigParseStringSafeAsciiOptionValueTest.kt | 42 ++ .../ai/ConfigParseStrvOptionValueTest.kt | 68 +++ .../ai/ConfigParseTimezoneOptionValueTest.kt | 44 ++ 66 files changed, 3256 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 36704512..caf2c628 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() 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..7a4c7185 --- /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.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 == 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..aaaffef3 --- /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-L530 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..50864098 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt @@ -0,0 +1,24 @@ +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.SafeAtouTerminal +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. + */ +class ConfigParseIpv6AddressLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_address_label_section", + SequenceCombinator(SafeAtouTerminal(0, 4_294_967_295L), 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..9e022430 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue + +/* + * [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 + val length = if (slash >= 0) safeAtouValue(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/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..b88bf974 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue + +/* + * [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) + */ + +private val METRIC = SafeAtouTerminal(0, UINT32_LIMIT) + +// high < medium < low, compared on the parsed numbers. +private val METRIC_TRIPLE = Checked(SequenceCombinator(METRIC, COLON, METRIC, COLON, METRIC)) { text -> + val v = text.split(':').map { safeAtouValue(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..5db1efb7 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt @@ -0,0 +1,83 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * 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 + * 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}""" +) + +/** + * One `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( + FlexibleLiteralChoiceTerminal("address", "prefix", "ifindex"), + COLON, + FlexibleLiteralChoiceTerminal("arp", "bridge", "inet", "ip", "ip6", "netdev"), + COLON, + NFT_IDENTIFIER, + COLON, + NFT_IDENTIFIER, +) + +private val DQ = LiteralChoiceTerminal("\"") +private val SQ = LiteralChoiceTerminal("'") + +// The list is split with EXTRACT_UNQUOTE first, so a whole tuple may be quoted. +private val NFT_WORD = AlternativeCombinator( + SequenceCombinator(DQ, NFT_TUPLE, DQ), + SequenceCombinator(SQ, NFT_TUPLE, SQ), + 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) of + * `source:family:table:set` tuples, each split on `:` into exactly four fields (extract_many_words with + * EXTRACT_CUNESCAPE; more or fewer fields 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: a quote that wraps only part of a tuple (`address:"inet":t:s`, which + * EXTRACT_UNQUOTE would silently remove) is flagged; a fully quoted tuple is accepted. C escapes inside + * a name are accepted without being decoded. + */ +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..34aacc97 --- /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.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 != 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..3dfd2f84 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.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.SafeAtouTerminal +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 then 1…1023. + * + * (`InitialAdvertisedReceiveWindow=` uses the same leaf and is already covered by + * ConfigParseRouteSectionInitRwndOptionValue.) + */ +class ConfigParseRouteMetricInitCwndOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator(SafeAtouTerminal(1, 1024), 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..80381799 --- /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.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 != 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..872886c6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt @@ -0,0 +1,59 @@ +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.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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. +private val DEVICE = AlternativeCombinator(ALTERNATIVE_INTERFACE_NAME, SafeAtouTerminal(1, 2_147_483_648L)) + +/** + * 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. + * + * 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(), SafeAtouTerminal(1, 257))), + 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..eefbcc7b --- /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.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 == 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..63bfc4d1 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT + +/* + * [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) + */ + +// A route table *name*: `default`, `main`, `local`, or one defined with RouteTable=NAME:NUMBER in +// networkd.conf. config_parse_route_table_names splits RouteTable= on whitespace and on the first `:`, +// and refuses an all-digit name — so a name has no whitespace or `:` and is not purely digits. +private val ROUTE_TABLE_NAME = RegexTerminal("""\S+""", """(?![0-9]+(?:\s|$))[^\s:]+""") + +/** + * Validator for `[Route] Table=` and `[RoutingPolicyRule] Table=` (.network). Both table slots end in + * manager_get_route_table_from_string(), so they share this 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 different file, so any well-formed name is accepted (a name cannot contain whitespace or `:` and + * cannot be all digits). An all-digit value must be a table number in 1…4294967295; `0`, an + * out-of-range number, or a value containing whitespace or `:` is flagged. + * + * Lenient corner: a non-decimal number such as `0x0` is indistinguishable from a possible user-defined + * name and is accepted. + */ +class ConfigParseRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + // The name branch must come first: its lenient shape matches digits too but only *validates* + // non-numeric names, so under the classic matcher a number falls through to the second branch. + AlternativeCombinator(ROUTE_TABLE_NAME, SafeAtouTerminal(1, UINT32_LIMIT)), + EOF() + ) +) 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..04701b06 --- /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.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 != 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..1f920931 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.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.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_ANY_CASE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [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-L48 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 this grammar accepts the union — a boolean, or + * `static` / `dhcpv6`. That is exact for `IPv6PrefixDelegation=` and lenient for `IPv6SendRA=`, where + * `static` and `dhcpv6` are in fact rejected. Booleans are matched case-insensitively, as parse_boolean() does. The names come before the boolean so the classic matcher + * tries them first. + */ +class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( + "config_parse_router_prefix_delegation", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("static", "dhcpv6"), + BOOLEAN_ANY_CASE, + ), + 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..0726ab65 --- /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.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 == 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..722dab49 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.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.CIDR_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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. + */ +class ConfigParseRoutingPolicyRuleFirewallMarkOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + SafeAtouTerminal(0, UINT32_LIMIT), + ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, SafeAtouTerminal(0, UINT32_LIMIT))), + 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..4c04cfe1 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal +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 IPPROTO_* names from + * linux https://man7.org/linux/man-pages/man5/protocols.5.html + */ + +/** + * The IPPROTO_* names the generated gperf table knows, lower-cased, as defined by glibc's + * (generate-ip-protocol-list.sh lists every `#define IPPROTO_*`). `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", "raw", +) + +/** + * 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). config_parse_ip_protocol then refuses + * anything above 255, because the netlink attribute is one byte. + * + * Deliberately lenient: with relaxed = false systemd also refuses a number that has no IPPROTO_* name + * (e.g. `3`), but the name list comes from the C library systemd was built against, so every number in + * 0…255 is accepted here rather than guess at it. + */ +class ConfigParseRoutingPolicyRuleIpProtocolOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal(*IP_PROTOCOL_NAMES, ignoreCase = true), + SafeAtouTerminal(0, 256), + ), + 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..802a7a2e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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.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 + */ + +private const val UID_LIMIT = 4_294_967_296L + +private fun uidIsValid(text: String): Boolean { + val v = text.toLongOrNull() ?: return false + return v < UID_LIMIT && v != 65_535L && v != 4_294_967_295L +} + +// parse_uid: strict decimal, no `+`/`-`, no leading zero (except `0` itself), and uid_is_valid(). +private val UID = Checked(RegexTerminal("""[0-9]+""", """0|[1-9][0-9]*""")) { uidIsValid(it) } + +private val UID_RANGE = Checked(SequenceCombinator(UID, ZeroOrOne(SequenceCombinator(HYPHEN, UID)))) { text -> + val bounds = text.split('-') + bounds.size == 1 || (bounds[0].toLong() <= bounds[1].toLong()) +} + +// 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). +private val USER_NAME = RegexTerminal("""\S+""", """(?![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..0ea8e3e8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.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.INTERFACE_NAME +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) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/21-vlan-test1.network + */ + +/** + * 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, 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(INTERFACE_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/grammar/BatchNetworkRouteAddrCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt new file mode 100644 index 00000000..da80b73c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt @@ -0,0 +1,428 @@ +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 -> + if (step is Parse && step.tokens.isNotEmpty() && step.end > offset && !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) +} + + +// --------------------------------------------------------------------------------------------------- +// safe_atou* (src/basic/parse-util.c, safe_atou_full / mangle_base). + +/** + * The value [text] denotes when read by systemd's `safe_atou*()` helpers with base 0, or null if they + * would reject it. + * + * Those helpers skip leading whitespace, tolerate a single `+` (and `-0`), understand the Python-style + * `0b`/`0o` prefixes via mangle_base(), and otherwise hand the string to `strtoul(…, 0)`, which reads + * `0x…` as hex and a leading `0` as octal. The whole string must be consumed. + */ +fun safeAtouValue(text: String): BigInteger? { + var s = text.trimStart(' ', '\t', '\n', '\r') + var negative = false + if (s.startsWith("+")) { + s = s.substring(1) + } else if (s.startsWith("-")) { + negative = true + s = s.substring(1) + } + val (digits, radix) = when { + s.length > 2 && (s.startsWith("0b") || s.startsWith("0B")) -> s.substring(2) to 2 + s.length > 2 && (s.startsWith("0o") || s.startsWith("0O")) -> s.substring(2) to 8 + s.length > 2 && (s.startsWith("0x") || s.startsWith("0X")) -> s.substring(2) to 16 + s.length > 1 && s.startsWith("0") -> s.substring(1) to 8 + else -> s to 10 + } + if (digits.isEmpty() || !digits.all { Character.digit(it, radix) >= 0 }) return null + val v = BigInteger(digits, radix) + // strtoul() happily negates; safe_atou_full() only lets "-0" through. + if (negative && v.signum() != 0) return null + return v +} + +/** + * An unsigned number in `[minInclusive, maxExclusive)` as `safe_atou*()` reads it: decimal, `0x` hex, + * `0o`/leading-zero octal, `0b` binary, with an optional leading `+`. + * + * Wider than [UnsignedNumberTerminal], which predates the `0b`/`0o`/`+` spellings. The syntactic shape + * is any run starting with a digit (or sign) and continuing with alphanumerics, so a malformed number + * like `12abc` is located as one token; use it only where the number is followed by a separator + * (whitespace, `/`, `-`, `:`, `@`) or the end of the value. + */ +class SafeAtouTerminal(private val minInclusive: Long, private val maxExclusive: Long) : TerminalCombinator { + + private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value + + private fun inRange(text: String): Boolean { + val v = safeAtouValue(text) ?: return false + return v >= BigInteger.valueOf(minInclusive) && v < BigInteger.valueOf(maxExclusive) + } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + if (!inRange(text)) return NoMatch.copy(longestMatch = offset) + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun parse(value: String, offset: Int): Sequence { + val text = span(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, inRange(text))))) + } + + override fun toString(): String = "SafeAtou($minInclusive,$maxExclusive)" + + private companion object { + val SHAPE = Regex("""[+-]?[0-9][0-9a-zA-Z]*""") + } +} + +const val UINT32_LIMIT = 4_294_967_296L + + +// --------------------------------------------------------------------------------------------------- +// 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, + ) +} + +/** USEC_INFINITY, i.e. UINT64_MAX. */ +val 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) + +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 PARSE_SEC = AlternativeCombinator( + TIME_INFINITY, + OneOrMore(SequenceCombinator(TIME_ELEMENT, ZeroOrOne(WhitespaceTerminal()))), +) + +private val TIME_ELEMENT_REGEX = Regex("""\s*\+?([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 — [USEC_INFINITY] for `infinity` — or null if + * parse_sec() would fail (including its -ERANGE overflow checks). [text] is expected to be something + * [PARSE_SEC] matched. + */ +fun parseSecUsec(text: String): BigInteger? { + val t = text.trim() + if (t == "infinity") return 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 >= 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 >= 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 [USEC_INFINITY] for + * `infinity`). Not followed by EOF. + */ +fun timeSpan(accept: (BigInteger) -> Boolean = { true }): Combinator = + Checked(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 + + +// --------------------------------------------------------------------------------------------------- +// parse_boolean() (src/basic/parse-util.c) compares with STRCASE_IN_SET, so `Yes`, `TRUE` and `Off` are +// all booleans. The shared [BOOLEAN] terminal is case-sensitive; this one is not. +val BOOLEAN_ANY_CASE = FlexibleLiteralChoiceTerminal(*BOOLEAN_SPELLINGS, ignoreCase = true) + + +// --------------------------------------------------------------------------------------------------- +// 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. 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, SafeAtouTerminal(0, 129)))) 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..7eea27f8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt @@ -0,0 +1,38 @@ +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() { + // language="unit file (systemd)" + val file = """ + [Address] + NetLabel=system_u:object_r:root_t:s0 + NetLabel=unconfined_u:object_r:* + """.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] + 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..931b67ef --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.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 + +/** [IPv6AddressLabel] Label=. */ +class ConfigParseIpv6AddressLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Label=0 + Label=100 + Label=4294967294 + Label=0xfffffffe + """.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 = """ + [IPv6AddressLabel] + Label=4294967295 + Label=0xffffffff + Label=-1 + Label=label + """.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/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt new file mode 100644 index 00000000..b9d2201d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.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 + +/** [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 + """.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 = """ + [IPv6AddressLabel] + Prefix=::ffff:0:0/97 + Prefix=::ffff:1.2.3.4 + Prefix=10.0.0.0/8 + Prefix=2001:db8::/200 + """.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/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..a48b8c72 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.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_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 + [DHCPv6] + RouteMetric=1024 + """.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 = """ + [IPv6AcceptRA] + RouteMetric=hoge + RouteMetric=300:200:100 + RouteMetric=100:100:200 + RouteMetric=1:2 + RouteMetric=1:2:3:4 + RouteMetric=1::3 + """.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/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..2f6cb8ad --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** 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 + """.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 + """.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/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..13c5ff24 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.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] InitialCongestionWindow=. */ +class ConfigParseRouteMetricInitCwndOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + InitialCongestionWindow=20 + InitialCongestionWindow=1 + InitialCongestionWindow=1023 + InitialCongestionWindow=0x10 + """.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] + InitialCongestionWindow=0 + InitialCongestionWindow=1024 + InitialCongestionWindow=ten + """.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/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..bf79ca95 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [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 + """.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] + MultiPathRoute=@eth0 0 + MultiPathRoute=@eth0 257 + MultiPathRoute=@ + MultiPathRoute=@0 + MultiPathRoute=10.0.0.1/24@eth0 + MultiPathRoute=@eth0 10 20 + MultiPathRoute=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/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..b05164af --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.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 + +/** [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 + """.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::/ + """.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/ConfigParseRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt new file mode 100644 index 00000000..5b1758a7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.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 + +/** [Route] Table= and [RoutingPolicyRule] Table=. */ +class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + Table=main + Table=local + Table=default + Table=1011 + Table=4294967295 + Table=custom-table + [RoutingPolicyRule] + Table=7 + Table=my_table + Table=0x10 + """.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=main table + """.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..13a7bf0a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.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_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 + """.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 + """.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/ConfigParseRouterPrefixDelegationOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt new file mode 100644 index 00000000..4cf284b9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt @@ -0,0 +1,40 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** 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 + """.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 + """.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/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..e8c6a19f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.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 + +/** [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 + """.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] + FirewallMark=4294967296 + FirewallMark=7/ + FirewallMark=/255 + FirewallMark=mark + FirewallMark=-1 + """.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/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt new file mode 100644 index 00000000..58aa7862 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.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 + +/** [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 + """.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] + IPProtocol=tcpp + IPProtocol=256 + IPProtocol=mptcp + IPProtocol=-6 + """.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/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt new file mode 100644 index 00000000..a5c82e23 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.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 + +/** [RoutingPolicyRule] User=. */ +class ConfigParseRoutingPolicyRuleUidRangeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + User=100-200 + User=1000 + User=0 + User=root + User=systemd-network + User=1000-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- + """.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/ConfigParseStackedNetdevOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt new file mode 100644 index 00000000..2fd9da6a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.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_stacked_netdev (all ten kinds). */ +class ConfigParseStackedNetdevOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // 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= + """.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 + """.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/ConfigParseStringSafeAsciiOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt new file mode 100644 index 00000000..8f75eca3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.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_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]? + """.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..44a50047 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.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 + +/** config_parse_timezone (syntax of verify_timezone). */ +class ConfigParseTimezoneOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // 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 + """.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()) + } +} From 2203a338935e038df93d445a16bcf86b4115f989 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 23 Oct 2026 18:56:33 -0700 Subject: [PATCH 2/6] fix: address review findings for network-route-addr validators - RoutingPolicyRule User=: no longer crash the new engine on an over-long UID (toLongOrNull); Checked now only runs its predicate on spans whose tokens are all individually valid. - RoutingPolicyRule IPProtocol=: accept aggfrag (the generated table comes from systemd's vendored uapi headers), and refuse numbers that have no IPPROTO_* name, as parse_ip_protocol_full(relaxed=false) does. - NFTSet=: model extract_many_words(":") separator coalescing (leading, repeated and trailing colons) and quotes anywhere in a word. - safe_atou* skip leading whitespace: accept it after ':' / '/' in RouteMetric=, IPv6 route/label prefixes and FirewallMark= masks; accept a single trailing ':' on the RouteMetric= triple. - parse_sec: refuse '+.5'. - IPv6SendRA=: boolean only (key-specific check); IPv6PrefixDelegation= keeps static/dhcpv6. - Stacked netdevs: refuse names parse_ifindex() reads as an ifindex (0x10). - RoutingPolicyRule Table= reports its own parser name; evidence anchors corrected. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 2 +- ...ConfigParseInAddrNonNullIpv6OptionValue.kt | 2 +- .../ConfigParseNdiscRouteMetricOptionValue.kt | 10 ++- .../ai/ConfigParseNftSetNetworkOptionValue.kt | 86 +++++++++++++++---- .../ai/ConfigParseRouteTableOptionValue.kt | 4 +- ...gParseRouterPrefixDelegationOptionValue.kt | 32 +++++-- ...outingPolicyRuleFirewallMarkOptionValue.kt | 2 +- ...eRoutingPolicyRuleIpProtocolOptionValue.kt | 35 +++++--- ...rseRoutingPolicyRuleUidRangeOptionValue.kt | 5 +- .../ai/ConfigParseStackedNetdevOptionValue.kt | 20 ++++- .../BatchNetworkRouteAddrCombinators.kt | 29 +++++-- ...seIpv6AddressLabelPrefixOptionValueTest.kt | 1 + ...figParseNdiscRouteMetricOptionValueTest.kt | 6 +- ...ConfigParseNftSetNetworkOptionValueTest.kt | 11 ++- .../ConfigParseRoutePrefixOptionValueTest.kt | 1 + ...onfigParseRouterLifetimeOptionValueTest.kt | 5 +- ...seRouterPrefixDelegationOptionValueTest.kt | 7 +- ...ngPolicyRuleFirewallMarkOptionValueTest.kt | 1 + ...tingPolicyRuleIpProtocolOptionValueTest.kt | 9 +- ...outingPolicyRuleUidRangeOptionValueTest.kt | 4 +- ...ConfigParseStackedNetdevOptionValueTest.kt | 8 +- 21 files changed, 218 insertions(+), 62 deletions(-) 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 caf2c628..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 @@ -454,7 +454,7 @@ fun getAllAIGeneratedValidators(): Map { 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() as OptionValueInformation, + 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, 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 index aaaffef3..0dd53b0c 100644 --- 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 @@ -13,7 +13,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * * 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-L530 in_addr_from_string + * 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 */ 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 index b88bf974..c9e072a3 100644 --- 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 @@ -8,6 +8,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue /* @@ -22,8 +23,13 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram private val METRIC = SafeAtouTerminal(0, UINT32_LIMIT) // high < medium < low, compared on the parsed numbers. -private val METRIC_TRIPLE = Checked(SequenceCombinator(METRIC, COLON, METRIC, COLON, METRIC)) { text -> - val v = text.split(':').map { safeAtouValue(it) } +// safe_atou32 skips leading whitespace, so `100: 200: 300` is fine. +private val NEXT_METRIC = SafeAtouTerminal(0, UINT32_LIMIT, allowLeadingWhitespace = 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. +private val METRIC_TRIPLE = Checked(SequenceCombinator(METRIC, COLON, NEXT_METRIC, COLON, NEXT_METRIC, ZeroOrOne(COLON))) { text -> + val v = text.removeSuffix(":").split(':').map { safeAtouValue(it) } v.size == 3 && v.all { it != null } && v[0]!! < v[1]!! && v[1]!! < v[2]!! } 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 index 5db1efb7..7fbb1d93 100644 --- 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 @@ -3,9 +3,10 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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.LiteralChoiceTerminal +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 @@ -19,6 +20,8 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * 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) */ @@ -31,27 +34,71 @@ private val NFT_IDENTIFIER = RegexTerminal( """[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 `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). + * 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( - FlexibleLiteralChoiceTerminal("address", "prefix", "ifindex"), - COLON, - FlexibleLiteralChoiceTerminal("arp", "bridge", "inet", "ip", "ip6", "netdev"), - COLON, + ZeroOrMore(COLON), + FlexibleLiteralChoiceTerminal(*NFT_SOURCES), + COLONS, + FlexibleLiteralChoiceTerminal(*NFT_FAMILIES), + COLONS, NFT_IDENTIFIER, - COLON, + COLONS, NFT_IDENTIFIER, + ZeroOrMore(COLON), ) -private val DQ = LiteralChoiceTerminal("\"") -private val SQ = LiteralChoiceTerminal("'") +/** + * 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 list is split with EXTRACT_UNQUOTE first, so a whole tuple may be quoted. +// The quoted form first: it requires a quote, so it never shadows a plain tuple. private val NFT_WORD = AlternativeCombinator( - SequenceCombinator(DQ, NFT_TUPLE, DQ), - SequenceCombinator(SQ, NFT_TUPLE, SQ), + QUOTED_NFT_WORD, NFT_TUPLE, ) @@ -60,18 +107,19 @@ private val NFT_WORD = AlternativeCombinator( * `[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) of - * `source:family:table:set` tuples, each split on `:` into exactly four fields (extract_many_words with - * EXTRACT_CUNESCAPE; more or fewer fields is an error): + * 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: a quote that wraps only part of a tuple (`address:"inet":t:s`, which - * EXTRACT_UNQUOTE would silently remove) is flagged; a fully quoted tuple is accepted. C escapes inside - * a name are accepted without being decoded. + * 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", 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 index 63bfc4d1..06fad808 100644 --- 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 @@ -39,8 +39,8 @@ private val ROUTE_TABLE_NAME = RegexTerminal("""\S+""", """(?![0-9]+(?:\s|$))[^\ * Lenient corner: a non-decimal number such as `0x0` is indistinguishable from a possible user-defined * name and is accepted. */ -class ConfigParseRouteTableOptionValue : SimpleGrammarOptionValues( - "config_parse_route_section", +class ConfigParseRouteTableOptionValue(parserName: String = "config_parse_route_section") : SimpleGrammarOptionValues( + parserName, SequenceCombinator( // The name branch must come first: its lenient shape matches digits too but only *validates* // non-numeric names, so under the classic matcher a number falls through to the second branch. 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 index 1f920931..f50747c4 100644 --- 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 @@ -1,5 +1,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai +import com.intellij.codeInspection.ProblemsHolder +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_ANY_CASE @@ -13,7 +15,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * 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-L48 parse_boolean (case-insensitive) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (case-insensitive) */ /** @@ -24,10 +26,11 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * 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 this grammar accepts the union — a boolean, or - * `static` / `dhcpv6`. That is exact for `IPv6PrefixDelegation=` and lenient for `IPv6SendRA=`, where - * `static` and `dhcpv6` are in fact rejected. Booleans are matched case-insensitively, as parse_boolean() does. The names come before the boolean so the classic matcher - * tries them first. + * 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. Booleans are matched case-insensitively, as + * parse_boolean() does. The names come before the boolean so the classic matcher tries them first. */ class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( "config_parse_router_prefix_delegation", @@ -38,4 +41,21 @@ class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( ), 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_ANY_CASE, 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 index 722dab49..ad73467a 100644 --- 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 @@ -30,7 +30,7 @@ class ConfigParseRoutingPolicyRuleFirewallMarkOptionValue : SimpleGrammarOptionV "config_parse_routing_policy_rule", SequenceCombinator( SafeAtouTerminal(0, UINT32_LIMIT), - ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, SafeAtouTerminal(0, UINT32_LIMIT))), + ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, SafeAtouTerminal(0, UINT32_LIMIT, allowLeadingWhitespace = 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 index 4c04cfe1..591c730c 100644 --- 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 @@ -2,10 +2,12 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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.SafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue /* * [RoutingPolicyRule] IPProtocol= (.network). @@ -14,19 +16,31 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * 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 IPPROTO_* names from + * 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, as defined by glibc's - * (generate-ip-protocol-list.sh lists every `#define IPPROTO_*`). `smc` (256) and - * `mptcp` (262) are left out because config_parse_ip_protocol refuses anything above 255. + * 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", "raw", + "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, ) /** @@ -35,19 +49,16 @@ private val IP_PROTOCOL_NAMES = arrayOf( * * 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). config_parse_ip_protocol then refuses - * anything above 255, because the netlink attribute is one byte. - * - * Deliberately lenient: with relaxed = false systemd also refuses a number that has no IPPROTO_* name - * (e.g. `3`), but the name list comes from the C library systemd was built against, so every number in - * 0…255 is accepted here rather than guess at it. + * 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( AlternativeCombinator( FlexibleLiteralChoiceTerminal(*IP_PROTOCOL_NAMES, ignoreCase = true), - SafeAtouTerminal(0, 256), + Checked(SafeAtouTerminal(0, 256)) { text -> safeAtouValue(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 index 802a7a2e..8706595a 100644 --- 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 @@ -34,7 +34,10 @@ private val UID = Checked(RegexTerminal("""[0-9]+""", """0|[1-9][0-9]*""")) { ui private val UID_RANGE = Checked(SequenceCombinator(UID, ZeroOrOne(SequenceCombinator(HYPHEN, UID)))) { text -> val bounds = text.split('-') - bounds.size == 1 || (bounds[0].toLong() <= bounds[1].toLong()) + // toLongOrNull: an over-long digit run is already rejected by UID; do not crash on it here. + val lo = bounds[0].toLongOrNull() + val hi = bounds.getOrNull(1)?.toLongOrNull() + bounds.size == 1 || lo == null || hi == null || lo <= hi } // A user name for getpwnam(): NSS is the judge, so anything without whitespace or `:` is accepted, as 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 index 0ea8e3e8..74608020 100644 --- 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 @@ -1,8 +1,11 @@ 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.plainNameList +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue +import java.math.BigInteger /* * [Network] IPoIB= / IPVLAN= / IPVTAP= / MACsec= / MACVLAN= / MACVTAP= / Tunnel= / VLAN= / VXLAN= / @@ -12,9 +15,19 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * 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) - * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/21-vlan-test1.network + * 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. +private val INT_MAX = BigInteger.valueOf(Int.MAX_VALUE.toLong()) +private val NOT_AN_IFINDEX_NAME = Checked(INTERFACE_NAME) { name -> + val v = safeAtouValue(name) + v == null || v.signum() <= 0 || v > INT_MAX +} + /** * 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 @@ -23,11 +36,12 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * * 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, and not `.`, `..`, `all` or `default`. An invalid name is logged and skipped; + * 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(INTERFACE_NAME), + plainNameList(NOT_AN_IFINDEX_NAME), ) 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 index da80b73c..1199d57b 100644 --- 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 @@ -42,7 +42,10 @@ class Checked(private val inner: Combinator, private val predicate: (String) -> override fun parse(value: String, offset: Int): Sequence = inner.parse(value, offset).map { step -> - if (step is Parse && step.tokens.isNotEmpty() && step.end > offset && !predicate(value.substring(offset, step.end))) { + // 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 @@ -97,10 +100,19 @@ fun safeAtouValue(text: String): BigInteger? { * is any run starting with a digit (or sign) and continuing with alphanumerics, so a malformed number * like `12abc` is located as one token; use it only where the number is followed by a separator * (whitespace, `/`, `-`, `:`, `@`) or the end of the value. + * + * safe_atou*() skip leading whitespace themselves. Pass [allowLeadingWhitespace] where the number comes + * straight after a non-whitespace separator that systemd splits on itself (`100: 200` in a `:` list, + * `::/ 64` after a prefix slash), so that whitespace is part of the number token. Leave it off where the + * value was already split into words on whitespace, or where whitespace is the separator. */ -class SafeAtouTerminal(private val minInclusive: Long, private val maxExclusive: Long) : TerminalCombinator { +class SafeAtouTerminal( + private val minInclusive: Long, + private val maxExclusive: Long, + private val allowLeadingWhitespace: Boolean = false, +) : TerminalCombinator { - private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value + private fun span(value: String, offset: Int): String? = (if (allowLeadingWhitespace) SHAPE_WS else SHAPE).matchAt(value, offset)?.value private fun inRange(text: String): Boolean { val v = safeAtouValue(text) ?: return false @@ -127,6 +139,7 @@ class SafeAtouTerminal(private val minInclusive: Long, private val maxExclusive: private companion object { val SHAPE = Regex("""[+-]?[0-9][0-9a-zA-Z]*""") + val SHAPE_WS = Regex("""[ \t]*[+-]?[0-9][0-9a-zA-Z]*""") } } @@ -174,7 +187,8 @@ val USEC_INFINITY: BigInteger = BigInteger.ONE.shiftLeft(64).subtract(BigInteger val USEC_PER_SEC: BigInteger = BigInteger.valueOf(1_000_000L) val USEC_PER_MSEC: BigInteger = BigInteger.valueOf(1_000L) -private val TIME_NUMBER = RegexTerminal("""[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]*)""", """\+?(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)""") +// `+.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") @@ -189,7 +203,7 @@ val PARSE_SEC = AlternativeCombinator( OneOrMore(SequenceCombinator(TIME_ELEMENT, ZeroOrOne(WhitespaceTerminal()))), ) -private val TIME_ELEMENT_REGEX = Regex("""\s*\+?([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)?""") +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 — [USEC_INFINITY] for `infinity` — or null if @@ -424,5 +438,6 @@ val BOOLEAN_ANY_CASE = FlexibleLiteralChoiceTerminal(*BOOLEAN_SPELLINGS, ignoreC // --------------------------------------------------------------------------------------------------- // 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. 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, SafeAtouTerminal(0, 129)))) +// be ≤ 128 (safe_atou8 skips whitespace, so `::/ 64` is 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, SafeAtouTerminal(0, 129, allowLeadingWhitespace = true)))) 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 index b9d2201d..ce814e57 100644 --- 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 @@ -16,6 +16,7 @@ class ConfigParseIpv6AddressLabelPrefixOptionValueTest : AbstractUnitFileTest() Prefix=::/0 Prefix=::ffff:0:0/96 Prefix=fe80::1 + Prefix=2001:db8::/ 32 """.trimIndent() setupFileInEditor("file.network", file) 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 index a48b8c72..c7da110f 100644 --- 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 @@ -17,6 +17,8 @@ class ConfigParseNdiscRouteMetricOptionValueTest : AbstractUnitFileTest() { RouteMetric=100:200:300 RouteMetric=0 RouteMetric=0x10:0x20:0x30 + RouteMetric=100: 200: 300 + RouteMetric=100:200:300: [DHCPv6] RouteMetric=1024 """.trimIndent() @@ -38,10 +40,12 @@ class ConfigParseNdiscRouteMetricOptionValueTest : AbstractUnitFileTest() { RouteMetric=1:2 RouteMetric=1:2:3:4 RouteMetric=1::3 + RouteMetric=1:2:3:: + RouteMetric=100 :200:300 """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(6, myFixture.doHighlighting()) + assertSize(8, 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 index 2f6cb8ad..ef45133c 100644 --- 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 @@ -23,6 +23,12 @@ class ConfigParseNftSetNetworkOptionValueTest : AbstractUnitFileTest() { 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) @@ -43,10 +49,13 @@ class ConfigParseNftSetNetworkOptionValueTest : AbstractUnitFileTest() { 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(6, myFixture.doHighlighting()) + assertSize(9, 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 index b05164af..bc421a7d 100644 --- 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 @@ -17,6 +17,7 @@ class ConfigParseRoutePrefixOptionValueTest : AbstractUnitFileTest() { Route=::/0 Route=2001:db8::1 Route=2001:db8::/0x40 + Route=2001:db8::/ 64 """.trimIndent() setupFileInEditor("file.network", file) 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 index 13a7bf0a..afcadc96 100644 --- 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 @@ -18,6 +18,8 @@ class ConfigParseRouterLifetimeOptionValueTest : AbstractUnitFileTest() { RouterLifetimeSec=1s RouterLifetimeSec=10000 RouterLifetimeSec=infinity + RouterLifetimeSec=.5 + RouterLifetimeSec=+5 """.trimIndent() setupFileInEditor("file.network", file) @@ -33,10 +35,11 @@ class ConfigParseRouterLifetimeOptionValueTest : AbstractUnitFileTest() { [IPv6SendRA] RouterLifetimeSec=forever RouterLifetimeSec=30 minuts + RouterLifetimeSec=+.5 """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(2, myFixture.doHighlighting()) + 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 index 4cf284b9..beecc332 100644 --- 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 @@ -16,6 +16,9 @@ class ConfigParseRouterPrefixDelegationOptionValueTest : AbstractUnitFileTest() IPv6SendRA=no IPv6SendRA=True IPv6SendRA=0 + IPv6PrefixDelegation=static + IPv6PrefixDelegation=dhcpv6 + IPv6PrefixDelegation=yes """.trimIndent() setupFileInEditor("file.network", file) @@ -31,10 +34,12 @@ class ConfigParseRouterPrefixDelegationOptionValueTest : AbstractUnitFileTest() [Network] IPv6SendRA=maybe IPv6SendRA=both + IPv6SendRA=static + IPv6SendRA=dhcpv6 """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(2, myFixture.doHighlighting()) + assertSize(4, 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 index e8c6a19f..05aeca94 100644 --- 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 @@ -18,6 +18,7 @@ class ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest : AbstractUnitFile FirewallMark=0 FirewallMark=4294967295/4294967295 FirewallMark=0b101 + FirewallMark=7/ 255 """.trimIndent() setupFileInEditor("file.network", file) 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 index 58aa7862..91e7bb9b 100644 --- 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 @@ -20,6 +20,11 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTe IPProtocol=17 IPProtocol=0x11 IPProtocol=255 + IPProtocol=aggfrag + IPProtocol=AGGFRAG + IPProtocol=144 + IPProtocol=0 + IPProtocol=58 """.trimIndent() setupFileInEditor("file.network", file) @@ -37,10 +42,12 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTe IPProtocol=256 IPProtocol=mptcp IPProtocol=-6 + IPProtocol=3 + IPProtocol=200 """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(4, myFixture.doHighlighting()) + assertSize(6, 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 index a5c82e23..5cdcd4ed 100644 --- 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 @@ -36,10 +36,12 @@ class ConfigParseRoutingPolicyRuleUidRangeOptionValueTest : AbstractUnitFileTest User=4294967295 User=01000 User=100- + User=1000-99999999999999999999 + User=99999999999999999999-1000 """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(5, myFixture.doHighlighting()) + 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 index 2fd9da6a..8e47ea05 100644 --- 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 @@ -24,6 +24,9 @@ class ConfigParseStackedNetdevOptionValueTest : AbstractUnitFileTest() { VXLAN=vxlan-slaac Xfrm=xfrm98 VLAN= + VLAN=0x0 + VLAN=-5 + VLAN=0xvlan """.trimIndent() setupFileInEditor("file.network", file) @@ -41,10 +44,13 @@ class ConfigParseStackedNetdevOptionValueTest : AbstractUnitFileTest() { 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(4, myFixture.doHighlighting()) + assertSize(7, myFixture.doHighlighting()) } } From 79e1c5ed4c13e347e006819837e0c23079a73737 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 23 Oct 2026 19:07:10 -0700 Subject: [PATCH 3/6] refactor: give network-route-addr batch grammar helpers family-unique names Several batch branches declared top-level helpers with the same name in the shared grammar package (SafeAtouTerminal, UINT32_LIMIT, BOOLEAN_ANY_CASE, PARSE_SEC, USEC_INFINITY). Each branch compiled alone but any two merged together would not. Prefix them per family so the batch PRs can merge in any order; consolidating them into Combinators.kt is left for a follow-up. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ConfigParseCanRestartUsecOptionValue.kt | 4 +-- .../ConfigParseIpv6AddressLabelOptionValue.kt | 4 +-- ...gedOomMemPressureDurationSecOptionValue.kt | 2 +- .../ConfigParseNdiscRouteMetricOptionValue.kt | 8 +++--- ...figParsePref64PrefixLifetimeOptionValue.kt | 4 +-- ...nfigParseRouteMetricInitCwndOptionValue.kt | 4 +-- ...ConfigParseRouteMetricTcpRtoOptionValue.kt | 4 +-- .../ConfigParseRouteMultiPathOptionValue.kt | 8 +++--- ...nfigParseRoutePrefixLifetimeOptionValue.kt | 4 +-- .../ai/ConfigParseRouteTableOptionValue.kt | 6 ++-- ...ParseRouterHomeAgentLifetimeOptionValue.kt | 4 +-- ...gParseRouterPrefixDelegationOptionValue.kt | 6 ++-- ...figParseRouterUint32MsecUsecOptionValue.kt | 4 +-- ...outingPolicyRuleFirewallMarkOptionValue.kt | 8 +++--- ...eRoutingPolicyRuleIpProtocolOptionValue.kt | 4 +-- .../BatchNetworkRouteAddrCombinators.kt | 28 +++++++++---------- 16 files changed, 51 insertions(+), 51 deletions(-) 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 index 7a4c7185..c0f64684 100644 --- 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 @@ -3,7 +3,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_INFINITY +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 @@ -30,5 +30,5 @@ private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) */ class ConfigParseCanRestartUsecOptionValue : SimpleGrammarOptionValues( "config_parse_can_restart_usec", - SequenceCombinator(timeSpan { it == USEC_INFINITY || divRoundUp(it, USEC_PER_MSEC) <= UINT32_MAX }, EOF()) + 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/ConfigParseIpv6AddressLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt index 50864098..8e2bb2d2 100644 --- 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 @@ -2,7 +2,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator /* @@ -20,5 +20,5 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram */ class ConfigParseIpv6AddressLabelOptionValue : SimpleGrammarOptionValues( "config_parse_ipv6_address_label_section", - SequenceCombinator(SafeAtouTerminal(0, 4_294_967_295L), EOF()) + SequenceCombinator(NetworkRouteAddrSafeAtouTerminal(0, 4_294_967_295L), 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/ConfigParseNdiscRouteMetricOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt index c9e072a3..226701c4 100644 --- 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 @@ -5,9 +5,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram 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.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue @@ -20,11 +20,11 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/systemd-networkd-tests.py#L8124 (RouteMetric=100:200:300) */ -private val METRIC = SafeAtouTerminal(0, UINT32_LIMIT) +private val METRIC = NetworkRouteAddrSafeAtouTerminal(0, NETWORK_ROUTE_ADDR_UINT32_LIMIT) // high < medium < low, compared on the parsed numbers. // safe_atou32 skips leading whitespace, so `100: 200: 300` is fine. -private val NEXT_METRIC = SafeAtouTerminal(0, UINT32_LIMIT, allowLeadingWhitespace = true) +private val NEXT_METRIC = NetworkRouteAddrSafeAtouTerminal(0, NETWORK_ROUTE_ADDR_UINT32_LIMIT, allowLeadingWhitespace = 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. 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 index 34aacc97..867cc632 100644 --- 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 @@ -3,7 +3,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_INFINITY +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 @@ -31,5 +31,5 @@ private val SCALED_LIFETIME_LIMIT: BigInteger = BigInteger.valueOf(1L shl 13) */ class ConfigParsePref64PrefixLifetimeOptionValue : SimpleGrammarOptionValues( "config_parse_pref64_prefix_lifetime", - SequenceCombinator(timeSpan { it != USEC_INFINITY && divRoundUp(it, EIGHT_SECONDS) < SCALED_LIFETIME_LIMIT }, EOF()) + 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/ConfigParseRouteMetricInitCwndOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt index 3dfd2f84..057a7ed1 100644 --- 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 @@ -2,7 +2,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator /* @@ -26,5 +26,5 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram */ class ConfigParseRouteMetricInitCwndOptionValue : SimpleGrammarOptionValues( "config_parse_route_section", - SequenceCombinator(SafeAtouTerminal(1, 1024), EOF()) + SequenceCombinator(NetworkRouteAddrSafeAtouTerminal(1, 1024), 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 index 80381799..c843ec2c 100644 --- 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 @@ -3,7 +3,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_INFINITY +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 @@ -33,7 +33,7 @@ private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) class ConfigParseRouteMetricTcpRtoOptionValue : SimpleGrammarOptionValues( "config_parse_route_section", SequenceCombinator( - timeSpan { it.signum() > 0 && it != USEC_INFINITY && divRoundUp(it, USEC_PER_MSEC) <= UINT32_MAX }, + 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 index 872886c6..f5f348fe 100644 --- 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 @@ -6,9 +6,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne @@ -30,7 +30,7 @@ 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. -private val DEVICE = AlternativeCombinator(ALTERNATIVE_INTERFACE_NAME, SafeAtouTerminal(1, 2_147_483_648L)) +private val DEVICE = AlternativeCombinator(ALTERNATIVE_INTERFACE_NAME, NetworkRouteAddrSafeAtouTerminal(1, 2_147_483_648L)) /** * Validator for `[Route] MultiPathRoute=` (.network) — the ROUTE_MULTIPATH slot, whose table entry is @@ -53,7 +53,7 @@ class ConfigParseRouteMultiPathOptionValue : SimpleGrammarOptionValues( SequenceCombinator(IP_ADDR, ZeroOrOne(SequenceCombinator(AT, DEVICE))), SequenceCombinator(AT, DEVICE), ), - ZeroOrOne(SequenceCombinator(WhitespaceTerminal(), SafeAtouTerminal(1, 257))), + ZeroOrOne(SequenceCombinator(WhitespaceTerminal(), NetworkRouteAddrSafeAtouTerminal(1, 257))), 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 index eefbcc7b..505dce41 100644 --- 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 @@ -3,7 +3,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_INFINITY +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 @@ -30,5 +30,5 @@ private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) */ class ConfigParseRoutePrefixLifetimeOptionValue : SimpleGrammarOptionValues( "config_parse_route_prefix_lifetime", - SequenceCombinator(timeSpan { it == USEC_INFINITY || divRoundUp(it, USEC_PER_SEC) < UINT32_MAX }, EOF()) + 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/ConfigParseRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt index 06fad808..29449ca2 100644 --- 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 @@ -4,9 +4,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_UINT32_LIMIT /* * [Route] Table= and [RoutingPolicyRule] Table= (.network). @@ -44,7 +44,7 @@ class ConfigParseRouteTableOptionValue(parserName: String = "config_parse_route_ SequenceCombinator( // The name branch must come first: its lenient shape matches digits too but only *validates* // non-numeric names, so under the classic matcher a number falls through to the second branch. - AlternativeCombinator(ROUTE_TABLE_NAME, SafeAtouTerminal(1, UINT32_LIMIT)), + AlternativeCombinator(ROUTE_TABLE_NAME, NetworkRouteAddrSafeAtouTerminal(1, NETWORK_ROUTE_ADDR_UINT32_LIMIT)), EOF() ) ) 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 index 04701b06..7bc1c829 100644 --- 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 @@ -3,7 +3,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_INFINITY +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 @@ -30,5 +30,5 @@ private val RADV_HOME_AGENT_MAX_LIFETIME_USEC: BigInteger = BigInteger.valueOf(6 */ class ConfigParseRouterHomeAgentLifetimeOptionValue : SimpleGrammarOptionValues( "config_parse_router_home_agent_lifetime", - SequenceCombinator(timeSpan { it.signum() > 0 && it != USEC_INFINITY && it <= RADV_HOME_AGENT_MAX_LIFETIME_USEC }, EOF()) + 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/ConfigParseRouterPrefixDelegationOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt index f50747c4..8f0f4e0c 100644 --- 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 @@ -4,7 +4,7 @@ import com.intellij.codeInspection.ProblemsHolder import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_ANY_CASE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator @@ -37,7 +37,7 @@ class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( SequenceCombinator( AlternativeCombinator( FlexibleLiteralChoiceTerminal("static", "dhcpv6"), - BOOLEAN_ANY_CASE, + NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE, ), EOF() ) @@ -55,7 +55,7 @@ class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( // streq(lvalue, "IPv6SendRA") → parse_boolean() only. val SEND_RA_BOOLEAN_ONLY = SimpleGrammarOptionValues( "config_parse_router_prefix_delegation", - SequenceCombinator(BOOLEAN_ANY_CASE, EOF()) + SequenceCombinator(NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE, 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 index 0726ab65..3da8d2ea 100644 --- 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 @@ -3,7 +3,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_INFINITY +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 @@ -32,5 +32,5 @@ private val RADV_MAX_UINT32_MSEC_USEC: BigInteger = BigInteger.valueOf(4_294_967 */ class ConfigParseRouterUint32MsecUsecOptionValue : SimpleGrammarOptionValues( "config_parse_router_uint32_msec_usec", - SequenceCombinator(timeSpan { it == USEC_INFINITY || it <= RADV_MAX_UINT32_MSEC_USEC }, EOF()) + 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 index ad73467a..d9d438db 100644 --- 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 @@ -3,9 +3,9 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne /* @@ -29,8 +29,8 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram class ConfigParseRoutingPolicyRuleFirewallMarkOptionValue : SimpleGrammarOptionValues( "config_parse_routing_policy_rule", SequenceCombinator( - SafeAtouTerminal(0, UINT32_LIMIT), - ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, SafeAtouTerminal(0, UINT32_LIMIT, allowLeadingWhitespace = true))), + NetworkRouteAddrSafeAtouTerminal(0, NETWORK_ROUTE_ADDR_UINT32_LIMIT), + ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, NetworkRouteAddrSafeAtouTerminal(0, NETWORK_ROUTE_ADDR_UINT32_LIMIT, allowLeadingWhitespace = 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 index 591c730c..5f391260 100644 --- 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 @@ -5,7 +5,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram 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.SafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue @@ -58,7 +58,7 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValue : SimpleGrammarOptionVal SequenceCombinator( AlternativeCombinator( FlexibleLiteralChoiceTerminal(*IP_PROTOCOL_NAMES, ignoreCase = true), - Checked(SafeAtouTerminal(0, 256)) { text -> safeAtouValue(text)?.toInt() in IP_PROTOCOL_NUMBERS }, + Checked(NetworkRouteAddrSafeAtouTerminal(0, 256)) { text -> safeAtouValue(text)?.toInt() in IP_PROTOCOL_NUMBERS }, ), EOF() ) 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 index 1199d57b..ebc7966d 100644 --- 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 @@ -106,7 +106,7 @@ fun safeAtouValue(text: String): BigInteger? { * `::/ 64` after a prefix slash), so that whitespace is part of the number token. Leave it off where the * value was already split into words on whitespace, or where whitespace is the separator. */ -class SafeAtouTerminal( +class NetworkRouteAddrSafeAtouTerminal( private val minInclusive: Long, private val maxExclusive: Long, private val allowLeadingWhitespace: Boolean = false, @@ -143,7 +143,7 @@ class SafeAtouTerminal( } } -const val UINT32_LIMIT = 4_294_967_296L +const val NETWORK_ROUTE_ADDR_UINT32_LIMIT = 4_294_967_296L // --------------------------------------------------------------------------------------------------- @@ -182,8 +182,8 @@ private val TIME_UNIT_USEC: Map = run { ) } -/** USEC_INFINITY, i.e. UINT64_MAX. */ -val USEC_INFINITY: BigInteger = BigInteger.ONE.shiftLeft(64).subtract(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) @@ -198,7 +198,7 @@ private val TIME_ELEMENT = AlternativeCombinator( ) /** Every spelling parse_sec() accepts, structurally; not range-checked. Not followed by EOF. */ -val PARSE_SEC = AlternativeCombinator( +val NETWORK_ROUTE_ADDR_PARSE_SEC = AlternativeCombinator( TIME_INFINITY, OneOrMore(SequenceCombinator(TIME_ELEMENT, ZeroOrOne(WhitespaceTerminal()))), ) @@ -206,13 +206,13 @@ val PARSE_SEC = AlternativeCombinator( 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 — [USEC_INFINITY] for `infinity` — or null if + * 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 - * [PARSE_SEC] matched. + * [NETWORK_ROUTE_ADDR_PARSE_SEC] matched. */ fun parseSecUsec(text: String): BigInteger? { val t = text.trim() - if (t == "infinity") return USEC_INFINITY + if (t == "infinity") return NETWORK_ROUTE_ADDR_USEC_INFINITY var pos = 0 var usec = BigInteger.ZERO var something = false @@ -224,14 +224,14 @@ fun parseSecUsec(text: String): BigInteger? { 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 >= USEC_INFINITY.divide(multiplier)) return null + 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 >= USEC_INFINITY) return null + if (usec >= NETWORK_ROUTE_ADDR_USEC_INFINITY) return null something = true pos = m.range.last + 1 } @@ -239,11 +239,11 @@ fun parseSecUsec(text: String): BigInteger? { } /** - * A parse_sec() value whose microsecond count must satisfy [accept] (which receives [USEC_INFINITY] for + * 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(PARSE_SEC) { text -> parseSecUsec(text)?.let(accept) ?: false } + 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) @@ -432,7 +432,7 @@ fun ipIsMulticast(bytes: ByteArray): Boolean = // --------------------------------------------------------------------------------------------------- // parse_boolean() (src/basic/parse-util.c) compares with STRCASE_IN_SET, so `Yes`, `TRUE` and `Off` are // all booleans. The shared [BOOLEAN] terminal is case-sensitive; this one is not. -val BOOLEAN_ANY_CASE = FlexibleLiteralChoiceTerminal(*BOOLEAN_SPELLINGS, ignoreCase = true) +val NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE = FlexibleLiteralChoiceTerminal(*BOOLEAN_SPELLINGS, ignoreCase = true) // --------------------------------------------------------------------------------------------------- @@ -440,4 +440,4 @@ val BOOLEAN_ANY_CASE = FlexibleLiteralChoiceTerminal(*BOOLEAN_SPELLINGS, ignoreC // and an optional `/LEN`, where LEN goes through in_addr_parse_prefixlen → safe_atou8 (base 0) and must // be ≤ 128 (safe_atou8 skips whitespace, so `::/ 64` is 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, SafeAtouTerminal(0, 129, allowLeadingWhitespace = true)))) +val IPV6_PREFIX_SAFE_LEN: Combinator = SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, NetworkRouteAddrSafeAtouTerminal(0, 129, allowLeadingWhitespace = true)))) From e48accdfb7a73b326d6bc67810b33dee1b59cb5d Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 06:35:31 -0700 Subject: [PATCH 4/6] test: pin network-route-addr validators to systemd oracle results Checked every network-route-addr mismatch from the systemd oracle diff against the real parsers at 0f0682b773. None is a plugin bug: - IPv6SendRA=static/dhcpv6: the inspection already flags them per key; the diff harness only sees the shared union combinator. - BootFilename=/NetLabel= with quotes or backslashes: systemd logs at warning level but drops the value (and for NetLabel= the whole [Address] section), so the plugin keeps flagging them. - Route/RoutingPolicyRule Table= names, User= names and Timezone= existence depend on networkd.conf, NSS and tzdata at runtime, so they stay deliberately lenient. Adds the oracle-verified values to the tests and refreshes a stale comment that said the TABLE/FWMARK/IP_PROTOCOL ltypes were unvalidated. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../optionvalues/ai/RoutingPolicyRuleOptionValues.kt | 7 ++++--- .../ai/ConfigParseAddressNetLabelOptionValueTest.kt | 6 ++++++ .../inspections/ai/ConfigParseRouteTableOptionValueTest.kt | 3 +++ .../ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt | 3 +++ .../ai/ConfigParseStringSafeAsciiOptionValueTest.kt | 1 + .../inspections/ai/ConfigParseTimezoneOptionValueTest.kt | 3 +++ 6 files changed, 20 insertions(+), 3 deletions(-) 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/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt index 7eea27f8..b82bc4a3 100644 --- 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 @@ -9,11 +9,15 @@ 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) @@ -23,6 +27,8 @@ class ConfigParseAddressNetLabelOptionValueTest : AbstractUnitFileTest() { @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 = """ 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 index 5b1758a7..934cbe24 100644 --- 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 @@ -9,6 +9,8 @@ class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { @Test fun testValidValues() { + // custom-table, my_table and 0x0 are deliberate leniencies: they are legal networkd.conf RouteTable= names + // (only all-digit names are refused), which the plugin cannot see. systemd without such a definition rejects them. // language="unit file (systemd)" val file = """ [Route] @@ -18,6 +20,7 @@ class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { Table=1011 Table=4294967295 Table=custom-table + Table=0x0 [RoutingPolicyRule] Table=7 Table=my_table 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 index 5cdcd4ed..a9caaa4d 100644 --- 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 @@ -9,6 +9,8 @@ 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] @@ -18,6 +20,7 @@ class ConfigParseRoutingPolicyRuleUidRangeOptionValueTest : AbstractUnitFileTest User=root User=systemd-network User=1000-1000 + User=+1000 """.trimIndent() setupFileInEditor("file.network", file) 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 index 8f75eca3..7b22ecc5 100644 --- 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 @@ -16,6 +16,7 @@ class ConfigParseStringSafeAsciiOptionValueTest : AbstractUnitFileTest() { BootFilename=efi/grubx64.efi BootFilename=boot file with spaces*.efi BootFilename=p@ss:w0rd!#[x]? + BootFilename=foo*bar[1]? """.trimIndent() setupFileInEditor("file.network", file) 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 index 44a50047..8cf6bfa5 100644 --- 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 @@ -9,6 +9,8 @@ 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] @@ -17,6 +19,7 @@ class ConfigParseTimezoneOptionValueTest : AbstractUnitFileTest() { Timezone=America/Argentina/Buenos_Aires Timezone=Etc/GMT+5 Timezone=Etc/GMT-14 + Timezone=Nowhere/Fake """.trimIndent() setupFileInEditor("file.network", file) From 3cf3c426b1de9ceff05c4f804146fd3c4a184a0c Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 07:40:59 -0700 Subject: [PATCH 5/6] refactor: use the shared case-insensitive BOOLEAN in network-route-addr validators Drop NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE now that BOOLEAN matches parse_boolean()'s STRCASE_IN_SET. IPv6SendRA=/IPv6PrefixDelegation= use the explicit PreferValidAlternativeCombinator(names, BOOLEAN) form, with case-sensitive names as string_table_lookup_from_string uses streq. IPProtocol= keeps ignoreCase (gperf --ignore-case plus an ascii_strlower retry) and moves to PreferValidAlternativeCombinator so the lenient name regex cannot shadow a number on the classic engine. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...figParseRouterPrefixDelegationOptionValue.kt | 17 ++++++++++------- ...rseRoutingPolicyRuleIpProtocolOptionValue.kt | 4 ++-- .../grammar/BatchNetworkRouteAddrCombinators.kt | 6 ------ ...arseRouterPrefixDelegationOptionValueTest.kt | 12 +++++++++++- ...outingPolicyRuleIpProtocolOptionValueTest.kt | 7 ++++++- 5 files changed, 29 insertions(+), 17 deletions(-) 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 index 8f0f4e0c..b5b0fd58 100644 --- 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 @@ -3,10 +3,10 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import com.intellij.codeInspection.ProblemsHolder import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE +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 /* @@ -29,15 +29,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * 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. Booleans are matched case-insensitively, as - * parse_boolean() does. The names come before the boolean so the classic matcher tries them first. + * 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( - AlternativeCombinator( + PreferValidAlternativeCombinator( FlexibleLiteralChoiceTerminal("static", "dhcpv6"), - NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE, + BOOLEAN, ), EOF() ) @@ -55,7 +58,7 @@ class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( // streq(lvalue, "IPv6SendRA") → parse_boolean() only. val SEND_RA_BOOLEAN_ONLY = SimpleGrammarOptionValues( "config_parse_router_prefix_delegation", - SequenceCombinator(NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE, EOF()) + SequenceCombinator(BOOLEAN, 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 index 5f391260..d309880f 100644 --- 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 @@ -1,11 +1,11 @@ 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.FlexibleLiteralChoiceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue @@ -56,7 +56,7 @@ private val IP_PROTOCOL_NUMBERS = setOf( class ConfigParseRoutingPolicyRuleIpProtocolOptionValue : SimpleGrammarOptionValues( "config_parse_routing_policy_rule", SequenceCombinator( - AlternativeCombinator( + PreferValidAlternativeCombinator( FlexibleLiteralChoiceTerminal(*IP_PROTOCOL_NAMES, ignoreCase = true), Checked(NetworkRouteAddrSafeAtouTerminal(0, 256)) { text -> safeAtouValue(text)?.toInt() in IP_PROTOCOL_NUMBERS }, ), 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 index ebc7966d..e8fb40c4 100644 --- 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 @@ -429,12 +429,6 @@ fun ipIsMulticast(bytes: ByteArray): Boolean = if (bytes.size == 4) (bytes[0].toInt() and 0xF0) == 0xE0 else (bytes[0].toInt() and 0xFF) == 0xFF -// --------------------------------------------------------------------------------------------------- -// parse_boolean() (src/basic/parse-util.c) compares with STRCASE_IN_SET, so `Yes`, `TRUE` and `Off` are -// all booleans. The shared [BOOLEAN] terminal is case-sensitive; this one is not. -val NETWORK_ROUTE_ADDR_BOOLEAN_ANY_CASE = FlexibleLiteralChoiceTerminal(*BOOLEAN_SPELLINGS, ignoreCase = true) - - // --------------------------------------------------------------------------------------------------- // 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 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 index beecc332..80f2a7e9 100644 --- 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 @@ -16,9 +16,16 @@ class ConfigParseRouterPrefixDelegationOptionValueTest : AbstractUnitFileTest() 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) @@ -36,10 +43,13 @@ class ConfigParseRouterPrefixDelegationOptionValueTest : AbstractUnitFileTest() IPv6SendRA=both IPv6SendRA=static IPv6SendRA=dhcpv6 + IPv6PrefixDelegation=Static + IPv6PrefixDelegation=DHCPv6 + IPv6PrefixDelegation=maybe """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(4, myFixture.doHighlighting()) + assertSize(7, 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 index 91e7bb9b..8563e068 100644 --- 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 @@ -25,6 +25,9 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTe IPProtocol=144 IPProtocol=0 IPProtocol=58 + IPProtocol=TcP + IPProtocol=IcMpV6 + IPProtocol=L2TP """.trimIndent() setupFileInEditor("file.network", file) @@ -44,10 +47,12 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTe IPProtocol=-6 IPProtocol=3 IPProtocol=200 + IPProtocol=TcPx + IPProtocol=Yes """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(6, myFixture.doHighlighting()) + assertSize(8, myFixture.doHighlighting()) } } From 2f917d2288e6d9a89ac84774841423ca58a2340c Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 11:37:42 -0700 Subject: [PATCH 6/6] refactor: use the shared SafeAtoTerminal in network-route-addr validators (#563) Replace the batch-local NetworkRouteAddrSafeAtouTerminal, safeAtouValue() and NETWORK_ROUTE_ADDR_UINT32_LIMIT with SafeAtoTerminal, and switch [Route] Table= / [RoutingPolicyRule] Table= to ROUTE_TABLE_FROM_STRING (dropping the private ROUTE_TABLE_NAME). parse_uid in [RoutingPolicyRule] User= and parse_ifindex in MultiPathRoute= and the stacked-netdev names now use SafeAtoTerminal too, with safe_atoi's signedness. Verdict changes, each confirmed with sd-check at 0f0682b773: - now flagged (EINVAL, glibc C23 0b parsing is off): +0b1, -0b0, +0o7 in InitialCongestionWindow=, Label=, FirewallMark=, RouteMetric=, the MultiPathRoute= weight, prefix lengths, and IPProtocol=+0b110 - now accepted: 0b 1, 0b+1, 0o 7 and a leading \v wherever the whole value or a field after ':' or '/' reaches safe_atou*; Table=0b 1; Table=main table (possible networkd.conf name); VLAN=+0b1 (not an ifindex, so a valid name); User=\v5 and User=5-\v6 Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ConfigParseIpv6AddressLabelOptionValue.kt | 18 ++- ...gParseIpv6AddressLabelPrefixOptionValue.kt | 17 ++- .../ConfigParseNdiscRouteMetricOptionValue.kt | 26 +++-- ...nfigParseRouteMetricInitCwndOptionValue.kt | 17 ++- .../ConfigParseRouteMultiPathOptionValue.kt | 36 +++++- .../ai/ConfigParseRouteTableOptionValue.kt | 37 ++---- ...outingPolicyRuleFirewallMarkOptionValue.kt | 31 ++++- ...eRoutingPolicyRuleIpProtocolOptionValue.kt | 18 ++- ...rseRoutingPolicyRuleUidRangeOptionValue.kt | 63 +++++++--- .../ai/ConfigParseStackedNetdevOptionValue.kt | 17 ++- .../BatchNetworkRouteAddrCombinators.kt | 110 +++--------------- ...figParseIpv6AddressLabelOptionValueTest.kt | 8 +- ...seIpv6AddressLabelPrefixOptionValueTest.kt | 8 +- ...figParseNdiscRouteMetricOptionValueTest.kt | 8 +- ...ParseRouteMetricInitCwndOptionValueTest.kt | 11 +- ...onfigParseRouteMultiPathOptionValueTest.kt | 9 +- .../ConfigParseRoutePrefixOptionValueTest.kt | 4 +- .../ConfigParseRouteTableOptionValueTest.kt | 9 +- ...ngPolicyRuleFirewallMarkOptionValueTest.kt | 9 +- ...tingPolicyRuleIpProtocolOptionValueTest.kt | 8 +- ...ConfigParseStackedNetdevOptionValueTest.kt | 4 + .../ai/NetworkRouteAddrSafeAtoVerdictsTest.kt | 63 ++++++++++ 22 files changed, 343 insertions(+), 188 deletions(-) create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt 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 index 8e2bb2d2..258aac7d 100644 --- 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 @@ -2,7 +2,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator /* @@ -16,9 +16,21 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram /** * 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. + * 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(NetworkRouteAddrSafeAtouTerminal(0, 4_294_967_295L), EOF()) + 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 index 9e022430..f0470d82 100644 --- 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 @@ -4,9 +4,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp 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 -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue /* * [IPv6AddressLabel] Prefix= (.network). @@ -24,7 +24,20 @@ 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 - val length = if (slash >= 0) safeAtouValue(text.substring(slash + 1))?.toInt() ?: return false else 128 + // 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 } 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 index 226701c4..5c06ad71 100644 --- 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 @@ -5,11 +5,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram 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.NetworkRouteAddrSafeAtouTerminal +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.NETWORK_ROUTE_ADDR_UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue /* * [IPv6AcceptRA] RouteMetric= and the legacy [DHCPv6] RouteMetric= (.network). @@ -20,16 +18,24 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/systemd-networkd-tests.py#L8124 (RouteMetric=100:200:300) */ -private val METRIC = NetworkRouteAddrSafeAtouTerminal(0, NETWORK_ROUTE_ADDR_UINT32_LIMIT) - -// high < medium < low, compared on the parsed numbers. -// safe_atou32 skips leading whitespace, so `100: 200: 300` is fine. -private val NEXT_METRIC = NetworkRouteAddrSafeAtouTerminal(0, NETWORK_ROUTE_ADDR_UINT32_LIMIT, allowLeadingWhitespace = true) +// 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. -private val METRIC_TRIPLE = Checked(SequenceCombinator(METRIC, COLON, NEXT_METRIC, COLON, NEXT_METRIC, ZeroOrOne(COLON))) { text -> - val v = text.removeSuffix(":").split(':').map { safeAtouValue(it) } +// 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]!! } 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 index 057a7ed1..c3fb87b9 100644 --- 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 @@ -2,7 +2,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator /* @@ -19,12 +19,23 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram /** * 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 then 1…1023. + * `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(NetworkRouteAddrSafeAtouTerminal(1, 1024), EOF()) + 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/ConfigParseRouteMultiPathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt index f5f348fe..c9473999 100644 --- 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 @@ -6,9 +6,8 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal +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.NETWORK_ROUTE_ADDR_UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne @@ -30,7 +29,20 @@ 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. -private val DEVICE = AlternativeCombinator(ALTERNATIVE_INTERFACE_NAME, NetworkRouteAddrSafeAtouTerminal(1, 2_147_483_648L)) +// 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 @@ -41,7 +53,8 @@ private val DEVICE = AlternativeCombinator(ALTERNATIVE_INTERFACE_NAME, NetworkRo * - 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. + * 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`. @@ -53,7 +66,20 @@ class ConfigParseRouteMultiPathOptionValue : SimpleGrammarOptionValues( SequenceCombinator(IP_ADDR, ZeroOrOne(SequenceCombinator(AT, DEVICE))), SequenceCombinator(AT, DEVICE), ), - ZeroOrOne(SequenceCombinator(WhitespaceTerminal(), NetworkRouteAddrSafeAtouTerminal(1, 257))), + 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/ConfigParseRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt index 29449ca2..c99d5808 100644 --- 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 @@ -1,12 +1,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator -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.NetworkRouteAddrSafeAtouTerminal -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_UINT32_LIMIT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING /* * [Route] Table= and [RoutingPolicyRule] Table= (.network). @@ -21,30 +16,18 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) */ -// A route table *name*: `default`, `main`, `local`, or one defined with RouteTable=NAME:NUMBER in -// networkd.conf. config_parse_route_table_names splits RouteTable= on whitespace and on the first `:`, -// and refuses an all-digit name — so a name has no whitespace or `:` and is not purely digits. -private val ROUTE_TABLE_NAME = RegexTerminal("""\S+""", """(?![0-9]+(?:\s|$))[^\s:]+""") - /** - * Validator for `[Route] Table=` and `[RoutingPolicyRule] Table=` (.network). Both table slots end in - * manager_get_route_table_from_string(), so they share this 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 different file, so any well-formed name is accepted (a name cannot contain whitespace or `:` and - * cannot be all digits). An all-digit value must be a table number in 1…4294967295; `0`, an - * out-of-range number, or a value containing whitespace or `:` is flagged. + * 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. * - * Lenient corner: a non-decimal number such as `0x0` is indistinguishable from a possible user-defined - * name and is accepted. + * 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, - SequenceCombinator( - // The name branch must come first: its lenient shape matches digits too but only *validates* - // non-numeric names, so under the classic matcher a number falls through to the second branch. - AlternativeCombinator(ROUTE_TABLE_NAME, NetworkRouteAddrSafeAtouTerminal(1, NETWORK_ROUTE_ADDR_UINT32_LIMIT)), - EOF() - ) + ROUTE_TABLE_FROM_STRING, ) 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 index d9d438db..8c3fe01b 100644 --- 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 @@ -3,9 +3,8 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkRouteAddrSafeAtouTerminal +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.NETWORK_ROUTE_ADDR_UINT32_LIMIT import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne /* @@ -24,13 +23,35 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * * `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. + * 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( - NetworkRouteAddrSafeAtouTerminal(0, NETWORK_ROUTE_ADDR_UINT32_LIMIT), - ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, NetworkRouteAddrSafeAtouTerminal(0, NETWORK_ROUTE_ADDR_UINT32_LIMIT, allowLeadingWhitespace = true))), + 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 index d309880f..21f3a51c 100644 --- 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 @@ -4,10 +4,9 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.Simp 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.NetworkRouteAddrSafeAtouTerminal 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 -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue /* * [RoutingPolicyRule] IPProtocol= (.network). @@ -43,6 +42,19 @@ private val IP_PROTOCOL_NUMBERS = setOf( 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. @@ -58,7 +70,7 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValue : SimpleGrammarOptionVal SequenceCombinator( PreferValidAlternativeCombinator( FlexibleLiteralChoiceTerminal(*IP_PROTOCOL_NAMES, ignoreCase = true), - Checked(NetworkRouteAddrSafeAtouTerminal(0, 256)) { text -> safeAtouValue(text)?.toInt() in IP_PROTOCOL_NUMBERS }, + 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 index 8706595a..147a8b1c 100644 --- 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 @@ -6,6 +6,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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 @@ -22,27 +23,57 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-fibrule-uidrange.network */ -private const val UID_LIMIT = 4_294_967_296L - -private fun uidIsValid(text: String): Boolean { - val v = text.toLongOrNull() ?: return false - return v < UID_LIMIT && v != 65_535L && v != 4_294_967_295L -} +// 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, +) -// parse_uid: strict decimal, no `+`/`-`, no leading zero (except `0` itself), and uid_is_valid(). -private val UID = Checked(RegexTerminal("""[0-9]+""", """0|[1-9][0-9]*""")) { uidIsValid(it) } +// 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, +) -private val UID_RANGE = Checked(SequenceCombinator(UID, ZeroOrOne(SequenceCombinator(HYPHEN, UID)))) { text -> - val bounds = text.split('-') - // toLongOrNull: an over-long digit run is already rejected by UID; do not crash on it here. - val lo = bounds[0].toLongOrNull() - val hi = bounds.getOrNull(1)?.toLongOrNull() - bounds.size == 1 || lo == null || hi == null || lo <= hi +// 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). -private val USER_NAME = RegexTerminal("""\S+""", """(?![0-9][0-9-]*(?:\s|$))[^\s:]+""") +// 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. 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 index 74608020..49b7cac8 100644 --- 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 @@ -3,9 +3,8 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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 -import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.safeAtouValue -import java.math.BigInteger /* * [Network] IPoIB= / IPVLAN= / IPVTAP= / MACsec= / MACVLAN= / MACVTAP= / Tunnel= / VLAN= / VXLAN= / @@ -22,10 +21,18 @@ import java.math.BigInteger // 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. -private val INT_MAX = BigInteger.valueOf(Int.MAX_VALUE.toLong()) +// 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 -> - val v = safeAtouValue(name) - v == null || v.signum() <= 0 || v > INT_MAX + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(name) == null } /** 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 index e8fb40c4..48154b00 100644 --- 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 @@ -58,94 +58,6 @@ class Checked(private val inner: Combinator, private val predicate: (String) -> } -// --------------------------------------------------------------------------------------------------- -// safe_atou* (src/basic/parse-util.c, safe_atou_full / mangle_base). - -/** - * The value [text] denotes when read by systemd's `safe_atou*()` helpers with base 0, or null if they - * would reject it. - * - * Those helpers skip leading whitespace, tolerate a single `+` (and `-0`), understand the Python-style - * `0b`/`0o` prefixes via mangle_base(), and otherwise hand the string to `strtoul(…, 0)`, which reads - * `0x…` as hex and a leading `0` as octal. The whole string must be consumed. - */ -fun safeAtouValue(text: String): BigInteger? { - var s = text.trimStart(' ', '\t', '\n', '\r') - var negative = false - if (s.startsWith("+")) { - s = s.substring(1) - } else if (s.startsWith("-")) { - negative = true - s = s.substring(1) - } - val (digits, radix) = when { - s.length > 2 && (s.startsWith("0b") || s.startsWith("0B")) -> s.substring(2) to 2 - s.length > 2 && (s.startsWith("0o") || s.startsWith("0O")) -> s.substring(2) to 8 - s.length > 2 && (s.startsWith("0x") || s.startsWith("0X")) -> s.substring(2) to 16 - s.length > 1 && s.startsWith("0") -> s.substring(1) to 8 - else -> s to 10 - } - if (digits.isEmpty() || !digits.all { Character.digit(it, radix) >= 0 }) return null - val v = BigInteger(digits, radix) - // strtoul() happily negates; safe_atou_full() only lets "-0" through. - if (negative && v.signum() != 0) return null - return v -} - -/** - * An unsigned number in `[minInclusive, maxExclusive)` as `safe_atou*()` reads it: decimal, `0x` hex, - * `0o`/leading-zero octal, `0b` binary, with an optional leading `+`. - * - * Wider than [UnsignedNumberTerminal], which predates the `0b`/`0o`/`+` spellings. The syntactic shape - * is any run starting with a digit (or sign) and continuing with alphanumerics, so a malformed number - * like `12abc` is located as one token; use it only where the number is followed by a separator - * (whitespace, `/`, `-`, `:`, `@`) or the end of the value. - * - * safe_atou*() skip leading whitespace themselves. Pass [allowLeadingWhitespace] where the number comes - * straight after a non-whitespace separator that systemd splits on itself (`100: 200` in a `:` list, - * `::/ 64` after a prefix slash), so that whitespace is part of the number token. Leave it off where the - * value was already split into words on whitespace, or where whitespace is the separator. - */ -class NetworkRouteAddrSafeAtouTerminal( - private val minInclusive: Long, - private val maxExclusive: Long, - private val allowLeadingWhitespace: Boolean = false, -) : TerminalCombinator { - - private fun span(value: String, offset: Int): String? = (if (allowLeadingWhitespace) SHAPE_WS else SHAPE).matchAt(value, offset)?.value - - private fun inRange(text: String): Boolean { - val v = safeAtouValue(text) ?: return false - return v >= BigInteger.valueOf(minInclusive) && v < BigInteger.valueOf(maxExclusive) - } - - override fun SyntacticMatch(value: String, offset: Int): MatchResult { - val text = span(value, offset) ?: return NoMatch - return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) - } - - override fun SemanticMatch(value: String, offset: Int): MatchResult { - val text = span(value, offset) ?: return NoMatch - if (!inRange(text)) return NoMatch.copy(longestMatch = offset) - return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) - } - - override fun parse(value: String, offset: Int): Sequence { - val text = span(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) - return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, inRange(text))))) - } - - override fun toString(): String = "SafeAtou($minInclusive,$maxExclusive)" - - private companion object { - val SHAPE = Regex("""[+-]?[0-9][0-9a-zA-Z]*""") - val SHAPE_WS = Regex("""[ \t]*[+-]?[0-9][0-9a-zA-Z]*""") - } -} - -const val NETWORK_ROUTE_ADDR_UINT32_LIMIT = 4_294_967_296L - - // --------------------------------------------------------------------------------------------------- // parse_sec() — parse_time(t, &usec, USEC_PER_SEC) in src/basic/time-util.c. // @@ -432,6 +344,22 @@ fun ipIsMulticast(bytes: ByteArray): Boolean = // --------------------------------------------------------------------------------------------------- // 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 (safe_atou8 skips whitespace, so `::/ 64` is 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, NetworkRouteAddrSafeAtouTerminal(0, 129, allowLeadingWhitespace = true)))) +// 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/ConfigParseIpv6AddressLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt index 931b67ef..dec188ef 100644 --- 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 @@ -16,6 +16,8 @@ class ConfigParseIpv6AddressLabelOptionValueTest : AbstractUnitFileTest() { Label=100 Label=4294967294 Label=0xfffffffe + Label=0b 1 + Label=0b+1 """.trimIndent() setupFileInEditor("file.network", file) @@ -25,7 +27,7 @@ class ConfigParseIpv6AddressLabelOptionValueTest : AbstractUnitFileTest() { @Test fun testInvalidValues() { - // One highlight per line. + // 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] @@ -33,10 +35,12 @@ class ConfigParseIpv6AddressLabelOptionValueTest : AbstractUnitFileTest() { Label=0xffffffff Label=-1 Label=label + Label=+0b1 + Label=-0b0 """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(4, myFixture.doHighlighting()) + 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 index ce814e57..52452cc4 100644 --- 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 @@ -17,6 +17,8 @@ class ConfigParseIpv6AddressLabelPrefixOptionValueTest : AbstractUnitFileTest() 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) @@ -26,7 +28,7 @@ class ConfigParseIpv6AddressLabelPrefixOptionValueTest : AbstractUnitFileTest() @Test fun testInvalidValues() { - // One highlight per line. + // 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] @@ -34,10 +36,12 @@ class ConfigParseIpv6AddressLabelPrefixOptionValueTest : AbstractUnitFileTest() 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(4, myFixture.doHighlighting()) + assertSize(6, 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 index c7da110f..67dcf434 100644 --- 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 @@ -19,6 +19,8 @@ class ConfigParseNdiscRouteMetricOptionValueTest : AbstractUnitFileTest() { RouteMetric=0x10:0x20:0x30 RouteMetric=100: 200: 300 RouteMetric=100:200:300: + RouteMetric=0b 1 + RouteMetric=0b1:0b 10:0b11 [DHCPv6] RouteMetric=1024 """.trimIndent() @@ -30,7 +32,7 @@ class ConfigParseNdiscRouteMetricOptionValueTest : AbstractUnitFileTest() { @Test fun testInvalidValues() { - // One highlight per line. + // 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] @@ -42,10 +44,12 @@ class ConfigParseNdiscRouteMetricOptionValueTest : AbstractUnitFileTest() { 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(8, myFixture.doHighlighting()) + assertSize(10, 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 index 13c5ff24..50a7ff2b 100644 --- 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 @@ -9,6 +9,7 @@ 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] @@ -16,6 +17,9 @@ class ConfigParseRouteMetricInitCwndOptionValueTest : AbstractUnitFileTest() { InitialCongestionWindow=1 InitialCongestionWindow=1023 InitialCongestionWindow=0x10 + InitialCongestionWindow=0b 1 + InitialCongestionWindow=0b+1 + InitialCongestionWindow=0o 7 """.trimIndent() setupFileInEditor("file.network", file) @@ -25,17 +29,20 @@ class ConfigParseRouteMetricInitCwndOptionValueTest : AbstractUnitFileTest() { @Test fun testInvalidValues() { - // One highlight per line. + // 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(3, myFixture.doHighlighting()) + assertSize(6, 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 index bf79ca95..38143c82 100644 --- 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 @@ -23,6 +23,8 @@ class ConfigParseRouteMultiPathOptionValueTest : AbstractUnitFileTest() { MultiPathRoute=fe80::1 MultiPathRoute=@eth0 256 MultiPathRoute=@eth0 1 + MultiPathRoute=@eth0 0b 1 + MultiPathRoute=@+0b1 """.trimIndent() setupFileInEditor("file.network", file) @@ -32,7 +34,8 @@ class ConfigParseRouteMultiPathOptionValueTest : AbstractUnitFileTest() { @Test fun testInvalidValues() { - // One highlight per line. + // 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] @@ -43,10 +46,12 @@ class ConfigParseRouteMultiPathOptionValueTest : AbstractUnitFileTest() { 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(7, myFixture.doHighlighting()) + assertSize(9, 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 index bc421a7d..4f269c92 100644 --- 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 @@ -18,6 +18,7 @@ class ConfigParseRoutePrefixOptionValueTest : AbstractUnitFileTest() { Route=2001:db8::1 Route=2001:db8::/0x40 Route=2001:db8::/ 64 + Route=2001:db8::/0b 1 """.trimIndent() setupFileInEditor("file.network", file) @@ -34,10 +35,11 @@ class ConfigParseRoutePrefixOptionValueTest : AbstractUnitFileTest() { 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(3, myFixture.doHighlighting()) + 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 index 934cbe24..cce75651 100644 --- 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 @@ -9,8 +9,9 @@ class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { @Test fun testValidValues() { - // custom-table, my_table and 0x0 are deliberate leniencies: they are legal networkd.conf RouteTable= names - // (only all-digit names are refused), which the plugin cannot see. systemd without such a definition rejects them. + // 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] @@ -25,6 +26,8 @@ class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { Table=7 Table=my_table Table=0x10 + Table=0b 1 + Table=main table """.trimIndent() setupFileInEditor("file.network", file) @@ -43,7 +46,7 @@ class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { Table=a:b [RoutingPolicyRule] Table=00 - Table=main table + Table=08 """.trimIndent() setupFileInEditor("file.network", file) 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 index 05aeca94..dc724aa2 100644 --- 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 @@ -19,6 +19,8 @@ class ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest : AbstractUnitFile FirewallMark=4294967295/4294967295 FirewallMark=0b101 FirewallMark=7/ 255 + FirewallMark=0b 1 + FirewallMark=1/0b 1 """.trimIndent() setupFileInEditor("file.network", file) @@ -28,7 +30,7 @@ class ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest : AbstractUnitFile @Test fun testInvalidValues() { - // One highlight per line. + // 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] @@ -37,10 +39,13 @@ class ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest : AbstractUnitFile FirewallMark=/255 FirewallMark=mark FirewallMark=-1 + FirewallMark=+0b1 + FirewallMark=1/+0b1 + FirewallMark=1 /2 """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(5, myFixture.doHighlighting()) + 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 index 8563e068..689f742d 100644 --- 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 @@ -28,6 +28,8 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTe IPProtocol=TcP IPProtocol=IcMpV6 IPProtocol=L2TP + IPProtocol=0b 110 + IPProtocol=-0 """.trimIndent() setupFileInEditor("file.network", file) @@ -37,7 +39,7 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTe @Test fun testInvalidValues() { - // One highlight per line. + // 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] @@ -49,10 +51,12 @@ class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTe IPProtocol=200 IPProtocol=TcPx IPProtocol=Yes + IPProtocol=+0b110 + IPProtocol=0b-1 """.trimIndent() setupFileInEditor("file.network", file) enableInspection(InvalidValueInspection::class.java) - assertSize(8, myFixture.doHighlighting()) + assertSize(10, 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 index 8e47ea05..32d3a47e 100644 --- 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 @@ -9,6 +9,8 @@ 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] @@ -27,6 +29,8 @@ class ConfigParseStackedNetdevOptionValueTest : AbstractUnitFileTest() { VLAN=0x0 VLAN=-5 VLAN=0xvlan + VLAN=+0b1 + VLAN=0b-1 """.trimIndent() setupFileInEditor("file.network", file) 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") + } +}