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