diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 36704512..07099f0e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt @@ -412,6 +412,249 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_unit_path_strv_printf", "0") to ConfigParseUnitPathStrvPrintfOptionValue() as OptionValueInformation, Validator("config_parse_socket_service", "0") to ConfigParseSocketServiceOptionValue() as OptionValueInformation, Validator("config_parse_service_sockets", "0") to ConfigParseServiceSocketsOptionValue() as OptionValueInformation, + // Batch 2026-10: network-route-addr + // Generic word-list / string / match parsers. + Validator("config_parse_match_strv", "0") to ConfigParseMatchStrvOptionValue() as OptionValueInformation, + Validator("config_parse_strv", "0") to ConfigParseStrvOptionValue() as OptionValueInformation, + Validator("config_parse_match_property", "0") to ConfigParseMatchPropertyOptionValue() as OptionValueInformation, + Validator("config_parse_string", "CONFIG_PARSE_STRING_SAFE_AND_ASCII") to ConfigParseStringSafeAsciiOptionValue() as OptionValueInformation, + Validator("config_parse_nft_set", "NFT_SET_PARSE_NETWORK") to ConfigParseNftSetNetworkOptionValue() as OptionValueInformation, + Validator("config_parse_timezone", "0") to ConfigParseTimezoneOptionValue() as OptionValueInformation, + Validator("config_parse_in_addr_non_null", "AF_INET6") to ConfigParseInAddrNonNullIpv6OptionValue() as OptionValueInformation, + Validator("config_parse_neighbor_proxy_address", "0") to ConfigParseNeighborProxyAddressOptionValue() as OptionValueInformation, + // [Network] stacked netdevs: the ltype only records the expected kind; every slot takes netdev names. + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPOIB") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPVLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPVTAP") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACSEC") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACVLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACVTAP") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_VLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_VXLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_XFRM") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "_NETDEV_KIND_TUNNEL") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + // [IPv6SendRA] / [IPv6Prefix] / [IPv6RoutePrefix] / [IPv6PREF64Prefix] / [IPv6AcceptRA]. + Validator("config_parse_router_prefix_delegation", "0") to ConfigParseRouterPrefixDelegationOptionValue() as OptionValueInformation, + Validator("config_parse_router_uint32_msec_usec", "0") to ConfigParseRouterUint32MsecUsecOptionValue() as OptionValueInformation, + Validator("config_parse_router_lifetime", "0") to ConfigParseRouterLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_router_home_agent_lifetime", "0") to ConfigParseRouterHomeAgentLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_route_prefix", "0") to ConfigParseRoutePrefixOptionValue() as OptionValueInformation, + Validator("config_parse_route_prefix_lifetime", "0") to ConfigParseRoutePrefixLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_pref64_prefix_lifetime", "0") to ConfigParsePref64PrefixLifetimeOptionValue() as OptionValueInformation, + // config_parse_prefix_token hands its value straight to config_parse_address_generation_type. + Validator("config_parse_prefix_token", "0") to ConfigParseAddressGenerationTypeOptionValue() as OptionValueInformation, + Validator("config_parse_ndisc_route_metric", "0") to ConfigParseNdiscRouteMetricOptionValue() as OptionValueInformation, + Validator("config_parse_ndisc_route_metric", "AF_INET6") to ConfigParseNdiscRouteMetricOptionValue() as OptionValueInformation, + Validator("config_parse_can_restart_usec", "0") to ConfigParseCanRestartUsecOptionValue() as OptionValueInformation, + // [Route] table slots (each ltype mirrors its own ConfigSectionParser entry). + Validator("config_parse_route_section", "ROUTE_TABLE") to ConfigParseRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_MULTIPATH") to ConfigParseRouteMultiPathOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_MTU") to ConfigParseRouteMetricMtuOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_ADVMSS") to ConfigParseRouteMetricAdvMssOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_INITCWND") to ConfigParseRouteMetricInitCwndOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_RTO_MIN") to ConfigParseRouteMetricTcpRtoOptionValue() as OptionValueInformation, + // [RoutingPolicyRule] table slots. + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_TABLE") to ConfigParseRouteTableOptionValue("config_parse_routing_policy_rule") as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_FWMARK") to ConfigParseRoutingPolicyRuleFirewallMarkOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_IP_PROTOCOL") to ConfigParseRoutingPolicyRuleIpProtocolOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_UID_RANGE") to ConfigParseRoutingPolicyRuleUidRangeOptionValue() as OptionValueInformation, + // [Address] / [Neighbor] / [IPv6AddressLabel] table slots. + Validator("config_parse_address_section", "ADDRESS_LABEL") to ConfigParseAddressLabelOptionValue() as OptionValueInformation, + // ADDRESS_NET_LABEL's table entry is config_parse_string with CONFIG_PARSE_STRING_SAFE. + Validator("config_parse_address_section", "ADDRESS_NET_LABEL") to ConfigParseStringOptionValue() as OptionValueInformation, + // ADDRESS_NFT_SET's table entry is config_parse_nft_set with NFT_SET_PARSE_NETWORK. + Validator("config_parse_address_section", "ADDRESS_NFT_SET") to ConfigParseNftSetNetworkOptionValue() as OptionValueInformation, + Validator("config_parse_neighbor_section", "NEIGHBOR_DESTINATION_ADDRESS") to ConfigParseNeighborDestinationAddressOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_address_label_section", "IPV6_ADDRESS_LABEL_PREFIX") to ConfigParseIpv6AddressLabelPrefixOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_address_label_section", "IPV6_ADDRESS_LABEL") to ConfigParseIpv6AddressLabelOptionValue() as OptionValueInformation, + + // Batch 2026-10: exec + Validator("config_parse_bind_paths", "0") to ConfigParseBindPathsOptionValue() as OptionValueInformation, + Validator("config_parse_temporary_filesystems", "0") to ConfigParseTemporaryFilesystemsOptionValue() as OptionValueInformation, + Validator("config_parse_root_image_options", "0") to ConfigParseRootImageOptionsOptionValue() as OptionValueInformation, + Validator("config_parse_numa_mask", "0") to ConfigParseNumaMaskOptionValue() as OptionValueInformation, + Validator("config_parse_mount_images", "0") to ConfigParseMountImagesOptionValue() as OptionValueInformation, + Validator("config_parse_import_credential", "0") to ConfigParseImportCredentialOptionValue() as OptionValueInformation, + Validator("config_parse_extension_images", "0") to ConfigParseExtensionImagesOptionValue() as OptionValueInformation, + Validator("config_parse_exec_root_hash_sig", "0") to ConfigParseExecRootHashSigOptionValue() as OptionValueInformation, + Validator("config_parse_exec_root_hash", "0") to ConfigParseExecRootHashOptionValue() as OptionValueInformation, + Validator("config_parse_exec_input_data", "0") to ConfigParseExecInputDataOptionValue() as OptionValueInformation, + Validator("config_parse_exec_coredump_filter", "0") to ConfigParseExecCoredumpFilterOptionValue() as OptionValueInformation, + Validator("config_parse_environ", "0") to ConfigParseEnvironOptionValue() as OptionValueInformation, + Validator("config_parse_concurrency_max", "0") to ConfigParseConcurrencyMaxOptionValue() as OptionValueInformation, + Validator("config_parse_luo_sessions", "0") to ConfigParseLuoSessionsOptionValue() as OptionValueInformation, + // Batch 2026-10: seccomp-bpf-cgroup + Validator("config_parse_syscall_filter", "0") to ConfigParseSyscallFilterOptionValue() as OptionValueInformation, + Validator("config_parse_syscall_log", "0") to ConfigParseSyscallLogOptionValue() as OptionValueInformation, + Validator("config_parse_restrict_filesystems", "0") to ConfigParseRestrictFilesystemsOptionValue() as OptionValueInformation, + Validator("config_parse_private_bpf", "0") to ConfigParsePrivateBpfOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_commands", "0") to ConfigParseBpfDelegateCommandsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_maps", "0") to ConfigParseBpfDelegateMapsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_programs", "0") to ConfigParseBpfDelegateProgramsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_attachments", "0") to ConfigParseBpfDelegateAttachmentsOptionValue() as OptionValueInformation, + Validator("config_parse_cgroup_nft_set", "NFT_SET_PARSE_CGROUP") to ConfigParseCgroupNftSetOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_foreign_program", "0") to ConfigParseBpfForeignProgramOptionValue() as OptionValueInformation, + Validator("config_parse_xattr", "0") to ConfigParseXattrOptionValue() as OptionValueInformation, + Validator("config_parse_socket_bindtodevice", "0") to ConfigParseSocketBindtodeviceOptionValue() as OptionValueInformation, + // END Batch 2026-10: seccomp-bpf-cgroup + // Batch 2026-10: unit-core + // config_parse_timer: TIMER_CALENDAR goes to calendar_spec_from_string, every other ltype to parse_sec. + Validator("config_parse_timer", "TIMER_CALENDAR") to ConfigParseTimerCalendarOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_ACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_BOOT") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_STARTUP") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_UNIT_ACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_UNIT_INACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + // config_parse_socket_listen: SOCKET_SOCKET covers four keys with different value types, so its + // validator dispatches on the key name, exactly as the C function does. + Validator("config_parse_socket_listen", "SOCKET_SOCKET") to ConfigParseSocketListenSocketOptionValue() as OptionValueInformation, + Validator("config_parse_socket_listen", "SOCKET_FIFO") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation, + Validator("config_parse_socket_listen", "SOCKET_MQUEUE") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation, + Validator("config_parse_socket_listen", "SOCKET_USB_FUNCTION") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation, + Validator("config_parse_socket_listen", "SOCKET_SPECIAL") to ConfigParseSocketListenSpecialOptionValue() as OptionValueInformation, + Validator("config_parse_service_refresh_on_reload", "0") to ConfigParseServiceRefreshOnReloadOptionValue() as OptionValueInformation, + Validator("config_parse_pid_file", "0") to ConfigParsePidFileOptionValue() as OptionValueInformation, + Validator("config_parse_open_file", "0") to ConfigParseOpenFileOptionValue() as OptionValueInformation, + Validator("config_parse_job_timeout_sec", "0") to ConfigParseJobTimeoutSecOptionValue() as OptionValueInformation, + Validator("config_parse_bus_name", "0") to ConfigParseBusNameOptionValue() as OptionValueInformation, + // RequiresOverridable=/RequisiteOverridable= just log and forward to config_parse_unit_deps. + Validator("config_parse_obsolete_unit_deps", "UNIT_REQUIRES") to UnitDependencyOptionValue() as OptionValueInformation, + Validator("config_parse_obsolete_unit_deps", "UNIT_REQUISITE") to UnitDependencyOptionValue() as OptionValueInformation, + Validator("config_parse_path", "0") to ConfigParsePathOptionValue() as OptionValueInformation, + Validator("config_parse_long", "0") to ConfigParseLongOptionValue() as OptionValueInformation, + Validator("config_parse_iec_size_long", "0") to ConfigParseIecSizeLongOptionValue() as OptionValueInformation, + // Batch 2026-10: nspawn + // .nspawn settings (src/nspawn/nspawn-settings.c at 0f0682b773). None of these parsers is used by + // any other gperf table, and every (parser, ltype) below is shared only by keys of one value type. + Validator("config_parse_capability", "0") to ConfigParseCapabilityOptionValue() as OptionValueInformation, + Validator("config_parse_volatile_mode", "0") to ConfigParseVolatileModeOptionValue() as OptionValueInformation, + Validator("config_parse_veth_extra", "0") to ConfigParseVethExtraOptionValue() as OptionValueInformation, + Validator("config_parse_tmpfs", "0") to ConfigParseTmpfsOptionValue() as OptionValueInformation, + Validator("config_parse_restrict_address_families", "0") to ConfigParseRestrictAddressFamiliesOptionValue() as OptionValueInformation, + Validator("config_parse_resolv_conf", "0") to ConfigParseResolvConfOptionValue() as OptionValueInformation, + Validator("config_parse_pivot_root", "0") to ConfigParsePivotRootOptionValue() as OptionValueInformation, + // ltype only selects read-only for Overlay=/OverlayReadOnly= and Bind=/BindReadOnly=. + Validator("config_parse_overlay", "0") to ConfigParseOverlayOptionValue() as OptionValueInformation, + Validator("config_parse_overlay", "1") to ConfigParseOverlayOptionValue() as OptionValueInformation, + Validator("config_parse_oom_score_adjust", "0") to ConfigParseOomScoreAdjustOptionValue() as OptionValueInformation, + Validator("config_parse_network_zone", "0") to ConfigParseNetworkZoneOptionValue() as OptionValueInformation, + Validator("config_parse_network_iface_pair", "0") to ConfigParseNetworkIfacePairOptionValue() as OptionValueInformation, + Validator("config_parse_macvlan_iface_pair", "0") to ConfigParseMacvlanIfacePairOptionValue() as OptionValueInformation, + Validator("config_parse_ipvlan_iface_pair", "0") to ConfigParseIpvlanIfacePairOptionValue() as OptionValueInformation, + Validator("config_parse_link_journal", "0") to ConfigParseLinkJournalOptionValue() as OptionValueInformation, + Validator("config_parse_inaccessible", "0") to ConfigParseInaccessibleOptionValue() as OptionValueInformation, + Validator("config_parse_id128", "0") to ConfigParseId128OptionValue() as OptionValueInformation, + Validator("config_parse_expose_port", "0") to ConfigParseExposePortOptionValue() as OptionValueInformation, + Validator("config_parse_bind_user", "0") to ConfigParseBindUserOptionValue() as OptionValueInformation, + Validator("config_parse_bind", "0") to ConfigParseBindOptionValue() as OptionValueInformation, + Validator("config_parse_bind", "1") to ConfigParseBindOptionValue() as OptionValueInformation, + // Batch 2026-10: netdev-link + // systemd-networkd .netdev (src/network/netdev/) and udev .link (src/udev/net/) settings, pinned to + // systemd 0f0682b773. Every key using each (parser, ltype) pair was checked in the gperf files; where + // a parser branches on the key name (vxlan_address) the grammar is the union and says so. + Validator("config_parse_badadv_bandwidth", "0") to ConfigParseBadadvBandwidthOptionValue() as OptionValueInformation, + Validator("config_parse_vxlan_address", "0") to ConfigParseVxlanAddressOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_peer_key_file", "0") to ConfigParseWireguardPeerKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_vlanprotocol", "0") to ConfigParseVlanprotocolOptionValue() as OptionValueInformation, + Validator("config_parse_vlan_qos_maps", "0") to ConfigParseVlanQosMapsOptionValue() as OptionValueInformation, + Validator("config_parse_macsec_key_id", "0") to ConfigParseMacsecKeyIdOptionValue() as OptionValueInformation, + Validator("config_parse_macsec_key_file", "0") to ConfigParseMacsecKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_cpu_set", "0") to ConfigParseCpuSetOptionValue() as OptionValueInformation, + Validator("config_parse_wol_password", "0") to ConfigParseWolPasswordOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_route_table", "0") to ConfigParseWireguardRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_private_key_file", "0") to ConfigParseWireguardPrivateKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_private_key", "0") to ConfigParseWireguardPrivateKeyOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_peer_route_table", "0") to ConfigParseWireguardPeerRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_endpoint", "0") to ConfigParseWireguardEndpointOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_allowed_ips", "0") to ConfigParseWireguardAllowedIpsOptionValue() as OptionValueInformation, + Validator("config_parse_vrf_table", "0") to ConfigParseVrfTableOptionValue() as OptionValueInformation, + Validator("config_parse_udev_property", "0") to ConfigParseUdevPropertyOptionValue() as OptionValueInformation, + Validator("config_parse_rps_cpu_mask", "0") to ConfigParseRpsCpuMaskOptionValue() as OptionValueInformation, + Validator("config_parse_port_range", "0") to ConfigParsePortRangeOptionValue() as OptionValueInformation, + Validator("config_parse_port", "0") to ConfigParsePortOptionValue() as OptionValueInformation, + Validator("config_parse_mac_address_policy", "0") to ConfigParseMacAddressPolicyOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_tunnel_remote_address", "0") to ConfigParseL2tpTunnelRemoteAddressOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_tunnel_local_address", "0") to ConfigParseL2tpTunnelLocalAddressOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_session_name", "0") to ConfigParseL2tpSessionNameOptionValue() as OptionValueInformation, + Validator("config_parse_irq_affinity_policy", "0") to ConfigParseIrqAffinityPolicyOptionValue() as OptionValueInformation, + Validator("config_parse_irq_affinity_numa", "0") to ConfigParseIrqAffinityNumaOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_flowlabel", "0") to ConfigParseIpv6FlowlabelOptionValue() as OptionValueInformation, + Validator("config_parse_ipoib_pkey", "0") to ConfigParseIpoibPkeyOptionValue() as OptionValueInformation, + Validator("config_parse_ifnames", "IFNAME_VALID_ALTERNATIVE") to ConfigParseIfnamesOptionValue() as OptionValueInformation, + Validator("config_parse_ifalias", "0") to ConfigParseIfaliasOptionValue() as OptionValueInformation, + Validator("config_parse_geneve_address", "0") to ConfigParseGeneveAddressOptionValue() as OptionValueInformation, + Validator("config_parse_default_port_vlanid", "0") to ConfigParseDefaultPortVlanidOptionValue() as OptionValueInformation, + Validator("config_parse_advertise", "0") to ConfigParseAdvertiseOptionValue() as OptionValueInformation, + // RxCoalesceCQENSec= registers config_parse_coalesce_sec with ltype `true` (parse_nsec instead of + // parse_sec); the `0` entry above (ConfigParseCoalesceSecOptionValue) covers the parse_sec keys. + Validator("config_parse_coalesce_sec", "true") to ConfigParseCoalesceNsecOptionValue() as OptionValueInformation, + + // Batch 2026-10: network-dhcp-misc + Validator("config_parse_dhcp_option_tlv", "0") to ConfigParseDhcpOptionTlvOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_option", "true") to ConfigParseDhcpOptionTypedOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp6_send_option", "0") to ConfigParseDhcp6SendOptionOptionValue() as OptionValueInformation, + Validator("config_parse_uplink", "0") to ConfigParseUplinkOptionValue() as OptionValueInformation, + Validator("config_parse_fdb_interface", "0") to ConfigParseFdbInterfaceOptionValue() as OptionValueInformation, + Validator("config_parse_mud_url", "0") to ConfigParseMudUrlOptionValue() as OptionValueInformation, + Validator("config_parse_resolve_support", "0") to ConfigParseResolveSupportOptionValue() as OptionValueInformation, + Validator("config_parse_dns_over_tls_mode", "0") to ConfigParseDnsOverTlsModeOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_pd_prefix_route_type", "0") to ConfigParseDhcpPdPrefixRouteTypeOptionValue() as OptionValueInformation, + Validator("config_parse_lldp_multicast_mode", "0") to ConfigParseLldpMulticastModeOptionValue() as OptionValueInformation, + Validator("config_parse_activation_policy", "0") to ConfigParseActivationPolicyOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_relay_interface_mode", "0") to ConfigParseDhcpRelayInterfaceModeOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_fallback_lease_lifetime", "0") to ConfigParseDhcpFallbackLeaseLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_ipv4ll", "0") to ConfigParseIpv4llOptionValue() as OptionValueInformation, + Validator("config_parse_ignore_carrier_loss", "0") to ConfigParseIgnoreCarrierLossOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_server_ipv6_only_preferred", "0") to ConfigParseDhcpServerIpv6OnlyPreferredOptionValue() as OptionValueInformation, + Validator("config_parse_bridge_vlan_id", "0") to ConfigParseBridgeVlanIdOptionValue() as OptionValueInformation, + Validator("config_parse_link_group", "0") to ConfigParseLinkGroupOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_pd_subnet_id", "0") to ConfigParseDhcpPdSubnetIdOptionValue() as OptionValueInformation, + Validator("config_parse_required_for_online", "0") to ConfigParseRequiredForOnlineOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_server_address", "0") to ConfigParseDhcpServerAddressOptionValue() as OptionValueInformation, + Validator("config_parse_ipv4ll_address", "0") to ConfigParseIpv4llAddressOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_label", "0") to ConfigParseDhcpLabelOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_static_lease_hostname", "0") to ConfigParseDhcpStaticLeaseHostnameOptionValue() as OptionValueInformation, + Validator("config_parse_domains", "0") to ConfigParseDomainsOptionValue() as OptionValueInformation, + Validator("config_parse_dnssec_negative_trust_anchors", "0") to ConfigParseDnssecNegativeTrustAnchorsOptionValue() as OptionValueInformation, + Validator("config_parse_ntp", "0") to ConfigParseNtpOptionValue() as OptionValueInformation, + Validator("config_parse_radv_dns", "0") to ConfigParseRadvDnsOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_request_options", "AF_INET") to ConfigParseDhcpRequestOptionsOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_request_options", "AF_INET6") to ConfigParseDhcpRequestOptionsOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp4_user_class", "0") to ConfigParseDhcp4UserClassOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp6_user_or_vendor_class", "0") to ConfigParseDhcp6UserOrVendorClassOptionValue() as OptionValueInformation, + Validator("config_parse_duid_type", "0") to ConfigParseDuidTypeOptionValue("config_parse_duid_type") as OptionValueInformation, + Validator("config_parse_network_duid_type", "0") to ConfigParseDuidTypeOptionValue("config_parse_network_duid_type") as OptionValueInformation, + Validator("config_parse_duid_rawdata", "0") to ConfigParseDuidRawdataOptionValue("config_parse_duid_rawdata") as OptionValueInformation, + Validator("config_parse_network_duid_rawdata", "0") to ConfigParseDuidRawdataOptionValue("config_parse_network_duid_rawdata") as OptionValueInformation, + Validator("config_parse_dhcp_or_ra_route_table", "NETWORK_CONFIG_SOURCE_DHCP4") to ConfigParseDhcpOrRaRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_or_ra_route_table", "NETWORK_CONFIG_SOURCE_DHCP6") to ConfigParseDhcpOrRaRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_or_ra_route_table", "NETWORK_CONFIG_SOURCE_NDISC") to ConfigParseDhcpOrRaRouteTableOptionValue() as OptionValueInformation, + + // Batch 2026-10: qdisc + Validator("config_parse_fq_codel_usec", "QDISC_KIND_FQ_CODEL") to ConfigParseFqCodelUsecOptionValue() as OptionValueInformation, + Validator("config_parse_tbf_rate", "QDISC_KIND_TBF") to ConfigParseTbfRateOptionValue() as OptionValueInformation, + Validator("config_parse_netem_rate", "QDISC_KIND_NETEM") to ConfigParseNetemRateOptionValue() as OptionValueInformation, + Validator("config_parse_netem_delay", "QDISC_KIND_NETEM") to ConfigParseNetemDelayOptionValue() as OptionValueInformation, + Validator("config_parse_htb_class_rate", "TCLASS_KIND_HTB") to ConfigParseHtbClassRateOptionValue() as OptionValueInformation, + Validator("config_parse_gred_u32", "QDISC_KIND_GRED") to ConfigParseGredU32OptionValue() as OptionValueInformation, + Validator("config_parse_fq_codel_u32", "QDISC_KIND_FQ_CODEL") to ConfigParseFqCodelU32OptionValue() as OptionValueInformation, + Validator("config_parse_teql_id", "QDISC_KIND_TEQL") to ConfigParseTeqlIdOptionValue() as OptionValueInformation, + Validator("config_parse_qfq_max_packet", "TCLASS_KIND_QFQ") to ConfigParseQfqMaxPacketOptionValue() as OptionValueInformation, + Validator("config_parse_pie_packet_limit", "QDISC_KIND_PIE") to ConfigParsePiePacketLimitOptionValue() as OptionValueInformation, + Validator("config_parse_htb_u32", "QDISC_KIND_HTB") to ConfigParseHtbU32OptionValue() as OptionValueInformation, + Validator("config_parse_htb_default_class", "QDISC_KIND_HTB") to ConfigParseHtbDefaultClassOptionValue() as OptionValueInformation, + Validator("config_parse_htb_class_u32", "TCLASS_KIND_HTB") to ConfigParseHtbClassU32OptionValue() as OptionValueInformation, + Validator("config_parse_gred_bool", "QDISC_KIND_GRED") to ConfigParseGredBoolOptionValue() as OptionValueInformation, + Validator("config_parse_fq_usec", "QDISC_KIND_FQ") to ConfigParseFqUsecOptionValue() as OptionValueInformation, + Validator("config_parse_fq_max_rate", "QDISC_KIND_FQ") to ConfigParseFqMaxRateOptionValue() as OptionValueInformation, + Validator("config_parse_fq_codel_bool", "QDISC_KIND_FQ_CODEL") to ConfigParseFqCodelBoolOptionValue() as OptionValueInformation, + Validator("config_parse_ets_quanta", "QDISC_KIND_ETS") to ConfigParseEtsQuantaOptionValue() as OptionValueInformation, + Validator("config_parse_ets_prio", "QDISC_KIND_ETS") to ConfigParseEtsPrioOptionValue() as OptionValueInformation, + Validator("config_parse_drr_size", "TCLASS_KIND_DRR") to ConfigParseDrrSizeOptionValue() as OptionValueInformation, + Validator("config_parse_cake_flow_isolation_mode", "QDISC_KIND_CAKE") to ConfigParseCakeFlowIsolationModeOptionValue() as OptionValueInformation, + Validator("config_parse_cake_bandwidth", "QDISC_KIND_CAKE") to ConfigParseCakeBandwidthOptionValue() as OptionValueInformation, + Validator("config_parse_cake_ack_filter", "QDISC_KIND_CAKE") to ConfigParseCakeAckFilterOptionValue() as OptionValueInformation, + Validator("config_parse_bfifo_size", "QDISC_KIND_BFIFO") to ConfigParseBfifoSizeOptionValue() as OptionValueInformation, ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseActivationPolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseActivationPolicyOptionValue.kt new file mode 100644 index 00000000..98285bf0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseActivationPolicyOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] ActivationPolicy= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ActivationPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1201 config_parse_activation_policy (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1191-L1198 activation_policy_table + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-activation-policy.network.d/always-down.conf#L2 + */ + +/** + * Validator for `[Link] ActivationPolicy=`: exactly one of `up`, `always-up`, `manual`, `always-down`, + * `down`, `bound` (plain DEFINE_STRING_TABLE_LOOKUP — case-sensitive, no boolean). + */ +class ConfigParseActivationPolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_activation_policy", + SequenceCombinator( + FlexibleLiteralChoiceTerminal("up", "always-up", "manual", "always-down", "down", "bound"), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt new file mode 100644 index 00000000..c05602ad --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Address] Label= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Label= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2298-L2363 config_parse_address_section (ADDRESS_LABEL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2174-L2200 config_parse_address_label + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L832-L852 address_label_valid + */ + +/** + * Validator for `[Address] Label=` (.network) — the ADDRESS_LABEL slot, whose table entry is + * config_parse_address_label → address_label_valid(). + * + * One to 15 characters (shorter than IFNAMSIZ), each a byte in 32…126 — printable ASCII, space + * included. A longer or non-ASCII label is ignored with "Interface label is too long or invalid". + */ +class ConfigParseAddressLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_address_section", + SequenceCombinator(RegexTerminal("""[\s\S]+""", """[\x20-\x7E]{1,15}"""), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt new file mode 100644 index 00000000..d5d929ee --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt @@ -0,0 +1,75 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * [Link] Advertise= and [EnergyEfficientEthernet] LinkMode= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Advertise= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L1125-L1177 config_parse_advertise (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L162-L168 ethtool_link_mode_bit_table (DEFINE_STRING_TABLE_LOOKUP) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-link-mode.py (generates the table from enum ethtool_link_mode_bit_indices) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/ethtool.h#L1966-L2109 enum ethtool_link_mode_bit_indices + * linux https://man7.org/linux/man-pages/man8/ethtool.8.html + */ + +/** + * The link-mode names ethtool-link-mode.py derives from the vendored kernel header: each + * `ETHTOOL_LINK_MODE__BIT` lower-cased with `_` turned into `-`, and `autoneg` renamed + * `autonegotiation`. 125 names at 0f0682b773. + */ +private val LINK_MODE = FlexibleLiteralChoiceTerminal( + "10baset-half", "10baset-full", "100baset-half", "100baset-full", "1000baset-half", + "1000baset-full", "autonegotiation", "tp", "aui", "mii", "fibre", "bnc", "10000baset-full", + "pause", "asym-pause", "2500basex-full", "backplane", "1000basekx-full", "10000basekx4-full", + "10000basekr-full", "10000baser-fec", "20000basemld2-full", "20000basekr2-full", + "40000basekr4-full", "40000basecr4-full", "40000basesr4-full", "40000baselr4-full", + "56000basekr4-full", "56000basecr4-full", "56000basesr4-full", "56000baselr4-full", + "25000basecr-full", "25000basekr-full", "25000basesr-full", "50000basecr2-full", + "50000basekr2-full", "100000basekr4-full", "100000basesr4-full", "100000basecr4-full", + "100000baselr4-er4-full", "50000basesr2-full", "1000basex-full", "10000basecr-full", + "10000basesr-full", "10000baselr-full", "10000baselrm-full", "10000baseer-full", + "2500baset-full", "5000baset-full", "fec-none", "fec-rs", "fec-baser", "50000basekr-full", + "50000basesr-full", "50000basecr-full", "50000baselr-er-fr-full", "50000basedr-full", + "100000basekr2-full", "100000basesr2-full", "100000basecr2-full", "100000baselr2-er2-fr2-full", + "100000basedr2-full", "200000basekr4-full", "200000basesr4-full", "200000baselr4-er4-fr4-full", + "200000basedr4-full", "200000basecr4-full", "100baset1-full", "1000baset1-full", + "400000basekr8-full", "400000basesr8-full", "400000baselr8-er8-fr8-full", "400000basedr8-full", + "400000basecr8-full", "fec-llrs", "100000basekr-full", "100000basesr-full", + "100000baselr-er-fr-full", "100000basecr-full", "100000basedr-full", "200000basekr2-full", + "200000basesr2-full", "200000baselr2-er2-fr2-full", "200000basedr2-full", "200000basecr2-full", + "400000basekr4-full", "400000basesr4-full", "400000baselr4-er4-fr4-full", "400000basedr4-full", + "400000basecr4-full", "100basefx-half", "100basefx-full", "10baset1l-full", "800000basecr8-full", + "800000basekr8-full", "800000basedr8-full", "800000basedr8-2-full", "800000basesr8-full", + "800000basevr8-full", "10baset1s-full", "10baset1s-half", "10baset1s-p2mp-half", + "10baset1brr-full", "200000basecr-full", "200000basekr-full", "200000basedr-full", + "200000basedr-2-full", "200000basesr-full", "200000basevr-full", "400000basecr2-full", + "400000basekr2-full", "400000basedr2-full", "400000basedr2-2-full", "400000basesr2-full", + "400000basevr2-full", "800000basecr4-full", "800000basekr4-full", "800000basedr4-full", + "800000basedr4-2-full", "800000basesr4-full", "800000basevr4-full", "1600000basecr8-full", + "1600000basekr8-full", "1600000basedr8-full", "1600000basedr8-2-full" +) + +/** + * Validator for `[Link] Advertise=` and `[EnergyEfficientEthernet] LinkMode=` (.link). + * + * C function: config_parse_advertise. A whitespace-separated list (extract_first_word, no unquoting) of + * ethtool link-mode names, each looked up exactly (case-sensitive) in ethtool_link_mode_bit_table — + * speed/duplex modes like `1000baset-full` as well as the non-speed bits (`autonegotiation`, `pause`, + * `asym-pause`, `tp`, `fibre`, `fec-rs`, …). + * + * The table is generated at build time from the kernel's enum ethtool_link_mode_bit_indices in + * systemd's vendored copy of linux/ethtool.h (src/include/uapi, which meson puts first on the include + * path). That is wider than the ethtool-link-mode.xml table in the plugin's generated man data, which + * lists 102 names; this follows the header the C is built against. An unknown name is logged and + * skipped by udev; an empty assignment clears the list. + */ +class ConfigParseAdvertiseOptionValue : SimpleGrammarOptionValues( + "config_parse_advertise", + SequenceCombinator(LINK_MODE, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), LINK_MODE)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt new file mode 100644 index 00000000..4d6c601d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetdevLinkCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkParseSizeSi +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.siSize +import java.math.BigInteger + +/* + * [BatmanAdvanced] GatewayBandwidthDown= / GatewayBandwidthUp= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#GatewayBandwidthDown= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/batadv.c#L53-L89 config_parse_badadv_bandwidth (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/batadv.c#L81-L84 (the 100 kbit uint32_t range warning) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1000) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-batadv.netdev#L14-L15 + */ + +/** + * Validator for `[BatmanAdvanced] GatewayBandwidthDown=` and `GatewayBandwidthUp=` (.netdev), plus the + * misspelled `GatewayBandwithDown=` / `GatewayBandwithUp=` that systemd still accepts as aliases. + * + * C function: config_parse_badadv_bandwidth, which hands the whole value to parse_size(rvalue, 1000, …): + * a number with an optional case-sensitive SI suffix (`K`, `M`, `G`, `T`, `P`, `E`, `B`), a + * fraction allowed, and several elements with strictly descending suffixes (`1G 500M`) summed. See + * [siSize]. + * + * The total is stored in units of 100 kbit in a uint32_t, so a total of 4294967296 × 100000 + * (429496729600000, e.g. `12P` or `3E 2P`) or more is flagged. systemd logs that case ("is outside of + * 0...429496729500000 range") and then stores the total truncated to 32 bits, which is never what was + * meant, so it is treated as invalid here. Totals that overflow parse_size itself (`20E`) are refused + * outright. Confirmed against systemd's parser built at 0f0682b773: `429496729599999` is silent, + * `429496729600000` warns. Any other invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `GatewayBandwidthDown=` or `GatewayBandwidthUp=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. + */ +class ConfigParseBadadvBandwidthOptionValue : SimpleGrammarOptionValues( + "config_parse_badadv_bandwidth", + SequenceCombinator( + NetdevLinkCheckedSpan(siSize(), "total < 429496729600000") { text -> + netdevLinkParseSizeSi(text)?.let { it < BATADV_BANDWIDTH_LIMIT } == true + }, + EOF() + ) +) + +/** `k/1000/100 > UINT32_MAX` (batadv.c#L81) first holds at this total. */ +private val BATADV_BANDWIDTH_LIMIT: BigInteger = BigInteger.valueOf(4_294_967_296L) * BigInteger.valueOf(100_000L) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBfifoSizeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBfifoSizeOptionValue.kt new file mode 100644 index 00000000..b6c02e8e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBfifoSizeOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [BFIFO] LimitBytes= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#LimitBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fifo.c#L103-L160 config_parse_bfifo_size (QDISC_KIND_BFIFO) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L42 LimitBytes=1000000 + */ + +/** + * Validator for [BFIFO] LimitBytes=. + * + * C function: config_parse_bfifo_size (QDISC_KIND_BFIFO). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A byte size as parse_size(rvalue, 1024) reads it (see [parseSize]) that must not exceed UINT32_MAX: + * above it the parser logs "Invalid 'LimitBytes=', ignoring assignment". So `4G` is rejected. + */ +class ConfigParseBfifoSizeOptionValue : SimpleGrammarOptionValues( + "config_parse_bfifo_size", + SequenceCombinator(parseSize(1024, maxInclusive = SIZE_UINT32_MAX), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindOptionValue.kt new file mode 100644 index 00000000..8a377247 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindOptionValue.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.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_FIELD_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_FIELD_SOURCE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.OneOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Files] Bind= / BindReadOnly= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Bind= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--bind= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L351-L375 config_parse_bind (0 = Bind=, 1 = BindReadOnly=) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-mount.c#L234-L275 bind_mount_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-mount.c#L86-L118 source_path_parse / source_path_parse_nullable + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240-L281 extract_many_words (":", EXTRACT_DONT_COALESCE_SEPARATORS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-mount.c#L759-L800 parse_mount_bind_options (only at container start, NOT at parse time) + * oracle steve-local/systemd-tools/bin/sd-check (fuzz-nspawn-settings at 0f0682b773): `Bind=/a:/b:bogus`, `Bind=/a:/b:rbind:x`, + * `BindReadOnly=/a:/b:ro`, `Bind=/a:/b:rbind,bogus`, `Bind=/a:/b::`, `Bind=/a:/b:has space` are all accepted + * (no output); `Bind=/a:relative:rbind` → "Invalid bind mount specification ... Invalid argument". + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L264-L287 + */ + +/** + * The options parse_mount_bind_options() knows. Listed first only for completion and highlighting: anything + * else is accepted too (see BIND_OPTIONS_FIELD), because only the container start rejects it. + */ +private val BIND_OPTION = FlexibleLiteralChoiceTerminal("rbind", "norbind", "idmap", "noidmap", "rootidmap", "owneridmap") + +private val COMMA = LiteralChoiceTerminal(",") + +/** + * The option string: split with extract_first_word(",", 0), which coalesces repeated commas and ignores + * leading and trailing ones (so a string of nothing but commas means "no options"). + */ +private val BIND_OPTIONS = SequenceCombinator( + ZeroOrMore(COMMA), + ZeroOrOne(SequenceCombinator(BIND_OPTION, ZeroOrMore(SequenceCombinator(OneOrMore(COMMA), BIND_OPTION)))), + ZeroOrMore(COMMA), +) + +/** + * Everything after the second colon. bind_mount_parse() strdup()s it verbatim (no unescaping, no checks), so + * any text is accepted at parse time. The known-option list is tried first so that a valid list is coloured + * and completed as options; otherwise the whole rest is taken as one opaque option string. + * parse_mount_bind_options() rejects unknown options only when the container starts ("Invalid bind mount + * option"); GitHub issue #565 tracks a separate warning for those. + */ +private val BIND_OPTIONS_FIELD = PreferValidAlternativeCombinator( + SequenceCombinator(BIND_OPTIONS, EOF()), + SequenceCombinator(RegexTerminal("""[\s\S]+""", """[\s\S]+"""), EOF()), +) + +/** A lone path is both source and destination, so it must be absolute (optionally `+`-prefixed). */ +private val SINGLE_PATH = RegexTerminal("""(?:[^:\\]|\\[\s\S])+""", """\+?\\?/(?:[^:\\]|\\[\s\S])*""") + +/** + * Validator for the .nspawn `[Files] Bind=` and `BindReadOnly=` settings (config_parse_bind; ltype 1 + * only sets the read-only flag, so both share one grammar). + * + * bind_mount_parse() splits the value into at most two colon-separated fields with extract_many_words(":", + * EXTRACT_DONT_COALESCE_SEPARATORS) — backslash makes the next character literal, so `\:` embeds a colon; + * quotes and whitespace are ordinary characters — and keeps whatever follows the second colon verbatim as + * the option string: + * + * - `PATH` alone is used as both source and destination: it must be absolute, optionally prefixed with + * `+` (meaning "relative to the container image"), which is dropped for the destination. + * - `SOURCE:DEST[:OPTIONS]` — SOURCE may be empty (a temporary directory under /var/tmp), `+/abs`, or any + * other path (a relative one is resolved against the working directory). DEST must be absolute. + * `SOURCE:` (an empty DEST) is invalid; an empty OPTIONS after the second colon is ignored. + * + * OPTIONS is not inspected at config-parse time, so any option string is accepted here. Only when the + * container starts does parse_mount_bind_options() refuse anything other than a comma-separated list of + * `rbind`, `norbind`, `idmap`, `noidmap`, `rootidmap`, `owneridmap` (e.g. `bogus`, `rbind:x`, `ro`); this + * validator follows the parser and accepts them, and GitHub issue #565 tracks a separate warning for + * such runtime-only rejections. Parse errors are logged and the setting ignored. + */ +class ConfigParseBindOptionValue : SimpleGrammarOptionValues( + "config_parse_bind", + AlternativeCombinator( + SequenceCombinator( + ZeroOrOne(COLON_FIELD_SOURCE_PATH), + COLON, + COLON_FIELD_ABSOLUTE_PATH, + AlternativeCombinator( + SequenceCombinator(COLON, BIND_OPTIONS_FIELD), + EOF(), + ), + ), + SequenceCombinator(SINGLE_PATH, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindPathsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindPathsOptionValue.kt new file mode 100644 index 00000000..f5db980c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindPathsOptionValue.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAINED_ABSOLUTE_PATH_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAINED_OPTIONAL_ABSOLUTE_PATH_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] BindPaths= / BindReadOnlyPaths=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BindPaths= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5187-L5305 config_parse_bind_paths (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (UNQUOTE|RETAIN_ESCAPE, then ':' with CUNESCAPE|UNESCAPE_SEPARATORS|DONT_COALESCE_SEPARATORS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240 extract_many_words_internal (4 slots: a 4th field is "Too many parameters") + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-load-fragment.c#L459 TEST(config_parse_bind_paths) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-bindpaths.service#L27-L30 + */ + +/** + * Validator for `BindPaths=` and `BindReadOnlyPaths=` (config_parse_bind_paths, ltype 0; the only + * difference between the two keys is `strstr(lvalue, "ReadOnly")`). + * + * A whitespace-separated list of `[-]SOURCE[:DESTINATION[:OPTIONS]]` tuples: + * - SOURCE and DESTINATION are absolute paths after specifier expansion (so `%t/foo` is fine) with no + * `..` component; only SOURCE may carry the `-` "ignore if missing" prefix. + * - OPTIONS is `rbind`, `norbind`, or empty (`/a:/b:` is accepted — `isempty(options)` means rbind). + * - fields are split with EXTRACT_DONT_COALESCE_SEPARATORS, so an empty field is a field: `/a:` and + * `/a::rbind` have an empty, non-absolute destination and are rejected; a fourth field is too many. + * - a ':' inside a path is written `\:`; other backslashes must be valid C escapes. + * + * Every rejected tuple is logged by systemd and skipped ("…, ignoring"). Lenient corners: quoted runs + * are opaque (see BatchExecCombinators.kt), and `%` specifiers are not checked. + */ +class ConfigParseBindPathsOptionValue : SimpleGrammarOptionValues( + "config_parse_bind_paths", + whitespaceList( + SequenceCombinator( + RETAINED_OPTIONAL_ABSOLUTE_PATH_FIELD, + ZeroOrOne( + SequenceCombinator( + COLON, + RETAINED_ABSOLUTE_PATH_FIELD, + ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(FlexibleLiteralChoiceTerminal("rbind", "norbind")))), + ) + ), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindUserOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindUserOptionValue.kt new file mode 100644 index 00000000..32d4cd52 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindUserOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.STRICT_USER_NAME +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 + +/* + * [Files] BindUser= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#BindUser= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--bind-user= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L979-L1024 config_parse_bind_user (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (WHITESPACE, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L718-L832 valid_user_group_name (flags 0 = strict) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L631-L632 + */ + +/** + * Validator for the .nspawn `[Files] BindUser=` setting (config_parse_bind_user, ltype 0). + * + * The value is a whitespace-separated list of user names (an empty value resets the list and never + * reaches the grammar). Each name must pass valid_user_group_name() in its STRICT form (no + * VALID_USER_RELAX): a letter or `_`, then letters, digits, `_` or `-`, at most 31 bytes (the utmp + * ut_user field). Dots, leading digits or dashes, and numeric UIDs are all refused. On the first invalid + * name the rest of the line is ignored with a warning. + */ +class ConfigParseBindUserOptionValue : SimpleGrammarOptionValues( + "config_parse_bind_user", + SequenceCombinator( + STRICT_USER_NAME, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), STRICT_USER_NAME)), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt new file mode 100644 index 00000000..6a596ce9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateAttachments= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateAttachments= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L181 config_parse_bpf_delegate_attachments (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L287 bpf_delegate_attachments_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4197 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_attach_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_attach_type_table from enum bpf_attach_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L52 BPFDelegateAttachments=BPFFlowDissector,BPFCgroupSysctl,BPFNetfilter + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateAttachments=` (the bpffs `delegate_attachs=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_attach_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateAttachments, Socket.BPFDelegateAttachments, Mount.BPFDelegateAttachments, Swap.BPFDelegateAttachments. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateAttachmentsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_attachments", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFCgroupInetIngress", "BPFCgroupInetEgress", "BPFCgroupInetSockCreate", "BPFCgroupSockOps", "BPFSkSkbStreamParser", + "BPFSkSkbStreamVerdict", "BPFCgroupDevice", "BPFSkMsgVerdict", "BPFCgroupInet4Bind", "BPFCgroupInet6Bind", + "BPFCgroupInet4Connect", "BPFCgroupInet6Connect", "BPFCgroupInet4PostBind", "BPFCgroupInet6PostBind", "BPFCgroupUdp4Sendmsg", + "BPFCgroupUdp6Sendmsg", "BPFLircMode2", "BPFFlowDissector", "BPFCgroupSysctl", "BPFCgroupUdp4Recvmsg", + "BPFCgroupUdp6Recvmsg", "BPFCgroupGetsockopt", "BPFCgroupSetsockopt", "BPFTraceRawTp", "BPFTraceFentry", + "BPFTraceFexit", "BPFModifyReturn", "BPFLsmMac", "BPFTraceIter", "BPFCgroupInet4Getpeername", + "BPFCgroupInet6Getpeername", "BPFCgroupInet4Getsockname", "BPFCgroupInet6Getsockname", "BPFXdpDevmap", "BPFCgroupInetSockRelease", + "BPFXdpCpumap", "BPFSkLookup", "BPFXdp", "BPFSkSkbVerdict", "BPFSkReuseportSelect", + "BPFSkReuseportSelectOrMigrate", "BPFPerfEvent", "BPFTraceKprobeMulti", "BPFLsmCgroup", "BPFStructOps", + "BPFNetfilter", "BPFTcxIngress", "BPFTcxEgress", "BPFTraceUprobeMulti", "BPFCgroupUnixConnect", + "BPFCgroupUnixSendmsg", "BPFCgroupUnixRecvmsg", "BPFCgroupUnixGetpeername", "BPFCgroupUnixGetsockname", "BPFNetkitPrimary", + "BPFNetkitPeer", "BPFTraceKprobeSession", "BPFTraceUprobeSession", "BPFTraceFsession", "BPFTraceFentryMulti", + "BPFTraceFexitMulti", "BPFTraceFsessionMulti", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt new file mode 100644 index 00000000..09f4167e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateCommands= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateCommands= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L178 config_parse_bpf_delegate_commands (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L263 bpf_delegate_commands_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4194 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_cmd) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_cmd_table from enum bpf_cmd in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L49 BPFDelegateCommands=BPFObjPin,BPFBtfLoad,BPFMapFreeze,BPFLinkDetach + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateCommands=` (the bpffs `delegate_cmds=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_cmd` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateCommands, Socket.BPFDelegateCommands, Mount.BPFDelegateCommands, Swap.BPFDelegateCommands. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateCommandsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_commands", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFMapCreate", "BPFMapLookupElem", "BPFMapUpdateElem", "BPFMapDeleteElem", "BPFMapGetNextKey", + "BPFProgLoad", "BPFObjPin", "BPFObjGet", "BPFProgAttach", "BPFProgDetach", + "BPFProgTestRun", "BPFProgGetNextId", "BPFMapGetNextId", "BPFProgGetFdById", "BPFMapGetFdById", + "BPFObjGetInfoByFd", "BPFProgQuery", "BPFRawTracepointOpen", "BPFBtfLoad", "BPFBtfGetFdById", + "BPFTaskFdQuery", "BPFMapLookupAndDeleteElem", "BPFMapFreeze", "BPFBtfGetNextId", "BPFMapLookupBatch", + "BPFMapLookupAndDeleteBatch", "BPFMapUpdateBatch", "BPFMapDeleteBatch", "BPFLinkCreate", "BPFLinkUpdate", + "BPFLinkGetFdById", "BPFLinkGetNextId", "BPFEnableStats", "BPFIterCreate", "BPFLinkDetach", + "BPFProgBindMap", "BPFTokenCreate", "BPFProgStreamReadByFd", "BPFProgAssocStructOps", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt new file mode 100644 index 00000000..b9c66217 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateMaps= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateMaps= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L179 config_parse_bpf_delegate_maps (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L271 bpf_delegate_maps_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4195 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_map_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_map_type_table from enum bpf_map_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L50 BPFDelegateMaps=BPFMapTypeArray,BPFMapTypeCpumap,BPFMapTypeRingbuf + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateMaps=` (the bpffs `delegate_maps=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_map_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateMaps, Socket.BPFDelegateMaps, Mount.BPFDelegateMaps, Swap.BPFDelegateMaps. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateMapsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_maps", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFMapTypeUnspec", "BPFMapTypeHash", "BPFMapTypeArray", "BPFMapTypeProgArray", "BPFMapTypePerfEventArray", + "BPFMapTypePercpuHash", "BPFMapTypePercpuArray", "BPFMapTypeStackTrace", "BPFMapTypeCgroupArray", "BPFMapTypeLruHash", + "BPFMapTypeLruPercpuHash", "BPFMapTypeLpmTrie", "BPFMapTypeArrayOfMaps", "BPFMapTypeHashOfMaps", "BPFMapTypeDevmap", + "BPFMapTypeSockmap", "BPFMapTypeCpumap", "BPFMapTypeXskmap", "BPFMapTypeSockhash", "BPFMapTypeCgroupStorageDeprecated", + "BPFMapTypeReuseportSockarray", "BPFMapTypePercpuCgroupStorageDeprecated", "BPFMapTypeQueue", "BPFMapTypeStack", "BPFMapTypeSkStorage", + "BPFMapTypeDevmapHash", "BPFMapTypeStructOps", "BPFMapTypeRingbuf", "BPFMapTypeInodeStorage", "BPFMapTypeTaskStorage", + "BPFMapTypeBloomFilter", "BPFMapTypeUserRingbuf", "BPFMapTypeCgrpStorage", "BPFMapTypeArena", "BPFMapTypeInsnArray", + "BPFMapTypeRhash", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt new file mode 100644 index 00000000..646b2410 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegatePrograms= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegatePrograms= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L180 config_parse_bpf_delegate_programs (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L279 bpf_delegate_programs_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4196 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_prog_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_prog_type_table from enum bpf_prog_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L51 BPFDelegatePrograms=BPFProgTypeTracepoint,BPFProgTypeXdp,BPFProgTypeTracing + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegatePrograms=` (the bpffs `delegate_progs=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_prog_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Quirk faithfully reproduced: the generator's `(\w+)\b,` full-match also skips enumerators followed + * by a comment, so `BPF_PROG_TYPE_SYSCALL, /* … */` (bpf.h#L1094) never becomes `BPFProgTypeSyscall` + * and that spelling is rejected by systemd (and every later program type is shifted down one bit — + * an upstream bug, see the batch QUESTIONS.md). + * + * Covers: Service.BPFDelegatePrograms, Socket.BPFDelegatePrograms, Mount.BPFDelegatePrograms, Swap.BPFDelegatePrograms. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateProgramsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_programs", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFProgTypeUnspec", "BPFProgTypeSocketFilter", "BPFProgTypeKprobe", "BPFProgTypeSchedCls", "BPFProgTypeSchedAct", + "BPFProgTypeTracepoint", "BPFProgTypeXdp", "BPFProgTypePerfEvent", "BPFProgTypeCgroupSkb", "BPFProgTypeCgroupSock", + "BPFProgTypeLwtIn", "BPFProgTypeLwtOut", "BPFProgTypeLwtXmit", "BPFProgTypeSockOps", "BPFProgTypeSkSkb", + "BPFProgTypeCgroupDevice", "BPFProgTypeSkMsg", "BPFProgTypeRawTracepoint", "BPFProgTypeCgroupSockAddr", "BPFProgTypeLwtSeg6local", + "BPFProgTypeLircMode2", "BPFProgTypeSkReuseport", "BPFProgTypeFlowDissector", "BPFProgTypeCgroupSysctl", "BPFProgTypeRawTracepointWritable", + "BPFProgTypeCgroupSockopt", "BPFProgTypeTracing", "BPFProgTypeStructOps", "BPFProgTypeExt", "BPFProgTypeLsm", + "BPFProgTypeSkLookup", "BPFProgTypeNetfilter", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt new file mode 100644 index 00000000..51ebc47d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt @@ -0,0 +1,61 @@ +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.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UNIT_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * BPFProgram= ([Service]/[Socket]/[Mount]/[Swap]/[Slice]/[Scope]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html#BPFProgram=type:program-path + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5878-L5938 config_parse_bpf_foreign_program (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (":", no flags; separators coalesce) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/bpf-program.c#L25-L46 bpf_cgroup_attach_type_table (exact streq lookup) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE, normalized) + * linux https://man7.org/linux/man-pages/man2/bpf.2.html (BPF_PROG_ATTACH attach types) + */ + +/** bpf_cgroup_attach_type_table: the cgroup attach types, by their bpftool names. */ +private val BPF_CGROUP_ATTACH_TYPE = FlexibleLiteralChoiceTerminal( + "ingress", "egress", "sock_create", "sock_ops", "device", "bind4", "bind6", "connect4", "connect6", + "post_bind4", "post_bind6", "sendmsg4", "sendmsg6", "sysctl", "recvmsg4", "recvmsg6", "getsockopt", "setsockopt", +) + +/** + * Validator for `BPFProgram=TYPE:PATH`. + * + * config_parse_bpf_foreign_program() splits off the first `:`-delimited word (extract_first_word with + * `:` as the only separator; a run of colons coalesces, so `egress::/x` still leaves `/x`), requires + * something after it (leading colons are skipped too, so `:egress:/x` is the type `egress`), and looks the word up in bpf_cgroup_attach_type_table — one of `ingress`, + * `egress`, `sock_create`, `sock_ops`, `device`, `bind4`, `bind6`, `connect4`, `connect6`, + * `post_bind4`, `post_bind6`, `sendmsg4`, `sendmsg6`, `sysctl`, `recvmsg4`, `recvmsg6`, `getsockopt`, + * `setsockopt` (exact, case-sensitive). + * + * The rest is a path: specifiers are expanded (unit_path_printf), then path_simplify_and_warn() + * requires it to be absolute and normalized (no `..` component). That is [UNIT_PATH]: it must start + * with `/` or a specifier and contain no `..` component; the rest of the value belongs to the path, so + * further colons are part of it. Every failure is "…, ignoring", so it is flagged. + * + * Not checked: that the path is on bpffs (the man page says it generally starts `/sys/fs/bpf/`, but the + * parser does not require it — the attach simply fails at runtime). + * + * Covers: Service.BPFProgram, Socket.BPFProgram, Mount.BPFProgram, Swap.BPFProgram, Slice.BPFProgram, + * Scope.BPFProgram. + * + * The empty assignment removes all programs and never reaches the grammar. + */ +class ConfigParseBpfForeignProgramOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_foreign_program", + SequenceCombinator( + ZeroOrOne(RegexTerminal(":+", ":+")), + BPF_CGROUP_ATTACH_TYPE, + RegexTerminal(":+", ":+"), + UNIT_PATH, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBridgeVlanIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBridgeVlanIdOptionValue.kt new file mode 100644 index 00000000..c9df8f0a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBridgeVlanIdOptionValue.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_FALSE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [BridgeVLAN] PVID= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PVID= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-bridge-vlan.c#L341-L381 config_parse_bridge_vlan_id (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.c#L8-L23 parse_vlanid (safe_atou16, <= VLANID_MAX = 4094) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/26-bridge-vlan-master.network.d/30-override.conf#L3 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/26-bridge-vlan-master.network#L9 + */ + +/** + * Validator for `[BridgeVLAN] PVID=` (config_parse_bridge_vlan_id): a *false* boolean (removes the PVID; + * a true one falls through to parse_vlanid and is refused), or a VLAN id 0…4094 read by safe_atou16 + * (base 0, so `0x10` is legal). + */ +class ConfigParseBridgeVlanIdOptionValue : SimpleGrammarOptionValues( + "config_parse_bridge_vlan_id", + AlternativeCombinator( + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_094L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), + SequenceCombinator(BOOLEAN_FALSE, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBusNameOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBusNameOptionValue.kt new file mode 100644 index 00000000..71255292 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBusNameOptionValue.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.BatchUnitCoreGrammar.withUnitSpecifiers +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DOT +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.OneOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Service] BusName= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#BusName= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2421-L2453 config_parse_bus_name (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L179 unit_full_printf_full (specifiers expanded first) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-bus/bus-type.c#L144 sd_bus_service_name_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-bus/bus-internal.c#L119 service_name_is_valid + * linux https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus + */ + +// One dot-separated element of a well-known name: letters, digits, `_` and `-`, not starting with a +// digit. The syntactic shape is wider so a digit-led element is reported as the bad token. +private val WELL_KNOWN_ELEMENT = RegexTerminal("[A-Za-z0-9_-]+", "[A-Za-z_-][A-Za-z0-9_-]*") + +// In a unique name (leading `:`) elements may start with a digit. +private val UNIQUE_ELEMENT = RegexTerminal("[A-Za-z0-9_-]+", "[A-Za-z0-9_-]+") + +/** + * Validator for `[Service] BusName=` (config_parse_bus_name, the only key using it). + * + * After specifier expansion the name must pass sd_bus_service_name_is_valid(): two or more + * dot-separated elements of `[A-Za-z0-9_-]`, none empty and none starting with a digit — or a unique + * name (`:1.42`), whose elements may start with a digit; systemd accepts that form too, odd as it is + * for this setting. The 255-character limit is not checked. Any value that uses a known unit + * specifier (`org.example.%i`) is accepted. + */ +class ConfigParseBusNameOptionValue : SimpleGrammarOptionValues( + "config_parse_bus_name", + withUnitSpecifiers( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal(":"), UNIQUE_ELEMENT, OneOrMore(SequenceCombinator(DOT, UNIQUE_ELEMENT)), EOF()), + SequenceCombinator(WELL_KNOWN_ELEMENT, OneOrMore(SequenceCombinator(DOT, WELL_KNOWN_ELEMENT)), EOF()), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeAckFilterOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeAckFilterOptionValue.kt new file mode 100644 index 00000000..67911f6d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeAckFilterOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [CAKE] AckFilter= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#AckFilter= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L680-L730 config_parse_cake_ack_filter (QDISC_KIND_CAKE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L672-L678 cake_ack_filter_table + DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32-L43 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (case-insensitive) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L13-L37 TEST(parse_boolean) — "YES", "TRUE", "ON" accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-cake.network#L25 AckFilter=aggressive + */ + +/** + * Validator for [CAKE] AckFilter=. + * + * C function: config_parse_cake_ack_filter (QDISC_KIND_CAKE). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A boolean (parse_boolean() is tried first, case-insensitively: `yes` -> filter, `no` -> off) or the + * table word `aggressive`, which is matched exactly (case-sensitive streq). The table's `no`/`yes` + * entries are not listed as names: parse_boolean() already takes them, in any case. + */ +class ConfigParseCakeAckFilterOptionValue : SimpleGrammarOptionValues( + "config_parse_cake_ack_filter", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("aggressive"), + BOOLEAN, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeBandwidthOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeBandwidthOptionValue.kt new file mode 100644 index 00000000..e6299ca0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeBandwidthOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [CAKE] Bandwidth= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Bandwidth= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L141-L193 config_parse_cake_bandwidth (QDISC_KIND_CAKE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-cake.network#L12 Bandwidth=500M + */ + +/** + * Validator for [CAKE] Bandwidth=. + * + * C function: config_parse_cake_bandwidth (QDISC_KIND_CAKE). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A rate in bits per second as parse_size(rvalue, 1000) reads it (see [parseSize]); the result is + * divided by 8 and stored as a uint64 with no further bound. + */ +class ConfigParseCakeBandwidthOptionValue : SimpleGrammarOptionValues( + "config_parse_cake_bandwidth", + SequenceCombinator(parseSize(1000), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeFlowIsolationModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeFlowIsolationModeOptionValue.kt new file mode 100644 index 00000000..ca8ecee8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeFlowIsolationModeOptionValue.kt @@ -0,0 +1,28 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [CAKE] FlowIsolationMode= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#FlowIsolationMode= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L442-L492 config_parse_cake_flow_isolation_mode (QDISC_KIND_CAKE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L429-L438 cake_flow_isolation_mode_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L440 DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING (exact, case-sensitive) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-cake.network#L18 FlowIsolationMode=dual-dst-host + */ + +/** + * Validator for [CAKE] FlowIsolationMode=. + * + * C function: config_parse_cake_flow_isolation_mode (QDISC_KIND_CAKE). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * One of `none`, `src-host`, `dst-host`, `hosts`, `flows`, `dual-src-host`, `dual-dst-host`, `triple`, + * matched exactly (string_table_lookup is case-sensitive, and this table has no boolean fallback). + */ +class ConfigParseCakeFlowIsolationModeOptionValue : SimpleGrammarOptionValues( + "config_parse_cake_flow_isolation_mode", + SequenceCombinator(FlexibleLiteralChoiceTerminal("none", "src-host", "dst-host", "hosts", "flows", "dual-src-host", "dual-dst-host", "triple"), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt new file mode 100644 index 00000000..c0f64684 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [CAN] RestartSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RestartSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-can.c#L210-L245 config_parse_can_restart_usec (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[CAN] RestartSec=` (.network). + * + * config_parse_can_restart_usec reads a time span with parse_sec() and ignores it unless it is + * `infinity` or rounds up to at most UINT32_MAX milliseconds (the kernel's IFLA_CAN_RESTART_MS is a + * u32). `0` (disable automatic restart) is accepted. Note this parser has no isempty() branch of its + * own; an empty assignment is not something this grammar is asked about. + */ +class ConfigParseCanRestartUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_can_restart_usec", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || divRoundUp(it, USEC_PER_MSEC) <= UINT32_MAX }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCapabilityOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCapabilityOptionValue.kt new file mode 100644 index 00000000..287d1665 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCapabilityOptionValue.kt @@ -0,0 +1,69 @@ +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.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * [Exec] Capability= / AmbientCapability= / DropCapability= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Capability= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#AmbientCapability= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L276-L323 config_parse_capability (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (WHITESPACE, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/capability-list.c#L45-L66 capability_from_name + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497-L520 safe_atoi (numeric capabilities) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/capability-util.h#L17 CAP_LIMIT + * check https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn.c#L1559-L1561 verify_arguments (refuses AmbientCapability=all after parsing) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L518-L520 + * linux https://man7.org/linux/man-pages/man7/capabilities.7.html + */ + +/** One capability word: the literal `all`, a capability number, or a capability name. */ +private val CAPABILITY_WORD = AlternativeCombinator( + LiteralChoiceTerminal("all"), + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 62L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + CAPABILITY_NAME, +) + +/** + * Validator for the .nspawn `[Exec] Capability=`, `AmbientCapability=` and `DropCapability=` settings, + * all of which use config_parse_capability with ltype 0. + * + * The value is a whitespace-separated list (extract_first_word with the default WHITESPACE separators, + * so runs of whitespace coalesce). Each word is either: + * + * - `all` — matched with a case-sensitive streq(), so `ALL` is not special and fails the name lookup; + * - a capability number — capability_from_name() first tries safe_atoi() (base 0, so `0x15` works) + * and accepts 0…CAP_LIMIT (62), even above the running kernel's last capability; + * - a capability name — looked up in a gperf table generated with `--ignore-case`, so `cap_net_admin` + * is as good as `CAP_NET_ADMIN`. + * + * An unknown word is logged and skipped rather than failing the whole line; it is still flagged here. + * + * Deliberately lenient: the man page and nspawn itself refuse `all` for AmbientCapability= — that check + * happens later, in verify_arguments() ("AmbientCapability= does not support the value all"), and the + * grammar is shared by all three keys via (parser, ltype), so it cannot single that key out. + */ +class ConfigParseCapabilityOptionValue : SimpleGrammarOptionValues( + "config_parse_capability", + SequenceCombinator( + CAPABILITY_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), CAPABILITY_WORD)), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt new file mode 100644 index 00000000..c4942d68 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt @@ -0,0 +1,104 @@ +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.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * NFTSet= in the cgroup context ([Service]/[Socket]/[Mount]/[Swap]/[Slice]/[Scope]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html#NFTSet=family:table:set (term anchor as generated from the man page) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6779-L6795 config_parse_cgroup_nft_set (NFT_SET_PARSE_CGROUP) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1207-L1295 config_parse_nft_set + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1136 nft_set_source_table (only cgroup/user/group allowed for NFT_SET_PARSE_CGROUP, #L1258-L1260) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1125 nfproto_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L824 nft_identifier_valid (NFT_NAME_MAXLEN 256) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240 extract_many_words (":" , EXTRACT_CUNESCAPE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L39-L55 extract_first_word (skips leading separators; trailing ones coalesce at #L151-L158) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.nft.sh#L24 NFTSet=cgroup:inet:sd_test:c user:inet:sd_test:u group:inet:sd_test:g + * linux https://www.netfilter.org/projects/nftables/manpage.html (nft(8): address families, table/set names) + */ + +/** The NFT_SET_PARSE_CGROUP sources; address/prefix/ifindex belong to networkd's NFTSet= only. */ +private val NFT_CGROUP_SOURCE = FlexibleLiteralChoiceTerminal("cgroup", "user", "group") + +/** nfproto_table: the nft address families. */ +private val NFT_FAMILY = FlexibleLiteralChoiceTerminal("arp", "bridge", "inet", "ip", "ip6", "netdev") + +/** + * nft_identifier_valid: an ASCII letter, then letters, digits, `/`, `\`, `_` or `.`, shorter than + * NFT_NAME_MAXLEN (256). The lenient side takes anything up to the next `:` or whitespace so a bad + * name is located as one token. + */ +private val NFT_IDENTIFIER = RegexTerminal("""[^\s:"']+""", """[A-Za-z][A-Za-z0-9/\\_.]{0,254}""") + +/** + * extract_many_words() is extract_first_word() in a loop, so runs of `:` coalesce (`cgroup::inet:t:s` + * is still four fields). + */ +private val FIELD_SEPARATOR = RegexTerminal(":+", ":+") + +/** + * extract_first_word() skips leading separators, and after the 4th word it swallows any trailing run of + * separators up to the end of the string (leaving q == NULL, so the isempty(q) check passes) — hence + * `:cgroup:inet:t:s` and `cgroup:inet:t:s:` are both accepted. + */ +private val NFT_SET_TUPLE = SequenceCombinator( + ZeroOrOne(FIELD_SEPARATOR), + NFT_CGROUP_SOURCE, FIELD_SEPARATOR, NFT_FAMILY, FIELD_SEPARATOR, NFT_IDENTIFIER, FIELD_SEPARATOR, NFT_IDENTIFIER, + ZeroOrOne(FIELD_SEPARATOR), +) + +/** The outer split uses EXTRACT_UNQUOTE, so a whole tuple may be quoted. */ +private val NFT_SET_ENTRY = AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), NFT_SET_TUPLE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), NFT_SET_TUPLE, LiteralChoiceTerminal("'")), + NFT_SET_TUPLE, +) + +/** + * Validator for the unit-file `NFTSet=` (cgroup context). + * + * A whitespace-separated list of `SOURCE:FAMILY:TABLE:SET` tuples: + * + * - SOURCE is `cgroup`, `user` or `group`. The shared config_parse_nft_set() also knows `address`, + * `prefix` and `ifindex`, but rejects them for this ltype ("Unknown NFT source"). + * - FAMILY is one of `arp`, `bridge`, `inet`, `ip`, `ip6`, `netdev` (exact match). + * - TABLE and SET must satisfy nft_identifier_valid(). + * + * Exactly four fields: extract_many_words() is asked for four and anything left over fails the tuple. + * Any failure logs a warning and abandons the rest of the line, so one bad tuple is flagged. + * + * The man page's term reads `NFTSet=family:table:set`, but both its body text and the C parser require + * the leading source type; this grammar follows the C. + * + * Leading and trailing `:` around a tuple are accepted (see NFT_SET_TUPLE). + * + * Known over-rejection: the outer whitespace split uses EXTRACT_UNQUOTE, so quotes *inside* a tuple + * (`cgroup:inet:"t":s`) are removed by systemd and accepted; this grammar only accepts a tuple quoted + * as a whole. No real configuration writes that. + * + * Deliberately lenient: backslash escapes inside a name (decoded by EXTRACT_CUNESCAPE before the + * identifier check) are not decoded — `\` is simply accepted as an identifier character, as + * nft_identifier_valid() does after decoding. + * + * Covers: Service.NFTSet, Socket.NFTSet, Mount.NFTSet, Swap.NFTSet, Slice.NFTSet, Scope.NFTSet. (The + * [Network]/[Address]/… NFTSet= of networkd uses ltype NFT_SET_PARSE_NETWORK and is not covered.) + * + * The empty assignment clears the list and never reaches the grammar. + */ +class ConfigParseCgroupNftSetOptionValue : SimpleGrammarOptionValues( + "config_parse_cgroup_nft_set", + SequenceCombinator( + NFT_SET_ENTRY, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), NFT_SET_ENTRY)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt new file mode 100644 index 00000000..5eaf1d1d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NSEC_DURATION_FINITE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] RxCoalesceCQENSec= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#RxCoalesceCQENSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L1370-L1415 config_parse_coalesce_sec (ltype "true", i.e. nsec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1374-L1466 parse_nsec + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L141 TEST(parse_nsec) + */ + +/** + * Validator for `[Link] RxCoalesceCQENSec=` (.link) — the one key that registers config_parse_coalesce_sec + * with ltype `true`, which switches the parser from parse_sec() to parse_nsec(). + * + * parse_nsec reads one or more `number[unit]` elements, a bare number meaning nanoseconds (`500`, + * `1us`, `2.5`, `1s500ms`, `.7`). See [NSEC_DURATION_FINITE]. + * + * `infinity` parses, but config_parse_coalesce_sec then refuses any result above UINT32_MAX ("Too large + * … value"), and NSEC_INFINITY is UINT64_MAX — so `infinity` is never a usable value here and is + * rejected. The same UINT32_MAX ceiling (about 4.29 seconds in nanoseconds) applies to every finite value + * too, but that compares the computed sum and is not modelled. An invalid value is logged and ignored. + */ +class ConfigParseCoalesceNsecOptionValue : SimpleGrammarOptionValues( + "config_parse_coalesce_sec", + SequenceCombinator(NSEC_DURATION_FINITE, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseConcurrencyMaxOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseConcurrencyMaxOptionValue.kt new file mode 100644 index 00000000..f103ca9b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseConcurrencyMaxOptionValue.kt @@ -0,0 +1,46 @@ +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.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Slice] ConcurrencyHardMax= / ConcurrencySoftMax= / ActivatingConcurrencyMax=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html#ConcurrencyHardMax= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html#ActivatingConcurrencyMax= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6091-L6111 config_parse_concurrency_max (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L966 config_parse_unsigned → safe_atou + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429 safe_atou_full (base 0, plus mangle_base 0b/0o at L403) + */ + +/** + * Validator for the [Slice] concurrency limits (config_parse_concurrency_max, ltype 0): + * `ConcurrencyHardMax=`, `ConcurrencySoftMax=` and `ActivatingConcurrencyMax=`. + * + * `infinity` (exact, streq), or an unsigned int as safe_atou() reads it (see [SafeAtoTerminal]), up to + * UINT_MAX (4294967295). The value is the whole (stripped) setting, so whitespace can still reach the parser: + * `0b 1` and a leading `\v` are accepted, as are `0b+1`, `0b-0`, a negative zero such as `-0`, and `+`-signed + * numbers (but not `+0b1`/`-0b0`, which mangle_base misses). `0` is legal (it blocks all activation). + */ +class ConfigParseConcurrencyMaxOptionValue : SimpleGrammarOptionValues( + "config_parse_concurrency_max", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("infinity"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt new file mode 100644 index 00000000..1e2210b0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CPU_SET +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] IRQAffinity= (.link) and [Exec] CPUAffinity= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinity= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#CPUAffinity= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L220-L307 config_parse_cpu_set (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L186-L206 cpu_set_add_range (CPU_SET_MAX_NCPU) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-cpu-set-util.c#L36 TEST(parse_cpu_set) + * linux https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html + */ + +/** + * Validator for `[Link] IRQAffinity=` (.link) and `[Exec] CPUAffinity=` (.nspawn). + * + * C function: config_parse_cpu_set. CPU indices and `N-M` ranges, separated by any mix of whitespace + * and commas (separators coalesce; leading/trailing ones are fine), each word optionally quoted. Each + * bound is a safe_atou() number (so hex works) below CPU_SET_MAX_NCPU = 8192. See [CPU_SET]. + * + * Lenient on one point: a reversed range such as `3-0` is rejected by systemd after parsing (it + * compares the numbers); that is not modelled. An empty assignment clears the set. + */ +class ConfigParseCpuSetOptionValue : SimpleGrammarOptionValues( + "config_parse_cpu_set", + SequenceCombinator(CPU_SET, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt new file mode 100644 index 00000000..4d75d13e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Bridge] DefaultPVID= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#DefaultPVID= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.c#L44-L67 config_parse_default_port_vlanid (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.c#L8-L23 parse_vlanid (safe_atou16, vlanid_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.h#L7-L13 VLANID_MAX (4094) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-bridge.netdev#L13 DefaultPVID=9 + */ + +/** + * Validator for `[Bridge] DefaultPVID=` (.netdev). + * + * C function: config_parse_default_port_vlanid. Either `none` (no PVID) or a VLAN id read by + * parse_vlanid (safe_atou16, so hex, octal and `0b`/`0o` work) and checked by vlanid_is_valid, i.e. 0…4094. + * + * The man page says "an integer in the range 1…4094", but vlanid_is_valid only checks `id <= 4094`, so + * `0` is accepted (and stored as 0, the same as `none`). This follows the C. An invalid value is logged + * and ignored by systemd. + * + * There is no isempty() branch, so an empty `DefaultPVID=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. + */ +class ConfigParseDefaultPortVlanidOptionValue : SimpleGrammarOptionValues( + "config_parse_default_port_vlanid", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("none"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4094L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp4UserClassOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp4UserClassOptionValue.kt new file mode 100644 index 00000000..922bad0d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp4UserClassOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CUNESCAPE_UNQUOTE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [DHCPv4] UserClass= (and the old [DHCP] UserClass=) (.network). + * + * Note: the plugin reports the deprecated [DHCP] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#UserClass= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L655-L698 config_parse_dhcp4_user_class (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + */ + +/** + * Validator for `[DHCPv4] UserClass=`: a whitespace-separated list of user-class strings, split by + * extract_first_word with EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE — so a class may be quoted to contain spaces + * and may use C escapes ([CUNESCAPE_UNQUOTE_WORD]). An unknown escape, a trailing backslash or an + * unterminated quote fails the assignment. + * + * Each class must also be 1…255 bytes long once unquoted/unescaped. An empty one (`""`, `''`) is + * dropped with a warning and flagged ([NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD]); the upper bound is not + * modelled (lenient). + */ +class ConfigParseDhcp4UserClassOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp4_user_class", + SequenceCombinator(wordList(NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6SendOptionOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6SendOptionOptionValue.kt new file mode 100644 index 00000000..d2ffe214 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6SendOptionOptionValue.kt @@ -0,0 +1,90 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_COALESCED_COLONS +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.KeyOrSectionDispatchingGrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.dhcpOptionTypedData +import java.math.BigInteger + +/* + * [DHCPv6] SendOption= / SendVendorOption= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#SendOption= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#SendVendorOption= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L745-L942 config_parse_dhcp6_send_option (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L781 streq(lvalue, "SendVendorOption") — the key decides the shape + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1176-L1183 dhcp_option_data_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L599-L613 safe_atou16_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (safe_atou32, base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +// safe_atou16/safe_atou32 skip leading whitespace, and extract_first_word(…, ":", 0) leaves any that +// follows a colon inside the next word, so `123: 1:string:x` is fine: hence acceptsWhitespace = true. +// CODE is safe_atou16 with `u16 < 1 || u16 >= UINT16_MAX` refused, so 1…65534. +private val CODE_TYPE_DATA = SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 65_534L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + NETWORK_DHCP_MISC_COALESCED_COLONS, + dhcpOptionTypedData(), + EOF(), +) + +private val SEND_OPTION = SequenceCombinator(ZeroOrMore(COLON), CODE_TYPE_DATA) + +private val SEND_VENDOR_OPTION = SequenceCombinator( + ZeroOrMore(COLON), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + NETWORK_DHCP_MISC_COALESCED_COLONS, + CODE_TYPE_DATA, +) + +/** + * Validator for `[DHCPv6] SendOption=` (`CODE:TYPE:DATA`) and `[DHCPv6] SendVendorOption=` + * (`ENTERPRISE:CODE:TYPE:DATA`), both parsed by config_parse_dhcp6_send_option. + * + * The parser branches on the *key name*: only for SendVendorOption= does it first read an enterprise + * identifier (safe_atou32, any uint32 — the man page says 1…4294967294 but C accepts 0 and 4294967295 + * too). The validator is registered per (parser, ltype) but dispatches on the key it is checking, so + * `SendOption=` takes exactly the three-field form and `SendVendorOption=` exactly the four-field one + * (`SendVendorOption=12:string:x` is refused, as systemd's safe_atou16("string") refuses it). + * + * CODE is safe_atou16 in 1…65534 — the C check is `u16 < 1 || u16 >= UINT16_MAX`, so 65535 is refused + * although the man page says "1…65536". The TYPE:DATA tail is [dhcpOptionTypedData]; all six types are + * accepted. Every separator coalesces, so leading and doubled colons are harmless. + */ +class ConfigParseDhcp6SendOptionOptionValue : KeyOrSectionDispatchingGrammarOptionValue( + VALIDATOR_NAME, + AlternativeCombinator(SEND_VENDOR_OPTION, SEND_OPTION), + { key, _ -> if (key == "SendVendorOption") VENDOR else PLAIN }, +) { + private companion object { + const val VALIDATOR_NAME = "config_parse_dhcp6_send_option" + val PLAIN = SimpleGrammarOptionValues(VALIDATOR_NAME, SEND_OPTION) + val VENDOR = SimpleGrammarOptionValues(VALIDATOR_NAME, SEND_VENDOR_OPTION) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6UserOrVendorClassOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6UserOrVendorClassOptionValue.kt new file mode 100644 index 00000000..d28876f8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6UserOrVendorClassOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [DHCPv6] UserClass= / VendorClass= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#UserClass= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#VendorClass= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L700-L743 config_parse_dhcp6_user_or_vendor_class (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + */ + +/** + * Validator for `[DHCPv6] UserClass=` and `[DHCPv6] VendorClass=`: the same quoted/escaped word list as + * [ConfigParseDhcp4UserClassOptionValue], with a 1…65535-byte length limit per entry: empty + * entries are flagged, the upper bound is not modelled. + */ +class ConfigParseDhcp6UserOrVendorClassOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp6_user_or_vendor_class", + SequenceCombinator(wordList(NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValue.kt new file mode 100644 index 00000000..d1c7154b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValue.kt @@ -0,0 +1,22 @@ +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 + +/* + * [DHCPv4] FallbackLeaseLifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#FallbackLeaseLifetimeSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp4.c#L2098-L2133 config_parse_dhcp_fallback_lease_lifetime (0) + */ + +/** + * Validator for `[DHCPv4] FallbackLeaseLifetimeSec=`. Despite the `Sec` suffix this is not a time span: + * the parser accepts only `forever` or `infinity` (STR_IN_SET, case-sensitive), as the man page says. + */ +class ConfigParseDhcpFallbackLeaseLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_fallback_lease_lifetime", + SequenceCombinator(FlexibleLiteralChoiceTerminal("forever", "infinity"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpLabelOptionValue.kt new file mode 100644 index 00000000..5e5678a2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpLabelOptionValue.kt @@ -0,0 +1,24 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPv4] Label= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Label= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp4.c#L2135-L2165 config_parse_dhcp_label (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L832-L852 address_label_valid + */ + +/** + * Validator for `[DHCPv4] Label=` (config_parse_dhcp_label → address_label_valid): 1…15 characters + * (shorter than IFNAMSIZ), each printable ASCII 0x20…0x7E — so, unlike an interface name, a label may + * contain `:`, `/`, `%` and inner spaces, and may be all digits. + */ +class ConfigParseDhcpLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_label", + SequenceCombinator(RegexTerminal("""[\s\S]+""", """[\x20-\x7E]{1,15}\z"""), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTlvOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTlvOptionValue.kt new file mode 100644 index 00000000..fa2dd445 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTlvOptionValue.kt @@ -0,0 +1,62 @@ +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.NETWORK_DHCP_MISC_COALESCED_COLONS +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.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.dhcpOptionTypedData + +/* + * [DHCPv4] SendOption= / SendVendorOption=, [DHCPServer] SendOption= / SendVendorOption=, + * [DHCPRelay] ExtraOption= (.network and networkd.conf). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#SendOption= + * https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#ExtraOption= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1062-L1108 config_parse_dhcp_option_tlv (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L944-L1060 config_parse_dhcp_option (TYPE:DATA tail, check_length=false) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1176-L1183 dhcp_option_data_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L583-L597 safe_atou8_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv4-only.network#L21 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for the `CODE:TYPE:DATA` raw-option settings parsed by config_parse_dhcp_option_tlv: + * `[DHCPv4] SendOption=`, `[DHCPv4] SendVendorOption=`, `[DHCPServer] SendOption=`, + * `[DHCPServer] SendVendorOption=` and `[DHCPRelay] ExtraOption=` (in both .network files and + * networkd.conf). + * + * CODE is read by safe_atou8 (so `0x0c` and `014` are 12 too, and leading whitespace, `0b 1` or `\v5` are + * fine because the field is cut on `:`, not on whitespace) and must be 1…254. The rest is handed to + * config_parse_dhcp_option and has the [dhcpOptionTypedData] shape: `uint8`/`uint16`/`uint32` take a + * number in range, `ipv4address`/`ipv6address` a bare address, `string` any C-escaped text (which may + * itself contain colons). Every separator is extract_first_word(…, ":", 0), so leading and repeated + * colons are coalesced and accepted. + * + * The man page lists only uint8/uint16/uint32/ipv4address/string for [DHCPv4]; the C parser shares + * the full type table, so `ipv6address` is accepted everywhere (C wins). + */ +class ConfigParseDhcpOptionTlvOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_option_tlv", + SequenceCombinator( + ZeroOrMore(COLON), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 254L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + NETWORK_DHCP_MISC_COALESCED_COLONS, + dhcpOptionTypedData(), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTypedOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTypedOptionValue.kt new file mode 100644 index 00000000..7b2c14d5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTypedOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.dhcpOptionTypedData + +/* + * [DHCPRelay] CircuitId= / RemoteId= / VirtualSubnetSelection= and the deprecated + * [DHCPServer] RelayAgentCircuitId= / RelayAgentRemoteId= (.network; RemoteId= also in networkd.conf). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#CircuitId= + * https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#RemoteId= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L944-L1060 config_parse_dhcp_option (true = check_length) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1176-L1183 dhcp_option_data_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-relay-downstream.network#L13-L14 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-agent-bridge.network#L12 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for config_parse_dhcp_option with ltype `true` (the gperf writes `/* check_length= */ true`): + * `[DHCPRelay] CircuitId=`, `RemoteId=`, `VirtualSubnetSelection=` and the deprecated + * `[DHCPServer] RelayAgentCircuitId=` / `RelayAgentRemoteId=`. + * + * The value is `TYPE:DATA` — see [dhcpOptionTypedData]: `uint8`/`uint16`/`uint32` with a number in range + * (base 0, so hex/octal are legal), `ipv4address`/`ipv6address` with a bare address, or `string` with any + * C-escaped text. With check_length set, systemd additionally refuses a string longer than 255 bytes once + * unescaped; that length limit is not modelled (lenient). + */ +class ConfigParseDhcpOptionTypedOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_option", + SequenceCombinator( + ZeroOrMore(COLON), + dhcpOptionTypedData(), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOrRaRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOrRaRouteTableOptionValue.kt new file mode 100644 index 00000000..4a8a1a75 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOrRaRouteTableOptionValue.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.ROUTE_TABLE_FROM_STRING + +/* + * [DHCPv4] RouteTable=, [DHCPv6] RouteTable=, [IPv6AcceptRA] RouteTable= (and the old [DHCP] RouteTable=) (.network). + * + * Note: the plugin reports the deprecated [DHCP] section as not allowed in .network files, so this validator never + * actually runs for the key there; it is listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RouteTable=num + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L560-L606 config_parse_dhcp_or_ra_route_table (NETWORK_CONFIG_SOURCE_DHCP4 / _DHCP6 / _NDISC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (safe_atou32, base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv4-only.network#L17 + */ + +/** + * Validator for `RouteTable=` in [DHCPv4], [DHCPv6] and [IPv6AcceptRA] (config_parse_dhcp_or_ra_route_table). The + * ltype only picks which Network field receives the table. The parser hands the whole value to + * manager_get_route_table_from_string() and adds nothing of its own: no special empty value and no extra bounds. + * So the grammar is the shared [ROUTE_TABLE_FROM_STRING]: `default`, `main` or `local` (case-sensitive), a table + * number 1…4294967295 read by safe_atou32 (so `0x10`, `010` and `0b 1` are numbers too, and `0` is refused), or + * anything networkd.conf could have defined as a table name (non-empty, no `:`, not all digits). + * + * The name branch is lenient by design: networkd.conf is not visible to the plugin, so `mytable` is accepted + * although systemd refuses it when networkd.conf does not define it. What is flagged is what systemd refuses + * whatever networkd.conf says: `0`, `00`, `08`, `4294967296` and anything containing `:` (checked with + * systemd's parser at 0f0682b773 on all three keys). + */ +class ConfigParseDhcpOrRaRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_or_ra_route_table", + ROUTE_TABLE_FROM_STRING, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValue.kt new file mode 100644 index 00000000..0dc04000 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPv4] UnassignedSubnetPolicy=, [DHCPv6] UnassignedSubnetPolicy= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#UnassignedSubnetPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-prefix-delegation.c#L1472-L1518 config_parse_dhcp_pd_prefix_route_type (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L341-L356 route_type_table / route_type_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L47-L49 route_type_is_reject + */ + +/** + * Validator for `UnassignedSubnetPolicy=` (config_parse_dhcp_pd_prefix_route_type). + * + * Accepts `none`, or a route type that route_type_is_reject() accepts: `unreachable`, `prohibit`, + * `blackhole` or `throw`. Other route types (`unicast`, `local`, …) parse but are then refused. No + * boolean; case-sensitive (streq / string table). + */ +class ConfigParseDhcpPdPrefixRouteTypeOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_pd_prefix_route_type", + SequenceCombinator( + FlexibleLiteralChoiceTerminal("none", "unreachable", "prohibit", "blackhole", "throw"), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdSubnetIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdSubnetIdOptionValue.kt new file mode 100644 index 00000000..81c70072 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdSubnetIdOptionValue.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.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import java.math.BigInteger + +/* + * [DHCPPrefixDelegation] SubnetId= (and the old [DHCPv6PrefixDelegation] SubnetId=) (.network). + * + * Note: the plugin reports the deprecated [DHCPv6PrefixDelegation] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#SubnetId= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-prefix-delegation.c#L1428-L1470 config_parse_dhcp_pd_subnet_id (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L522-L558 safe_atollu_full (safe_atoux64: explicit base 16) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp6pd-upstream.network#L16 + */ + +/** + * Validator for `SubnetId=` (config_parse_dhcp_pd_subnet_id): `auto`, or a *hexadecimal* number (always + * base 16 — `SubnetId=10` means 0x10 — with an optional `0x`) no larger than INT64_MAX, i.e. + * 0x7fffffffffffffff. + * + * `auto` comes first, but `a`, `add` etc. are hex numbers too; each branch ends in EOF so the classic + * engine can't strand one in the other. + */ +class ConfigParseDhcpPdSubnetIdOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_pd_subnet_id", + AlternativeCombinator( + SequenceCombinator(FlexibleLiteralChoiceTerminal("auto"), EOF()), + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 16, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.INT64_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRelayInterfaceModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRelayInterfaceModeOptionValue.kt new file mode 100644 index 00000000..ed768a8c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRelayInterfaceModeOptionValue.kt @@ -0,0 +1,24 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] DHCPRelay= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DHCPRelay= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-relay.c#L376-L380 config_parse_dhcp_relay_interface_mode (0), DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-relay.c#L369-L372 dhcp_relay_interface_mode_table + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-relay-downstream.network + */ + +/** + * Validator for `[Network] DHCPRelay=`: `upstream` or `downstream` (case-sensitive, no boolean). The + * empty assignment resets to "unset" and never reaches the grammar. + */ +class ConfigParseDhcpRelayInterfaceModeOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_relay_interface_mode", + SequenceCombinator(FlexibleLiteralChoiceTerminal("upstream", "downstream"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRequestOptionsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRequestOptionsOptionValue.kt new file mode 100644 index 00000000..dfbccf8d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRequestOptionsOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [DHCPv4] RequestOptions= (AF_INET), [DHCPv6] RequestOptions= (AF_INET6) (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RequestOptions= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1110-L1174 config_parse_dhcp_request_options (AF_INET / AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (safe_atou32, base 0) + */ + +/** + * Validator for `RequestOptions=` in both [DHCPv4] and [DHCPv6] (config_parse_dhcp_request_options; + * the two ltypes only pick which set is filled): a whitespace-separated list of option codes, each read + * by safe_atou32 and required to be 1…254 — for DHCPv6 too, although its option space is 16-bit; the C + * check is `i < 1 || i >= UINT8_MAX` for both families. + * + * Each code is a word from a whitespace split, so whitespace never reaches safe_atou32 + * (acceptsWhitespace = false): `RequestOptions=0b 1` is the words `0b` and `1`, and `0b` is refused + * ("DHCP request option is invalid", checked with systemd's parser at 0f0682b773). + */ +class ConfigParseDhcpRequestOptionsOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_request_options", + SequenceCombinator( + wordList( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 254L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerAddressOptionValue.kt new file mode 100644 index 00000000..664b198d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerAddressOptionValue.kt @@ -0,0 +1,59 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkDhcpMiscCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [DHCPServer] ServerAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ServerAddress= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server.c#L889-L932 config_parse_dhcp_server_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L799-L843 in_addr_prefix_from_string (AF_INET) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server.c#L922-L927 localhost / link-local → ignored + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L43-L47 in4_addr_is_link_local (169.254.0.0/16) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L121-L126 in4_addr_is_localhost (127.0.0.0/8) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L779-L797 in_addr_parse_prefixlen (safe_atou8, <= 32) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-server.network + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[DHCPServer] ServerAddress=`: an IPv4 address with an optional `/PREFIXLEN` of 0…32 + * (read by safe_atou8, so base-0 spellings like `0x18` are legal, and whitespace after the `/` reaches + * the parser and is skipped), e.g. `192.168.5.1/24`. + * + * A localhost (127.0.0.0/8) or link-local (169.254.0.0/16) address is flagged: systemd ignores it with + * "DHCP server address cannot be a localhost or link-local address" (checked with systemd's parser at + * 0f0682b773). + */ +class ConfigParseDhcpServerAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_server_address", + SequenceCombinator( + NetworkDhcpMiscCheckedSpan(IPV4_ADDR, "NotLoopbackOrLinkLocal") { addr -> + val o = addr.split('.').map { it.toIntOrNull() } + !(o.getOrNull(0) == 127 || (o.getOrNull(0) == 169 && o.getOrNull(1) == 254)) + }, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 32L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValue.kt new file mode 100644 index 00000000..e90e3194 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkDhcpMiscCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.networkDhcpMiscParseSecUsec +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SEC_TIMESPAN + +/* + * [DHCPServer] IPv6OnlyPreferredSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6OnlyPreferredSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server.c#L934-L974 config_parse_dhcp_server_ipv6_only_preferred (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server.c#L966-L970 t < MIN_V6ONLY_WAIT_USEC && !network_test_mode_enabled() + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/dhcp-protocol.h#L15 MIN_V6ONLY_WAIT_USEC (300 s) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289-L1291 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L82 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-server-ipv6-only-mode.network#L16 + */ + +/** + * Validator for `[DHCPServer] IPv6OnlyPreferredSec=`: a time span as parse_sec() reads it (`30min`, + * `1h30min`, `.5h`, `infinity`) — see [PARSE_SEC_TIMESPAN]. + * + * The duration must also be at least MIN_V6ONLY_WAIT_USEC, 300 s (`5min`, `300`, `4min 60s`); anything + * shorter, `0` included, is ignored with "Invalid [DHCPServer] IPv6OnlyPreferredSec=" (checked with + * systemd's parser at 0f0682b773). The only exception is networkd's test mode + * (`SYSTEMD_NETWORK_TEST_MODE`), which systemd's own test suite uses for its `20s` fixture; that is not a + * production setting, so short values are flagged. + */ +class ConfigParseDhcpServerIpv6OnlyPreferredOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_server_ipv6_only_preferred", + SequenceCombinator( + NetworkDhcpMiscCheckedSpan(PARSE_SEC_TIMESPAN, "AtLeast300s") { (networkDhcpMiscParseSecUsec(it) ?: Long.MAX_VALUE) >= 300_000_000L }, + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpStaticLeaseHostnameOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpStaticLeaseHostnameOptionValue.kt new file mode 100644 index 00000000..78069204 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpStaticLeaseHostnameOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LDH_DNS_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPServerStaticLease] Hostname= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Hostname= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server-static-lease.c#L222-L262 config_parse_dhcp_static_lease_hostname (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.h#L38-L47 dns_name_is_valid_ldh (DNS_LABEL_LDH|DNS_LABEL_NO_ESCAPES) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hostname-util.c#L42-L48 valid_ldh_char + */ + +/** + * Validator for `[DHCPServerStaticLease] Hostname=`: a DNS name made of LDH labels — letters, digits and + * hyphens, no leading or trailing hyphen, 1…63 characters each — joined by dots, with an optional single + * trailing dot; no escapes (see [LDH_DNS_NAME]). The 253-character total is not enforced. + */ +class ConfigParseDhcpStaticLeaseHostnameOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_static_lease_hostname", + SequenceCombinator(LDH_DNS_NAME, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnsOverTlsModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnsOverTlsModeOptionValue.kt new file mode 100644 index 00000000..cbcd0a40 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnsOverTlsModeOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] DNSOverTLS= (.network) and [Resolve] DNSOverTLS= (resolved.conf). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DNSOverTLS= + * https://www.freedesktop.org/software/systemd/man/latest/resolved.conf.html#DNSOverTLS= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/resolve-util.c#L10 config_parse_dns_over_tls_mode (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/resolve-util.c#L26-L31 dns_over_tls_mode_table (DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32-L43 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + */ + +/** + * Validator for `DNSOverTLS=` (config_parse_dns_over_tls_mode): `opportunistic`, or any boolean + * (parse_boolean first, case-insensitively; then the case-sensitive table `no`/`opportunistic`/`yes`). + */ +class ConfigParseDnsOverTlsModeOptionValue : SimpleGrammarOptionValues( + "config_parse_dns_over_tls_mode", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("opportunistic"), + BOOLEAN, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValue.kt new file mode 100644 index 00000000..8c9c26f3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DNS_NAME_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [Network] DNSSECNegativeTrustAnchors= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DNSSECNegativeTrustAnchors= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dns.c#L270-L319 config_parse_dnssec_negative_trust_anchors (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.h#L26-L36 dns_name_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape + */ + +/** + * Validator for `[Network] DNSSECNegativeTrustAnchors=`: a whitespace-separated list of DNS names, each + * checked by dns_name_is_valid() (non-LDH: any printable label characters, single dots, optional + * trailing dot, `.` for the root). See [DNS_NAME_WORD]. + */ +class ConfigParseDnssecNegativeTrustAnchorsOptionValue : SimpleGrammarOptionValues( + "config_parse_dnssec_negative_trust_anchors", + SequenceCombinator(wordList(DNS_NAME_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDomainsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDomainsOptionValue.kt new file mode 100644 index 00000000..b17cdd76 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDomainsOptionValue.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DNS_NAME_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkDhcpMiscCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.networkDhcpMiscIsLocalhost +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.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [Network] Domains= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Domains= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dns.c#L134-L209 config_parse_domains (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L742-L749 dns_name_is_root + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape (via dns_name_normalize, no LDH) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hostname-util.c#L145-L161 is_localhost + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dns.c#L192-L197 is_localhost(domain) → ignored + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-state-file-tests.network#L13 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/24-search-domain.network#L9 + */ + +// One Domains= word: an optional `~` (route-only), then the root domain `.`, the wildcard `*`, or a +// DNS name. `~` on its own is the root routing domain (dns_name_is_root("") is true). `*` needs no +// branch of its own: it is already a valid one-label non-LDH DNS name, and a separate `*` literal would +// strand the classic engine on a label such as `*foo`. +// +// The localhost check runs on the whole word, minus exactly one leading `~` as in the C: checking only +// the name part would let the parse engine read `~localhost` as a one-label name that starts with `~`. +private val DOMAIN_WORD = NetworkDhcpMiscCheckedSpan( + SequenceCombinator( + ZeroOrOne(LiteralChoiceTerminal("~")), + ZeroOrOne(DNS_NAME_WORD), + ), + "NotLocalhost", +) { !networkDhcpMiscIsLocalhost(it.removePrefix("~")) } + +/** + * Validator for `[Network] Domains=` (config_parse_domains): a whitespace-separated list of search + * domains, each optionally prefixed with `~` to make it a routing-only domain; `.`, `~.` and `*` select + * the root/wildcard routing domain. Names are validated as non-LDH DNS names ([DNS_NAME_WORD]), so + * underscores and most punctuation are fine but empty labels (`a..b`, `.a`) are not. + * + * `localhost`, `localhost.localdomain` and their subdomains (is_localhost, any case, with or without `~`) + * are flagged: systemd ignores them with "'localhost' domain may not be configured as search or route + * domain" (checked with systemd's parser at 0f0682b773). + */ +class ConfigParseDomainsOptionValue : SimpleGrammarOptionValues( + "config_parse_domains", + SequenceCombinator(wordList(DOMAIN_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDrrSizeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDrrSizeOptionValue.kt new file mode 100644 index 00000000..cd52278d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDrrSizeOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [DeficitRoundRobinSchedulerClass] QuantumBytes= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#QuantumBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/drr.c#L45-L102 config_parse_drr_size (TCLASS_KIND_DRR) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-drr.network#L16 QuantumBytes=2000 + */ + +/** + * Validator for [DeficitRoundRobinSchedulerClass] QuantumBytes=. + * + * C function: config_parse_drr_size (TCLASS_KIND_DRR). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A byte size as parse_size(rvalue, 1024) reads it (see [parseSize]) that must not exceed UINT32_MAX: + * above it the parser logs "Invalid 'QuantumBytes=', ignoring assignment". So `4G` (4294967296) is + * rejected and `4095M` is accepted. + */ +class ConfigParseDrrSizeOptionValue : SimpleGrammarOptionValues( + "config_parse_drr_size", + SequenceCombinator(parseSize(1024, maxInclusive = SIZE_UINT32_MAX), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidRawdataOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidRawdataOptionValue.kt new file mode 100644 index 00000000..e54a791c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidRawdataOptionValue.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_COALESCED_COLONS +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.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Repeat +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * DUIDRawData= — [DHCPv6] DUIDRawData= (.network, config_parse_duid_rawdata), [DHCPv4] DUIDRawData= and + * the old [DHCP] DUIDRawData= (.network, config_parse_network_duid_rawdata), and [DHCPv4]/[DHCPv6] + * DUIDRawData= in networkd.conf (config_parse_duid_rawdata). + * + * Note: the plugin reports the deprecated [DHCP] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#DUIDRawData= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DUIDRawData= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1321-L1392 config_parse_duid_rawdata (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1418-L1439 config_parse_network_duid_rawdata (0) → config_parse_duid_rawdata + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/dhcp-duid-internal.h#L26 MAX_DUID_DATA_LEN (128) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/network/test-networkd-conf.c#L108-L122 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv6-only-custom-client-identifier.network#L15 + */ + +// One byte: one or two hex digits (`0` is 0x00, `ab` is 0xab). The shape is wider so `000` / `abc` +// highlight as a single bad byte. +private val DUID_BYTE = RegexTerminal("""[0-9a-zA-Z]+""", """[0-9a-fA-F]{1,2}(?![0-9a-zA-Z])""") + +/** + * Validator for `DUIDRawData=`, registered for both config_parse_duid_rawdata and + * config_parse_network_duid_rawdata. + * + * Colon-separated bytes `NN:NN:…`, each one or two hex digits, at most MAX_DUID_DATA_LEN = 128 of them. + * The separator is extract_first_word(…, ":", 0), so runs of colons coalesce and leading/trailing colons + * are accepted — test-networkd-conf.c accepts `00:11:22:` and `11::`, and rejects `000:11:22`, + * `00:111:22` and `abcdef`. A value of nothing but colons parses to an empty DUID and is accepted too. + */ +class ConfigParseDuidRawdataOptionValue(validatorName: String = "config_parse_duid_rawdata") : SimpleGrammarOptionValues( + validatorName, + SequenceCombinator( + ZeroOrMore(COLON), + ZeroOrOne( + SequenceCombinator( + DUID_BYTE, + Repeat(SequenceCombinator(NETWORK_DHCP_MISC_COALESCED_COLONS, DUID_BYTE), 0, 127), + ZeroOrMore(COLON), + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidTypeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidTypeOptionValue.kt new file mode 100644 index 00000000..8fe24079 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidTypeOptionValue.kt @@ -0,0 +1,92 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +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.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * DUIDType= — [DHCPv6] DUIDType= (.network, config_parse_duid_type), [DHCPv4] DUIDType= and the old + * [DHCP] DUIDType= (.network, config_parse_network_duid_type), and [DHCPv4]/[DHCPv6] DUIDType= in + * networkd.conf (config_parse_duid_type). + * + * Note: the plugin reports the deprecated [DHCP] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#DUIDType= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DUIDType= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1195-L1272 config_parse_duid_type (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1298-L1319 config_parse_network_duid_type (0) → config_parse_duid_type + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1187-L1193 duid_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L599-L613 safe_atou16_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1083-L1145 parse_timestamp + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/network/test-networkd-conf.c#L25-L37 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv6-only-custom-client-identifier.network#L14 + */ + +private val DUID_TYPE_NAME = FlexibleLiteralChoiceTerminal("link-layer-time", "vendor", "link-layer", "uuid") + +// parse_timestamp() takes the whole remainder (it may itself contain spaces and colons, e.g. +// "2000-01-01 00:00:00 UTC", "@1700000000", "yesterday"); its grammar is large and is not modelled. +private val DUID_TIMESTAMP = RegexTerminal(""".+""", """.+""") + +private fun duidType(): Combinator = SequenceCombinator( + ZeroOrMore(COLON), + AlternativeCombinator( + // Only DUID-LLT (type 1) may carry a time; spelled either by name or as the number 1. + SequenceCombinator(FlexibleLiteralChoiceTerminal("link-layer-time"), NETWORK_DHCP_MISC_COALESCED_COLONS, DUID_TIMESTAMP, EOF()), + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 1L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + NETWORK_DHCP_MISC_COALESCED_COLONS, + DUID_TIMESTAMP, + EOF(), + ), + SequenceCombinator(DUID_TYPE_NAME, ZeroOrMore(COLON), EOF()), + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ZeroOrMore(COLON), + EOF(), + ), + ), +) + +/** + * Validator for `DUIDType=`, registered for both config_parse_duid_type and config_parse_network_duid_type + * (the latter only forwards to the former, twice). + * + * The value is `TYPE[:TIME]`. TYPE is one of `link-layer-time`, `vendor`, `link-layer`, `uuid` + * (case-sensitive) or any number 0…65535 read by safe_atou16 (so `0x42`, as systemd's own test data + * writes, is legal). A `:TIME` suffix — anything parse_timestamp() accepts — is allowed only when the type + * is DUID-LLT, i.e. `link-layer-time` or the number 1; `vendor:2000-01-01` is refused, as + * test-networkd-conf.c checks. The separator coalesces, so leading or trailing colons are harmless. + * + * The timestamp text itself is accepted as-is (lenient). + */ +class ConfigParseDuidTypeOptionValue(validatorName: String = "config_parse_duid_type") : SimpleGrammarOptionValues( + validatorName, + duidType(), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEnvironOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEnvironOptionValue.kt new file mode 100644 index 00000000..c240a40c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEnvironOptionValue.kt @@ -0,0 +1,77 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.C_ESCAPE_BODY +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 +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] Environment=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#Environment= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2763-L2834 config_parse_environ (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one (an unknown escape is -EINVAL: the rest of the line is ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L265 unit_env_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L81 env_assignment_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L31 env_name_is_valid_n (ASCII letters, digits, '_'; not starting with a digit) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-environment.service#L8 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-unsetenvironment.service#L10 + * linux https://man7.org/linux/man-pages/man7/environ.7.html + */ + +// One raw chunk of a word: a double- or single-quoted run, a C escape, or an ordinary character. +// Inside quotes a backslash is an escape too (EXTRACT_CUNESCAPE applies there as well). +private const val QUOTED_SYNTAX = """"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'""" +private const val QUOTED_SEMANTIC = """"(?:[^"\\]|\\$C_ESCAPE_BODY)*"|'(?:[^'\\]|\\$C_ESCAPE_BODY)*'""" +private const val WORD_SYNTAX = """(?:$QUOTED_SYNTAX|\\\S|[^\s"'\\])+""" +private const val WORD_SEMANTIC = """(?:$QUOTED_SEMANTIC|\\$C_ESCAPE_BODY|[^\s"'\\])+""" + +// A specifier stands in for name characters; which specifier, and what it expands to, is not checked. +private const val NAME_SPECIFIER = """%[^\s="'\\]""" + +// The plain form NAME=VALUE, where NAME has no quotes or backslashes in it. The name may contain +// specifiers (systemd's own test-execute uses `X%b=%U`). +private val ENV_NAME = RegexTerminal( + """[^\s="'\\]+""", + """(?:[A-Za-z_]|$NAME_SPECIFIER)(?:[A-Za-z0-9_]|$NAME_SPECIFIER)*(?![^\s="'\\])""", +) +private val ENV_VALUE = RegexTerminal(WORD_SYNTAX, "$WORD_SEMANTIC(?!\\S)") + +// Any word with a quote or backslash before its first '=' — the documented `"NAME=value with spaces"` +// form above all. The assignment only exists after unquoting, so this is one token: the lookahead +// requires that, skipping quotes, the word starts with a valid name followed by '='. Disjoint from the +// plain form (whose name may not contain a quote), so the two never both claim a word. +private val QUOTED_ENV_ASSIGNMENT = RegexTerminal( + """(?=[^\s="'\\]*["'\\])$WORD_SYNTAX""", + """(?=["']*(?:[A-Za-z_]|$NAME_SPECIFIER|\\\S)(?:["']|[A-Za-z0-9_]|$NAME_SPECIFIER|\\\S)*=)$WORD_SEMANTIC(?!\S)""", +) + +/** + * Validator for `Environment=` (config_parse_environ, ltype 0). + * + * A whitespace-separated list of `NAME=VALUE` assignments, each unquoted (`"VAR1=word1 word2"`, + * `VAR="a b"`) and C-unescaped before it is checked. NAME is ASCII letters, digits and `_`, not + * starting with a digit; VALUE may be empty (`FOO=`) and may contain anything, including further `=`. + * A word without `=` (e.g. the stray `production` in `UNICORN_OPTS=-E production`) is logged and + * skipped. An unbalanced quote or an unknown escape such as `\q` makes systemd drop the rest of the line. + * + * The man page says non-printable characters in values are rejected; env_value_is_valid() at + * 0f0682b773 only requires valid UTF-8, so control characters (e.g. `\t`) are accepted, following the C. + * + * Lenient corners: `%` specifiers are not checked, and in a word that is quoted before its `=` the name + * is only checked roughly (quotes inside it are skipped over). + */ +class ConfigParseEnvironOptionValue : SimpleGrammarOptionValues( + "config_parse_environ", + whitespaceList( + AlternativeCombinator( + SequenceCombinator(ENV_NAME, LiteralChoiceTerminal("="), ZeroOrOne(ENV_VALUE)), + QUOTED_ENV_ASSIGNMENT, + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsPrioOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsPrioOptionValue.kt new file mode 100644 index 00000000..2e27d6cb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsPrioOptionValue.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [EnhancedTransmissionSelection] PriorityMap= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PriorityMap= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/ets.c#L227-L302 config_parse_ets_prio (QDISC_KIND_ETS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L45-L47 safe_atou8 + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L583-L597 safe_atou8_full (rejects > UINT8_MAX) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/pkt_sched.h#L28 TC_PRIO_MAX = 15 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-ets.network#L18-L21 PriorityMap=8 7 6 5 / PriorityMap= / PriorityMap=3 4 5 + */ + +private val ETS_BAND = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT8_MAX, + acceptsWhitespace = false, + acceptsMinus = true, +) + +/** + * Validator for [EnhancedTransmissionSelection] PriorityMap=. + * + * C function: config_parse_ets_prio (QDISC_KIND_ETS). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A whitespace-separated list of band numbers, each read with safe_atou8 (base 0, 0…255), at most + * TC_PRIO_MAX + 1 (16) entries per line. Each entry is a word from extract_first_word(), so the number + * cannot contain a blank: `0b+1` is 1, but `0b 1` is the words `0b` (invalid) and `1`. Out-of-range or unparsable entries are logged and skipped by + * the C, and so are flagged here; so is a 17th entry. + * + * Lenient: enhanced_transmission_selection_verify later rejects the whole section if any entry is not + * below the number of bands (at most 16), so values 16…255 can never actually work; they are accepted + * here because the man page documents 0…255 and that is what the parser itself checks. The 16-entry + * cap is cumulative across lines; only the per-line count is checked. + */ +class ConfigParseEtsPrioOptionValue : SimpleGrammarOptionValues( + "config_parse_ets_prio", + SequenceCombinator(ETS_BAND, Repeat(SequenceCombinator(WhitespaceTerminal(), ETS_BAND), 0, 15), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsQuantaOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsQuantaOptionValue.kt new file mode 100644 index 00000000..c6d3a587 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsQuantaOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import java.math.BigInteger +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [EnhancedTransmissionSelection] QuantumBytes= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#QuantumBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/ets.c#L144-L225 config_parse_ets_quanta (QDISC_KIND_ETS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/pkt_sched.h#L1210 TCQ_ETS_MAX_BANDS = 16 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-ets.network#L14-L17 QuantumBytes=2 4 6 / QuantumBytes= / QuantumBytes=1001 1002 1003 + */ + +private val ETS_QUANTUM = parseSize(1024, minInclusive = BigInteger.ONE, maxInclusive = SIZE_UINT32_MAX, allowInnerWhitespace = false) + +/** + * Validator for [EnhancedTransmissionSelection] QuantumBytes=. + * + * C function: config_parse_ets_quanta (QDISC_KIND_ETS). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A whitespace-separated list (extract_first_word, no flags) of byte sizes, each read with + * parse_size(word, 1024) and required to be 1…4294967295; at most TCQ_ETS_MAX_BANDS (16) entries. A + * zero, oversized or unparsable entry is logged and skipped by the C ("ignoring assignment"), and so is + * flagged here; so is a 17th entry on one line. + * + * Because the list is split on whitespace first, each entry is a single word: `1K` or `1K512B` is one + * entry, but `1K 512B` is two. + * + * Lenient: the 16-entry cap is cumulative across repeated QuantumBytes= lines (until an empty + * assignment resets the list); only the per-line count is checked. + */ +class ConfigParseEtsQuantaOptionValue : SimpleGrammarOptionValues( + "config_parse_ets_quanta", + SequenceCombinator(ETS_QUANTUM, Repeat(SequenceCombinator(WhitespaceTerminal(), ETS_QUANTUM), 0, 15), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecCoredumpFilterOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecCoredumpFilterOptionValue.kt new file mode 100644 index 00000000..b5a1e31e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecCoredumpFilterOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] CoredumpFilter=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#CoredumpFilter= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L802-L838 config_parse_exec_coredump_filter (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/coredump-util.c#L38-L81 coredump_filter_mask_from_string (words split on WHITESPACE, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/coredump-util.c#L24-L34 coredump_filter_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L95 safe_atoux64 (base 16) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-coredump-util.c#L26-L70 TEST(coredump_filter_mask_from_string) + * linux https://man7.org/linux/man-pages/man5/core.5.html + */ + +// A raw mask: strtoull(…, 16) — an optional 0x prefix, at most 16 significant hex digits (uint64_t). +// strtoull also takes a sign: '+' is fine, and '-' is only refused when the value is non-zero, so "-0" +// and "-0x00" are accepted (safe_atollu_full: `l != 0 && s[0] == '-'`). +// The trailing lookahead keeps this from matching a hex-looking prefix of a longer word (the "de" of +// "default"), so it and the names never claim the same word. +private val COREDUMP_FILTER_MASK = RegexTerminal( + """[+-]?[0-9a-fA-FxX]+(?![a-zA-Z0-9-])""", + """(?:\+?(?:0[xX])?0*[0-9a-fA-F]{1,16}|-(?:0[xX])?0+)(?![a-zA-Z0-9-])""", +) + +/** + * Validator for `CoredumpFilter=` (config_parse_exec_coredump_filter, ltype 0). + * + * A whitespace-separated list of mapping-type names (`private-anonymous`, `shared-anonymous`, + * `private-file-backed`, `shared-file-backed`, `elf-headers`, `private-huge`, `shared-huge`, + * `private-dax`, `shared-dax`), the specials `default` and `all`, or raw masks, which are always + * hexadecimal (`11` is 0x11; `0x1101` is fine) and must fit 64 bits. Names are matched exactly + * (streq / string table, case-sensitive). One bad word makes systemd ignore the whole assignment. + */ +class ConfigParseExecCoredumpFilterOptionValue : SimpleGrammarOptionValues( + "config_parse_exec_coredump_filter", + whitespaceList( + AlternativeCombinator( + COREDUMP_FILTER_MASK, + FlexibleLiteralChoiceTerminal( + "default", "all", + "private-anonymous", "shared-anonymous", "private-file-backed", "shared-file-backed", + "elf-headers", "private-huge", "shared-huge", "private-dax", "shared-dax", + ), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecInputDataOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecInputDataOptionValue.kt new file mode 100644 index 00000000..d4e20f3a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecInputDataOptionValue.kt @@ -0,0 +1,28 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NON_EMPTY_BASE64_DATA +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Service]/[Socket]/[Mount]/[Swap] StandardInputData=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#StandardInputData= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1271-L1327 config_parse_exec_input_data (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755 unbase64mem_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L528 unbase64char (standard and URL-safe alphabets) + */ + +/** + * Validator for `StandardInputData=` (config_parse_exec_input_data, ltype 0). + * + * Base64 as unbase64mem() decodes it: whitespace anywhere is ignored (so a value continued over several + * lines with a trailing `\` works), standard (`+/`) or URL-safe (`-_`) alphabet, complete 4-character + * groups, `=` padding only in the last group and only where the discarded bits are zero. No escapes or + * specifiers are resolved. The 64 MiB cumulative cap (EXEC_STDIN_DATA_MAX) is not checked. + */ +class ConfigParseExecInputDataOptionValue : SimpleGrammarOptionValues( + "config_parse_exec_input_data", + SequenceCombinator(NON_EMPTY_BASE64_DATA, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashOptionValue.kt new file mode 100644 index 00000000..aed48bb8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashOptionValue.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.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Service]/[Socket]/[Mount]/[Swap] RootHash=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RootHash= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1715-L1771 config_parse_exec_root_hash (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.h#L30 path_is_absolute (just `p[0] == '/'`) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L109-L158 unhexmem_full / unhex_next (whitespace skipped, pairs of hex digits) + */ + +/** + * Validator for `RootHash=` (config_parse_exec_root_hash, ltype 0). + * + * Either a path to a file holding the root hash, or the hash itself in hexadecimal: + * - the path branch is taken whenever path_is_absolute(), i.e. the value starts with `/`; the rest is + * stored verbatim (no specifier expansion, no normalization check), so nothing more is checked; + * - otherwise unhexmem() must decode it: an even number of hex digits (either case), whitespace + * anywhere ignored, and at least sizeof(sd_id128_t) = 16 bytes (32 digits) — shorter is "too short". + * + * Because there is no specifier expansion, `%t/foo.roothash` is not absolute and fails as hex — so it is + * flagged, matching systemd. + */ +class ConfigParseExecRootHashOptionValue : SimpleGrammarOptionValues( + "config_parse_exec_root_hash", + SequenceCombinator( + AlternativeCombinator( + RegexTerminal("""/[\s\S]*""", """/[\s\S]*"""), + RegexTerminal("""[^/][\s\S]*""", """(?:\s*[0-9a-fA-F]\s*[0-9a-fA-F]){16,}\s*(?![\s\S])"""), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashSigOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashSigOptionValue.kt new file mode 100644 index 00000000..61f3377e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashSigOptionValue.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.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BASE64_DATA +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 + +/* + * [Service]/[Socket]/[Mount]/[Swap] RootHashSignature=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RootHashSignature= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1773-L1832 config_parse_exec_root_hash_sig (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.h#L30 path_is_absolute (just `p[0] == '/'`) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755 unbase64mem_full (after the "base64:" prefix) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L528 unbase64char (standard and URL-safe alphabets) + */ + +/** + * Validator for `RootHashSignature=` (config_parse_exec_root_hash_sig, ltype 0). + * + * Either a path to a DER-encoded PKCS#7 signature (any value starting with `/`, stored verbatim with no + * specifier expansion), or `base64:` followed by the signature in Base64 — whitespace ignored, standard + * (`+/`) or URL-safe (`-_`) alphabet, complete 4-character groups, padding only at the end. The prefix + * is matched case-sensitively (startswith). An empty payload (`base64:`) decodes to zero bytes and is + * accepted by the parser. + */ +class ConfigParseExecRootHashSigOptionValue : SimpleGrammarOptionValues( + "config_parse_exec_root_hash_sig", + SequenceCombinator( + AlternativeCombinator( + RegexTerminal("""/[\s\S]*""", """/[\s\S]*"""), + SequenceCombinator(LiteralChoiceTerminal("base64:"), BASE64_DATA), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExposePortOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExposePortOptionValue.kt new file mode 100644 index 00000000..404b54d1 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExposePortOptionValue.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +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.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Network] Port= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Port= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--port= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L248-L274 config_parse_expose_port (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-expose-ports.c#L17-L73 expose_port_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L718-L734 parse_ip_port + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0, SAFE_ATO_REFUSE_LEADING_WHITESPACE) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L557-L560 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L315-L326 (accepted and rejected --port= spellings) + */ + +/** + * parse_ip_port(): safe_atou16_full(s, SAFE_ATO_REFUSE_LEADING_WHITESPACE) (base 0), then zero is refused. + * Whitespace between a `0b`/`0o` prefix and the digits is skipped by strtoul() (`Port=0o 120` is valid), but a + * leading space or tab is refused (`80: 90` is not). The flag only checks `" \t\n\r"`, so a leading `\v` + * reaches strtoul(), which skips it (`Port=\v80` is valid). + */ +private val PORT = SafeAtoTerminal( + signed = false, + base = 0, + flags = setOf(SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE), + minInclusive = 1L, + maxInclusive = 65535L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +/** + * Validator for the .nspawn `[Network] Port=` setting (config_parse_expose_port, ltype 0). + * + * expose_port_parse() strips an optional `tcp:` or `udp:` prefix (case-sensitive startswith(); the + * default is TCP), then splits the rest at the first `:` into a host port and a container port. With no + * colon the one number is used for both. Each port goes through parse_ip_port(): safe_atou16() with + * base 0 (so `0x50` is port 80) and no leading whitespace, then 1…65535. A malformed or duplicate + * specification is logged and ignored. + */ +class ConfigParseExposePortOptionValue : SimpleGrammarOptionValues( + "config_parse_expose_port", + SequenceCombinator( + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal("tcp", "udp"), COLON)), + PORT, + ZeroOrOne(SequenceCombinator(COLON, PORT)), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExtensionImagesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExtensionImagesOptionValue.kt new file mode 100644 index 00000000..248e98d4 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExtensionImagesOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EXEC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EXTENSION_IMAGE_SOURCE_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.imageMountOptionsTail +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] ExtensionImages=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#ExtensionImages= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5457-L5592 config_parse_extension_images (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (UNQUOTE|RETAIN_ESCAPE words, then ':' with CUNESCAPE|UNESCAPE_SEPARATORS, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (source specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721 path_below_api_vfs (/dev, /sys, /proc) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/gpt.c#L174 partition_designator_from_string + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-50-DISSECT.mountfsd.sh#L135 + */ + +/** + * Validator for `ExtensionImages=` (config_parse_extension_images, ltype 0). + * + * A whitespace-separated list of `[-]SOURCE[:OPTIONS | :PARTITION:OPTIONS…]` words. SOURCE is absolute + * after specifier expansion, free of `..`, and — unlike MountImages= — may not be below /dev, /proc or + * /sys (PATH_CHECK_NON_API_VFS, no DEV_OK). The option tail is the same as MountImages=: a single + * root-partition option string, or `PARTITION:OPTIONS` pairs. ':' separators are coalesced; a ':' in + * the path is written `\:`. + * + * Lenient corners: quoted runs are opaque, `%` specifiers and the mount options are not checked. + */ +class ConfigParseExtensionImagesOptionValue : SimpleGrammarOptionValues( + "config_parse_extension_images", + whitespaceList( + SequenceCombinator( + ZeroOrOne(EXEC_COALESCED_COLONS), + EXTENSION_IMAGE_SOURCE_FIELD, + imageMountOptionsTail(), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFdbInterfaceOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFdbInterfaceOptionValue.kt new file mode 100644 index 00000000..4ac2c4a0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFdbInterfaceOptionValue.kt @@ -0,0 +1,24 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IFNAME_OR_IFINDEX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [BridgeFDB] OutgoingInterface= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#OutgoingInterface= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-bridge-fdb.c#L485-L539 config_parse_fdb_interface (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + */ + +/** + * Validator for `[BridgeFDB] OutgoingInterface=` (config_parse_fdb_interface): a positive interface + * index, or an alternative interface name of up to 127 characters — see [IFNAME_OR_IFINDEX]. + */ +class ConfigParseFdbInterfaceOptionValue : SimpleGrammarOptionValues( + "config_parse_fdb_interface", + SequenceCombinator(IFNAME_OR_IFINDEX, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelBoolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelBoolOptionValue.kt new file mode 100644 index 00000000..b04ad462 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelBoolOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [FairQueueingControlledDelay] ECN= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ECN= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq-codel.c#L222-L265 config_parse_fq_codel_bool (QDISC_KIND_FQ_CODEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L48-L64 parse_tristate_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (STRCASE_IN_SET: case-insensitive) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L13-L37 TEST(parse_boolean) — "YES", "TRUE", "ON" accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq_codel.network#L18 ECN=yes + */ + +/** + * Validator for [FairQueueingControlledDelay] ECN=. + * + * C function: config_parse_fq_codel_bool (QDISC_KIND_FQ_CODEL). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A boolean via parse_tristate(): any parse_boolean() spelling, case-insensitively. Empty resets to + * "unset" (handled before the grammar). + */ +class ConfigParseFqCodelBoolOptionValue : SimpleGrammarOptionValues( + "config_parse_fq_codel_bool", + SequenceCombinator(BOOLEAN, EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelU32OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelU32OptionValue.kt new file mode 100644 index 00000000..6357d525 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelU32OptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [FairQueueingControlledDelay] PacketLimit=, Flows= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PacketLimit= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Flows= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq-codel.c#L97-L155 config_parse_fq_codel_u32 (QDISC_KIND_FQ_CODEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq_codel.network#L12-L14 PacketLimit=20480, Flows=2048 + */ + +/** + * Validator for [FairQueueingControlledDelay] PacketLimit=, Flows=. + * + * C function: config_parse_fq_codel_u32 (QDISC_KIND_FQ_CODEL). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned 32-bit integer read with safe_atou32 (base 0: decimal, `0x` hex, `0o`/leading-zero + * octal, `0b` binary, optional `+`), 0…4294967295. No further bound in the parser. strtoul starts + * afresh after a `0b`/`0o` prefix, so `0b+1` and `0b 1` are 1 (see [SafeAtoTerminal]). + */ +class ConfigParseFqCodelU32OptionValue : SimpleGrammarOptionValues( + "config_parse_fq_codel_u32", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelUsecOptionValue.kt new file mode 100644 index 00000000..c6ba5d11 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelUsecOptionValue.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.* + +/* + * [FairQueueingControlledDelay] TargetSec=, IntervalSec=, CEThresholdSec= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#TargetSec= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IntervalSec= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#CEThresholdSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq-codel.c#L157-L220 config_parse_fq_codel_usec (QDISC_KIND_FQ_CODEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L83 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq_codel.network#L15-L19 TargetSec=10ms, IntervalSec=200ms, CEThresholdSec=100ms + */ + +/** + * Validator for [FairQueueingControlledDelay] TargetSec=, IntervalSec=, CEThresholdSec=. + * + * C function: config_parse_fq_codel_usec (QDISC_KIND_FQ_CODEL). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A time span as parse_sec() reads it: `infinity`, or one or more `number[unit]` elements such as + * `10ms`, `1min30s`, `5s 500ms`, `.5s`, `+3.1s` (see [parseSec]). There is no extra bound: the parsed + * usec_t is stored as is (CEThresholdSec= additionally treats an empty value as USEC_INFINITY). + * + * Overflow is rejected exactly as parse_time() does (ERANGE): an integer part above LLONG_MAX, or a finite + * total reaching USEC_INFINITY (e.g. `18446744073709551615us`). + */ +class ConfigParseFqCodelUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_fq_codel_usec", + SequenceCombinator(parseSec(), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqMaxRateOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqMaxRateOptionValue.kt new file mode 100644 index 00000000..381bbb81 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqMaxRateOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import java.math.BigInteger +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [FairQueueing] MaximumRate= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MaximumRate= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq.c#L344-L402 config_parse_fq_max_rate (QDISC_KIND_FQ) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq.network#L16 MaximumRate=1M + */ + +/** + * Validator for [FairQueueing] MaximumRate=. + * + * C function: config_parse_fq_max_rate (QDISC_KIND_FQ). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A rate in bits per second as parse_size(rvalue, 1000) reads it (see [parseSize]) whose byte rate + * (value / 8) must fit a uint32: above it the parser logs "Specified 'MaximumRate=' is too large, + * ignoring assignment". So the largest accepted value is 34359738367 bits/s (≈ 34.35G); `34G` is + * legal and `35G` is not. + */ +class ConfigParseFqMaxRateOptionValue : SimpleGrammarOptionValues( + "config_parse_fq_max_rate", + SequenceCombinator(parseSize(1000, maxInclusive = SIZE_UINT32_MAX * BigInteger.valueOf(8) + BigInteger.valueOf(7)), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqUsecOptionValue.kt new file mode 100644 index 00000000..d48539d4 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqUsecOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [FairQueueing] CEThresholdSec= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#CEThresholdSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq.c#L284-L342 config_parse_fq_usec (QDISC_KIND_FQ) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L83 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq.network#L20 CEThresholdSec=100ms + */ + +/** + * Validator for [FairQueueing] CEThresholdSec=. + * + * C function: config_parse_fq_usec (QDISC_KIND_FQ). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A time span as parse_sec() reads it (see [parseSec]) that must not exceed UINT32_MAX microseconds + * (about 71min 34.967s): above it the parser logs "Specified 'CEThresholdSec=' is too large, ignoring + * assignment". In particular `infinity` (USEC_INFINITY) is rejected — leave the value empty to get the + * "unset/infinity" default. + */ +class ConfigParseFqUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_fq_usec", + SequenceCombinator(parseSec(UINT32_MAX_USEC), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt new file mode 100644 index 00000000..941564c0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetdevLinkCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkIsMulticast + +/* + * [GENEVE] Remote= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Remote= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/geneve.c#L124-L163 config_parse_geneve_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L103-L113 in_addr_is_multicast + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[GENEVE] Remote=` (.netdev). + * + * C function: config_parse_geneve_address: an IPv4 or IPv6 literal (in_addr_from_string_auto → + * inet_pton) that is not a multicast address (224.0.0.0/4, ff00::/8). An invalid value is logged and + * ignored by systemd. + */ +class ConfigParseGeneveAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_geneve_address", + SequenceCombinator(NetdevLinkCheckedSpan(IP_ADDR, "not multicast") { !netdevLinkIsMulticast(it) }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredBoolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredBoolOptionValue.kt new file mode 100644 index 00000000..7fa893b7 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredBoolOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [GenericRandomEarlyDetection] GenericRIO= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#GenericRIO= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/gred.c#L134-L177 config_parse_gred_bool (QDISC_KIND_GRED) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L48-L64 parse_tristate_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (STRCASE_IN_SET: case-insensitive) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L13-L37 TEST(parse_boolean) — "YES", "TRUE", "ON" accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-gred.network#L14 GenericRIO=yes + */ + +/** + * Validator for [GenericRandomEarlyDetection] GenericRIO=. + * + * C function: config_parse_gred_bool (QDISC_KIND_GRED). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A boolean via parse_tristate(): any parse_boolean() spelling (`yes`/`no`/`1`/`0`/`true`/`false`/ + * `on`/`off`/`y`/`n`/`t`/`f`), case-insensitively. Empty resets to "unset" (handled before the grammar). + */ +class ConfigParseGredBoolOptionValue : SimpleGrammarOptionValues( + "config_parse_gred_bool", + SequenceCombinator(BOOLEAN, EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredU32OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredU32OptionValue.kt new file mode 100644 index 00000000..fbf483a5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredU32OptionValue.kt @@ -0,0 +1,49 @@ +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.* + +/* + * [GenericRandomEarlyDetection] VirtualQueues=, DefaultVirtualQueue= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#VirtualQueues= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DefaultVirtualQueue= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/gred.c#L68-L133 config_parse_gred_u32 (QDISC_KIND_GRED) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/pkt_sched.h#L305 MAX_DPs = 16 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-gred.network#L12-L13 VirtualQueues=12, DefaultVirtualQueue=10 + */ + +/** + * Validator for [GenericRandomEarlyDetection] VirtualQueues=, DefaultVirtualQueue=. + * + * C function: config_parse_gred_u32 (QDISC_KIND_GRED). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned integer read with safe_atou32 (base 0: decimal, `0x` hex, `0o`/leading-zero octal, `0b` + * binary, optional `+`; `0b+1` and `0b 1` are 1, see [SafeAtoTerminal]) that must not exceed MAX_DPs (16). Above 16 the parser logs "Invalid ..., ignoring + * assignment" (although, at 0f0682b773, it still stores the value — a C bug; the kernel refuses more than + * 16 DPs anyway), so it is flagged here. + * + * Lenient: both keys share the parser, so 0..16 is accepted for both, although VirtualQueues=0 and + * DefaultVirtualQueue=16 always fail generic_random_early_detection_verify (DefaultVirtualQueue= must + * be less than VirtualQueues=). The man page documents VirtualQueues= as 1…16. + */ +class ConfigParseGredU32OptionValue : SimpleGrammarOptionValues( + "config_parse_gred_u32", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 16L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassRateOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassRateOptionValue.kt new file mode 100644 index 00000000..4f027c87 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassRateOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [HierarchyTokenBucketClass] Rate=, CeilRate= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Rate= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#CeilRate= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/htb.c#L378-L436 config_parse_htb_class_rate (TCLASS_KIND_HTB) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L19-L20 Rate=1M, CeilRate=0.5M + */ + +/** + * Validator for [HierarchyTokenBucketClass] Rate=, CeilRate=. + * + * C function: config_parse_htb_class_rate (TCLASS_KIND_HTB). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A rate in bits per second as parse_size(rvalue, 1000) reads it (see [parseSize]): `1M`, `0.5M`, + * `1G 500M`, `125000`. The result is divided by 8 and stored as a uint64; no further bound. + */ +class ConfigParseHtbClassRateOptionValue : SimpleGrammarOptionValues( + "config_parse_htb_class_rate", + SequenceCombinator(parseSize(1000), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassU32OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassU32OptionValue.kt new file mode 100644 index 00000000..da722eda --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassU32OptionValue.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.* +import java.math.BigInteger + +/* + * [HierarchyTokenBucketClass] Priority= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Priority= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/htb.c#L242-L293 config_parse_htb_class_u32 (TCLASS_KIND_HTB) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L18 Priority=1 + */ + +/** + * Validator for [HierarchyTokenBucketClass] Priority=. + * + * C function: config_parse_htb_class_u32 (TCLASS_KIND_HTB). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned 32-bit integer read with safe_atou32 (base 0, so `0b+1` and `0b 1` are 1; see + * [SafeAtoTerminal]), 0…4294967295. No further bound. + */ +class ConfigParseHtbClassU32OptionValue : SimpleGrammarOptionValues( + "config_parse_htb_class_u32", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbDefaultClassOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbDefaultClassOptionValue.kt new file mode 100644 index 00000000..9ab738f3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbDefaultClassOptionValue.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.* +import java.math.BigInteger + +/* + * [HierarchyTokenBucket] DefaultClass= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DefaultClass= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/htb.c#L48-L98 config_parse_htb_default_class (QDISC_KIND_HTB) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L61-L64 safe_atou32_full(rvalue, 16, ..) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (explicit base: mangle_base is skipped) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L12 DefaultClass=30 (i.e. 0x30) + */ + +/** + * Validator for [HierarchyTokenBucket] DefaultClass=. + * + * C function: config_parse_htb_default_class (QDISC_KIND_HTB). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * The default class's minor id, always read as HEXADECIMAL (safe_atou32_full(.., 16)): `30` is 0x30, `0x30` + * is the same, `ff` is legal, `0b1` is read as hex 0xb1, and `0o7` is rejected (with an explicit base, + * mangle_base does not run). strtoul skips a leading vertical tab, so `\v5` is 5 (see [SafeAtoTerminal]). + * Range 0…0xffffffff. + */ +class ConfigParseHtbDefaultClassOptionValue : SimpleGrammarOptionValues( + "config_parse_htb_default_class", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 16, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbU32OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbU32OptionValue.kt new file mode 100644 index 00000000..5470f63a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbU32OptionValue.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.* +import java.math.BigInteger + +/* + * [HierarchyTokenBucket] RateToQuantum= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RateToQuantum= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/htb.c#L100-L150 config_parse_htb_u32 (QDISC_KIND_HTB) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L13 RateToQuantum=20 + */ + +/** + * Validator for [HierarchyTokenBucket] RateToQuantum=. + * + * C function: config_parse_htb_u32 (QDISC_KIND_HTB). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned 32-bit integer read with safe_atou32 (base 0, so `0b+1` and `0b 1` are 1; see + * [SafeAtoTerminal]), 0…4294967295. No further bound. + */ +class ConfigParseHtbU32OptionValue : SimpleGrammarOptionValues( + "config_parse_htb_u32", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseId128OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseId128OptionValue.kt new file mode 100644 index 00000000..b64c4969 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseId128OptionValue.kt @@ -0,0 +1,46 @@ +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.ID128_URN_PREFIX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Labeled +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NONZERO_ID128 +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NONZERO_ID128_UUID +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Role +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Exec] MachineID= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#MachineID= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--uuid= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1190-L1218 config_parse_id128 (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-id128/id128-util.c#L25-L40 id128_from_string_nonzero + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-id128/sd-id128.c#L60-L112 sd_id128_from_string + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-id128/test-id128.c#L68-L97 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L522 + */ + +/** + * Validator for the .nspawn `[Exec] MachineID=` setting (config_parse_id128, ltype 0; no other gperf + * table uses this parser). + * + * id128_from_string_nonzero() hands the value to sd_id128_from_string(), which accepts either 32 hex + * digits or the dashed 8-4-4-4-12 UUID form (hex digits in any case), optionally behind a + * case-insensitive `urn:uuid:` prefix — which then requires the dashed form. The all-zero ID is + * refused (logged and ignored). + */ +class ConfigParseId128OptionValue : SimpleGrammarOptionValues( + "config_parse_id128", + SequenceCombinator( + Labeled( + Role.LITERAL, + AlternativeCombinator( + SequenceCombinator(ID128_URN_PREFIX, NONZERO_ID128_UUID), + NONZERO_ID128, + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIecSizeLongOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIecSizeLongOptionValue.kt new file mode 100644 index 00000000..1d2bb94c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIecSizeLongOptionValue.kt @@ -0,0 +1,28 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.IEC_SIZE + +/* + * [Socket] MessageQueueMessageSize= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#MessageQueueMessageSize= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1004-L1032 config_parse_iec_size_long (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L111 parse_size cases (1024 block) + */ + +/** + * Validator for config_parse_iec_size_long, used only by `[Socket] MessageQueueMessageSize=`. + * + * A parse_size(…, 1024) byte size: `8192`, `8K`, `1.5M`, `3. K`, `4 M 11.5K`, `3.5G3B` — see + * [IEC_SIZE]. Suffixes are upper-case B/K/M/G/T/P/E only. systemd additionally requires the result to + * fit in a long and the suffixes of a multi-part size to be strictly descending; neither is checked + * here. + * + * The C parser has no empty-value case; the plugin does not validate empty assignments. + */ +class ConfigParseIecSizeLongOptionValue : SimpleGrammarOptionValues( + "config_parse_iec_size_long", + IEC_SIZE +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt new file mode 100644 index 00000000..b4b05b86 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] Alias= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Alias= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L1944-L1980 config_parse_ifalias (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/if.h#L33 IFALIASZ (256) + */ + +/** + * Validator for `[Link] Alias=` (.link). + * + * C function: config_parse_ifalias. The value is free text, but it must be ASCII (ascii_is_valid) and + * shorter than IFALIASZ = 256 bytes, so at most 255 characters. Anything containing a non-ASCII + * character (`Café uplink`) or longer than that is logged and ignored by udev. An empty assignment + * clears the alias. + */ +class ConfigParseIfaliasOptionValue : SimpleGrammarOptionValues( + "config_parse_ifalias", + SequenceCombinator(RegexTerminal("[\\s\\S]+", "[\\x00-\\x7F]{1,255}"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt new file mode 100644 index 00000000..a3f6805b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * [Link] AlternativeName= (.link) and [HSR] Ports= (.netdev). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#AlternativeName= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Ports= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1651-L1702 config_parse_ifnames (IFNAME_VALID_ALTERNATIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-hsr.netdev#L7 Ports=test1 dummy98 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-util.c#L29 TEST(ifname_valid) + */ + +/** + * Validator for `[Link] AlternativeName=` (.link) and `[HSR] Ports=` (.netdev). + * + * C function: config_parse_ifnames with ltype IFNAME_VALID_ALTERNATIVE: a whitespace-separated list + * (extract_first_word, no unquoting) of interface names, each checked by ifname_valid_full with + * IFNAME_VALID_ALTERNATIVE — up to 127 printable ASCII characters other than `:`, `/` and `%`, not + * entirely digits, and not `.`, `..`, `all` or `default`. See [ALTERNATIVE_INTERFACE_NAME]. An + * invalid name is logged and skipped; an empty assignment clears the list. + */ +class ConfigParseIfnamesOptionValue : SimpleGrammarOptionValues( + "config_parse_ifnames", + SequenceCombinator( + ALTERNATIVE_INTERFACE_NAME, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), ALTERNATIVE_INTERFACE_NAME)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIgnoreCarrierLossOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIgnoreCarrierLossOptionValue.kt new file mode 100644 index 00000000..beb82af7 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIgnoreCarrierLossOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SEC_TIMESPAN + +/* + * [Network] IgnoreCarrierLoss= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IgnoreCarrierLoss= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1096-L1138 config_parse_ignore_carrier_loss (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289-L1291 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L82 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-bridge99-ignore-carrier-loss.network#L9 + */ + +/** + * Validator for `[Network] IgnoreCarrierLoss=`: a boolean (case-insensitive; yes = infinity, no = 0), or + * a time span as parse_sec() reads it (`5s`, `1min 30s`, `1h30min`, `.5s`, `infinity`) — see + * [PARSE_SEC_TIMESPAN]. + */ +class ConfigParseIgnoreCarrierLossOptionValue : SimpleGrammarOptionValues( + "config_parse_ignore_carrier_loss", + AlternativeCombinator( + SequenceCombinator(BOOLEAN, EOF()), + SequenceCombinator(PARSE_SEC_TIMESPAN, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseImportCredentialOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseImportCredentialOptionValue.kt new file mode 100644 index 00000000..d892495c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseImportCredentialOptionValue.kt @@ -0,0 +1,53 @@ +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.COLON +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.ZeroOrOne + +/* + * [Service]/[Socket]/[Mount]/[Swap] ImportCredential=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#ImportCredential=GLOB + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L4868-L4926 config_parse_import_credential (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (':' with EXTRACT_DONT_COALESCE_SEPARATORS; no unquoting) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L55 credential_glob_valid (glob, after unit_cred_printf) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L49 credential_name_valid (rename; filename_is_valid && fdname_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/fd-util.c#L554 fdname_is_valid (ASCII 0x20-0x7E, no ':', <= 255) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-creds.c#L301-L328 TEST(credential_glob_valid) + */ + +// A credential glob: printable ASCII without '/' or ':' and without the glob characters `*?[]`, except +// one trailing '*'; a bare '*' matches everything. The prefix must itself be a valid name, so '.', '..', +// '.*' and '..*' are refused. +private val CREDENTIAL_GLOB = RegexTerminal( + "[^:]+", + """\*|(?!\.\.?\*?(?::|$))[\x20-\x7E&&[^/:*?\[\]]]{1,255}\*?(?![^:])""", +) + +// The rename target: a credential name, checked verbatim (no specifier expansion), and — being the +// whole remainder of the value — it may not contain another ':'. +private val CREDENTIAL_RENAME = RegexTerminal("[^:]+", """(?!\.\.?$)[\x20-\x7E&&[^/:]]{1,255}(?![^:])""") + +/** + * Validator for `ImportCredential=` (config_parse_import_credential, ltype 0). + * + * `GLOB[:NAME]`. GLOB is a credential name optionally ending in a single `*` (or just `*`); NAME, if + * present, is the credential name to rename matches to (`foo.*:bar.` is the documented prefix-rename + * form). The value is not split on whitespace and not unquoted — credential names may contain spaces + * — and an empty NAME (`foo:`) is accepted. Credential names are printable ASCII (0x20–0x7E) without + * `/` or `:`, at most 255 characters, and not `.` or `..`. + * + * Lenient corner: `%` specifiers in GLOB are not checked (they are ordinary characters as far as the + * name rules go). + */ +class ConfigParseImportCredentialOptionValue : SimpleGrammarOptionValues( + "config_parse_import_credential", + SequenceCombinator( + CREDENTIAL_GLOB, + ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(CREDENTIAL_RENAME))), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt new file mode 100644 index 00000000..0dd53b0c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsNull +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes + +/* + * [Network] IPv6StableSecretAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6StableSecretAddress= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2036-L2082 config_parse_in_addr_non_null (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for config_parse_in_addr_non_null with ltype AF_INET6 — `[Network] + * IPv6StableSecretAddress=` (.network). + * + * The value goes through in_addr_from_string(AF_INET6), i.e. inet_pton(AF_INET6): an IPv6 literal, no + * prefix length and no `%zone`. An IPv4 literal is not accepted. The parser then refuses the + * all-zero address (`::`, `0:0:0:0:0:0:0:0`, …) with "cannot be the ANY address", which is flagged + * here as well. + */ +class ConfigParseInAddrNonNullIpv6OptionValue : SimpleGrammarOptionValues( + "config_parse_in_addr_non_null", + SequenceCombinator( + Checked(IPV6_ADDR) { text -> ipLiteralBytes(text)?.let { !ipIsNull(it) } ?: false }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInaccessibleOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInaccessibleOptionValue.kt new file mode 100644 index 00000000..139c4fd3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInaccessibleOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Files] Inaccessible= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Inaccessible= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--inaccessible= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L403-L427 config_parse_inaccessible (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-mount.c#L387-L408 inaccessible_mount_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.h#L30-L35 path_is_absolute + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L542-L543 + */ + +/** + * Validator for the .nspawn `[Files] Inaccessible=` setting (config_parse_inaccessible, ltype 0). + * + * inaccessible_mount_parse() takes the whole value verbatim — no splitting, unquoting or unescaping, so + * spaces, colons and backslashes are ordinary characters — and only requires path_is_absolute(), i.e. a + * leading `/`. A relative path is logged and ignored. + */ +class ConfigParseInaccessibleOptionValue : SimpleGrammarOptionValues( + "config_parse_inaccessible", + SequenceCombinator( + RegexTerminal("""[\s\S]+""", """/[\s\S]*"""), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInterfacePairOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInterfacePairOptionValues.kt new file mode 100644 index 00000000..49e02e1b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInterfacePairOptionValues.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.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_DELIMITED_INTERFACE_NAME +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.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 + +/* + * [Network] Interface= / MACVLAN= / IPVLAN= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Interface= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#MACVLAN= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L481-L500 config_parse_network_iface_pair (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L502-L521 config_parse_macvlan_iface_pair (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L523-L542 config_parse_ipvlan_iface_pair (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-network.c#L999-L1045 network_iface_pair_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L553-L555 + * linux https://man7.org/linux/man-pages/man7/netdevice.7.html + */ + +/** `HOST[:CONTAINER]`, where an empty CONTAINER after the colon means "not given". */ +private val INTERFACE_PAIR = SequenceCombinator( + COLON_DELIMITED_INTERFACE_NAME, + ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(COLON_DELIMITED_INTERFACE_NAME))), +) + +private val INTERFACE_PAIR_LIST: Combinator = SequenceCombinator( + INTERFACE_PAIR, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), INTERFACE_PAIR)), + EOF(), +) + +/** + * The shared grammar of the .nspawn `[Network] Interface=`, `MACVLAN=` and `IPVLAN=` settings, whose + * three parsers are thin wrappers around network_iface_pair_parse() (they differ only in the `mv-` / + * `iv-` prefix used when the container-side name is omitted). + * + * The value is a whitespace-separated list (extract_first_word with WHITESPACE). Each word is split once + * at the first `:`: the host-side name must pass ifname_valid() (printable ASCII except `:`, `/`, `%`; at + * most 15 bytes; not all digits, `.`, `..`, `all` or `default`, and nothing parse_ifindex() reads as a + * positive index such as `0x10` or `+5`), and the container-side name, if present and non-empty, must + * too — so `eth0:` is the same as `eth0`, and a second colon is an invalid name. Unlike most nspawn + * settings an invalid word is a hard error that fails loading the file. + * + * Backslash escapes: each word is unescaped twice (whitespace split, then colon split), so a name that + * runs into whitespace after a backslash run that is not a multiple of 4 long is refused, as C refuses it — + * `eth0\ x` is one invalid name `eth0 x`, and `eth0\\ x` leaves a trailing backslash for the colon split. + * + * The prefixed default name (`mv-eth0`) may exceed 15 bytes; nspawn shortens it later, so the + * host-side name alone is what is length-checked. + */ +open class ConfigParseInterfacePairOptionValue(validatorName: String) : SimpleGrammarOptionValues(validatorName, INTERFACE_PAIR_LIST) + +/** `[Network] Interface=` — config_parse_network_iface_pair (0). */ +class ConfigParseNetworkIfacePairOptionValue : ConfigParseInterfacePairOptionValue("config_parse_network_iface_pair") + +/** `[Network] MACVLAN=` — config_parse_macvlan_iface_pair (0). */ +class ConfigParseMacvlanIfacePairOptionValue : ConfigParseInterfacePairOptionValue("config_parse_macvlan_iface_pair") + +/** `[Network] IPVLAN=` — config_parse_ipvlan_iface_pair (0). */ +class ConfigParseIpvlanIfacePairOptionValue : ConfigParseInterfacePairOptionValue("config_parse_ipvlan_iface_pair") diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt new file mode 100644 index 00000000..b75098ef --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [IPoIB] PartitionKey= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PartitionKey= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/ipoib.c#L100-L140 config_parse_ipoib_pkey (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L599-L613 safe_atou16_full (base 0) + */ + +/** + * Validator for `[IPoIB] PartitionKey=` (.netdev). + * + * C function: config_parse_ipoib_pkey. A uint16 read with safe_atou16() (so `0x8001` works, which is + * how partition keys are usually written) in 1…0xffff, except 0x8000 (the full-membership bit with an + * empty key). Spelled as two ranges either side of 0x8000. An empty assignment unsets it; an invalid + * value is logged and ignored by systemd. + */ +class ConfigParseIpoibPkeyOptionValue : SimpleGrammarOptionValues( + "config_parse_ipoib_pkey", + SequenceCombinator( + AlternativeCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 0x7fffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0x8001L, + maxInclusive = 0xffffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llAddressOptionValue.kt new file mode 100644 index 00000000..6b213a61 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llAddressOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DOT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Labeled +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Role +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] IPv4LLStartAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv4LLStartAddress= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-ipv4ll.c#L389-L429 config_parse_ipv4ll_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton, AF_INET) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L49-L61 in4_addr_is_link_local_dynamic + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-with-ipv4ll.network#L9 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[Network] IPv4LLStartAddress=`: an IPv4 address usable for dynamic IPv4 link-local + * assignment — inside 169.254.0.0/16 but outside its first and last /24 (RFC 3927 §2.1), i.e. + * 169.254.1.0 … 169.254.254.255. + * + * Every octet matches any number so a wrong address still highlights as an address; the ranges are + * then enforced per octet. + */ +class ConfigParseIpv4llAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv4ll_address", + SequenceCombinator( + Labeled( + Role.LITERAL, + SequenceCombinator( + IntegerTerminal(169, 170), DOT, IntegerTerminal(254, 255), DOT, IntegerTerminal(1, 255), DOT, IntegerTerminal(0, 256), + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llOptionValue.kt new file mode 100644 index 00000000..722e0840 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llOptionValue.kt @@ -0,0 +1,23 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] IPv4LL= (.network; deprecated in favour of LinkLocalAddressing=). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#LinkLocalAddressing= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-ipv4ll.c#L348-L387 config_parse_ipv4ll (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + */ + +/** + * Validator for the deprecated `[Network] IPv4LL=`: a boolean (parse_boolean, case-insensitive). systemd + * logs a deprecation warning for every assignment; the value itself is still applied. + */ +class ConfigParseIpv4llOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv4ll", + SequenceCombinator(BOOLEAN, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt new file mode 100644 index 00000000..258aac7d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt @@ -0,0 +1,36 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [IPv6AddressLabel] Label= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Label= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L419-L461 config_parse_ipv6_address_label_section (IPV6_ADDRESS_LABEL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L386-L417 config_parse_ipv6_address_label + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[IPv6AddressLabel] Label=` (.network): safe_atou32() — decimal, `0x` hex, `0o`/leading + * zero octal, `0b` binary — in 0…4294967294. 4294967295 (0xffffffff) is reserved and refused. The value is + * whole, so whitespace after a `0b`/`0o` prefix (`0b 1`) and a leading `\v` reach safe_atou32 and are fine. + */ +class ConfigParseIpv6AddressLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_address_label_section", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt new file mode 100644 index 00000000..f0470d82 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_PREFIX_SAFE_LEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes + +/* + * [IPv6AddressLabel] Prefix= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6AddressLabel%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L419-L461 config_parse_ipv6_address_label_section (IPV6_ADDRESS_LABEL_PREFIX) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L346-L384 config_parse_ipv6_address_label_prefix + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L799-L843 in_addr_prefix_from_string (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L165-L171 in6_addr_is_ipv4_mapped_address + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html (kernel net/ipv6/addrlabel.c ip6addrlbl_alloc) + */ + +/** True for an IPv4-mapped address (`::ffff:a.b.c.d`) whose prefix length exceeds 96. */ +private fun isMappedTooLong(text: String): Boolean { + val slash = text.indexOf('/') + val bytes = ipLiteralBytes(if (slash >= 0) text.substring(0, slash) else text) ?: return false + val mapped = (0 until 10).all { bytes[it].toInt() == 0 } && bytes[10].toInt() == -1 && bytes[11].toInt() == -1 + // The same safe_atou8 reading as the length in IPV6_PREFIX_SAFE_LEN, so `/0b1100001` counts as 97. + val length = if (slash >= 0) { + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(text.substring(slash + 1))?.toInt() ?: return false + } else { + 128 + } + return mapped && length > 96 +} + +/** + * Validator for `[IPv6AddressLabel] Prefix=` (.network). + * + * An IPv6 prefix as in_addr_prefix_from_string(AF_INET6) reads it — an IPv6 literal with an optional + * `/LEN` in 0…128 (no slash means /128). config_parse_ipv6_address_label_prefix then refuses an + * IPv4-mapped address (`::ffff:0:0/96`) with a prefix length above 96, mirroring the kernel's + * ip6addrlbl_alloc(); note that this includes an IPv4-mapped address written without a slash. + */ +class ConfigParseIpv6AddressLabelPrefixOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_address_label_section", + SequenceCombinator(Checked(IPV6_PREFIX_SAFE_LEN) { !isMappedTooLong(it) }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt new file mode 100644 index 00000000..2b7c51d3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt @@ -0,0 +1,43 @@ +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.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Tunnel] IPv6FlowLabel= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#IPv6FlowLabel= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/tunnel.c#L907-L942 config_parse_ipv6_flowlabel (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2151-L2182 config_parse_unsigned_bounded → safe_atou_bounded + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Tunnel] IPv6FlowLabel=` (.netdev). + * + * C function: config_parse_ipv6_flowlabel. Either `inherit` (copy the inner packet's flow label) or a + * 20-bit flow label 0…0xFFFFF read by config_parse_uint32_bounded (safe_atou, so hex works, as the man + * page's own range notation suggests). An out-of-range value is logged and ignored by systemd. + */ +class ConfigParseIpv6FlowlabelOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_flowlabel", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("inherit"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 0xfffffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt new file mode 100644 index 00000000..ac5afd41 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] IRQAffinityNUMA= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinityNUMA= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2157-L2201 config_parse_irq_affinity_numa (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.h#L35-L36 IRQ_AFFINITY_NUMA_UNSET / IRQ_AFFINITY_NUMA_LOCAL + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Link] IRQAffinityNUMA=` (.link). + * + * C function: config_parse_irq_affinity_numa. Either `local` (the device's own NUMA node) or a NUMA + * node number read with safe_atou() (so hex works). UINT_MAX and UINT_MAX - 1 are reserved as the + * "unset" and "local" markers, so the number must be at most 4294967293. An empty assignment unsets it. + * An out-of-range number is logged and ignored by udev. + */ +class ConfigParseIrqAffinityNumaOptionValue : SimpleGrammarOptionValues( + "config_parse_irq_affinity_numa", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("local"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_293L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt new file mode 100644 index 00000000..726caac6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] IRQAffinityPolicy= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinityPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2209-L2213 config_parse_irq_affinity_policy (0), DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2203-L2206 irq_affinity_policy_table + */ + +/** + * Validator for `[Link] IRQAffinityPolicy=` (.link). + * + * C function: config_parse_irq_affinity_policy, generated by DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT over + * irq_affinity_policy_table: exactly `single` or `spread` (case-sensitive). An empty assignment unsets + * it. An invalid value is logged and ignored by udev. + */ +class ConfigParseIrqAffinityPolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_irq_affinity_policy", + SequenceCombinator(FlexibleLiteralChoiceTerminal("single", "spread"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseJobTimeoutSecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseJobTimeoutSecOptionValue.kt new file mode 100644 index 00000000..6ebad830 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseJobTimeoutSecOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.TIMESPAN + +/* + * [Unit] JobTimeoutSec= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#JobTimeoutSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5594-L5630 config_parse_job_timeout_sec (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1293 parse_sec_fix_0 (parse_sec, then 0 means infinity) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L85 TEST(parse_sec_fix_0) + */ + +/** + * Validator for `[Unit] JobTimeoutSec=` (config_parse_job_timeout_sec, the only key using it). + * + * The value goes straight to parse_sec_fix_0() — no specifier expansion — so it is a plain + * [TIMESPAN]: `infinity`, `0` (which this parser also turns into infinity), `90`, `5min 20s`, `1h30min`. + * + * There is no special case for an empty value in the C parser: parse_sec("") fails and the assignment + * is ignored with a warning. The plugin does not run value validators on empty assignments, so that + * case is not reported here. + */ +class ConfigParseJobTimeoutSecOptionValue : SimpleGrammarOptionValues( + "config_parse_job_timeout_sec", + TIMESPAN +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt new file mode 100644 index 00000000..c063fc5f --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [L2TPSession] Name= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Name= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L727-L765 config_parse_l2tp_session_name (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0, via ifname_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-util.c#L29 TEST(ifname_valid) + */ + +/** + * Validator for `[L2TPSession] Name=` (.netdev). + * + * C function: config_parse_l2tp_session_name, which requires ifname_valid(): 1…15 printable ASCII + * characters other than `:`, `/` and `%`, not entirely digits, and not `.`, `..`, `all` or + * `default`. See [INTERFACE_NAME]. An invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `Name=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. + */ +class ConfigParseL2tpSessionNameOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_session_name", + SequenceCombinator(INTERFACE_NAME, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt new file mode 100644 index 00000000..94ce597e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [L2TP] Local= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Local= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L477-L569 config_parse_l2tp_tunnel_local_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L38-L42 l2tp_local_address_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("@", no flags; leading-separator skip at #L39-L56) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + */ + +/** + * Validator for `[L2TP] Local=` (.netdev). + * + * C function: config_parse_l2tp_tunnel_local_address. `ADDRESS[@INTERFACE]`, where ADDRESS is one of + * the l2tp_local_address_type_table names `auto`, `static`, `dynamic` or an IPv4/IPv6 literal, and the + * optional interface (everything after the first `@`) must pass ifname_valid_full with + * IFNAME_VALID_ALTERNATIVE (up to 127 characters). + * + * The `@` is cut with extract_first_word(…, "@", 0), which coalesces a run of separators, so `auto@@eth0` + * and even a trailing `auto@` (no interface) are accepted; it also skips leading separators, so `@auto` + * is `auto` (a lone `@` is an empty word and refused). Not modelled: systemd refuses the null + * address and an address family that conflicts with `Remote=`. An invalid value is logged and ignored. + */ +class ConfigParseL2tpTunnelLocalAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_tunnel_local_address", + SequenceCombinator( + // extract_first_word() skips leading separators, so `@auto` reads as `auto`. + ZeroOrOne(RegexTerminal("@+", "@+")), + AlternativeCombinator( + IPV6_ADDR, + IPV4_ADDR, + FlexibleLiteralChoiceTerminal("auto", "static", "dynamic"), + ), + ZeroOrOne(SequenceCombinator(RegexTerminal("@+", "@+"), ZeroOrOne(ALTERNATIVE_INTERFACE_NAME))), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt new file mode 100644 index 00000000..97aa2f17 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt @@ -0,0 +1,28 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [L2TP] Remote= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Remote= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L571-L623 config_parse_l2tp_tunnel_remote_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[L2TP] Remote=` (.netdev). + * + * C function: config_parse_l2tp_tunnel_remote_address: an IPv4 or IPv6 literal (in_addr_from_string_auto + * → inet_pton). Not modelled: systemd also refuses the null address (`0.0.0.0`, `::`) and an address + * whose family differs from an earlier `Local=`; both depend on the parsed value or on other keys. An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseL2tpTunnelRemoteAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_tunnel_remote_address", + SequenceCombinator(IP_ADDR, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkGroupOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkGroupOptionValue.kt new file mode 100644 index 00000000..e235d344 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkGroupOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] Group= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Group= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1054-L1094 config_parse_link_group (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497-L520 safe_atoi (safe_atoi32, base 0 + mangle_base) + */ + +/** + * Validator for `[Link] Group=` (config_parse_link_group): an integer 0…2147483647, read by safe_atoi32 + * — so `0x10`, `0o17`, `0b101`, `010` (octal) and `+5` are all legal, and negatives are refused by the + * explicit `group < 0` check. + */ +class ConfigParseLinkGroupOptionValue : SimpleGrammarOptionValues( + "config_parse_link_group", + SequenceCombinator( + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 2_147_483_647L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkJournalOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkJournalOptionValue.kt new file mode 100644 index 00000000..fdfe54eb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkJournalOptionValue.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.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Exec] LinkJournal= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#LinkJournal= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--link-journal= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L888-L910 config_parse_link_journal (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L852-L886 parse_link_journal + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L534 + */ + +/** + * Validator for the .nspawn `[Exec] LinkJournal=` setting (config_parse_link_journal, ltype 0). + * + * parse_link_journal() compares with streq() against `auto`, `guest`, `host`, `try-guest` and + * `try-host` (case-sensitive), and otherwise falls back to parse_boolean() (case-insensitive; true is + * treated as `auto`, false as `no`). Invalid input is logged and ignored. The names come first so that + * the case-insensitive [BOOLEAN] cannot strand part of one. + */ +class ConfigParseLinkJournalOptionValue : SimpleGrammarOptionValues( + "config_parse_link_journal", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("auto", "guest", "host", "try-guest", "try-host"), + BOOLEAN, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLldpMulticastModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLldpMulticastModeOptionValue.kt new file mode 100644 index 00000000..1891dd2d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLldpMulticastModeOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] EmitLLDP= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#EmitLLDP= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-lldp-tx.c#L134-L174 config_parse_lldp_multicast_mode (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-lldp-tx.c#L126-L132 lldp_multicast_mode_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/23-emit-lldp.network#L7 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/26-bridge-vlan-slave-issue-20373.network#L9 + */ + +/** + * Validator for `EmitLLDP=` (config_parse_lldp_multicast_mode): a boolean (case-insensitive; true means + * `nearest-bridge`) or one of `nearest-bridge`, `non-tpmr-bridge`, `customer-bridge` (case-sensitive). + */ +class ConfigParseLldpMulticastModeOptionValue : SimpleGrammarOptionValues( + "config_parse_lldp_multicast_mode", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("nearest-bridge", "non-tpmr-bridge", "customer-bridge"), + BOOLEAN, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLongOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLongOptionValue.kt new file mode 100644 index 00000000..6aa7bad9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLongOptionValue.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Socket] MessageQueueMaxMessages= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#MessageQueueMaxMessages= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L960 config_parse_long (DEFINE_PARSER(long, long, safe_atoli)) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L114 safe_atoli (safe_atolli where long is 64-bit) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L560 safe_atolli (strtoll base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403 mangle_base (0b / 0o prefixes) + * linux https://man7.org/linux/man-pages/man7/mq_overview.7.html + * linux https://man7.org/linux/man-pages/man3/strtol.3.html + */ + +/** + * Validator for config_parse_long, used only by `[Socket] MessageQueueMaxMessages=`. + * + * A signed `long` as safe_atoli() reads it ([SafeAtoTerminal], signed, base 0): optional leading + * whitespace and sign, then decimal, `0x` hex, leading-zero octal, or a `0b`/`0o` prefix — after which + * strtoll starts afresh, so `0b-1` and `0b -1` are -1 (sd-check accepts both) — anything from LONG_MIN + * to LONG_MAX on a 64-bit host. + * The setting itself only means something when positive (socket.c tests `mq_maxmsg > 0`), but the + * parser accepts any long and so does this. + * + * The C parser has no empty-value case (safe_atoli("") fails and the line is ignored with a warning); + * the plugin does not validate empty assignments, so that is not reported. + */ +class ConfigParseLongOptionValue : SimpleGrammarOptionValues( + "config_parse_long", + SequenceCombinator( + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = SafeAtoTerminal.INT64_MIN, + maxInclusive = SafeAtoTerminal.INT64_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLuoSessionsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLuoSessionsOptionValue.kt new file mode 100644 index 00000000..e70e259d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLuoSessionsOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service] LUOSession=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#LUOSession= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6847-L6903 config_parse_luo_sessions (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE, flags 0: `\x` → x, quotes literal) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/luo-util.c#L119 luo_session_name_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/fd-util.c#L554 fdname_is_valid (ASCII 0x20-0x7E, no ':', <= 255) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-util.c#L1154 string_is_safe (STRING_DISALLOW_WHITESPACE: no blanks, backslashes, quotes or `*?[`) + */ + +// One name character: printable ASCII except ':' (fdname), and except backslash, quotes and the glob +// characters `*?[` (string_is_safe). The word is extracted with flags 0, so a raw `\x` contributes x. +private const val LUO_CHAR = """[\x21-\x7E&&[^:\\"'*?\[]]""" + +/** + * Validator for `LUOSession=` (config_parse_luo_sessions, ltype 0). + * + * A whitespace-separated list of session names, each used as an fd-store FDNAME=: 1–255 printable + * ASCII characters, none of them `:`, a backslash, a quote, or `*`, `?`, `[`. Invalid names are logged + * and skipped. Not checked: a name repeated in the same unit ("Duplicate LUO session name, ignoring"). + */ +class ConfigParseLuoSessionsOptionValue : SimpleGrammarOptionValues( + "config_parse_luo_sessions", + whitespaceList(RegexTerminal("""\S+""", """(?:$LUO_CHAR|\\$LUO_CHAR){1,255}(?!\S)""")) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt new file mode 100644 index 00000000..d96abc78 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] MACAddressPolicy= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#MACAddressPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2145-L2149 config_parse_mac_address_policy (0), DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2138-L2142 mac_address_policy_table + */ + +/** + * Validator for `[Link] MACAddressPolicy=` (.link). + * + * C function: config_parse_mac_address_policy, generated by DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT over + * mac_address_policy_table: exactly one of `persistent`, `random` or `none` (case-sensitive). An empty + * assignment selects the default, `none`. An invalid value is logged and ignored by udev. + */ +class ConfigParseMacAddressPolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_mac_address_policy", + SequenceCombinator(FlexibleLiteralChoiceTerminal("persistent", "random", "none"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt new file mode 100644 index 00000000..9d999f8c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [MACsecTransmitAssociation] / [MACsecReceiveAssociation] KeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#KeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/macsec.c#L770-L821 config_parse_macsec_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `KeyFile=` in both `[MACsecTransmitAssociation]` and `[MACsecReceiveAssociation]` (.netdev). + * + * C function: config_parse_macsec_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseMacsecKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_macsec_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt new file mode 100644 index 00000000..8b0af0d6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Repeat +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [MACsecTransmitAssociation] / [MACsecReceiveAssociation] KeyId= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#KeyId= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/macsec.c#L823-L879 config_parse_macsec_key_id (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L109-L158 unhexmem_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/if_macsec.h#L23 MACSEC_KEYID_LEN (16) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-macsec.netdev#L12 KeyId=01 … KeyId=0203040506070809 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-hexdecoct.c#L110 TEST(unhexmem) + */ + +// One byte: two hex digits. unhex_next() skips whitespace around every digit, so a space may even +// split a byte. +private val KEY_ID_BYTE = RegexTerminal("[0-9a-fA-F][ \\t]*[0-9a-fA-F]", "[0-9a-fA-F][ \\t]*[0-9a-fA-F]") + +/** + * Validator for `KeyId=` in `[MACsecTransmitAssociation]` and `[MACsecReceiveAssociation]` (.netdev). + * + * C function: config_parse_macsec_key_id, which decodes the value with unhexmem(): an even number of + * hex digits (case-insensitive, whitespace between digits ignored) encoding at most MACSEC_KEYID_LEN = + * 16 bytes, i.e. 2 to 32 digits. Shorter ids are zero-padded. + * + * The man page still says "Takes a number between 0-255", but the parser is hex-only: `KeyId=1` (odd + * length) and `KeyId=255` are refused, while systemd's own fixtures write `KeyId=01` and + * `KeyId=0203040506070809`. This follows the C. An invalid value is logged and ignored by systemd. + */ +class ConfigParseMacsecKeyIdOptionValue : SimpleGrammarOptionValues( + "config_parse_macsec_key_id", + SequenceCombinator( + KEY_ID_BYTE, + Repeat(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), KEY_ID_BYTE), 0, 15), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt index cf7679ba..4b338096 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt @@ -7,7 +7,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Validator for ManagedOOMMemoryPressureDurationSec=. * * C function: config_parse_managed_oom_mem_pressure_duration_sec in src/core/load-fragment.c. - * Empty resets to USEC_INFINITY. Otherwise calls parse_sec(rvalue) and additionally requires + * Empty resets to NETWORK_ROUTE_ADDR_USEC_INFINITY. Otherwise calls parse_sec(rvalue) and additionally requires * the result to be at least 1s and strictly less than infinity. * * The grammar matches parse_sec syntax (which is the existing TIME_VALUE: "infinity" or a diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt new file mode 100644 index 00000000..fcac5eeb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt @@ -0,0 +1,67 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BANG +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CUNESCAPE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unquoteWord + +/* + * [Match] Property= (.network and .link). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Property= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Property= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/net-condition.c#L344-L403 config_parse_match_property (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L81-L102 env_assignment_is_valid + */ + +// env_name_is_valid_n: one or more of VALID_BASH_ENV_NAME_CHARS (letters, digits, `_`), not starting +// with a digit. +private val ENV_ASSIGNMENT = Regex("""[A-Za-z_][A-Za-z0-9_]*=[\s\S]*""") + +/** + * Validator for `[Match] Property=` in .network and .link files. + * + * config_parse_match_property strips one optional leading `!` (inverting the match) and splits the rest + * with extract_first_word(EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE): quotes group, and a backslash starts a C + * escape (`\"`, `\\`, `\n`, `\x41`, …) both inside and outside quotes — an unknown escape such as `\q`, + * or an unterminated quote, makes the whole assignment "Invalid syntax, ignoring". Each resulting word + * must then satisfy env_assignment_is_valid(): `NAME=VALUE` where NAME is letters, digits and `_` not + * starting with a digit, and VALUE is anything (including empty). A word that fails is logged and + * skipped — flagged here. + * + * The man page's own example is accepted as written: + * `Property=ID_MODEL_ID=9999 "ID_VENDOR_FROM_DATABASE=vendor name" "KEY=with \"quotation\""`. + * + * The NAME check runs on the unquoted word, so quoting the pair, quoting just the value, or even + * quoting the name are all judged exactly as systemd judges them. Lenient corners: `\x00`/`\u0000` + * (refused by cunescape_one as NUL) and UTF-16 surrogates in `\u` escapes are not singled out, and + * VALUE's UTF-8 validity / ARG_MAX length limits are not checked. + */ +class ConfigParseMatchPropertyOptionValue : SimpleGrammarOptionValues( + "config_parse_match_property", + AlternativeCombinator( + // `p += invert` and then extract_first_word() skips leading whitespace, so `! A=b` == `!A=b`. + SequenceCombinator(BANG, ZeroOrOne(WhitespaceTerminal()), PROPERTY_LIST), + // A lone `!` yields no words at all, which is accepted. + SequenceCombinator(BANG, EOF()), + PROPERTY_LIST, + ) +) { + private companion object { + val PROPERTY_WORD = Checked(CUNESCAPE_WORD) { ENV_ASSIGNMENT.matches(unquoteWord(it)) } + val PROPERTY_LIST = SequenceCombinator( + PROPERTY_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), PROPERTY_WORD)), + EOF(), + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt new file mode 100644 index 00000000..dba2916a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAIN_ESCAPE_WORD_LIST + +/* + * [Match] Path= / Driver= / Type= / Kind= (.network and .link), WLANInterfaceType= / SSID= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Path= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Path= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/net-condition.c#L228-L281 config_parse_match_strv (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) + */ + +/** + * Validator for the glob-list `[Match]` settings: `Path=`, `Driver=`, `Type=`, `Kind=` (.network and + * .link) and `WLANInterfaceType=`, `SSID=` (.network). + * + * config_parse_match_strv strips one optional leading `!` (inverting the whole list) and then splits the + * rest with extract_first_word(EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE). Each word is an fnmatch() glob + * stored verbatim — the kind, type and driver names are open-ended (the man page itself says the lists of + * examples are "not comprehensive"), so no word is checked against a list. + * + * What *is* checked is the one thing systemd rejects: an unterminated `"` or `'` quote makes + * extract_first_word fail, and the parser then logs "Invalid syntax, ignoring" and drops the whole + * assignment. Quoted pieces may be glued to bare text (`"pci-0000 00"*`), and because of + * EXTRACT_RETAIN_ESCAPE a backslash is an ordinary character, even inside quotes. The `!` needs no + * special handling: it is an ordinary first character of the first word, and `!` on its own is accepted + * (it yields an empty list). + */ +class ConfigParseMatchStrvOptionValue : SimpleGrammarOptionValues( + "config_parse_match_strv", + RETAIN_ESCAPE_WORD_LIST, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMountImagesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMountImagesOptionValue.kt new file mode 100644 index 00000000..3804aff3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMountImagesOptionValue.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.EXEC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MOUNT_IMAGE_DESTINATION_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MOUNT_IMAGE_SOURCE_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.imageMountOptionsTail +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] MountImages=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#MountImages= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5307-L5455 config_parse_mount_images (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (UNQUOTE|RETAIN_ESCAPE words, then ':' with CUNESCAPE|UNESCAPE_SEPARATORS, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (source/destination specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS_DEV_OK) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721 path_below_api_vfs (/dev, /sys, /proc) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/gpt.c#L174 partition_designator_from_string + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-50-DISSECT.mountfsd.sh#L161 + */ + +/** + * Validator for `MountImages=` (config_parse_mount_images, ltype 0). + * + * A whitespace-separated list of `[-]SOURCE:DESTINATION[:OPTIONS | :PARTITION:OPTIONS…]` words: + * - SOURCE (optionally `-`-prefixed) and DESTINATION are both required, absolute after specifier + * expansion, free of `..`, and not below /proc or /sys (/dev is allowed: DEV_OK); + * - then either a single mount-option string for the root partition, or `PARTITION:OPTIONS` pairs + * (an optional lone trailing option string still applies to root). A pair whose partition name is + * unknown is logged and skipped. + * - ':' separators are coalesced, so `a::b` equals `a:b`; a ':' inside a path is written `\:`. + * + * Lenient corners: quoted runs are opaque, `%` specifiers and the mount options themselves are not + * checked. + */ +class ConfigParseMountImagesOptionValue : SimpleGrammarOptionValues( + "config_parse_mount_images", + whitespaceList( + SequenceCombinator( + ZeroOrOne(EXEC_COALESCED_COLONS), + MOUNT_IMAGE_SOURCE_FIELD, + EXEC_COALESCED_COLONS, + MOUNT_IMAGE_DESTINATION_FIELD, + imageMountOptionsTail(), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMudUrlOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMudUrlOptionValue.kt new file mode 100644 index 00000000..504f609b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMudUrlOptionValue.kt @@ -0,0 +1,43 @@ +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.C_ESCAPE_REGEX +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 + +/* + * [DHCPv4] MUDURL=, [DHCPv6] MUDURL=, [LLDP] MUDURL= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MUDURL= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-util.c#L199-L238 config_parse_mud_url (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/web-util.c#L10-L24 http_url_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one (via cunescape, flags 0) + */ + +// After the scheme: at least one character, every one of them ASCII (ascii_is_valid), with backslashes +// only as valid C escapes (the whole value goes through cunescape first). +private val MUD_URL_REST = RegexTerminal( + """(?:[^\\]|\\[\s\S]?)+""", + """(?:[\x01-\x5B\x5D-\x7F]|$C_ESCAPE_REGEX)+\z""", +) + +/** + * Validator for the three `MUDURL=` settings (config_parse_mud_url). + * + * The value is cunescape()d and must then satisfy http_url_is_valid(): it starts with `http://` or + * `https://` (case-sensitive, STARTSWITH_SET), has something after the scheme, and is pure ASCII. The + * 255-byte limit on the unescaped URL is not modelled, and an escape that decodes to a non-ASCII byte + * (`\xff`) is accepted although systemd would then reject it (both lenient). + */ +class ConfigParseMudUrlOptionValue : SimpleGrammarOptionValues( + "config_parse_mud_url", + SequenceCombinator( + // An exact LiteralChoiceTerminal, not a Flexible one: the flexible shape regex would run on into the + // host name ("http://e") and never match a choice under the list-of-successes engine. + LiteralChoiceTerminal("http://", "https://"), + MUD_URL_REST, + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt new file mode 100644 index 00000000..5c06ad71 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [IPv6AcceptRA] RouteMetric= and the legacy [DHCPv6] RouteMetric= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RouteMetric= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L451-L507 config_parse_ndisc_route_metric (0, AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/systemd-networkd-tests.py#L8124 (RouteMetric=100:200:300) + */ + +// safe_atou32() on the whole value, or on one field of the `:`-separated triple. Either way whitespace +// inside the text reaches safe_atou32, which skips it at the start (`100: 200: 300`) and after a +// `0b`/`0o` prefix (`0b 1`). +private val METRIC = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// extract_many_words(…, EXTRACT_DONT_COALESCE_SEPARATORS) leaves p at "" after a single trailing `:`, +// which passes the isempty(p) check, so `100:200:300:` is accepted too. +// high < medium < low, compared on the parsed numbers. +private val METRIC_TRIPLE = Checked(SequenceCombinator(METRIC, COLON, METRIC, COLON, METRIC, ZeroOrOne(COLON))) { text -> + val v = text.removeSuffix(":").split(':').map { METRIC.valueOf(it) } + v.size == 3 && v.all { it != null } && v[0]!! < v[1]!! && v[1]!! < v[2]!! +} + +/** + * Validator for config_parse_ndisc_route_metric: `[IPv6AcceptRA] RouteMetric=` (ltype 0) and the legacy + * `[DHCPv6] RouteMetric=` alias (ltype AF_INET6; the function ignores its ltype). + * + * Either one safe_atou32() metric used for all three router preferences, or `HIGH:MEDIUM:LOW` — exactly + * three safe_atou32() fields split on `:` — which systemd only accepts when strictly increasing + * (HIGH < MEDIUM < LOW, e.g. the default `512:1024:2048`). An empty field, a fourth field or a + * non-increasing triple is ignored with a warning and flagged here. + */ +class ConfigParseNdiscRouteMetricOptionValue : SimpleGrammarOptionValues( + "config_parse_ndisc_route_metric", + SequenceCombinator(AlternativeCombinator(METRIC_TRIPLE, METRIC), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt new file mode 100644 index 00000000..33ad8a2a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Neighbor] Address= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BNeighbor%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-neighbor.c#L747-L781 config_parse_neighbor_section (NEIGHBOR_DESTINATION_ADDRESS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2084-L2112 config_parse_in_addr_data + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[Neighbor] Address=` (.network) — the NEIGHBOR_DESTINATION_ADDRESS slot of + * config_parse_neighbor_section, whose table entry is config_parse_in_addr_data. + * + * That reads one address of either family with in_addr_from_string_auto (inet_pton(AF_INET), then + * inet_pton(AF_INET6)): no prefix length and no `%zone`. Unlike some address settings it does not + * refuse the ANY address at parse time, so neither does this grammar. + */ +class ConfigParseNeighborDestinationAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_neighbor_section", + SequenceCombinator(IP_ADDR, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt new file mode 100644 index 00000000..dc867038 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsMulticast +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsNull +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes + +/* + * [Network] ProxyNeighbor= and its deprecated alias IPv6ProxyNDPAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ProxyNeighbor= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-neighbor-proxy.c#L157-L216 config_parse_neighbor_proxy_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[Network] ProxyNeighbor=` and the deprecated `IPv6ProxyNDPAddress=` (.network). + * + * config_parse_neighbor_proxy_address reads one address of either family with in_addr_from_string_auto + * (inet_pton(AF_INET) then inet_pton(AF_INET6) — no prefix length, no zone index) and then ignores it + * with a warning if it is + * - the ANY address (`0.0.0.0`, `::`), + * - multicast (224.0.0.0/4 or ff00::/8), or + * - the IPv4 limited broadcast address `255.255.255.255`, + * because the kernel would refuse such a proxy entry. All three are flagged here. + */ +class ConfigParseNeighborProxyAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_neighbor_proxy_address", + SequenceCombinator( + Checked(IP_ADDR) { text -> + val bytes = ipLiteralBytes(text) + bytes != null && !ipIsNull(bytes) && !ipIsMulticast(bytes) && !(bytes.size == 4 && bytes.all { it.toInt() == -1 }) + }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemDelayOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemDelayOptionValue.kt new file mode 100644 index 00000000..1989c2a5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemDelayOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [NetworkEmulator] DelaySec=, DelayJitterSec= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DelaySec= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DelayJitterSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/netem.c#L51-L110 config_parse_netem_delay (QDISC_KIND_NETEM) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L83 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-netem.network#L12-L13 DelaySec=50ms, DelayJitterSec=10ms + */ + +/** + * Validator for [NetworkEmulator] DelaySec=, DelayJitterSec=. + * + * C function: config_parse_netem_delay (QDISC_KIND_NETEM). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A time span as parse_sec() reads it (see [parseSec]). No further bound in the parser. + * + * Lenient: a delay too large for the kernel's tick conversion only fails later, in + * network_emulator_fill_message, and is not checked here. + */ +class ConfigParseNetemDelayOptionValue : SimpleGrammarOptionValues( + "config_parse_netem_delay", + SequenceCombinator(parseSec(), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemRateOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemRateOptionValue.kt new file mode 100644 index 00000000..64e87776 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemRateOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [NetworkEmulator] LossRate=, DuplicateRate= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#LossRate= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DuplicateRate= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/netem.c#L112-L170 config_parse_netem_rate (QDISC_KIND_NETEM) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/tc-util.c#L63-L75 parse_tc_percent + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L135-L157 parse_permyriad / parse_permyriad_unbounded + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L57-L99 parse_parts_value_with_hundredths_place + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497-L519 safe_atoi (base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-percent-util.c#L87-L130 TEST(parse_permyriad) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-netem.network#L14 LossRate=20% + */ + +/** + * Validator for [NetworkEmulator] LossRate=, DuplicateRate=. + * + * C function: config_parse_netem_rate (QDISC_KIND_NETEM). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A percentage as parse_tc_percent() -> parse_permyriad() reads it: an integer followed by `%` (up to + * two decimals), `‰` (up to one decimal) or `‱` (no decimals), at most 100% (see [TC_PERCENT]). A + * bare number without a symbol is rejected. The integer part goes through safe_atoi (base 0), so + * `0x10%` is legal; that corner is accepted. + */ +class ConfigParseNetemRateOptionValue : SimpleGrammarOptionValues( + "config_parse_netem_rate", + SequenceCombinator(TC_PERCENT, EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetworkZoneOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetworkZoneOptionValue.kt new file mode 100644 index 00000000..15d5f099 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetworkZoneOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] Zone= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Zone= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--network-zone= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L544-L570 config_parse_network_zone (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L769-L783 ifname_valid_char + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L556 + * linux https://man7.org/linux/man-pages/man7/netdevice.7.html + */ + +/** + * Validator for the .nspawn `[Network] Zone=` setting (config_parse_network_zone, ltype 0). + * + * The raw value is prefixed with `vz-` and the result must pass ifname_valid(): every character + * printable ASCII (33…126) other than `:`, `/` and `%`, and the whole name at most IFNAMSIZ-1 = 15 + * bytes — leaving 12 for the zone name itself. The prefix makes the numeric / `.` / `all` / `default` + * exclusions irrelevant. An invalid name is logged and ignored. + */ +class ConfigParseNetworkZoneOptionValue : SimpleGrammarOptionValues( + "config_parse_network_zone", + SequenceCombinator( + RegexTerminal("""\S+""", """[\x21-\x7E&&[^:/%]]{1,12}"""), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt new file mode 100644 index 00000000..7fbb1d93 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt @@ -0,0 +1,131 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.OneOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * NFTSet= in [Address], [DHCPv4], [DHCPv6], [IPv6AcceptRA] and [DHCPPrefixDelegation] (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#NFTSet=source:family:table:set + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1207-L1295 config_parse_nft_set (NFT_SET_PARSE_NETWORK) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2298-L2363 config_parse_address_section (ADDRESS_NFT_SET → config_parse_nft_set, NFT_SET_PARSE_NETWORK) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1125-L1145 nfproto_table, nft_set_source_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L824-L835 nft_identifier_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (leading separators skipped L39-L56, coalescing L145-L159) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240-L281 extract_many_words_internal + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv4-only.network + * linux https://www.netfilter.org/projects/nftables/manpage.html (NFT_NAME_MAXLEN = 256, include/uapi/linux/netfilter/nf_tables.h) + */ + +// nft_identifier_valid: a letter, then letters, digits, `/`, `\`, `_` and `.`; shorter than +// NFT_NAME_MAXLEN (256). The tuple is split with EXTRACT_CUNESCAPE, so a backslash in the file starts a C +// escape; any escaped character is accepted here rather than decoding it. +private val NFT_IDENTIFIER = RegexTerminal( + """[^\s:"']+""", + """[A-Za-z](?:[A-Za-z0-9/_.]|\\[\s\S]){0,254}""" +) + +private val NFT_SOURCES = arrayOf("address", "prefix", "ifindex") +private val NFT_FAMILIES = arrayOf("arp", "bridge", "inet", "ip", "ip6", "netdev") +private val NFT_IDENTIFIER_REGEX = Regex("""[A-Za-z](?:[A-Za-z0-9/_.]|\\[\s\S]){0,254}""") + +// extract_many_words(":") is called without EXTRACT_DONT_COALESCE_SEPARATORS, so a run of colons is one +// separator, and leading / trailing colons are skipped. +private val COLONS = OneOrMore(COLON) + +/** + * One unquoted `source:family:table:set` tuple. `source` is restricted by NFT_SET_PARSE_NETWORK to + * `address`, `prefix` or `ifindex` (the cgroup/user/group sources are for unit files). + */ +private val NFT_TUPLE = SequenceCombinator( + ZeroOrMore(COLON), + FlexibleLiteralChoiceTerminal(*NFT_SOURCES), + COLONS, + FlexibleLiteralChoiceTerminal(*NFT_FAMILIES), + COLONS, + NFT_IDENTIFIER, + COLONS, + NFT_IDENTIFIER, + ZeroOrMore(COLON), +) + +/** + * The tuple text after the first-level EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE split: quotes removed + * wherever they are, backslashes kept. + */ +private fun stripQuotesRetainEscape(text: String): String { + val sb = StringBuilder() + var quote: Char? = null + for (c in text) { + when { + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> sb.append(c) + } + } + return sb.toString() +} + +/** Whether the (already unquoted) tuple is valid: four non-empty `:`-separated fields, each acceptable. */ +private fun nftTupleValid(tuple: String): Boolean { + val fields = tuple.split(':').filter { it.isNotEmpty() } + return fields.size == 4 && + fields[0] in NFT_SOURCES && + fields[1] in NFT_FAMILIES && + NFT_IDENTIFIER_REGEX.matches(fields[2]) && + NFT_IDENTIFIER_REGEX.matches(fields[3]) +} + +// A word that contains a quote somewhere (`"address:inet:t:s"`, `address:"inet":t:s`, ...). The first +// split removes the quotes wherever they are, so the word is judged as a whole after unquoting; it is a +// single token because the quotes may cut across the tuple's fields. An unterminated quote does not +// match here and is then flagged by the unquoted branch. +private val QUOTED_NFT_WORD = Checked( + RegexTerminal( + """(?=[^\s"']*["'])(?:[^\s"']+|"[^"]*"|'[^']*')+""", + """(?=[^\s"']*["'])(?:[^\s"']+|"[^"]*"|'[^']*')+""" + ) +) { nftTupleValid(stripQuotesRetainEscape(it)) } + +// The quoted form first: it requires a quote, so it never shadows a plain tuple. +private val NFT_WORD = AlternativeCombinator( + QUOTED_NFT_WORD, + NFT_TUPLE, +) + +/** + * Validator for the .network `NFTSet=` settings: `[Address]`, `[DHCPv4]`, `[DHCPv6]`, + * `[IPv6AcceptRA]` and `[DHCPPrefixDelegation]` — config_parse_nft_set with NFT_SET_PARSE_NETWORK, + * reached either directly or through config_parse_address_section's ADDRESS_NFT_SET table slot. + * + * The value is a whitespace-separated list (split with EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE, so quotes + * anywhere in a word are removed) of `source:family:table:set` tuples, each split on `:` into exactly + * four fields (extract_many_words with EXTRACT_CUNESCAPE and without DONT_COALESCE_SEPARATORS: runs of + * `:` count as one separator and leading/trailing `:` are skipped; a fifth field is an error): + * - source: `address`, `prefix` or `ifindex`; + * - family: nfproto_table — `arp`, `bridge`, `inet`, `ip`, `ip6`, `netdev`; + * - table and set: nft_identifier_valid — a letter followed by letters, digits, `/`, `\`, `_`, `.`, + * at most 255 characters. + * Any invalid tuple aborts the rest of the assignment with a warning. + * + * Lenient / strict corners: C escapes inside a table or set name are accepted without being decoded. + * A C escape that spells part of the source or family (`addr\x65ss`) is flagged, although systemd + * would decode it; nobody writes that. + */ +class ConfigParseNftSetNetworkOptionValue : SimpleGrammarOptionValues( + "config_parse_nft_set", + SequenceCombinator( + NFT_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), NFT_WORD)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNtpOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNtpOptionValue.kt new file mode 100644 index 00000000..12533e1a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNtpOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DNS_NAME_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [Network] NTP= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#NTP= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-ntp.c#L48-L104 config_parse_ntp (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L1331-L1341 dns_name_is_valid_or_address + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-state-file-tests.network#L12 + */ + +/** + * Validator for `[Network] NTP=` (config_parse_ntp): a whitespace-separated list of NTP servers, each a + * DNS name or an IP address (dns_name_is_valid_or_address). + * + * Every IP literal systemd accepts here is also a syntactically valid non-LDH DNS name (`192.168.5.1` is + * four labels, `2001:db8::1` one label containing colons), so the grammar is simply a list of + * [DNS_NAME_WORD]s; what it catches is empty labels (`pool..ntp.org`, `.org`), double trailing dots, + * over-long labels and stray control characters. The MAX_NTP_SERVERS cap is not modelled. + */ +class ConfigParseNtpOptionValue : SimpleGrammarOptionValues( + "config_parse_ntp", + SequenceCombinator(wordList(DNS_NAME_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNumaMaskOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNumaMaskOptionValue.kt new file mode 100644 index 00000000..17c0a1be --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNumaMaskOptionValue.kt @@ -0,0 +1,100 @@ +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.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Service]/[Socket]/[Mount]/[Swap] NUMAMask=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#NUMAMask= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1572-L1608 config_parse_numa_mask (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3790-L3819 config_parse_unit_cpu_set (unit_full_printf, then config_parse_cpu_set) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L220 config_parse_cpu_set (words split on WHITESPACE "," with EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305 parse_range (N or N-M, '-' not coalesced) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429 safe_atou_full (base 0, plus mangle_base 0b/0o at L403) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L17 CPU_SET_MAX_NCPU 8192 (cpu_set_add_range refuses end >= it) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-36-NUMAPOLICY.sh#L166 + */ + +// parse_range() cuts the word at its first '-', so the lower bound never sees a '-' (`-0` is an empty lower +// word, refused) while the upper bound is the rest of the word and may itself start with one (`0--0` is 0-0). +// Both are below CPU_SET_MAX_NCPU (8192). The word was split on whitespace, so no whitespace reaches either. +private val NODE_RANGE = SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = false, + ), + ZeroOrOne( + SequenceCombinator( + HYPHEN, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + ), +) + +private val DOUBLE_QUOTE = LiteralChoiceTerminal("\"") +private val SINGLE_QUOTE = LiteralChoiceTerminal("'") + +// unit_full_printf() runs over the whole value before it is split, so a word with a specifier in it +// could expand to anything; such words are accepted unchecked. Tried first: it needs a '%', which a +// plain range never contains, so it cannot shadow one. +private val SPECIFIER_WORD = RegexTerminal("""[^\s,]*%[^\s,]*""", """[^\s,]*%[^\s,]*""") + +private val NODE_WORD = AlternativeCombinator( + SPECIFIER_WORD, + SequenceCombinator(DOUBLE_QUOTE, NODE_RANGE, DOUBLE_QUOTE), + SequenceCombinator(SINGLE_QUOTE, NODE_RANGE, SINGLE_QUOTE), + NODE_RANGE, +) + +// WHITESPACE and ',' are both separators and are coalesced, so leading/trailing/doubled ones are fine. +private val NODE_SEPARATOR = RegexTerminal("""[\s,]+""", """[\s,]+""") + +/** + * Validator for `NUMAMask=` (config_parse_numa_mask, ltype 0). + * + * Either exactly `all` (compared with streq before anything else) or a CPU-set style list: node + * indexes `N` or ranges `N-M`, separated by whitespace and/or commas. Each number is read by + * safe_atou() (see [SafeAtoTerminal]), so `0x10`, `010` (octal), `0b11`, `0o7` and `0b+1` are all legal + * spellings, and must be below CPU_SET_MAX_NCPU (8192). The upper bound may be a negative zero (`0--0`); + * the lower bound may not, because parse_range() cuts at the first `-`. systemd does not check against the machine's actual NUMA node count at + * parse time. + * + * Not checked: a reversed range such as `3-1` (systemd logs "Invalid CPU range" and skips that word — + * the grammar framework cannot compare two numbers); and words containing a `%` specifier, which are + * accepted unchecked. + */ +class ConfigParseNumaMaskOptionValue : SimpleGrammarOptionValues( + "config_parse_numa_mask", + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("all"), EOF()), + SequenceCombinator( + ZeroOrOne(NODE_SEPARATOR), + NODE_WORD, + ZeroOrMore(SequenceCombinator(NODE_SEPARATOR, NODE_WORD)), + ZeroOrOne(NODE_SEPARATOR), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOomScoreAdjustOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOomScoreAdjustOptionValue.kt new file mode 100644 index 00000000..61aedc45 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOomScoreAdjustOptionValue.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Exec] OOMScoreAdjust= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#OOMScoreAdjust= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--oom-score-adjust= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L792-L828 config_parse_oom_score_adjust (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L762-L777 parse_oom_score_adjust + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497-L520 safe_atoi + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/process-util.c#L1934-L1936 oom_score_adjust_is_valid + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L529 + * linux https://man7.org/linux/man-pages/man5/proc_pid_oom_score_adj.5.html + */ + +/** + * Validator for the .nspawn `[Exec] OOMScoreAdjust=` setting (config_parse_oom_score_adjust, ltype 0). + * + * parse_oom_score_adjust() reads the value with safe_atoi() — base 0 after mangle_base(), so besides + * plain decimal it takes a sign, `0x` hex, leading-zero octal, `0o` octal and `0b` binary (where whitespace and a + * sign may follow the prefix, as in `0b-1` or `0b 1`, because strtol() skips them; a leading `\v` or `\f` is + * skipped by strtol() too) — and then requires OOM_SCORE_ADJ_MIN…OOM_SCORE_ADJ_MAX, i.e. -1000…1000 inclusive. + * The shared [SafeAtoTerminal] models all of that. An empty value resets the setting and never reaches the + * grammar. Out-of-range or malformed input is logged and ignored. + */ +class ConfigParseOomScoreAdjustOptionValue : SimpleGrammarOptionValues( + "config_parse_oom_score_adjust", + SequenceCombinator( + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = -1000L, + maxInclusive = 1000L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOpenFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOpenFileOptionValue.kt new file mode 100644 index 00000000..0d5b7a7d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOpenFileOptionValue.kt @@ -0,0 +1,94 @@ +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.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.OneOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Service] OpenFile= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#OpenFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6743-L6777 config_parse_open_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L12-L68 open_file_parse (a repeated flag is EINVAL, #L48-L49) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L70 open_file_validate (absolute path, valid fd name, at most one of read-only/append/truncate) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L134 open_file_flags_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/fd-util.c#L554 fdname_is_valid (printable ASCII except ':', at most 255) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (":" with EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_CUNESCAPE; "," with 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-open-file.c#L7 TEST(open_file_parse) + */ + +// A C escape as cunescape_one() understands it; any other backslash sequence makes open_file_parse fail. +private const val C_ESCAPE = """\\(?:[abfnrtv\\"'s]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-7]{3})""" + +// The path: everything up to the first ':' (a ':' inside it has to be escaped, e.g. \x3a). Must be +// absolute; path_is_valid() allows ".." here, unlike most unit-file paths. No specifier expansion. +private val OPEN_FILE_PATH = RegexTerminal("""(?:[^:\\]|\\[\s\S]?)+""", """(?:/|$C_ESCAPE)(?:[^:\\]|$C_ESCAPE)*(?=:|$)""") + +// The fd name: printable ASCII other than ':'. Empty means "use the file name". +private val OPEN_FILE_FDNAME = RegexTerminal("""(?:[^:\\]|\\[\s\S]?)+""", """(?:[\x20-\x7E&&[^:\\]]|$C_ESCAPE){1,255}(?=:|$)""") + +// open_file_parse refuses a flag given twice, and open_file_validate refuses more than one of +// read-only/append/truncate, so a valid list holds at most one access flag and at most one `graceful`. +// Exact literals rather than FlexibleLiteralChoiceTerminal: the two choices must not match each other's +// words syntactically, or the original (greedy, non-backtracking) engine commits to the wrong branch. +private val OPEN_FILE_ACCESS_FLAG = LiteralChoiceTerminal("read-only", "append", "truncate") +private val OPEN_FILE_GRACEFUL_FLAG = LiteralChoiceTerminal("graceful") +private val COMMA = LiteralChoiceTerminal(",") + +// extract_first_word(…, ",", 0) coalesces separators, so stray commas anywhere are harmless (even a +// field of nothing but commas, which yields no flags). +private val OPEN_FILE_OPTIONS = SequenceCombinator( + ZeroOrMore(COMMA), + ZeroOrOne( + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(OPEN_FILE_ACCESS_FLAG, ZeroOrOne(SequenceCombinator(OneOrMore(COMMA), OPEN_FILE_GRACEFUL_FLAG))), + SequenceCombinator(OPEN_FILE_GRACEFUL_FLAG, ZeroOrOne(SequenceCombinator(OneOrMore(COMMA), OPEN_FILE_ACCESS_FLAG))), + ), + ZeroOrMore(COMMA), + ) + ), +) + +/** + * Validator for `[Service] OpenFile=` (config_parse_open_file, the only key using it). + * + * `PATH[:FD-NAME[:OPTIONS]]`, split on the first two colons with C escapes decoded (so a literal colon + * in the path is written `\x3a`) and nothing allowed after a third field except one trailing `:`: + * + * - PATH must be absolute; + * - FD-NAME, if given, is printable ASCII without `:` (at most 255 characters); an empty one defaults + * to the file name; + * - OPTIONS is a comma-separated list of at most one of `read-only`, `append`, `truncate` and at + * most one `graceful`, in either order (a repeated flag, or two of read-only/append/truncate, is + * refused by systemd). + * + * Not checked: a path such as `/` whose file name cannot serve as the default fd name. Unlike most + * settings this one performs no specifier expansion, so `%` is an ordinary character. + * + * An empty assignment resets the list and is handled before this grammar is consulted. + */ +class ConfigParseOpenFileOptionValue : SimpleGrammarOptionValues( + "config_parse_open_file", + SequenceCombinator( + OPEN_FILE_PATH, + ZeroOrOne( + SequenceCombinator( + COLON, + ZeroOrOne(OPEN_FILE_FDNAME), + // The third extract (EXTRACT_DONT_COALESCE_SEPARATORS) consumes the colon that ends it, so one + // trailing `:` after the options leaves nothing behind and passes the isempty(v) check. + ZeroOrOne(SequenceCombinator(COLON, OPEN_FILE_OPTIONS, ZeroOrOne(COLON))), + ) + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOverlayOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOverlayOptionValue.kt new file mode 100644 index 00000000..a33a4f86 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOverlayOptionValue.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.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_FIELD_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_FIELD_SOURCE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Files] Overlay= / OverlayReadOnly= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Overlay= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--overlay= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L429-L453 config_parse_overlay (0 = Overlay=, 1 = OverlayReadOnly=) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-mount.c#L312-L385 overlay_mount_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-mount.c#L86-L118 source_path_parse / source_path_parse_nullable + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/strv.c#L415-L448 strv_split_full (":", EXTRACT_DONT_COALESCE_SEPARATORS) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L545 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L303 + */ + +/** + * A middle source path — one that is followed by another `:`. Same rules as [COLON_FIELD_SOURCE_PATH], + * with a `(?=:)` lookahead so the repetition never even tentatively consumes the final DEST field: under + * the classic matcher a tentative semantic match there would report a match length past the last + * syntactic token and break error localization. + */ +private val MIDDLE_SOURCE_PATH = RegexTerminal( + """(?:[^:\\]|\\[\s\S])+(?=:)""", + """(?:\+\\?/(?:[^:\\]|\\[\s\S])*|(?!\+)(?:[^:\\]|\\[\s\S])+)(?=:)""", +) + +/** + * Validator for the .nspawn `[Files] Overlay=` and `OverlayReadOnly=` settings (config_parse_overlay; + * ltype 1 only sets the read-only flag, so both share one grammar). + * + * overlay_mount_parse() splits the value on every unescaped `:` (strv_split_full with + * EXTRACT_DONT_COALESCE_SEPARATORS, so empty fields are kept and `\:` embeds a colon). At least two + * fields are required: + * + * - two fields, `LOWER:UPPER` — both are source paths (non-empty; `+/abs` relative to the image, or any + * other path, a relative one being resolved against the working directory) and UPPER is also the + * mount point; + * - three or more, `LOWER…:UPPER:DEST` — every LOWER is a non-empty source path, UPPER is a source path + * or empty (a throw-away directory under /var/tmp, as in `+/var::/var`), and DEST must be absolute. + * + * The grammar only lets an empty field appear immediately before DEST, which is exactly the UPPER slot + * when there are three or more fields. Parse errors are logged and the setting ignored. + */ +class ConfigParseOverlayOptionValue : SimpleGrammarOptionValues( + "config_parse_overlay", + AlternativeCombinator( + SequenceCombinator(COLON_FIELD_SOURCE_PATH, COLON, COLON_FIELD_SOURCE_PATH, EOF()), + SequenceCombinator( + COLON_FIELD_SOURCE_PATH, + COLON, + ZeroOrMore(SequenceCombinator(MIDDLE_SOURCE_PATH, COLON)), + ZeroOrOne(COLON), + COLON_FIELD_ABSOLUTE_PATH, + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePathOptionValue.kt new file mode 100644 index 00000000..18aa24d2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePathOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.LITERAL_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * .nspawn [Exec] WorkingDirectory= and [Network] NamespacePath= (also journald.conf TTYPath=, a file + * type the plugin does not load). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#WorkingDirectory= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#NamespacePath= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1378-L1414 config_parse_path (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.c#L1192 path_is_normalized (no ".." component) + */ + +/** + * Validator for config_parse_path with ltype 0, used by `[Exec] WorkingDirectory=` and + * `[Network] NamespacePath=` in .nspawn files. + * + * The value is taken literally (no specifier expansion) and must be an absolute path with no `..` + * component; `.` components and repeated slashes are simplified away rather than refused. There is no + * API VFS restriction, so `NamespacePath=/proc/1/ns/net` is fine. A non-zero ltype would make the error + * fatal but accepts the same values; no gperf entry uses one today. + * + * An empty assignment clears the setting and is handled before this grammar is consulted. + */ +class ConfigParsePathOptionValue : SimpleGrammarOptionValues( + "config_parse_path", + SequenceCombinator(LITERAL_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePidFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePidFileOptionValue.kt new file mode 100644 index 00000000..574dc291 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePidFileOptionValue.kt @@ -0,0 +1,40 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.NOT_API_VFS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.NO_DOT_DOT_COMPONENT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_TEXT_CHAR +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 + +/* + * [Service] PIDFile= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#PIDFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5702-L5750 config_parse_pid_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers expanded first) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE | PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.c#L1192 path_is_normalized (no ".." component) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721 path_below_api_vfs (/dev, /sys, /proc) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L574 patch_var_run (/var/run/ is rewritten with a warning, not refused) + */ + +/** + * Validator for `[Service] PIDFile=` (config_parse_pid_file, the only key using it). + * + * After specifier expansion a relative path is made absolute by prefixing the runtime directory (/run, + * or $XDG_RUNTIME_DIR for user services), so both `foo.pid` and `/run/foo.pid` are fine. The result + * then goes through path_simplify_and_warn(PATH_CHECK_ABSOLUTE | PATH_CHECK_NON_API_VFS): no `..` + * component anywhere, and an absolute path may not be /dev, /sys, /proc or below them. Unknown + * specifiers make systemd ignore the setting, so those are flagged too. + * + * An empty assignment clears the setting and is handled before this grammar is consulted. + */ +class ConfigParsePidFileOptionValue : SimpleGrammarOptionValues( + "config_parse_pid_file", + SequenceCombinator( + RegexTerminal("""[\s\S]+""", """$NO_DOT_DOT_COMPONENT$NOT_API_VFS$UNIT_TEXT_CHAR+$"""), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePiePacketLimitOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePiePacketLimitOptionValue.kt new file mode 100644 index 00000000..1e4354ac --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePiePacketLimitOptionValue.kt @@ -0,0 +1,43 @@ +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.* +import java.math.BigInteger + +/* + * [PIE] PacketLimit= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PacketLimit= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/pie.c#L40-L90 config_parse_pie_packet_limit (QDISC_KIND_PIE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-pie.network#L12 PacketLimit=200000 + */ + +/** + * Validator for [PIE] PacketLimit=. + * + * C function: config_parse_pie_packet_limit (QDISC_KIND_PIE). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned 32-bit integer read with safe_atou32 (base 0, so `0b+1` and `0b 1` are 1; see + * [SafeAtoTerminal]), 0…4294967295. No further bound in the + * parser (the man page's "1…4294967294" is not enforced by it). + */ +class ConfigParsePiePacketLimitOptionValue : SimpleGrammarOptionValues( + "config_parse_pie_packet_limit", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePivotRootOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePivotRootOptionValue.kt new file mode 100644 index 00000000..4c7b92c2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePivotRootOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_FIELD_NORMALIZED_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.REST_NORMALIZED_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Exec] PivotRoot= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#PivotRoot= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--pivot-root= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L325-L349 config_parse_pivot_root (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-mount.c#L1350-L1383 pivot_root_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":", EXTRACT_DONT_COALESCE_SEPARATORS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.c#L1192-L1203 path_is_normalized + */ + +/** + * Validator for the .nspawn `[Exec] PivotRoot=` setting (config_parse_pivot_root, ltype 0). + * + * pivot_root_parse() extracts the new root up to the first unescaped `:` (backslash makes the next + * character literal) and keeps the rest verbatim as the place to pivot the old root to; an empty rest + * means "unmount the old root". Both paths must be absolute AND normalized — path_is_normalized() + * refuses `//`, `.` and `..` components, though a trailing slash is fine. The man page only says + * "absolute"; this follows the C. Invalid values are logged and ignored. + * + * Deliberately lenient: path_is_normalized() also caps each component at NAME_MAX bytes and the whole + * path at PATH_MAX; those limits are not modelled. + */ +class ConfigParsePivotRootOptionValue : SimpleGrammarOptionValues( + "config_parse_pivot_root", + SequenceCombinator( + COLON_FIELD_NORMALIZED_PATH, + ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(REST_NORMALIZED_PATH))), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt new file mode 100644 index 00000000..9c1bf25f --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] Port= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Port= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L84 config_parse_port (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L75-L81 port_table + */ + +/** + * Validator for `[Link] Port=` (.link). + * + * C function: config_parse_port, generated by DEFINE_CONFIG_PARSE_ENUM over port_table: exactly one of + * `tp`, `aui`, `mii`, `fibre` or `bnc` (case-sensitive string-table lookup). An invalid value is + * logged and ignored by udev. + */ +class ConfigParsePortOptionValue : SimpleGrammarOptionValues( + "config_parse_port", + SequenceCombinator(FlexibleLiteralChoiceTerminal("tp", "aui", "mii", "fibre", "bnc"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt new file mode 100644 index 00000000..ec880400 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [VXLAN] PortRange= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PortRange= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vxlan.c#L299-L324 config_parse_port_range (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L736-L760 parse_ip_port_range (allow_zero=false) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + */ + +/** + * Validator for `[VXLAN] PortRange=` (.netdev). + * + * C function: config_parse_port_range → parse_ip_port_range(…, allow_zero = false): a single port or a + * `low-high` range, each bound a safe_atou() number (hex, octal and `0b`/`0o` work) in 1…65535. + * parse_range() cuts the whole value at the first `-`, so the lower bound never sees a minus (`-0` is an empty + * word) and the upper bound is the rest. Whitespace inside the value reaches safe_atou(), which skips it before + * the number, so `1000- 2000`, `0b 1-5` and `1-0b 1` are legal (but `1000 -2000` is not: the lower word keeps + * its trailing space). + * A range with `high < low` is refused by systemd after parsing; that compares numbers and is not + * modelled. An invalid value is logged and ignored by systemd. There is no isempty() branch, so `PortRange=` + * is a parse error in systemd rather than a reset; the plugin never runs a grammar on an empty value, so it + * is not flagged here. + */ +class ConfigParsePortRangeOptionValue : SimpleGrammarOptionValues( + "config_parse_port_range", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = false, + ), + ZeroOrOne( + SequenceCombinator( + HYPHEN, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt new file mode 100644 index 00000000..867cc632 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6PREF64Prefix] LifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6PREF64Prefix%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1240-L1283 config_parse_pref64_prefix_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + */ + +private val EIGHT_SECONDS: BigInteger = BigInteger.valueOf(8).multiply(USEC_PER_SEC) +private val SCALED_LIFETIME_LIMIT: BigInteger = BigInteger.valueOf(1L shl 13) + +/** + * Validator for `[IPv6PREF64Prefix] LifetimeSec=` (.network). + * + * The PREF64 option (RFC 8781) carries the lifetime as a 13-bit count of 8-second units, so + * config_parse_pref64_prefix_lifetime ignores ("PREF64 lifetime is too long") `infinity` and any value + * where DIV_ROUND_UP(usec, 8s) ≥ 8192 — i.e. it accepts parse_sec() values up to 8191 × 8 = 65528 + * seconds (18h 12min 8s). `0` is accepted. + */ +class ConfigParsePref64PrefixLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_pref64_prefix_lifetime", + SequenceCombinator(timeSpan { it != NETWORK_ROUTE_ADDR_USEC_INFINITY && divRoundUp(it, EIGHT_SECONDS) < SCALED_LIFETIME_LIMIT }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt new file mode 100644 index 00000000..69b1094a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.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.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * PrivateBPF= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#PrivateBPF= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L150 config_parse_private_bpf (0), via DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4185-L4190 private_bpf_table / DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23 parse_boolean (STRCASE_IN_SET) + */ + +/** + * Validator for `PrivateBPF=`. + * + * private_bpf_from_string() tries parse_boolean() first and then the table, whose only entries are + * `no` and `yes` — both already boolean spellings. So the accepted set is exactly parse_boolean's: + * `1 yes y true t on` / `0 no n false f off`, compared case-insensitively (STRCASE_IN_SET), so `Yes` + * and `TRUE` are fine. That is the shared [BOOLEAN]; there are no non-boolean names left to put in front + * of it, so no PreferValidAlternativeCombinator is needed. (Were the table to grow a name such as + * `auto`, it would be matched case-sensitively by streq, unlike the boolean spellings.) + * + * Note: DEFINE_CONFIG_PARSE_ENUM has no empty-string branch, so systemd actually logs `PrivateBPF=` as + * a parse error; the plugin never hands empty values to a validator, so that is not flagged. + * + * Covers: Service.PrivateBPF, Socket.PrivateBPF, Mount.PrivateBPF, Swap.PrivateBPF. + */ +class ConfigParsePrivateBpfOptionValue : SimpleGrammarOptionValues( + "config_parse_private_bpf", + SequenceCombinator( + BOOLEAN, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseQfqMaxPacketOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseQfqMaxPacketOptionValue.kt new file mode 100644 index 00000000..5b8f40de --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseQfqMaxPacketOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import java.math.BigInteger +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [QuickFairQueueingClass] MaxPacketBytes= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MaxPacketBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/qfq.c#L115-L173 config_parse_qfq_max_packet (TCLASS_KIND_QFQ) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/qfq.c#L14-L15 QFQ_MIN_MAX_PACKET 512, QFQ_MAX_MAX_PACKET 1<<16 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-qfq.network#L17 MaxPacketBytes=16000 + */ + +/** + * Validator for [QuickFairQueueingClass] MaxPacketBytes=. + * + * C function: config_parse_qfq_max_packet (TCLASS_KIND_QFQ). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A byte size as parse_size(rvalue, 1024) reads it (IEC suffixes `K`/`M`/…, see [parseSize]) that + * must lie in 512…65536 bytes (QFQ_MIN_MAX_PACKET…QFQ_MAX_MAX_PACKET); outside it the parser logs + * "Invalid ..., ignoring assignment". So `64K` is accepted and `65K` or `511` are not. + */ +class ConfigParseQfqMaxPacketOptionValue : SimpleGrammarOptionValues( + "config_parse_qfq_max_packet", + SequenceCombinator(parseSize(1024, BigInteger.valueOf(512), BigInteger.valueOf(65_536)), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRadvDnsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRadvDnsOptionValue.kt new file mode 100644 index 00000000..5be47953 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRadvDnsOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +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.wordList + +/* + * [IPv6SendRA] DNS= (and the old [IPv6PrefixDelegation] DNS=) (.network). + * + * Note: the plugin reports the deprecated [IPv6PrefixDelegation] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DNS= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1285-L1347 config_parse_radv_dns (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (AF_INET6, inet_pton) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipv6-prefix.network#L10 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[IPv6SendRA] DNS=` (config_parse_radv_dns): a whitespace-separated list of IPv6 + * addresses (inet_pton AF_INET6 — no brackets, ports or `%scope`) or the special word `_link_local`. + * + * systemd also drops the null address (`::`) with a warning; spellings of it are not singled out here + * (lenient). + */ +class ConfigParseRadvDnsOptionValue : SimpleGrammarOptionValues( + "config_parse_radv_dns", + SequenceCombinator(wordList(AlternativeCombinator(LiteralChoiceTerminal("_link_local"), IPV6_ADDR)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRequiredForOnlineOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRequiredForOnlineOptionValue.kt new file mode 100644 index 00000000..6fa9ef75 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRequiredForOnlineOptionValue.kt @@ -0,0 +1,46 @@ +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.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Link] RequiredForOnline= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RequiredForOnline= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1010-L1052 config_parse_required_for_online (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-network/network-util.c#L94-L137 parse_operational_state_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-network/network-util.c#L44-L56 link_operstate_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-state-file-tests.network#L6 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-activation-policy.network.d/required-yes.conf#L2 + */ + +// link_operstate_table, compared with streq (case-sensitive). +private val OPERSTATE = FlexibleLiteralChoiceTerminal( + "missing", "off", "no-carrier", "dormant", "degraded-carrier", "carrier", "degraded", "enslaved", "routable", +) + +/** + * Validator for `[Link] RequiredForOnline=` (config_parse_required_for_online). + * + * Tried first as an operational-state range — `MIN`, `MIN:`, `MIN:MAX` or `:MAX` over the names in + * link_operstate_table (split on the first `:`; a missing MIN means `missing`, a missing MAX means + * `routable`) — and failing that as a boolean (case-insensitive). + * + * parse_operational_state_range also refuses a range whose MIN is above its MAX (`routable:off`); that + * compares table positions rather than spelling and is not modelled (lenient). + */ +class ConfigParseRequiredForOnlineOptionValue : SimpleGrammarOptionValues( + "config_parse_required_for_online", + AlternativeCombinator( + SequenceCombinator(OPERSTATE, ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(OPERSTATE))), EOF()), + SequenceCombinator(COLON, OPERSTATE, EOF()), + SequenceCombinator(BOOLEAN, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolvConfOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolvConfOptionValue.kt new file mode 100644 index 00000000..304a0093 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolvConfOptionValue.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.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Exec] ResolvConf= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#ResolvConf= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--resolv-conf= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L830 config_parse_resolv_conf (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L832-L850 resolv_conf_mode_table, DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32-L43 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L532 + */ + +/** + * Validator for the .nspawn `[Exec] ResolvConf=` setting (config_parse_resolv_conf, ltype 0). + * + * resolv_conf_mode_from_string() is a DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN lookup: any + * parse_boolean() spelling (case-insensitive — false means `off`, true means `auto`), or one of the + * table names, matched case-sensitively: `off`, `copy-host`, `copy-static`, `copy-uplink`, `copy-stub`, + * `replace-host`, `replace-static`, `replace-uplink`, `replace-stub`, `bind-host`, `bind-static`, + * `bind-uplink`, `bind-stub`, `delete`, `auto`. + * + * Table entry 0 is `off`, a parse_boolean() spelling, so it is left to [BOOLEAN] (which also takes + * `OFF`) rather than listed with the names. + */ +class ConfigParseResolvConfOptionValue : SimpleGrammarOptionValues( + "config_parse_resolv_conf", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal( + "copy-host", "copy-static", "copy-uplink", "copy-stub", + "replace-host", "replace-static", "replace-uplink", "replace-stub", + "bind-host", "bind-static", "bind-uplink", "bind-stub", + "delete", "auto", + ), + BOOLEAN, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolveSupportOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolveSupportOptionValue.kt new file mode 100644 index 00000000..5f43fc67 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolveSupportOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] LLMNR= / MulticastDNS= (.network) and [Resolve] LLMNR= / MulticastDNS= (resolved.conf). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#LLMNR= + * https://www.freedesktop.org/software/systemd/man/latest/resolved.conf.html#LLMNR= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/resolve-util.c#L8 config_parse_resolve_support (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/resolve-util.c#L12-L17 resolve_support_table (DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32-L43 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-state-file-tests.network#L14-L15 + */ + +/** + * Validator for `LLMNR=` and `MulticastDNS=` (config_parse_resolve_support): `resolve`, or any boolean. + * + * The lookup tries parse_boolean() first — case-insensitive, all twelve spellings — and only then the + * case-sensitive table (`no`, `yes`, `resolve`), so `Yes` is fine but `Resolve` is not. + */ +class ConfigParseResolveSupportOptionValue : SimpleGrammarOptionValues( + "config_parse_resolve_support", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("resolve"), + BOOLEAN, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictAddressFamiliesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictAddressFamiliesOptionValue.kt new file mode 100644 index 00000000..27d756f4 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictAddressFamiliesOptionValue.kt @@ -0,0 +1,66 @@ +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.ADDRESS_FAMILY_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Exec] RestrictAddressFamilies= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#RestrictAddressFamilies= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--restrict-address-families= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L1068-L1095 config_parse_restrict_address_families (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L90-L145 parse_address_families + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (WHITESPACE ",", EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/af-list.c#L39-L51 af_from_name + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/meson.build#L168 gperf --ignore-case for the af lookup table + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L412-L419 + * linux https://man7.org/linux/man-pages/man7/address_families.7.html + */ + +/** One or more separators: parse_address_families splits on whitespace and commas, coalescing runs. */ +private val SEPARATORS = RegexTerminal("""[\s,]+""", """[\s,]+""") + +/** One family name, optionally in single or double quotes (EXTRACT_UNQUOTE strips them). */ +private val FAMILY_WORD = AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("'"), ADDRESS_FAMILY_NAME, LiteralChoiceTerminal("'")), + SequenceCombinator(LiteralChoiceTerminal("\""), ADDRESS_FAMILY_NAME, LiteralChoiceTerminal("\"")), + ADDRESS_FAMILY_NAME, +) + +/** + * Validator for the .nspawn `[Exec] RestrictAddressFamilies=` setting + * (config_parse_restrict_address_families, ltype 0), which hands the value to the same + * parse_address_families() helper as the unit-file RestrictAddressFamilies=. + * + * At 0f0682b773 that helper accepts: + * + * - exactly `none` (allow nothing); + * - an optional leading `~` (deny-list), immediately followed by a list of family names separated by + * whitespace and/or commas (runs coalesce; leading/trailing separators are fine; a bare `~` is an + * empty deny-list). Words are unquoted, so `'AF_INET'` is fine. + * + * Names go through af_from_name(), whose gperf table is case-insensitive and which retries with `AF_` + * prepended: `AF_INET`, `af_inet`, `inet` and `INET6` all resolve. Kernel-removed families (AF_DECnet, + * AF_IRDA, AF_ECONET, AF_WANPIPE) still resolve and are marked deprecated. An unknown name is logged and + * the rest of the line ignored. + */ +class ConfigParseRestrictAddressFamiliesOptionValue : SimpleGrammarOptionValues( + "config_parse_restrict_address_families", + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("none"), EOF()), + SequenceCombinator( + ZeroOrOne(LiteralChoiceTerminal("~")), + ZeroOrMore(SEPARATORS), + ZeroOrOne(SequenceCombinator(FAMILY_WORD, ZeroOrMore(SequenceCombinator(SEPARATORS, FAMILY_WORD)))), + ZeroOrMore(SEPARATORS), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt new file mode 100644 index 00000000..e2884df5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt @@ -0,0 +1,172 @@ +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.FILESYSTEM_GROUP_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FILESYSTEM_NAME +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.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * RestrictFileSystems= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RestrictFileSystems= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3645-L3712 config_parse_restrict_filesystems (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/bpf-restrict-fs.c#L212 bpf_restrict_fs_parse_filesystem + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/filesystems.c#L21 filesystem_set_find (group names) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/bpf-restrict-fs.c#L117 fs_type_from_string at unit start ("Invalid filesystem name, ignoring"), runtime only + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/filesystem-sets.py#L9 NAME_TO_MAGIC / FILESYSTEM_SETS (the name tables) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.exec-context.sh#L351-L358 ("foo", "@foo" are logged and ignored) + * linux https://man7.org/linux/man-pages/man2/statfs.2.html (the f_type magics the names map to) + * + * sd-check at 0f0682b773 (one value per file): accepted `EXT4`, `ext5`, `invalid`, `temporary`, + * `~proc,sysfs,foo`, `@auxiliary-api btrfs nfs @temporary unknown`, `ext"4"`, `ext4""`, `""`, `"a\"b"`, + * `ext4 ~proc`, `ext4 ~@foo`, `~ ~@bogus`, `"~@bogus"`, `\~proc`, `foo.bar!`; refused `@unknowngroup`, `@`, + * `@Basic-api`, `~@bogus`, `"@foo"`, `""@foo`, `\@foo`, `"@basic-api"x` ("Unknown filesystem group") and + * `ext"4`, `ext4 "unterminated` ("Trailing garbage"). + */ + +// Regex pieces for one word as extract_first_word(…, WHITESPACE ",", EXTRACT_UNQUOTE) reads it: unquoted +// characters, `\x` escapes (the backslash is dropped and any character may follow, also inside quotes) and +// '…' / "…" runs whose quotes are removed. An unterminated quote is "Trailing garbage", so quoted runs must close. + +/** Any one piece of a word. */ +private const val WORD_PIECE = """[^\s,"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'""" + +/** + * The first piece of a word, which must not unquote to `@` (bpf_restrict_fs_parse_filesystem() would look the + * word up as a group and refuse an unknown one) and must not be a raw `~` (at the start of the value that is + * the deny-list marker; elsewhere [TILDE_WORD] covers it). + */ +private const val FIRST_PIECE_NO_AT_NO_TILDE = + """[^\s,"'\\@~]|\\[^@]|"(?:[^"\\@]|\\[^@])(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\@]|\\[^@])(?:[^'\\]|\\[\s\S])*'""" + +/** [FIRST_PIECE_NO_AT_NO_TILDE] after leading empty quotes, where a `~` is just a name character (`""~x` is `~x`). */ +private const val FIRST_PIECE_NO_AT = + """[^\s,"'\\@]|\\[^@]|"(?:[^"\\@]|\\[^@])(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\@]|\\[^@])(?:[^'\\]|\\[\s\S])*'""" + +/** + * The known file system names as exact literals (completion, enum coloring). Not the lenient + * [FILESYSTEM_NAME] itself: since any name is valid here, its "well-formed but unknown" token would only add + * a second, invalid parse for every unknown word, and a list of unknown words would then explode into + * 2^n parses on the new engine. + */ +private val KNOWN_FILESYSTEM_NAME = LiteralChoiceTerminal(*FILESYSTEM_NAME.choices) + +/** + * A known name, bare or quoted as a whole, filling a whole word. [ANY_FILESYSTEM_NAME] steps aside for these, + * so they are matched by [KNOWN_FILESYSTEM_NAME] and each word has a single parse. + */ +private val KNOWN_NAME_WORD = """(?["']?)(?:""" + KNOWN_FILESYSTEM_NAME.choices.joinToString("|") { Regex.escape(it) } + + """)\k(?=[\s,]|\z)""" + +/** + * Any word the unit-file parser stores as a plain file system name, i.e. one that does not unquote to a + * leading `@`. The parser keeps such names unchecked: an unknown one (`EXT4`, `ext5`, `foo`) is only logged + * and skipped when the unit starts and the BPF map is filled ("bpf-restrict-fs: Invalid filesystem name '%s', + * ignoring.", src/core/bpf-restrict-fs.c#L117-L121), so it is accepted here. Issue #565 tracks a separate + * warning for such names. A word that is wholly empty quotes (`""`) is stored as the empty name and accepted. + */ +private val ANY_FILESYSTEM_NAME = RegexTerminal( + "(?!$KNOWN_NAME_WORD)(?:(?:\"\"|'')+(?:(?:$FIRST_PIECE_NO_AT)(?:$WORD_PIECE)*)?|(?:$FIRST_PIECE_NO_AT_NO_TILDE)(?:$WORD_PIECE)*)", + "(?!$KNOWN_NAME_WORD)(?:(?:\"\"|'')+(?:(?:$FIRST_PIECE_NO_AT)(?:$WORD_PIECE)*)?|(?:$FIRST_PIECE_NO_AT_NO_TILDE)(?:$WORD_PIECE)*)", +) + +/** + * A word starting with a raw `~` anywhere except the very start of the value. Only `rvalue[0]` is the + * deny-list marker, so `ext4 ~proc` or `~ ~@bogus` store the literal name `~proc` / `~@bogus`, which never + * matches a file system at unit start. Accepted for the same reason as [ANY_FILESYSTEM_NAME] (#565). + */ +private val TILDE_WORD = RegexTerminal("~(?:$WORD_PIECE)*", "~(?:$WORD_PIECE)*") + +/** A word that does not start with a raw `~`: an unknown name, a known name or `@group`, either possibly quoted. */ +private val ENTRY_NOT_TILDE = AlternativeCombinator( + // First, so the classic engine does not take a known-name prefix (`ext4` of `ext4foo`) and strand the rest. + ANY_FILESYSTEM_NAME, + SequenceCombinator( + LiteralChoiceTerminal("\""), + AlternativeCombinator(SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), KNOWN_FILESYSTEM_NAME), + LiteralChoiceTerminal("\""), + ), + SequenceCombinator( + LiteralChoiceTerminal("'"), + AlternativeCombinator(SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), KNOWN_FILESYSTEM_NAME), + LiteralChoiceTerminal("'"), + ), + SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), + KNOWN_FILESYSTEM_NAME, +) + +/** Any word, including one starting with a literal `~`. */ +private val ENTRY = AlternativeCombinator(TILDE_WORD, ENTRY_NOT_TILDE) + +/** + * Validator for `RestrictFileSystems=`. + * + * An optional leading `~` (deny-list instead of allow-list), then words split on whitespace and commas + * (separators coalesce; stray ones at either end are fine, and `~` alone is an empty deny list). Words are + * unquoted with EXTRACT_UNQUOTE, so quotes and backslash escapes may appear anywhere in a word. + * + * - A word that unquotes to `@group` must be one of filesystem-sets.py's FILESYSTEM_SETS (`@basic-api`, + * `@common-block`, `@network`, `@known`, …). The parser refuses an unknown group ("Unknown filesystem + * group, ignoring"), so it is flagged. The lookup is exact and case-sensitive. + * - Any other word is a file system name. The parser stores it unchecked. An unknown name (`EXT4`, `ext5`, + * `foo`, a literal `~proc` after the first word) is only rejected when the unit starts ("Invalid + * filesystem name, ignoring"), so it is accepted here. Issue #565 tracks a separate inspection that + * warns about values like this. Known names stay a separate alternative for completion and coloring. + * + * Known over-rejection: a word that only becomes `@group` through escaping or partial quoting + * (`\@basic-api`, `'@'basic-api`, `""@basic-api`) is accepted by systemd but flagged here; `"@basic-api"` + * quoted as a whole is accepted. + * + * Covers: Service.RestrictFileSystems, Socket.RestrictFileSystems, Mount.RestrictFileSystems, + * Swap.RestrictFileSystems. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseRestrictFilesystemsOptionValue : SimpleGrammarOptionValues( + "config_parse_restrict_filesystems", + // `~` is a separate alternative rather than ZeroOrOne("~"): the classic engine's ZeroOrOne probes its + // inner combinator a second time and reports that as the furthest point reached, which on `~~x` + // produced a highlight range past the end of the value. + AlternativeCombinator( + // Deny list: `rvalue[0] == '~'`; every following word, even one starting with `~`, is a name. + SequenceCombinator( + LiteralChoiceTerminal("~"), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + // Allow list starting with a word: that first word cannot start with a raw `~` (it would be the marker). + SequenceCombinator( + ENTRY_NOT_TILDE, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + EOF(), + ), + // Allow list starting with separators (`,~proc`): every word may start with `~`. + SequenceCombinator( + WHITESPACE_OR_COMMA_SEPARATOR, + ZeroOrOne( + SequenceCombinator( + ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRootImageOptionsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRootImageOptionsOptionValue.kt new file mode 100644 index 00000000..230d212c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRootImageOptionsOptionValue.kt @@ -0,0 +1,49 @@ +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.EXEC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAINED_FREE_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.partitionDesignator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] RootImageOptions=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RootImageOptions= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1648-L1713 config_parse_root_image_options (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/strv.c#L481-L531 strv_split_colon_pairs (UNQUOTE|RETAIN_ESCAPE words, then ':' with CUNESCAPE|UNESCAPE_SEPARATORS; >2 fields is -EINVAL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/gpt.c#L156-L183 partition_designator_table / partition_designator_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L249 unit_full_printf (options) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-gpt.c#L136-L142 partition designator shorthands accepted/rejected + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-50-DISSECT.mountfsd.sh#L155-L156 + */ + +/** + * Validator for `RootImageOptions=` (config_parse_root_image_options, ltype 0). + * + * A whitespace-separated list of `[PARTITION:]OPTIONS` words. A word with two fields names a partition + * designator first; a word with one field is the root partition's options. A third field makes + * strv_split_colon_pairs() fail with -EINVAL, which discards the whole assignment. + * + * The man page lists only root, usr, home, srv, esp, xbootldr, tmp and var; the C table also accepts + * swap, root-verity, usr-verity, root-verity-sig, usr-verity-sig and the -vty/-vsig shorthands, so this + * follows the C. The ':' separators are coalesced (no EXTRACT_DONT_COALESCE_SEPARATORS), so `root::ro` + * and a trailing ':' are accepted. The mount options themselves are free-form (handed to mount(8) at + * runtime) and only their escapes are checked. + */ +class ConfigParseRootImageOptionsOptionValue : SimpleGrammarOptionValues( + "config_parse_root_image_options", + whitespaceList( + SequenceCombinator( + ZeroOrOne(EXEC_COALESCED_COLONS), + AlternativeCombinator( + SequenceCombinator(partitionDesignator(), EXEC_COALESCED_COLONS, RETAINED_FREE_FIELD), + RETAINED_FREE_FIELD, + ), + ZeroOrOne(EXEC_COALESCED_COLONS), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt new file mode 100644 index 00000000..9fe3e29c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SIZE_IEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseSizeIec +import java.math.BigInteger + +/* + * [Route] TCPAdvertisedMaximumSegmentSize= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#TCPAdvertisedMaximumSegmentSize= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_ADVMSS) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_ADVMSS) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L221-L251 config_parse_route_metric_advmss + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-static.network + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] TCPAdvertisedMaximumSegmentSize=` (.network) — ROUTE_METRIC_ADVMSS, which + * config_parse_route_metric dispatches to config_parse_route_metric_advmss. + * + * A parse_size(…, 1024) byte count (`1460`, `1K`, `1.5K`, …) that must be in 1…UINT32_MAX. The man page + * says 1…4294967294; the C bound is `u == 0 || u > UINT32_MAX`, so 4294967295 is accepted here. + */ +class ConfigParseRouteMetricAdvMssOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + Checked(PARSE_SIZE_IEC) { text -> parseSizeIec(text)?.let { it.signum() > 0 && it <= UINT32_MAX } ?: false }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt new file mode 100644 index 00000000..c3fb87b9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Route] InitialCongestionWindow= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#InitialCongestionWindow= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_INITCWND) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_INITCWND) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L284-L313 config_parse_tcp_window + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-static.network#L37 + */ + +/** + * Validator for `[Route] InitialCongestionWindow=` (.network) — ROUTE_METRIC_INITCWND, which + * config_parse_route_metric dispatches to config_parse_tcp_window: safe_atou32() (so `0x10`, `0o20`, + * `020` are numbers too, and so are `0b 1` and `0b+1`, since safe_atou32 sees the whole value) and then 1…1023. + * + * (`InitialAdvertisedReceiveWindow=` uses the same leaf and is already covered by + * ConfigParseRouteSectionInitRwndOptionValue.) + */ +class ConfigParseRouteMetricInitCwndOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 1023L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt new file mode 100644 index 00000000..9f3baa4c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SIZE_IEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseSizeIec +import java.math.BigInteger + +/* + * [Route] MTUBytes= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MTUBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_MTU) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_MTU) + * https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1737-L1766 config_parse_mtu (ltype 0 = AF_UNSPEC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L126-L155 parse_mtu + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024) + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] MTUBytes=` (.network) — ROUTE_METRIC_MTU, which config_parse_route_metric + * dispatches to config_parse_mtu with ltype 0 (AF_UNSPEC, so no family minimum). + * + * parse_mtu() reads the value with parse_size(…, 1024) — a byte count with optional `K`/`M`/`G`/… + * suffixes (base 1024), fractions and multi-part sums like `1K 512` — and refuses anything above + * UINT32_MAX. With AF_UNSPEC there is no lower bound, so `0` is accepted. + */ +class ConfigParseRouteMetricMtuOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator(Checked(PARSE_SIZE_IEC) { text -> parseSizeIec(text)?.let { it <= UINT32_MAX } ?: false }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt new file mode 100644 index 00000000..c843ec2c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [Route] TCPRetransmissionTimeoutSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#TCPRetransmissionTimeoutSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_RTO_MIN) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_RTO_MIN) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L315-L346 config_parse_route_metric_tcp_rto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.h#L106-L108 timestamp_is_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-congctl.network + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] TCPRetransmissionTimeoutSec=` (.network) — ROUTE_METRIC_RTO_MIN, which + * config_parse_route_metric dispatches to config_parse_route_metric_tcp_rto. + * + * A parse_sec() time span that must be set (neither `0` nor `infinity`) and must round up to at most + * UINT32_MAX milliseconds, since RTAX_RTO_MIN is a u32 of milliseconds. + */ +class ConfigParseRouteMetricTcpRtoOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + timeSpan { it.signum() > 0 && it != NETWORK_ROUTE_ADDR_USEC_INFINITY && divRoundUp(it, USEC_PER_MSEC) <= UINT32_MAX }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt new file mode 100644 index 00000000..c9473999 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt @@ -0,0 +1,85 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Route] MultiPathRoute= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MultiPathRoute= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_MULTIPATH) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-nexthop.c#L1041-L1142 config_parse_multipath_route + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex (safe_atoi, > 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (weight, base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/network/test-networkd-conf.c#L352-L440 TEST(config_parse_multipath_route) + */ + +private val AT = LiteralChoiceTerminal("@") + +// After `@`: parse_ifindex() (a positive int) or, failing that, an alternative-length interface name. +// The name branch comes first because its lenient shape also matches digits but it only *validates* +// non-numeric names; under the classic matcher a number then falls through to the index branch. +// parse_ifindex() is safe_atoi() refusing anything below 1. The device is part of the first word, which +// extract_first_word() cut at whitespace, so no whitespace may follow the `@`. +private val DEVICE = AlternativeCombinator( + ALTERNATIVE_INTERFACE_NAME, + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = false, + acceptsMinus = true, + ), +) + +/** + * Validator for `[Route] MultiPathRoute=` (.network) — the ROUTE_MULTIPATH slot, whose table entry is + * config_parse_multipath_route. + * + * The first whitespace-separated word is `[GATEWAY][@DEVICE]` with at least one part present: + * - GATEWAY is an IPv4 or IPv6 literal (in_addr_from_string_auto — no prefix, no zone); + * - DEVICE, after the first `@`, is an interface index (parse_ifindex: a positive integer) or an + * interface name valid with IFNAME_VALID_ALTERNATIVE (up to 127 characters, not all digits). + * Anything after that word is the weight, read as a whole with safe_atou32() and accepted in 1…256 — + * a second word, or anything else trailing, is an error. Because the weight is the whole rest of the value, + * `@eth0 0b 1` is weight 1. + * + * Examples from systemd's own unit test: `@wg0 10`, `@1 15`, `10.0.0.1@eth0 20`, + * `2001:db8::1@wg0 15`, `192.168.1.1 100`, `fe80::1`, `@999999 10`. + */ +class ConfigParseRouteMultiPathOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(IP_ADDR, ZeroOrOne(SequenceCombinator(AT, DEVICE))), + SequenceCombinator(AT, DEVICE), + ), + ZeroOrOne( + SequenceCombinator( + WhitespaceTerminal(), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 256L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt new file mode 100644 index 00000000..505dce41 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6RoutePrefix] LifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6RoutePrefix%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1116-L1159 config_parse_route_prefix_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipv6ra-prefix.network#L24-L26 + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[IPv6RoutePrefix] LifetimeSec=` (.network). + * + * config_parse_route_prefix_lifetime reads a time span with parse_sec() and ignores it ("Lifetime is + * too long") unless it is `infinity` or rounds up to fewer than UINT32_MAX whole seconds — the Route + * Information option carries a 32-bit seconds field in which all-ones means infinity. + */ +class ConfigParseRoutePrefixLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_route_prefix_lifetime", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || divRoundUp(it, USEC_PER_SEC) < UINT32_MAX }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt new file mode 100644 index 00000000..0853a577 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_PREFIX_SAFE_LEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [IPv6RoutePrefix] Route= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Route= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1076-L1114 config_parse_route_prefix (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L799-L843 in_addr_prefix_from_string (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L779-L797 in_addr_parse_prefixlen (safe_atou8, ≤ 128) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipv6ra-prefix.network#L24-L26 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[IPv6RoutePrefix] Route=` (.network). + * + * config_parse_route_prefix reads an IPv6 prefix with in_addr_prefix_from_string(AF_INET6): an IPv6 + * literal (inet_pton — no IPv4, no zone) and an optional `/LEN` in 0…128, LEN read with safe_atou8 so + * hex/octal spellings count too. The man page asks for the slash, but without it the C simply uses a + * /128, so that is accepted. Host bits are masked off silently, so `2001:db8::1/64` is fine. + */ +class ConfigParseRoutePrefixOptionValue : SimpleGrammarOptionValues( + "config_parse_route_prefix", + SequenceCombinator(IPV6_PREFIX_SAFE_LEN, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt new file mode 100644 index 00000000..c99d5808 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING + +/* + * [Route] Table= and [RoutingPolicyRule] Table= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Table= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_TABLE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1862-L1886 config_parse_route_table + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_TABLE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1397-L1424 config_parse_routing_policy_rule_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may look like) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Route] Table=` and `[RoutingPolicyRule] Table=` (.network). Both table slots hand the whole + * value to manager_get_route_table_from_string(), so they use the shared [ROUTE_TABLE_FROM_STRING] grammar. + * + * That tries, in order: the predefined names `default`, `main`, `local`; the names defined by `RouteTable=` in + * networkd.conf; and finally safe_atou32(), refusing 0. The user-defined names live in a file the plugin cannot + * see, so every value that could be such a name is accepted: anything non-empty without a `:` that is not all + * digits. That includes `0x0`, `-0`, and names with whitespace such as `main table` (networkd.conf can define + * `main\ table:100`). An all-digit value must be a table number in 1…4294967295, so `0`, `00`, `08` and + * `4294967296` are flagged, as is any value containing `:`. + */ +class ConfigParseRouteTableOptionValue(parserName: String = "config_parse_route_section") : SimpleGrammarOptionValues( + parserName, + ROUTE_TABLE_FROM_STRING, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt new file mode 100644 index 00000000..7bc1c829 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6SendRA] HomeAgentLifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#HomeAgentLifetimeSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1583-L1624 config_parse_router_home_agent_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.h#L106-L108 timestamp_is_set + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L72 RADV_HOME_AGENT_MAX_LIFETIME_USEC + */ + +private val RADV_HOME_AGENT_MAX_LIFETIME_USEC: BigInteger = BigInteger.valueOf(65_535L).multiply(USEC_PER_SEC) + +/** + * Validator for `[IPv6SendRA] HomeAgentLifetimeSec=` (.network). + * + * config_parse_router_home_agent_lifetime reads a time span with parse_sec() and ignores it unless + * timestamp_is_set() (neither `0` nor `infinity`) and it is at most RADV_HOME_AGENT_MAX_LIFETIME_USEC = + * 65535 seconds. Sub-second values such as `500ms` pass the C check (the man page's "1…65535" is in + * whole seconds, but the comparison is on microseconds), so they are accepted here too. + */ +class ConfigParseRouterHomeAgentLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_router_home_agent_lifetime", + SequenceCombinator(timeSpan { it.signum() > 0 && it != NETWORK_ROUTE_ADDR_USEC_INFINITY && it <= RADV_HOME_AGENT_MAX_LIFETIME_USEC }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt new file mode 100644 index 00000000..396db662 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan + +/* + * [IPv6SendRA] RouterLifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RouterLifetimeSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1463-L1512 config_parse_router_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L38-L39 RADV_MIN/MAX_ROUTER_LIFETIME_USEC + */ + +/** + * Validator for `[IPv6SendRA] RouterLifetimeSec=` (.network). + * + * config_parse_router_lifetime reads a time span with parse_sec(). Only a parse failure ignores the + * assignment: `0` is kept (the host is not a default router), and any other value outside + * RADV_MIN_ROUTER_LIFETIME_USEC…9000s — `infinity` included — is *clamped* into that range with a + * warning, not rejected. So every parse_sec() spelling is accepted here; the clamping is not flagged. + */ +class ConfigParseRouterLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_router_lifetime", + SequenceCombinator(timeSpan(), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt new file mode 100644 index 00000000..b5b0fd58 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt @@ -0,0 +1,64 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import com.intellij.codeInspection.ProblemsHolder +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] IPv6SendRA= and the deprecated [Network] IPv6PrefixDelegation= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6SendRA= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1418-L1461 config_parse_router_prefix_delegation (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1406-L1416 radv_prefix_delegation_table (DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (case-insensitive) + */ + +/** + * Validator for config_parse_router_prefix_delegation: `[Network] IPv6SendRA=` and its deprecated + * predecessor `[Network] IPv6PrefixDelegation=`. + * + * The parser branches on the key name. For `IPv6SendRA=` it takes a plain parse_boolean() value. For + * the backward-compatible `IPv6PrefixDelegation=` it uses radv_prefix_delegation_from_string(), a + * string table with boolean fallback: any boolean, or `static` / `dhcpv6`. + * + * Both keys share one (parser, ltype) registration, so the registered grammar is the union — a boolean, + * or `static` / `dhcpv6` — which is exact for `IPv6PrefixDelegation=`. For `IPv6SendRA=` + * [generateProblemDescriptors] branches on the key, as the C does, and checks against a boolean-only + * grammar, so `IPv6SendRA=static` is flagged. + * + * string_table_lookup_from_string_with_boolean() runs parse_boolean() first (STRCASE_IN_SET, so `Yes`, + * `TRUE`, `oFf` are fine), then a case-sensitive streq() lookup, so `Static` is refused. The table's + * `no` / `yes` entries are left out of the names because [BOOLEAN] already covers them in any case. + */ +class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( + "config_parse_router_prefix_delegation", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("static", "dhcpv6"), + BOOLEAN, + ), + EOF() + ) +) { + + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + if (property.key == "IPv6SendRA") { + SEND_RA_BOOLEAN_ONLY.generateProblemDescriptors(property, holder) + } else { + super.generateProblemDescriptors(property, holder) + } + } + + private companion object { + // streq(lvalue, "IPv6SendRA") → parse_boolean() only. + val SEND_RA_BOOLEAN_ONLY = SimpleGrammarOptionValues( + "config_parse_router_prefix_delegation", + SequenceCombinator(BOOLEAN, EOF()) + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt new file mode 100644 index 00000000..3da8d2ea --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt @@ -0,0 +1,36 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6SendRA] ReachableTimeSec= / RetransmitSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ReachableTimeSec= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RetransmitSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1514-L1555 config_parse_router_uint32_msec_usec (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L44 RADV_MAX_UINT32_MSEC_USEC + */ + +private val RADV_MAX_UINT32_MSEC_USEC: BigInteger = BigInteger.valueOf(4_294_967_295L).multiply(USEC_PER_MSEC) + +/** + * Validator for `[IPv6SendRA] ReachableTimeSec=` and `RetransmitSec=` (.network). + * + * config_parse_router_uint32_msec_usec reads a time span with parse_sec() (default unit seconds, every + * suffix extract_multiplier() knows, `infinity`) and ignores it unless it is `infinity` or at most + * RADV_MAX_UINT32_MSEC_USEC = UINT32_MAX milliseconds (~49.7 days), since the value is sent as a 32-bit + * millisecond count. The man page documents only the 0…4294967295 ms range; the C also lets `infinity` + * through, so it is accepted here. + */ +class ConfigParseRouterUint32MsecUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_router_uint32_msec_usec", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || it <= RADV_MAX_UINT32_MSEC_USEC }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt new file mode 100644 index 00000000..8c3fe01b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [RoutingPolicyRule] FirewallMark= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#FirewallMark= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_FWMARK) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1426-L1450 config_parse_routing_policy_rule_fwmark + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1298-L1330 parse_fwmark_fwmask + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[RoutingPolicyRule] FirewallMark=` (.network) — the ROUTING_POLICY_RULE_FWMARK slot, + * whose table entry is config_parse_routing_policy_rule_fwmark → parse_fwmark_fwmask. + * + * `MARK` or `MARK/MASK`, split at the first `/`, each read with safe_atou32() — so decimal, `0x` hex, + * leading-zero / `0o` octal and `0b` binary are all accepted, up to 4294967295. A missing number on + * either side of the slash is an error. Both parts reach safe_atou32 with any whitespace they contain, so a + * leading `\v` or `0b 1` is fine, but whitespace before the slash (`1 /2`) is trailing junk for the mark. + */ +class ConfigParseRoutingPolicyRuleFirewallMarkOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt new file mode 100644 index 00000000..21f3a51c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt @@ -0,0 +1,77 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [RoutingPolicyRule] IPProtocol= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPProtocol= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_IP_PROTOCOL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2257-L2287 config_parse_ip_protocol (ltype 0 = not relaxed) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ip-protocol-list.c#L36-L70 parse_ip_protocol_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/generate-ip-protocol-list.sh every `#define IPPROTO_*` cpp sees for + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/meson.build#L328-L333 ... run with system_include_args, so is systemd's override + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/override/netinet/in.h#L4-L6 which includes the bundled uapi , + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/in.h#L27-L92 IPPROTO_IP ... IPPROTO_MPTCP (incl. AGGFRAG = 144) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/in6.h#L131-L139 IPPROTO_HOPOPTS ... IPPROTO_MH + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ip-protocol-to-name.awk number -> name table (HOPOPTS skipped; 0 is "ip") + * linux https://man7.org/linux/man-pages/man5/protocols.5.html + */ + +/** + * The IPPROTO_* names the generated gperf table knows, lower-cased. The table is generated from + * systemd's own vendored uapi headers (the override includes and + * , not glibc's), so it is fixed per systemd commit. `smc` (256) and `mptcp` (262) are left + * out because config_parse_ip_protocol refuses anything above 255. + */ +private val IP_PROTOCOL_NAMES = arrayOf( + "ip", "hopopts", "icmp", "igmp", "ipip", "tcp", "egp", "pup", "udp", "idp", "tp", "dccp", "ipv6", + "routing", "fragment", "rsvp", "gre", "esp", "ah", "icmpv6", "none", "dstopts", "mtp", "beetph", + "encap", "pim", "comp", "l2tp", "mh", "sctp", "udplite", "mpls", "ethernet", "aggfrag", "raw", +) + +/** The protocol numbers ip_protocol_to_name() has a name for (and that fit in a byte). */ +private val IP_PROTOCOL_NUMBERS = setOf( + 0, 1, 2, 4, 6, 8, 12, 17, 22, 29, 33, 41, 43, 44, 46, 47, 50, 51, 58, 59, 60, 92, 94, 98, 103, 108, 115, + 132, 135, 136, 137, 143, 144, 255, +) + +// parse_ip_protocol_full reads a number with safe_atoi() and refuses a negative one with -ERANGE; +// config_parse_ip_protocol then refuses anything above UINT8_MAX. The value is whole, so `\v6` and +// `0b 110` reach safe_atoi too. +private val IP_PROTOCOL_NUMBER = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 255L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +/** + * Validator for `[RoutingPolicyRule] IPProtocol=` (.network) — the ROUTING_POLICY_RULE_IP_PROTOCOL slot, + * whose table entry is config_parse_ip_protocol with ltype 0. + * + * parse_ip_protocol_full(…, relaxed = false) accepts an IPPROTO_* name looked up case-insensitively + * (the gperf table is built with `--ignore-case`, and the value is also retried lower-cased), or a + * number read with safe_atoi() (base 0, so `0x11` works) that ip_protocol_to_name() has a name for + * (otherwise -EPROTONOSUPPORT; e.g. `3` is refused). config_parse_ip_protocol then refuses anything + * above 255, because the netlink attribute is one byte. + */ +class ConfigParseRoutingPolicyRuleIpProtocolOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal(*IP_PROTOCOL_NAMES, ignoreCase = true), + Checked(IP_PROTOCOL_NUMBER) { text -> IP_PROTOCOL_NUMBER.valueOf(text)?.toInt() in IP_PROTOCOL_NUMBERS }, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt new file mode 100644 index 00000000..147a8b1c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt @@ -0,0 +1,97 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [RoutingPolicyRule] User= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#User= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_UID_RANGE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1478-L1508 config_parse_routing_policy_rule_uid_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L285-L369 get_user_creds + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L97-L135 parse_uid_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L66-L95 parse_uid (base 10, no sign, no leading zero, no whitespace) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L51-L64 uid_is_valid (not 65535, not 4294967295) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-fibrule-uidrange.network + */ + +// parse_uid: safe_atou32_full(s, 10 | SAFE_ATO_REFUSE_PLUS_MINUS | SAFE_ATO_REFUSE_LEADING_ZERO | +// SAFE_ATO_REFUSE_LEADING_WHITESPACE), so strict decimal, no `+`/`-`, no leading zero (except `0` itself) and +// no leading " \t\n\r" (a leading `\v` still gets through, as in C). 4294967295 is refused by uid_is_valid(). +// The lower bound is the text before the first `-` (extract_first_word with "-"), so it cannot contain one. +private val UID_LOWER = SafeAtoTerminal( + signed = false, + base = 10, + flags = setOf( + SafeAtoTerminal.Flag.REFUSE_PLUS_MINUS, + SafeAtoTerminal.Flag.REFUSE_LEADING_ZERO, + SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE, + ), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = false, +) + +// The upper bound is the rest of the value after that `-`. +private val UID_UPPER = SafeAtoTerminal( + signed = false, + base = 10, + flags = setOf( + SafeAtoTerminal.Flag.REFUSE_PLUS_MINUS, + SafeAtoTerminal.Flag.REFUSE_LEADING_ZERO, + SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE, + ), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// uid_is_valid() also refuses the 16-bit -1, 65535. +private val UID_RANGE = Checked( + SequenceCombinator( + Checked(UID_LOWER) { UID_LOWER.valueOf(it)?.toLong() != 65_535L }, + ZeroOrOne(SequenceCombinator(HYPHEN, Checked(UID_UPPER) { UID_UPPER.valueOf(it)?.toLong() != 65_535L })), + ) +) { text -> + val dash = text.indexOf('-') + val lo = if (dash >= 0) UID_LOWER.valueOf(text.substring(0, dash)) else null + val hi = if (dash >= 0) UID_UPPER.valueOf(text.substring(dash + 1)) else null + dash < 0 || lo == null || hi == null || lo <= hi +} + +// A user name for getpwnam(): NSS is the judge, so anything without whitespace or `:` is accepted, as +// long as it does not look like a UID or UID range (those are judged by the numeric branch). The lenient +// shape stops only at systemd's WHITESPACE (" \t\n\r"), so under the classic engine a range such as +// `5-\v6` (a `\v` that parse_uid lets through) is not cut short at the `\v` and falls through to UID_RANGE. +private val USER_NAME = RegexTerminal("""[^ \t\n\r]+""", """(?![0-9][0-9-]*(?:\s|$))[^\s:]+""") + +/** + * Validator for `[RoutingPolicyRule] User=` (.network) — the ROUTING_POLICY_RULE_UID_RANGE slot. + * + * config_parse_routing_policy_rule_uid_range first tries get_user_creds() (a user name, or a numeric UID + * that exists in the user database), and falls back to parse_uid_range(): `UID` or `UID-UID`, each + * read by parse_uid() — decimal only, no sign, no leading zeros, no whitespace, and not one of the + * invalid UIDs 65535 / 4294967295 — with the lower bound not above the upper. + * + * User names can only be checked at runtime, so any token that is not purely digits and dashes is + * accepted as a possible name. A digits-and-dashes token is judged strictly as a UID or UID range. + */ +class ConfigParseRoutingPolicyRuleUidRangeOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + // Name first: its lenient shape also covers numbers, but it only validates non-numeric names, so + // the classic matcher falls through to the numeric branch for those. + AlternativeCombinator(USER_NAME, UID_RANGE), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt new file mode 100644 index 00000000..ad9921de --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CPU_SET +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] ReceivePacketSteeringCPUMask= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#ReceivePacketSteeringCPUMask= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2095-L2136 config_parse_rps_cpu_mask (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L220-L307 config_parse_cpu_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-rps-cpu-multi.link#L6-L7 ReceivePacketSteeringCPUMask=0 1 / =2,3 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/24-rps-cpu-invalid.link#L6 ReceivePacketSteeringCPUMask=0 3 8-invalid (deliberately invalid) + */ + +/** + * Validator for `[Link] ReceivePacketSteeringCPUMask=` (.link). + * + * C function: config_parse_rps_cpu_mask. The whole value `disable` (an empty mask) or `all` (every + * online CPU), compared with streq, or else a CPU set exactly as config_parse_cpu_set reads it — see + * [CPU_SET]. An empty assignment clears the mask. An invalid word is logged and skipped by systemd. + */ +class ConfigParseRpsCpuMaskOptionValue : SimpleGrammarOptionValues( + "config_parse_rps_cpu_mask", + AlternativeCombinator( + SequenceCombinator(FlexibleLiteralChoiceTerminal("disable", "all"), EOF()), + SequenceCombinator(CPU_SET, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseServiceRefreshOnReloadOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseServiceRefreshOnReloadOptionValue.kt new file mode 100644 index 00000000..a62ca7a2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseServiceRefreshOnReloadOptionValue.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.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.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] RefreshOnReload= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#RefreshOnReload= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L4928-L4975 config_parse_service_refresh_on_reload (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (STRCASE_IN_SET: any case) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/service.c#L6630-L6657 service_refresh_on_reload_from_string_many + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/service.c#L6615 service_refresh_on_reload_table ("extensions", "credentials"; streq, so case-sensitive) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (whitespace-separated, flags 0) + */ + +private val REFRESH_RESOURCE = FlexibleLiteralChoiceTerminal("extensions", "credentials") +private val OPTIONAL_WHITESPACE = ZeroOrOne(WhitespaceTerminal()) + +/** + * Validator for `[Service] RefreshOnReload=` (config_parse_service_refresh_on_reload, the only key using + * it). + * + * Either a boolean in any case (parse_boolean is tried first and is case-insensitive, so `Yes` and + * `TRUE` are fine), or a whitespace-separated list of `extensions` and + * `credentials`, optionally prefixed by a single `~` to invert it. A bare `~` is accepted by systemd + * (an empty inverted list, i.e. the default) and so is accepted here. A boolean cannot be inverted + * (`~yes` is an unknown resource name). + * + * Not modelled: extract_first_word() (flags 0) still decodes a backslash escape, so the obfuscated + * spelling `ext\ensions` is accepted by systemd but flagged here. + * + * An empty assignment resets to the default and is handled before this grammar is consulted. + */ +class ConfigParseServiceRefreshOnReloadOptionValue : SimpleGrammarOptionValues( + "config_parse_service_refresh_on_reload", + AlternativeCombinator( + SequenceCombinator(BOOLEAN, EOF()), + SequenceCombinator( + ZeroOrOne(LiteralChoiceTerminal("~")), + OPTIONAL_WHITESPACE, + ZeroOrOne( + SequenceCombinator(REFRESH_RESOURCE, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), REFRESH_RESOURCE)), OPTIONAL_WHITESPACE) + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt new file mode 100644 index 00000000..d5afc0cb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt @@ -0,0 +1,111 @@ +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.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MatchResult +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NoMatch +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Parse +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ParseStep +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.TerminalCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UNIT_SPECIFIER_LETTERS + +/* + * BindToDevice= ([Socket]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#BindToDevice= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1097-L1134 config_parse_socket_bindtodevice (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L249 unit_full_printf + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0: < IFNAMSIZ, not numeric, not ./../all/default) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L769 ifname_valid_char (33..126 except ':' '/' '%') + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L797-L799 parse_ifindex() >= 0 → refused (no IFNAME_VALID_NUMERIC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497 safe_atoi (via parse_ifindex: value must be > 0) + * linux https://man7.org/linux/man-pages/man7/socket.7.html (SO_BINDTODEVICE) + * linux https://man7.org/linux/man-pages/man7/netdevice.7.html (IFNAMSIZ) + */ + +// Characters ifname_valid_char accepts. +private const val IFNAME_CHAR = """[\x21-\x7E&&[^:/%]]""" + +// A specifier that can appear inside an interface name. `%%` is left out on purpose: it expands to a +// literal `%`, which ifname_valid_char refuses. +private val IFNAME_SPECIFIER = "%[${UNIT_SPECIFIER_LETTERS.replace("%", "")}]" + +/** + * One interface name, possibly built from specifiers. Without a `%` it must satisfy the same rules as + * [INTERFACE_NAME] (ifname_valid with no flags: 1–15 characters from ifname_valid_char, not all digits, + * not `.`, `..`, `all` or `default`). With specifiers the expanded length and content are unknown, so + * only the literal characters are checked and every `%x` must be a known specifier. + */ +private val IFNAME_WITH_SPECIFIERS = RegexTerminal( + """\S+""", + """(?:${INTERFACE_NAME.semanticMatch.pattern})(?!\S)|(?=\S*%)(?:$IFNAME_CHAR|$IFNAME_SPECIFIER)+(?!\S)""" +) + +/** + * True if parse_ifindex() would take [name] as an interface index: safe_atoi() reads all of it (sign, `0x`, + * `0b`, `0o`, leading-`0` octal, exactly as [SafeAtoTerminal] models it) and the result is a positive `int`. ifname_valid() with no flags + * refuses those, so `0x5`, `+5`, `0b101`, `0o7`, `010` and `0b+1` are invalid, while `-5`, `+0`, `0x0`, + * `+0b1` (no prefix stripping after a sign) and out-of-range `0x80000000` are ordinary names. + * Checked against systemd's own parser (sd-check) at 0f0682b773. + */ +private val IFINDEX = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, +) + +private fun bindToDeviceIsIfindex(name: String): Boolean = IFINDEX.valueOf(name) != null + +/** [IFNAME_WITH_SPECIFIERS], additionally refusing the spellings [bindToDeviceIsIfindex] matches. */ +private class BindToDeviceIfnameTerminal(private val inner: RegexTerminal) : TerminalCombinator { + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val result = inner.SemanticMatch(value, offset) + if (result.tokens.size == 1 && bindToDeviceIsIfindex(result.tokens[0])) return NoMatch.copy(longestMatch = offset) + return result + } + + override fun parse(value: String, offset: Int): Sequence = inner.parse(value, offset).map { step -> + if (step is Parse) { + step.copy(tokens = step.tokens.map { it.copy(terminal = this, valid = it.valid && !bindToDeviceIsIfindex(it.text)) }) + } else { + step + } + } + + override fun toString(): String = "BindToDeviceIfname($inner)" +} + +/** + * Validator for `BindToDevice=`. + * + * `*` (like the empty value) clears the setting. Anything else is run through unit_full_printf() and + * must then be a valid interface name per ifname_valid() with no flags — so alternative names longer + * than 15 characters are refused, unlike BindNetworkInterface=. An invalid name is "ignoring", so it is + * flagged. Specifiers are allowed (`BindToDevice=%i` in a template socket is a real pattern). + * + * Numeric spellings that parse_ifindex() takes as an index (`0x5`, `+5`, `0b101`) are refused like + * plain all-digit names, see [bindToDeviceIsIfindex]. + * + * Deliberately lenient: a name containing specifiers is not length-checked. + * + * Covers: Socket.BindToDevice. + */ +class ConfigParseSocketBindtodeviceOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_bindtodevice", + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("*"), EOF()), + SequenceCombinator(BindToDeviceIfnameTerminal(IFNAME_WITH_SPECIFIERS), EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketListenOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketListenOptionValues.kt new file mode 100644 index 00000000..ddd372c6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketListenOptionValues.kt @@ -0,0 +1,252 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import com.intellij.codeInspection.ProblemsHolder +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +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.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.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_ABSOLUTE_NON_API_VFS_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_SPECIFIER_LETTERS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_TEXT_CHAR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.withUnitSpecifiers + +/* + * [Socket] ListenStream= / ListenDatagram= / ListenSequentialPacket= / ListenNetlink= (SOCKET_SOCKET), + * ListenFIFO= (SOCKET_FIFO), ListenMessageQueue= (SOCKET_MQUEUE), ListenUSBFunction= (SOCKET_USB_FUNCTION) + * and ListenSpecial= (SOCKET_SPECIAL). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#ListenStream= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#ListenFIFO= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#ListenNetlink= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L603-L722 config_parse_socket_listen (SOCKET_SOCKET, SOCKET_FIFO, SOCKET_SPECIAL, SOCKET_MQUEUE, SOCKET_USB_FUNCTION) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers expanded first, every ltype) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/specifier.c#L33-L105 specifier_printf (unknown %X is an error, %% is a literal %) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (file ltypes: absolute, no "..", not below /dev /sys /proc unless SPECIAL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721 path_below_api_vfs + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L574-L601 patch_var_run (/var/run/ -> /run/ before the length check) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L24-L96 socket_address_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1668 socket_address_parse_unix + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1352 sockaddr_un_set_path (2..107 bytes) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1754 socket_address_parse_vsock + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1708 vsock_parse_port + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1731 vsock_parse_cid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L718 parse_ip_port (1..65535, safe_atou16_full with SAFE_ATO_REFUSE_LEADING_WHITESPACE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (vsock CID/port, netlink group) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L220 in_addr_port_ifindex_name_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785 ifname_valid_full (IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L119-L152 socket_address_parse_netlink + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L656 netlink_family_table (WITH_FALLBACK: a number up to INT_MAX also works) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L65-L80 string_table_lookup_from_string_fallback (safe_atou, then <= max) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-netlink.c#L40 TEST(socket_address_parse) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-netlink.c#L105 TEST(socket_address_parse_netlink) + * linux https://man7.org/linux/man-pages/man7/unix.7.html + * linux https://man7.org/linux/man-pages/man7/netlink.7.html + * linux https://man7.org/linux/man-pages/man7/vsock.7.html + * linux https://man7.org/linux/man-pages/man7/mq_overview.7.html + */ + +// --------------------------------------------------------------------------------------------------- +// Socket addresses (socket_address_parse), applied to the specifier-expanded text. + +private val PERCENT_START_SPECIFIER = """%[$UNIT_SPECIFIER_LETTERS]""" + +// AF_UNIX: '/' (file system) or '@' (abstract namespace), then anything. sockaddr_un_set_path insists on +// 2..107 bytes including the leading character. The length is only checked when no specifier is present, +// because a specifier's expansion length is unknown; a specifier may also stand at the start (%t/foo). +// config_parse_socket_listen runs patch_var_run() before parsing, which rewrites a /var/run/ prefix +// (path_startswith, so repeated slashes count as one) to /run/; the length limit applies to the result, +// so after /var/run/ up to 102 characters may follow. Length is counted in UTF-16 units, not bytes, so +// non-ASCII paths are checked leniently. +private val UNIX_SOCKET_ADDRESS = RegexTerminal( + """[/@%][\s\S]*""", + """(?:(?=[^%]*$)(?:/+var/+run/+[\s\S]{0,102}|[/@][\s\S]{1,106})|(?=[\s\S]*%)(?:[/@]|$PERCENT_START_SPECIFIER)$UNIT_TEXT_CHAR*)$""" +) + +// parse_ip_port: safe_atou16_full(…, SAFE_ATO_REFUSE_LEADING_WHITESPACE), then 0 is refused. The port is a +// whole value or the text after the last ':' (cut before a '%' scope), so a leading `\v` and `0b 1` reach +// the parser (accepted) while a leading space is refused (sd-check: `ListenStream=1.2.3.4: 80`). +private val IP_PORT = SafeAtoTerminal( + signed = false, + base = 0, + flags = setOf(SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// AF_VSOCK: vsock[-stream|-dgram|-seqpacket]:CID:PORT; the CID may be empty (VMADDR_CID_ANY), one of the +// named CIDs, "-1", or a number; the port is any unsigned except VMADDR_PORT_ANY (UINT32_MAX). Both numbers +// are plain safe_atou() on a field cut at the first ':', so whitespace reaches the parser +// (sd-check accepts `vsock:2: 80` and `vsock: 2:80`). +private val VSOCK_CID = AlternativeCombinator( + FlexibleLiteralChoiceTerminal("hypervisor", "local", "host", "any"), + LiteralChoiceTerminal("-1"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), +) +private val VSOCK_ADDRESS = SequenceCombinator( + LiteralChoiceTerminal("vsock-seqpacket:", "vsock-stream:", "vsock-dgram:", "vsock:"), + ZeroOrOne(VSOCK_CID), + COLON, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = true, + ), +) + +// An interface scope after the port. The `%` that introduces it has to be written `%%` in a unit file: +// unit_path_printf() runs first, and `%eth0` is an unknown specifier (an error), `%lo` would expand %l. +private val INTERFACE_SCOPE = SequenceCombinator( + LiteralChoiceTerminal("%%"), + AlternativeCombinator(ALTERNATIVE_INTERFACE_NAME, RegexTerminal("[1-9][0-9]*", "[1-9][0-9]*")), +) + +private val IPV6_SOCKET_ADDRESS = SequenceCombinator( + LiteralChoiceTerminal("["), IPV6_ADDR, LiteralChoiceTerminal("]"), COLON, IP_PORT, ZeroOrOne(INTERFACE_SCOPE), +) +private val IPV4_SOCKET_ADDRESS = SequenceCombinator(IPV4_ADDR, COLON, IP_PORT, ZeroOrOne(INTERFACE_SCOPE)) + +/** Anything socket_address_parse() accepts — the grammar for ListenStream= and ListenDatagram=. */ +private val SOCKET_ADDRESS = withUnitSpecifiers( + AlternativeCombinator( + SequenceCombinator(UNIX_SOCKET_ADDRESS, EOF()), + SequenceCombinator(VSOCK_ADDRESS, EOF()), + SequenceCombinator(IPV6_SOCKET_ADDRESS, EOF()), + SequenceCombinator(IPV4_SOCKET_ADDRESS, EOF()), + SequenceCombinator(IP_PORT, EOF()), + ) +) + +/** ListenSequentialPacket=: config_parse_socket_listen refuses every family but AF_UNIX for SOCK_SEQPACKET. */ +private val SEQPACKET_ADDRESS = withUnitSpecifiers(SequenceCombinator(UNIX_SOCKET_ADDRESS, EOF())) + +/** + * ListenNetlink=: `FAMILY [GROUP]`, the family a name from netlink_family_table or a number. + * + * socket_address_parse_netlink() takes the family as the first word (extract_first_word, split on + * `" \t\n\r"` only, so `route\v5` is one word and refused) and hands the whole REST to safe_atou(), so + * the group may contain whitespace that strtoul skips (sd-check accepts `route 0b 1` and `route \v5`). + */ +private val NETLINK_ADDRESS = withUnitSpecifiers( + SequenceCombinator( + // The number goes first: the name list contains digits and '-' (ip6-fw), so its syntactic shape also + // matches the `0b` of `0b+1`, and the classic engine commits to the first syntactic match. + AlternativeCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = false, + acceptsMinus = true, + ), + FlexibleLiteralChoiceTerminal( + "route", "firewall", "inet-diag", "nflog", "xfrm", "selinux", "iscsi", "audit", "fib-lookup", + "connector", "netfilter", "ip6-fw", "dnrtmsg", "kobject-uevent", "generic", "scsitransport", + "ecryptfs", "rdma", + ), + ), + ZeroOrOne( + SequenceCombinator( + RegexTerminal("[ \t\n\r]+", "[ \t\n\r]+"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), + EOF(), + ) +) + +/** + * Validator for the four `[Socket]` keys config_parse_socket_listen handles with ltype SOCKET_SOCKET. + * + * The ltype is shared but the value type is not — config_parse_socket_listen branches on the key name + * itself — so this dispatches on [UnitFilePropertyType.key]: + * + * - `ListenStream=` / `ListenDatagram=`: any socket_address_parse() address — an absolute path or + * `@abstract` name (2..107 bytes), `vsock:CID:PORT` (also `vsock-stream:` etc.), a bare port + * (1..65535; parse_ip_port, so `0x50` is port 80), `IPv4:PORT` or `[IPv6]:PORT`, the latter two + * optionally followed by an interface scope. Because unit_path_printf() runs first, that scope's + * `%` must be written `%%` (`[fe80::1]:80%%eth0`). + * - `ListenSequentialPacket=`: AF_UNIX only. + * - `ListenNetlink=`: a netlink family name or number, optionally followed by a multicast group. + * + * Any value that uses a known unit specifier is accepted. [combinator] (used for colouring, completion + * and anything key-agnostic) is the union of the per-key grammars. + */ +class ConfigParseSocketListenSocketOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_listen", + AlternativeCombinator(SOCKET_ADDRESS, NETLINK_ADDRESS) +) { + private val byKey: Map = mapOf( + "ListenStream" to SimpleGrammarOptionValues("config_parse_socket_listen", SOCKET_ADDRESS), + "ListenDatagram" to SimpleGrammarOptionValues("config_parse_socket_listen", SOCKET_ADDRESS), + "ListenSequentialPacket" to SimpleGrammarOptionValues("config_parse_socket_listen", SEQPACKET_ADDRESS), + "ListenNetlink" to SimpleGrammarOptionValues("config_parse_socket_listen", NETLINK_ADDRESS), + ) + + /** The grammar config_parse_socket_listen applies to [key]; the union for an unexpected key. */ + fun combinatorForKey(key: String): Combinator = byKey[key]?.combinator ?: combinator + + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + val delegate = byKey[property.key] + if (delegate != null) delegate.generateProblemDescriptors(property, holder) else super.generateProblemDescriptors(property, holder) + } +} + +/** + * Validator for `[Socket] ListenFIFO=`, `ListenMessageQueue=` and `ListenUSBFunction=` (ltypes + * SOCKET_FIFO, SOCKET_MQUEUE, SOCKET_USB_FUNCTION). + * + * After unit_path_printf() the value goes through path_simplify_and_warn(PATH_CHECK_ABSOLUTE | + * PATH_CHECK_NON_API_VFS): it must be absolute (or start with a specifier such as `%t`), contain no + * `..` component, and not be /dev, /sys, /proc or below. A message queue name is a single-component + * path like `/queue`; systemd does not enforce that at parse time, so neither does this. + */ +class ConfigParseSocketListenFileOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_listen", + SequenceCombinator(UNIT_ABSOLUTE_NON_API_VFS_PATH, EOF()) +) + +/** + * Validator for `[Socket] ListenSpecial=` (ltype SOCKET_SPECIAL): as + * [ConfigParseSocketListenFileOptionValue] but without PATH_CHECK_NON_API_VFS, because special files + * live precisely in /dev, /proc and /sys (`ListenSpecial=/dev/rfkill`). + */ +class ConfigParseSocketListenSpecialOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_listen", + SequenceCombinator(UNIT_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt new file mode 100644 index 00000000..49b7cac8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.plainNameList + +/* + * [Network] IPoIB= / IPVLAN= / IPVTAP= / MACsec= / MACVLAN= / MACVTAP= / Tunnel= / VLAN= / VXLAN= / + * Xfrm= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPoIB= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L945-L1008 config_parse_stacked_netdev (NETDEV_KIND_IPOIB, …_IPVLAN, …_IPVTAP, …_MACSEC, …_MACVLAN, …_MACVTAP, …_VLAN, …_VXLAN, …_XFRM, _NETDEV_KIND_TUNNEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0, via ifname_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex (safe_atoi base 0, > 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-macvlan.network#L7 (MACVLAN=macvlan99) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipvlan.network#L7 (IPVLAN=ipvlan99) + */ + +// ifname_valid() first refuses anything parse_ifindex() accepts: safe_atoi() with base 0 (so hex, `0b`, +// `0o` and a leading `+` count) giving a value in 1…INT_MAX. INTERFACE_NAME only refuses all-digit names. +// The name is a whole word handed to safe_atoi, so leading whitespace that survived the split (`\v`) is +// skipped as C does. +private val NOT_AN_IFINDEX_NAME = Checked(INTERFACE_NAME) { name -> + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(name) == null +} + +/** + * Validator for the stacked-netdev settings of a .network `[Network]` section: `IPoIB=`, `IPVLAN=`, + * `IPVTAP=`, `MACsec=`, `MACVLAN=`, `MACVTAP=`, `Tunnel=`, `VLAN=`, `VXLAN=` and `Xfrm=`. All ten use + * config_parse_stacked_netdev; the ltype only records which netdev kind is expected, so they share a + * grammar. + * + * The value is a whitespace-separated list (extract_first_word with no flags: no quote handling) of + * netdev names, each checked with ifname_valid(): 1–15 printable ASCII characters other than `:`, `/` + * and `%`, not all digits, not something parse_ifindex() reads as a positive ifindex (`0x10`, `+5`, + * `0b1`), and not `.`, `..`, `all` or `default`. An invalid name is logged and skipped; + * it is flagged here. An unmatched backslash at the very end of the value would also make + * extract_first_word fail, but is not singled out. + */ +class ConfigParseStackedNetdevOptionValue : SimpleGrammarOptionValues( + "config_parse_stacked_netdev", + plainNameList(NOT_AN_IFINDEX_NAME), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt new file mode 100644 index 00000000..cb64c294 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPServer] BootFilename=, [MobileNetwork] APN= / OperatorId= / User= / Password= / PIN= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#BootFilename= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#APN= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1253-L1300 config_parse_string (CONFIG_PARSE_STRING_SAFE_AND_ASCII) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-util.c#L1154-L1198 string_is_safe (STRING_ALLOW_GLOBS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/utf8.h#L17 ascii_is_valid + */ + +/** + * Validator for config_parse_string with CONFIG_PARSE_STRING_SAFE_AND_ASCII + * (= CONFIG_PARSE_STRING_SAFE | CONFIG_PARSE_STRING_ASCII): `[DHCPServer] BootFilename=` and the + * `[MobileNetwork]` `APN=`, `OperatorId=`, `User=`, `Password=` and `PIN=` settings of .network files. + * + * Both checks run on the raw value. string_is_safe(…, STRING_ALLOW_GLOBS) refuses control characters + * (0x01–0x1F, 0x7F), backslashes and the quote characters `"` and `'` (globs `*?[` are fine); and + * ascii_is_valid() refuses any byte of 0x80 or above. Either failure logs a warning and ignores the + * assignment. What remains is printable ASCII, space included, minus `"`, `'` and `\`. + */ +class ConfigParseStringSafeAsciiOptionValue : SimpleGrammarOptionValues( + "config_parse_string", + SequenceCombinator( + RegexTerminal("""[\s\S]+""", """[\x20-\x7E&&[^"'\\]]+"""), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt new file mode 100644 index 00000000..942eeb56 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAIN_ESCAPE_WORD_LIST + +/* + * [Network] BindCarrier= (.network), [Exec] Parameters= / Environment= (.nspawn). + * (logind.conf's KillOnlyUsers= / KillExcludeUsers= share the parser but are not a file type the plugin + * handles.) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#BindCarrier= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Parameters= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Environment= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1416-L1460 config_parse_strv (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) + */ + +/** + * Validator for config_parse_strv with ltype 0: `[Network] BindCarrier=` in .network files and + * `[Exec] Parameters=` / `Environment=` in .nspawn files. + * + * config_parse_strv splits the value with extract_first_word(EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) and + * appends every word verbatim; it validates nothing about the words themselves (BindCarrier= globs are + * matched later, nspawn's Environment= entries are not even checked for an `=`). The only input it + * refuses is one extract_first_word cannot split — an unterminated `"` or `'` quote — and for that it + * logs a parse error and ignores the assignment. That is all this grammar checks; see + * [RETAIN_ESCAPE_WORD_LIST] for the word shape. + */ +class ConfigParseStrvOptionValue : SimpleGrammarOptionValues( + "config_parse_strv", + RETAIN_ESCAPE_WORD_LIST, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt new file mode 100644 index 00000000..a8fc03a2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt @@ -0,0 +1,150 @@ +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.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ERRNO_OR_SECCOMP_ACTION +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.SYSCALL_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SYSCALL_OR_GROUP +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * SystemCallFilter= ([Service]/[Socket]/[Mount]/[Swap] and nspawn's [Exec]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#SystemCallFilter= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#SystemCallFilter= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3290-L3382 config_parse_syscall_filter (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L747-L790 config_parse_syscall_filter (0), nspawn's own function of the same name + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L2508 parse_syscall_and_errno + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L2688 seccomp_parse_errno_or_action + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L342 parse_errno + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1293 seccomp_parse_syscall_filter + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L334 syscall_filter_sets[] (group names) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1035 seccomp_add_syscall_filter_item (nspawn's consumer) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-seccomp.c#L56 TEST(parse_syscall_and_errno) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L729 TEST(parse_errno) + * linux https://man7.org/linux/man-pages/man2/syscalls.2.html + * linux https://man7.org/linux/man-pages/man3/errno.3.html + * + * sd-check at 0f0682b773 (one value per file), a `~` word after the first position: + * .nspawn `@system-service ~@mount`, `@system-service ~@bogus`, `~~@mount`, `@system-service ~@mount:EPERM`: accepted + * (nspawn's parser stores every word unchecked). + * .service `@system-service ~@mount`, `read ~write`, `~~@mount`, `@system-service ~@bogus`: "System call ~@mount is not + * known, ignoring." (the same libseccomp-lookup warning as `madeupcall`); `@system-service ~@mount:EPERM`: + * "Allow-listed system calls cannot take error number"; `~@bogus` (marker + unknown group): "Unknown system + * call group". + */ + +/** `@group` or a bare system call name. */ +private val NAME: Combinator = SYSCALL_OR_GROUP + +/** + * A word starting with `~` after the first position of the value (`@system-service ~@mount`). Only + * `rvalue[0]` is the deny-list marker in both parsers, so this word is taken literally as the name `~@mount`. + * + * - nspawn's parser stores it unchecked; at container start seccomp_add_syscall_filter_item() cannot resolve + * it and drops it with a debug message. That is a runtime-only rejection, so it is accepted here; issue #565 + * tracks a separate warning for it. + * - the unit-file parser hands it to libseccomp, which does not know it ("System call ~@mount is not known, + * ignoring."). That is the same unknown-name warning this grammar already leaves to the shape check for + * `madeupcall`. + * + * The validator registry is keyed only by (parser, ltype), and both files register (config_parse_syscall_filter, + * 0), so the two cannot be told apart; the lenient nspawn behaviour wins. After the `~` the name is not looked + * up as a group by either parser, so any group-shaped word is accepted (`~@bogus`). + */ +private val TILDE_NAME: Combinator = SequenceCombinator( + LiteralChoiceTerminal("~"), + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("@"), RegexTerminal("[^\\s,:]+", "[a-z0-9_-]+")), + SYSCALL_NAME, + ), +) + +/** One allow-list entry after the first position: a name or a literal `~` word, with no `:errno`. */ +private val ALLOW_ENTRY: Combinator = AlternativeCombinator(TILDE_NAME, NAME) + +/** One deny-list entry: a name or a literal `~` word, optionally `:errno`/`:kill`. */ +private val DENY_ENTRY: Combinator = SequenceCombinator( + AlternativeCombinator(TILDE_NAME, NAME), + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal(":"), ERRNO_OR_SECCOMP_ACTION)), +) + +/** + * Validator for `SystemCallFilter=` — both systemd's (exec context of service/socket/mount/swap units) + * and systemd-nspawn's (`[Exec]` in .nspawn files), which share the (config_parse_syscall_filter, 0) + * pair but are different C functions. + * + * Syntax (core): an optional leading `~` (deny-list instead of allow-list), then a list of entries + * split on whitespace and commas. extract_first_word skips leading separators and coalesces runs, so + * `~ @clock @debug`, `read,write` and a trailing separator are all fine; `~` on its own is an + * (empty) deny list. Each entry is + * + * - `@group` — one of the predefined sets in syscall_filter_sets[] (`@system-service`, `@mount`, …). + * The parser refuses an unknown group ("Unknown system call group, ignoring"), so it is flagged. + * - a system call name, resolved by libseccomp for the native architecture. Unknown names are only + * logged and the set is architecture-dependent, so names are only checked for shape (lower-case + * letters, digits, `_`); `DONOTEXIST` is flagged, `madeupcall` is not. + * - a word starting with `~` after the first position (`@system-service ~@mount`), which both parsers take + * as a literal, never-matching name. nspawn only drops it at container start, so it is accepted (see + * [TILDE_NAME]; issue #565 tracks a warning for it). + * - in deny-list mode only, any of those followed by `:` and an errno name (case-insensitive, + * `EPERM`), a number 0…4095 (decimal, `0x` hex or `0` octal) or `kill`. parse_syscall_and_errno + * rejects an empty name (`:EPERM`), an empty or junk errno (`uname:`, `uname:EINVALaaa`) and an + * out-of-range number (`uname:4096`). Without the `~`, config_parse_syscall_filter refuses any + * errno suffix ("Allow-listed system calls cannot take error number, ignoring"). + * + * nspawn: the same `~` and separators, but each word is stored verbatim and only handed to libseccomp / + * the group table at container start. nspawn does not understand `:errno` at all (`uname:EPERM` is just an + * unresolvable name there). Because the two parsers share one registration, this grammar applies the core + * rules above to nspawn files too, except for the non-first `~` word, where nspawn's lenient behaviour wins. + * + * Covers: Service.SystemCallFilter, Socket.SystemCallFilter, Mount.SystemCallFilter, + * Swap.SystemCallFilter (load-fragment-gperf) and Exec.SystemCallFilter (nspawn-gperf). + * + * The empty assignment resets the filter and never reaches the grammar. + */ +class ConfigParseSyscallFilterOptionValue : SimpleGrammarOptionValues( + "config_parse_syscall_filter", + AlternativeCombinator( + // Deny list: `rvalue[0] == '~'`; a further `~` word (`~~@mount`) is a literal name. + SequenceCombinator( + LiteralChoiceTerminal("~"), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + DENY_ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, DENY_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + // Allow list starting with a word: the first word cannot start with `~` (it would be the marker). + SequenceCombinator( + NAME, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ALLOW_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + EOF(), + ), + // Allow list starting with separators (`,~read`): every word may start with `~`. + SequenceCombinator( + WHITESPACE_OR_COMMA_SEPARATOR, + ZeroOrOne( + SequenceCombinator( + ALLOW_ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ALLOW_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt new file mode 100644 index 00000000..f2010a99 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt @@ -0,0 +1,62 @@ +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.SYSCALL_OR_GROUP +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * SystemCallLog= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#SystemCallLog= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3384-L3454 config_parse_syscall_log (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1293 seccomp_parse_syscall_filter (errno_num = -1) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L334 syscall_filter_sets[] (group names) + * linux https://man7.org/linux/man-pages/man2/syscalls.2.html + */ + +/** A possibly-empty whitespace/comma separated list, with stray separators allowed at either end. */ +private val LIST = SequenceCombinator( + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + SYSCALL_OR_GROUP, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, SYSCALL_OR_GROUP)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), +) + +/** + * Validator for `SystemCallLog=`. + * + * Same list syntax as `SystemCallFilter=` (see [ConfigParseSyscallFilterOptionValue]): an optional + * leading `~` (log everything *except* the listed calls), then system call names and `@group`s split + * on whitespace and commas, with stray separators tolerated. Unlike SystemCallFilter=, each word goes + * straight to seccomp_parse_syscall_filter() with no parse_syscall_and_errno() step, so there is no + * `:errno` suffix — `uname:EPERM` would be looked up as a system call called `uname:EPERM`, fail to + * resolve, and be dropped with a warning; it is flagged here. + * + * Unknown `@group`s are flagged (warned and ignored by systemd). Bare names are only shape-checked + * (lower-case letters, digits, `_`), because libseccomp resolves them per native architecture. + * + * Covers: Service.SystemCallLog, Socket.SystemCallLog, Mount.SystemCallLog, Swap.SystemCallLog. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseSyscallLogOptionValue : SimpleGrammarOptionValues( + "config_parse_syscall_log", + // `~` is a separate alternative rather than ZeroOrOne("~"): the classic engine's ZeroOrOne probes its + // inner combinator a second time and reports that as the furthest point reached, which on `~~x` + // produced a highlight range past the end of the value. + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("~"), LIST, EOF()), + SequenceCombinator(LIST, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTbfRateOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTbfRateOptionValue.kt new file mode 100644 index 00000000..4a8bd014 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTbfRateOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [TokenBucketFilter] Rate=, PeakRate= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Rate= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PeakRate= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/tbf.c#L184-L243 config_parse_tbf_rate (QDISC_KIND_TBF) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-tbf.network#L12-L16 Rate=1G, PeakRate=100G + */ + +/** + * Validator for [TokenBucketFilter] Rate=, PeakRate=. + * + * C function: config_parse_tbf_rate (QDISC_KIND_TBF). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A rate in bits per second as parse_size(rvalue, 1000) reads it: a number with an optional SI suffix + * `K`/`M`/`G`/`T`/`P`/`E` (upper case only, powers of 1000) or `B`, optionally several descending + * elements (`1G 500M`). The result is divided by 8 and stored as a uint64; there is no further bound. + * Note `10Mbit` / `10mbit` (tc(8) spellings) are NOT accepted by systemd. + */ +class ConfigParseTbfRateOptionValue : SimpleGrammarOptionValues( + "config_parse_tbf_rate", + SequenceCombinator(parseSize(1000), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTemporaryFilesystemsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTemporaryFilesystemsOptionValue.kt new file mode 100644 index 00000000..781f1ba3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTemporaryFilesystemsOptionValue.kt @@ -0,0 +1,53 @@ +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.COLON +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 +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] TemporaryFileSystem=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#TemporaryFileSystem= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5116-L5185 config_parse_temporary_filesystems (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (EXTRACT_UNQUOTE, then ':' with EXTRACT_DONT_COALESCE_SEPARATORS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L3347 temporary_filesystem_add (options stored verbatim) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-temporaryfilesystem-options.service#L10 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-temporaryfilesystem-ro.service#L35 + */ + +// Words are split with EXTRACT_UNQUOTE and WITHOUT EXTRACT_RETAIN_ESCAPE, so (unlike BindPaths=) a +// backslash escapes any following character, including a blank, both inside and outside quotes. +private const val TMPFS_CHUNK = """(?:[^\s:"'\\]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'|\\[\s\S])""" +private const val TMPFS_OPTIONS_CHUNK = """(?:[^\s"'\\]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'|\\[\s\S])""" + +// The mount point: absolute after specifier expansion, with no ".." component. +private val TMPFS_MOUNT_POINT = RegexTerminal( + "$TMPFS_CHUNK+", + """(?!(?:[^\s:]*/)?\.\.(?:[/\s:"']|$))(?=["']*[/%])$TMPFS_CHUNK+(?![^\s:])""", +) + +// Everything after the first ':' is the option string, stored verbatim (it may itself contain ':'). +private val TMPFS_OPTIONS = RegexTerminal("$TMPFS_OPTIONS_CHUNK+", "$TMPFS_OPTIONS_CHUNK+") + +/** + * Validator for `TemporaryFileSystem=` (config_parse_temporary_filesystems, ltype 0). + * + * A whitespace-separated list of `PATH[:OPTIONS]`. PATH is cut at the first ':' and must be absolute + * after specifier expansion, with no `..` component; OPTIONS is everything after that ':' (mount + * options such as `ro,mode=0700`), stored verbatim and only interpreted at mount time, so it is not + * checked. `PATH:` (empty options) is accepted. An invalid mount point is logged and skipped. + * + * Lenient corners: quoted runs are opaque (a ':' inside quotes is really the separator), and `%` + * specifiers are not checked. + */ +class ConfigParseTemporaryFilesystemsOptionValue : SimpleGrammarOptionValues( + "config_parse_temporary_filesystems", + whitespaceList( + SequenceCombinator(TMPFS_MOUNT_POINT, ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(TMPFS_OPTIONS)))) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTeqlIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTeqlIdOptionValue.kt new file mode 100644 index 00000000..47b289ab --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTeqlIdOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [TrivialLinkEqualizer] Id= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Id= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/teql.c#L44-L100 config_parse_teql_id (QDISC_KIND_TEQL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-teql.network#L12 Id=1 + */ + +/** + * Validator for [TrivialLinkEqualizer] Id=. + * + * C function: config_parse_teql_id (QDISC_KIND_TEQL). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned integer read with safe_atou (base 0, so `0b+1` and `0b 1` are 1; see [SafeAtoTerminal]) + * that must not exceed INT_MAX: above it the parser + * logs "'Id=' is too large, ignoring assignment" (and, at 0f0682b773, still stores it — the qdisc kind + * then becomes e.g. "teql4294967295"), so 0…2147483647 is accepted. + */ +class ConfigParseTeqlIdOptionValue : SimpleGrammarOptionValues( + "config_parse_teql_id", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.INT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerCalendarOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerCalendarOptionValue.kt new file mode 100644 index 00000000..5175d980 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerCalendarOptionValue.kt @@ -0,0 +1,53 @@ +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.BatchUnitCoreGrammar +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.CALENDAR_SPEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.withUnitSpecifiers + +/* + * [Timer] OnCalendar= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html#OnCalendar= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html (section "Calendar Events") + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2043-L2114 config_parse_timer (TIMER_CALENDAR) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L179 unit_full_printf_full (specifiers expanded first) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L870-L1103 calendar_spec_from_string_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L375 parse_weekdays + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L719 parse_date + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L802 parse_calendar_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L619 prepend_component + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L500 parse_component_decimal + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L125 fix_year + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L169 chain_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L212 calendar_spec_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L887-L933 timezone suffix: " UTC" (any case), tzname[0..1], or last word if timezone_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L480-L498 parse_one_number (@EPOCH: bare strtoul, so "@ +5" is fine) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1596 verify_timezone + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-calendarspec.c#L117 calendar_spec_one (accepted spellings) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-calendarspec.c#L231 calendar_spec_from_string (rejected spellings) + */ + +/** + * Validator for `[Timer] OnCalendar=`, the only key using config_parse_timer with TIMER_CALENDAR. + * + * After unit_full_printf() the value goes to calendar_spec_from_string(), which accepts either a + * shorthand keyword (`daily`, `weekly`, `semi-annually`, … — any case) or + * + * [WEEKDAYS] [[YEAR-]MONTH-DAY | [YEAR-]MONTH~DAY | @EPOCH] [HOUR:MINUTE[:SECOND]] + * + * optionally followed by a timezone after exactly one space (` UTC`, the local zone abbreviation or an + * IANA zone name; extra spaces are only tolerated after a weekday list or a bare date, which skip + * them). A weekday or keyword in the timezone position (`*-*-* 12:00 Mon`, `Mon Fri`) is refused, as + * systemd refuses it; see [BatchUnitCoreGrammar.CalendarTimezoneTerminal] for what counts as a zone. Each + * date/time field is `*` or a comma list of `value[..value][/repeat]`; seconds may carry fractions. + * See [CALENDAR_SPEC] for exactly which per-field bounds are checked and which cross-field checks + * (reversed ranges, overshooting repetitions, descending weekday ranges, zone existence) are left to + * systemd. A value that uses a unit specifier is accepted as long as the specifier is a known one. + * + * An empty assignment resets the timer list and is handled before this grammar is consulted. + */ +class ConfigParseTimerCalendarOptionValue : SimpleGrammarOptionValues( + "config_parse_timer", + withUnitSpecifiers(CALENDAR_SPEC) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerMonotonicOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerMonotonicOptionValue.kt new file mode 100644 index 00000000..3eff2f67 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerMonotonicOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.TIMESPAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.withUnitSpecifiers + +/* + * [Timer] OnActiveSec= / OnBootSec= / OnStartupSec= / OnUnitActiveSec= / OnUnitInactiveSec= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html#OnActiveSec= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html (section "Parsing Time Spans") + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2043-L2114 config_parse_timer (TIMER_ACTIVE, TIMER_BOOT, TIMER_STARTUP, TIMER_UNIT_ACTIVE, TIMER_UNIT_INACTIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L179 unit_full_printf_full (specifiers expanded first) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec, default unit seconds) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21 TEST(parse_sec) + */ + +/** + * Validator for the five monotonic `[Timer]` settings — `OnActiveSec=`, `OnBootSec=`, + * `OnStartupSec=`, `OnUnitActiveSec=` and `OnUnitInactiveSec=`. They share config_parse_timer, whose + * non-calendar branch hands the specifier-expanded value to parse_sec(). + * + * Accepted: `infinity`, or one or more `number[unit]` elements — `50`, `5min`, `1h30min`, `5s 500ms`, + * `.5y`, `+3.1s`, `3.1s.2` — exactly as described for [TIMESPAN]; any value using a known unit + * specifier. Refused: negative numbers, a dangling `.` (`5.`), unknown or mis-pluralised units + * (`5mins`, `2hs`) and two bare numbers glued together (`12.34.56`). + * + * An empty assignment resets the timer list and is handled before this grammar is consulted. + */ +class ConfigParseTimerMonotonicOptionValue : SimpleGrammarOptionValues( + "config_parse_timer", + withUnitSpecifiers(TIMESPAN) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt new file mode 100644 index 00000000..2cd08bb1 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt @@ -0,0 +1,38 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * [DHCPServer] Timezone= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Timezone= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2222-L2255 config_parse_timezone (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1596-L1655 verify_timezone + * linux https://man7.org/linux/man-pages/man5/tzfile.5.html + */ + +// One path component of a zoneinfo name: verify_timezone allows ASCII letters, digits, `-`, `_`, `+`. +private val TZ_COMPONENT = RegexTerminal("""[^/]+""", """[A-Za-z0-9_+-]+""") + +/** + * Validator for `[DHCPServer] Timezone=` (.network). + * + * config_parse_timezone calls verify_timezone(), which accepts `UTC` unconditionally and otherwise + * requires a relative zoneinfo path: components of ASCII letters, digits, `-`, `_` and `+`, separated by + * single `/`, with no leading or trailing `/` (so no `.` or `..` components either). It then opens + * `/usr/share/zoneinfo/` and checks the TZif magic — that runtime check cannot be done here, so + * a well-formed but non-existent zone such as `Mars/Olympus_Mons` is accepted. + */ +class ConfigParseTimezoneOptionValue : SimpleGrammarOptionValues( + "config_parse_timezone", + SequenceCombinator( + TZ_COMPONENT, + ZeroOrMore(SequenceCombinator(CIDR_SEPARATOR, TZ_COMPONENT)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTmpfsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTmpfsOptionValue.kt new file mode 100644 index 00000000..3a6a09ec --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTmpfsOptionValue.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.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_FIELD_ABSOLUTE_PATH +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.ZeroOrOne + +/* + * [Files] TemporaryFileSystem= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#TemporaryFileSystem= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--tmpfs= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L377-L401 config_parse_tmpfs (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-mount.c#L277-L310 tmpfs_mount_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":", EXTRACT_DONT_COALESCE_SEPARATORS) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L540-L541 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L297 + * linux https://man7.org/linux/man-pages/man5/tmpfs.5.html + */ + +/** The tmpfs mount options: the raw remainder, handed to mount(2) and only checked by the kernel. */ +private val TMPFS_OPTIONS = RegexTerminal("""[\s\S]+""", """[\s\S]+""") + +/** + * Validator for the .nspawn `[Files] TemporaryFileSystem=` setting (config_parse_tmpfs, ltype 0). + * + * tmpfs_mount_parse() extracts the path up to the first unescaped `:` (backslash makes the next + * character literal, so `\:` embeds a colon) and requires it to be absolute (path_is_absolute(): just + * a leading `/`). Everything after the colon is kept verbatim as the mount option string; when it is + * absent or empty `mode=0755` is used. + * + * Deliberately lenient: the option string is not checked — it is passed to the kernel's tmpfs, which + * is the only thing that validates it. + */ +class ConfigParseTmpfsOptionValue : SimpleGrammarOptionValues( + "config_parse_tmpfs", + SequenceCombinator( + COLON_FIELD_ABSOLUTE_PATH, + ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(TMPFS_OPTIONS))), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt new file mode 100644 index 00000000..81994454 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt @@ -0,0 +1,115 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ExtractedWordTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WordEnd +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Link] Property= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Property= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L1810-L1878 config_parse_udev_property (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/specifier.c#L33-L105 specifier_printf (link_specifier_table) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L81-L102 env_assignment_is_valid (via udev_property_assignment_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-device/device-util.c#L219-L237 device_property_can_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/85-unmanaged.link#L18 Property=ID_NET_MANAGED_BY=io.systemd.Network + */ + +// The properties udev reserves for itself (device_property_can_set), refused as assignment targets, plus +// the kernel's SYNTH_ARG_ prefix. The trailing lookahead makes each reserved word match only as the whole +// name, so `DEVPATHX` or `TAGS_EXTRA` are still fine. +private const val RESERVED_PROPERTY = + """(?!(?:ACTION|SEQNUM|SYNTH_UUID|DEVPATH|DEVPATH_OLD|SUBSYSTEM|DEVTYPE|DRIVER|MODALIAS|DEVNAME|DEVMODE|""" + + """DEVUID|DEVGID|MAJOR|MINOR|DISKSEQ|PARTN|IFINDEX|INTERFACE|INTERFACE_OLD|DEVLINKS|TAGS|CURRENT_TAGS|""" + + """USEC_INITIALIZED|UDEV_DATABASE_VERSION)(?![A-Za-z0-9_%]))(?!SYNTH_ARG_)""" + +// A link_specifier_table specifier (COMMON_SYSTEM_SPECIFIERS + COMMON_TMP_SPECIFIERS). `%%` is left out on +// purpose: it expands to a literal `%`, which no property name may contain. +private const val SPECIFIER = """%[aAbBHlqmMovwWTV]""" + +// The property name: env_name_is_valid — bash-style, i.e. letters, digits and `_`, not starting with a +// digit. Specifiers are expanded before the check, so one may appear in the name too. +private val PROPERTY_NAME = RegexTerminal( + """[A-Za-z0-9_%]+""", + """$RESERVED_PROPERTY(?:[A-Za-z_]|$SPECIFIER)(?:[A-Za-z0-9_]|$SPECIFIER)*""", +) + +private val EQUALS = LiteralChoiceTerminal("=") + +// The value, in its raw (still-quoted, still-escaped) form. env_value_is_valid only requires valid UTF-8, +// so any character is fine; whitespace ends the word unless it is quoted or backslash-escaped. +private val RAW_VALUE = RegexTerminal( + """(?:[^\s"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""", + """(?:[^\s"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""", +) +private val DOUBLE_QUOTED_INNER = RegexTerminal("""(?:[^"\\]|\\[\s\S])+""", """(?:[^"\\]|\\[\s\S])+""") +private val SINGLE_QUOTED_INNER = RegexTerminal("""(?:[^'\\]|\\[\s\S])+""", """(?:[^'\\]|\\[\s\S])+""") + +private fun quotedAssignment(quote: String, inner: RegexTerminal) = SequenceCombinator( + LiteralChoiceTerminal(quote), PROPERTY_NAME, EQUALS, ZeroOrOne(inner), LiteralChoiceTerminal(quote), ZeroOrOne(RAW_VALUE), +) + +// The name check of PROPERTY_NAME, applied to an already unquoted and unescaped word. +private val UNQUOTED_PROPERTY_NAME = Regex("""$RESERVED_PROPERTY(?:[A-Za-z_]|$SPECIFIER)(?:[A-Za-z0-9_]|$SPECIFIER)*""") + +private fun assignmentNameIsValid(word: String): Boolean { + val eq = word.indexOf('=') + return eq > 0 && UNQUOTED_PROPERTY_NAME.matches(word.substring(0, eq)) +} + +// One word: NAME=VALUE, either plain (the value may still contain quoted runs) or with the whole +// assignment quoted from the start of the name. Anything else extract_first_word could turn into a valid +// assignment — a quote or C escape inside the name (`F"OO"=bar`, `"FOO"=bar`, `FOO\x41=1`) — falls +// through to the ExtractedWordTerminal, which unquotes and unescapes the word first and then applies the +// same name check. WordEnd stops the classic engine committing to a structured branch that only matched +// a prefix of the word. +private val PROPERTY_ASSIGNMENT = AlternativeCombinator( + SequenceCombinator( + AlternativeCombinator( + quotedAssignment("\"", DOUBLE_QUOTED_INNER), + quotedAssignment("'", SINGLE_QUOTED_INNER), + SequenceCombinator(PROPERTY_NAME, EQUALS, ZeroOrOne(RAW_VALUE)), + ), + WordEnd("""\s"""), + ), + ExtractedWordTerminal("""\s""", cunescape = true, "udev property assignment", ::assignmentNameIsValid), +) + +/** + * Validator for `[Link] Property=` (.link). + * + * C function: config_parse_udev_property. A whitespace-separated list of `NAME=VALUE` udev property + * assignments. Each word is split with extract_first_word(…, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE), so it may + * be quoted (`"FOO=bar baz"` or `FOO="bar baz"`) or contain C escapes; specifiers from the link specifier + * table (`%H`, `%v`, …) are expanded; then the assignment must pass env_assignment_is_valid — a + * bash-style name (letters, digits, `_`, not starting with a digit), `=`, and any UTF-8 value, which may be + * empty — and the name must not be one udev reserves (device_property_can_set: `ACTION`, `DEVPATH`, + * `INTERFACE`, `TAGS`, …, or anything starting `SYNTH_ARG_`). + * + * Quotes and C escapes may appear anywhere in the word, including inside the name (`F"OO"=bar`, + * `"FOO"=bar`, `FOO\x41=1` are all `FOO…=…` after extract_first_word); such words are unquoted and + * unescaped before the name is checked. + * + * Lenient corners: in the common `NAME=VALUE` spelling the escapes inside the value are not checked + * against cunescape_one's list (an unknown escape such as `\q` makes systemd ignore the whole + * assignment), the value's UTF-8 validity is not checked, and an unknown `%` specifier in the value is + * not flagged. An invalid word is logged and + * skipped by udev; an empty assignment clears the list. + */ +class ConfigParseUdevPropertyOptionValue : SimpleGrammarOptionValues( + "config_parse_udev_property", + SequenceCombinator( + PROPERTY_ASSIGNMENT, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), PROPERTY_ASSIGNMENT)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUplinkOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUplinkOptionValue.kt new file mode 100644 index 00000000..552d3792 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUplinkOptionValue.kt @@ -0,0 +1,49 @@ +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.IFNAME_OR_IFINDEX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.KeyOrSectionDispatchingGrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPServer] UplinkInterface=, [DHCPPrefixDelegation] UplinkInterface=, [IPv6SendRA] UplinkInterface=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#UplinkInterface= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1441-L1516 config_parse_uplink (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1463-L1475 section → accept_none (:none vs :self) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp6pd-upstream.network#L15 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-server-downstream.network#L13 + */ + +private fun uplink(vararg special: String) = AlternativeCombinator( + SequenceCombinator(FlexibleLiteralChoiceTerminal(*special), EOF()), + SequenceCombinator(IFNAME_OR_IFINDEX, EOF()), +) + +/** + * Validator for the three `UplinkInterface=` settings, all parsed by config_parse_uplink. + * + * Accepts `:auto`, a positive interface index, or an alternative interface name (see + * [IFNAME_OR_IFINDEX]), plus one section-specific word: `:none` in [DHCPServer] and [IPv6SendRA], `:self` + * in [DHCPPrefixDelegation] (the C sets `accept_none = false` for that section and its deprecated + * [DHCPv6PrefixDelegation] alias, which the plugin does not recognise as a section). The validator + * dispatches on the section of the property it checks. The special values are compared with streq, so + * they are case-sensitive. A `:none` in [DHCPPrefixDelegation] then fails ifname_valid_full (`:` is not + * a name character) and is ignored with a warning — flagged here. + */ +class ConfigParseUplinkOptionValue : KeyOrSectionDispatchingGrammarOptionValue( + VALIDATOR_NAME, + uplink(":auto", ":none", ":self"), + { _, section -> if (section == "DHCPPrefixDelegation" || section == "DHCPv6PrefixDelegation") SELF else NONE }, +) { + private companion object { + const val VALIDATOR_NAME = "config_parse_uplink" + val NONE = SimpleGrammarOptionValues(VALIDATOR_NAME, uplink(":auto", ":none")) + val SELF = SimpleGrammarOptionValues(VALIDATOR_NAME, uplink(":auto", ":self")) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVethExtraOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVethExtraOptionValue.kt new file mode 100644 index 00000000..c6cfe114 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVethExtraOptionValue.kt @@ -0,0 +1,53 @@ +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.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_DELIMITED_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON_FIELD_CHAR +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.ZeroOrOne + +/* + * [Network] VirtualEthernetExtra= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#VirtualEthernetExtra= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--network-veth-extra= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L455-L479 config_parse_veth_extra (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-network.c#L947-L975 veth_extra_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":", EXTRACT_DONT_COALESCE_SEPARATORS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L551-L552 + * linux https://man7.org/linux/man-pages/man7/netdevice.7.html + */ + +/** + * The container-side name. veth_extra_parse() does not reject an invalid one: if it is empty or fails + * ifname_valid() it is silently replaced by the host-side name. So any field is accepted here; the + * field ends at the next unescaped colon, because extract_first_word() (no flags) makes the character + * after a backslash literal — `a:b\:c` is host `a`, container `b:c` (invalid, so it falls back to `a`) + * — and refuses a lone trailing backslash. + */ +private val CONTAINER_SIDE = RegexTerminal("""$COLON_FIELD_CHAR+""", """$COLON_FIELD_CHAR+""") + +/** + * Validator for the .nspawn `[Network] VirtualEthernetExtra=` setting (config_parse_veth_extra, ltype 0). + * + * veth_extra_parse() takes `HOST[:CONTAINER]`, split with extract_first_word(":", + * EXTRACT_DONT_COALESCE_SEPARATORS). HOST must pass ifname_valid() (printable ASCII except `:`, `/`, `%`; + * at most 15 bytes; not all digits, `.`, `..`, `all` or `default`). CONTAINER may be omitted or empty. + * Anything after a second colon — even an empty field, as in `a:b:` — is an error. Errors are logged + * and the setting ignored. + * + * Deliberately lenient: an invalid CONTAINER name (too long, containing `/`, …) is accepted, because + * systemd silently falls back to the host-side name rather than rejecting it. + */ +class ConfigParseVethExtraOptionValue : SimpleGrammarOptionValues( + "config_parse_veth_extra", + SequenceCombinator( + COLON_DELIMITED_INTERFACE_NAME, + ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(CONTAINER_SIDE))), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt new file mode 100644 index 00000000..66fccd7e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.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.ExtractedWordTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WordEnd +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkParseRangeAccepts +import java.math.BigInteger + +/* + * [VLAN] EgressQOSMaps= / IngressQOSMaps= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#EgressQOSMaps= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vlan.c#L120-L180 config_parse_vlan_qos_maps (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +// parse_range() bounds as safe_atou() reads them (any unsigned int). The word was split on whitespace, so no +// whitespace reaches safe_atou(). The lower bound is cut at the first `-`, so it never sees a minus; the upper +// bound is the rest of the word, which may (`1--0`). +private val QOS_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = false, + acceptsMinus = false, +) +private val QOS_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = false, + acceptsMinus = true, +) +private val QOS_RANGE = SequenceCombinator(QOS_LOWER, ZeroOrOne(SequenceCombinator(HYPHEN, QOS_UPPER))) + +// The same bounds for a word whose quotes and escapes have been removed: quoted or escaped whitespace does reach +// safe_atou(). +private val QOS_UNQUOTED_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = false, +) +private val QOS_UNQUOTED_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// One word: the structured `from[-to]` grammar, bare or quoted as a whole, for highlighting; then an +// [ExtractedWordTerminal] fallback for quotes and C escapes anywhere in the word. +private val QOS_MAPPING = AlternativeCombinator( + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), QOS_RANGE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), QOS_RANGE, LiteralChoiceTerminal("'")), + QOS_RANGE, + ), + WordEnd("""\s"""), + ), + ExtractedWordTerminal("""\s""", cunescape = true, "range 0..4294967295") { word -> + netdevLinkParseRangeAccepts(word, QOS_UNQUOTED_LOWER, QOS_UNQUOTED_UPPER) + }, +) + +/** + * Validator for `[VLAN] EgressQOSMaps=` and `IngressQOSMaps=` (.netdev). + * + * C function: config_parse_vlan_qos_maps. A whitespace-separated list of words (each may be quoted, and + * may contain C escapes — the quotes and escapes are removed before parsing, see [ExtractedWordTerminal]); + * every word goes to parse_range(), so it is `from-to` or a single number meaning `from == to`, each + * side an unsigned int as safe_atou() reads it ([SafeAtoTerminal]: decimal, `0x` hex, octal, `0b`/`0o`). + * + * The man page says each integer is in 1…4294967294 and that `from` must be >= `to`; the parser + * enforces neither (it stores whatever parse_range returns), so this follows the C and accepts the full + * unsigned range and any order. A malformed word is logged and skipped by systemd. + */ +class ConfigParseVlanQosMapsOptionValue : SimpleGrammarOptionValues( + "config_parse_vlan_qos_maps", + SequenceCombinator(QOS_MAPPING, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), QOS_MAPPING)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt new file mode 100644 index 00000000..1eafd4bc --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [VLAN] Protocol= and [Bridge] VLANProtocol= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Protocol= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1832-L1862 config_parse_vlanprotocol (0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-bridge.netdev#L17 + */ + +/** + * Validator for `[VLAN] Protocol=` and `[Bridge] VLANProtocol=` (.netdev). + * + * C function: config_parse_vlanprotocol, which compares the value with STR_IN_SET against exactly four + * spellings: `802.1q` / `802.1Q` (ETH_P_8021Q) and `802.1ad` / `802.1AD` (ETH_P_8021AD). Mixed case + * such as `802.1Ad` is refused. An empty assignment resets to the kernel default. + */ +class ConfigParseVlanprotocolOptionValue : SimpleGrammarOptionValues( + "config_parse_vlanprotocol", + SequenceCombinator(FlexibleLiteralChoiceTerminal("802.1q", "802.1Q", "802.1ad", "802.1AD"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVolatileModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVolatileModeOptionValue.kt new file mode 100644 index 00000000..a8939f87 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVolatileModeOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Files] Volatile= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Volatile= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd-nspawn.html#--volatile=MODE + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L246 config_parse_volatile_mode (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/volatile-util.c#L36-L43 volatile_mode_table, DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32-L43 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-13-NSPAWN.nspawn.sh#L539 + */ + +/** + * Validator for the .nspawn `[Files] Volatile=` setting (config_parse_volatile_mode, ltype 0). + * + * volatile_mode_from_string() is a DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN lookup: any parse_boolean() + * spelling (case-insensitive — false means `no`, true means `yes`), or one of the table names `no`, + * `yes`, `state`, `overlay`, matched case-sensitively. + * + * `no` and `yes` are parse_boolean() spellings, so they are left to [BOOLEAN] (which also takes `Yes` + * and `NO`) rather than listed with the names. + */ +class ConfigParseVolatileModeOptionValue : SimpleGrammarOptionValues( + "config_parse_volatile_mode", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("state", "overlay"), + BOOLEAN, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt new file mode 100644 index 00000000..fbb9ea6a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING + +/* + * [VRF] Table= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Table= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vrf.c#L10-L39 config_parse_vrf_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + */ + +/** + * Validator for `[VRF] Table=` (.netdev). + * + * C function: config_parse_vrf_table → manager_get_route_table_from_string: `default`, `main`, + * `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 (safe_atou32, so hex, + * octal and `0b`/`0o` work; `0` is refused with ERANGE). See [ROUTE_TABLE_FROM_STRING]. + * + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name. What is flagged: `0`, out-of-range or + * malformed all-digit numbers (`00`, `08`), and anything containing `:` (`name:number` is networkd.conf + * syntax, not a table reference). An invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `Table=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. + */ +class ConfigParseVrfTableOptionValue : SimpleGrammarOptionValues( + "config_parse_vrf_table", + ROUTE_TABLE_FROM_STRING +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt new file mode 100644 index 00000000..f70ff004 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt @@ -0,0 +1,74 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import com.intellij.codeInspection.ProblemsHolder +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetdevLinkCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkIsMulticast + +/* + * [VXLAN] Group= / Local= / Remote= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Local= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vxlan.c#L220-L297 config_parse_vxlan_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/netdev-util.c#L9-L16 netdev_local_address_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L103-L113 in_addr_is_multicast + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +private const val VXLAN_VALIDATOR = "config_parse_vxlan_address" + +private val VXLAN_LOCAL_TYPE = FlexibleLiteralChoiceTerminal("ipv4_link_local", "ipv6_link_local", "dhcp4", "dhcp6", "slaac", "dhcp_pd") + +/** An address literal that is (or, with [multicast] false, is not) a multicast address. */ +private fun vxlanAddress(multicast: Boolean): Combinator = AlternativeCombinator( + NetdevLinkCheckedSpan(IPV6_ADDR, if (multicast) "multicast" else "not multicast") { netdevLinkIsMulticast(it) == multicast }, + NetdevLinkCheckedSpan(IPV4_ADDR, if (multicast) "multicast" else "not multicast") { netdevLinkIsMulticast(it) == multicast }, +) + +/** `Group=`: a multicast address. */ +private val VXLAN_GROUP = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(vxlanAddress(multicast = true), EOF())) + +/** `Remote=`: a unicast address. */ +private val VXLAN_REMOTE = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(vxlanAddress(multicast = false), EOF())) + +/** `Local=`: a local-address type name, or a unicast address. Addresses first, so the name terminal cannot take a prefix. */ +private val VXLAN_LOCAL = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(AlternativeCombinator(vxlanAddress(multicast = false), VXLAN_LOCAL_TYPE), EOF())) + +/** + * Validator for `[VXLAN] Group=`, `Local=` and `Remote=` (.netdev). + * + * C function: config_parse_vxlan_address, which branches on the key name. The value is an IPv4 or IPv6 + * literal (in_addr_from_string_auto → inet_pton). For `Local=` only, one of the + * netdev_local_address_type_table names (`ipv4_link_local`, `ipv6_link_local`, `dhcp4`, `dhcp6`, `slaac`, + * `dhcp_pd`) is accepted first, telling networkd to pick up the address that mechanism assigned. `Group=` + * must be a multicast address; `Local=` and `Remote=` must not be. + * + * Validators are looked up by (parser, ltype), not by key, so the key-specific rules are applied by + * choosing a per-key grammar in [generateProblemDescriptors]. [combinator] (used for colouring and + * completion) stays the union of the three. All of this was confirmed against systemd's parser built at + * 0f0682b773: `Group=dhcp4`, `Remote=slaac`, `Group=192.168.1.1`, `Remote=239.1.1.1` and + * `Local=239.1.1.1` are each logged and ignored. + */ +class ConfigParseVxlanAddressOptionValue : SimpleGrammarOptionValues( + VXLAN_VALIDATOR, + SequenceCombinator(AlternativeCombinator(IPV6_ADDR, IPV4_ADDR, VXLAN_LOCAL_TYPE), EOF()) +) { + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + when (property.key) { + "Group" -> VXLAN_GROUP.generateProblemDescriptors(property, holder) + "Remote" -> VXLAN_REMOTE.generateProblemDescriptors(property, holder) + "Local" -> VXLAN_LOCAL.generateProblemDescriptors(property, holder) + else -> super.generateProblemDescriptors(property, holder) + } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt new file mode 100644 index 00000000..eb33a27d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_SAFE_PREFIX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [WireGuardPeer] AllowedIPs= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#AllowedIPs= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L673-L749 config_parse_wireguard_allowed_ips (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("," WHITESPACE, no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L845-L897 in_addr_prefix_from_string_auto_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L779-L797 in_addr_parse_prefixlen (safe_atou8) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L15 AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24 + */ + +// One extract_first_word() separator run; "," and whitespace both separate and runs coalesce. +private val ALLOWED_IPS_SEPARATOR = RegexTerminal("[\\s,]+", "[\\s,]+") + +/** + * Validator for `[WireGuardPeer] AllowedIPs=` (.netdev). + * + * C function: config_parse_wireguard_allowed_ips. A list of IPv4/IPv6 addresses with optional prefix + * lengths, separated by commas and/or whitespace. Each entry goes to in_addr_prefix_from_string_auto: a + * missing length means the full width, `/0` is allowed, and the length is a safe_atou8 number, so `/0x18` + * works (see [IP_ADDR_AND_SAFE_PREFIX]). Separators coalesce, and leading or trailing ones are harmless. + * An address that is not properly masked (`192.168.26.3/24`) is accepted and masked with a warning. + * An invalid entry is logged and skipped by systemd; an empty assignment clears the list. + */ +class ConfigParseWireguardAllowedIpsOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_allowed_ips", + SequenceCombinator( + ZeroOrOne(ALLOWED_IPS_SEPARATOR), + IP_ADDR_AND_SAFE_PREFIX, + ZeroOrMore(SequenceCombinator(ALLOWED_IPS_SEPARATOR, IP_ADDR_AND_SAFE_PREFIX)), + ZeroOrOne(ALLOWED_IPS_SEPARATOR), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt new file mode 100644 index 00000000..2526f660 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt @@ -0,0 +1,81 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CREDENTIAL_REFERENCE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [WireGuardPeer] Endpoint= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Endpoint= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L751-L862 config_parse_wireguard_endpoint (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L220-L354 in_addr_port_ifindex_name_from_string_auto (no ifindex, no server name) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L718-L734 parse_ip_port + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape (via dns_name_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L156-L180 read_credential (credential_name_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-netlink.c#L356 TEST(in_addr_port_ifindex_name_from_string_auto) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L17 Endpoint=@network.wireguard.peer0.endpoint + */ + +// parse_ip_port: safe_atou16_full(…, SAFE_ATO_REFUSE_LEADING_WHITESPACE) (base 0), then 0 is refused. The port is +// everything after the last `:`, so whitespace can reach the parser: `\v80` and `0b 1` are accepted, ` 80` is not. +private val ENDPOINT_PORT = SafeAtoTerminal( + signed = false, + base = 0, + flags = setOf(SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// The hostname fallback: everything up to the LAST colon (strrchr), checked with dns_name_is_valid(): +// dot-separated labels of at most 63 characters, each character printable (>= ' ', not DEL) or one of +// the escapes `\.`, `\\` or `\DDD` with DDD at most 255 (dns_label_unescape refuses `k > 255`). A single trailing dot is allowed, and dns_name_is_valid() also +// accepts the root name — `.` or nothing at all — so `:51820` passes the parser too. +private const val DNS_LABEL = """(?:[^.\\\x00-\x1F\x7F]|\\[.\\]|\\(?:[01][0-9]{2}|2[0-4][0-9]|25[0-5])){1,63}""" +private val ENDPOINT_HOST = RegexTerminal( + """[^\x00-\x1F\x7F]+(?=:[^:]*$)""", + """(?:$DNS_LABEL(?:\.$DNS_LABEL)*\.?|\.)(?=:[^:]*$)""", +) + +/** + * Validator for `[WireGuardPeer] Endpoint=` (.netdev). + * + * C function: config_parse_wireguard_endpoint. In the order systemd tries them: + * + * - `@name` reads the endpoint from a credential; + * - in_addr_port_ifindex_name_from_string_auto with no interface and no server name: `[IPv6]:port`, + * `IPv4:port`, or — because that helper makes the port optional — a bare IPv4 or IPv6 address (the + * man page says a port is required; the parser stores port 0); + * - otherwise `host:port`, split at the last colon, where the host only has to pass + * dns_name_is_valid(), which is permissive (any printable characters, labels up to 63 bytes). + * + * The port is a parse_ip_port() number, 1…65535 (safe_atou16 base 0, so hex, octal and `0b`/`0o` work, and + * a leading space is refused while a leading `\v` is not). Because the host + * fallback is so permissive, something like `[fe80::1%eth0]:51820` is accepted (as a hostname), as it + * is by systemd at parse time. What is flagged: a missing or invalid port, empty labels, over-long labels + * and control characters. Lenient: the overall name length (DNS_HOSTNAME_MAX, enforced by + * dns_name_concat) and the 63-byte label limit, which is counted here in characters, not UTF-8 bytes. An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardEndpointOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_endpoint", + AlternativeCombinator( + SequenceCombinator(CREDENTIAL_REFERENCE, EOF()), + SequenceCombinator(LiteralChoiceTerminal("["), IPV6_ADDR, LiteralChoiceTerminal("]"), COLON, ENDPOINT_PORT, EOF()), + SequenceCombinator(IPV4_ADDR, COLON, ENDPOINT_PORT, EOF()), + SequenceCombinator(IPV6_ADDR, EOF()), + SequenceCombinator(IPV4_ADDR, EOF()), + SequenceCombinator(ZeroOrOne(ENDPOINT_HOST), COLON, ENDPOINT_PORT, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt new file mode 100644 index 00000000..daf299e9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuardPeer] PublicKeyFile= / PresharedKeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PublicKeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L623-L671 config_parse_wireguard_peer_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `[WireGuardPeer] PublicKeyFile=` and `PresharedKeyFile=` (.netdev). + * + * C function: config_parse_wireguard_peer_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseWireguardPeerKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_peer_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt new file mode 100644 index 00000000..14ab1c47 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_FALSE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuardPeer] RouteTable= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#RouteTable= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L941-L990 config_parse_wireguard_peer_route_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L10 + */ + +/** + * Validator for `[WireGuardPeer] RouteTable=` (.netdev). + * + * C function: config_parse_wireguard_peer_route_table. A value that parse_boolean() reads as false (`no`, `off`, `0`, …) + * disables route configuration; anything else is a route table for manager_get_route_table_from_string: + * `default`, `main`, `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 + * (safe_atou32, so hex, octal and `0b`/`0o` work). See [ROUTE_TABLE_FROM_STRING]. An empty assignment falls back to the `[WireGuard]` section's table. + * + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name — including `yes`, which is only valid if + * networkd.conf defines it. What is flagged: out-of-range or malformed all-digit numbers (`4294967296`, + * `08`) and anything containing `:` (`name:number` is networkd.conf syntax, not a table reference). An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardPeerRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_peer_route_table", + AlternativeCombinator( + SequenceCombinator(BOOLEAN_FALSE, EOF()), + ROUTE_TABLE_FROM_STRING, + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt new file mode 100644 index 00000000..4e818fc7 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuard] PrivateKeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PrivateKeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L563-L591 config_parse_wireguard_private_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `[WireGuard] PrivateKeyFile=` (.netdev). + * + * C function: config_parse_wireguard_private_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseWireguardPrivateKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_private_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt new file mode 100644 index 00000000..db993546 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CREDENTIAL_REFERENCE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WIREGUARD_BASE64_KEY + +/* + * [WireGuard] PrivateKey= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PrivateKey= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L546-L561 config_parse_wireguard_private_key (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L488-L544 wireguard_decode_key_and_warn + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755-L851 unbase64mem_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L528-L555 unbase64char (also accepts the URL-safe '-' and '_') + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L156-L180 read_credential (credential_name_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard-23-peers.netdev#L12 PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev.d/private-key.conf#L2 (same key, drop-in) + */ + +/** + * Validator for `[WireGuard] PrivateKey=` (.netdev). + * + * C function: config_parse_wireguard_private_key → wireguard_decode_key_and_warn. Either `@name`, + * which reads the key from the named credential (the name must pass credential_name_valid), or the key + * itself in base64, which must decode to exactly 32 bytes: 43 characters plus one mandatory `=` of + * padding, the 43rd character restricted to those whose low two bits are clear. See + * [WIREGUARD_BASE64_KEY]. An invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardPrivateKeyOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_private_key", + SequenceCombinator(AlternativeCombinator(CREDENTIAL_REFERENCE, WIREGUARD_BASE64_KEY), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt new file mode 100644 index 00000000..0c1e49e5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_FALSE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuard] RouteTable= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#RouteTable= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L905-L939 config_parse_wireguard_route_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L10 + */ + +/** + * Validator for `[WireGuard] RouteTable=` (.netdev). + * + * C function: config_parse_wireguard_route_table. A value that parse_boolean() reads as false (`no`, `off`, `0`, …) + * disables route configuration; anything else is a route table for manager_get_route_table_from_string: + * `default`, `main`, `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 + * (safe_atou32, so hex, octal and `0b`/`0o` work). See [ROUTE_TABLE_FROM_STRING]. An empty assignment also disables it. + * + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name — including `yes`, which is only valid if + * networkd.conf defines it. What is flagged: out-of-range or malformed all-digit numbers (`4294967296`, + * `08`) and anything containing `:` (`name:number` is networkd.conf syntax, not a table reference). An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_route_table", + AlternativeCombinator( + SequenceCombinator(BOOLEAN_FALSE, EOF()), + ROUTE_TABLE_FROM_STRING, + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt new file mode 100644 index 00000000..761800da --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MAC_ADDRESS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SAFE_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] WakeOnLanPassword= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#WakeOnLanPassword= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2050-L2093 config_parse_wol_password (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L134-L155 link_parse_wol_password → parse_ether_addr + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/ether-addr-util.c#L282-L295 parse_ether_addr (parse_hw_addr_full, ETH_ALEN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.h#L152-L154 path_is_safe + */ + +/** + * Validator for `[Link] WakeOnLanPassword=` (.link). + * + * C function: config_parse_wol_password. If the value is an absolute path that path_is_safe() accepts + * (no `..` component) it names a file to read the password from; otherwise it must be the 6-byte + * SecureOn password written like an Ethernet address (parse_ether_addr: `00:11:22:33:44:55`, + * `00-11-22-33-44-55` or `0011.2233.4455`). The path is taken verbatim (no specifiers, spaces are + * ordinary characters). An invalid value is logged and ignored by systemd. + */ +class ConfigParseWolPasswordOptionValue : SimpleGrammarOptionValues( + "config_parse_wol_password", + AlternativeCombinator( + SequenceCombinator(MAC_ADDRESS, EOF()), + SequenceCombinator(SAFE_ABSOLUTE_PATH, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt new file mode 100644 index 00000000..fbab9d1d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.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.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PRINTF_CHAR +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.printfCharExcluding + +/* + * XAttrEntryPoint= / XAttrListen= / XAttrAccept= ([Socket]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#XAttrEntryPoint= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2269-L2327 config_parse_xattr (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-util.c#L1049 split_pair (first "=") + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L249 unit_full_printf (name and value) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/specifier.c#L33 specifier_printf (unknown alphanumeric specifier = -EBADSLT) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-74-AUX-UTILS.socket-xattr.sh#L55-L57 user.varlink=entrypoint / listen / server + * linux https://man7.org/linux/man-pages/man7/xattr.7.html (the user. namespace) + */ + +/** + * Validator for the socket extended-attribute settings, `NAME=VALUE`. + * + * config_parse_xattr() splits at the first `=` (split_pair — no `=` at all is "Failed to parse extended + * attribute expression, ignoring"), expands specifiers in both halves, and then requires the expanded + * name to start with `user.` (the only xattr namespace an unprivileged socket inode can carry, see + * xattr(7)). Everything after the first `=` is the value and may contain further `=` or be empty. + * + * The only other failure is specifier expansion: an unknown alphanumeric `%x` is an error, while `%%`, + * a known specifier, or `%` before a non-alphanumeric character is fine ([PRINTF_CHAR]). + * + * Deliberately strict on one corner: the name must start with a literal `user.`. A name that only + * becomes `user.…` after specifier expansion is theoretically possible but not realistic. + * + * Covers: Socket.XAttrEntryPoint, Socket.XAttrListen, Socket.XAttrAccept. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseXattrOptionValue : SimpleGrammarOptionValues( + "config_parse_xattr", + SequenceCombinator( + LiteralChoiceTerminal("user."), + RegexTerminal("[^=]*", "${printfCharExcluding("=")}*"), + LiteralChoiceTerminal("="), + RegexTerminal("[\\s\\S]*", "$PRINTF_CHAR*"), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt index 1aa294f2..a73d2188 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt @@ -33,9 +33,10 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * actually reads the value. The classes below mirror that table entry by entry, so each one is * registered under the matching ROUTING_POLICY_RULE_* ltype. * - * Not covered here: ROUTING_POLICY_RULE_TABLE (route table names are user-defined via RouteTable= in - * networkd.conf, so there is no closed set to check), ROUTING_POLICY_RULE_FWMARK and - * ROUTING_POLICY_RULE_IP_PROTOCOL. + * Covered in their own files instead: ROUTING_POLICY_RULE_TABLE (ConfigParseRouteTableOptionValue), + * ROUTING_POLICY_RULE_FWMARK (ConfigParseRoutingPolicyRuleFirewallMarkOptionValue), + * ROUTING_POLICY_RULE_IP_PROTOCOL (ConfigParseRoutingPolicyRuleIpProtocolOptionValue) and + * ROUTING_POLICY_RULE_UID_RANGE (ConfigParseRoutingPolicyRuleUidRangeOptionValue). */ private const val RULE = "config_parse_routing_policy_rule" diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchExecCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchExecCombinators.kt new file mode 100644 index 00000000..2c10db38 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchExecCombinators.kt @@ -0,0 +1,178 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +/* + * Shared value grammars for the ExecContext sandboxing / image / credential / environment settings in + * systemd src/core/load-fragment.c (exec batch, 2026-10). Everything here is pinned to systemd + * 0f0682b773, the commit recorded in systemd-build/build/last_commit_hash. + * + * words https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word + * escape https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one + * path https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn + * b64 https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755 unbase64mem_full + * + * The grammar sees the RAW value text, before systemd unquotes or unescapes anything, so the terminals + * below model the raw spelling of a word. Most of these settings split their value twice: + * + * 1. into whitespace-separated words with extract_first_word(…, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) + * — quotes are removed (and may appear anywhere in a word), backslashes are kept verbatim; + * 2. each word into ':'-separated fields with EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS — so a + * backslash must start a valid C escape (cunescape_one) or escape the ':' separator (`\:`). + * + * Deliberate approximations, shared by every grammar built from these pieces: + * - a quoted run ("…" or '…') is treated as opaque field text. systemd unquotes BEFORE splitting on + * ':', so a ':' inside quotes is really a separator. The grammar keeps it inside the field, which is + * usually lenient, but NOT always: when such a run is followed by more ':'-fields outside the quotes + * (`BindPaths="/a:/b":rbind`, `MountImages="/a.raw:/mnt":ro`), or a literal field such as rbind or + * a partition designator is itself quoted (`BindPaths=/a:/b:"rbind"`, `RootImageOptions="root":ro`), + * the field count shifts and a value systemd accepts is flagged. None of these spellings occurs in + * the corpus; modelling them needs a combinator that matches against the unquoted text. + * Escapes inside quotes are not checked. + * - under EXTRACT_RETAIN_ESCAPE a backslash is not special in the outer split, so a raw `\"` keeps the + * backslash and then opens a quote. The grammar therefore does not treat `\"` / `\'` as escapes + * outside quotes (an unbalanced quote is refused by systemd, and is flagged here too). + * - '%' specifiers are accepted wherever a path may start, without checking that the specifier exists + * or that it expands to an absolute path. + */ + +// `\uXXXX`: not NUL, not a UTF-16 surrogate (D800-DFFF). Noncharacters such as FFFE are deliberately allowed. +private const val ESCAPE_U16 = """u(?!0000)(?![dD][89abAB])[0-9a-fA-F]{4}""" + +// `\UXXXXXXXX`: not NUL, and unichar_is_valid(): below 0x110000, not a surrogate, not FDD0-FDEF, and the +// low 16 bits not FFFE/FFFF. +private const val ESCAPE_U32 = "U(?!0{8})(?=00(?:0[0-9a-fA-F]|10)[0-9a-fA-F]{4})(?!0000[dD][89abAB])" + + "(?!0000[fF][dD][dDeE])(?![0-9a-fA-F]{4}[fF]{3}[eEfF])[0-9a-fA-F]{8}" + +private const val C_ESCAPE_COMMON = """x(?!00)[0-9a-fA-F]{2}|$ESCAPE_U16|$ESCAPE_U32|(?!000)[0-3][0-7]{2}""" + +/** + * A valid C escape for cunescape_one, minus the leading backslash. NUL bytes, octal > 0377, UTF-16 + * surrogates in `\u` and invalid code points in `\U` (unichar_is_valid) are refused. + */ +internal const val C_ESCAPE_BODY = """(?:[abfnrtv\\"'s]|$C_ESCAPE_COMMON)""" + +// As C_ESCAPE_BODY but without `\"` / `\'`, which under EXTRACT_RETAIN_ESCAPE are not escapes in the outer +// split (the backslash is kept and the quote opens a quoted run). +private const val RETAINED_ESCAPE_BODY = """(?:[abfnrtv\\s]|$C_ESCAPE_COMMON)""" + +// --------------------------------------------------------------------------------------------------- +// ':'-separated fields of a word that was split with EXTRACT_RETAIN_ESCAPE and is then re-split with +// EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS (BindPaths=, MountImages=, ExtensionImages=, +// RootImageOptions=). + +// Lenient shape: any run of non-blank, non-':' characters, quoted runs, and backslash + non-blank. +// A backslash followed by whitespace is never valid here: the outer split keeps the backslash and cuts +// the word at the blank, leaving a trailing backslash that the inner split refuses with -EINVAL. +private const val RETAINED_FIELD_SYNTAX = """(?:[^\s:"'\\]|"[^"]*"|'[^']*'|\\[^\s"']|\\(?=["']))+""" + +// Strict body: as above, but a backslash must start a C escape or escape ':' itself. A backslash before a +// quote is kept verbatim by the outer split and the quote then opens a quoted run, which must be closed; +// the escape the backslash forms with the run's first character is not checked. +private const val RETAINED_FIELD_BODY = """(?:[^\s:"'\\]|"[^"]*"|'[^']*'|\\(?:$RETAINED_ESCAPE_BODY|:|(?=["'])))+""" + +// The unquoted field must start with '/' (absolute) or a specifier. Quotes are skipped over first. +private const val ABSOLUTE_HEAD = """(?=["']*[/%])""" + +// path_is_normalized() refuses a surviving ".." component (path_simplify() has already removed "." and +// "//"). The optional `[^\s:]*/` swallows leading directories so "/a/../b" and "/a/.." are caught but +// "/a/..b" and "/a/b.." are not. +private const val NO_DOT_DOT_COMPONENT = """(?!(?:[^\s:]*/)?\.\.(?:[/\s:"']|$))""" + +// path_below_api_vfs(): "/proc", "/sys" and (unless DEV_OK) "/dev", as whole leading components. +private const val NOT_PROC_OR_SYS = """(?!["']*-?["']*/+(?:proc|sys)(?:[/\s:"']|$))""" +private const val NOT_API_VFS = """(?!["']*-?["']*/+(?:proc|sys|dev)(?:[/\s:"']|$))""" + +// Every semantic regex in this file ends with a lookahead that stops it matching a strict prefix of the +// span its syntactic regex claimed. The classic engine localises a semantic failure by re-walking the +// syntactic token list, and a semantic match that ends mid-token makes it compute a highlight range past +// the end of the value (IntelliJ then logs "rangeInElement … must not exceed …"). +private const val FIELD_END = """(?![^\s:])""" + +private fun retainedField(head: String) = RegexTerminal(RETAINED_FIELD_SYNTAX, head + RETAINED_FIELD_BODY + FIELD_END) + +/** An absolute path field (after unit_path_printf), as path_simplify_and_warn(PATH_CHECK_ABSOLUTE) checks it. */ +val RETAINED_ABSOLUTE_PATH_FIELD = retainedField(NO_DOT_DOT_COMPONENT + ABSOLUTE_HEAD) + +/** An absolute path field optionally prefixed by `-` ("ignore if missing"), as BindPaths= sources are. */ +val RETAINED_OPTIONAL_ABSOLUTE_PATH_FIELD = retainedField(NO_DOT_DOT_COMPONENT + """(?=["']*-?["']*[/%])""") + +/** MountImages= source: `-`-prefixable absolute path, not below /proc or /sys (PATH_CHECK_NON_API_VFS_DEV_OK). */ +val MOUNT_IMAGE_SOURCE_FIELD = retainedField(NOT_PROC_OR_SYS + NO_DOT_DOT_COMPONENT + """(?=["']*-?["']*[/%])""") + +/** MountImages= destination: absolute path, not below /proc or /sys (PATH_CHECK_NON_API_VFS_DEV_OK). */ +val MOUNT_IMAGE_DESTINATION_FIELD = retainedField(NOT_PROC_OR_SYS + NO_DOT_DOT_COMPONENT + ABSOLUTE_HEAD) + +/** ExtensionImages= source: `-`-prefixable absolute path, not below /proc, /sys or /dev (PATH_CHECK_NON_API_VFS). */ +val EXTENSION_IMAGE_SOURCE_FIELD = retainedField(NOT_API_VFS + NO_DOT_DOT_COMPONENT + """(?=["']*-?["']*[/%])""") + +/** + * A free-form field — mount options, which systemd only runs through unit_full_printf() and hands to + * mount(8) later. Only the escape rules are checked. + */ +val RETAINED_FREE_FIELD = retainedField("") + +/** + * The ':' run between fields of a MountImages=/ExtensionImages=/RootImageOptions= word. Those three + * split WITHOUT EXTRACT_DONT_COALESCE_SEPARATORS, so `a::b` is the same as `a:b` and leading/trailing + * colons are skipped. + */ +val EXEC_COALESCED_COLONS = RegexTerminal(":+", ":+") + +/** + * A partition designator — partition_designator_from_string (src/shared/gpt.c#L156-L183). The table + * names, plus the "-vty"/"-vsig" shorthands that partition_name_expand() rewrites to "-verity" / + * "-verity-sig" (the "2" = "-secondary" shorthand expands to names the table does not contain, so it + * is never accepted here). Matching is case-sensitive (string_table_lookup_from_string uses streq). + */ +fun partitionDesignator() = FlexibleLiteralChoiceTerminal( + "root", "usr", "home", "srv", "esp", "xbootldr", "swap", + "root-verity", "usr-verity", "root-verity-sig", "usr-verity-sig", "tmp", "var", + "root-vty", "usr-vty", "root-vsig", "usr-vsig", +) + +/** + * The trailing `[:OPTIONS | (:PARTITION:OPTIONS)*[:OPTIONS]]` part of a MountImages=/ExtensionImages= + * word: the loop in config_parse_mount_images / config_parse_extension_images reads pairs with + * extract_many_words(partition, options); a lone trailing word is the root partition's options, while + * a pair whose first word is not a partition designator is logged and skipped. + */ +fun imageMountOptionsTail(): Combinator = SequenceCombinator( + ZeroOrMore(SequenceCombinator(EXEC_COALESCED_COLONS, partitionDesignator(), EXEC_COALESCED_COLONS, RETAINED_FREE_FIELD)), + optionalNoReprobe(SequenceCombinator(EXEC_COALESCED_COLONS, RETAINED_FREE_FIELD)), + ZeroOrOne(EXEC_COALESCED_COLONS), +) + +private val EMPTY = RegexTerminal("", "") + +/** + * Like [ZeroOrOne], but without its second probe. ZeroOrOne tries its inner combinator again after a + * match to report how far input "could" reach; when the inner can match twice in a row (`:OPTIONS` can), + * a semantic failure further left then reports a longest-match at the very end of the value, and the + * classic engine's error localisation indexes one past its token list (IndexOutOfBoundsException, + * logged as an internal error). An alternative with an empty branch has no such probe. + */ +fun optionalNoReprobe(inner: Combinator): Combinator = AlternativeCombinator(inner, EMPTY) + +/** A whitespace-separated list of [item]s ending the value. */ +fun whitespaceList(item: Combinator): Combinator = SequenceCombinator( + item, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), item)), + EOF(), +) + +// --------------------------------------------------------------------------------------------------- +// Base64 — unbase64mem_full / unbase64_next / unbase64char (src/basic/hexdecoct.c#L528-L850). +// +// Whitespace is skipped anywhere. Both the standard (+/) and URL-safe (-_) alphabets are accepted. The +// non-blank characters must form complete 4-character groups; padding may only appear as the last one +// or two characters of the last group, and then the bits it discards must be zero (b & 15 / c & 3). + +private const val B64 = """[A-Za-z0-9+/_-]""" +private const val B64_GROUPS = """(?:(?:\s*$B64){4})*""" +private const val B64_TAIL = + """(?:\s*$B64\s*[AQgw]\s*=\s*=|\s*$B64\s*$B64\s*[AEIMQUYcgkosw048]\s*=)?\s*""" + +/** Base64 as unbase64mem() accepts it, possibly empty. */ +val BASE64_DATA = RegexTerminal("""[\s\S]*""", """$B64_GROUPS$B64_TAIL(?![\s\S])""") + +/** Base64 as unbase64mem() accepts it, at least one group. */ +val NON_EMPTY_BASE64_DATA = RegexTerminal("""[\s\S]+""", """(?=\s*\S)$B64_GROUPS$B64_TAIL(?![\s\S])""") diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt new file mode 100644 index 00000000..10ad2f15 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -0,0 +1,517 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigInteger + +/* + * Shared grammars for the systemd-networkd .netdev (src/network/netdev/) and udev .link (src/udev/net/) + * validators added in the 2026-10 netdev-link batch. + * + * Everything here is pinned to systemd 0f0682b773, the commit recorded in + * systemd-build/build/last_commit_hash. Browse it at https://github.com/systemd/systemd/blob/0f0682b773. + * + * These live in their own file (rather than Combinators.kt) only because several batches were written + * in parallel. Several are general-purpose and are good candidates for promotion — see the batch's + * RECOMMENDATIONS.md. + */ + +// --------------------------------------------------------------------------------------------------- +// parse_range() — src/basic/parse-util.c. Its bounds are safe_atou() numbers, modelled by [SafeAtoTerminal]. + +/** + * Whether parse_range() (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340) + * accepts the already-extracted [word]: the text before the first `-` must satisfy [lower] and, if there is a `-`, + * the non-empty rest must satisfy [upper]. A predicate for an [ExtractedWordTerminal], whose words have had their + * quotes and escapes removed first; the structured grammar spells the same check out with the terminals. + */ +fun netdevLinkParseRangeAccepts(word: String, lower: SafeAtoTerminal, upper: SafeAtoTerminal): Boolean { + val dash = word.indexOf('-') + if (dash < 0) return lower.valueOf(word) != null + val upperText = word.substring(dash + 1) + return lower.valueOf(word.substring(0, dash)) != null && upperText.isNotEmpty() && upper.valueOf(upperText) != null +} + + +// --------------------------------------------------------------------------------------------------- +// Words as extract_first_word() (src/basic/extract-word.c) returns them. + +/** + * A zero-width check that the input ends here or continues with one of [separators] (a regex character + * class body such as `\s,`). Put after a structured word so that, under the classic first-match engine, + * an [AlternativeCombinator] does not commit to a branch that matched only a prefix of the word. + */ +class WordEnd(separators: String) : Combinator { + private val separator = Regex("[$separators]") + + private fun at(value: String, offset: Int): Boolean = + offset == value.length || separator.matchesAt(value, offset) + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = + if (at(value, offset)) MatchResult(emptyList(), offset, emptyList(), offset) else NoMatch + + override fun SemanticMatch(value: String, offset: Int): MatchResult = SyntacticMatch(value, offset) + + override fun parse(value: String, offset: Int): Sequence = + if (at(value, offset)) sequenceOf(Parse(offset, emptyList())) else sequenceOf(Stuck(offset, setOf(this))) + + override fun toStringIndented(indent: Int): String = "WordEnd" +} + +/** + * One raw word of an extract_first_word(…, separators, EXTRACT_UNQUOTE[|EXTRACT_CUNESCAPE]) list, checked + * the way systemd checks it: AFTER the quotes (which may open and close anywhere in the word, as in + * `0-"3"`) and backslash escapes are removed. + * + * It matches a run of non-separator characters, backslash escapes and balanced quoted runs (quoted runs + * may contain separators) — but ONLY when that word contains a quote or backslash, [extractWord] succeeds + * on it and [check] accepts the result. Use it as the last alternative after a structured grammar for the + * common spelling: the common spelling keeps its highlighting and error localization, and this only + * rescues the exotic quoting and escaping systemd also accepts. + * + * It deliberately has no "syntactically right but semantically wrong" state. Under the classic engine + * GrammarOptionValue locates a semantic error by lining the semantic match's longestMatch up against the + * syntactic tokens; a whole-word token that only fails semantically would be misaligned with the + * structured branch's partial progress and produce an out-of-range highlight. So an invalid word simply + * does not match, and the value is reported as not matching the expected format. + * + * @param separators a regex character-class body for the separator set, e.g. `\s` or `\s,` + * @param cunescape whether the caller passes EXTRACT_CUNESCAPE + */ +class ExtractedWordTerminal( + separators: String, + private val cunescape: Boolean, + private val description: String, + private val check: (String) -> Boolean, +) : TerminalCombinator { + + private val shape = Regex("""(?:[^$separators"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""") + + /** The word at [offset] if it is one this terminal accepts, else null. */ + private fun word(value: String, offset: Int): String? { + val text = shape.matchAt(value, offset)?.value ?: return null + if (text.none { it == '"' || it == '\'' || it == '\\' }) return null + return text.takeIf { extractWord(it, cunescape)?.let(check) == true } + } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = word(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult = SyntacticMatch(value, offset) + + override fun parse(value: String, offset: Int): Sequence { + val text = word(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, true)))) + } + + override fun toString(): String = "ExtractedWord($description)" +} + +/** + * The text extract_first_word() returns for the single raw word [raw] under EXTRACT_UNQUOTE (plus + * EXTRACT_CUNESCAPE when [cunescape]), or null where it fails: an unbalanced quote, a trailing + * backslash, or — with [cunescape] — an escape cunescape_one() (src/basic/escape.c) refuses. Without + * [cunescape], a backslash simply makes the next character literal. + */ +fun extractWord(raw: String, cunescape: Boolean): String? { + val out = StringBuilder() + var quote: Char? = null + var i = 0 + while (i < raw.length) { + val c = raw[i] + if (c == '\\') { + if (i + 1 >= raw.length) return null + if (cunescape) { + val (text, length) = cunescapeOne(raw, i + 1) ?: return null + out.append(text) + i += 1 + length + } else { + out.append(raw[i + 1]) + i += 2 + } + continue + } + when { + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> out.append(c) + } + i++ + } + return if (quote == null) out.toString() else null +} + +/** cunescape_one(…, accept_nul = false) on the escape starting at [p]: the decoded text and its length. */ +private fun cunescapeOne(s: String, p: Int): Pair? { + fun digits(count: Int, radix: Int): Int? = + if (p + 1 + count > s.length) null else s.substring(p + 1, p + 1 + count).let { d -> if (d.all { Character.digit(it, radix) >= 0 }) d.toInt(radix) else null } + + return when (val c = s[p]) { + 'a' -> "\u0007" to 1 + 'b' -> "\b" to 1 + 'f' -> "\u000C" to 1 + 'n' -> "\n" to 1 + 'r' -> "\r" to 1 + 't' -> "\t" to 1 + 'v' -> "\u000B" to 1 + '\\', '"', '\'' -> c.toString() to 1 + 's' -> " " to 1 + 'x' -> digits(2, 16)?.takeIf { it != 0 }?.let { it.toChar().toString() to 3 } + 'u' -> digits(4, 16)?.takeIf { it != 0 && it !in 0xD800..0xDFFF }?.let { it.toChar().toString() to 5 } + 'U' -> { + if (p + 9 > s.length) return null + val v = s.substring(p + 1, p + 9).toLongOrNull(16) ?: return null + if (v == 0L || v > 0x10FFFF || v in 0xD800..0xDFFF) return null + String(Character.toChars(v.toInt())) to 9 + } + in '0'..'7' -> { + if (p + 3 > s.length) return null + val d = s.substring(p, p + 3) + if (!d.all { it in '0'..'7' }) return null + val v = d.toInt(8) + if (v == 0 || v > 255) null else v.toChar().toString() to 3 + } + else -> null + } +} + +// --------------------------------------------------------------------------------------------------- +// CPU sets — config_parse_cpu_set (src/shared/cpu-set-util.c). + +// One extract_first_word() separator run: any mix of whitespace and commas (separators coalesce). +private val CPU_SET_SEPARATOR = RegexTerminal("""[\s,]+""", """[\s,]+""") + +// parse_range() bounds as safe_atou() reads them, below CPU_SET_MAX_NCPU (8192, cpu_set_add_range). The word was +// split on whitespace and commas, so no whitespace can reach safe_atou(). The lower bound is cut at the first `-` +// by parse_range(), so it never sees a minus; the upper bound is the rest of the word, which may (`0--0`). +private val CPU_SET_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = false, +) +private val CPU_SET_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = true, +) +private val CPU_SET_RANGE = SequenceCombinator(CPU_SET_LOWER, ZeroOrOne(SequenceCombinator(HYPHEN, CPU_SET_UPPER))) + +// The same bounds for a word whose quotes have been removed: quoted whitespace does reach safe_atou() (`" 3"`). +private val CPU_SET_UNQUOTED_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = true, + acceptsMinus = false, +) +private val CPU_SET_UNQUOTED_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// One word: the structured `N[-M]` grammar, bare or quoted as a whole, for highlighting; then an +// [ExtractedWordTerminal] fallback for quotes and escapes anywhere in the word (`0-"3"`, `'0'-'3'`). +private val CPU_SET_ITEM = AlternativeCombinator( + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), CPU_SET_RANGE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), CPU_SET_RANGE, LiteralChoiceTerminal("'")), + CPU_SET_RANGE, + ), + WordEnd("""\s,"""), + ), + ExtractedWordTerminal("""\s,""", cunescape = false, "range 0..8191") { word -> + netdevLinkParseRangeAccepts(word, CPU_SET_UNQUOTED_LOWER, CPU_SET_UNQUOTED_UPPER) + }, +) + +/** + * A CPU set: CPU indices and `N-M` ranges separated by whitespace and/or commas. + * + * config_parse_cpu_set splits with extract_first_word(…, WHITESPACE ",", EXTRACT_UNQUOTE), so separators + * coalesce, leading and trailing separators are harmless (`0, 1, 2, 3, 4, 5, 6, 7, 63, ` is in + * test-cpu-set-util.c), and a word may be quoted — anywhere in the word, not only around it (`0-"3"` is + * `0-3`; the [ExtractedWordTerminal] fallback handles that). Each word goes to parse_range(), whose bounds are + * safe_atou() numbers ([SafeAtoTerminal]: hex, octal, `0b`/`0o`) — and cpu_set_add_range() refuses anything at or above + * CPU_SET_MAX_NCPU (8192). A range whose lower end exceeds its upper end (`3-0`) is rejected by systemd + * after parsing; that compares numbers and is not modelled. + */ +val CPU_SET: Combinator = SequenceCombinator( + ZeroOrOne(CPU_SET_SEPARATOR), + CPU_SET_ITEM, + ZeroOrMore(SequenceCombinator(CPU_SET_SEPARATOR, CPU_SET_ITEM)), + ZeroOrOne(CPU_SET_SEPARATOR), +) + + +// --------------------------------------------------------------------------------------------------- +// Sizes — parse_size() (src/basic/parse-util.c). + +// strtoull(…, 10) after parse_size has skipped whitespace: an optional `+` (a `-` is refused), digits, +// then an optional `.` and fraction. parse_size steps over the `.` unconditionally and ignores a +// missing fraction ("E.g. 10.M is valid"), so `10.` and `10.M` are both legal. +// Labeled so the number colours as a literal, like ByteSizeTerminal does. +private val SIZE_NUMBER = Labeled(Role.LITERAL, RegexTerminal("""\+?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""")) + +/** + * A parse_size() value with base 1000 (the SI table: `E`, `P`, `T`, `G`, `M`, `K`, `B` and bare). + * + * parse_size accepts a run of `number[suffix]` elements whose suffixes strictly descend through the + * table (`start_pos = i + 1`), with optional whitespace before each number and between a number and its + * suffix. The bare (suffix-less) element is the last table entry, so it can only come last. That + * ordering is spelled out structurally: [sizeChain] for table index `i` offers every suffix from `i` + * onwards, each optionally followed by a chain starting after it. + * + * Suffixes are case-sensitive: `1k` is not a size. + */ +fun siSize(): Combinator = sizeChain(listOf("E", "P", "T", "G", "M", "K", "B")) + +private fun sizeChain(suffixes: List): Combinator { + val ws = ZeroOrOne(WhitespaceTerminal()) + // Built from the last suffix backwards so each chain reuses the shorter one after it. + var chainAfter: Combinator? = null + val chains = ArrayList() + // The bare element (no suffix) is always the final alternative of every chain. + val bare: Combinator = SequenceCombinator(ws, SIZE_NUMBER) + for (i in suffixes.indices.reversed()) { + val element = SequenceCombinator(ws, SIZE_NUMBER, ws, LiteralChoiceTerminal(suffixes[i])) + val withRest = if (chainAfter == null) SequenceCombinator(element, ZeroOrOne(bare)) + else SequenceCombinator(element, ZeroOrOne(chainAfter)) + chains.add(0, withRest) + chainAfter = AlternativeCombinator(*chains.toTypedArray(), bare) + } + return chainAfter ?: bare +} + + +// --------------------------------------------------------------------------------------------------- +// Durations — parse_nsec() (src/basic/time-util.c). + +// strtoll(…, 10) as parse_nsec drives it: an optional `+` (a leading `-` is refused), then digits with +// an optional fraction — or a bare fraction like `.5`, which parse_nsec accepts because it checks for +// `.` before it checks whether strtoll consumed anything. A trailing `.` with no digits (`3.`) is refused. +private val DURATION_NUMBER = Labeled( + Role.LITERAL, + RegexTerminal("""\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""", """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+"""), +) + +/** extract_nsec_multiplier's table (minus its empty default entry, which is the bare-number case). */ +private val NSEC_SUFFIX = LiteralChoiceTerminal( + "seconds", "second", "sec", "s", "minutes", "minute", "min", "months", "month", "M", "msec", "ms", "m", + "hours", "hour", "hr", "h", "days", "day", "d", "weeks", "week", "w", "years", "year", "y", + "usec", "us", "μs", "µs", "nsec", "ns", +) + +/** + * A parse_nsec() duration WITHOUT the `infinity` spelling: one or more `number[unit]` elements, an + * element without a unit meaning nanoseconds. Whitespace may separate elements and may sit between a + * number and its unit; an element with a unit may be followed directly by the next one (`1s500ms`). + * + * Lenient on one corner: parse_nsec refuses a bare number followed immediately by a `.` fraction + * (`12.34.56`), which this grammar would read as `12.34` + `.56`. + */ +val NSEC_DURATION_FINITE: Combinator = run { + val ws = ZeroOrOne(WhitespaceTerminal()) + val element = AlternativeCombinator( + SequenceCombinator(DURATION_NUMBER, ws, NSEC_SUFFIX), + DURATION_NUMBER, + ) + SequenceCombinator(element, ZeroOrMore(SequenceCombinator(ws, element))) +} + + +// --------------------------------------------------------------------------------------------------- +// Paths. + +// Rejects a `..` component anywhere (see NO_DOT_DOT in Combinators.kt). +private const val NO_DOT_DOT_COMPONENT = """(?!(?:[\s\S]*/)?\.\.(?:/|$))""" + +/** + * An absolute path checked by path_simplify_and_warn(…, PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * (src/shared/parse-helpers.c), taken verbatim — no specifier expansion, no unquoting — as the netdev + * `*KeyFile=` settings do. After path_simplify() the path must be normalized (so no `..` component) and + * must not be at or below one of the API VFS mounts `/dev`, `/sys`, `/proc` (path_below_api_vfs, which + * compares whole components, so `/devices` is fine). Because path_simplify() runs first, `.` components + * and repeated slashes in front of those names do not hide them (`/./dev/shm/key`, `//sys`). Spaces + * are ordinary path characters. Lenient: path_is_valid()'s NAME_MAX/PATH_MAX length limits. + */ +val NON_API_VFS_ABSOLUTE_PATH = RegexTerminal( + """[\s\S]+""", + """(?!/(?:\.?/)*(?:dev|sys|proc)(?:/|$))$NO_DOT_DOT_COMPONENT/[\s\S]*""", +) + +/** + * An absolute path accepted by path_is_absolute() && path_is_safe() (src/basic/path-util.h): it starts + * with `/` and has no `..` component. Taken verbatim. + */ +val SAFE_ABSOLUTE_PATH = RegexTerminal( + """/[\s\S]*""", + """$NO_DOT_DOT_COMPONENT/[\s\S]*""", +) + + +// --------------------------------------------------------------------------------------------------- +// WireGuard keys and credentials — wireguard_decode_key_and_warn (src/network/netdev/wireguard.c) → +// unbase64mem_full (src/basic/hexdecoct.c). + +/** + * A 32-byte WireGuard key in base64, as unbase64mem_full() decodes it. + * + * 32 bytes is ten full 4-character groups plus a final 3-character group padded with one `=`; the + * padding is mandatory — without it unbase64_next() runs out of input mid-group and fails. The third + * character of a padded group must have its low two bits clear (`c & 3`), which limits the 43rd + * character to `AEIMQUYcgkosw048`. unbase64char() accepts the URL-safe alphabet too (`-` for `+`, + * `_` for `/`), and unbase64_next() skips whitespace anywhere in the input. + */ +val WIREGUARD_BASE64_KEY = RegexTerminal( + """[A-Za-z0-9+/_=\-]+(?:[ \t]+[A-Za-z0-9+/_=\-]+)*""", + """(?:[A-Za-z0-9+/_\-][ \t]*){42}[AEIMQUYcgkosw048][ \t]*=""", +) + +/** + * `@name` — a credential reference resolved by read_credential(), which refuses any name that fails + * credential_name_valid() (src/shared/creds-util.c). [CONDITION_CREDENTIAL] models that check. + */ +val CREDENTIAL_REFERENCE: Combinator = SequenceCombinator(LiteralChoiceTerminal("@"), CONDITION_CREDENTIAL) + + +// --------------------------------------------------------------------------------------------------- +// Address prefixes — in_addr_prefix_from_string_auto_full (src/basic/in-addr-util.c). + +/** + * An IPv4 or IPv6 address with an optional `/prefixlen`, as in_addr_prefix_from_string_auto (mode + * PREFIXLEN_FULL) reads it. Like [IP_ADDR_AND_ANY_PREFIX], but the length goes through + * in_addr_parse_prefixlen → safe_atou8, so it is a base-0 number (`/0x18` is 24, `/0b+1000` is 8) up to the + * family width. Its only user, AllowedIPs=, splits the list on whitespace first, so no whitespace reaches + * safe_atou8 (`/0b 1` is the two words `…/0b` and `1`). IPv6 is tried first so the IPv4 branch never strands + * part of an IPv6 literal. + */ +val IP_ADDR_AND_SAFE_PREFIX: Combinator = AlternativeCombinator( + SequenceCombinator( + IPV6_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + ), + ), + SequenceCombinator( + IPV4_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 32L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + ), + ), +) + + +// --------------------------------------------------------------------------------------------------- +// Checks on a whole matched span — for rules systemd applies to the parsed value rather than its shape. + +/** + * Matches exactly what [inner] matches, but the match is only semantically valid when [check] accepts + * the matched text. For limits systemd applies after parsing, which a grammar cannot spell out: a + * magnitude bound on a parse_size() total, or "must (not) be a multicast address". + * + * Under the parse engine a rejected span marks its first token invalid; under the classic engine the + * semantic match fails with no progress at the span's start. Either way the first token of the span is + * highlighted, and the value keeps the structured grammar's colouring. + */ +class NetdevLinkCheckedSpan( + private val inner: Combinator, + private val description: String, + private val check: (String) -> Boolean, +) : Combinator { + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val match = inner.SemanticMatch(value, offset) + if (match.matchResult == -1 || check(value.substring(offset, match.matchResult))) return match + return NoMatch.copy(longestMatch = offset) + } + + override fun parse(value: String, offset: Int): Sequence = + inner.parse(value, offset).map { step -> + if (step is Parse && step.tokens.isNotEmpty() && !check(value.substring(offset, step.end))) { + step.copy(tokens = listOf(step.tokens.first().copy(valid = false)) + step.tokens.drop(1)) + } else { + step + } + } + + override fun toString(): String = "Checked($description)" + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "Checked($description,\n" + inner.toStringIndented(indent + 1) + ")" +} + +/** + * in_addr_is_multicast() (src/basic/in-addr-util.c) on an address literal [IP_ADDR] has matched: IPv4 + * in 224.0.0.0/4 (IN_MULTICAST), IPv6 in ff00::/8 (IN6_IS_ADDR_MULTICAST). The first IPv6 byte is 0xff + * only when the first group is written with four hex digits starting `ff` — `ff::1` is 0x00ff::1, and an + * address starting `::` (including IPv4-mapped `::ffff:239.1.1.1`) has a zero first byte. + */ +fun netdevLinkIsMulticast(address: String): Boolean { + if (':' in address) { + val first = address.substringBefore(':') + return first.length == 4 && first.startsWith("ff", ignoreCase = true) + } + return address.substringBefore('.').toIntOrNull() in 224..239 +} + +/** + * The total parse_size(text, 1000, …) (src/basic/parse-util.c) computes for a value [siSize] has + * matched: each element contributes `l * factor + (unsigned long long) (frac * factor)`. Exact decimal + * arithmetic stands in for the C double, which only matters within a unit of the truncated fraction. + * Null if [text] does not have [siSize]'s shape. + */ +fun netdevLinkParseSizeSi(text: String): BigInteger? { + val factors = mapOf("E" to 18, "P" to 15, "T" to 12, "G" to 9, "M" to 6, "K" to 3, "B" to 0, "" to 0) + val element = Regex("""\s*\+?([0-9]+)(?:\.([0-9]*))?\s*([EPTGMKB]?)""") + var total = BigInteger.ZERO + var pos = 0 + while (pos < text.length) { + val m = element.matchAt(text, pos) ?: return null + if (m.value.isEmpty()) return null + val factor = BigInteger.TEN.pow(factors.getValue(m.groupValues[3])) + val fraction = m.groupValues[2].takeIf { it.isNotEmpty() } + ?.let { java.math.BigDecimal("0.$it").multiply(java.math.BigDecimal(factor)).toBigInteger() } ?: BigInteger.ZERO + total += BigInteger(m.groupValues[1]) * factor + fraction + pos = m.range.last + 1 + } + return total +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkDhcpMiscCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkDhcpMiscCombinators.kt new file mode 100644 index 00000000..315ea413 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkDhcpMiscCombinators.kt @@ -0,0 +1,436 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.util.PsiTreeUtil +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionGroups +import java.math.BigInteger + +/* + * Shared grammar pieces for the network-dhcp-misc validator batch (systemd-networkd DHCPv4/DHCPv6/DHCP + * server/DUID/DNS/NTP/LLDP and assorted [Network]/[Link] settings). + * + * Everything here is pinned to systemd 0f0682b773 (systemd-build/build/last_commit_hash); browse it at + * https://github.com/systemd/systemd/blob/0f0682b773. These live in their own file rather than in + * Combinators.kt so that parallel batches don't collide; several of them are candidates for promotion + * (see the batch's RECOMMENDATIONS.md). + * + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex + * + * Integers (safe_atou8/16/32, safe_atoi, safe_atoux64, parse_ifindex) are read with the shared [SafeAtoTerminal]. + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L304-L387 dns_name_concat + */ + +/** One or more `:` — extract_first_word(…, ":", 0) coalesces runs of its separator. */ +val NETWORK_DHCP_MISC_COALESCED_COLONS = OneOrMore(COLON) + +/** + * One C escape sequence as cunescape_one() accepts it: `\a \b \f \n \r \t \v \\ \" \' \s`, `\xHH`, + * `\uHHHH`, `\UHHHHHHHH`, or a three-digit octal `\OOO` no greater than 0377. (cunescape_one also refuses + * NUL unless asked, UTF-16 surrogates and out-of-range code points; those corners are left lenient.) + */ +const val C_ESCAPE_REGEX = """\\(?:[abfnrtv\\"'s]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-3][0-7]{2})""" + +/** + * A run of text that cunescape() must be able to decode: any characters, with every backslash starting a + * valid [C_ESCAPE_REGEX]. A stray or trailing backslash is an error (no UNESCAPE_RELAX). Non-empty. + */ +val C_ESCAPED_TEXT = RegexTerminal("""(?:[^\\]|\\[\s\S]?)+""", """(?:[^\\]|$C_ESCAPE_REGEX)+\z""") + +/** + * One word as extract_first_word(…, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) splits it: unquoted text, + * `"…"` and `'…'` sections (which may contain whitespace) glued together, with backslash escapes — + * inside or outside quotes — decoded by cunescape_one. An escape that cunescape_one doesn't know (`\q`, + * `\ `), a trailing backslash or an unterminated quote fails the whole assignment. + */ +val CUNESCAPE_UNQUOTE_WORD = RegexTerminal( + """(?:[^\s"'\\]|\\[\s\S]?|"(?:[^"\\]|\\[\s\S]?)*"?|'(?:[^'\\]|\\[\s\S]?)*'?)+""", + """(?:[^\s"'\\]|$C_ESCAPE_REGEX|"(?:[^"\\]|$C_ESCAPE_REGEX)*"|'(?:[^'\\]|$C_ESCAPE_REGEX)*')+(?=\s|\z)""", +) + +/** + * [CUNESCAPE_UNQUOTE_WORD] minus the words that unquote to the empty string (`""`, `''`, `""''`): + * config_parse_dhcp4_user_class and config_parse_dhcp6_user_or_vendor_class drop those with "The length of + * the … entry '' is not in the range 1…255/65535, ignoring" (checked with systemd's parser at 0f0682b773). + * The upper length bound is not modelled (lenient). + * + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L687-L692 len == 0 check (DHCPv4) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L732-L737 len == 0 check (DHCPv6) + */ +val NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD: Combinator = NetworkDhcpMiscCheckedSpan(CUNESCAPE_UNQUOTE_WORD, "NonEmptyWord") { + !NETWORK_DHCP_MISC_EMPTY_QUOTES.matches(it) +} + +private val NETWORK_DHCP_MISC_EMPTY_QUOTES = Regex("""(?:""|'')+""") + +/** A whitespace-separated list of [word]s (the value arrives stripped, so no leading/trailing space). */ +fun wordList(word: Combinator): Combinator = SequenceCombinator( + word, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), word)), +) + +// A note on the RegexTerminals below: each semantic regex either matches the *whole* span its syntactic +// regex matched or nothing (trailing lookaheads / \z). Under the classic engine a semantic match that +// stops short inside the syntactic span misaligns GrammarOptionValue's token bookkeeping and can index +// past the end of its token list (seen as an IndexOutOfBoundsException on DUIDRawData=000:11:22). + +// --------------------------------------------------------------------------------------------------- +// Interface name or index — `parse_ifindex(s) > 0 || ifname_valid_full(s, IFNAME_VALID_ALTERNATIVE)`, +// the pattern used by config_parse_uplink and config_parse_fdb_interface. +private const val IFNAME_CHAR = """[\x21-\x7E&&[^:/%]]""" + +private val IFNAME_ALTERNATIVE = Regex("""(?!(?:\.\.?|all|default)\z)$IFNAME_CHAR{1,127}""") + +/** + * A terminal that takes the span [shape] matches and accepts it iff [valid] says so, for leaves whose + * acceptance is a computation rather than a regular language. + */ +class CheckedTerminal(private val shape: Regex, private val name: String, private val valid: (String) -> Boolean) : TerminalCombinator { + + private fun span(value: String, offset: Int): String? = shape.matchAt(value, offset)?.value?.takeIf { it.isNotEmpty() } + + 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 (!valid(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, valid(text))))) + } + + override fun toString(): String = name +} + +/** + * A positive interface index, or an alternative interface name: `parse_ifindex(s) > 0 || + * ifname_valid_full(s, IFNAME_VALID_ALTERNATIVE)`. + * + * parse_ifindex() is safe_atoi() (base 0) plus `> 0`, so `010` is index 8, `0x10` is 16 and `+5` is 5, while + * `0`, `08` and `99999999999` are refused. ifname_valid_full() then takes 1…127 printable ASCII characters + * other than `:`, `/`, `%`, but not `.`, `..`, `all`, `default`, or a spelling made only of digits (those + * failed parse_ifindex above, so they are zero, too large or malformed octal). + * + * The span is the whole value, whitespace included, because safe_atoi sees all of it: `0b 1`, `0o 7` and `\v5` + * are indexes 1, 7 and 5, while `eth 0` and `0b 0` are refused (checked with systemd's parser at 0f0682b773). + */ +val IFNAME_OR_IFINDEX = CheckedTerminal(Regex("""[\s\S]+"""), "IfnameOrIfindex") { text -> + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(text) != null || + (!text.all { it in '0'..'9' } && IFNAME_ALTERNATIVE.matches(text)) +} + +// --------------------------------------------------------------------------------------------------- +// DNS names. + +// A character that may appear unescaped in a non-LDH DNS label (dns_label_unescape without +// DNS_LABEL_LDH): anything >= 0x20 except DEL, the `.` separator and the `\` escape introducer. In a +// whitespace-separated word list whitespace is also excluded (it ends the word). +private const val DNS_PLAIN = """[^\s.\\\p{Cntrl}]""" + +/** + * One non-LDH DNS label inside a whitespace-separated word. + * + * The words are cut by extract_first_word(…, NULL, 0), which strips one level of backslash (a raw `\x` + * becomes `x`), and the result then goes through dns_label_unescape, which treats a remaining `\` as the + * start of a `\\`, `\.` or `\DDD` (000…255) escape. So in the raw text: + * - `\x` for any `x` other than a backslash is just `x` (accepted as label content); + * - a doubled backslash `\\` survives as `\` and must start a valid dns_label_unescape escape: it must + * be followed by `\\` or `\.` (an escaped backslash/dot), by three digits no greater than 255, or by + * the `.` separator (raw `foo\\.bar` is `foo\.bar`, one label with a literal dot — accepted either + * way). `foo\\bar`, `foo\\256` and a trailing `foo\\` are refused, as systemd refuses them. + * + * Still lenient: a raw `\.` is treated as label content although extract_first_word turns it into a + * separator, so `a\..b` and `\.a` (empty labels once unescaped) are accepted. A label of 64 or more + * plain characters is refused: that is over DNS_LABEL_MAX whatever follows. + * + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L60-L100 dns_label_unescape escapes + */ +val DNS_WORD_LABEL = RegexTerminal( + """(?:[^\s.\\]|\\[\s\S])+""", + """(?!$DNS_PLAIN{64})(?:$DNS_PLAIN|\\[^\\]|\\\\(?:\\[\\.]|[01][0-9]{2}|2[0-4][0-9]|25[0-5]|(?=\.)))+(?=[\s.]|\z)""", +) + +/** + * A DNS name as dns_name_is_valid() accepts it, written as one whitespace-separated word: labels joined + * by single dots with an optional single trailing dot, or the root domain `.` on its own. Empty labels + * (`a..b`, `.a`) and a double trailing dot are rejected, as in dns_label_unescape. The 253-character + * whole-name limit is not enforced. + */ +val DNS_NAME_WORD = AlternativeCombinator( + SequenceCombinator(DNS_WORD_LABEL, ZeroOrMore(SequenceCombinator(DOT, DNS_WORD_LABEL)), ZeroOrOne(DOT)), + DOT, +) + +/** + * is_localhost(): `localhost`, `localhost.localdomain` and any name ending in `.localhost` or + * `.localhost.localdomain`, case-insensitively, with or without a trailing dot. [name] is a raw + * [DNS_NAME_WORD]; single-backslash escapes are dropped first, as extract_first_word drops them. + * + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hostname-util.c#L145-L161 is_localhost + */ +fun networkDhcpMiscIsLocalhost(name: String): Boolean { + val n = name.replace(Regex("""\\(.)"""), "$1").lowercase().removeSuffix(".") + return n == "localhost" || n == "localhost.localdomain" || n.endsWith(".localhost") || n.endsWith(".localhost.localdomain") +} + +/** An LDH label: letters, digits and inner hyphens, 1…63 characters (dns_label_unescape with DNS_LABEL_LDH). */ +val LDH_DNS_LABEL = RegexTerminal( + """[A-Za-z0-9-]+""", + """[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?![A-Za-z0-9-])""", +) + +/** + * A DNS name as dns_name_is_valid_ldh() accepts it: [LDH_DNS_LABEL]s joined by single dots, an optional + * trailing dot, no escapes (DNS_LABEL_NO_ESCAPES), or the root domain `.`. The 253-character limit is not + * enforced. + */ +val LDH_DNS_NAME = AlternativeCombinator( + SequenceCombinator(LDH_DNS_LABEL, ZeroOrMore(SequenceCombinator(DOT, LDH_DNS_LABEL)), ZeroOrOne(DOT)), + DOT, +) + +// --------------------------------------------------------------------------------------------------- +// Typed DHCP option payloads — config_parse_dhcp_option / the tail of config_parse_dhcp6_send_option. + +/** + * [IPV6_ADDR] minus the two forms that begin with `::` (`::1`, `::ffff:1.2.3.4`, `::`) — for positions + * where a preceding coalescing `:` separator has already swallowed any leading colons. + */ +val IPV6_ADDR_NO_LEADING_COLON = Labeled(Role.LITERAL, tag = SemanticTag.IPV6, inner = AlternativeCombinator( + IPV6_IPV4_SUFFIX_FULL, + IPV6_IPV4_SUFFIX_ONE_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_TWO_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_THREE_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_FOUR_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_FIVE_HEXTET_BEFORE_ZERO_COMP, + IPV6_FULL_SPECIFIED, + IPV6_SEVEN_HEXTET_BEFORE_ZERO_COMP, + IPV6_SIX_HEXTET_BEFORE_ZERO_COMP, + IPV6_FIVE_HEXTET_BEFORE_ZERO_COMP, + IPV6_FOUR_HEXTET_BEFORE_ZERO_COMP, + IPV6_THREE_HEXTET_BEFORE_ZERO_COMP, + IPV6_TWO_HEXTET_BEFORE_ZERO_COMP, + IPV6_ONE_HEXTET_BEFORE_ZERO_COMP, +)) + +/** + * `TYPE:DATA`, the shape config_parse_dhcp_option() parses and config_parse_dhcp_option_tlv() / + * config_parse_dhcp6_send_option() parse after their numeric prefix(es). TYPE is one of + * dhcp_option_data_type_table and decides how the remainder is read: + * + * - `uint8` / `uint16` / `uint32` — safe_atou8/16/32 ([SafeAtoTerminal], base 0); + * - `ipv4address` / `ipv6address` — inet_pton() of that family, no prefix, no scope; + * - `string` — cunescape(…, UNESCAPE_ACCEPT_NUL): anything, with valid C escapes; may contain `:`. + * + * The separator is extract_first_word(…, ":", 0), which coalesces a run of colons, so `uint8::5` is + * fine. That coalescing also eats the leading colons of the data itself: for `ipv6address` an address + * beginning with `::` therefore cannot be written at all (`ipv6address:::1` hands inet_pton just `1` + * and fails), while `ipv6address::2001:db8::1` is fine. Hence [IPV6_ADDR_NO_LEADING_COLON] after + * the colon run. Likewise a `string` value cannot start with `:`. + * + * The number after `uintN:` is a field cut on `:`, so whitespace reaches safe_atou_full, which skips it + * (acceptsWhitespace = true). The string length check (`check_length`, <= 255 bytes once unescaped) is not + * modelled. + */ +fun dhcpOptionTypedData(): Combinator = AlternativeCombinator( + SequenceCombinator( + LiteralChoiceTerminal("uint8"), + NETWORK_DHCP_MISC_COALESCED_COLONS, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT8_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + SequenceCombinator( + LiteralChoiceTerminal("uint16"), + NETWORK_DHCP_MISC_COALESCED_COLONS, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT16_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + SequenceCombinator( + LiteralChoiceTerminal("uint32"), + NETWORK_DHCP_MISC_COALESCED_COLONS, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + SequenceCombinator(LiteralChoiceTerminal("ipv4address"), NETWORK_DHCP_MISC_COALESCED_COLONS, IPV4_ADDR), + SequenceCombinator(LiteralChoiceTerminal("ipv6address"), NETWORK_DHCP_MISC_COALESCED_COLONS, IPV6_ADDR_NO_LEADING_COLON), + SequenceCombinator(LiteralChoiceTerminal("string"), NETWORK_DHCP_MISC_COALESCED_COLONS, C_ESCAPED_TEXT), +) + +// --------------------------------------------------------------------------------------------------- +// Time spans — parse_sec() / parse_time(…, USEC_PER_SEC). +// +// leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1146-L1196 extract_multiplier +// leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time +// tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c + +// extract_multiplier's table, in table order; the first prefix that matches wins (startswith, no +// longest-match), hence the atomic group. Note: no `ns`/`nsec` — those belong to parse_nsec only. +private const val PARSE_SEC_UNIT_REGEX = + "(?>seconds|second|sec|s|minutes|minute|min|months|month|M|msec|ms|m|hours|hour|hr|h|days|day|d|weeks|week|w|years|year|y|usec|us|\u03bcs|\u00b5s)" + +/** + * One number of a parse_time() element: strtoll() digits (an optional `+`, never `-`) with an optional + * `.FRACTION`, or a bare `.FRACTION` (strtoll converts nothing, then the `.` branch runs: `.5s` is + * legal). A trailing `.` without digits (`5.`, `5.s`) is refused. + * + * The lookahead encodes the one adjacency rule of parse_time(): when no unit follows a number, the next + * element must be separated by whitespace (`5.5.5` and `5+5` fail, `5 .5` and `5s.5` are fine). So a + * number may only be followed by whitespace, the end, or the first letter of a unit. + */ +private val PARSE_SEC_NUMBER = RegexTerminal( + """\+?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]*)""", + """(?:\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?=[ \t\n\r]|\z|[smMhdwyu\u03bc\u00b5])""", +) + +private val PARSE_SEC_UNIT = RegexTerminal(PARSE_SEC_UNIT_REGEX, PARSE_SEC_UNIT_REGEX) + +private val PARSE_SEC_ELEMENT = SequenceCombinator( + PARSE_SEC_NUMBER, + ZeroOrOne(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), PARSE_SEC_UNIT)), +) + +/** + * A time span exactly as parse_sec() reads it: `infinity`, or one or more `NUMBER [UNIT]` elements whose + * values are summed, with optional whitespace anywhere between a number and its unit and between + * elements — elements may also be written back to back after a unit (`1h30min`, `55s500ms`, `5s.5`). + * The default unit is seconds. A `-` anywhere at the start of an element is refused (`-0` too). + * + * Overflow (a sum >= USEC_INFINITY) is not modelled. The shared TIME_VALUE in Combinators.kt is both + * stricter (it needs whitespace between elements and a digit before `.`) and looser (it accepts the + * `ns`/`nsec` units parse_sec doesn't know); this one follows the C. + */ +val PARSE_SEC_TIMESPAN = AlternativeCombinator( + LiteralChoiceTerminal("infinity"), + SequenceCombinator(PARSE_SEC_ELEMENT, ZeroOrMore(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), PARSE_SEC_ELEMENT))), +) + +private val NETWORK_DHCP_MISC_PARSE_SEC_MULTIPLIERS: Map = mapOf( + "seconds" to 1_000_000L, "second" to 1_000_000L, "sec" to 1_000_000L, "s" to 1_000_000L, + "minutes" to 60_000_000L, "minute" to 60_000_000L, "min" to 60_000_000L, "m" to 60_000_000L, + "months" to 2_629_800_000_000L, "month" to 2_629_800_000_000L, "M" to 2_629_800_000_000L, + "msec" to 1_000L, "ms" to 1_000L, + "hours" to 3_600_000_000L, "hour" to 3_600_000_000L, "hr" to 3_600_000_000L, "h" to 3_600_000_000L, + "days" to 86_400_000_000L, "day" to 86_400_000_000L, "d" to 86_400_000_000L, + "weeks" to 604_800_000_000L, "week" to 604_800_000_000L, "w" to 604_800_000_000L, + "years" to 31_557_600_000_000L, "year" to 31_557_600_000_000L, "y" to 31_557_600_000_000L, + "usec" to 1L, "us" to 1L, "\u03bcs" to 1L, "\u00b5s" to 1L, +) + +private val NETWORK_DHCP_MISC_PARSE_SEC_STEP = + Regex("""[ \t\n\r]*\+?([0-9]*(?:\.[0-9]*)?)[ \t\n\r]*($PARSE_SEC_UNIT_REGEX)?""") + +/** + * The duration, in microseconds, that parse_sec() computes for a [PARSE_SEC_TIMESPAN]-shaped [text] + * (the elements summed, the default unit seconds, `infinity` as [Long.MAX_VALUE]), or null if [text] + * isn't of that shape. Fractions are evaluated exactly rather than digit-by-digit as the C does; the two + * differ by less than a microsecond per element. + */ +fun networkDhcpMiscParseSecUsec(text: String): Long? { + if (text == "infinity") return Long.MAX_VALUE + var total = java.math.BigDecimal.ZERO + var i = 0 + while (i < text.length) { + val m = NETWORK_DHCP_MISC_PARSE_SEC_STEP.matchAt(text, i) ?: return null + val number = m.groupValues[1] + if (m.value.isEmpty() || number.isEmpty() || number == ".") return null + val multiplier = m.groups[2]?.value?.let { NETWORK_DHCP_MISC_PARSE_SEC_MULTIPLIERS[it] ?: return null } ?: 1_000_000L + total += java.math.BigDecimal(if (number.startsWith(".")) "0$number" else number.trimEnd('.')) * java.math.BigDecimal(multiplier) + i = m.range.last + 1 + } + return total.toBigInteger().min(java.math.BigInteger.valueOf(Long.MAX_VALUE)).toLong() +} + +/** + * Wraps [inner] and additionally requires the whole span it matched to satisfy [valid] — for rules + * that are a computation over a structured value (a minimum duration, an excluded address range, a + * reserved name) rather than part of its shape. Matching, colouring and completion are [inner]'s; when + * [valid] fails, the first token of the span is reported as the semantic error (both engines). + */ +class NetworkDhcpMiscCheckedSpan( + private val inner: Combinator, + private val name: String, + private val valid: (String) -> Boolean, +) : Combinator { + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val m = inner.SemanticMatch(value, offset) + if (m.matchResult == -1 || valid(value.substring(offset, m.matchResult))) return m + return NoMatch.copy(longestMatch = offset) + } + + override fun parse(value: String, offset: Int): Sequence = inner.parse(value, offset).map { step -> + if (step !is Parse || step.tokens.isEmpty() || valid(value.substring(offset, step.end))) step + else step.copy(tokens = listOf(step.tokens[0].copy(valid = false)) + step.tokens.drop(1)) + } + + override fun toString(): String = "$name(${inner})" + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "$name(\n" + inner.toStringIndented(indent + 1) + "\n" + " ".repeat(indent) + ")" +} + +// --------------------------------------------------------------------------------------------------- +// Parsers whose accepted syntax depends on the key or section (they test `lvalue` / `section`). + +/** + * A grammar validator for a (parser, ltype) pair whose C code branches on the key name or the section + * name — e.g. config_parse_dhcp6_send_option (`streq(lvalue, "SendVendorOption")`) and + * config_parse_uplink (`:none` vs `:self` by section). + * + * The registration is still per (parser, ltype), but [generateProblemDescriptors] receives the PSI + * property, so it asks [select] for the grammar that applies to that (key, section) and validates with + * it. [combinator] (the [lenientUnion] of all shapes) is what the key-independent features — value + * colouring, completion, the deprecation annotator — see; it should accept the union of the shapes. + */ +open class KeyOrSectionDispatchingGrammarOptionValue( + validatorName: String, + lenientUnion: Combinator, + private val select: (key: String, section: String?) -> GrammarOptionValue, +) : GrammarOptionValue(validatorName, lenientUnion) { + + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + val section = PsiTreeUtil.getParentOfType(property, UnitFileSectionGroups::class.java)?.sectionName + select(property.key, section).generateProblemDescriptors(property, holder) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt new file mode 100644 index 00000000..48154b00 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt @@ -0,0 +1,365 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigInteger + +/* + * Shared helpers for the network-route-addr validator batch (2026-10): [Route], [RoutingPolicyRule], + * [Address], [Neighbor], [IPv6SendRA] / [IPv6Prefix] / [IPv6RoutePrefix] / [IPv6PREF64Prefix] and the + * generic strv / string / match parsers. + * + * Everything here is pinned to systemd 0f0682b773, the commit recorded in + * systemd-build/build/last_commit_hash. Browse it at https://github.com/systemd/systemd/blob/0f0682b773. + * + * Kept in its own file (rather than Combinators.kt) because several validator batches were written in + * parallel; the candidates for promotion are listed in the batch RECOMMENDATIONS.md. + */ + +// --------------------------------------------------------------------------------------------------- +// Checked — a value-level check on top of a structural grammar. + +/** + * Matches exactly what [inner] matches, but additionally requires [predicate] to hold for the text it + * consumed. + * + * Use it when systemd's acceptance depends on the *value* a well-formed span denotes rather than on its + * shape — a time span that has to fit in a uint32 of milliseconds, a metric triple that has to be + * strictly increasing, an address that may not be the ANY address. The structural grammar still drives + * tokenization and highlighting; only validity changes. + * + * When the predicate fails the first token of the span is reported as the bad one (under both engines), + * since there is no single token to blame for a property of the whole span. + */ +class Checked(private val inner: Combinator, private val predicate: (String) -> Boolean) : Combinator { + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val match = inner.SemanticMatch(value, offset) + if (match.matchResult == -1) return match + if (!predicate(value.substring(offset, match.matchResult))) return NoMatch.copy(longestMatch = offset) + return match + } + + override fun parse(value: String, offset: Int): Sequence = + inner.parse(value, offset).map { step -> + // The predicate only runs on spans whose tokens are all individually valid, so predicates may assume + // well-formed input (e.g. a digit run that fits its range) and need not be total. + if (step is Parse && step.tokens.isNotEmpty() && step.end > offset && step.tokens.all { it.valid } && + !predicate(value.substring(offset, step.end))) { + step.copy(tokens = listOf(step.tokens[0].copy(valid = false)) + step.tokens.drop(1)) + } else { + step + } + } + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "Checked(\n" + inner.toStringIndented(indent + 1) + "\n" + " ".repeat(indent) + ")" + + override fun toString(): String = toStringIndented(0) +} + + +// --------------------------------------------------------------------------------------------------- +// parse_sec() — parse_time(t, &usec, USEC_PER_SEC) in src/basic/time-util.c. +// +// The grammar mirrors parse_time()'s loop: a run of `NUMBER [whitespace] [UNIT]` elements, where +// - NUMBER is strtoll() base 10 (so an optional `+`) with an optional `.fraction`; `.5` is fine, `5.` +// is not ("Don't allow … 3.sec"); +// - elements may abut (`1h30min`, `5s+3s`) or be separated by whitespace; +// - a number with no unit defaults to seconds and must then be followed by whitespace or the end — +// extract_multiplier() refuses `12.34.56` but accepts `12.34 .56`; +// - `infinity` stands alone. +// Units are matched by a FlexibleLiteralChoiceTerminal: a unit is always followed by a digit, a sign, +// whitespace or the end, so the terminal's greedy letter run lines up with extract_multiplier()'s +// longest-prefix-first table and a misspelt unit is highlighted as one token. + +/** extract_multiplier()'s table, in microseconds. */ +private val TIME_UNIT_USEC: Map = run { + val sec = BigInteger.valueOf(1_000_000L) + val min = sec.multiply(BigInteger.valueOf(60)) + val hour = min.multiply(BigInteger.valueOf(60)) + val day = hour.multiply(BigInteger.valueOf(24)) + mapOf( + "seconds" to sec, "second" to sec, "sec" to sec, "s" to sec, + "minutes" to min, "minute" to min, "min" to min, + "months" to sec.multiply(BigInteger.valueOf(2_629_800L)), "month" to sec.multiply(BigInteger.valueOf(2_629_800L)), + "M" to sec.multiply(BigInteger.valueOf(2_629_800L)), + "msec" to BigInteger.valueOf(1000), "ms" to BigInteger.valueOf(1000), + "m" to min, + "hours" to hour, "hour" to hour, "hr" to hour, "h" to hour, + "days" to day, "day" to day, "d" to day, + "weeks" to day.multiply(BigInteger.valueOf(7)), "week" to day.multiply(BigInteger.valueOf(7)), "w" to day.multiply(BigInteger.valueOf(7)), + "years" to sec.multiply(BigInteger.valueOf(31_557_600L)), "year" to sec.multiply(BigInteger.valueOf(31_557_600L)), + "y" to sec.multiply(BigInteger.valueOf(31_557_600L)), + "usec" to BigInteger.ONE, "us" to BigInteger.ONE, "μs" to BigInteger.ONE, "µs" to BigInteger.ONE, + ) +} + +/** NETWORK_ROUTE_ADDR_USEC_INFINITY, i.e. UINT64_MAX. */ +val NETWORK_ROUTE_ADDR_USEC_INFINITY: BigInteger = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE) +val USEC_PER_SEC: BigInteger = BigInteger.valueOf(1_000_000L) +val USEC_PER_MSEC: BigInteger = BigInteger.valueOf(1_000L) + +// `+.5` is refused: strtoll() reads no digits after the sign, so e == p and *e is not '.'. +private val TIME_NUMBER = RegexTerminal("""[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]*)""", """(?:\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+)""") +private val TIME_UNIT = FlexibleLiteralChoiceTerminal(*TIME_UNIT_USEC.keys.toTypedArray()) +private val TIME_INFINITY = FlexibleLiteralChoiceTerminal("infinity") + +private val TIME_ELEMENT = AlternativeCombinator( + SequenceCombinator(TIME_NUMBER, ZeroOrOne(WhitespaceTerminal()), TIME_UNIT), + SequenceCombinator(TIME_NUMBER, AlternativeCombinator(WhitespaceTerminal(), EOF())), +) + +/** Every spelling parse_sec() accepts, structurally; not range-checked. Not followed by EOF. */ +val NETWORK_ROUTE_ADDR_PARSE_SEC = AlternativeCombinator( + TIME_INFINITY, + OneOrMore(SequenceCombinator(TIME_ELEMENT, ZeroOrOne(WhitespaceTerminal()))), +) + +private val TIME_ELEMENT_REGEX = Regex("""\s*(?:\+(?=[0-9]))?([0-9]*)(?:\.([0-9]+))?\s*(seconds|second|sec|s|minutes|minute|min|months|month|M|msec|ms|m|hours|hour|hr|h|days|day|d|weeks|week|w|years|year|y|usec|us|μs|µs)?""") + +/** + * The value parse_sec() gives [text] in microseconds — [NETWORK_ROUTE_ADDR_USEC_INFINITY] for `infinity` — or null if + * parse_sec() would fail (including its -ERANGE overflow checks). [text] is expected to be something + * [NETWORK_ROUTE_ADDR_PARSE_SEC] matched. + */ +fun parseSecUsec(text: String): BigInteger? { + val t = text.trim() + if (t == "infinity") return NETWORK_ROUTE_ADDR_USEC_INFINITY + var pos = 0 + var usec = BigInteger.ZERO + var something = false + while (pos < t.length) { + val m = TIME_ELEMENT_REGEX.matchAt(t, pos) ?: return null + if (m.value.isEmpty()) return null + val whole = m.groupValues[1] + val frac = m.groupValues[2] + if (whole.isEmpty() && frac.isEmpty()) return null + val multiplier = TIME_UNIT_USEC[m.groupValues[3]] ?: USEC_PER_SEC + val l = if (whole.isEmpty()) BigInteger.ZERO else BigInteger(whole) + if (l >= NETWORK_ROUTE_ADDR_USEC_INFINITY.divide(multiplier)) return null + usec = usec.add(l.multiply(multiplier)) + var scale = multiplier.divide(BigInteger.TEN) + for (c in frac) { + usec = usec.add(BigInteger.valueOf((c - '0').toLong()).multiply(scale)) + scale = scale.divide(BigInteger.TEN) + } + if (usec >= NETWORK_ROUTE_ADDR_USEC_INFINITY) return null + something = true + pos = m.range.last + 1 + } + return if (something) usec else null +} + +/** + * A parse_sec() value whose microsecond count must satisfy [accept] (which receives [NETWORK_ROUTE_ADDR_USEC_INFINITY] for + * `infinity`). Not followed by EOF. + */ +fun timeSpan(accept: (BigInteger) -> Boolean = { true }): Combinator = + Checked(NETWORK_ROUTE_ADDR_PARSE_SEC) { text -> parseSecUsec(text)?.let(accept) ?: false } + +/** DIV_ROUND_UP(a, b) for non-negative a. */ +fun divRoundUp(a: BigInteger, b: BigInteger): BigInteger = a.add(b).subtract(BigInteger.ONE).divide(b) + + +// --------------------------------------------------------------------------------------------------- +// parse_size(t, 1024, …) — src/basic/parse-util.c. +// +// A run of `NUMBER [whitespace] [SUFFIX]` elements where each suffix must be strictly smaller than the +// previous one (start_pos), a bare number (the "" suffix, bytes) ends the run, and NUMBER is strtoull() +// base 10 with an optional fraction (`10.M` is accepted: "Ignore failure"). + +private val SIZE_NUMBER = RegexTerminal("""[+-]?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""") +private val SIZE_SUFFIX = LiteralChoiceTerminal("E", "P", "T", "G", "M", "K", "B") +private val SIZE_ELEMENT = SequenceCombinator(SIZE_NUMBER, ZeroOrOne(WhitespaceTerminal()), ZeroOrOne(SIZE_SUFFIX)) + +/** Every spelling parse_size() (base 1024) accepts, structurally. Not followed by EOF. */ +val PARSE_SIZE_IEC = SequenceCombinator(SIZE_ELEMENT, ZeroOrMore(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), SIZE_ELEMENT))) + +private val SIZE_ELEMENT_REGEX = Regex("""\s*\+?([0-9]+)(?:\.([0-9]*))?\s*([EPTGMKB]?)""") +private val SIZE_ORDER = listOf("E", "P", "T", "G", "M", "K", "B", "") + +/** The byte count parse_size(text, 1024) yields, or null if it would fail. */ +fun parseSizeIec(text: String): BigInteger? { + val t = text.trim() + var pos = 0 + var total = BigInteger.ZERO + var startPos = 0 + val max = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE) + if (t.isEmpty()) return null + while (pos < t.length) { + val m = SIZE_ELEMENT_REGEX.matchAt(t, pos) ?: return null + val suffix = m.groupValues[3] + val index = SIZE_ORDER.indexOf(suffix) + if (index < startPos) return null + val factor = BigInteger.valueOf(1024).pow(6 - minOf(index, 6)) + val whole = BigInteger(m.groupValues[1]) + val fracDigits = m.groupValues[2] + var bytes = whole.multiply(factor) + if (fracDigits.isNotEmpty()) { + bytes = bytes.add(factor.multiply(BigInteger(fracDigits)).divide(BigInteger.TEN.pow(fracDigits.length))) + } + total = total.add(bytes) + if (total > max) return null + startPos = index + 1 + pos = m.range.last + 1 + if (m.value.isEmpty()) return null + } + return total +} + + +// --------------------------------------------------------------------------------------------------- +// extract_first_word() word lists (src/basic/extract-word.c). + +/** + * One word as extract_first_word(…, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) reads it: runs of bare + * characters and `"…"` / `'…'` quoted pieces, glued together (`a"b c"d` is the single word `ab cd`). + * With EXTRACT_RETAIN_ESCAPE a backslash is an ordinary character, even inside quotes, so the only way + * such a word can fail is an unterminated quote. The quoted pieces match leniently without their + * closing quote so the unterminated piece is the token highlighted. + */ +val RETAIN_ESCAPE_WORD: Combinator = OneOrMore( + AlternativeCombinator( + RegexTerminal("\"[^\"]*\"?", "\"[^\"]*\""), + RegexTerminal("'[^']*'?", "'[^']*'"), + RegexTerminal("[^\\s\"']+", "[^\\s\"']+"), + ) +) + +/** A whitespace-separated list of [RETAIN_ESCAPE_WORD]s, followed by EOF. */ +val RETAIN_ESCAPE_WORD_LIST: Combinator = SequenceCombinator( + RETAIN_ESCAPE_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), RETAIN_ESCAPE_WORD)), + EOF(), +) + +// The C escapes cunescape_one() understands (src/basic/escape.c). NUL and surrogate code points, which +// it refuses, are not singled out. +private const val C_ESCAPE = """\\(?:[abfnrtv\\"'s]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-3][0-7]{2})""" + +/** + * One word as extract_first_word(…, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) reads it: like + * [RETAIN_ESCAPE_WORD], but a backslash starts a C escape both inside and outside quotes, and an + * unknown escape is an error. + */ +val CUNESCAPE_WORD: Combinator = OneOrMore( + AlternativeCombinator( + RegexTerminal("\"(?:[^\"\\\\]|\\\\[\\s\\S])*\"?", "\"(?:[^\"\\\\]|$C_ESCAPE)*\""), + RegexTerminal("'(?:[^'\\\\]|\\\\[\\s\\S])*'?", "'(?:[^'\\\\]|$C_ESCAPE)*'"), + RegexTerminal("(?:[^\\s\"'\\\\]|\\\\[\\s\\S])+", "(?:[^\\s\"'\\\\]|$C_ESCAPE)+"), + ) +) + +/** + * [text] with its quotes removed and its escapes collapsed to the escaped character — enough to look at + * the word's structure (where its `=` is, what precedes it). Escapes are not decoded faithfully. + */ +fun unquoteWord(text: String): String { + val sb = StringBuilder() + var quote: Char? = null + var i = 0 + while (i < text.length) { + val c = text[i] + when { + c == '\\' && i + 1 < text.length -> { + sb.append(text[i + 1]); i += 2; continue + } + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> sb.append(c) + } + i++ + } + return sb.toString() +} + +/** + * A whitespace-separated list of interface names as extract_first_word(…, 0) splits it — no quote + * handling, so a name is simply a run of non-whitespace characters — each checked with [name]. + */ +fun plainNameList(name: Combinator): Combinator = SequenceCombinator( + name, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), name)), + EOF(), +) + + +// --------------------------------------------------------------------------------------------------- +// Address literals, as bytes — for the value checks inet_pton() parsing alone cannot express. + +/** The 4 or 16 bytes an IPv4 / IPv6 literal (as matched by [IP_ADDR]) denotes, or null. */ +fun ipLiteralBytes(text: String): ByteArray? = if (':' in text) ipv6Bytes(text) else ipv4Bytes(text) + +private fun ipv4Bytes(text: String): ByteArray? { + val parts = text.split('.') + if (parts.size != 4) return null + val out = ByteArray(4) + for ((i, p) in parts.withIndex()) { + val v = p.toIntOrNull() ?: return null + if (v !in 0..255) return null + out[i] = v.toByte() + } + return out +} + +private fun ipv6Bytes(text: String): ByteArray? { + var s = text + var tail = ByteArray(0) + // An embedded dotted-quad supplies the last two groups. + val lastColon = s.lastIndexOf(':') + if ('.' in s.substring(lastColon + 1)) { + tail = ipv4Bytes(s.substring(lastColon + 1)) ?: return null + s = s.substring(0, lastColon + 1) + "0:0" + } + val dc = s.indexOf("::") + val groups: List = if (dc >= 0) { + val head = s.substring(0, dc).split(':').filter { it.isNotEmpty() } + val rest = s.substring(dc + 2).split(':').filter { it.isNotEmpty() } + val missing = 8 - head.size - rest.size + if (missing < 1) return null + head + List(missing) { "0" } + rest + } else { + s.split(':') + } + if (groups.size != 8) return null + val out = ByteArray(16) + for ((i, g) in groups.withIndex()) { + if (g.isEmpty() || g.length > 4) return null + val v = g.toIntOrNull(16) ?: return null + out[2 * i] = (v shr 8).toByte() + out[2 * i + 1] = v.toByte() + } + if (tail.isNotEmpty()) tail.copyInto(out, 12) + return out +} + +/** in_addr_is_null(): every byte zero. */ +fun ipIsNull(bytes: ByteArray): Boolean = bytes.all { it.toInt() == 0 } + +/** in_addr_is_multicast(): 224.0.0.0/4 for IPv4, ff00::/8 for IPv6. */ +fun ipIsMulticast(bytes: ByteArray): Boolean = + if (bytes.size == 4) (bytes[0].toInt() and 0xF0) == 0xE0 else (bytes[0].toInt() and 0xFF) == 0xFF + + +// --------------------------------------------------------------------------------------------------- +// IPv6 prefixes — in_addr_prefix_from_string(…, AF_INET6, …) (src/basic/in-addr-util.c): an IPv6 literal +// and an optional `/LEN`, where LEN goes through in_addr_parse_prefixlen → safe_atou8 (base 0) and must +// be ≤ 128. The length is everything after the `/`, so whitespace reaches safe_atou8 (`::/ 64` and +// `::/0b 1` are fine). Without a slash the prefix length is 128. Not followed by EOF. +val IPV6_PREFIX_SAFE_LEN: Combinator = SequenceCombinator( + IPV6_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNspawnCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNspawnCombinators.kt new file mode 100644 index 00000000..a68892a1 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNspawnCombinators.kt @@ -0,0 +1,202 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +/* + * Shared value grammars for the .nspawn settings parsers (systemd src/nspawn/nspawn-settings.c and the + * helpers it calls in nspawn-mount.c / nspawn-network.c / nspawn-expose-ports.c). + * + * Everything here is pinned to systemd 0f0682b773, the commit recorded in + * systemd-build/build/last_commit_hash. Browse it at https://github.com/systemd/systemd/blob/0f0682b773. + * + * Several of these are general-purpose (the colon-delimited interface name, the address-family list) + * and are candidates for promotion into Combinators.kt; they live here only so this batch does not + * collide with parallel edits there. Booleans use the shared [BOOLEAN], and numbers the shared + * [SafeAtoTerminal]. + */ + +// --------------------------------------------------------------------------------------------------- +// Interface names — ifname_valid() (ifname_valid_full with no flags, src/basic/socket-util.c): +// printable ASCII 33…126 except `:`, `/` and `%`, at most IFNAMSIZ-1 = 15 bytes, not `.`/`..`, not +// `all`/`default`, not all digits, and not anything parse_ifindex() accepts — i.e. anything safe_atoi() +// reads as a positive `int`, such as `0x10`, `+5`, `0o7` or `0b1`. (`0x0`, `-5` or an out-of-range +// `0xFFFFFFFFFF` are not ifindexes and so are valid names.) +// +// Unlike [INTERFACE_NAME] the syntactic form stops at `:` as well as whitespace, because every nspawn +// caller splits `NAME:NAME` pairs on the colon. +// +// Backslashes: Interface=/MACVLAN=/IPVLAN= unescape each word twice — once in the outer +// extract_first_word(WHITESPACE, 0) and again in the extract_first_word(":", 0) that splits the pair — +// and VirtualEthernetExtra= once. A name that runs into whitespace after a run of n backslashes is +// therefore invalid unless n is a multiple of 4: an odd n escapes the whitespace into the name (`a\ b`, +// "interface name not valid: a b"), and n = 2 (mod 4) leaves a lone trailing backslash for the second +// pass (`a\\ b`, "Failed to extract first word"). [nspawnIfnameEscapesWhitespace] models exactly that; +// both verdicts were confirmed with fuzz-nspawn-settings at 0f0682b773. Still not modelled: an escaping +// backslash elsewhere is counted towards the 15-byte limit here although C drops it. + +private const val NSPAWN_IFNAME_CHAR = """[\x21-\x7E&&[^:/%]]""" + +private fun nspawnIfnameSemantic(maxLength: Int) = + """(?!(?:[0-9]+|\.\.?|all|default)(?!$NSPAWN_IFNAME_CHAR))$NSPAWN_IFNAME_CHAR{1,$maxLength}""" + +/** + * A [RegexTerminal] for interface names that additionally refuses, as ifname_valid() does, a span that + * parse_ifindex() would read as an interface index. + */ +private class NspawnInterfaceNameTerminal : TerminalCombinator { + private val regex = RegexTerminal("""[^\s:]+""", nspawnIfnameSemantic(15)) + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = regex.SyntacticMatch(value, offset).owned() + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val result = regex.SemanticMatch(value, offset) + if (result.matchResult >= 0 && isRefused(value, offset, result.matchResult)) { + return NoMatch.copy(longestMatch = offset) + } + return result.owned() + } + + override fun parse(value: String, offset: Int): Sequence = regex.parse(value, offset).map { step -> + when (step) { + is Parse -> step.copy(tokens = step.tokens.map { it.copy(terminal = this, valid = it.valid && !isRefused(value, it.start, it.end)) }) + is Stuck -> Stuck(step.offset, setOf(this)) + } + } + + private fun isRefused(value: String, start: Int, end: Int): Boolean = + ifindex.valueOf(value.substring(start, end)) != null || nspawnIfnameEscapesWhitespace(value, start, end) + + /** parse_ifindex(): safe_atoi() on the whole name, then refuses anything not greater than zero. */ + private val ifindex = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, + ) + + /** Report this terminal, not the wrapped regex, as the one that matched. */ + private fun MatchResult.owned(): MatchResult = if (matchResult < 0) this else copy(terminals = listOf(this@NspawnInterfaceNameTerminal)) + + override fun toString(): String = "NspawnIfname" +} + +/** + * Whether the name `value[start, end)` ends in a run of backslashes that is not a multiple of 4 long and is + * followed by whitespace — see the comment above [NSPAWN_IFNAME_CHAR] for why C refuses that. + */ +private fun nspawnIfnameEscapesWhitespace(value: String, start: Int, end: Int): Boolean { + if (end >= value.length || !value[end].isWhitespace()) return false + var run = 0 + while (end - run - 1 >= start && value[end - run - 1] == '\\') run++ + return run % 4 != 0 +} + +/** One interface name in a `NAME[:NAME]` pair. */ +val COLON_DELIMITED_INTERFACE_NAME: TerminalCombinator = NspawnInterfaceNameTerminal() + + +// --------------------------------------------------------------------------------------------------- +// Colon-separated path fields, as split by extract_first_word(..., ":", EXTRACT_DONT_COALESCE_SEPARATORS) +// (src/basic/extract-word.c). With no EXTRACT_UNQUOTE/EXTRACT_CUNESCAPE flag a backslash simply makes +// the next character literal (so `\:` embeds a colon) and quotes are ordinary characters; a trailing lone +// backslash is an error. Whitespace is NOT a separator here, so a path may contain spaces. + +/** The characters one colon-delimited field may contain. */ +internal const val COLON_FIELD_CHAR = """(?:[^:\\]|\\[\s\S])""" + +/** Any non-empty colon-delimited field — the lenient (syntactic) shape shared by the terminals below. */ +private const val COLON_FIELD = """$COLON_FIELD_CHAR+""" + +/** A colon-delimited field that must be an absolute path: path_is_absolute() only checks for a leading `/`. */ +val COLON_FIELD_ABSOLUTE_PATH = RegexTerminal(COLON_FIELD, """\\?/$COLON_FIELD_CHAR*""") + +/** + * A colon-delimited nspawn *source* path — source_path_parse() (src/nspawn/nspawn-mount.c): non-empty; + * either `+` followed by an absolute path (relative to the container image) or any other path, which + * is made absolute against the working directory, so relative paths are fine. + */ +val COLON_FIELD_SOURCE_PATH = RegexTerminal(COLON_FIELD, """\+\\?/$COLON_FIELD_CHAR*|(?!\+)$COLON_FIELD_CHAR+""") + +/** + * A colon-delimited field that must be absolute *and* normalized — path_is_absolute() && + * path_is_normalized() (src/basic/path-util.c): no empty (`//`), `.` or `..` component. A trailing + * slash is fine. The component length limit (NAME_MAX) is not modelled. + */ +val COLON_FIELD_NORMALIZED_PATH = RegexTerminal(COLON_FIELD, normalizedAbsolutePath("""[^/:\\]|\\[\s\S]""", fieldEnd = """:|$""")) + +/** + * The rest of the value as an absolute, normalized path, colons and backslashes included verbatim — + * for the trailing field that a parser strdup()s instead of extracting (PivotRoot='s second path). + */ +val REST_NORMALIZED_PATH = RegexTerminal("""[\s\S]+""", normalizedAbsolutePath("""[^/]""", fieldEnd = """$""")) + +/** + * `/`, or one or more `/component`s with an optional trailing slash, where no component is `.`/`..`. + * [fieldEnd] is what may follow the path (the end of the value, or also a `:` separator). + */ +private fun normalizedAbsolutePath(componentChar: String, fieldEnd: String): String { + val component = """(?!\.\.?(?:/|$fieldEnd))(?:$componentChar)+""" + return """(?:/$component)+/?|/""" +} + + +// --------------------------------------------------------------------------------------------------- +// User names — valid_user_group_name(name, 0), i.e. the STRICT flavour (src/basic/user-util.c): a +// letter or underscore, then letters, digits, `_` or `-`, at most 31 bytes in total (the utmp ut_user +// field). No dots, no leading digit or dash, no numeric UIDs. + +/** One whitespace-separated user name in strict valid_user_group_name() form. */ +val STRICT_USER_NAME = RegexTerminal("""\S+""", """[A-Za-z_][A-Za-z0-9_-]{0,30}(?![A-Za-z0-9_-])""") + + +// --------------------------------------------------------------------------------------------------- +// 128-bit IDs — sd_id128_from_string() (src/libsystemd/sd-id128/sd-id128.c): 32 hex digits, or the +// 8-4-4-4-12 UUID form, optionally behind a case-insensitive `urn:uuid:` prefix, which then requires the +// UUID form. Hex digits are case-insensitive. + +private const val ID128_PLAIN = """[0-9a-fA-F]{32}""" +private const val ID128_UUID = """[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}""" + +// id128_from_string_nonzero() additionally refuses the all-zero ID. +private const val ID128_NONZERO = """(?=[0-]*[1-9a-fA-F])""" + +/** The `urn:uuid:` prefix sd_id128_from_string() strips with startswith_no_case(). */ +val ID128_URN_PREFIX = RegexTerminal("""(?i)urn:uuid:""", """(?i)urn:uuid:""") + +/** A non-zero 128-bit ID in UUID (dashed) form only. */ +val NONZERO_ID128_UUID = RegexTerminal("""[0-9A-Za-z-]+""", """$ID128_NONZERO$ID128_UUID""") + +/** A non-zero 128-bit ID in either the plain 32-hex-digit or the UUID form. */ +val NONZERO_ID128 = RegexTerminal("""[0-9A-Za-z-]+""", """$ID128_NONZERO(?:$ID128_PLAIN|$ID128_UUID)""") + + +// --------------------------------------------------------------------------------------------------- +// Address families — af_from_name() (src/basic/af-list.c). The lookup table is gperf-generated with +// `--ignore-case` (src/basic/meson.build), and af_from_name() retries a failed lookup with `AF_` +// prepended, so `AF_INET`, `af_inet`, `inet` and `INET6` all resolve. +// +// The canonical names are those of ConfigParseAddressFamiliesOptionValue (the AF_* macros in +// , minus AF_UNSPEC and AF_MAX). +private val ADDRESS_FAMILY_CANONICAL_NAMES = arrayOf( + "AF_ALG", "AF_APPLETALK", "AF_ASH", "AF_ATMPVC", "AF_ATMSVC", "AF_AX25", "AF_BLUETOOTH", "AF_BRIDGE", + "AF_CAIF", "AF_CAN", "AF_DECnet", "AF_ECONET", "AF_FILE", "AF_IB", "AF_IEEE802154", "AF_INET", + "AF_INET6", "AF_IPX", "AF_IRDA", "AF_ISDN", "AF_IUCV", "AF_KCM", "AF_KEY", "AF_LLC", "AF_LOCAL", + "AF_MCTP", "AF_MPLS", "AF_NETBEUI", "AF_NETLINK", "AF_NETROM", "AF_NFC", "AF_PACKET", "AF_PHONET", + "AF_PPPOX", "AF_QIPCRTR", "AF_RDS", "AF_ROSE", "AF_ROUTE", "AF_RXRPC", "AF_SECURITY", "AF_SMC", + "AF_SNA", "AF_TIPC", "AF_UNIX", "AF_VSOCK", "AF_WANPIPE", "AF_X25", "AF_XDP", +) + +private val REMOVED_ADDRESS_FAMILIES = mapOf( + "AF_DECnet" to "AF_DECnet is obsolete: DECnet support was removed from the Linux kernel in 6.1.", + "AF_IRDA" to "AF_IRDA is obsolete: IrDA support was removed from the Linux kernel in 4.17.", + "AF_ECONET" to "AF_ECONET is obsolete: Acorn Econet support was removed from the Linux kernel in 3.5.", + "AF_WANPIPE" to "AF_WANPIPE is obsolete: WANPIPE support was removed from the Linux kernel in 2.6.21.", +) + +/** An address family name, with or without the `AF_` prefix, in any case. */ +val ADDRESS_FAMILY_NAME = FlexibleLiteralChoiceTerminal( + *ADDRESS_FAMILY_CANONICAL_NAMES, + *ADDRESS_FAMILY_CANONICAL_NAMES.map { it.removePrefix("AF_") }.toTypedArray(), + ignoreCase = true, +).deprecating(REMOVED_ADDRESS_FAMILIES + REMOVED_ADDRESS_FAMILIES.mapKeys { it.key.removePrefix("AF_") }) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchQdiscCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchQdiscCombinators.kt new file mode 100644 index 00000000..e1797519 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchQdiscCombinators.kt @@ -0,0 +1,324 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigDecimal +import java.math.BigInteger + +/* + * Shared grammars for the systemd-networkd traffic-control validators (.network [QDisc]-family and + * [...Class] sections, systemd src/network/tc/), written for the 2026-10 qdisc batch. + * + * Everything here is pinned to systemd 0f0682b773 (systemd-build/build/last_commit_hash): + * https://github.com/systemd/systemd/blob/0f0682b773 + * + * The tc parsers lean on four leaf helpers. safe_atou / safe_atou32 / safe_atou8 / + * safe_atou32_full(.., 16) (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429) + * are the shared [SafeAtoTerminal], written out at each call site. The other three are modelled here as + * closely to the C as the combinator framework allows: + * + * - parse_sec / parse_time -> [parseSec] + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198 + * - parse_size(.., 1000 | 1024) -> [parseSize] + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157 + * - parse_permyriad (via parse_tc_percent) -> [TC_PERCENT] + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L149 + * + * These are more faithful than the older shared terminals in Combinators.kt (TIME_VALUE rejects + * "1min30s"; ByteSizeTerminal rejects "1G 512M"). They live in this file only so the batch does not + * collide with parallel edits to Combinators.kt; see RECOMMENDATIONS.md for promoting + * them. + */ + +// --------------------------------------------------------------------------------------------------- +// Generic helpers + +private val OPT_WS = ZeroOrOne(WhitespaceTerminal()) + +/** + * Wraps [inner] and additionally requires the text it matched to satisfy [accept]. + * + * Used for value-level range checks that no single terminal can see — e.g. a parse_size() value + * written as several elements (`1G 512M`), where the bound applies to the sum. Matching (and so + * syntax highlighting and error localisation) is delegated to [inner] unchanged; only validity is + * narrowed. When [accept] returns null ("could not evaluate") the value is accepted: the grammar has + * already decided it is well-formed, and an evaluator gap must never flag a valid value. + * + * Empty matches are never accepted, so a grammar built only of optional parts (like [parseSize]'s + * chain of optional elements) cannot match the empty string. + */ +class ValueChecked(private val inner: Combinator, private val accept: (String) -> Boolean?) : Combinator { + + private fun ok(text: String): Boolean = accept(text) ?: true + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val m = inner.SyntacticMatch(value, offset) + return if (m.matchResult == offset) NoMatch.copy(longestMatch = offset) else m + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val m = inner.SemanticMatch(value, offset) + if (m.matchResult == -1) return m + if (m.matchResult == offset) return NoMatch.copy(longestMatch = offset) + return if (ok(value.substring(offset, m.matchResult))) m else NoMatch.copy(longestMatch = offset) + } + + override fun parse(value: String, offset: Int): Sequence = + inner.parse(value, offset).map { step -> + when (step) { + is Stuck -> step + is Parse -> when { + step.end == offset -> Stuck(offset, setOf(this)) + ok(value.substring(offset, step.end)) -> step + // Out of range: keep the tokens (so the span still highlights) but mark them invalid. + else -> Parse(step.end, step.tokens.map { it.copy(valid = false) }, step.regions) + } + } + } + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "Checked(\n" + inner.toStringIndented(indent + 1) + "\n" + " ".repeat(indent) + ")" + + override fun toString(): String = toStringIndented(0) +} + +// --------------------------------------------------------------------------------------------------- +// Bounds + +/** UINT32_MAX, for the size/time range checks. */ +private val UINT32_MAX_BIG: BigInteger = SafeAtoTerminal.UINT32_MAX + +/** ULLONG_MAX: parse_size() and parse_time() return ERANGE above it. */ +private val UINT64_MAX_BIG: BigInteger = SafeAtoTerminal.UINT64_MAX + +// --------------------------------------------------------------------------------------------------- +// parse_sec / parse_time + +// extract_multiplier()'s table, in table order: the C takes the first entry the text starts with, +// and a regex alternation does the same (time-util.c#L1147-L1196). +private const val SEC_SUFFIX_ALTERNATION = + "seconds|second|sec|s|minutes|minute|min|months|month|M|msec|ms|m|hours|hour|hr|h|days|day|d|" + + "weeks|week|w|years|year|y|usec|us|μs|µs" + +private val SEC_SUFFIX = Labeled(Role.ENUM, RegexTerminal(SEC_SUFFIX_ALTERNATION, SEC_SUFFIX_ALTERNATION)) + +// strtoll() digits with an optional '+', then an optional '.digits'; or a bare '.digits' (".5s" is +// legal: strtoll consumes nothing, and the '.' branch takes over). "5." and "+.5" are not. +private const val SEC_NUMBER_RE = """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""" +private val SEC_NUMBER = Labeled(Role.LITERAL, RegexTerminal(SEC_NUMBER_RE, SEC_NUMBER_RE)) + +// After a number comes either a unit (optionally surrounded by whitespace), or whitespace, or the end. +// A unit may be followed directly by the next number ("5s500ms", "3.1s.2"); a bare number may not +// ("12.34.56" is EINVAL). +private val SEC_ELEMENT = SequenceCombinator( + SEC_NUMBER, + AlternativeCombinator( + SequenceCombinator(OPT_WS, SEC_SUFFIX, OPT_WS), + WhitespaceTerminal(), + EOF(), + ), +) + +private val PARSE_SEC = AlternativeCombinator( + LiteralChoiceTerminal("infinity"), + OneOrMore(SEC_ELEMENT), +) + +private val USEC_INFINITY: BigInteger = UINT64_MAX_BIG + +private val LLONG_MAX_BIG: BigInteger = BigInteger.valueOf(Long.MAX_VALUE) + +/** Returned by [evaluateParseSec] when parse_time() would fail with ERANGE; above every accepted bound. */ +private val PARSE_SEC_ERANGE: BigInteger = USEC_INFINITY + BigInteger.ONE + +private val SEC_MULTIPLIERS: Map = run { + val sec = 1_000_000L + val minute = 60 * sec + val hour = 60 * minute + val day = 24 * hour + mapOf( + "seconds" to sec, "second" to sec, "sec" to sec, "s" to sec, + "minutes" to minute, "minute" to minute, "min" to minute, "m" to minute, + "months" to 2_629_800L * sec, "month" to 2_629_800L * sec, "M" to 2_629_800L * sec, + "msec" to 1000L, "ms" to 1000L, + "hours" to hour, "hour" to hour, "hr" to hour, "h" to hour, + "days" to day, "day" to day, "d" to day, + "weeks" to 7 * day, "week" to 7 * day, "w" to 7 * day, + "years" to 31_557_600L * sec, "year" to 31_557_600L * sec, "y" to 31_557_600L * sec, + "usec" to 1L, "us" to 1L, "μs" to 1L, "µs" to 1L, + ).mapValues { BigInteger.valueOf(it.value) } +} + +private val SEC_EVAL_ELEMENT = Regex("""\s*\+?([0-9]*)(?:\.([0-9]+))?\s*($SEC_SUFFIX_ALTERNATION)?""") + +/** + * parse_time(t, USEC_PER_SEC) as a value in microseconds, or null if [text] is not evaluable. Overflow + * (C's ERANGE: an integer part above LLONG_MAX, or a finite total reaching USEC_INFINITY) yields a value + * greater than USEC_INFINITY so that every bound check rejects it. + */ +fun evaluateParseSec(text: String): BigInteger? { + val t = text.trim() + if (t == "infinity") return USEC_INFINITY + var pos = 0 + var total = BigInteger.ZERO + while (pos < t.length) { + while (pos < t.length && t[pos].isWhitespace()) pos++ + if (pos >= t.length) break + val m = SEC_EVAL_ELEMENT.matchAt(t, pos) ?: return null + if (m.value.isEmpty()) return null + pos += m.value.length + val multiplier = m.groupValues[3].takeIf { it.isNotEmpty() }?.let { SEC_MULTIPLIERS[it] } ?: SEC_MULTIPLIERS.getValue("s") + val whole = BigInteger(m.groupValues[1].ifEmpty { "0" }) + // strtoll() sets ERANGE above LLONG_MAX; then `(usec_t) l >= USEC_INFINITY / multiplier` and + // `k >= USEC_INFINITY - usec` are ERANGE too, so a finite total can never reach USEC_INFINITY. + if (whole > LLONG_MAX_BIG || whole >= USEC_INFINITY / multiplier) return PARSE_SEC_ERANGE + val k = whole * multiplier + if (k >= USEC_INFINITY - total) return PARSE_SEC_ERANGE + total += k + // The C adds the fractional digits one by one, each times a truncated multiplier/10^k. + var step = multiplier / BigInteger.TEN + for (digit in m.groupValues[2]) { + val kf = BigInteger.valueOf((digit - '0').toLong()) * step + if (kf >= USEC_INFINITY - total) return PARSE_SEC_ERANGE + total += kf + step /= BigInteger.TEN + } + } + return total +} + +/** + * A whole parse_sec() value: `infinity`, or one or more `number[unit]` elements. + * + * Mirrors parse_time() (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287) + * including the forms its unit test asserts are legal (test-time-util.c#L21): `5s500ms`, `.22s`, + * `+3.1s`, `3.1s.2`, `3.1 .2`, `23µs`; and rejects what it asserts are not: `5.`, `.s`, `-5s`, + * `3.s`, `3. 1s`, `12.34.56`, `infinity .7`. + * + * @param maxUsec when set, the value (in microseconds, `infinity` counting as USEC_INFINITY) must not + * exceed it. + */ +fun parseSec(maxUsec: BigInteger? = null): Combinator = + ValueChecked(PARSE_SEC) { text -> + evaluateParseSec(text)?.let { it <= (maxUsec ?: USEC_INFINITY) } + } + +/** [maxUsec] for parsers that refuse anything above UINT32_MAX microseconds (and so `infinity`). */ +val UINT32_MAX_USEC: BigInteger = UINT32_MAX_BIG + +// --------------------------------------------------------------------------------------------------- +// parse_size + +// strtoull(base 10) digits with an optional '+', then an optional '.' with optional digits: "3." and +// "10.M" are legal (parse-util.c "Ignore failure. E.g. 10.M is valid"); ".5" is not. +private const val SIZE_NUMBER_RE = """\+?[0-9]+(?:\.[0-9]*)?""" +private val SIZE_NUMBER = Labeled(Role.LITERAL, RegexTerminal(SIZE_NUMBER_RE, SIZE_NUMBER_RE)) + +private val SIZE_SUFFIXES = listOf("E", "P", "T", "G", "M", "K", "B") + +private fun sizeChain(allowInnerWhitespace: Boolean): Combinator { + val parts = mutableListOf() + for (suffix in SIZE_SUFFIXES) { + val element = if (allowInnerWhitespace) { + SequenceCombinator(SIZE_NUMBER, OPT_WS, LiteralChoiceTerminal(suffix), OPT_WS) + } else { + SequenceCombinator(SIZE_NUMBER, LiteralChoiceTerminal(suffix)) + } + parts.add(ZeroOrOne(element)) + } + // The "" entry of the table: a bare number, which can only be the last element. + parts.add(ZeroOrOne(SIZE_NUMBER)) + return SequenceCombinator(*parts.toTypedArray()) +} + +private val SIZE_EVAL_ELEMENT = Regex("""\s*\+?([0-9]+)(?:\.([0-9]*))?\s*([EPTGMKB]?)\s*""") + +/** parse_size(text, base) as a byte count, or null if [text] is not evaluable. */ +fun evaluateParseSize(text: String, base: Int): BigInteger? { + val factorBase = BigInteger.valueOf(base.toLong()) + var pos = 0 + var total = BigInteger.ZERO + while (pos < text.length) { + val m = SIZE_EVAL_ELEMENT.matchAt(text, pos) ?: return null + if (m.value.isEmpty()) return null + pos += m.value.length + val power = when (m.groupValues[3]) { + "E" -> 6; "P" -> 5; "T" -> 4; "G" -> 3; "M" -> 2; "K" -> 1; else -> 0 + } + val factor = factorBase.pow(power) + total += BigInteger(m.groupValues[1]) * factor + val frac = m.groupValues[2] + if (frac.isNotEmpty()) total += BigDecimal("0.$frac").multiply(BigDecimal(factor)).toBigInteger() + } + return total +} + +/** + * A parse_size() value (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283): + * one or more `number[.fraction][suffix]` elements whose suffixes strictly descend through + * `E P T G M K B ""` — `4M 11.5K`, `3.5G3B` and `4T3G3B` are legal; `3B3.5G`, `12P12P`, `12X`, `5k` + * (suffixes are upper-case only) and `-1` are not (test-parse-util.c#L108, #L178). A bare number (the + * `""` entry) must be the last element. + * + * @param base 1000 (SI, for rates) or 1024 (IEC, for sizes); only affects the range check. + * @param minInclusive / maxInclusive bounds on the resulting byte (or bit) count. The upper bound + * defaults to ULLONG_MAX, above which parse_size() itself returns ERANGE. + * @param allowInnerWhitespace false for an element of a whitespace-separated list (each word is + * parsed on its own, so a space would split the size into two words). + */ +fun parseSize( + base: Int, + minInclusive: BigInteger = BigInteger.ZERO, + maxInclusive: BigInteger = UINT64_MAX_BIG, + allowInnerWhitespace: Boolean = true, +): Combinator = + ValueChecked(sizeChain(allowInnerWhitespace)) { text -> + evaluateParseSize(text, base)?.let { it >= minInclusive && it <= maxInclusive } + } + +/** UINT32_MAX, the cap several tc size parsers apply after parse_size(). */ +val SIZE_UINT32_MAX: BigInteger = UINT32_MAX_BIG + +// --------------------------------------------------------------------------------------------------- +// parse_permyriad / parse_tc_percent + +// The integer part goes through safe_atoi() (base 0, mangle_base), so `0x10%` is 16% and `0b 1%` is 1%, +// and parse_parts_value_*() return ERANGE when it is negative (`-0` is fine, and so is `0b-0`). Its +// magnitude is capped by the 100% check below, so int's range on the terminal itself is enough. The +// whole value reaches safe_atoi() up to the '.' or the symbol, so whitespace can reach the parser. +// https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L7-L23 +private val PCT_INT = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, +) + +/** + * A percentage as parse_permyriad() reads it + * (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L135-L157): an integer + * followed by `%` (up to two decimals), `‰` (up to one decimal) or `‱` (no decimals), at most 100%. + * test-percent-util.c#L87 asserts `55.53%`, `555.5‰` and `1000‱` legal and `3.212%`, `3.22‰`, + * `3.2‱`, `107%`, `-7%` and a bare `50` illegal. + */ +val TC_PERCENT: Combinator = ValueChecked( + AlternativeCombinator( + SequenceCombinator(PCT_INT, ZeroOrOne(SequenceCombinator(DOT, RegexTerminal("[0-9]{1,2}", "[0-9]{1,2}"))), LiteralChoiceTerminal("%")), + SequenceCombinator(PCT_INT, ZeroOrOne(SequenceCombinator(DOT, RegexTerminal("[0-9]", "[0-9]"))), LiteralChoiceTerminal("‰")), + SequenceCombinator(PCT_INT, LiteralChoiceTerminal("‱")), + ), +) { text -> evaluatePermyriad(text)?.let { it in 0..10_000 } } + +private val PCT_EVAL = Regex("""(.*?)(?:\.([0-9]+))?([%‰‱])""") + +/** parse_permyriad_unbounded(text), or null if not evaluable (the integer part is read like [PCT_INT]). */ +fun evaluatePermyriad(text: String): Long? { + val m = PCT_EVAL.matchEntire(text) ?: return null + val n = PCT_INT.valueOf(m.groupValues[1])?.toLong() ?: return null + val frac = m.groupValues[2] + return when (m.groupValues[3]) { + "%" -> n * 100 + (frac.padEnd(2, '0').toLongOrNull() ?: 0) + "‰" -> n * 10 + (frac.toLongOrNull() ?: 0) + else -> n + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt new file mode 100644 index 00000000..64b9fb21 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt @@ -0,0 +1,209 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +/* + * Shared terminals for the seccomp / BPF / cgroup validators (batch 2026-10). + * + * Everything here is pinned to systemd 0f0682b773, the commit in systemd-build/build/last_commit_hash + * that the plugin's gperf and man data are generated from. Browse it at + * https://github.com/systemd/systemd/blob/0f0682b773. + * + * These live in their own file rather than Combinators.kt because several validator families were + * written in parallel; the generally useful ones (ERRNO_OR_SECCOMP_ACTION, PRINTF_CHAR, the + * whitespace-or-comma separator) are candidates for promotion. + */ + +/** + * One run of separators for a list split with `extract_first_word(&p, &word, WHITESPACE ",", …)`. + * + * extract_first_word skips any leading separators and coalesces runs of them (unless + * EXTRACT_DONT_COALESCE_SEPARATORS is passed, which none of these callers do), so `a, b`, `a,,b` and + * `a b` all split into the same two words. + * + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 + */ +val WHITESPACE_OR_COMMA_SEPARATOR = RegexTerminal("[\\s,]+", "[\\s,]+") + +// --------------------------------------------------------------------------------------------------- +// errno names / numbers — parse_errno (src/basic/parse-util.c#L342) and +// seccomp_parse_errno_or_action (src/shared/seccomp-util.c#L2688). + +/** + * The errno names errno_from_name() knows: the `#define E…` macros of ``, as listed by + * src/basic/generate-errno-list.sh (which drops the parisc-only aliases ECANCELLED, EREFUSED, + * EFSBADCRC and EFSCORRUPTED). Regenerate with + * + * echo | gcc -dM -include errno.h -E - | grep -Ev '^#define[[:space:]]+(ECANCELLED|EREFUSED|EFSBADCRC|EFSCORRUPTED)' \ + * | awk '/^#define[ \t]+E[^ _]+[ \t]+/ { print $2; }' | sort + * + * The lookup table is built by gperf with `--ignore-case` (src/basic/meson.build#L168), so `eperm` + * resolves as well as `EPERM`. + */ +val ERRNO_NAMES = arrayOf( + "E2BIG", "EACCES", "EADDRINUSE", "EADDRNOTAVAIL", "EADV", "EAFNOSUPPORT", "EAGAIN", "EALREADY", "EBADE", "EBADF", + "EBADFD", "EBADMSG", "EBADR", "EBADRQC", "EBADSLT", "EBFONT", "EBUSY", "ECANCELED", "ECHILD", "ECHRNG", + "ECOMM", "ECONNABORTED", "ECONNREFUSED", "ECONNRESET", "EDEADLK", "EDEADLOCK", "EDESTADDRREQ", "EDOM", "EDOTDOT", "EDQUOT", + "EEXIST", "EFAULT", "EFBIG", "EHOSTDOWN", "EHOSTUNREACH", "EHWPOISON", "EIDRM", "EILSEQ", "EINPROGRESS", "EINTR", + "EINVAL", "EIO", "EISCONN", "EISDIR", "EISNAM", "EKEYEXPIRED", "EKEYREJECTED", "EKEYREVOKED", "EL2HLT", "EL2NSYNC", + "EL3HLT", "EL3RST", "ELIBACC", "ELIBBAD", "ELIBEXEC", "ELIBMAX", "ELIBSCN", "ELNRNG", "ELOOP", "EMEDIUMTYPE", + "EMFILE", "EMLINK", "EMSGSIZE", "EMULTIHOP", "ENAMETOOLONG", "ENAVAIL", "ENETDOWN", "ENETRESET", "ENETUNREACH", "ENFILE", + "ENOANO", "ENOBUFS", "ENOCSI", "ENODATA", "ENODEV", "ENOENT", "ENOEXEC", "ENOKEY", "ENOLCK", "ENOLINK", + "ENOMEDIUM", "ENOMEM", "ENOMSG", "ENONET", "ENOPKG", "ENOPROTOOPT", "ENOSPC", "ENOSR", "ENOSTR", "ENOSYS", + "ENOTBLK", "ENOTCONN", "ENOTDIR", "ENOTEMPTY", "ENOTNAM", "ENOTRECOVERABLE", "ENOTSOCK", "ENOTSUP", "ENOTTY", "ENOTUNIQ", + "ENXIO", "EOPNOTSUPP", "EOVERFLOW", "EOWNERDEAD", "EPERM", "EPFNOSUPPORT", "EPIPE", "EPROTO", "EPROTONOSUPPORT", "EPROTOTYPE", + "ERANGE", "EREMCHG", "EREMOTE", "EREMOTEIO", "ERESTART", "ERFKILL", "EROFS", "ESHUTDOWN", "ESOCKTNOSUPPORT", "ESPIPE", + "ESRCH", "ESRMNT", "ESTALE", "ESTRPIPE", "ETIME", "ETIMEDOUT", "ETOOMANYREFS", "ETXTBSY", "EUCLEAN", "EUNATCH", + "EUSERS", "EWOULDBLOCK", "EXDEV", "EXFULL", +) + +/** An errno name as errno_from_name() resolves it (case-insensitively). */ +val ERRNO_NAME = FlexibleLiteralChoiceTerminal(*ERRNO_NAMES, ignoreCase = true) + +/** + * The argument of a `syscall:ERRNO` entry: `kill`, an errno name, or a number. + * seccomp_parse_errno_or_action() special-cases `kill` (exact, lower case) and otherwise calls + * parse_errno(), which takes a name first and then falls back to safe_atoi() (sign, `0x`, `0b`, `0o`, + * leading-`0` octal, see [SafeAtoTerminal]), accepting 0 and 1…ERRNO_MAX (4095), so `-0` is fine but `-3` + * is not. + * + * The entry is a word from a whitespace split, so whitespace never reaches safe_atoi() here + * (`acceptsWhitespace = false`; sd-check rejects `SystemCallFilter=~uname:0b 1` because `0b` is a word of its + * own). A malformed number such as `08` is a syntax error located where strtol() stopped. + * + * The number comes before the name because ERRNO_NAME's lenient shape includes digits (E2BIG): under + * the classic engine, with the name first, `0x1F` would be taken as the one-character name `0` and the + * rest stranded. + */ +val ERRNO_OR_SECCOMP_ACTION = AlternativeCombinator( + LiteralChoiceTerminal("kill"), + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4095L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ERRNO_NAME, +) + +// --------------------------------------------------------------------------------------------------- +// System call names and groups — seccomp_parse_syscall_filter (src/shared/seccomp-util.c#L1293). + +/** + * The predefined system call groups, without their leading `@` (syscall_filter_sets[], + * src/shared/seccomp-util.c#L334; looked up by exact string compare in syscall_filter_set_find, + * #L1016). An unknown group is logged ("Unknown system call group, ignoring") and dropped. + */ +val SYSCALL_GROUP_NAME = FlexibleLiteralChoiceTerminal( + "default", "aio", "basic-io", "chown", "clock", "cpu-emulation", "debug", "file-system", "io-event", "ipc", + "keyring", "memlock", "module", "mount", "network-io", "obsolete", "pkey", "privileged", "process", "raw-io", + "reboot", "resources", "sandbox", "setuid", "signal", "swap", "sync", "system-service", "timer", "known", +) + +/** + * A bare system call name. These are resolved at runtime by libseccomp's + * seccomp_syscall_resolve_name() for the *native* architecture, so whether e.g. `arm_fadvise64_64` is + * "known" depends on the machine; an unknown name is logged and ignored. The set is therefore not + * enumerated here: the only check is the shape every libseccomp name has (lower case letters, digits + * and underscores — `_llseek`, `epoll_wait`, `s390_pci_mmio_read`). The lenient syntactic side takes + * anything up to a separator or `:`, so `DONOTEXIST` is located and flagged as a single token. It does not + * start at `@` or `~`, which belong to the `@group` and literal-`~` alternatives: otherwise each such word + * would also get a second (invalid) parse here, and a long invalid list would explode into 2^n parses on the + * new engine. + */ +val SYSCALL_NAME = RegexTerminal("[^\\s,:@~][^\\s,:]*", "[a-z_][a-z0-9_]*") + +/** `@group` or a bare system call name. */ +val SYSCALL_OR_GROUP = AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("@"), SYSCALL_GROUP_NAME), + SYSCALL_NAME, +) + +// --------------------------------------------------------------------------------------------------- +// File system names — bpf_restrict_fs_parse_filesystem (src/core/bpf-restrict-fs.c#L212). + +/** + * The file system names fs_type_from_string() resolves: the keys of NAME_TO_MAGIC in + * src/basic/filesystem-sets.py, compiled into a case-sensitive gperf table. A name outside this set is + * accepted by the unit-file parser and only logged and skipped when the unit starts and the BPF map is + * filled ("bpf-restrict-fs: Invalid filesystem name '%s', ignoring.", src/core/bpf-restrict-fs.c#L117-L121). + * Validators must therefore accept such names too (RestrictFileSystems= uses these choices as exact + * literals next to a lenient word); issue #565 tracks a separate warning for them. The list is kept for + * completion and coloring. + */ +val FILESYSTEM_NAME = FlexibleLiteralChoiceTerminal( + "apparmorfs", "adfs", "affs", "afs", "anon_inodefs", "autofs", "balloon-kvm", "bcachefs", "bdev", "binder", + "binfmt_misc", "bpf", "btrfs", "btrfs_test_fs", "cpuset", "ceph", "cgroup2", "cgroup", "cifs", "coda", + "configfs", "cramfs", "dax", "debugfs", "devmem", "devpts", "devtmpfs", "dmabuf", "ecryptfs", "efivarfs", + "efs", "erofs", "ext2", "ext3", "ext4", "exfat", "f2fs", "failfs", "fuseblk", "fuse", + "fusectl", "gfs", "gfs2", "gmem", "hostfs", "hpfs", "hugetlbfs", "iso9660", "jffs2", "minix", + "mqueue", "msdos", "ncp", "ncpfs", "nfs", "nfs4", "nilfs2", "nsfs", "ntfs", "ntfs3", + "nullfs", "ocfs2", "openpromfs", "orangefs", "overlay", "pidfs", "pipefs", "ppc-cmm", "proc", "pstore", + "pvfs2", "qnx4", "qnx6", "ramfs", "resctrl", "reiserfs", "rpc_pipefs", "secretmem", "securityfs", "selinuxfs", + "shiftfs", "smackfs", "smb3", "smbfs", "sockfs", "squashfs", "sysfs", "tmpfs", "tracefs", "udf", + "usbdevfs", "vboxsf", "vfat", "v9fs", "xenfs", "xfs", "z3fold", "zonefs", "zsmalloc", +) + +/** + * The file system groups, without the leading `@` (FILESYSTEM_SETS in src/basic/filesystem-sets.py, + * looked up by exact compare in filesystem_set_find, src/basic/filesystems.c#L21). The unit-file parser + * refuses an unknown group ("Unknown filesystem group, ignoring"), so validators flag it. + */ +val FILESYSTEM_GROUP_NAME = FlexibleLiteralChoiceTerminal( + "basic-api", "anonymous", "application", "auxiliary-api", "common-block", "historical-block", "network", + "privileged-api", "security", "temporary", "known", +) + +// --------------------------------------------------------------------------------------------------- +// Specifier expansion — unit_full_printf_full (src/core/unit-printf.c#L179). + +/** + * The specifier letters unit_full_printf() knows (src/core/unit-printf.c#L210-L238 plus + * COMMON_SYSTEM/CREDS/TMP_SPECIFIERS from src/shared/specifier.h#L76-L99), and `%` for a literal + * percent sign. Same set as ConfigParseUnitStringPrintfOptionValue uses. + */ +const val UNIT_SPECIFIER_LETTERS = "iIjJnNpPfyYcrRCdDELSthsaAbBHlqmMovwWgGuUTV%" + +/** + * A regex fragment for one character (or `%x` pair) of specifier-expanded text, per specifier_printf + * (src/shared/specifier.c#L33): a known specifier expands; an unknown *alphanumeric* one is an error + * (-EBADSLT, the assignment is dropped); `%` followed by anything else — or at the very end — is kept + * literally. Use inside a character-class-free regex, e.g. `(?:$PRINTF_CHAR)*`. + */ +const val PRINTF_CHAR = "(?:[^%]|%[$UNIT_SPECIFIER_LETTERS]|%(?![A-Za-z0-9]))" + +/** [PRINTF_CHAR], but also excluding [excluded] (a character-class body, e.g. `=` or `\\s`) as a plain character. */ +fun printfCharExcluding(excluded: String): String = "(?:[^%$excluded]|%[$UNIT_SPECIFIER_LETTERS]|%(?![A-Za-z0-9]))" + +// --------------------------------------------------------------------------------------------------- +// BPF token delegation lists — bpf_delegate_from_string (src/core/namespace.c#L4221). + +/** + * The value grammar shared by BPFDelegateCommands= / BPFDelegateMaps= / BPFDelegatePrograms= / + * BPFDelegateAttachments=: either the whole value `any`, or a comma-separated list of names from + * [names]. + * + * bpf_delegate_from_string() checks `streq(s, "any")` against the whole value, then splits on `,` + * *only* — no whitespace — with extract_first_word(..., ",", 0), which skips leading commas and + * coalesces runs of them. So `A, B` makes a word ` B` that is not in the table ("Unknown BPF delegate + * option, ignoring") and is flagged here, while `A,,B` and a trailing comma are fine. Each word is + * looked up with DEFINE_STRING_TABLE_LOOKUP, i.e. an exact, case-sensitive streq. + */ +fun bpfDelegateList(names: FlexibleLiteralChoiceTerminal): Combinator { + val commas = RegexTerminal(",+", ",+") + return AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("any"), EOF()), + SequenceCombinator( + ZeroOrOne(commas), + ZeroOrOne( + SequenceCombinator( + names, + ZeroOrMore(SequenceCombinator(commas, names)), + ZeroOrOne(commas), + ) + ), + EOF(), + ), + ) +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchUnitCoreCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchUnitCoreCombinators.kt new file mode 100644 index 00000000..968a388d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchUnitCoreCombinators.kt @@ -0,0 +1,403 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigInteger + +/* + * Shared grammars for the unit-core validator batch ([Timer] timers, [Socket] Listen*=, assorted + * [Unit]/[Service] settings and the generic long / iec_size_long / path parsers). + * + * Everything here is modelled on systemd 0f0682b773, the commit recorded in + * systemd-build/build/last_commit_hash: https://github.com/systemd/systemd/blob/0f0682b773 + * + * These were written for one batch and live in their own file so parallel work on Combinators.kt does + * not collide; some of them (the time span, the unit-specifier escape hatch) are general enough to be + * promoted there. Integers read by the safe_ato* family use the shared [SafeAtoTerminal] instead. + * + * Every grammar here is careful about the classic engine: no repetition wraps something that can match + * the empty string (classic ZeroOrMore/OneOrMore would loop forever), and every alternative that + * could stop on a prefix of another is either ordered after it or terminated with EOF(). + * + * Everything is namespaced in [BatchUnitCoreGrammar] so that generic names like TIMESPAN cannot collide + * with top-level declarations other batches add to this package in parallel. + */ + +object BatchUnitCoreGrammar { + + // --------------------------------------------------------------------------------------------------- + // Unit specifiers — specifier_printf (src/shared/specifier.c) driven by the table in + // unit_full_printf_full (src/core/unit-printf.c), which also pulls in COMMON_SYSTEM_SPECIFIERS, + // COMMON_CREDS_SPECIFIERS and COMMON_TMP_SPECIFIERS (src/shared/specifier.h). + // + // `%%` is a literal percent sign. `%` followed by a letter or digit that is NOT in the table is a hard + // error (-EBADSLT, "Failed to resolve unit specifiers"), so the setting is ignored. `%` followed by + // anything else (or at the very end) is copied through verbatim. + + /** Every specifier letter unit_full_printf() knows about. */ + const val UNIT_SPECIFIER_LETTERS = "aAbBcCdDEfgGhHiIjJlLmMnNopPqrRsStTuUvVwWyY" + + /** One character of specifier-expanded text: anything but a `%`, or a `%` that expands to something. */ + const val UNIT_TEXT_CHAR = """(?:[^%]|%%|%[$UNIT_SPECIFIER_LETTERS]|%(?![A-Za-z0-9%]))""" + + // A run of text that contains at least one real (value-changing) specifier, and no unknown ones. + private const val NON_SPECIFIER_CHAR = """(?:[^%]|%%|%(?![A-Za-z0-9%]))""" + + /** + * A whole value that contains at least one unit specifier (`%i`, `%t`, …) and no unknown ones. + * + * What a specifier expands to is only known when the unit is loaded, so a value that uses one cannot + * be checked against a grammar for the expanded text. This terminal is the escape hatch: it accepts + * such a value as long as every `%X` in it is one systemd knows (an unknown one makes systemd ignore + * the whole assignment, so that is still flagged). + */ + val UNIT_SPECIFIED_VALUE = RegexTerminal( + """[\s\S]*%[A-Za-z0-9][\s\S]*""", + """(?=$NON_SPECIFIER_CHAR*%[$UNIT_SPECIFIER_LETTERS])$UNIT_TEXT_CHAR*$""" + ) + + /** + * [grammar] (which must itself end in [EOF]), or any value that uses a unit specifier — for settings + * whose C parser runs unit_full_printf()/unit_path_printf() before parsing the expanded text. + */ + fun withUnitSpecifiers(grammar: Combinator): Combinator = + AlternativeCombinator(grammar, SequenceCombinator(UNIT_SPECIFIED_VALUE, EOF())) + + + // --------------------------------------------------------------------------------------------------- + // Time spans — parse_time(…, USEC_PER_SEC) a.k.a. parse_sec (src/basic/time-util.c). + // + // - Leading/trailing whitespace is skipped. `infinity` stands alone (only whitespace may follow). + // - Each element is a number read by strtoll(…, 10) — so an optional `+`, never `-` — with an + // optional `.digits` fraction (a bare `.5` is fine, `5.` is not), then optional whitespace and an + // optional unit from extract_multiplier's table. With no unit the default (seconds) applies. + // - Elements may follow each other directly after a unit (`1h30min`, `3.1s.2`) but a number without + // a unit must be followed by whitespace or the end (`12.34.56`, `5+5` are refused). + // - A unit is matched by prefix in table order, so `5mins` reads `min` and then chokes on `s`. + // + // Overflow (ERANGE past UINT64_MAX µs) is not modelled. + + private val TS_OPTIONAL_WS = ZeroOrOne(WhitespaceTerminal()) + + /** One number of a time span, as strtoll(…, 10) plus parse_time's own `.fraction` handling reads it. */ + val TIMESPAN_NUMBER = RegexTerminal("""\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""", """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""") + + /** extract_multiplier's suffix table (case-sensitive: `m` is minutes, `M` is months). */ + val TIMESPAN_UNIT = FlexibleLiteralChoiceTerminal( + "seconds", "second", "sec", "s", + "minutes", "minute", "min", + "months", "month", "M", + "msec", "ms", "m", + "hours", "hour", "hr", "h", + "days", "day", "d", + "weeks", "week", "w", + "years", "year", "y", + "usec", "us", "μs", "µs", + ) + + private val TIMESPAN_ELEMENT = AlternativeCombinator( + SequenceCombinator(TIMESPAN_NUMBER, TS_OPTIONAL_WS, TIMESPAN_UNIT, TS_OPTIONAL_WS), + SequenceCombinator(TIMESPAN_NUMBER, AlternativeCombinator(WhitespaceTerminal(), EOF())), + ) + + /** A complete parse_sec() time span, `infinity` included, ending at [EOF]. */ + val TIMESPAN = AlternativeCombinator( + SequenceCombinator(TS_OPTIONAL_WS, LiteralChoiceTerminal("infinity"), TS_OPTIONAL_WS, EOF()), + SequenceCombinator(TS_OPTIONAL_WS, OneOrMore(TIMESPAN_ELEMENT), EOF()), + ) + + + // --------------------------------------------------------------------------------------------------- + // Calendar events — calendar_spec_from_string (src/shared/calendarspec.c). + // + // [WEEKDAYS] [DATE] [TIME] [TIMEZONE] or KEYWORD [TIMEZONE] + // + // What is checked: the shape the C parser walks (weekday list with `..`/`-` ranges and an optional + // trailing comma, a two- or three-part date with `-` or `~`, a two- or three-part time, `@epoch`), and + // the per-field ranges calendar_spec_valid() enforces after fix_year() (years 0..99 or 1970..2199, + // months 1..12, days 1..31 or 1..28 counted from the end of the month, hours 0..23, minutes and + // seconds 0..59), with repetitions bounded by `to - from` and never zero. + // + // What is deliberately left lenient: cross-field checks (reversed ranges like `12..1`, a repetition + // that overshoots the field like `20/4` hours, descending weekday ranges like `Fri..Mon`), rounding + // of second fractions, and whether a timezone actually exists on the machine (see + // [CalendarTimezoneTerminal] for what is accepted in its place). + + private val CAL_SPACES = RegexTerminal(" +", " +") + private val CAL_STAR = LiteralChoiceTerminal("*") + private val CAL_COMMA = LiteralChoiceTerminal(",") + private val CAL_RANGE = LiteralChoiceTerminal("..") + private val CAL_REPEAT = LiteralChoiceTerminal("/") + private val CAL_TILDE = LiteralChoiceTerminal("~") + /** + * A plain decimal number in `[minInclusive, maxExclusive)`, as calendarspec's parse_component_decimal + * reads one: it insists on a leading digit (so no sign, unlike [IntegerTerminal], which accepts `-0`) + * and then calls strtoul(…, 10). + */ + class DecimalNumberTerminal(private val minInclusive: Long, private val maxExclusive: Long) : TerminalCombinator { + + private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value + + private fun inRange(text: String): Boolean = + text.toBigInteger().let { it >= BigInteger.valueOf(minInclusive) && it < BigInteger.valueOf(maxExclusive) } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + if (!inRange(text)) return NoMatch.copy(longestMatch = offset) + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun parse(value: String, offset: Int): Sequence { + val text = span(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, inRange(text))))) + } + + override fun toString(): String = "Decimal($minInclusive,$maxExclusive)" + + private companion object { + val SHAPE = Regex("[0-9]+") + } + } + + private val CAL_FRACTION = SequenceCombinator(LiteralChoiceTerminal("."), RegexTerminal("[0-9]+", "[0-9]+")) + + /** One chain element: `value[..value][/repeat]` — prepend_component. */ + private fun calComponent(value: Combinator, repeat: Combinator): Combinator = SequenceCombinator( + value, + ZeroOrOne(SequenceCombinator(CAL_RANGE, value)), + ZeroOrOne(SequenceCombinator(CAL_REPEAT, repeat)), + ) + + /** `*`, or a comma-separated list of components — parse_chain. `*` never combines with a list. */ + private fun calChain(value: Combinator, repeat: Combinator): Combinator { + val component = calComponent(value, repeat) + return AlternativeCombinator(CAL_STAR, SequenceCombinator(component, ZeroOrMore(SequenceCombinator(CAL_COMMA, component)))) + } + + // fix_year() maps 0..69 to 2000..2069 and 70..99 to 1970..1999 before the 1970..2199 range check. + private val CAL_YEAR = AlternativeCombinator(DecimalNumberTerminal(1970, 2200), DecimalNumberTerminal(0, 100)) + private val CAL_YEAR_CHAIN = calChain(CAL_YEAR, DecimalNumberTerminal(1, 230)) + private val CAL_MONTH_CHAIN = calChain(DecimalNumberTerminal(1, 13), DecimalNumberTerminal(1, 12)) + private val CAL_DAY_CHAIN = calChain(DecimalNumberTerminal(1, 32), DecimalNumberTerminal(1, 31)) + // With `~` the day counts back from the end of the month and chain_valid() lowers the ceiling to 28. + private val CAL_END_OF_MONTH_DAY_CHAIN = calChain(DecimalNumberTerminal(1, 29), DecimalNumberTerminal(1, 28)) + private val CAL_HOUR_CHAIN = calChain(DecimalNumberTerminal(0, 24), DecimalNumberTerminal(1, 24)) + private val CAL_MINUTE_CHAIN = calChain(DecimalNumberTerminal(0, 60), DecimalNumberTerminal(1, 60)) + // Seconds are parsed with usec = true: a `.fraction` is allowed on values and repetitions alike. + private val CAL_SECOND_VALUE = SequenceCombinator(DecimalNumberTerminal(0, 60), ZeroOrOne(CAL_FRACTION)) + private val CAL_SECOND_REPEAT = AlternativeCombinator( + SequenceCombinator(DecimalNumberTerminal(0, 60), CAL_FRACTION), + DecimalNumberTerminal(1, 60), + ) + private val CAL_SECOND_CHAIN = calChain(CAL_SECOND_VALUE, CAL_SECOND_REPEAT) + + private val CAL_DAY_PART = AlternativeCombinator( + SequenceCombinator(HYPHEN, CAL_DAY_CHAIN), + SequenceCombinator(CAL_TILDE, CAL_END_OF_MONTH_DAY_CHAIN), + ) + + /** `[YEAR-]MONTH-DAY` or `[YEAR-]MONTH~DAY` — parse_date. A lone component is not a date. */ + private val CAL_DATE = AlternativeCombinator( + SequenceCombinator(CAL_YEAR_CHAIN, HYPHEN, CAL_MONTH_CHAIN, CAL_DAY_PART), + SequenceCombinator(CAL_MONTH_CHAIN, CAL_DAY_PART), + ) + + /** `HOUR:MINUTE[:SECOND]` — parse_calendar_time. */ + private val CAL_TIME = SequenceCombinator( + CAL_HOUR_CHAIN, COLON, CAL_MINUTE_CHAIN, + ZeroOrOne(SequenceCombinator(COLON, CAL_SECOND_CHAIN)), + ) + + // `@SECONDS` since the epoch; the resulting year must still pass the 1970..2199 check, i.e. the value + // must be below 2200-01-01T00:00:00Z. parse_one_number() is a bare strtoul(…, 10), so leading + // whitespace and a `+` are fine (`@ +5`). A `-` is wrapped modulo 2^64 by strtoul and lands before + // 1970, so is refused — except `@-0`, which is 0; that one spelling is not modelled. + private val CAL_EPOCH = SequenceCombinator( + LiteralChoiceTerminal("@"), + ZeroOrOne(WhitespaceTerminal()), + ZeroOrOne(LiteralChoiceTerminal("+")), + DecimalNumberTerminal(0L, 7_258_118_400L), + ) + + /** parse_weekdays' table; startswith_no_case, so any casing. */ + val CALENDAR_WEEKDAY = FlexibleLiteralChoiceTerminal( + "Monday", "Mon", "Tuesday", "Tue", "Wednesday", "Wed", "Thursday", "Thu", + "Friday", "Fri", "Saturday", "Sat", "Sunday", "Sun", + ignoreCase = true, + ) + + private val CAL_WEEKDAY_ITEM = SequenceCombinator( + CALENDAR_WEEKDAY, + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal("..", "-"), CALENDAR_WEEKDAY)), + ) + + // A trailing comma is allowed ("Wednesday,", "Wed, 17:48"), an open range is not ("Monday.."). + private val CAL_WEEKDAYS = SequenceCombinator( + CAL_WEEKDAY_ITEM, + ZeroOrMore(SequenceCombinator(CAL_COMMA, CAL_WEEKDAY_ITEM)), + ZeroOrOne(CAL_COMMA), + ) + + /** The shorthand keywords, matched with strcaseeq. `anually` is a kept-for-compatibility misspelling. */ + val CALENDAR_KEYWORD = FlexibleLiteralChoiceTerminal( + "minutely", "hourly", "daily", "monthly", "weekly", "yearly", "annually", "anually", "quarterly", + "biannually", "bi-annually", "semiannually", "semi-annually", + ignoreCase = true, + ) + + /** + * The timezone that may end a calendar event, as calendar_spec_from_string_full() recognises one. + * + * In C the suffix is tried three ways, each after exactly one space: ` UTC` in any case + * (endswith_no_case); the host's own zone abbreviations tzname[0]/tzname[1] in any case (`CET`, + * `CEST`, or numeric ones like `-03` for zones without a letter abbreviation); and otherwise the last + * space-separated word, if timezone_is_valid() finds a TZif file of that name under + * /usr/share/zoneinfo. Anything else is left in the expression, which then fails to parse. + * + * Which zones exist depends on the machine, so this accepts: `UTC` in any case; any zone ID the JDK + * knows (also under the `posix/` and `right/` trees); any `Area/Location` shape whose area is one of + * the tzdata regions (newer zones than the JDK's); a 2..6 letter word that is not a weekday or + * calendar keyword (a plausible host abbreviation); and `+NN`/`-NN`/`+NNNN`. What it refuses is the + * common mistake: a weekday or keyword written where systemd expects the date/time to have ended + * (`*-*-* 12:00 Mon`, `Mon Fri`), or a long word that is no zone (`everyday`). + */ + class CalendarTimezoneTerminal : TerminalCombinator { + + private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value + + 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 (!isPlausibleZone(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, isPlausibleZone(text))))) + } + + override fun toString(): String = "CalendarTimezone" + + companion object { + // verify_timezone's character set; the leading character excludes digits so a time is never read as a zone. + private val SHAPE = Regex("""[A-Za-z+\-][A-Za-z0-9_+\-]*(?:/[A-Za-z0-9_+\-]+)*""") + + private val KNOWN_ZONE_IDS: Set by lazy { + // "Factory" is a tzdata zone file the JDK does not ship (checked against a host's /usr/share/zoneinfo). + java.util.TimeZone.getAvailableIDs().toSet() + java.time.ZoneId.getAvailableZoneIds() + "Factory" + } + + private val TZDATA_AREAS = setOf( + "Africa", "America", "Antarctica", "Arctic", "Asia", "Atlantic", "Australia", "Brazil", "Canada", + "Chile", "Etc", "Europe", "Indian", "Mexico", "Pacific", "US", + ) + + private val ABBREVIATION = Regex("[A-Za-z]{2,6}") + private val NUMERIC_ABBREVIATION = Regex("[+-][0-9]{2}(?:[0-9]{2})?") + + private val NOT_A_ZONE: Set = ( + listOf("monday", "mon", "tuesday", "tue", "wednesday", "wed", "thursday", "thu", "friday", "fri", + "saturday", "sat", "sunday", "sun") + + listOf("minutely", "hourly", "daily", "monthly", "weekly", "yearly", "annually", "anually", + "quarterly", "biannually", "semiannually") + ).toSet() + + fun isPlausibleZone(text: String): Boolean { + if (text.equals("UTC", ignoreCase = true)) return true + val bare = text.removePrefix("posix/").removePrefix("right/") + if (bare in KNOWN_ZONE_IDS) return true + if (bare.contains('/') && bare.substringBefore('/') in TZDATA_AREAS && !bare.endsWith("/")) return true + if (ABBREVIATION.matches(text)) return text.lowercase() !in NOT_A_ZONE + return NUMERIC_ABBREVIATION.matches(text) + } + } + } + + private val CAL_TIMEZONE = CalendarTimezoneTerminal() + + // The zone is split off at exactly one space (endswith " UTC", or the last space for a zone name), so + // any extra spaces stay in the expression. parse_weekdays() and parse_date() skip a run of trailing + // spaces, so after a weekday list or a bare date they are harmless; after a keyword, a time or an + // `@epoch` they are left over and the value is refused. + private val CAL_ONE_SPACE = LiteralChoiceTerminal(" ") + private val CAL_TIMEZONE_AFTER_ONE_SPACE = ZeroOrOne(SequenceCombinator(CAL_ONE_SPACE, CAL_TIMEZONE)) + private val CAL_TIMEZONE_AFTER_SPACES = ZeroOrOne(SequenceCombinator(CAL_SPACES, CAL_TIMEZONE)) + + // `[WEEKDAYS ]` before each body is spelled out as two branches rather than a ZeroOrOne prefix: the + // classic engine's ZeroOrOne re-applies its operand after a match to compute longestMatch, so on + // `Mon Fri` it would read `Fri` as a second weekday prefix, report the whole value as matched and send + // GrammarOptionValue's error localisation past its last token. + private fun withOptionalWeekdays(vararg body: Combinator): List = listOf( + SequenceCombinator(CAL_WEEKDAYS, CAL_SPACES, *body), + SequenceCombinator(*body), + ) + + /** A complete calendar event expression, ending at [EOF]. Every branch is EOF-terminated. */ + val CALENDAR_SPEC = AlternativeCombinator( + *( + listOf(SequenceCombinator(CALENDAR_KEYWORD, CAL_TIMEZONE_AFTER_ONE_SPACE, EOF())) + + withOptionalWeekdays(CAL_EPOCH, CAL_TIMEZONE_AFTER_ONE_SPACE, EOF()) + + withOptionalWeekdays(CAL_DATE, CAL_SPACES, CAL_TIME, CAL_TIMEZONE_AFTER_ONE_SPACE, EOF()) + + withOptionalWeekdays(CAL_DATE, CAL_TIMEZONE_AFTER_SPACES, EOF()) + + withOptionalWeekdays(CAL_TIME, CAL_TIMEZONE_AFTER_ONE_SPACE, EOF()) + + SequenceCombinator(CAL_WEEKDAYS, CAL_TIMEZONE_AFTER_SPACES, EOF()) + ).toTypedArray() + ) + + + // --------------------------------------------------------------------------------------------------- + // Byte sizes — parse_size(…, 1024) (src/basic/parse-util.c). + // + // A run of `number[.fraction] [suffix]` elements, whitespace allowed before each element and between a + // number and its suffix. The number is read by strtoull(…, 10), so a leading `+` is fine and a `-` is + // ERANGE; the fraction may be empty (`10.M` is accepted). Suffixes are B, K, M, G, T, P, E (upper case + // only) or none, and an element without a suffix can only come last. systemd also insists suffixes + // appear in strictly descending order (`1G 512M`, not `512M 1G`) and that the sum fits — neither is + // modelled. + + private val SIZE_NUMBER = RegexTerminal("""\+?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""") + private val SIZE_SUFFIX = LiteralChoiceTerminal("B", "K", "M", "G", "T", "P", "E") + + /** A complete parse_size(…, 1024) value, ending at [EOF]. */ + val IEC_SIZE = SequenceCombinator( + TS_OPTIONAL_WS, + ZeroOrMore(SequenceCombinator(SIZE_NUMBER, TS_OPTIONAL_WS, SIZE_SUFFIX, TS_OPTIONAL_WS)), + ZeroOrOne(SequenceCombinator(SIZE_NUMBER, TS_OPTIONAL_WS)), + EOF(), + ) + + + // --------------------------------------------------------------------------------------------------- + // Paths as path_simplify_and_warn() (src/shared/parse-helpers.c) checks them after specifier expansion: + // PATH_CHECK_ABSOLUTE, then path_simplify() (which drops `.` components and duplicate slashes), then + // path_is_normalized() (which refuses any `..` component), and with PATH_CHECK_NON_API_VFS also + // path_below_api_vfs() (refuses /dev, /sys, /proc and anything below them). + + // Rejects a `..` component anywhere: the optional `(?:[\s\S]*/)` swallows any leading directories. + const val NO_DOT_DOT_COMPONENT = """(?!(?:[\s\S]*/)?\.\.(?:/|$))""" + + // path_startswith() is component-aware: /dev and /dev/x are API VFS, /devices is not. + const val NOT_API_VFS = """(?!/+(?:dev|sys|proc)(?:/|$))""" + + private const val SPECIFIER_OR_SLASH = """(?:/|%[$UNIT_SPECIFIER_LETTERS])""" + + /** An absolute, normalized path after unit_path_printf(); may begin with a specifier such as `%t`. */ + val UNIT_ABSOLUTE_PATH = RegexTerminal("""[\s\S]+""", """$NO_DOT_DOT_COMPONENT$SPECIFIER_OR_SLASH$UNIT_TEXT_CHAR*$""") + + /** As [UNIT_ABSOLUTE_PATH], but also not at or below /dev, /sys or /proc (PATH_CHECK_NON_API_VFS). */ + val UNIT_ABSOLUTE_NON_API_VFS_PATH = RegexTerminal( + """[\s\S]+""", + """$NO_DOT_DOT_COMPONENT$NOT_API_VFS$SPECIFIER_OR_SLASH$UNIT_TEXT_CHAR*$""" + ) + + /** An absolute, normalized path taken literally (no specifier expansion) — config_parse_path. */ + val LITERAL_ABSOLUTE_PATH = RegexTerminal("""[\s\S]+""", """$NO_DOT_DOT_COMPONENT/[\s\S]*""") +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseActivationPolicyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseActivationPolicyOptionValueTest.kt new file mode 100644 index 00000000..f210dba0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseActivationPolicyOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseActivationPolicyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ActivationPolicy=up + ActivationPolicy=always-up + ActivationPolicy=manual + ActivationPolicy=always-down + ActivationPolicy=down + ActivationPolicy=bound + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ActivationPolicy=yes + ActivationPolicy=Up + ActivationPolicy=always + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt new file mode 100644 index 00000000..ed63fe98 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Address] Label= (ADDRESS_LABEL). */ +class ConfigParseAddressLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Address] + Label=eth0:1 + Label=with space + Label=123456789012345 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + Label=1234567890123456 + Label=café + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt new file mode 100644 index 00000000..b82bc4a3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Address] NetLabel= (ADDRESS_NET_LABEL -> config_parse_string(CONFIG_PARSE_STRING_SAFE)). */ +class ConfigParseAddressNetLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // CONFIG_PARSE_STRING_SAFE uses STRING_ALLOW_GLOBS and does not require ASCII, so globs and UTF-8 are fine + // (systemd oracle check 2026-10-24). + // language="unit file (systemd)" + val file = """ + [Address] + NetLabel=system_u:object_r:root_t:s0 + NetLabel=unconfined_u:object_r:* + NetLabel=foo*? + NetLabel=café + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // systemd logs these at warning level, but it drops the value and with it the whole [Address] section + // (config_section_parse returns 0, so address_unref_or_set_invalidp marks the section invalid). + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + NetLabel="system_u:object_r:root_t:s0" + NetLabel=back\\slash + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt new file mode 100644 index 00000000..e8af0df8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseAdvertiseOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Advertise=1000baset-full + Advertise=10baset-half 100baset-full autonegotiation pause asym-pause + Advertise=10000baser-fec fec-rs + Advertise=1600000basedr8-2-full + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Advertise=hoge foo + Advertise=1000BaseT-Full + Advertise=autoneg + Advertise=1000baset-full,100baset-full + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt new file mode 100644 index 00000000..8b5eef20 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt @@ -0,0 +1,67 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBadadvBandwidthOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [BatmanAdvanced] + GatewayBandwidthDown=205M + GatewayBandwidthUp=2G + GatewayBandwidthDown=1000 + GatewayBandwidthUp=1.5G + GatewayBandwidthDown=10.M + GatewayBandwidthUp=3. K + GatewayBandwidthDown=4 M 11.5K + GatewayBandwidthUp=3.5G3B + GatewayBandwidthDown=4T3G3B + GatewayBandwidthUp=+5K + GatewayBandwidthDown=429496729599999 + GatewayBandwidthUp=429T 496G 729M 599K 999 + GatewayBandwidthDown=429.496729599999T + GatewayBandwithDown=100K + GatewayBandwithUp=1T + GatewayBandwidthDown=111.4 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BatmanAdvanced] + GatewayBandwidthDown=5k + GatewayBandwidthUp=-1 + GatewayBandwidthDown=12X + GatewayBandwidthUp=3B3.5G + GatewayBandwidthDown=12P12P + GatewayBandwidthUp=3. 0 K + GatewayBandwidthDown=fast + GatewayBandwithUp=1 Mbit + GatewayBandwidthDown=12P + GatewayBandwidthUp=3E 2P + GatewayBandwidthDown=429496729600000 + GatewayBandwidthUp=429.4967296T + GatewayBandwidthDown=0.5P + GatewayBandwidthUp=20E + GatewayBandwidthDown=18446744073709551616 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(15, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBfifoSizeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBfifoSizeOptionValueTest.kt new file mode 100644 index 00000000..27920cc0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBfifoSizeOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBfifoSizeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [BFIFO] + LimitBytes=1000000 + LimitBytes=1K + LimitBytes=1M + LimitBytes=1.5M + LimitBytes=4095M + LimitBytes=4294967295 + LimitBytes=3G 1023M + LimitBytes= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BFIFO] + LimitBytes=4G + LimitBytes=4294967296 + LimitBytes=1k + LimitBytes=-1 + LimitBytes=abc + LimitBytes=1M 1M + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindOptionValueTest.kt new file mode 100644 index 00000000..0d0c09fa --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindOptionValueTest.kt @@ -0,0 +1,86 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBindOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN and systemd-nspawn(1): empty source (temporary dir), relative source, `+` prefix, + // escaped colons, and the bind options parse_mount_bind_options knows. + // language="unit file (systemd)" + val file = """ + [Files] + Bind=/var/lib/foo + BindReadOnly=/tmp/ephemeral-config + Bind=+/sysroot/ostree/deploy/os/var:/var + Bind=/tmp/dir:/foo + Bind=/tmp/dir:/also-foo:noidmap,norbind + BindReadOnly=/etc/ld.so.cache:/etc/ld.so.cache + Bind=/tmp/owneridmap/bind:/home/testuser:owneridmap + BindReadOnly=/tmp/rootidmap/bind:/mnt:rootidmap + Bind=:/scratch + Bind=relative/dir:/mnt + Bind=/path\:with\:colons:/mnt + Bind=/a:/b: + Bind=/a:/b:rbind,,idmap, + Bind=/my dir:/mnt/my dir + Bind=+/foo + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // A lone path must be absolute; empty or relative destination; `+` must be followed by an absolute path. + // These are rejected by bind_mount_parse() itself (sd-check: "Invalid bind mount specification"). + // language="unit file (systemd)" + val file = """ + [Files] + Bind=relative + Bind=/a: + Bind=/a:relative + Bind=+relative:/b + Bind=+:/b + Bind=/a:relative:rbind + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + + @Test + fun testOptionsRejectedOnlyAtContainerStart() { + // Accepted: bind_mount_parse() stores the option string verbatim, so the settings parser (sd-check / + // fuzz-nspawn-settings at 0f0682b773) accepts all of these. parse_mount_bind_options() fails the container + // start with "Invalid bind mount option" (`ro` is not an option, read-only is BindReadOnly=; `rbind:x` is one + // word), but that is not a parse error. Issue #565 tracks a separate warning for them. + // language="unit file (systemd)" + val file = """ + [Files] + Bind=/a:/b:bogus + Bind=/a:/b:rbind:x + BindReadOnly=/a:/b:ro + Bind=/a:/b:rbind,bogus + Bind=/a:/b:: + Bind=/a:/b:has space + Bind=+/a:/b:x:y:z + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindPathsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindPathsOptionValueTest.kt new file mode 100644 index 00000000..61aaed36 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindPathsOptionValueTest.kt @@ -0,0 +1,96 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBindPathsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidServiceValues() { + // Values from systemd's test-load-fragment.c, test/test-execute/exec-bindpaths.service and the corpus. + // language="unit file (systemd)" + val file = """ + [Service] + BindPaths=/tmp:/tmp/test-exec-bindpaths + BindPaths="/tmp/test-exec-bindpaths source:/tmp/test-exec-bindpaths destination" /tmp/test-exec-bindpaths\:source:/tmp/test-exec-bindpaths\:destination + BindReadOnlyPaths= /usr/bin /usr/lib /lib64 + BindReadOnlyPaths=-/usr/lib -/usr/lib64 /usr/bin /usr/sbin + BindPaths=/etc /home:/mnt:norbind -/foo/bar/baz:/usr:rbind + BindPaths=%f:/tmp/scrub:norbind + BindPaths=%t/foo:%h/bar + BindPaths=/a:/b: + BindPaths=/tmp/markers/ + BindPaths=/a/./b//c:/d + BindPaths=/a\x20b:/c\sd + BindReadOnlyPaths=/dev/log:/var/lib/haproxy/dev/log + BindReadOnlyPaths='/srv/a b':/srv/c + BindPaths= + BindPaths=/a\"b":/c + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidInOtherUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BindPaths=/run/foo:/run/bar + BindReadOnlyPaths=-/etc/resolv.conf + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BindPaths=/run/foo:/run/bar:rbind + BindReadOnlyPaths=/etc/machine-id + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BindPaths=/run/foo + BindReadOnlyPaths=/run/dbus/system_bus_socket + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BindPaths=relative/path + BindPaths=/a:relative + BindPaths=/a: + BindPaths=/a::rbind + BindPaths=/a:/b:ro + BindPaths=/a:/b:rbind:extra + BindPaths=/a/../b + BindPaths=/a:-/b + BindPaths=/a\qb + BindReadOnlyPaths=/usr/bin foo + BindReadOnlyPaths=/usr/bin /a:/b:recursive + BindPaths=/a\" + BindPaths=/a\ud800 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(13, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindUserOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindUserOptionValueTest.kt new file mode 100644 index 00000000..d5ce3b14 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindUserOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBindUserOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // valid_user_group_name in strict mode: up to 31 characters. + // language="unit file (systemd)" + val file = """ + [Files] + BindUser= + BindUser=testuser + BindUser=root + BindUser=alice bob _svc + BindUser=user-name_1 + BindUser=a234567890123456789012345678901 + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Leading digit or dash, a dot, 32 characters, and a numeric UID are all refused by the strict check. + // language="unit file (systemd)" + val file = """ + [Files] + BindUser=1user + BindUser=john.doe + BindUser=a2345678901234567890123456789012 + BindUser=-dash + BindUser=1000 + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt new file mode 100644 index 00000000..f149d101 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateAttachmentsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateAttachments=BPFFlowDissector,BPFCgroupSysctl,BPFNetfilter + BPFDelegateAttachments=BPFCgroupInetIngress + BPFDelegateAttachments=any + BPFDelegateAttachments=BPFCgroupInetIngress,BPFCgroupInetEgress,BPFCgroupInetSockCreate + BPFDelegateAttachments=BPFTraceFsessionMulti + BPFDelegateAttachments=BPFLircMode2 + BPFDelegateAttachments=BPFCgroupInet4Bind,BPFCgroupInet6Bind + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateAttachments=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateAttachments=BPFCgroupInetIngress + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateAttachments=BPFTraceFsessionMulti,BPFCgroupInetIngress + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateAttachments=BPFCgroupInetSkbIngress + BPFDelegateAttachments=BPFFlowDissector, BPFNetfilter + BPFDelegateAttachments=BPF_NETFILTER + BPFDelegateAttachments=ingress + BPFDelegateAttachments=BPFTcxIngress BPFTcxEgress + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt new file mode 100644 index 00000000..be3529fa --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateCommandsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateCommands=BPFObjPin,BPFBtfLoad,BPFMapFreeze,BPFLinkDetach + BPFDelegateCommands=BPFProgLoad + BPFDelegateCommands=any + BPFDelegateCommands=BPFMapCreate,BPFMapLookupElem,BPFMapUpdateElem + BPFDelegateCommands=BPFMapCreate + BPFDelegateCommands=BPFProgAssocStructOps + BPFDelegateCommands=BPFProgLoad,BPFMapCreate,,BPFLinkCreate, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateCommands=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateCommands=BPFMapCreate + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateCommands=BPFProgAssocStructOps,BPFMapCreate + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateCommands=BPF_PROG_LOAD + BPFDelegateCommands=BPFProgload + BPFDelegateCommands=BPFProgLoad, BPFMapCreate + BPFDelegateCommands=BPFProgLoad BPFMapCreate + BPFDelegateCommands=any,BPFProgLoad + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt new file mode 100644 index 00000000..43b02250 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateMapsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateMaps=BPFMapTypeArray,BPFMapTypeCpumap,BPFMapTypeRingbuf + BPFDelegateMaps=BPFMapTypeHash + BPFDelegateMaps=any + BPFDelegateMaps=BPFMapTypeUnspec,BPFMapTypeHash,BPFMapTypeArray + BPFDelegateMaps=BPFMapTypeUnspec + BPFDelegateMaps=BPFMapTypeCgroupStorageDeprecated + BPFDelegateMaps=BPFMapTypeRhash + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateMaps=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateMaps=BPFMapTypeUnspec + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateMaps=BPFMapTypeRhash,BPFMapTypeUnspec + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateMaps=BPFMapTypeCgroupStorage + BPFDelegateMaps=BPFMapTypeArray, BPFMapTypeHash + BPFDelegateMaps=BPF_MAP_TYPE_ARRAY + BPFDelegateMaps=Array + BPFDelegateMaps=Any + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt new file mode 100644 index 00000000..60ef19dd --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateProgramsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegatePrograms=BPFProgTypeTracepoint,BPFProgTypeXdp,BPFProgTypeTracing + BPFDelegatePrograms=BPFProgTypeKprobe + BPFDelegatePrograms=any + BPFDelegatePrograms=BPFProgTypeUnspec,BPFProgTypeSocketFilter,BPFProgTypeKprobe + BPFDelegatePrograms=BPFProgTypeUnspec + BPFDelegatePrograms=BPFProgTypeLwtSeg6local + BPFDelegatePrograms=BPFProgTypeNetfilter + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegatePrograms=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegatePrograms=BPFProgTypeUnspec + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegatePrograms=BPFProgTypeNetfilter,BPFProgTypeUnspec + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegatePrograms=BPFProgTypeXDP + BPFDelegatePrograms=BPFProgTypeXdp BPFProgTypeTracing + BPFDelegatePrograms=BPFProgTypeLwtSeg6Local + BPFDelegatePrograms=tracepoint + BPFDelegatePrograms=BPFProgTypeSyscall + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt new file mode 100644 index 00000000..fee4e7cf --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt @@ -0,0 +1,102 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfForeignProgramOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first two are the systemd.resource-control(5) examples. + // language="unit file (systemd)" + val file = """ + [Service] + BPFProgram=egress:/sys/fs/bpf/egress-hook + BPFProgram=bind6:/sys/fs/bpf/sock-addr-hook + BPFProgram=ingress:/sys/fs/bpf/ingress + BPFProgram=sock_create:/sys/fs/bpf/a + BPFProgram=sock_ops:/sys/fs/bpf/a + BPFProgram=device:/sys/fs/bpf/a + BPFProgram=bind4:/sys/fs/bpf/a + BPFProgram=connect4:/sys/fs/bpf/a + BPFProgram=connect6:/sys/fs/bpf/a + BPFProgram=post_bind4:/sys/fs/bpf/a + BPFProgram=post_bind6:/sys/fs/bpf/a + BPFProgram=sendmsg4:/sys/fs/bpf/a + BPFProgram=sendmsg6:/sys/fs/bpf/a + BPFProgram=sysctl:/sys/fs/bpf/a + BPFProgram=recvmsg4:/sys/fs/bpf/a + BPFProgram=recvmsg6:/sys/fs/bpf/a + BPFProgram=getsockopt:/sys/fs/bpf/a + BPFProgram=setsockopt:/sys/fs/bpf/a + BPFProgram=egress:/sys/fs/bpf/%N-egress + BPFProgram=egress:%t/bpf/prog + BPFProgram=egress::/sys/fs/bpf/double-colon + BPFProgram=egress:/sys/fs/bpf/with:colon + BPFProgram=:egress:/sys/fs/bpf/leading-colon + BPFProgram=::ingress::/sys/fs/bpf/x + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFProgram=egress:/sys/fs/bpf/egress-hook + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFProgram=ingress:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFProgram=device:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val slice = """ + [Slice] + BPFProgram=sock_create:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.slice", slice) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFProgram=/usr/lib/systemd/bpf/custom.bpf + BPFProgram=egress + BPFProgram=egress: + BPFProgram=xdp:/sys/fs/bpf/x + BPFProgram=Egress:/sys/fs/bpf/x + BPFProgram=egress:sys/fs/bpf/x + BPFProgram=egress:/sys/fs/bpf/../x + BPFProgram=cgroup_inet_egress:/sys/fs/bpf/x + BPFProgram=:/sys/fs/bpf/x + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBridgeVlanIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBridgeVlanIdOptionValueTest.kt new file mode 100644 index 00000000..491e3fee --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBridgeVlanIdOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBridgeVlanIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [BridgeVLAN] + PVID=1020 + PVID=0 + PVID=4094 + PVID=0x10 + PVID=no + PVID=False + PVID=oFf + PVID=FALSE + PVID= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BridgeVLAN] + PVID=4095 + PVID=yes + PVID=-1 + PVID=10-20 + PVID=TRUE + PVID=Yes + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBusNameOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBusNameOptionValueTest.kt new file mode 100644 index 00000000..e1f0fd1c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBusNameOptionValueTest.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBusNameOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Well-known names, a unique name (accepted by sd_bus_service_name_is_valid), and a specifier. + // language="unit file (systemd)" + val file = """ + [Service] + BusName=org.freedesktop.systemd1 + BusName=org.freedesktop.NetworkManager + BusName=com.example.my-service_2 + BusName=a.b + BusName=_foo.-bar + BusName=:1.42 + BusName=org.example.%i + BusName=%N.service + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BusName=org + BusName=org. + BusName=.org.freedesktop + BusName=org..freedesktop + BusName=org.1freedesktop + BusName=org.free desktop + BusName=org.freedesktop/foo + BusName=org.example.%e + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeAckFilterOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeAckFilterOptionValueTest.kt new file mode 100644 index 00000000..78625491 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeAckFilterOptionValueTest.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCakeAckFilterOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [CAKE] + AckFilter=aggressive + AckFilter=yes + AckFilter=no + AckFilter=true + AckFilter=false + AckFilter=1 + AckFilter=0 + AckFilter=on + AckFilter=off + AckFilter=YES + AckFilter=No + AckFilter=Yes + AckFilter=TRUE + AckFilter=oFf + AckFilter=tRUE + AckFilter=N + AckFilter= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [CAKE] + AckFilter=Aggressive + AckFilter=maybe + AckFilter=aggresive + AckFilter=2 + AckFilter=filter + AckFilter=AGGRESSIVE + AckFilter=aggressivE + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeBandwidthOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeBandwidthOptionValueTest.kt new file mode 100644 index 00000000..2dcedf88 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeBandwidthOptionValueTest.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCakeBandwidthOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [CAKE] + Bandwidth=500M + Bandwidth=1G + Bandwidth=100K + Bandwidth=1.5M + Bandwidth=1G 500M + Bandwidth=100000 + Bandwidth= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [CAKE] + Bandwidth=10Mbit + Bandwidth=1k + Bandwidth=-1M + Bandwidth=3B3.5G + Bandwidth=12P12P + Bandwidth=1M 1M + Bandwidth=abc + Bandwidth=.5M + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeFlowIsolationModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeFlowIsolationModeOptionValueTest.kt new file mode 100644 index 00000000..a97157ad --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeFlowIsolationModeOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCakeFlowIsolationModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [CAKE] + FlowIsolationMode=none + FlowIsolationMode=src-host + FlowIsolationMode=dst-host + FlowIsolationMode=hosts + FlowIsolationMode=flows + FlowIsolationMode=dual-src-host + FlowIsolationMode=dual-dst-host + FlowIsolationMode=triple + FlowIsolationMode= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [CAKE] + FlowIsolationMode=None + FlowIsolationMode=dual + FlowIsolationMode=triple-isolate + FlowIsolationMode=src_host + FlowIsolationMode=yes + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt new file mode 100644 index 00000000..6fe8125e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_can_restart_usec. */ +class ConfigParseCanRestartUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [CAN] + RestartSec=0 + RestartSec=100ms + RestartSec=1.5s + RestartSec=infinity + RestartSec=49d + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [CAN] + RestartSec=50d + RestartSec=never + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCapabilityOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCapabilityOptionValueTest.kt new file mode 100644 index 00000000..d655f23b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCapabilityOptionValueTest.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCapabilityOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Values from systemd's TEST-13-NSPAWN plus the C edges: `all`, any-case names (gperf --ignore-case), + // and numeric capabilities read by safe_atoi (base 0: hex, octal, 0b/0o, sign) up to CAP_LIMIT (62). + // language="unit file (systemd)" + val file = """ + [Exec] + Capability=CAP_BLOCK_SUSPEND CAP_BPF CAP_CHOWN + DropCapability=CAP_AUDIT_CONTROL CAP_AUDIT_WRITE + AmbientCapability=CAP_BPF CAP_CHOWN + Capability=all + DropCapability=all + Capability=cap_net_admin Cap_Sys_Admin + Capability=12 0x15 062 0 +3 0b101 0o7 + DropCapability=CAP_NET_ADMIN CAP_SYS_TIME + Capability=62 + AmbientCapability=CAP_CHECKPOINT_RESTORE CAP_WAKE_ALARM + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Unknown name; comma is not a separator; above CAP_LIMIT; `all` is case-sensitive; negative; one bad word in a list; + // `0b 1` is split into the words `0b` (invalid) and `1` before safe_atoi() sees it. + // language="unit file (systemd)" + val file = """ + [Exec] + Capability=CAP_BOGUS + DropCapability=CAP_NET_ADMIN,CAP_SYS_ADMIN + AmbientCapability=63 + Capability=ALL + Capability=-1 + DropCapability=CAP_NET_ADMIN CAP_NOPE + Capability=0b 1 + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt new file mode 100644 index 00000000..6eaacd02 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt @@ -0,0 +1,90 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCgroupNftSetOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first two are from systemd's test/units/TEST-07-PID1.nft.sh and the man page example. + // language="unit file (systemd)" + val file = """ + [Service] + NFTSet=cgroup:inet:sd_test:c user:inet:sd_test:u group:inet:sd_test:g + NFTSet=cgroup:inet:filter:my_service user:inet:filter:serviceuser + NFTSet=cgroup:ip:t:s + NFTSet=user:ip6:my.table:set/v6 + NFTSet=group:arp:a:b group:bridge:a:b group:netdev:a:b + NFTSet="cgroup:inet:filter:quoted" + NFTSet=cgroup::inet:t:s + NFTSet=:cgroup:inet:t:s + NFTSet=cgroup:inet:t:s: + NFTSet=::cgroup:inet:t:s:: user:inet:t:u: + NFTSet="cgroup:inet:t:s:" + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + NFTSet=cgroup:inet:sd_test:c + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + NFTSet=user:inet:sd_test:u + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + NFTSet=group:inet:sd_test:g + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val slice = """ + [Slice] + NFTSet=cgroup:inet:sd_test:c + """.trimIndent() + setupFileInEditor("file.slice", slice) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + NFTSet=address:inet:sd_test:addr4 + NFTSet=cgroup:ipv4:t:s + NFTSet=cgroup:inet:1table:s + NFTSet=cgroup:inet:t + NFTSet=cgroup:inet:t:s:extra + NFTSet=inet:filter:my_service + NFTSet=cgroup:inet:t:s,user:inet:t:u + NFTSet=cgroup:inet:t-x:s + NFTSet=CGROUP:inet:t:s + NFTSet=cgroup:inet:t: + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt new file mode 100644 index 00000000..e22f59a9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCoalesceNsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RxCoalesceCQENSec=500 + RxCoalesceCQENSec=1us + RxCoalesceCQENSec=2.5 + RxCoalesceCQENSec=1s500ms + RxCoalesceCQENSec=.7 + RxCoalesceCQENSec=3.1 sec .2 sec + RxCoalesceCQENSec=+3.1s + RxCoalesceCQENSec=3.1s.2 + RxCoalesceCQENSec=100 ns + RxCoalesceCQENSec=1µs + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RxCoalesceCQENSec=infinity + RxCoalesceCQENSec=-5 + RxCoalesceCQENSec=5. + RxCoalesceCQENSec=3.s + RxCoalesceCQENSec=xyz + RxCoalesceCQENSec=12..34 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseConcurrencyMaxOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseConcurrencyMaxOptionValueTest.kt new file mode 100644 index 00000000..f2605b38 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseConcurrencyMaxOptionValueTest.kt @@ -0,0 +1,72 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseConcurrencyMaxOptionValueTest : AbstractUnitFileTest() { + + // Every value below was checked with sd-check (systemd 0f0682b773). The 0b/0o forms with a sign or a space after + // the prefix are accepted because strtoul() starts afresh after mangle_base(); "0b -1" and "0b-1" are out of + // range (-ERANGE), "+0b1", "-0b0", "+0o7", "08" and "0x" are -EINVAL. (systemd also accepts a leading \v, but the + // unit-file lexer does not let one into a value, so SafeAtoTerminalTest covers that.) + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Slice] + ConcurrencySoftMax=infinity + ConcurrencyHardMax=10 + ActivatingConcurrencyMax=0 + ConcurrencySoftMax=4294967295 + ConcurrencyHardMax=0x10 + ActivatingConcurrencyMax=010 + ConcurrencySoftMax=0b101 + ConcurrencyHardMax=0o17 + ActivatingConcurrencyMax=+5 + ConcurrencySoftMax= + ConcurrencyHardMax=-0 + ActivatingConcurrencyMax=-0x0 + ConcurrencySoftMax=+0x10 + ConcurrencyHardMax=0b+1 + ActivatingConcurrencyMax=0b-0 + ConcurrencySoftMax=0b 1 + ConcurrencyHardMax=0o 7 + ActivatingConcurrencyMax=0b +1 + ConcurrencySoftMax=0b -0 + """.trimIndent() + + setupFileInEditor("file.slice", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Slice] + ConcurrencySoftMax=4294967296 + ConcurrencyHardMax=-1 + ActivatingConcurrencyMax=Infinity + ConcurrencySoftMax=unlimited + ConcurrencyHardMax=08 + ActivatingConcurrencyMax=10 20 + ConcurrencySoftMax=1.5 + ConcurrencyHardMax=+0b1 + ActivatingConcurrencyMax=+0o7 + ConcurrencySoftMax=-0b0 + ConcurrencyHardMax=0b-1 + ActivatingConcurrencyMax=0b -1 + ConcurrencySoftMax=0x + """.trimIndent() + + setupFileInEditor("file.slice", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(13, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt new file mode 100644 index 00000000..06a3074c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCpuSetOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinity= + IRQAffinity=0 + IRQAffinity=1 2 4 + IRQAffinity=0-3,8-11 + IRQAffinity=0, 1, 2, 3, 4, 5, 6, 7, 63, + IRQAffinity=8 '9' 10 "11" + IRQAffinity=0-3 8-11, + IRQAffinity=8000-8191 + IRQAffinity=0x0-0x3 + IRQAffinity=,0 + IRQAffinity=0-"3" + IRQAffinity='0'-'3', 4 + IRQAffinity=0b+1 + IRQAffinity=0-0b+1 + IRQAffinity=0-0b-0 + IRQAffinity="0b 1" + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinity=0 1 2 3 garbage + IRQAffinity=0-3 8-garbage + IRQAffinity=8192 + IRQAffinity=1- + IRQAffinity=-1 + IRQAffinity=0 1 2 3 "4 5 6 7 + IRQAffinity=-0 + IRQAffinity=0-"9000" + IRQAffinity=0b-0 + IRQAffinity=0b 1 + IRQAffinity=0-0b 1 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt new file mode 100644 index 00000000..44b4c1b6 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDefaultPortVlanidOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Bridge] + DefaultPVID=9 + DefaultPVID=none + DefaultPVID=1 + DefaultPVID=4094 + DefaultPVID=0 + DefaultPVID=0xFFE + DefaultPVID=0b 1 + DefaultPVID=0b+1 + DefaultPVID=0o 7 + DefaultPVID=0b -0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Bridge] + DefaultPVID=4095 + DefaultPVID=None + DefaultPVID=-1 + DefaultPVID=1-10 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp4UserClassOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp4UserClassOptionValueTest.kt new file mode 100644 index 00000000..06c3ac1f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp4UserClassOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcp4UserClassOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + UserClass=foo + UserClass=foo bar baz + UserClass="with space" 'single quoted' + UserClass=esc\x41ped tab\tbed + UserClass=mid"dle quo"te + UserClass="a" + UserClass= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + UserClass="unterminated + UserClass=bad\qescape + UserClass=bad\ escaped-space + UserClass="" + UserClass='' + UserClass=a "" + UserClass="" b + UserClass=""'' + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6SendOptionOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6SendOptionOptionValueTest.kt new file mode 100644 index 00000000..322b6ba9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6SendOptionOptionValueTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcp6SendOptionOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + SendOption=1:string:\x41 + SendOption=65534:uint8:1 + SendOption=0x10:uint16:0x1234 + SendOption=23:ipv6address:2001:db8::53 + SendOption=23:ipv4address:10.0.0.1 + SendOption=12:uint32:4294967295 + SendOption=1:ipv6address::2001:db8::1 + SendOption=1:uint8:0b+1 + SendVendorOption=1: 2:uint8:0b 1 + SendVendorOption=123: 1:string:x + SendVendorOption=123:1:string:hello + SendVendorOption=0:1:uint8:2 + SendVendorOption=4294967295:65534:string:x + SendVendorOption= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // An unknown TYPE (`bytes`, `int8`, `text`) makes systemd drop the WHOLE .network file ("Failed to parse + // file: Invalid argument"): DHCPOptionDataType has no negative enumerator, so the C's `type < 0` check + // never fires and the switch's `default: return -EINVAL` does (sd-check, 0f0682b773). + // language="unit file (systemd)" + val file = """ + [DHCPv6] + SendOption=0:string:x + SendOption=65535:string:x + SendOption=1:uint8: + SendOption=1:ipv6address:52:54:00:b9:b5:61 + SendOption=1:string:\U + SendVendorOption=4294967296:1:string:x + SendVendorOption=123:1:bytes:x + SendVendorOption=12:string:x + SendOption=123:1:string:x + SendOption=1:ipv6address:::1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(10, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6UserOrVendorClassOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6UserOrVendorClassOptionValueTest.kt new file mode 100644 index 00000000..2f1a3ff0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6UserOrVendorClassOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcp6UserOrVendorClassOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + UserClass=foo bar + UserClass="quoted value" + VendorClass=vendor-1 vendor\s2 + VendorClass= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + UserClass='unterminated + VendorClass=bad\zescape + UserClass="" + VendorClass="" + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest.kt new file mode 100644 index 00000000..284222df --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + FallbackLeaseLifetimeSec=forever + FallbackLeaseLifetimeSec=infinity + FallbackLeaseLifetimeSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + FallbackLeaseLifetimeSec=5min + FallbackLeaseLifetimeSec=never + FallbackLeaseLifetimeSec=Infinity + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpLabelOptionValueTest.kt new file mode 100644 index 00000000..3c72db9e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpLabelOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + Label=label1 + Label=eth0:dhcp + Label=4444 + Label=a b/c%d + Label=exactly15chars! + Label= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + Label=sixteen-chars-xx + Label=café + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTlvOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTlvOptionValueTest.kt new file mode 100644 index 00000000..86c9b32f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTlvOptionValueTest.kt @@ -0,0 +1,76 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpOptionTlvOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // [DHCPRelay] ExtraOption= shares this parser, but the plugin's section data doesn't know [DHCPRelay] + // yet, so it can't be exercised here. + // language="unit file (systemd)" + val file = """ + [DHCPv4] + SendOption=12:string:test + SendOption=1:uint8:255 + SendOption=0x0c:uint16:0xffff + SendOption=254:uint32:4294967295 + SendOption=014:ipv4address:192.168.1.1 + SendOption=17:ipv6address:2001:db8::1 + SendOption=60:string:has:colons:inside + SendOption=60:string:tab\there\x41\u00e9\101 é + SendOption=12::uint8::5 + SendOption=12:uint8: 7 + SendOption=12:uint8:0b+1 + SendOption=12:uint8:0b 1 + SendOption=12:ipv6address::2001:db8::1 + SendOption=12:ipv6address:::2001:db8::1 + SendOption= + SendVendorOption=43:string:vendor + [DHCPServer] + SendOption=42:ipv4address:10.0.0.1 + SendVendorOption=43:uint8:1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // An unknown TYPE (`bytes`, `int8`, `text`) makes systemd drop the WHOLE .network file ("Failed to parse + // file: Invalid argument"): DHCPOptionDataType has no negative enumerator, so the C's `type < 0` check + // never fires and the switch's `default: return -EINVAL` does (sd-check, 0f0682b773). + // language="unit file (systemd)" + val file = """ + [DHCPv4] + SendOption=0:string:test + SendOption=255:string:test + SendOption=12:uint8:256 + SendOption=12:uint16:65536 + SendOption=12:int8:5 + SendOption=12:ipv4address:300.1.1.1 + SendOption=12:string: + SendOption=12:string:bad\qescape + SendOption=12 + SendOption=abc:string:x + [DHCPServer] + SendVendorOption=12:ipv6address:1.2.3.4 + SendVendorOption=12:uint32:-1 + SendOption=12:ipv6address:::1 + SendOption=12:ipv6address:: + SendOption=12:uint8:0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(15, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTypedOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTypedOptionValueTest.kt new file mode 100644 index 00000000..c8caec50 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTypedOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpOptionTypedOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // [DHCPRelay] CircuitId=/RemoteId=/VirtualSubnetSelection= share this parser, but the plugin's + // section data doesn't know [DHCPRelay] yet; the deprecated [DHCPServer] spellings exercise it. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + RelayAgentCircuitId=string:sample_circuit_id + RelayAgentRemoteId=string:aabbccdd + RelayAgentRemoteId=uint8:0xff + RelayAgentRemoteId=uint16:65535 + RelayAgentRemoteId=uint32:0 + RelayAgentRemoteId=ipv4address:192.0.2.1 + RelayAgentRemoteId=ipv6address:fe80::1 + RelayAgentRemoteId=ipv6address::2001:db8::1 + RelayAgentRemoteId=string:with\x00nul\ttab + RelayAgentRemoteId=::string:leading-colons + RelayAgentCircuitId= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // An unknown TYPE (`bytes`, `int8`, `text`) makes systemd drop the WHOLE .network file ("Failed to parse + // file: Invalid argument"): DHCPOptionDataType has no negative enumerator, so the C's `type < 0` check + // never fires and the switch's `default: return -EINVAL` does (sd-check, 0f0682b773). + // language="unit file (systemd)" + val file = """ + [DHCPServer] + RelayAgentCircuitId=sample_circuit_id + RelayAgentCircuitId=text:hello + RelayAgentCircuitId=uint8:256 + RelayAgentCircuitId=uint16:-1 + RelayAgentCircuitId=ipv4address:192.0.2.1/24 + RelayAgentCircuitId=string + RelayAgentRemoteId=ipv6address:::1 + RelayAgentRemoteId=ipv6address::ffff:192.0.2.1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOrRaRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOrRaRouteTableOptionValueTest.kt new file mode 100644 index 00000000..7023d304 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOrRaRouteTableOptionValueTest.kt @@ -0,0 +1,77 @@ +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 + +/** + * RouteTable= in [DHCPv4], [DHCPv6] and [IPv6AcceptRA] (config_parse_dhcp_or_ra_route_table). Every value was checked + * with systemd's parser at 0f0682b773 on all three keys, which agree. `mytable` and `Main` are rejected there only + * because sd-check loads no networkd.conf; they are accepted here on purpose (a networkd.conf RouteTable=NAME:NUMBER + * could define them). + */ +class ConfigParseDhcpOrRaRouteTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + RouteTable=211 + RouteTable=main + RouteTable=default + RouteTable=local + RouteTable=4294967295 + RouteTable=0x10 + RouteTable=010 + RouteTable=0b 1 + RouteTable=mytable + RouteTable=Main + RouteTable= + [DHCPv6] + RouteTable=211 + RouteTable=local + RouteTable=0x10 + RouteTable=mytable + [IPv6AcceptRA] + RouteTable=211 + RouteTable=main + RouteTable=0b 1 + RouteTable=mytable + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + RouteTable=0 + RouteTable=00 + RouteTable=08 + RouteTable=4294967296 + RouteTable=a:b + RouteTable=1:2 + [DHCPv6] + RouteTable=0 + RouteTable=4294967296 + RouteTable=a:b + [IPv6AcceptRA] + RouteTable=0 + RouteTable=08 + RouteTable=1:2 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValueTest.kt new file mode 100644 index 00000000..898c56f5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpPdPrefixRouteTypeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + UnassignedSubnetPolicy=none + UnassignedSubnetPolicy=unreachable + UnassignedSubnetPolicy= + [DHCPv6] + UnassignedSubnetPolicy=blackhole + UnassignedSubnetPolicy=prohibit + UnassignedSubnetPolicy=throw + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + UnassignedSubnetPolicy=unicast + UnassignedSubnetPolicy=no + [DHCPv6] + UnassignedSubnetPolicy=local + UnassignedSubnetPolicy=Blackhole + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdSubnetIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdSubnetIdOptionValueTest.kt new file mode 100644 index 00000000..61dc584e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdSubnetIdOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpPdSubnetIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // SubnetId= is always hexadecimal (safe_atoux64), so `ff` and `add` are numbers. + // language="unit file (systemd)" + val file = """ + [DHCPPrefixDelegation] + SubnetId=auto + SubnetId=0 + SubnetId=10 + SubnetId=ff + SubnetId=0xabcd + SubnetId=add + SubnetId=7fffffffffffffff + SubnetId= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPPrefixDelegation] + SubnetId=8000000000000000 + SubnetId=Auto + SubnetId=xyz + SubnetId=1g + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRelayInterfaceModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRelayInterfaceModeOptionValueTest.kt new file mode 100644 index 00000000..47179f70 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRelayInterfaceModeOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpRelayInterfaceModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DHCPRelay=upstream + DHCPRelay=downstream + DHCPRelay= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DHCPRelay=yes + DHCPRelay=both + DHCPRelay=Upstream + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRequestOptionsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRequestOptionsOptionValueTest.kt new file mode 100644 index 00000000..43659785 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRequestOptionsOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpRequestOptionsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + RequestOptions=59 60 + RequestOptions=17 + RequestOptions=1 254 0xfe 012 + RequestOptions=0b+1 0b1 + RequestOptions= + [DHCPv6] + RequestOptions=23 24 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + RequestOptions=0 + RequestOptions=255 + RequestOptions=59,60 + RequestOptions=0b 1 + RequestOptions=0o 7 + [DHCPv6] + RequestOptions=dns + RequestOptions=0b-0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerAddressOptionValueTest.kt new file mode 100644 index 00000000..4284b30e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerAddressOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpServerAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + ServerAddress=192.168.5.1/24 + ServerAddress=0.0.0.0/24 + ServerAddress=10.0.0.1 + ServerAddress=10.0.0.1/0 + ServerAddress=10.0.0.1/32 + ServerAddress=10.0.0.1/0x18 + ServerAddress=10.0.0.1/ 24 + ServerAddress=169.253.255.255/16 + ServerAddress=128.0.0.1 + ServerAddress= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + ServerAddress=192.168.5.1/33 + ServerAddress=2001:db8::1/64 + ServerAddress=192.168.5 + ServerAddress=server.example.com + ServerAddress=127.0.0.1/8 + ServerAddress=127.255.0.1 + ServerAddress=169.254.1.1/16 + ServerAddress=169.254.0.0/16 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest.kt new file mode 100644 index 00000000..a797d59b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Anything >= MIN_V6ONLY_WAIT_USEC (300s) is accepted by systemd's parser (sd-check, 0f0682b773). + // language="unit file (systemd)" + val file = """ + [DHCPServer] + IPv6OnlyPreferredSec=5min + IPv6OnlyPreferredSec=5m + IPv6OnlyPreferredSec=4min 60s + IPv6OnlyPreferredSec=300000000us + IPv6OnlyPreferredSec=1M + IPv6OnlyPreferredSec=300 + IPv6OnlyPreferredSec=1h 5min + IPv6OnlyPreferredSec=1h30min + IPv6OnlyPreferredSec=.5h + IPv6OnlyPreferredSec=10min5s + IPv6OnlyPreferredSec=infinity + IPv6OnlyPreferredSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + IPv6OnlyPreferredSec=yes + IPv6OnlyPreferredSec=5 fortnights + IPv6OnlyPreferredSec=-5min + IPv6OnlyPreferredSec=1h. + IPv6OnlyPreferredSec=1.2.3 + IPv6OnlyPreferredSec=Infinity + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + + @Test + fun testBelowMinimumIsInvalid() { + // systemd: "Invalid [DHCPServer] IPv6OnlyPreferredSec=, ignoring assignment" for every value below + // 300s outside its test mode (sd-check, 0f0682b773). 20s is systemd's own test-suite value. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + IPv6OnlyPreferredSec=20s + IPv6OnlyPreferredSec=0 + IPv6OnlyPreferredSec=299 + IPv6OnlyPreferredSec=299.999999s + IPv6OnlyPreferredSec=4min + IPv6OnlyPreferredSec=299999999us + IPv6OnlyPreferredSec=0.0833333h + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpStaticLeaseHostnameOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpStaticLeaseHostnameOptionValueTest.kt new file mode 100644 index 00000000..e6781889 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpStaticLeaseHostnameOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpStaticLeaseHostnameOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServerStaticLease] + Hostname=test-hostname + Hostname=fqdn.example.com + Hostname=fqdn.example.com. + Hostname=0123 + Hostname=a + Hostname=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com + Hostname= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServerStaticLease] + Hostname=-leading + Hostname=trailing- + Hostname=under_score + Hostname=double..dot + Hostname=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + Hostname=with\.escape + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnsOverTlsModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnsOverTlsModeOptionValueTest.kt new file mode 100644 index 00000000..17f9289e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnsOverTlsModeOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDnsOverTlsModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DNSOverTLS=opportunistic + DNSOverTLS=yes + DNSOverTLS=no + DNSOverTLS=TRUE + DNSOverTLS=1 + DNSOverTLS=Yes + DNSOverTLS=oFf + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DNSOverTLS=Opportunistic + DNSOverTLS=strict + DNSOverTLS=allow-downgrade + DNSOverTLS=OPPORTUNISTIC + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValueTest.kt new file mode 100644 index 00000000..bb226b27 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDnssecNegativeTrustAnchorsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DNSSECNegativeTrustAnchors=example.com + DNSSECNegativeTrustAnchors=test local. corp.example + DNSSECNegativeTrustAnchors=. + DNSSECNegativeTrustAnchors=_tcp.example + DNSSECNegativeTrustAnchors=foo\.bar + DNSSECNegativeTrustAnchors=foo\\\.bar + DNSSECNegativeTrustAnchors=localhost + DNSSECNegativeTrustAnchors= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DNSSECNegativeTrustAnchors=a..b + DNSSECNegativeTrustAnchors=good.example .. + DNSSECNegativeTrustAnchors=~example.com.. + DNSSECNegativeTrustAnchors=foo\\bar + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDomainsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDomainsOptionValueTest.kt new file mode 100644 index 00000000..7c876458 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDomainsOptionValueTest.kt @@ -0,0 +1,65 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDomainsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + Domains=example.com + Domains=hogehoge ~foofoo + Domains=one two three four five six seven eight nine ten + Domains=~. + Domains=~ + Domains=. + Domains=* + Domains=~* + Domains=example.com. + Domains=_srv.example.com + Domains=foo\.bar + Domains=foo\\\\bar + Domains=foo\\.bar + Domains=foo\\\.bar + Domains=foo\\065bar + Domains=localhost.example.com + Domains= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + Domains=example..com + Domains=.example.com + Domains=example.com.. + Domains=ok.example xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.com + Domains=localhost + Domains=foo.localhost + Domains=~localhost + Domains=Foo.LocalHost. + Domains=localhost.localdomain + Domains=x.localhost.localdomain + Domains=foo\\bar + Domains=foo\\256bar + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDrrSizeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDrrSizeOptionValueTest.kt new file mode 100644 index 00000000..21931169 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDrrSizeOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDrrSizeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [DeficitRoundRobinSchedulerClass] + QuantumBytes=2000 + QuantumBytes=1K + QuantumBytes=1.5K + QuantumBytes=4095M + QuantumBytes=4294967295 + QuantumBytes=3G 1023M + QuantumBytes= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DeficitRoundRobinSchedulerClass] + QuantumBytes=4G + QuantumBytes=4294967296 + QuantumBytes=1k + QuantumBytes=-1 + QuantumBytes=abc + QuantumBytes=1K 1K + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidRawdataOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidRawdataOptionValueTest.kt new file mode 100644 index 00000000..d9e9db1d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidRawdataOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDuidRawdataOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // [DHCPv6] uses config_parse_duid_rawdata and [DHCPv4] config_parse_network_duid_rawdata. + // Accepted/rejected spellings mirror systemd's test-networkd-conf.c. + // language="unit file (systemd)" + val file = """ + [DHCPv6] + DUIDRawData=00:00:ab:11:f9:2a:c2:77:29:f9:5c:00 + DUIDRawData=00:11:22:33:44:55:66:77 + DUIDRawData=00:11:22: + DUIDRawData=0:1:2:3:4:5:6:7 + DUIDRawData=11:: + DUIDRawData=ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab + DUIDRawData= + [DHCPv4] + DUIDRawData=AB:cd + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + DUIDRawData=000:11:22 + DUIDRawData=00:111:22 + DUIDRawData=abcdef + DUIDRawData=ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab + [DHCPv4] + DUIDRawData=00:gg + DUIDRawData=00: 11 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidTypeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidTypeOptionValueTest.kt new file mode 100644 index 00000000..703d8e4e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidTypeOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDuidTypeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // [DHCPv6] uses config_parse_duid_type and [DHCPv4] config_parse_network_duid_type; same grammar. + // Values from systemd's test-networkd-conf.c and test-network fixtures. + // language="unit file (systemd)" + val file = """ + [DHCPv6] + DUIDType=link-layer-time + DUIDType=link-layer-time:2000-01-01 00:00:00 UTC + DUIDType=vendor + DUIDType=link-layer + DUIDType=uuid + DUIDType=0x42 + DUIDType=1:2000-01-01 00:00:00 UTC + DUIDType=vendor: + [DHCPv4] + DUIDType=uuid + DUIDType=link-layer-time:@1700000000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + DUIDType=foo + DUIDType=vendor:2000-01-01 00:00:00 UTC + DUIDType=uuid:2000-01-01 00:00:00 UTC + DUIDType=65536 + [DHCPv4] + DUIDType=Vendor + DUIDType=2:2000-01-01 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEnvironOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEnvironOptionValueTest.kt new file mode 100644 index 00000000..26c55d1f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEnvironOptionValueTest.kt @@ -0,0 +1,97 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseEnvironOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From systemd's test/test-execute/exec-environment*.service, exec-unsetenvironment.service and the corpus. + // language="unit file (systemd)" + val file = """ + [Service] + Environment="VAR1=word1 word2" VAR2=word3 "VAR3=${'$'}word 5 6" + Environment="VAR3=foobar" + Environment=FOO=bar QUUX=waldo VAR3=value3 VAR4=value4 VAR5=value5 X%b=%U VAR6=value6 + Environment=TOP_SECRET=%d/very_top_secret + Environment='SHM_MEMORY=64' + Environment="TZ=:/etc/localtime" + Environment=SHELL_PROMPT_PREFIX="pre-udev " + Environment=PYTHONPATH='/path/to/mysite' + Environment=USER= CONFIG= HOST= PORT= EXTRAOPTS= + Environment=JAVA_OPTS=-Djava.awt.headless=true + Environment=USER=${'$'}{SERVICE_USER} HOME=${'$'}{SERVICE_HOME} + Environment="OPTIONS=" + Environment=_UNDERSCORE=1 lower_case=2 A1=3 + Environment=TAB=a\tb NL=x\ny HEX=\x41 OCT=\101 UNI=é SP=a\sb + Environment="QUOTED=say \"hi\"" + Environment="FOO"=bar + Environment=U=\uFFFE V=\U0010FFFD W=\U0001F600 + Environment= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidInOtherUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + Environment=FOO=bar + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + Environment="A=b c" + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + Environment=A=1 B=2 + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + Environment=UNICORN_OPTS=-E production + Environment=FOO + Environment=1FOO=bar + Environment==bar + Environment=FOO-BAR=1 + Environment="FOO=unbalanced + Environment=FOO=a\qb + Environment="=bar" + Environment="NOEQUALS" + Environment=FOO.BAR=1 + Environment=FOO=\ud800 + Environment=FOO=\U00110000 + Environment=FOO=\U0000FDD0 + Environment=FOO=\U0001FFFF + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(14, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsPrioOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsPrioOptionValueTest.kt new file mode 100644 index 00000000..5796bcb5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsPrioOptionValueTest.kt @@ -0,0 +1,58 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseEtsPrioOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts 0b+1 and 0b-0 (a sign may follow the 0b prefix). + // language="unit file (systemd)" + val file = """ + [EnhancedTransmissionSelection] + PriorityMap=8 7 6 5 + PriorityMap=3 4 5 + PriorityMap=6 7 + PriorityMap=0 + PriorityMap=255 + PriorityMap=0x0f 017 0b1 + PriorityMap=0b+1 + PriorityMap=1 0b-0 2 + PriorityMap=0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + PriorityMap= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects 0b 1 and 0b +1 (the list splits on blanks, so the word is "0b") and 0b-1 (ERANGE). + // language="unit file (systemd)" + val file = """ + [EnhancedTransmissionSelection] + PriorityMap=256 + PriorityMap=-1 + PriorityMap=1,2 + PriorityMap=a b + PriorityMap=0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 + PriorityMap=0b 1 + PriorityMap=0b +1 + PriorityMap=0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsQuantaOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsQuantaOptionValueTest.kt new file mode 100644 index 00000000..e836feb7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsQuantaOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseEtsQuantaOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [EnhancedTransmissionSelection] + QuantumBytes=2 4 6 + QuantumBytes=1001 1002 1003 + QuantumBytes=1004 1005 + QuantumBytes=1K + QuantumBytes=1K512B + QuantumBytes=1.5K 2M + QuantumBytes=4294967295 + QuantumBytes=1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + QuantumBytes= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [EnhancedTransmissionSelection] + QuantumBytes=0 + QuantumBytes=4G + QuantumBytes=1 0 2 + QuantumBytes=1K 512 B + QuantumBytes=abc + QuantumBytes=1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + QuantumBytes=1k + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecCoredumpFilterOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecCoredumpFilterOptionValueTest.kt new file mode 100644 index 00000000..be8f14bf --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecCoredumpFilterOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExecCoredumpFilterOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + CoredumpFilter=default + CoredumpFilter=all + CoredumpFilter=default private-dax shared-dax + CoredumpFilter=private-anonymous shared-anonymous private-file-backed shared-file-backed elf-headers private-huge shared-huge + CoredumpFilter=private-file-backed private-dax 0xF + CoredumpFilter=11 + CoredumpFilter=0x1101 + CoredumpFilter=0 + CoredumpFilter=ff deadbeef FFFFFFFFFFFFFFFF + CoredumpFilter=-0 + CoredumpFilter=-0x00 +1f + CoredumpFilter= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + CoredumpFilter=defaulta + CoredumpFilter=default defaulta default + CoredumpFilter=Default + CoredumpFilter=private + CoredumpFilter=0x + CoredumpFilter=1FFFFFFFFFFFFFFFF + CoredumpFilter=12g + CoredumpFilter=-1 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecInputDataOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecInputDataOptionValueTest.kt new file mode 100644 index 00000000..2f5ca4d0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecInputDataOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExecInputDataOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZXMgYW5kIHNvIGRv \ + IEkKQSBmdWxsIGNvbW1pdG1lbnQncyB3aGF0IEnigLJtIHRoaW5raW5nIG9mCllvdSB3b3VsZG4n + StandardInputData=aGVsbG8= + StandardInputData=aGVsbA== + StandardInputData=aGVs + StandardInputData= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + StandardInputData=hello + StandardInputData=aGVsbG8 + StandardInputData=a!b= + StandardInputData==== + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashOptionValueTest.kt new file mode 100644 index 00000000..f0042d63 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExecRootHashOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootHash=e0e5e0b9e6d9bd4b8d2e2e3cbb6f0f4d0b0c0e5e0b9e6d9bd4b8d2e2e3cbb6f0 + RootHash=0123456789ABCDEF0123456789abcdef + RootHash=01234567 89abcdef 01234567 89abcdef + RootHash=/var/lib/images/foo.roothash + RootHash=/anything goes here + RootHash= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootHash=0123456789abcdef + RootHash=0123456789abcdef0123456789abcdef0 + RootHash=0123456789abcdefg123456789abcdef + RootHash=%S/foo.roothash + RootHash=foo.roothash + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashSigOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashSigOptionValueTest.kt new file mode 100644 index 00000000..059fcc46 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashSigOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExecRootHashSigOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootHashSignature=/var/lib/images/foo.roothash.p7s + RootHashSignature=base64:MIIB + RootHashSignature=base64:MIIBAA== + RootHashSignature=base64:MIIBAAE= + RootHashSignature=base64:MI IB AA== + RootHashSignature=base64:-_-_ + RootHashSignature=base64: + RootHashSignature= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootHashSignature=MIIBAA== + RootHashSignature=base64:MII + RootHashSignature=base64:MI*B + RootHashSignature=base64:AB== + RootHashSignature=base64:=AAA + RootHashSignature=BASE64:MIIB + RootHashSignature=base64:AA==AAAA + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExposePortOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExposePortOptionValueTest.kt new file mode 100644 index 00000000..39159d77 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExposePortOptionValueTest.kt @@ -0,0 +1,87 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseExposePortOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import org.junit.Assert.assertEquals +import org.junit.Test + +class ConfigParseExposePortOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN; ports are safe_atou16 (base 0). A `-` after the 0b prefix escapes safe_atou's minus + // check and strtoul() negates modulo 2^64, so `0b -<2^64-80 in binary>` is port 80 (sd-check accepts it). + // language="unit file (systemd)" + val file = """ + [Network] + Port=80 + Port=81:8181 + Port=tcp:60 + Port=udp:60:61 + Port=65535 + Port=0x50 + Port=tcp:0x50:010 + Port=0o+120 + Port=0b 1 + Port=80:0b 1 + Port=tcp:0o 120 + Port=0b -$PORT_80_WRAPPED + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Rejected spellings from TEST-13-NSPAWN (--port=-1, :, icmp:..., tcp::8080, 8080:) plus port 0, overflow, + // an upper-case protocol (startswith is case-sensitive), a third field, and whitespace around a port (parse_ip_port + // refuses leading whitespace; only the whitespace strtoul() skips after a 0b/0o prefix is fine). + // language="unit file (systemd)" + val file = """ + [Network] + Port=0 + Port=-1 + Port=: + Port=icmp:80:8080 + Port=tcp::8080 + Port=8080: + Port=65536 + Port=TCP:80 + Port=80:8080:1 + Port=80: 90 + Port=tcp: 80 + Port=80 :90 + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } + + @Test + fun testLeadingVerticalTab() { + // SAFE_ATO_REFUSE_LEADING_WHITESPACE only checks " \t\n\r", so a \v reaches strtoul(), which skips it. + // sd-check accepts Port=\v80 and Port=tcp:\v80:81 but rejects Port=tcp:80: 81. The editor's lexer does not take + // \v right after `=`, so these are checked on the grammar directly, on both engines. + val grammar = ConfigParseExposePortOptionValue().combinator + for (value in listOf("\u000B80", "tcp:\u000B80:81", "80:\u000B81")) { + assertEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(grammar, value)) + assertEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(grammar, value)) + } + for (value in listOf("tcp:80: 81", "tcp:\t80")) { + assertEquals("classic engine on <$value>", GrammarEngineVerdict.Kind.SYNTAX, GrammarEngineVerdict.classic(grammar, value).kind) + assertEquals("parse engine on <$value>", GrammarEngineVerdict.Kind.SYNTAX, GrammarEngineVerdict.parseEngine(grammar, value).kind) + } + } +} + +/** 2^64 - 80 in binary: strtoul() negates it modulo 2^64 to 80. */ +private val PORT_80_WRAPPED = (java.math.BigInteger.ONE.shiftLeft(64) - java.math.BigInteger.valueOf(80)).toString(2) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExtensionImagesOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExtensionImagesOptionValueTest.kt new file mode 100644 index 00000000..2ef1535d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExtensionImagesOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExtensionImagesOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + ExtensionImages=/tmp/app0.raw + ExtensionImages=/tmp/app-data-only.raw + ExtensionImages=-/srv/ext.raw /srv/ext2.raw:root:ro + ExtensionImages=/srv/ext.raw:x-systemd.relax-extension-release-check + ExtensionImages=/srv/ext.raw:root:ro,nosuid:usr:nodev + ExtensionImages=/srv/ext.raw:nosuid: + ExtensionImages=%S/ext.raw + ExtensionImages=/srv/a\:b.raw + ExtensionImages= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + ExtensionImages=ext.raw + ExtensionImages=/dev/sda1 + ExtensionImages=/proc/x.raw + ExtensionImages=/srv/ext.raw:bogus:ro + ExtensionImages=/srv/../ext.raw + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFdbInterfaceOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFdbInterfaceOptionValueTest.kt new file mode 100644 index 00000000..d98cb471 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFdbInterfaceOptionValueTest.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFdbInterfaceOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [BridgeFDB] + OutgoingInterface=test1 + OutgoingInterface=dummy98 + OutgoingInterface=7 + OutgoingInterface=010 + OutgoingInterface=0b 1 + OutgoingInterface=0o 7 + OutgoingInterface=an-alternative-name-longer-than-fifteen + OutgoingInterface= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BridgeFDB] + OutgoingInterface=0 + OutgoingInterface=default + OutgoingInterface=a%d + OutgoingInterface=.. + OutgoingInterface=08 + OutgoingInterface=eth 0 + OutgoingInterface=0b -1 + OutgoingInterface=4294967297 + OutgoingInterface=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(9, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelBoolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelBoolOptionValueTest.kt new file mode 100644 index 00000000..7163924a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelBoolOptionValueTest.kt @@ -0,0 +1,64 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqCodelBoolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + ECN=yes + ECN=no + ECN=1 + ECN=0 + ECN=true + ECN=false + ECN=on + ECN=off + ECN=y + ECN=n + ECN=t + ECN=f + ECN=YES + ECN=False + ECN=On + ECN=Yes + ECN=TRUE + ECN=oFf + ECN=Y + ECN= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + ECN=maybe + ECN=2 + ECN=enable + ECN=nope + ECN=Maybe + ECN=ENABLE + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelU32OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelU32OptionValueTest.kt new file mode 100644 index 00000000..bd8d3047 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelU32OptionValueTest.kt @@ -0,0 +1,96 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqCodelU32OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + PacketLimit=0 + Flows=0 + PacketLimit=1 + Flows=1 + PacketLimit=20480 + Flows=20480 + PacketLimit=4294967295 + Flows=4294967295 + PacketLimit=0x10 + Flows=0x10 + PacketLimit=0o17 + Flows=0o17 + PacketLimit=0b101 + Flows=0b101 + PacketLimit=017 + Flows=017 + PacketLimit=+5 + Flows=+5 + PacketLimit=-0 + Flows=-0 + PacketLimit=0b+1 + Flows=0b+1 + PacketLimit=0b-0 + Flows=0b-0 + PacketLimit=0b 1 + Flows=0b 1 + PacketLimit=0o 7 + Flows=0o 7 + PacketLimit=0b +1 + Flows=0b +1 + PacketLimit=0b -0 + Flows=0b -0 + PacketLimit= + Flows= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + PacketLimit=4294967296 + Flows=4294967296 + PacketLimit=-1 + Flows=-1 + PacketLimit=08 + Flows=08 + PacketLimit=0x + Flows=0x + PacketLimit=1.5 + Flows=1.5 + PacketLimit=abc + Flows=abc + PacketLimit=+0b1 + Flows=+0b1 + PacketLimit=12 13 + Flows=12 13 + PacketLimit=0b-1 + Flows=0b-1 + PacketLimit=0b -1 + Flows=0b -1 + PacketLimit=-0b0 + Flows=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(22, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelUsecOptionValueTest.kt new file mode 100644 index 00000000..8ceaf4d8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelUsecOptionValueTest.kt @@ -0,0 +1,130 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqCodelUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + TargetSec=10ms + IntervalSec=10ms + CEThresholdSec=10ms + TargetSec=200ms + IntervalSec=200ms + CEThresholdSec=200ms + TargetSec=1min30s + IntervalSec=1min30s + CEThresholdSec=1min30s + TargetSec=5s 500ms + IntervalSec=5s 500ms + CEThresholdSec=5s 500ms + TargetSec=5s500ms + IntervalSec=5s500ms + CEThresholdSec=5s500ms + TargetSec=.5s + IntervalSec=.5s + CEThresholdSec=.5s + TargetSec=+3.1s + IntervalSec=+3.1s + CEThresholdSec=+3.1s + TargetSec=3.1s.2 + IntervalSec=3.1s.2 + CEThresholdSec=3.1s.2 + TargetSec=3.1 .2 + IntervalSec=3.1 .2 + CEThresholdSec=3.1 .2 + TargetSec=1.5h + IntervalSec=1.5h + CEThresholdSec=1.5h + TargetSec=23us + IntervalSec=23us + CEThresholdSec=23us + TargetSec=23µs + IntervalSec=23µs + CEThresholdSec=23µs + TargetSec=2 weeks + IntervalSec=2 weeks + CEThresholdSec=2 weeks + TargetSec=100 + IntervalSec=100 + CEThresholdSec=100 + # Largest finite values parse_time() accepts (total < USEC_INFINITY, each integer <= LLONG_MAX). + TargetSec=9223372036854775807us 9223372036854775807us + IntervalSec=9223372036854775807us 9223372036854775807us + CEThresholdSec=9223372036854775807us 9223372036854775807us + TargetSec=18446744073708s + IntervalSec=18446744073708s + CEThresholdSec=18446744073708s + TargetSec=infinity + IntervalSec=infinity + CEThresholdSec=infinity + TargetSec= + IntervalSec= + CEThresholdSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + TargetSec=5. + IntervalSec=5. + CEThresholdSec=5. + TargetSec=-5s + IntervalSec=-5s + CEThresholdSec=-5s + TargetSec=3.s + IntervalSec=3.s + CEThresholdSec=3.s + TargetSec=12.34.56 + IntervalSec=12.34.56 + CEThresholdSec=12.34.56 + TargetSec=5mins + IntervalSec=5mins + CEThresholdSec=5mins + TargetSec=10 foo + IntervalSec=10 foo + CEThresholdSec=10 foo + TargetSec=infinity 5s + IntervalSec=infinity 5s + CEThresholdSec=infinity 5s + TargetSec=Infinity + IntervalSec=Infinity + CEThresholdSec=Infinity + # parse_time() ERANGE: integer above LLONG_MAX, or total reaching USEC_INFINITY. + TargetSec=9223372036854775808us + IntervalSec=9223372036854775808us + CEThresholdSec=9223372036854775808us + TargetSec=18446744073709551615us + IntervalSec=18446744073709551615us + CEThresholdSec=18446744073709551615us + TargetSec=9223372036854775807us 9223372036854775807us 1us + IntervalSec=9223372036854775807us 9223372036854775807us 1us + CEThresholdSec=9223372036854775807us 9223372036854775807us 1us + TargetSec=18446744073709s + IntervalSec=18446744073709s + CEThresholdSec=18446744073709s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(36, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqMaxRateOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqMaxRateOptionValueTest.kt new file mode 100644 index 00000000..dc9792fe --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqMaxRateOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqMaxRateOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [FairQueueing] + MaximumRate=1M + MaximumRate=0.5G + MaximumRate=34G + MaximumRate=34359738367 + MaximumRate=34359738367B + MaximumRate=1G 500M + MaximumRate= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [FairQueueing] + MaximumRate=34359738368 + MaximumRate=35G + MaximumRate=1T + MaximumRate=10mbit + MaximumRate=-1M + MaximumRate=1M 2G + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqUsecOptionValueTest.kt new file mode 100644 index 00000000..818697c5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqUsecOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [FairQueueing] + CEThresholdSec=100ms + CEThresholdSec=1s + CEThresholdSec=4294967295us + CEThresholdSec=71min 34s + CEThresholdSec=1h 11min 34.967295s + CEThresholdSec=0 + CEThresholdSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [FairQueueing] + CEThresholdSec=4294967296us + CEThresholdSec=72min + CEThresholdSec=infinity + CEThresholdSec=1h 12min + CEThresholdSec=5x + CEThresholdSec=-1s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt new file mode 100644 index 00000000..124aa131 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseGeneveAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [GENEVE] + Remote=10.0.0.2 + Remote=2001:db8::1 + Remote=223.255.255.255 + Remote=240.0.0.1 + Remote=ff::1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [GENEVE] + Remote=10.0.0 + Remote=geneve.example.com + Remote=10.0.0.2/24 + Remote=239.1.1.1 + Remote=ff02::1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredBoolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredBoolOptionValueTest.kt new file mode 100644 index 00000000..0f81298a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredBoolOptionValueTest.kt @@ -0,0 +1,64 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseGredBoolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [GenericRandomEarlyDetection] + GenericRIO=yes + GenericRIO=no + GenericRIO=1 + GenericRIO=0 + GenericRIO=true + GenericRIO=false + GenericRIO=on + GenericRIO=off + GenericRIO=y + GenericRIO=n + GenericRIO=t + GenericRIO=f + GenericRIO=YES + GenericRIO=False + GenericRIO=On + GenericRIO=Yes + GenericRIO=TRUE + GenericRIO=oFf + GenericRIO=Y + GenericRIO= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [GenericRandomEarlyDetection] + GenericRIO=maybe + GenericRIO=2 + GenericRIO=enable + GenericRIO=nope + GenericRIO=Maybe + GenericRIO=ENABLE + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredU32OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredU32OptionValueTest.kt new file mode 100644 index 00000000..657eee48 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredU32OptionValueTest.kt @@ -0,0 +1,88 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseGredU32OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [GenericRandomEarlyDetection] + VirtualQueues=12 + DefaultVirtualQueue=12 + VirtualQueues=10 + DefaultVirtualQueue=10 + VirtualQueues=0 + DefaultVirtualQueue=0 + VirtualQueues=16 + DefaultVirtualQueue=16 + VirtualQueues=0x10 + DefaultVirtualQueue=0x10 + VirtualQueues=0b1 + DefaultVirtualQueue=0b1 + VirtualQueues=020 + DefaultVirtualQueue=020 + VirtualQueues=+3 + DefaultVirtualQueue=+3 + VirtualQueues=0b+1 + DefaultVirtualQueue=0b+1 + VirtualQueues=0b-0 + DefaultVirtualQueue=0b-0 + VirtualQueues=0b 1 + DefaultVirtualQueue=0b 1 + VirtualQueues=0o 7 + DefaultVirtualQueue=0o 7 + VirtualQueues=0b +1 + DefaultVirtualQueue=0b +1 + VirtualQueues=0b -0 + DefaultVirtualQueue=0b -0 + VirtualQueues= + DefaultVirtualQueue= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [GenericRandomEarlyDetection] + VirtualQueues=17 + DefaultVirtualQueue=17 + VirtualQueues=0x11 + DefaultVirtualQueue=0x11 + VirtualQueues=-1 + DefaultVirtualQueue=-1 + VirtualQueues=abc + DefaultVirtualQueue=abc + VirtualQueues=4294967296 + DefaultVirtualQueue=4294967296 + VirtualQueues=1.0 + DefaultVirtualQueue=1.0 + VirtualQueues=0b-1 + DefaultVirtualQueue=0b-1 + VirtualQueues=0b -1 + DefaultVirtualQueue=0b -1 + VirtualQueues=-0b0 + DefaultVirtualQueue=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(18, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassRateOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassRateOptionValueTest.kt new file mode 100644 index 00000000..a4f3475b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassRateOptionValueTest.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseHtbClassRateOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucketClass] + Rate=1M + CeilRate=1M + Rate=0.5M + CeilRate=0.5M + Rate=1G + CeilRate=1G + Rate=100G + CeilRate=100G + Rate=1G 500M + CeilRate=1G 500M + Rate=3.5G3B + CeilRate=3.5G3B + Rate=4T3G3B + CeilRate=4T3G3B + Rate=10.M + CeilRate=10.M + Rate=125000 + CeilRate=125000 + Rate=+5K + CeilRate=+5K + Rate=1000 B + CeilRate=1000 B + Rate= + CeilRate= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucketClass] + Rate=10Mbit + CeilRate=10Mbit + Rate=1k + CeilRate=1k + Rate=-1M + CeilRate=-1M + Rate=3B3.5G + CeilRate=3B3.5G + Rate=12P12P + CeilRate=12P12P + Rate=1M 1M + CeilRate=1M 1M + Rate=abc + CeilRate=abc + Rate=.5M + CeilRate=.5M + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(16, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassU32OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassU32OptionValueTest.kt new file mode 100644 index 00000000..6401d690 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassU32OptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseHtbClassU32OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucketClass] + Priority=0 + Priority=1 + Priority=20480 + Priority=4294967295 + Priority=0x10 + Priority=0o17 + Priority=0b101 + Priority=017 + Priority=+5 + Priority=-0 + Priority=0b+1 + Priority=0b-0 + Priority=0b 1 + Priority=0o 7 + Priority=0b +1 + Priority=0b -0 + Priority= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucketClass] + Priority=4294967296 + Priority=-1 + Priority=08 + Priority=0x + Priority=1.5 + Priority=abc + Priority=+0b1 + Priority=12 13 + Priority=0b-1 + Priority=0b -1 + Priority=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbDefaultClassOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbDefaultClassOptionValueTest.kt new file mode 100644 index 00000000..c97135b9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbDefaultClassOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseHtbDefaultClassOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucket] + DefaultClass=30 + DefaultClass=0x30 + DefaultClass=ff + DefaultClass=FFFF + DefaultClass=ffffffff + DefaultClass=0 + DefaultClass=+a + DefaultClass=0b1 + DefaultClass= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucket] + DefaultClass=100000000 + DefaultClass=0x100000000 + DefaultClass=0o7 + DefaultClass=g + DefaultClass=-1 + DefaultClass=1 2 + DefaultClass=0b 1 + DefaultClass=0x 1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbU32OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbU32OptionValueTest.kt new file mode 100644 index 00000000..a09ee769 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbU32OptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseHtbU32OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucket] + RateToQuantum=0 + RateToQuantum=1 + RateToQuantum=20480 + RateToQuantum=4294967295 + RateToQuantum=0x10 + RateToQuantum=0o17 + RateToQuantum=0b101 + RateToQuantum=017 + RateToQuantum=+5 + RateToQuantum=-0 + RateToQuantum=0b+1 + RateToQuantum=0b-0 + RateToQuantum=0b 1 + RateToQuantum=0o 7 + RateToQuantum=0b +1 + RateToQuantum=0b -0 + RateToQuantum= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucket] + RateToQuantum=4294967296 + RateToQuantum=-1 + RateToQuantum=08 + RateToQuantum=0x + RateToQuantum=1.5 + RateToQuantum=abc + RateToQuantum=+0b1 + RateToQuantum=12 13 + RateToQuantum=0b-1 + RateToQuantum=0b -1 + RateToQuantum=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseId128OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseId128OptionValueTest.kt new file mode 100644 index 00000000..e94ec094 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseId128OptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseId128OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN and src/libsystemd/sd-id128/test-id128.c. + // language="unit file (systemd)" + val file = """ + [Exec] + MachineID=f28f129b51874b1280a89421ec4b4ad4 + MachineID=deadbeef-dead-dead-beef-111111111111 + MachineID=F28F129B51874B1280A89421EC4B4AD4 + MachineID=urn:uuid:01020304-0506-0708-090a-0b0c0d0e0f10 + MachineID=URN:UUID:01020304-0506-0708-090a-0b0c0d0e0f10 + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // All-zero IDs, urn: without dashes, misplaced/missing dash, too long, too short (test-id128.c), garbage. + // language="unit file (systemd)" + val file = """ + [Exec] + MachineID=00000000000000000000000000000000 + MachineID=00000000-0000-0000-0000-000000000000 + MachineID=urn:uuid:0102030405060708090a0b0c0d0e0f10 + MachineID=01020304-0506-0708-090a0b0c0d0e0f10 + MachineID=01020304-0506-0708-090a-0b0c0d0e0f101 + MachineID=f28f129b51874b1280a89421ec4b4ad + MachineID=xyz + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIecSizeLongOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIecSizeLongOptionValueTest.kt new file mode 100644 index 00000000..2e1e04d1 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIecSizeLongOptionValueTest.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIecSizeLongOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // parse_size(…, 1024) spellings from TEST(parse_size) (src/test/test-parse-util.c). + // language="unit file (systemd)" + val file = """ + [Socket] + MessageQueueMessageSize=8192 + MessageQueueMessageSize=111.4 + MessageQueueMessageSize=112 B + MessageQueueMessageSize=112.6 B + MessageQueueMessageSize=3.5 K + MessageQueueMessageSize=3. K + MessageQueueMessageSize=3.0 K + MessageQueueMessageSize=4 M 11.5K + MessageQueueMessageSize=3.5G3B + MessageQueueMessageSize=3.5G 4B + MessageQueueMessageSize=4T3G3B + MessageQueueMessageSize=4 T 3 G 3 B + MessageQueueMessageSize=12P + MessageQueueMessageSize=3E 2P + MessageQueueMessageSize=+8K + MessageQueueMessageSize=1K5 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + MessageQueueMessageSize=3. 0 K + MessageQueueMessageSize=12X + MessageQueueMessageSize=12.5X + MessageQueueMessageSize=12.5e3 + MessageQueueMessageSize=-1 + MessageQueueMessageSize=-10B 20K + MessageQueueMessageSize=8k + MessageQueueMessageSize=8 KiB + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt new file mode 100644 index 00000000..4fb1e872 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIfaliasOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Alias= + Alias=uplink to core switch + Alias=eth0 (management) + Alias=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Alias=Café uplink + Alias=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt new file mode 100644 index 00000000..8cf16f95 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIfnamesOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + AlternativeName=dummy98-2-altname + AlternativeName=dummyalt hogehogehogehogehogehoge + AlternativeName=a-very-long-alternative-interface-name-that-is-fine + AlternativeName=sim99 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + AlternativeName=eth:0 + AlternativeName=all + AlternativeName=123 + AlternativeName=ok bad/name + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIgnoreCarrierLossOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIgnoreCarrierLossOptionValueTest.kt new file mode 100644 index 00000000..6f104590 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIgnoreCarrierLossOptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIgnoreCarrierLossOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IgnoreCarrierLoss=true + IgnoreCarrierLoss=false + IgnoreCarrierLoss=No + IgnoreCarrierLoss=Yes + IgnoreCarrierLoss=TRUE + IgnoreCarrierLoss=oFf + IgnoreCarrierLoss=5s + IgnoreCarrierLoss=1min 30s + IgnoreCarrierLoss=infinity + IgnoreCarrierLoss=0 + IgnoreCarrierLoss=1h30min + IgnoreCarrierLoss=55s500ms + IgnoreCarrierLoss=.5s + IgnoreCarrierLoss=5s.5 + IgnoreCarrierLoss=+3.1s + IgnoreCarrierLoss=3.1 sec .2 sec + IgnoreCarrierLoss=2.5 + IgnoreCarrierLoss=23µs + IgnoreCarrierLoss=5 min + IgnoreCarrierLoss= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IgnoreCarrierLoss=sometimes + IgnoreCarrierLoss=5 parsecs + IgnoreCarrierLoss=-1s + IgnoreCarrierLoss=5. + IgnoreCarrierLoss=12.34.56 + IgnoreCarrierLoss=3.s + IgnoreCarrierLoss=3. 1s + IgnoreCarrierLoss=..1234 + IgnoreCarrierLoss=5ns + IgnoreCarrierLoss=infinity .7 + IgnoreCarrierLoss=5+5 + IgnoreCarrierLoss=Infinity + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseImportCredentialOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseImportCredentialOptionValueTest.kt new file mode 100644 index 00000000..56fa4eed --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseImportCredentialOptionValueTest.kt @@ -0,0 +1,58 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseImportCredentialOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + ImportCredential=fsck.* + ImportCredential=test.creds.* + ImportCredential=test.creds.*:renamed.creds. + ImportCredential=test.creds.first:renamed.creds.first + ImportCredential=brummbaer + ImportCredential=* + ImportCredential=x* + ImportCredential=my cred + ImportCredential=foo: + ImportCredential=%N.* + ImportCredential= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + ImportCredential=. + ImportCredential=.. + ImportCredential=foo/bar + ImportCredential=? + ImportCredential=*a + ImportCredential=a? + ImportCredential=a[abc] + ImportCredential=foo:bar:baz + ImportCredential=foo:.. + ImportCredential=foo:a/b + ImportCredential=:foo + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt new file mode 100644 index 00000000..30321237 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_in_addr_non_null(AF_INET6). */ +class ConfigParseInAddrNonNullIpv6OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv6StableSecretAddress=2001:db8::1 + IPv6StableSecretAddress=::1 + IPv6StableSecretAddress=fe80::1234:5678 + IPv6StableSecretAddress=::ffff:192.168.1.1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + IPv6StableSecretAddress=:: + IPv6StableSecretAddress=0:0:0:0:0:0:0:0 + IPv6StableSecretAddress=192.168.1.1 + IPv6StableSecretAddress=2001:db8::/64 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInaccessibleOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInaccessibleOptionValueTest.kt new file mode 100644 index 00000000..60c82e70 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInaccessibleOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseInaccessibleOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN; the value is taken verbatim, so spaces and colons are part of the path. + // language="unit file (systemd)" + val file = """ + [Files] + Inaccessible=/opt/inaccessible + Inaccessible=/var + Inaccessible=/path with spaces + Inaccessible=/a:b + Inaccessible=/ + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Only absolute paths are accepted. + // language="unit file (systemd)" + val file = """ + [Files] + Inaccessible=opt/x + Inaccessible=./x + Inaccessible=~/x + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInterfacePairOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInterfacePairOptionValueTest.kt new file mode 100644 index 00000000..307ca597 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInterfacePairOptionValueTest.kt @@ -0,0 +1,65 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseInterfacePairOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN; an empty container-side name after the colon means `not given`. + // language="unit file (systemd)" + val file = """ + [Network] + Interface=sd-shared1 sd-shared2:sd-renamed2 sd-shared3:sd-altname3 + MACVLAN=sd-macvlan1 sd-macvlan2:my-macvlan2 sd-macvlanloong + IPVLAN=sd-ipvlan1 sd-ipvlan2:my-ipvlan2 sd-ipvlanlooong + Interface=eth0: + Interface=eth0 wlan0 + MACVLAN=enp0s31f6 + IPVLAN=a.b:c-d + Interface=0x0 -5 0b-1 0xFFFFFFFFFF + Interface=a\\\\ b + Interface=a\:b c + Interface=a\b + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Reserved name, all digits, a second colon, too long (either side), `/`, a reserved name later in the list, + // names that parse_ifindex() reads as a positive index (so ifname_valid() refuses them), and backslash runs + // before whitespace that C unescapes into the name or leaves dangling (all confirmed with fuzz-nspawn-settings). + // language="unit file (systemd)" + val file = """ + [Network] + Interface=all + MACVLAN=0123 + IPVLAN=eth0:host0:x + Interface=eth0:averyveryverylongname + Interface=averyveryverylongname + MACVLAN=eth/0 + IPVLAN=eth0 default + Interface=0x10 + MACVLAN=+5 + IPVLAN=eth0:0o7 + Interface=a\ b + Interface=a\\ b + MACVLAN=a\\\ b + Interface=x a\:b\ c + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(14, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt new file mode 100644 index 00000000..7b252817 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIpoibPkeyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPoIB] + PartitionKey=1 + PartitionKey=0x8001 + PartitionKey=0xffff + PartitionKey=0x7fff + PartitionKey=32769 + PartitionKey=0b1 + PartitionKey=0b 1 + PartitionKey=0b+1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [IPoIB] + PartitionKey=0 + PartitionKey=0x8000 + PartitionKey=32768 + PartitionKey=0x10000 + PartitionKey=default + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llAddressOptionValueTest.kt new file mode 100644 index 00000000..93e6e58f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llAddressOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIpv4llAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv4LLStartAddress=169.254.133.11 + IPv4LLStartAddress=169.254.1.0 + IPv4LLStartAddress=169.254.254.255 + IPv4LLStartAddress= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv4LLStartAddress=169.254.0.5 + IPv4LLStartAddress=169.254.255.1 + IPv4LLStartAddress=192.168.1.1 + IPv4LLStartAddress=169.254.1.256 + IPv4LLStartAddress=fe80::1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llOptionValueTest.kt new file mode 100644 index 00000000..5189d038 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIpv4llOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv4LL=yes + IPv4LL=no + IPv4LL=True + IPv4LL=Yes + IPv4LL=TRUE + IPv4LL=Off + IPv4LL=oFf + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv4LL=ipv4 + IPv4LL=both + IPv4LL=Yess + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt new file mode 100644 index 00000000..dec188ef --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [IPv6AddressLabel] Label=. */ +class ConfigParseIpv6AddressLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Label=0 + Label=100 + Label=4294967294 + Label=0xfffffffe + Label=0b 1 + Label=0b+1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1 and -0b0 (EINVAL) and accepts 0b 1 and 0b+1. + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Label=4294967295 + Label=0xffffffff + Label=-1 + Label=label + Label=+0b1 + Label=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt new file mode 100644 index 00000000..52452cc4 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [IPv6AddressLabel] Prefix=. */ +class ConfigParseIpv6AddressLabelPrefixOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Prefix=2001:db8::/32 + Prefix=::/0 + Prefix=::ffff:0:0/96 + Prefix=fe80::1 + Prefix=2001:db8::/ 32 + Prefix=::/0b 1 + Prefix=::ffff:0:0/0b1100000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects ::/+0b1 (EINVAL) and ::ffff:0:0/0b1100001 (97 > 96), and accepts ::/0b 1. + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Prefix=::ffff:0:0/97 + Prefix=::ffff:1.2.3.4 + Prefix=10.0.0.0/8 + Prefix=2001:db8::/200 + Prefix=::/+0b1 + Prefix=::ffff:0:0/0b1100001 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt new file mode 100644 index 00000000..e115519f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIpv6FlowlabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Tunnel] + IPv6FlowLabel=inherit + IPv6FlowLabel=0 + IPv6FlowLabel=1048575 + IPv6FlowLabel=0xFFFFF + IPv6FlowLabel=0x12345 + IPv6FlowLabel=0b 1 + IPv6FlowLabel=0b-0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Tunnel] + IPv6FlowLabel=1048576 + IPv6FlowLabel=0x100000 + IPv6FlowLabel=Inherit + IPv6FlowLabel=-1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt new file mode 100644 index 00000000..2b368796 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIrqAffinityNumaOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityNUMA=local + IRQAffinityNUMA=0 + IRQAffinityNUMA=3 + IRQAffinityNUMA=0x1 + IRQAffinityNUMA=4294967293 + IRQAffinityNUMA=0b 1 + IRQAffinityNUMA=0b+1 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityNUMA=4294967294 + IRQAffinityNUMA=-1 + IRQAffinityNUMA=remote + IRQAffinityNUMA=1.5 + IRQAffinityNUMA=Local + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt new file mode 100644 index 00000000..32344a7a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt @@ -0,0 +1,40 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIrqAffinityPolicyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityPolicy=single + IRQAffinityPolicy=spread + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityPolicy=Single + IRQAffinityPolicy=balanced + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseJobTimeoutSecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseJobTimeoutSecOptionValueTest.kt new file mode 100644 index 00000000..1f19efa3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseJobTimeoutSecOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseJobTimeoutSecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // parse_sec_fix_0: any parse_sec time span, 0 meaning infinity. No specifier expansion. + // language="unit file (systemd)" + val file = """ + [Unit] + JobTimeoutSec=infinity + JobTimeoutSec=0 + JobTimeoutSec=90 + JobTimeoutSec=5min 20s + JobTimeoutSec=1h30min + JobTimeoutSec=.5s + JobTimeoutSec=+10 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Unit] + JobTimeoutSec=-1 + JobTimeoutSec=5mins + JobTimeoutSec=forever + JobTimeoutSec=%i + JobTimeoutSec=5. + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt new file mode 100644 index 00000000..651d76a0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseL2tpSessionNameOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TPSession] + Name=l2tp-ses1 + Name=sess.a + Name=1a + Name=abcdefghijklmno + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TPSession] + Name=abcdefghijklmnop + Name=123 + Name=all + Name=a:b + Name=a/b + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt new file mode 100644 index 00000000..daa73eba --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseL2tpTunnelLocalAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Local=192.168.30.100 + Local=2001:db8::1 + Local=auto + Local=static + Local=dynamic + Local=auto@eth0 + Local=dynamic@test1 + Local=192.168.30.100@eth0 + Local=auto@@eth0 + Local=auto@ + Local=static@a-very-long-alternative-interface-name + Local=@auto + Local=@@192.168.30.100@eth0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Local=any + Local=auto@all + Local=auto@eth/0 + Local=192.168.30.256 + Local=auto@123 + Local=@ + Local=Auto + Local=STATIC@eth0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt new file mode 100644 index 00000000..6d7d654d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseL2tpTunnelRemoteAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Remote=192.168.30.101 + Remote=2001:db8::2 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Remote=auto + Remote=192.168.30 + Remote=peer.example.com + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkGroupOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkGroupOptionValueTest.kt new file mode 100644 index 00000000..b1df989c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkGroupOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseLinkGroupOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Group=0 + Group=2147483647 + Group=0x10 + Group=010 + Group=0b101 + Group=0o17 + Group=+5 + Group= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Group=-1 + Group=2147483648 + Group=root + Group=08 + Group=1.5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkJournalOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkJournalOptionValueTest.kt new file mode 100644 index 00000000..77b88f28 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkJournalOptionValueTest.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseLinkJournalOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // parse_link_journal's names plus parse_boolean (case-insensitive). + // language="unit file (systemd)" + val file = """ + [Exec] + LinkJournal=no + LinkJournal=auto + LinkJournal=guest + LinkJournal=host + LinkJournal=try-guest + LinkJournal=try-host + LinkJournal=yes + LinkJournal=false + LinkJournal=0 + LinkJournal=On + LinkJournal=Yes + LinkJournal=TRUE + LinkJournal=oFf + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Unknown mode; the names are compared with streq(). + // language="unit file (systemd)" + val file = """ + [Exec] + LinkJournal=try-auto + LinkJournal=Guest + LinkJournal=persistent + LinkJournal=AUTO + LinkJournal=Try-Host + LinkJournal=nothing + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLldpMulticastModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLldpMulticastModeOptionValueTest.kt new file mode 100644 index 00000000..8ea15587 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLldpMulticastModeOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseLldpMulticastModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + EmitLLDP=nearest-bridge + EmitLLDP=non-tpmr-bridge + EmitLLDP=customer-bridge + EmitLLDP=yes + EmitLLDP=no + EmitLLDP=On + EmitLLDP=Yes + EmitLLDP=TRUE + EmitLLDP=oFf + EmitLLDP= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + EmitLLDP=nearest + EmitLLDP=Customer-Bridge + EmitLLDP=routers-only + EmitLLDP=Nearest-Bridge + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLongOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLongOptionValueTest.kt new file mode 100644 index 00000000..aa4c466f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLongOptionValueTest.kt @@ -0,0 +1,75 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseLongOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import org.junit.Test + +class ConfigParseLongOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // safe_atoli: signed long, base 0 plus the 0b/0o prefixes mangle_base adds. strtoll starts afresh + // after the prefix, so a sign or whitespace may follow it (the last five confirmed accepted one per + // file with sd-check at 0f0682b773). + // language="unit file (systemd)" + val file = """ + [Socket] + MessageQueueMaxMessages=10 + MessageQueueMaxMessages=0 + MessageQueueMaxMessages=-5 + MessageQueueMaxMessages=+5 + MessageQueueMaxMessages=0x10 + MessageQueueMaxMessages=-0x10 + MessageQueueMaxMessages=010 + MessageQueueMaxMessages=0b101 + MessageQueueMaxMessages=0o17 + MessageQueueMaxMessages=9223372036854775807 + MessageQueueMaxMessages=-9223372036854775808 + MessageQueueMaxMessages=0b-1 + MessageQueueMaxMessages=0o-7 + MessageQueueMaxMessages=0b -1 + MessageQueueMaxMessages=0b+1 + MessageQueueMaxMessages=0b 1 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testVerticalTabSpellings() { + // strtoll skips a leading \v (C isspace); sd-check at 0f0682b773 accepts both. The plugin's lexer does + // not take a raw \v, so this checks the grammar directly on both engines. + val grammar = ConfigParseLongOptionValue().combinator + for (engine in listOf(GrammarEngineVerdict::classic, GrammarEngineVerdict::parseEngine)) { + assertEquals(GrammarEngineVerdict.VALID, engine(grammar, "\u000B5")) + assertEquals(GrammarEngineVerdict.VALID, engine(grammar, "\u000B-1")) + } + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + MessageQueueMaxMessages=ten + MessageQueueMaxMessages=08 + MessageQueueMaxMessages=0x + MessageQueueMaxMessages=9223372036854775808 + MessageQueueMaxMessages=-0b1 + MessageQueueMaxMessages=10K + MessageQueueMaxMessages=1.5 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLuoSessionsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLuoSessionsOptionValueTest.kt new file mode 100644 index 00000000..ddecad52 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLuoSessionsOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseLuoSessionsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + LUOSession=mysession + LUOSession=vm-memory vm.state cache_1 + LUOSession=a]b + LUOSession= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + LUOSession=foo:bar + LUOSession=foo* + LUOSession="quoted" + LUOSession=what? + LUOSession=ok bad[1] + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt new file mode 100644 index 00000000..54f08a4c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMacAddressPolicyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + MACAddressPolicy=persistent + MACAddressPolicy=random + MACAddressPolicy=none + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + MACAddressPolicy=Persistent + MACAddressPolicy=keep + MACAddressPolicy=yes + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt new file mode 100644 index 00000000..97c1be19 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMacsecKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyFile=/etc/systemd/network/macsec.key + [MACsecReceiveAssociation] + KeyFile=/run/keys/rx key.hex + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyFile=macsec.key + KeyFile=/sys/kernel/key + [MACsecReceiveAssociation] + KeyFile=/etc/keys/../secret + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt new file mode 100644 index 00000000..7be9e45d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMacsecKeyIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyId=01 + KeyId=0203 + KeyId=0203040506070809 + KeyId=ABCDEF + KeyId=00112233445566778899aabbccddeeff + KeyId=01 02 03 + [MACsecReceiveAssociation] + KeyId=020304 + KeyId=ff + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyId=1 + KeyId=255 + KeyId=0 + KeyId=0x01 + KeyId=00112233445566778899aabbccddeeff00 + [MACsecReceiveAssociation] + KeyId=zz + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt new file mode 100644 index 00000000..31e08f81 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt @@ -0,0 +1,76 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_match_property: NAME=VALUE words with CUNESCAPE|UNQUOTE. */ +class ConfigParseMatchPropertyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Match] + Property=ID_MODEL_ID=9999 "ID_VENDOR_FROM_DATABASE=vendor name" "KEY=with \"quotation\"" + Property=HOGE=foo BAR=baz + Property=ID_NET_MANAGED_BY=io.systemd.Network + Property=!ID_BUS=usb + Property=! ID_BUS=usb + Property=! + Property=EMPTY= + Property=KEY="value in quotes" + Property=_UNDERSCORE1=x\x41 + Property=GLOB=foo* + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Property=NOEQUALS + Property=1BAD=x + Property=BAD-NAME=x + Property="KEY=unterminated + Property=KEY=bad\qescape + Property=GOOD=1 !BAD=2 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } + @Test + fun testValidValuesLink() { + // language="unit file (systemd)" + val file = """ + [Match] + Property=ID_PATH=pci-0000:00:1f.6 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesLink() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Property==value + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt new file mode 100644 index 00000000..9d58d26d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_match_strv: glob lists with only quote balance checked. */ +class ConfigParseMatchStrvOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Match] + Path=pci-0000:02:00.0-* + Driver=dummy + Driver=!e1000e igb + Type=ether wlan + Type=!loopback + Kind=dummy veth + Kind=! + WLANInterfaceType=ap station + SSID="My Home Wifi" 'Other Net' + SSID=Cafe\ Wifi + SSID=pre"fix mid"dle + Path= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + SSID="unterminated + Kind=bond 'veth + Driver=a"b + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } + @Test + fun testValidValuesLink() { + // language="unit file (systemd)" + val file = """ + [Match] + Path=pci-0000:00:1f.6 + Driver=e1000e + Type=ether + Kind=!* + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesLink() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Path="pci-0000 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMountImagesOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMountImagesOptionValueTest.kt new file mode 100644 index 00000000..4ff391b6 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMountImagesOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMountImagesOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + MountImages=/tmp/app0.raw:/var/tmp:noatime,nosuid + MountImages=/tmp/app0.raw:/var/tmp:nosuid + MountImages=-/srv/foo.raw:/srv/foo + MountImages=/srv/foo.raw:/srv/foo:root:ro,nosuid:usr:nodev + MountImages=/srv/foo.raw:/srv/foo:root:ro:nosuid + MountImages=/srv/foo.raw:/srv/foo:root: + MountImages=/srv/foo.raw::/srv/foo + MountImages=/dev/sda1:/mnt/disk + MountImages=%S/img.raw:%t/img + MountImages=/srv/a\:b.raw:/srv/c /srv/d.raw:/srv/e:ro + MountImages=/srv/foo.raw:/srv/foo:rootcontext=x + MountImages= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + MountImages=/srv/foo.raw + MountImages=/srv/foo.raw: + MountImages=relative.raw:/srv/foo + MountImages=/srv/foo.raw:relative + MountImages=/srv/foo.raw:/proc/foo + MountImages=/sys/foo.raw:/srv/foo + MountImages=/srv/foo.raw:/srv/foo:bogus:ro + MountImages=/srv/foo.raw:/srv/../foo + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMudUrlOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMudUrlOptionValueTest.kt new file mode 100644 index 00000000..176ab8e1 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMudUrlOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMudUrlOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + MUDURL=https://www.example.com/mud.json + MUDURL=http://example.com/a b + MUDURL=https://example.com/\x41 + MUDURL= + [DHCPv6] + MUDURL=https://example.com/ + [LLDP] + MUDURL=http://x + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + MUDURL=ftp://example.com/mud.json + MUDURL=https:// + MUDURL=HTTPS://example.com/ + [DHCPv6] + MUDURL=https://example.com/café + [LLDP] + MUDURL=https://example.com/\q + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt new file mode 100644 index 00000000..67dcf434 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_ndisc_route_metric. */ +class ConfigParseNdiscRouteMetricOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AcceptRA] + RouteMetric=256 + RouteMetric=512:1024:2048 + RouteMetric=100:200:300 + RouteMetric=0 + RouteMetric=0x10:0x20:0x30 + RouteMetric=100: 200: 300 + RouteMetric=100:200:300: + RouteMetric=0b 1 + RouteMetric=0b1:0b 10:0b11 + [DHCPv6] + RouteMetric=1024 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1 and 1:+0b10:3, and accepts 0b 1 and 0b1:0b 10:0b11. + // language="unit file (systemd)" + val file = """ + [IPv6AcceptRA] + RouteMetric=hoge + RouteMetric=300:200:100 + RouteMetric=100:100:200 + RouteMetric=1:2 + RouteMetric=1:2:3:4 + RouteMetric=1::3 + RouteMetric=1:2:3:: + RouteMetric=100 :200:300 + RouteMetric=+0b1 + RouteMetric=1:+0b10:3 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt new file mode 100644 index 00000000..0a98a141 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Neighbor] Address= (NEIGHBOR_DESTINATION_ADDRESS). */ +class ConfigParseNeighborDestinationAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Neighbor] + Address=192.168.10.1 + Address=2004:da8:1:0::1 + Address=0.0.0.0 + Address=fe80::1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Neighbor] + Address=192.168.10.1/24 + Address=not-an-address + Address=256.1.1.1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt new file mode 100644 index 00000000..fa88ef74 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_neighbor_proxy_address. */ +class ConfigParseNeighborProxyAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + ProxyNeighbor=192.168.1.10 + ProxyNeighbor=2001:db8::1 + ProxyNeighbor=::ffff:10.0.0.1 + ProxyNeighbor=255.255.255.254 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + ProxyNeighbor=0.0.0.0 + ProxyNeighbor=:: + ProxyNeighbor=224.0.0.1 + ProxyNeighbor=ff02::1 + ProxyNeighbor=255.255.255.255 + ProxyNeighbor=192.168.1.0/24 + ProxyNeighbor=fe80::1%eth0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemDelayOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemDelayOptionValueTest.kt new file mode 100644 index 00000000..b9573441 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemDelayOptionValueTest.kt @@ -0,0 +1,100 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNetemDelayOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [NetworkEmulator] + DelaySec=10ms + DelayJitterSec=10ms + DelaySec=200ms + DelayJitterSec=200ms + DelaySec=1min30s + DelayJitterSec=1min30s + DelaySec=5s 500ms + DelayJitterSec=5s 500ms + DelaySec=5s500ms + DelayJitterSec=5s500ms + DelaySec=.5s + DelayJitterSec=.5s + DelaySec=+3.1s + DelayJitterSec=+3.1s + DelaySec=3.1s.2 + DelayJitterSec=3.1s.2 + DelaySec=3.1 .2 + DelayJitterSec=3.1 .2 + DelaySec=1.5h + DelayJitterSec=1.5h + DelaySec=23us + DelayJitterSec=23us + DelaySec=23µs + DelayJitterSec=23µs + DelaySec=2 weeks + DelayJitterSec=2 weeks + DelaySec=100 + DelayJitterSec=100 + # Largest finite values parse_time() accepts (total < USEC_INFINITY, each integer <= LLONG_MAX). + DelaySec=9223372036854775807us 9223372036854775807us + DelayJitterSec=9223372036854775807us 9223372036854775807us + DelaySec=18446744073708s + DelayJitterSec=18446744073708s + DelaySec=infinity + DelayJitterSec=infinity + DelaySec= + DelayJitterSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [NetworkEmulator] + DelaySec=5. + DelayJitterSec=5. + DelaySec=-5s + DelayJitterSec=-5s + DelaySec=3.s + DelayJitterSec=3.s + DelaySec=12.34.56 + DelayJitterSec=12.34.56 + DelaySec=5mins + DelayJitterSec=5mins + DelaySec=10 foo + DelayJitterSec=10 foo + DelaySec=infinity 5s + DelayJitterSec=infinity 5s + DelaySec=Infinity + DelayJitterSec=Infinity + # parse_time() ERANGE: integer above LLONG_MAX, or total reaching USEC_INFINITY. + DelaySec=9223372036854775808us + DelayJitterSec=9223372036854775808us + DelaySec=18446744073709551615us + DelayJitterSec=18446744073709551615us + DelaySec=9223372036854775807us 9223372036854775807us 1us + DelayJitterSec=9223372036854775807us 9223372036854775807us 1us + DelaySec=18446744073709s + DelayJitterSec=18446744073709s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(24, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemRateOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemRateOptionValueTest.kt new file mode 100644 index 00000000..5a4ca6ee --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemRateOptionValueTest.kt @@ -0,0 +1,94 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNetemRateOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts the 0b/0o spellings: the integer part goes through safe_atoi(). + // language="unit file (systemd)" + val file = """ + [NetworkEmulator] + LossRate=20% + DuplicateRate=20% + LossRate=0% + DuplicateRate=0% + LossRate=100% + DuplicateRate=100% + LossRate=55.5% + DuplicateRate=55.5% + LossRate=55.53% + DuplicateRate=55.53% + LossRate=555.5‰ + DuplicateRate=555.5‰ + LossRate=1000‰ + DuplicateRate=1000‰ + LossRate=1000‱ + DuplicateRate=1000‱ + LossRate=0x10% + DuplicateRate=0x10% + LossRate=0b 1% + DuplicateRate=0b 1% + LossRate=0b+1% + DuplicateRate=0b+1% + LossRate=0b-0% + DuplicateRate=0b-0% + LossRate=-0% + DuplicateRate=-0% + LossRate=0b 1.5% + DuplicateRate=0b 1.5% + LossRate=0o 7‰ + DuplicateRate=0o 7‰ + LossRate= + DuplicateRate= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects 0b-1% and 0b -1% (the integer part is -1: ERANGE). + // language="unit file (systemd)" + val file = """ + [NetworkEmulator] + LossRate=20 + DuplicateRate=20 + LossRate=107% + DuplicateRate=107% + LossRate=-7% + DuplicateRate=-7% + LossRate=3.212% + DuplicateRate=3.212% + LossRate=3.22‰ + DuplicateRate=3.22‰ + LossRate=3.2‱ + DuplicateRate=3.2‱ + LossRate=abc% + DuplicateRate=abc% + LossRate=% + DuplicateRate=% + LossRate=20 % + DuplicateRate=20 % + LossRate=0b-1% + DuplicateRate=0b-1% + LossRate=0b -1% + DuplicateRate=0b -1% + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(22, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetworkZoneOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetworkZoneOptionValueTest.kt new file mode 100644 index 00000000..33ccd0e2 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetworkZoneOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNetworkZoneOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // `vz-` + name must be a valid ifname: up to 12 characters; digits-only is fine once prefixed. + // language="unit file (systemd)" + val file = """ + [Network] + Zone=sd-zone0 + Zone=foo + Zone=abcdefghijkl + Zone=123 + Zone=a.b + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // 13 characters is too long; `/`, space and `%` are not ifname characters. + // language="unit file (systemd)" + val file = """ + [Network] + Zone=abcdefghijklm + Zone=foo/bar + Zone=my zone + Zone=a%d + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt new file mode 100644 index 00000000..ef45133c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_nft_set(NFT_SET_PARSE_NETWORK) and [Address] NFTSet=. */ +class ConfigParseNftSetNetworkOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Address] + NFTSet=address:inet:sd_test:addr4 prefix:inet:sd_test:network4 ifindex:inet:sd_test:ifindex + NFTSet=prefix:netdev:filter:eth_ipv4_prefix + NFTSet="address:ip6:my.table:set/one" + [DHCPv4] + NFTSet=address:ip:filter:dhcp4 + [DHCPv6] + NFTSet=prefix:ip6:filter:dhcp6_prefix + [IPv6AcceptRA] + NFTSet=address:inet:t:s + [DHCPPrefixDelegation] + NFTSet=prefix:bridge:t_1:s_2 + NFTSet=address::inet:t:s + NFTSet=:address:inet:t:s + NFTSet=address:inet:t:s: + NFTSet=address:"inet":t:s + NFTSet='address':inet:"t":s prefix:ip:t:s + NFTSet=address:inet:"t:s" + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + NFTSet=cgroup:inet:filter:set + NFTSet=address:ipx:filter:set + NFTSet=address:inet:1table:set + NFTSet=address:inet:table + NFTSet=address:inet:table:set:extra + [DHCPv4] + NFTSet=address:inet:filter:bad-name + NFTSet=address:"inet:t + NFTSet=address:"ipx":t:s + NFTSet="address:inet:t:s:extra" + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNtpOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNtpOptionValueTest.kt new file mode 100644 index 00000000..1de65ec4 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNtpOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNtpOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + NTP=0.fedora.pool.ntp.org 1.fedora.pool.ntp.org + NTP=192.168.5.1 + NTP=2001:db8::123 + NTP=ntp.example.com. + NTP=time + NTP=. + NTP=foo\.bar + NTP=foo\qbar + NTP=foo\065bar + NTP=foo\\\\bar + NTP=foo\\.bar + NTP=foo\\065bar + NTP=localhost.localdomain + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + NTP=pool..ntp.org + NTP=.ntp.org + NTP=ok.example bad..example + NTP=foo\\bar + NTP=foo\\256bar + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNumaMaskOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNumaMaskOptionValueTest.kt new file mode 100644 index 00000000..c69f15c4 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNumaMaskOptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNumaMaskOptionValueTest : AbstractUnitFileTest() { + + // Every value below was checked with sd-check (systemd 0f0682b773). parse_range() cuts each word at its first + // '-', so the upper bound may be "-0" or "+1" ("0--0", "0-+1") while "-0" alone has an empty lower bound and + // "0b-0" has the lower bound "0b" (both "Failed to parse CPU range"). + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + NUMAMask=0 + NUMAMask=all + NUMAMask=0-3 + NUMAMask=0,2,4-7 + NUMAMask=0 1 2 + NUMAMask=0, 1,,2 + NUMAMask=0x10 010 0b11 0o7 + NUMAMask=8191 + NUMAMask="0-3" + NUMAMask=%i + NUMAMask= + NUMAMask=0b+1 + NUMAMask=0-0b+1 + NUMAMask=0--0 + NUMAMask=0-+1 + NUMAMask=+1 + NUMAMask=0-0b-0 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + NUMAMask=8192 + NUMAMask=0-8192 + NUMAMask=-1 + NUMAMask=1- + NUMAMask=all 1 + NUMAMask=ALL + NUMAMask=foo + NUMAMask=08 + NUMAMask=+0b1 + NUMAMask=-0 + NUMAMask=0b-0 + NUMAMask=1-2-3 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseObsoleteUnitDepsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseObsoleteUnitDepsOptionValueTest.kt new file mode 100644 index 00000000..f0346af7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseObsoleteUnitDepsOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** + * RequiresOverridable=/RequisiteOverridable= (config_parse_obsolete_unit_deps) forward to + * config_parse_unit_deps, so they share the unit-dependency validator with Requires=/Requisite=. + */ +class ConfigParseObsoleteUnitDepsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Unit] + RequiresOverridable=foo.service + RequiresOverridable=foo.service bar.socket + RequisiteOverridable=network-online.target + RequisiteOverridable=getty@tty1.service + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Unit] + RequiresOverridable=foo + RequisiteOverridable=foo.notatype + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOomScoreAdjustOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOomScoreAdjustOptionValueTest.kt new file mode 100644 index 00000000..5c4b605c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOomScoreAdjustOptionValueTest.kt @@ -0,0 +1,80 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseOomScoreAdjustOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import org.junit.Assert.assertEquals +import org.junit.Test + +class ConfigParseOomScoreAdjustOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // safe_atoi reads base 0 after mangle_base: sign, hex, octal, 0o and 0b all work; range is -1000..1000. + // language="unit file (systemd)" + val file = """ + [Exec] + OOMScoreAdjust=32 + OOMScoreAdjust=-1000 + OOMScoreAdjust=1000 + OOMScoreAdjust=0 + OOMScoreAdjust=+5 + OOMScoreAdjust=0x10 + OOMScoreAdjust=-0x3e8 + OOMScoreAdjust=0o17 + OOMScoreAdjust=0b101 + OOMScoreAdjust=017 + OOMScoreAdjust=-0 + OOMScoreAdjust=0b-1 + OOMScoreAdjust=0o+120 + OOMScoreAdjust=0O-1750 + OOMScoreAdjust=0b 1 + OOMScoreAdjust=0o -7 + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Out of range (also when spelled in hex), not a number, a sign defeating mangle_base's 0b prefix, and + // whitespace strtol() does not skip (after a sign, after 0x, between a sign and the digits). + // language="unit file (systemd)" + val file = """ + [Exec] + OOMScoreAdjust=1001 + OOMScoreAdjust=-1001 + OOMScoreAdjust=abc + OOMScoreAdjust=0x3e9 + OOMScoreAdjust=1.5 + OOMScoreAdjust=+0b1 + OOMScoreAdjust=0b-1111101001 + OOMScoreAdjust=- 5 + OOMScoreAdjust=0x 5 + OOMScoreAdjust=0b - 1 + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(10, highlights) + } + + @Test + fun testLeadingVerticalTabAndFormFeed() { + // config_parse strips " \t\n\r" but not \v or \f, and strtol() skips them, so sd-check accepts + // OOMScoreAdjust=\v5, \v-5 and \f-1000. The editor's lexer does not take \v right after `=`, so these are + // checked on the grammar directly, on both engines. + val grammar = ConfigParseOomScoreAdjustOptionValue().combinator + for (value in listOf("\u000B5", "\u000B-5", "\u000C-1000")) { + assertEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(grammar, value)) + assertEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(grammar, value)) + } + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOpenFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOpenFileOptionValueTest.kt new file mode 100644 index 00000000..14244208 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOpenFileOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseOpenFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST(open_file_parse) (src/test/test-open-file.c) plus escape and comma edge cases. + // language="unit file (systemd)" + val file = """ + [Service] + OpenFile=/proc/1/ns/mnt:host-mount-namespace:read-only + OpenFile=/proc/1/ns/mnt + OpenFile=/proc/1/ns/mnt:host-mount-namespace + OpenFile=/proc/1/ns/mnt::read-only + OpenFile=/proc/1/ns/mnt:host-mount-namespace:append + OpenFile=/proc/1/ns/mnt:host-mount-namespace:truncate + OpenFile=/proc/1/ns/mnt:host-mount-namespace:graceful + OpenFile=/proc/1/ns/mnt:host-mount-namespace:read-only,graceful + OpenFile=/run/a\x3ab:name + OpenFile=/run/with space/file:my name + OpenFile=/run/x: + OpenFile=/run/x:: + OpenFile=/etc/foo:fd:read-only: + OpenFile=/run/x:fd:: + OpenFile=/run/x::,read-only,,graceful, + OpenFile=/run/x:fd:graceful,truncate + OpenFile=/run/x:fd:truncate,graceful + OpenFile=/run/x:fd:graceful,read-only + OpenFile=/run/x:fd:,,, + OpenFile=/run/%i + OpenFile=/run/../x + OpenFile= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // The last five are refused by open_file_parse/open_file_validate (repeated flag, or more than one of + // read-only/append/truncate); confirmed with systemd's own parser at 0f0682b773 (sd-check). + // language="unit file (systemd)" + val file = """ + [Service] + OpenFile=../file.dat:file:read-only + OpenFile=/proc/1/ns/mnt:host-mount-namespace:rw + OpenFile=/proc/1/ns/mnt:host-mount-namespace:read-only:other + OpenFile=relative + OpenFile=/run/x:name:read-only append + OpenFile=/run/x\q:name + OpenFile=/run/x:name:read-only:: + OpenFile=/run/x:fd:read-only,read-only + OpenFile=/run/x:fd:read-only,append + OpenFile=/run/x:fd:graceful,graceful + OpenFile=/run/x:fd:append,graceful,truncate + OpenFile=/some/path/:some-name:append,graceful,read-only,truncate + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOverlayOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOverlayOptionValueTest.kt new file mode 100644 index 00000000..788bc280 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOverlayOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseOverlayOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN and systemd-nspawn(1): an empty upper directory, relative sources, `+` prefixes, escaped colons. + // language="unit file (systemd)" + val file = """ + [Files] + Overlay=+/var::/var + Overlay=/tmp/1:/tmp/2:/tmp/3:/var + OverlayReadOnly=/tmp/1:/tmp/2:/tmp/3:/var + Overlay=/lower:/upper + Overlay=lower:upper + Overlay=/l1:/l2::/dest + OverlayReadOnly=+/usr:/tmp/upper:/usr + Overlay=/a\:b:/c + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Fewer than two paths, empty lower directories, and a destination that is not absolute + // (with three or more paths the destination keeps any `+`, so `+/c` is not absolute). + // language="unit file (systemd)" + val file = """ + [Files] + Overlay=/only + Overlay=/a: + Overlay=:/b + Overlay=/a:/b:relative + Overlay=/a::/b:/c + OverlayReadOnly=/a:/b:+/c + Overlay=+x:/b + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePathOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePathOptionValueTest.kt new file mode 100644 index 00000000..ecfe59e8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePathOptionValueTest.kt @@ -0,0 +1,65 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePathOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // config_parse_path: literal absolute path, no '..' component, no API VFS restriction. + // language="unit file (systemd)" + val file = """ + [Exec] + WorkingDirectory=/ + WorkingDirectory=/root + WorkingDirectory=/home/user/work dir + WorkingDirectory=/srv//data/./x + WorkingDirectory=/..foo/bar.. + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Exec] + WorkingDirectory=root + WorkingDirectory=/srv/../etc + WorkingDirectory=.. + WorkingDirectory=%h/work + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } + + @Test + fun testNamespacePath() { + // The [Network] key shares config_parse_path; /proc is fine here (no PATH_CHECK_NON_API_VFS). + // language="unit file (systemd)" + val file = """ + [Network] + NamespacePath=/run/netns/foo + NamespacePath=/proc/1/ns/net + NamespacePath=netns/foo + NamespacePath=/run/netns/../foo + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePidFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePidFileOptionValueTest.kt new file mode 100644 index 00000000..148f9628 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePidFileOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePidFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Absolute and relative paths (relative ones are placed under /run), specifiers, and /var/run (rewritten, not refused). + // language="unit file (systemd)" + val file = """ + [Service] + PIDFile=/run/foo.pid + PIDFile=foo.pid + PIDFile=nginx/nginx.pid + PIDFile=./foo.pid + PIDFile=/var/run/foo.pid + PIDFile=%t/foo.pid + PIDFile=/run/%i.pid + PIDFile=/devices/foo.pid + PIDFile= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + // PidFileOptionWarning adds an informational note on every PIDFile= line; only value problems count here. + val highlights = myFixture.doHighlighting().filter { it.description?.startsWith("PID files should be avoided") != true } + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + PIDFile=../foo.pid + PIDFile=/run/../foo.pid + PIDFile=/run/foo/.. + PIDFile=/dev/foo.pid + PIDFile=/proc/self/foo.pid + PIDFile=/sys + PIDFile=/run/%e.pid + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + // PidFileOptionWarning adds an informational note on every PIDFile= line; only value problems count here. + val highlights = myFixture.doHighlighting().filter { it.description?.startsWith("PID files should be avoided") != true } + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePiePacketLimitOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePiePacketLimitOptionValueTest.kt new file mode 100644 index 00000000..298306d2 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePiePacketLimitOptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePiePacketLimitOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [PIE] + PacketLimit=0 + PacketLimit=1 + PacketLimit=20480 + PacketLimit=4294967295 + PacketLimit=0x10 + PacketLimit=0o17 + PacketLimit=0b101 + PacketLimit=017 + PacketLimit=+5 + PacketLimit=-0 + PacketLimit=0b+1 + PacketLimit=0b-0 + PacketLimit=0b 1 + PacketLimit=0o 7 + PacketLimit=0b +1 + PacketLimit=0b -0 + PacketLimit= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [PIE] + PacketLimit=4294967296 + PacketLimit=-1 + PacketLimit=08 + PacketLimit=0x + PacketLimit=1.5 + PacketLimit=abc + PacketLimit=+0b1 + PacketLimit=12 13 + PacketLimit=0b-1 + PacketLimit=0b -1 + PacketLimit=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePivotRootOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePivotRootOptionValueTest.kt new file mode 100644 index 00000000..f58c8cf1 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePivotRootOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePivotRootOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Absolute, normalized paths; a trailing slash and dot-prefixed names are fine. + // language="unit file (systemd)" + val file = """ + [Exec] + PivotRoot=/sysroot + PivotRoot=/sysroot/ostree/deploy:/sysroot + PivotRoot=/new/:/old/ + PivotRoot=/new: + PivotRoot=/ + PivotRoot=/a/.hidden:/b/..c + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // path_is_normalized refuses `//`, `.` and `..` components in either path; both must be absolute. + // language="unit file (systemd)" + val file = """ + [Exec] + PivotRoot=relative + PivotRoot=/a//b + PivotRoot=/a/./b + PivotRoot=/a/../b + PivotRoot=/new:relative + PivotRoot=/new:/old//x + PivotRoot=:/old + PivotRoot=/a/. + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt new file mode 100644 index 00000000..0be26b67 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePortOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Port=tp + Port=aui + Port=mii + Port=fibre + Port=bnc + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Port=fiber + Port=TP + Port=da + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt new file mode 100644 index 00000000..96e428c8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePortRangeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + PortRange=1-65535 + PortRange=4789 + PortRange=0x100-0x200 + PortRange=32768-61000 + PortRange=1000- 2000 + PortRange=0b 1-5 + PortRange=0b+1-5 + PortRange=1-0b 1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + PortRange=0-100 + PortRange=1-65536 + PortRange=100:200 + PortRange=-5 + PortRange=5- + PortRange=1000 -2000 + PortRange=+0b1-2 + PortRange=-0b0-2 + PortRange=5--0 + PortRange=-0-5 + PortRange=0b-0 + PortRange=1-0b -1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt new file mode 100644 index 00000000..b283ebe1 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_pref64_prefix_lifetime: at most 8191 * 8s, not infinity. */ +class ConfigParsePref64PrefixLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6PREF64Prefix] + LifetimeSec=1800 + LifetimeSec=0 + LifetimeSec=65528 + LifetimeSec=18h 12min 8s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6PREF64Prefix] + LifetimeSec=65529 + LifetimeSec=infinity + LifetimeSec=1d + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt new file mode 100644 index 00000000..551a212f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [IPv6Prefix] Token= (config_parse_prefix_token -> config_parse_address_generation_type). */ +class ConfigParsePrefixTokenOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6Prefix] + Token=eui64 + Token=::1a:2b:3c:4d + Token=static:::fa:de:ca:fe + Token=prefixstable + Token=prefixstable:2002:da8:1::,86b123b969ba4b7eb8b3d8605123525a + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6Prefix] + Token=prefixstable@ + Token=prefixstable,00000000000000000000000000000000 + Token=static: + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt new file mode 100644 index 00000000..29e94048 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt @@ -0,0 +1,83 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePrivateBpfOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + PrivateBPF=yes + PrivateBPF=no + PrivateBPF=true + PrivateBPF=false + PrivateBPF=on + PrivateBPF=off + PrivateBPF=1 + PrivateBPF=0 + PrivateBPF=y + PrivateBPF=n + PrivateBPF=t + PrivateBPF=f + PrivateBPF=Yes + PrivateBPF=TRUE + PrivateBPF=oFf + PrivateBPF=fAlSe + PrivateBPF=Y + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + PrivateBPF=yes + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + PrivateBPF=no + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + PrivateBPF=off + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + PrivateBPF=maybe + PrivateBPF=2 + PrivateBPF=private + PrivateBPF=yes no + PrivateBPF=Auto + PrivateBPF=Private + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseQfqMaxPacketOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseQfqMaxPacketOptionValueTest.kt new file mode 100644 index 00000000..5fd5c2b8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseQfqMaxPacketOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseQfqMaxPacketOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [QuickFairQueueingClass] + MaxPacketBytes=512 + MaxPacketBytes=16000 + MaxPacketBytes=8000 + MaxPacketBytes=65536 + MaxPacketBytes=64K + MaxPacketBytes=1K + MaxPacketBytes=0.5K + MaxPacketBytes=1K 512B + MaxPacketBytes=63K1023B + MaxPacketBytes= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [QuickFairQueueingClass] + MaxPacketBytes=511 + MaxPacketBytes=65537 + MaxPacketBytes=65K + MaxPacketBytes=1M + MaxPacketBytes=0 + MaxPacketBytes=16000b + MaxPacketBytes=abc + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRadvDnsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRadvDnsOptionValueTest.kt new file mode 100644 index 00000000..50460140 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRadvDnsOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRadvDnsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + DNS=_link_local 2002:da8:1:0::1 + DNS=2001:db8:1:1::2 + DNS=fe80::1 fe80::2 + DNS=::ffff:192.0.2.1 + DNS= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + DNS=192.168.1.1 + DNS=[2001:db8::1]:53 + DNS=_link_locals + DNS=2001:db8::1%eth0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRequiredForOnlineOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRequiredForOnlineOptionValueTest.kt new file mode 100644 index 00000000..09199dbb --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRequiredForOnlineOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRequiredForOnlineOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RequiredForOnline=routable + RequiredForOnline=no + RequiredForOnline=yes + RequiredForOnline=False + RequiredForOnline=Yes + RequiredForOnline=TRUE + RequiredForOnline=oFf + RequiredForOnline=degraded-carrier + RequiredForOnline=carrier:routable + RequiredForOnline=off: + RequiredForOnline=:degraded + RequiredForOnline=no-carrier + RequiredForOnline=enslaved + RequiredForOnline= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RequiredForOnline=online + RequiredForOnline=: + RequiredForOnline=routable:bogus + RequiredForOnline=carrier:routable:off + RequiredForOnline=Routable + RequiredForOnline=No-Carrier + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolvConfOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolvConfOptionValueTest.kt new file mode 100644 index 00000000..879ed538 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolvConfOptionValueTest.kt @@ -0,0 +1,65 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseResolvConfOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Every resolv_conf_mode_table entry plus booleans (case-insensitive). + // language="unit file (systemd)" + val file = """ + [Exec] + ResolvConf=off + ResolvConf=copy-host + ResolvConf=copy-static + ResolvConf=copy-uplink + ResolvConf=copy-stub + ResolvConf=replace-host + ResolvConf=replace-static + ResolvConf=replace-uplink + ResolvConf=replace-stub + ResolvConf=bind-host + ResolvConf=bind-static + ResolvConf=bind-uplink + ResolvConf=bind-stub + ResolvConf=delete + ResolvConf=auto + ResolvConf=yes + ResolvConf=no + ResolvConf=True + ResolvConf=OFF + ResolvConf=Yes + ResolvConf=TRUE + ResolvConf=oFf + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Not a table entry; table entries are case-sensitive. + // language="unit file (systemd)" + val file = """ + [Exec] + ResolvConf=copy + ResolvConf=Copy-Host + ResolvConf=bind-hosts + ResolvConf=AUTO + ResolvConf=offline + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolveSupportOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolveSupportOptionValueTest.kt new file mode 100644 index 00000000..4a73ffd2 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolveSupportOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseResolveSupportOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + LLMNR=resolve + LLMNR=yes + LLMNR=no + LLMNR=Yes + LLMNR=0 + MulticastDNS=resolve + MulticastDNS=true + MulticastDNS=OFF + MulticastDNS=TRUE + MulticastDNS=oFf + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + LLMNR=Resolve + MulticastDNS=RESOLVE + LLMNR=maybe + MulticastDNS=allow-downgrade + MulticastDNS=yes no + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictAddressFamiliesOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictAddressFamiliesOptionValueTest.kt new file mode 100644 index 00000000..735b2bd3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictAddressFamiliesOptionValueTest.kt @@ -0,0 +1,74 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRestrictAddressFamiliesOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.deprecatedTokens +import org.junit.Test + +class ConfigParseRestrictAddressFamiliesOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN (--restrict-address-families=): with or without AF_, any case, comma or space separated, + // quoted words, and a bare `~`. + // language="unit file (systemd)" + val file = """ + [Exec] + RestrictAddressFamilies=unix,inet + RestrictAddressFamilies=unix inet + RestrictAddressFamilies=AF_UNIX AF_INET + RestrictAddressFamilies=~netlink + RestrictAddressFamilies=none + RestrictAddressFamilies=unix,inet,netlink + RestrictAddressFamilies=unix inet AF_NETLINK + RestrictAddressFamilies=~packet + RestrictAddressFamilies=~ + RestrictAddressFamilies=AF_INET, AF_INET6 + RestrictAddressFamilies=af_inet6 + RestrictAddressFamilies=~ AF_UNIX + RestrictAddressFamilies="AF_INET" 'inet6' + RestrictAddressFamilies=AF_DECnet decnet + RestrictAddressFamilies=Unix INET6 Af_Netlink + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Unknown family, `~` only allowed at the start, `none` only allowed on its own (and compared with streq()). + // language="unit file (systemd)" + val file = """ + [Exec] + RestrictAddressFamilies=AF_BOGUS + RestrictAddressFamilies=inet ~unix + RestrictAddressFamilies=~none + RestrictAddressFamilies=none inet + RestrictAddressFamilies=AF-INET + RestrictAddressFamilies=None + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + + @Test + fun testRemovedFamiliesAreDeprecatedInEitherSpelling() { + // af_from_name still resolves kernel-removed families, so they are valid but flagged as obsolete, + // whether written with the AF_ prefix or without it and in any case. + val grammar = ConfigParseRestrictAddressFamiliesOptionValue().combinator + val deprecated = grammar.deprecatedTokens("inet,AF_DECnet irda") + assertEquals(2, deprecated.size) + assertTrue(deprecated[0].message.contains("AF_DECnet")) + assertTrue(deprecated[1].message.contains("AF_IRDA")) + assertTrue(grammar.deprecatedTokens("AF_INET inet6 unix").isEmpty()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt new file mode 100644 index 00000000..d7d7f5af --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt @@ -0,0 +1,128 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRestrictFilesystemsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first six are the man page examples and TEST-07-PID1.exec-context.sh values. + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=ext4 tmpfs + RestrictFileSystems=ext2 ext4 + RestrictFileSystems=~ext4 tmpfs + RestrictFileSystems=~tmpfs,ext4 + RestrictFileSystems=~@basic-api + RestrictFileSystems=~proc devtmpfs sysfs + RestrictFileSystems=~ + RestrictFileSystems=@common-block @network @known + RestrictFileSystems=btrfs_test_fs balloon-kvm ppc-cmm cgroup2 cgroup nfs4 nfs + RestrictFileSystems="ext4" 'xfs' + RestrictFileSystems=~ fuse, fuseblk, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + RestrictFileSystems=@temporary + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + RestrictFileSystems=~@privileged-api + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + RestrictFileSystems=@security @application + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testRuntimeOnlyRejectionsAreAccepted() { + // sd-check at 0f0682b773 accepts every one of these (one value per probe file). The unit-file parser + // stores any word that does not unquote to `@group` unchecked; unknown names are only logged and + // skipped when the unit starts ("bpf-restrict-fs: Invalid filesystem name '…', ignoring.", + // src/core/bpf-restrict-fs.c#L117-L121). Issue #565 tracks a separate warning for them. The kf6 + // autotest values ("invalid", "temporary", "@auxiliary-api btrfs nfs @temporary unknown") are here. + // A `~` word after the first position is the literal name `~proc` / `~@foo`, and quotes and + // backslash escapes are removed anywhere in a word (EXTRACT_UNQUOTE). + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=~proc,sysfs,foo + RestrictFileSystems=EXT4 + RestrictFileSystems=ext5 + RestrictFileSystems=invalid + RestrictFileSystems=temporary + RestrictFileSystems=@auxiliary-api btrfs nfs @temporary unknown + RestrictFileSystems=ext4foo nfs4x foo.bar! + RestrictFileSystems=ext4 ~proc + RestrictFileSystems=ext4 ~@foo + RestrictFileSystems=ext4 ~ + RestrictFileSystems=~ ~@bogus + RestrictFileSystems=~~proc + RestrictFileSystems="~@bogus" + RestrictFileSystems=\~proc + RestrictFileSystems=ext"4" + RestrictFileSystems=ext4"" + RestrictFileSystems="" + RestrictFileSystems="a\"b" + RestrictFileSystems='a\'b' + RestrictFileSystems="@basic-api" '@network' + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // "@foo" and "@bar" are TEST-07's deliberately unknown groups. The parser refuses unknown groups + // ("bpf-restrict-fs: Unknown filesystem group, ignoring: …", sd-check at 0f0682b773), also when + // the word only unquotes to `@…` ("@foo", ""@foo, \@foo) and when the `~` marker comes first. + // An unterminated quote is "Trailing garbage in RestrictFileSystems". + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=ext4 @foo @basic-api + RestrictFileSystems=~proc foo @bar @basic-api + RestrictFileSystems=@basic_api + RestrictFileSystems=@unknowngroup + RestrictFileSystems=@ + RestrictFileSystems=@Basic-api + RestrictFileSystems=~@bogus + RestrictFileSystems="@foo" + RestrictFileSystems=""@foo + RestrictFileSystems=\@foo + RestrictFileSystems="@basic-api"x + RestrictFileSystems=ext"4 + RestrictFileSystems=ext4 "unterminated + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(13, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRootImageOptionsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRootImageOptionsOptionValueTest.kt new file mode 100644 index 00000000..10f17176 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRootImageOptionsOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRootImageOptionsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootImageOptions=root:ro,noatime home:ro,dev + RootImageOptions=root:ro,noatime,nosuid home:ro,dev nosuid,dev + RootImageOptions=home:ro,dev nosuid,dev,%%foo + RootImageOptions=root:nosuid + RootImageOptions=ro + RootImageOptions=usr:ro xbootldr:ro esp:ro srv:ro tmp:ro var:ro swap:ro + RootImageOptions=root-verity:ro usr-verity-sig:ro root-vty:ro usr-vsig:ro + RootImageOptions=root::ro + RootImageOptions=root:ro: + RootImageOptions=rootcontext=system_u\:object_r\:etc_t\:s0 + RootImageOptions=uid=0\:1 + RootImageOptions= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootImageOptions=bogus:ro + RootImageOptions=root:ro:nosuid + RootImageOptions=ROOT:ro + RootImageOptions=root2:ro + RootImageOptions=esp-vty:ro + RootImageOptions=ro,x\qy + RootImageOptions=rootcontext=system_u:object_r:etc_t:s0 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt new file mode 100644 index 00000000..d728ab06 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] TCPAdvertisedMaximumSegmentSize=. */ +class ConfigParseRouteMetricAdvMssOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + TCPAdvertisedMaximumSegmentSize=30 + TCPAdvertisedMaximumSegmentSize=1460 + TCPAdvertisedMaximumSegmentSize=1K + TCPAdvertisedMaximumSegmentSize=4294967295 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + TCPAdvertisedMaximumSegmentSize=0 + TCPAdvertisedMaximumSegmentSize=4G + TCPAdvertisedMaximumSegmentSize=big + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt new file mode 100644 index 00000000..50a7ff2b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] InitialCongestionWindow=. */ +class ConfigParseRouteMetricInitCwndOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // safe_atou32 sees the whole value, so whitespace or a sign after a 0b/0o prefix is fine (sd-check accepts 0b 1, 0b+1). + // language="unit file (systemd)" + val file = """ + [Route] + InitialCongestionWindow=20 + InitialCongestionWindow=1 + InitialCongestionWindow=1023 + InitialCongestionWindow=0x10 + InitialCongestionWindow=0b 1 + InitialCongestionWindow=0b+1 + InitialCongestionWindow=0o 7 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. +0b1 and -0b0 are EINVAL: systemd turns off glibc's C23 0b parsing (sd-check rejects both). + // language="unit file (systemd)" + val file = """ + [Route] + InitialCongestionWindow=0 + InitialCongestionWindow=1024 + InitialCongestionWindow=ten + InitialCongestionWindow=+0b1 + InitialCongestionWindow=-0b0 + InitialCongestionWindow=08 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt new file mode 100644 index 00000000..f94ca474 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] MTUBytes= (config_parse_mtu, AF_UNSPEC). */ +class ConfigParseRouteMetricMtuOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + MTUBytes=1480 + MTUBytes=9000 + MTUBytes=0 + MTUBytes=1K + MTUBytes=1.5K + MTUBytes=1 K + MTUBytes=1K 512 + MTUBytes=4294967295 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + MTUBytes=4G + MTUBytes=1500X + MTUBytes=512 1K + MTUBytes=-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt new file mode 100644 index 00000000..18841fde --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] TCPRetransmissionTimeoutSec=. */ +class ConfigParseRouteMetricTcpRtoOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + TCPRetransmissionTimeoutSec=300s + TCPRetransmissionTimeoutSec=200ms + TCPRetransmissionTimeoutSec=1 + TCPRetransmissionTimeoutSec=1us + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + TCPRetransmissionTimeoutSec=0 + TCPRetransmissionTimeoutSec=infinity + TCPRetransmissionTimeoutSec=50d + TCPRetransmissionTimeoutSec=fast + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt new file mode 100644 index 00000000..38143c82 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] MultiPathRoute=. */ +class ConfigParseRouteMultiPathOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + MultiPathRoute=10.0.0.1@eth0 20 + MultiPathRoute=192.168.1.1 50 + MultiPathRoute=@wg0 15 + MultiPathRoute=2001:db8::1@eth0 + MultiPathRoute=2001:1234:5:9fff:ff:ff:ff:ff@dummy98 5 + MultiPathRoute=@1 15 + MultiPathRoute=@999999 10 + MultiPathRoute=10.0.0.1@5 + MultiPathRoute=fe80::1 + MultiPathRoute=@eth0 256 + MultiPathRoute=@eth0 1 + MultiPathRoute=@eth0 0b 1 + MultiPathRoute=@+0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects the weight +0b1 and the empty device in `@ 5`; it accepts the + // weight 0b 1 and the device name +0b1 (parse_ifindex refuses it, ifname_valid accepts it). + // language="unit file (systemd)" + val file = """ + [Route] + MultiPathRoute=@eth0 0 + MultiPathRoute=@eth0 257 + MultiPathRoute=@ + MultiPathRoute=@0 + MultiPathRoute=10.0.0.1/24@eth0 + MultiPathRoute=@eth0 10 20 + MultiPathRoute=eth0 + MultiPathRoute=@eth0 +0b1 + MultiPathRoute=@ 5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt new file mode 100644 index 00000000..69b8e838 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_route_prefix_lifetime. */ +class ConfigParseRoutePrefixLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + LifetimeSec=1000 + LifetimeSec=infinity + LifetimeSec=0 + LifetimeSec=4294967294 + LifetimeSec=1w + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + LifetimeSec=4294967295 + LifetimeSec=200y + LifetimeSec=forever + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt new file mode 100644 index 00000000..4f269c92 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [IPv6RoutePrefix] Route=. */ +class ConfigParseRoutePrefixOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + Route=2001:db0:fff::/64 + Route=2001:db2:fff::/48 + Route=::/0 + Route=2001:db8::1 + Route=2001:db8::/0x40 + Route=2001:db8::/ 64 + Route=2001:db8::/0b 1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + Route=192.168.0.0/24 + Route=2001:db8::/129 + Route=2001:db8::/ + Route=2001:db8::/+0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt new file mode 100644 index 00000000..cce75651 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] Table= and [RoutingPolicyRule] Table=. */ +class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // custom-table, my_table, 0x0 and `main table` are deliberate leniencies: they are legal networkd.conf RouteTable= + // names (only all-digit names and names with `:` are refused; `main\ table:100` defines one with a space), which the + // plugin cannot see. systemd without such a definition rejects them. 0b 1 is safe_atou32 = 1 (sd-check accepts it). + // language="unit file (systemd)" + val file = """ + [Route] + Table=main + Table=local + Table=default + Table=1011 + Table=4294967295 + Table=custom-table + Table=0x0 + [RoutingPolicyRule] + Table=7 + Table=my_table + Table=0x10 + Table=0b 1 + Table=main table + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + Table=0 + Table=4294967296 + Table=a:b + [RoutingPolicyRule] + Table=00 + Table=08 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt new file mode 100644 index 00000000..5c782dde --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_router_home_agent_lifetime: 0 < t <= 65535s. */ +class ConfigParseRouterHomeAgentLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + HomeAgentLifetimeSec=1 + HomeAgentLifetimeSec=65535 + HomeAgentLifetimeSec=18h 12min 15s + HomeAgentLifetimeSec=500ms + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + HomeAgentLifetimeSec=0 + HomeAgentLifetimeSec=65536 + HomeAgentLifetimeSec=infinity + HomeAgentLifetimeSec=1day + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt new file mode 100644 index 00000000..afcadc96 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_router_lifetime: any parse_sec() value (out-of-range values are clamped). */ +class ConfigParseRouterLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + RouterLifetimeSec=1800 + RouterLifetimeSec=0 + RouterLifetimeSec=30min + RouterLifetimeSec=1s + RouterLifetimeSec=10000 + RouterLifetimeSec=infinity + RouterLifetimeSec=.5 + RouterLifetimeSec=+5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + RouterLifetimeSec=forever + RouterLifetimeSec=30 minuts + RouterLifetimeSec=+.5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt new file mode 100644 index 00000000..80f2a7e9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_router_prefix_delegation (IPv6SendRA= and deprecated IPv6PrefixDelegation=). */ +class ConfigParseRouterPrefixDelegationOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv6SendRA=yes + IPv6SendRA=no + IPv6SendRA=True + IPv6SendRA=0 + IPv6SendRA=Yes + IPv6SendRA=TRUE + IPv6SendRA=oFf + IPv6PrefixDelegation=static + IPv6PrefixDelegation=dhcpv6 + IPv6PrefixDelegation=yes + IPv6PrefixDelegation=Yes + IPv6PrefixDelegation=TRUE + IPv6PrefixDelegation=oFf + IPv6PrefixDelegation=No + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + IPv6SendRA=maybe + IPv6SendRA=both + IPv6SendRA=static + IPv6SendRA=dhcpv6 + IPv6PrefixDelegation=Static + IPv6PrefixDelegation=DHCPv6 + IPv6PrefixDelegation=maybe + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt new file mode 100644 index 00000000..7dcd2384 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_router_uint32_msec_usec. */ +class ConfigParseRouterUint32MsecUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + ReachableTimeSec=30s + ReachableTimeSec=0 + ReachableTimeSec=1h30min + ReachableTimeSec=1500ms + ReachableTimeSec=4294967295ms + ReachableTimeSec=infinity + RetransmitSec=1.5 + RetransmitSec=.5s + RetransmitSec=2 min + RetransmitSec=1d 2h + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + ReachableTimeSec=4294967296ms + ReachableTimeSec=50d + RetransmitSec=5x + RetransmitSec=-1s + RetransmitSec=5. + RetransmitSec=12.34.56 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt new file mode 100644 index 00000000..dc724aa2 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [RoutingPolicyRule] FirewallMark=. */ +class ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + FirewallMark=1011 + FirewallMark=7/255 + FirewallMark=0x10/0xff + FirewallMark=0 + FirewallMark=4294967295/4294967295 + FirewallMark=0b101 + FirewallMark=7/ 255 + FirewallMark=0b 1 + FirewallMark=1/0b 1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1, 1/+0b1 and 1 /2, and accepts 0b 1 and 1/0b 1. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + FirewallMark=4294967296 + FirewallMark=7/ + FirewallMark=/255 + FirewallMark=mark + FirewallMark=-1 + FirewallMark=+0b1 + FirewallMark=1/+0b1 + FirewallMark=1 /2 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(8, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt new file mode 100644 index 00000000..689f742d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [RoutingPolicyRule] IPProtocol=. */ +class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + IPProtocol=tcp + IPProtocol=UDP + IPProtocol=sctp + IPProtocol=icmpv6 + IPProtocol=6 + IPProtocol=17 + IPProtocol=0x11 + IPProtocol=255 + IPProtocol=aggfrag + IPProtocol=AGGFRAG + IPProtocol=144 + IPProtocol=0 + IPProtocol=58 + IPProtocol=TcP + IPProtocol=IcMpV6 + IPProtocol=L2TP + IPProtocol=0b 110 + IPProtocol=-0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b110 (EINVAL) and 0b-1 (-1: ERANGE), and accepts 0b 110 (tcp) and -0. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + IPProtocol=tcpp + IPProtocol=256 + IPProtocol=mptcp + IPProtocol=-6 + IPProtocol=3 + IPProtocol=200 + IPProtocol=TcPx + IPProtocol=Yes + IPProtocol=+0b110 + IPProtocol=0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt new file mode 100644 index 00000000..a9caaa4d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [RoutingPolicyRule] User=. */ +class ConfigParseRoutingPolicyRuleUidRangeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // systemd-network and +1000 are accepted as possible user names: systemd resolves them via NSS at runtime + // (get_user_creds), so they are only rejected on a machine without that user. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + User=100-200 + User=1000 + User=0 + User=root + User=systemd-network + User=1000-1000 + User=+1000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + User=200-100 + User=65535 + User=4294967295 + User=01000 + User=100- + User=1000-99999999999999999999 + User=99999999999999999999-1000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt new file mode 100644 index 00000000..6a8927d8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRpsCpuMaskOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ReceivePacketSteeringCPUMask=0 1 + ReceivePacketSteeringCPUMask=2,3 + ReceivePacketSteeringCPUMask=all + ReceivePacketSteeringCPUMask=disable + ReceivePacketSteeringCPUMask=0-7 + ReceivePacketSteeringCPUMask=1 + ReceivePacketSteeringCPUMask=0-"3" + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ReceivePacketSteeringCPUMask=invalid + ReceivePacketSteeringCPUMask=0 3 8-invalid + ReceivePacketSteeringCPUMask=all 1 + ReceivePacketSteeringCPUMask=none + ReceivePacketSteeringCPUMask=-0 + ReceivePacketSteeringCPUMask=All + ReceivePacketSteeringCPUMask=Disable + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseServiceRefreshOnReloadOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseServiceRefreshOnReloadOptionValueTest.kt new file mode 100644 index 00000000..465840ed --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseServiceRefreshOnReloadOptionValueTest.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseServiceRefreshOnReloadOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Booleans (parse_boolean is case-insensitive), resource lists, the ~ inversion (a bare ~ is an empty inverted list), and an empty reset. + // language="unit file (systemd)" + val file = """ + [Service] + RefreshOnReload=yes + RefreshOnReload=false + RefreshOnReload=1 + RefreshOnReload=Yes + RefreshOnReload=TRUE + RefreshOnReload=oN + RefreshOnReload=OFF + RefreshOnReload=oFf + RefreshOnReload=fAlSe + RefreshOnReload=T + RefreshOnReload=extensions + RefreshOnReload=credentials + RefreshOnReload=extensions credentials + RefreshOnReload=~credentials + RefreshOnReload=~ extensions + RefreshOnReload=~ + RefreshOnReload= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RefreshOnReload=~yes + RefreshOnReload=Extensions + RefreshOnReload=extensions,credentials + RefreshOnReload=~~extensions + RefreshOnReload=extensions ~credentials + RefreshOnReload=all + RefreshOnReload=CREDENTIALS + RefreshOnReload=extensions Credentials + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt new file mode 100644 index 00000000..385c1fb3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt @@ -0,0 +1,83 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSocketBindtodeviceOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // eth0/lo/wlan0 are from the Debian corpus and systemd's tests. The numeric-looking names after `**` + // are ones parse_ifindex() does not take as an index, so systemd accepts them (sd-check at 0f0682b773). + // language="unit file (systemd)" + val file = """ + [Socket] + BindToDevice=eth0 + BindToDevice=lo + BindToDevice=wlan0 + BindToDevice=* + BindToDevice=enp0s31f6 + BindToDevice=br-lan.100 + BindToDevice=abcdefghijklmno + BindToDevice=%i + BindToDevice=vrf-%i + BindToDevice=1a + BindToDevice=alliance + BindToDevice=** + BindToDevice=-5 + BindToDevice=+0 + BindToDevice=0x0 + BindToDevice=+0b1 + BindToDevice=0x+5 + BindToDevice=0x80000000 + BindToDevice=+2147483648 + BindToDevice=0b2 + BindToDevice=0xg + BindToDevice=00x5 + BindToDevice=0b-0 + BindToDevice=-0x0 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // 08 is all digits, which ifname_valid() refuses outright. + // 0x5 … 0o+7: parse_ifindex() reads them as an ifindex, which ifname_valid() refuses + // ("Invalid interface name, ignoring", sd-check at 0f0682b773). + // language="unit file (systemd)" + val file = """ + [Socket] + BindToDevice=abcdefghijklmnop + BindToDevice=eth0:1 + BindToDevice=eth/0 + BindToDevice=all + BindToDevice=default + BindToDevice=42 + BindToDevice=.. + BindToDevice=eth0 eth1 + BindToDevice=%z + BindToDevice=100%% + BindToDevice=0x5 + BindToDevice=0X5 + BindToDevice=+5 + BindToDevice=+0x5 + BindToDevice=0b101 + BindToDevice=0o7 + BindToDevice=0b+1 + BindToDevice=010 + BindToDevice=+010 + BindToDevice=0x7fffffff + BindToDevice=0o+7 + BindToDevice=08 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(22, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenFileOptionValueTest.kt new file mode 100644 index 00000000..a46d4fca --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenFileOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSocketListenFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + ListenFIFO=/run/acpi_fakekey + ListenFIFO=/run/cloud-init/share/hook-hotplug-cmd + ListenFIFO=%t/wob.sock + ListenFIFO=/var/run/legacy.fifo + ListenFIFO=/run//double/./slash + ListenFIFO=/devices/not-api-vfs + ListenMessageQueue=/queue + ListenMessageQueue=/%i + ListenUSBFunction=/run/usb-ffs/adb + ListenUSBFunction=/some/path + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Relative paths, ".." components, API VFS paths (PATH_CHECK_NON_API_VFS) and an unknown specifier. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenFIFO=relative/path + ListenFIFO=/run/../etc/fifo + ListenFIFO=/dev/foo + ListenFIFO=/proc + ListenMessageQueue=queue + ListenMessageQueue=/sys/fs/mqueue/q + ListenUSBFunction=/dev/usb-ffs/adb + ListenUSBFunction=%e/ffs + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSocketOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSocketOptionValueTest.kt new file mode 100644 index 00000000..9223db2c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSocketOptionValueTest.kt @@ -0,0 +1,242 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseSocketListenSocketOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import org.junit.Test + +class ConfigParseSocketListenSocketOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidStreamAndDatagramAddresses() { + // From TEST(socket_address_parse) (src/test/test-socket-netlink.c) and the Debian corpus. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenStream=65535 + ListenStream=8888 + ListenStream=0x50 + ListenStream=[2001:0db8:0000:85a3:0000:0000:ac1f:8001]:8888 + ListenStream=[::1]:8888 + ListenStream=[::]:111 + ListenStream=[::1]:1234%%lo + ListenStream=[fe80::1]:80%%eth0 + ListenStream=[fe80::1]:80%%2 + ListenStream=192.168.1.254:8888 + ListenStream=0.0.0.0:80 + ListenStream=127.0.0.1:1234%%lo + ListenStream=/foo/bar + ListenStream=/run/foo bar.sock + ListenStream=@abstract + ListenStream=@/org/freedesktop/systemd1/logger + ListenStream=%t/pulse/native + ListenStream=%t/foot-%i.sock + ListenStream=%C/sigstore/gitsign/cache.sock + ListenStream=%i + ListenStream=vsock:2:1234 + ListenStream=vsock::1234 + ListenStream=vsock:host:22 + ListenStream=vsock:-1:22 + ListenStream=vsock-stream:any:22 + ListenStream=vsock:-0:5 + ListenDatagram=69 + ListenDatagram=[::]:53 + ListenDatagram=127.0.0.1:1153 + ListenDatagram=@fcm_clif + ListenDatagram=/path/to/socket + ListenDatagram=vsock-dgram:3:4000 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidStreamAndDatagramAddresses() { + // `0` and `65536` are ports out of range for ListenStream= (they would be netlink family numbers, + // which only ListenNetlink= takes); all 23 confirmed rejected by sd-check at 0f0682b773. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenStream=junk + ListenStream=192.168.1.1 + ListenStream=.168.1.1 + ListenStream=989.168.1.1:80 + ListenStream=192.168.1.1:65536 + ListenStream=192.168.1.1:0 + ListenStream=0 + ListenStream=65536 + ListenStream=[::1] + ListenStream=[::1]8888 + ListenStream=::1 + ListenStream=[::1]:0 + ListenStream=[a:b:1]:8888 + ListenStream=[::1]%%lo:1234 + ListenStream=[::1]:1234%%lo%%lo + ListenStream=/ + ListenStream=@ + ListenStream=vsock:2:1234x + ListenStream=vsock:2x:1234 + ListenStream=vsock:2 + ListenStream=[fe80::1]:80%eth0 + ListenDatagram=80/udp + ListenDatagram=localhost:53 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(23, highlights) + } + + @Test + fun testUnixSocketPathLength() { + // sockaddr_un_set_path: 107 bytes is the longest path that fits with its NUL terminator. The limit + // applies after patch_var_run() has rewritten /var/run/ to /run/, so /var/run/ paths get 4 more. + val ok = "/" + "a".repeat(106) + val tooLong = "/" + "a".repeat(107) + val file = """ + [Socket] + ListenStream=$ok + ListenStream=@${"a".repeat(106)} + ListenStream=$tooLong + ListenStream=/var/run/${"a".repeat(102)} + ListenStream=/var/run/${"a".repeat(103)} + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } + + @Test + fun testSequentialPacketIsUnixOnly() { + // The last three are refused by config_parse_socket_listen ("Address family not supported"), checked + // with sd-check at 0f0682b773. The inspection dispatches on the key; the key-agnostic union grammar + // (`combinator`) accepts them, which is why a harness that only reads `combinator` sees them as valid. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenSequentialPacket=/run/foo.sock + ListenSequentialPacket=@namespace + ListenSequentialPacket=%t/foo.sock + ListenSequentialPacket=1.2.3.4:1234 + ListenSequentialPacket=1234 + ListenSequentialPacket=vsock-seqpacket:2:1234 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } + + @Test + fun testNetlink() { + // From TEST(socket_address_parse_netlink) and the corpus. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenNetlink=route + ListenNetlink=route 10 + ListenNetlink=kobject-uevent 1 + ListenNetlink=kobject-uevent 10 + ListenNetlink=audit 1 + ListenNetlink=route 1361 + ListenNetlink=rdma 4 + ListenNetlink=16 + ListenNetlink=generic 0x10 + ListenNetlink=route -0 + ListenNetlink=junk + ListenNetlink=route ten + ListenNetlink=route 1 2 + ListenNetlink=/run/foo.sock + ListenNetlink=route -1 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } + + @Test + fun testSafeAtoSpellings() { + // The numbers go through the shared SafeAtoTerminal. Every value here was checked one per file with + // sd-check at 0f0682b773: the first 15 are accepted, the last 5 rejected. Ports are parse_ip_port + // (SAFE_ATO_REFUSE_LEADING_WHITESPACE, then 0 refused); vsock CID/port and the netlink group are plain + // safe_atou() on the rest of the text; the netlink family is the first " \t\n\r"-separated word. + // The plugin's lexer does not take a raw \v, so those spellings are in testVerticalTabSpellings. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenStream=0b+1 + ListenStream=0b 1 + ListenStream=0b +1 + ListenStream=0o 7 + ListenStream=1.2.3.4:0b 1 + ListenStream=[::1]:0b+1 + ListenStream=1.2.3.4:0b+1%%lo + ListenStream=vsock:2: 80 + ListenStream=vsock: 2:80 + ListenStream=vsock:2:0b 1 + ListenStream=vsock:0b+1:80 + ListenNetlink=route 0b 1 + ListenNetlink=route 0b+1 + ListenNetlink=0b+1 5 + ListenNetlink=route 5 + ListenStream=0b-0 + ListenStream=1.2.3.4: 80 + ListenStream=+0b1 + ListenStream=vsock:2:0b -1 + ListenNetlink=route 0b -1 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } + + @Test + fun testVerticalTabSpellings() { + // strtoul skips a leading \v, but neither SAFE_ATO_REFUSE_LEADING_WHITESPACE nor extract_first_word + // treat it as whitespace. sd-check at 0f0682b773, one value per file: the first three are accepted, + // `route\v5` is rejected (one word, not a netlink family). + val validator = ConfigParseSocketListenSocketOptionValue() + val stream = validator.combinatorForKey("ListenStream") + val netlink = validator.combinatorForKey("ListenNetlink") + for (engine in listOf(GrammarEngineVerdict::classic, GrammarEngineVerdict::parseEngine)) { + assertEquals(GrammarEngineVerdict.VALID, engine(stream, "\u000B80")) + assertEquals(GrammarEngineVerdict.VALID, engine(stream, "1.2.3.4:\u000B80")) + assertEquals(GrammarEngineVerdict.VALID, engine(netlink, "route \u000B5")) + assertEquals(GrammarEngineVerdict.Kind.SYNTAX, engine(netlink, "route\u000B5").kind) + } + } + + @Test + fun testEmptyValueResets() { + // language="unit file (systemd)" + val file = """ + [Socket] + ListenStream= + ListenNetlink= + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSpecialOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSpecialOptionValueTest.kt new file mode 100644 index 00000000..3237a435 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSpecialOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSocketListenSpecialOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // ListenSpecial= is the one file ltype without PATH_CHECK_NON_API_VFS (systemd 8bd473ff25). + // language="unit file (systemd)" + val file = """ + [Socket] + ListenSpecial=/dev/rfkill + ListenSpecial=/proc/kmsg + ListenSpecial=/sys/kernel/debug/foo + ListenSpecial=/some/path + ListenSpecial=%t/special + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + ListenSpecial=dev/rfkill + ListenSpecial=/dev/../rfkill + ListenSpecial=%e + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt new file mode 100644 index 00000000..32d3a47e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_stacked_netdev (all ten kinds). */ +class ConfigParseStackedNetdevOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // +0b1 is not an ifindex (safe_atoi gives EINVAL because systemd turns off glibc's C23 0b parsing), so it is a + // valid name; 0b-1 is the ifindex -1, which is not positive. sd-check accepts both names. + // language="unit file (systemd)" + val file = """ + [Network] + IPoIB=ib0.8001 + IPVLAN=ipvlan99 + IPVTAP=ipvtap99 + MACsec=macsec99 + MACVLAN=macvlan99 macvlan98 + MACVTAP=macvtap99 + Tunnel=sittun99 + VLAN=vlan99 + VLAN=vlan-1 vlan-2 + VXLAN=vxlan-slaac + Xfrm=xfrm98 + VLAN= + VLAN=0x0 + VLAN=-5 + VLAN=0xvlan + VLAN=+0b1 + VLAN=0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + VLAN=vlan:1 + VXLAN=this-name-is-way-too-long + Tunnel=123 + MACVLAN=ok all + IPVLAN=0x10 + VLAN=+5 + VLAN=0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt new file mode 100644 index 00000000..7b22ecc5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_string(CONFIG_PARSE_STRING_SAFE_AND_ASCII). */ +class ConfigParseStringSafeAsciiOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + BootFilename=pxelinux.0 + BootFilename=efi/grubx64.efi + BootFilename=boot file with spaces*.efi + BootFilename=p@ss:w0rd!#[x]? + BootFilename=foo*bar[1]? + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + BootFilename=café.efi + BootFilename=back\\slash + BootFilename="quoted" + BootFilename=it's + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt new file mode 100644 index 00000000..c209d662 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_strv(0): word lists with only quote balance checked. */ +class ConfigParseStrvOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Network] + BindCarrier=eth0 eth1 + BindCarrier=en* + BindCarrier= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + BindCarrier=eth0 "eth1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } + @Test + fun testValidValuesNspawn() { + // language="unit file (systemd)" + val file = """ + [Exec] + Parameters=/bin/sh -c "echo hello world" + Parameters='single quoted' plain + Environment=FOO=bar "BAZ=with space" + Environment=PATH=/usr/bin\:/bin + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNspawn() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Exec] + Parameters=/bin/sh -c "echo + Environment=FOO='bar + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt new file mode 100644 index 00000000..adb46fc6 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt @@ -0,0 +1,176 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSyscallFilterOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidAllowLists() { + // Real values from the Debian corpus and systemd's own units/tests. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=@system-service + SystemCallFilter=@system-service @mount + SystemCallFilter=@system-service @module @raw-io bpf + SystemCallFilter=@default @file-system @basic-io @system-service + SystemCallFilter=read write open execve + SystemCallFilter=ptrace,reboot swapoff + SystemCallFilter=read, write + SystemCallFilter=_llseek _newselect epoll_wait s390_pci_mmio_read + SystemCallFilter=@known + SystemCallFilter=,read, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testUnknownSyscallNamesAreAcceptedDeliberately() { + // systemd warns "System call invalid is not known, ignoring." for these (sd-check at 0f0682b773), + // but whether a name resolves depends on libseccomp and the native architecture, so only the + // shape of a syscall name is checked. Documented leniency. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=invalid + SystemCallFilter=madeupcall + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidDenyListsAndErrno() { + // ~uname:EILSEQ, :kill, :255 come from systemd's test units; :4095/:0 from test-seccomp.c. + // language="unit file (systemd)" + val file = """ + [Socket] + SystemCallFilter=~@mount + SystemCallFilter=~ @clock @debug @module @mount @raw-io @reboot @swap @cpu-emulation @obsolete + SystemCallFilter=~@privileged @resources + SystemCallFilter=~uname:EILSEQ + SystemCallFilter=~uname:ENOENT uname:EILSEQ + SystemCallFilter=~uname:kill + SystemCallFilter=~uname:255 + SystemCallFilter=~@sync:4095 + SystemCallFilter=~uname:0 + SystemCallFilter=~uname:0x1F + SystemCallFilter=~uname:eperm + SystemCallFilter=~uname:ePerm uname:Eilseq + SystemCallFilter=~uname:0777 uname:0b11 uname:0o17 uname:+5 uname:-0 uname:+0x10 uname:0b+1 uname:0o+7 uname:0B-0 uname:-0x0 uname:0o-0 + SystemCallFilter=~@sync:EPERM,@mount:kill uname:E2BIG + SystemCallFilter=~ + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidOtherUnitTypesAndNspawn() { + // language="unit file (systemd)" + val mount = """ + [Mount] + SystemCallFilter=@system-service + """.trimIndent() + setupFileInEditor("file.mount", mount) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + SystemCallFilter=~@swap + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val nspawn = """ + [Exec] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=~@clock add_key + """.trimIndent() + setupFileInEditor("file.nspawn", nspawn) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testTildeWordAfterTheFirstPositionIsAccepted() { + // nspawn only honours `~` as the first character, so "~@mount" below is stored verbatim as a name. + // sd-check at 0f0682b773 accepts all of these in a .nspawn file (nspawn's parser stores words + // unchecked); the word is only dropped at container start by seccomp_add_syscall_filter_item(), a + // runtime-only rejection that issue #565 will warn about. + // language="unit file (systemd)" + val nspawn = """ + [Exec] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=@system-service ~@bogus + SystemCallFilter=~~@mount + SystemCallFilter=,~read + """.trimIndent() + setupFileInEditor("file.nspawn", nspawn) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // In a unit file sd-check says "System call ~@mount is not known, ignoring." for the same words: the + // libseccomp lookup warning that this grammar already leaves unflagged for names like "madeupcall". + // Both files share the (config_parse_syscall_filter, 0) registration, so the lenient nspawn + // behaviour applies here as well. Documented leniency. + // language="unit file (systemd)" + val service = """ + [Service] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=read ~write + SystemCallFilter=~~@mount + SystemCallFilter=~read ~write:EPERM + """.trimIndent() + setupFileInEditor("file.service", service) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=@bogus-group + SystemCallFilter=~write open execve DONOTEXIST + SystemCallFilter=uname:EILSEQ + SystemCallFilter=~uname: + SystemCallFilter=~:255 + SystemCallFilter=~uname:4096 + SystemCallFilter=~uname:-3 + SystemCallFilter=~uname:12.3 + SystemCallFilter=~uname:EINVALaaa + SystemCallFilter=~uname:255:EILSEQ + SystemCallFilter=~uname:KILL + SystemCallFilter=~uname:Kill + SystemCallFilter=@service + SystemCallFilter=~uname:0b-1 + SystemCallFilter=~uname:+0b1 + SystemCallFilter=~uname:08 + SystemCallFilter=~uname:0x + SystemCallFilter=@system-service ~@mount:EPERM + SystemCallFilter=~@bogus + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(19, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt new file mode 100644 index 00000000..93f4a207 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt @@ -0,0 +1,77 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSyscallLogOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // "~ some_name another_name" (kf6 autotest): systemd warns that both names are unknown, but syscall + // names are resolved by libseccomp per architecture, so only their shape is checked (deliberate). + // "@system-service", "read write open" and "ptrace,reboot swapoff" are from systemd's + // test/units/TEST-*.sh and the man page. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallLog=@system-service + SystemCallLog=read write open + SystemCallLog=ptrace,reboot swapoff + SystemCallLog=~@sync @mount + SystemCallLog=~ read, write + SystemCallLog=~ + SystemCallLog=_llseek, + SystemCallLog=~ some_name another_name + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + SystemCallLog=@network-io + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + SystemCallLog=~@mount + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + SystemCallLog=swapon swapoff + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallLog=@not-a-group + SystemCallLog=uname:EPERM + SystemCallLog=~uname:EPERM + SystemCallLog=read Write + SystemCallLog=~~read + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTbfRateOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTbfRateOptionValueTest.kt new file mode 100644 index 00000000..59fd41fd --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTbfRateOptionValueTest.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTbfRateOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [TokenBucketFilter] + Rate=1M + PeakRate=1M + Rate=0.5M + PeakRate=0.5M + Rate=1G + PeakRate=1G + Rate=100G + PeakRate=100G + Rate=1G 500M + PeakRate=1G 500M + Rate=3.5G3B + PeakRate=3.5G3B + Rate=4T3G3B + PeakRate=4T3G3B + Rate=10.M + PeakRate=10.M + Rate=125000 + PeakRate=125000 + Rate=+5K + PeakRate=+5K + Rate=1000 B + PeakRate=1000 B + Rate= + PeakRate= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [TokenBucketFilter] + Rate=10Mbit + PeakRate=10Mbit + Rate=1k + PeakRate=1k + Rate=-1M + PeakRate=-1M + Rate=3B3.5G + PeakRate=3B3.5G + Rate=12P12P + PeakRate=12P12P + Rate=1M 1M + PeakRate=1M 1M + Rate=abc + PeakRate=abc + Rate=.5M + PeakRate=.5M + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(16, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTemporaryFilesystemsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTemporaryFilesystemsOptionValueTest.kt new file mode 100644 index 00000000..33500262 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTemporaryFilesystemsOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTemporaryFilesystemsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + TemporaryFileSystem=/var:ro + TemporaryFileSystem=/var:ro,mode=0700,nostrictatime + TemporaryFileSystem=/tmp:ro /usr:ro + TemporaryFileSystem=/var + TemporaryFileSystem=/var: + TemporaryFileSystem=%t/foo:size=10% + TemporaryFileSystem=/run/a\ b:ro + TemporaryFileSystem="/run/a b":ro + TemporaryFileSystem=/run/x:context=system_u:object_r:tmp_t:s0 + TemporaryFileSystem= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + TemporaryFileSystem=var:ro + TemporaryFileSystem=:ro + TemporaryFileSystem=/var/../etc + TemporaryFileSystem=/tmp relative + TemporaryFileSystem="/unbalanced + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTeqlIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTeqlIdOptionValueTest.kt new file mode 100644 index 00000000..55c99220 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTeqlIdOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTeqlIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [TrivialLinkEqualizer] + Id=0 + Id=1 + Id=2147483647 + Id=0x7fffffff + Id=0o7 + Id=0b+1 + Id=0b-0 + Id=0b 1 + Id=0o 7 + Id=0b +1 + Id=0b -0 + Id= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [TrivialLinkEqualizer] + Id=2147483648 + Id=4294967295 + Id=-1 + Id=abc + Id=teql1 + Id=0b-1 + Id=0b -1 + Id=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerCalendarOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerCalendarOptionValueTest.kt new file mode 100644 index 00000000..f0c81ba8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerCalendarOptionValueTest.kt @@ -0,0 +1,206 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTimerCalendarOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesFromSystemdTests() { + // Every input of TEST(calendar_spec_one) in src/test/test-calendarspec.c, plus the man page examples. + // language="unit file (systemd)" + val file = """ + [Timer] + OnCalendar=Sat,Thu,Mon-Wed,Sat-Sun + OnCalendar=Sat,Thu,Mon..Wed,Sat..Sun + OnCalendar=Mon,Sun 12-*-* 2,1:23 + OnCalendar=Wed *-1 + OnCalendar=Wed-Wed,Wed *-1 + OnCalendar=Wed..Wed,Wed *-1 + OnCalendar=Wed, 17:48 + OnCalendar=Wednesday, + OnCalendar=Wed-Sat,Tue 12-10-15 1:2:3 + OnCalendar=Wed..Sat,Tue 12-10-15 1:2:3 + OnCalendar=*-*-7 0:0:0 + OnCalendar=10-15 + OnCalendar=monday *-12-* 17:00 + OnCalendar=Mon,Fri *-*-3,1,2 *:30:45 + OnCalendar=12,14,13,12:20,10,30 + OnCalendar=mon,fri *-1/2-1,3 *:30:45 + OnCalendar=03-05 08:05:40 + OnCalendar=08:05:40 + OnCalendar=05:40 + OnCalendar=Sat,Sun 12-05 08:05:40 + OnCalendar=Sat,Sun 08:05:40 + OnCalendar=2003-03-05 05:40 + OnCalendar=2003-03-05 + OnCalendar=03-05 + OnCalendar=hourly + OnCalendar=daily + OnCalendar=monthly + OnCalendar=weekly + OnCalendar=minutely + OnCalendar=quarterly + OnCalendar=semi-annually + OnCalendar=annually + OnCalendar=*:2/3 + OnCalendar=2015-10-25 01:00:00 uTc + OnCalendar=2015-10-25 01:00:00 Asia/Vladivostok + OnCalendar=weekly Pacific/Auckland + OnCalendar=2016-03-27 03:17:00.4200005 + OnCalendar=2016-03-27 03:17:00/0.42 + OnCalendar=9..11,13:00,30 + OnCalendar=1..3-1..3 1..3:1..3 + OnCalendar=00:00:1.125..2.125 + OnCalendar=00:00:1.0..3.8 + OnCalendar=00:00:01..03 + OnCalendar=00:00:01/2,02..03 + OnCalendar=*:4,30:0..3 + OnCalendar=*:4,30:0/1 + OnCalendar=*:4,30:0/1,3,5 + OnCalendar=*-*~1 Utc + OnCalendar=*-*~* 00:00:00 + OnCalendar=Monday + OnCalendar=Monday *-*-* + OnCalendar=*-*-* + OnCalendar=*:*:* + OnCalendar=*:* + OnCalendar=12:* + OnCalendar=*:30 + OnCalendar=93..00-*-* + OnCalendar=00..07-*-* + OnCalendar=*:20..39/5 + OnCalendar=00:00:20..40/1 + OnCalendar=*~03/1,03..05 + OnCalendar=@1493187147 + OnCalendar=@1493187147 UTC + OnCalendar=@0 + OnCalendar=*:05..05 + OnCalendar=*:05..10/6 + OnCalendar=Thu,Fri 2012-*-1,5 11:12:13 + OnCalendar=12..14:10,20,30 + OnCalendar=05:40:23.4200004/3.1700005 + OnCalendar=2003-02..04-05 + OnCalendar=daily UTC + OnCalendar=*-02~03 + OnCalendar=Mon *-05~07/1 + OnCalendar=Mon 2017-05~01..07 UTC + OnCalendar=Sun *-*-* 01:00:00 Europe/Dublin + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidValuesFromCorpus() { + // language="unit file (systemd)" + val file = """ + [Timer] + OnCalendar=Daily + OnCalendar=*-*-* 6:00 + OnCalendar=*:0/5 + OnCalendar=*-*-* 00,12:00:00 + OnCalendar=Sun *-*-1..7 0:45:00 + OnCalendar=Mon..Fri *-*-* 06:30:00 + OnCalendar=Mon,Tue,Wed,Thu,Fri,Sat + OnCalendar=Mon *-*-* 6:15:0 + OnCalendar=*-*-* 16,17,18,19,20,21,22,23,00,01,02,03,04,05,06:00:00 + OnCalendar=*-*-* *:00/10:00 + OnCalendar=Fri 2012-11-23 11:12:13 + OnCalendar=daily utc + OnCalendar=*-*-* %i:00 + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidTimezoneAndEpochSpellings() { + // Extra spaces before the zone survive only after a weekday list or a bare date (both skip a run of + // spaces); @EPOCH is read by strtoul, so whitespace and '+' may follow the '@'. CET/-03 stand for the + // host's tzname abbreviations, posix/ and Etc/ zones are real files under /usr/share/zoneinfo. + // Whether systemd accepts the zone depends on the host (sd-check at 0f0682b773): `daily CET` passes + // only with TZ=Europe/Berlin (or a tzdata that ships a CET file), `12:00 -03` only with + // TZ=America/Sao_Paulo, and posix/Europe/Berlin only where tzdata installs the posix/ tree. The plugin + // cannot know the target host, so it accepts these shapes. + // language="unit file (systemd)" + val file = """ + [Timer] + OnCalendar=Mon UTC + OnCalendar=2024-01-01 UTC + OnCalendar=Mon 2024-01-01 Europe/Berlin + OnCalendar=@+5 + OnCalendar=@ 5 + OnCalendar=daily CET + OnCalendar=12:00 -03 + OnCalendar=Mon 12:00 posix/Europe/Berlin + OnCalendar=daily Etc/GMT+5 + OnCalendar=daily Factory + OnCalendar=Mon,Fri UTC + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Rejections from TEST(calendar_spec_from_string) whose cause is a shape or a single-field bound, + // plus a few typos seen in the wild. + // language="unit file (systemd)" + val file = """ + [Timer] + OnCalendar=test + OnCalendar=7 + OnCalendar=121212:1:2 + OnCalendar=2000-03-05.23 00:00:00 + OnCalendar=2000-03-05 00:00.1:00 + OnCalendar=2016~11-22 + OnCalendar=Monday.. 12:00 + OnCalendar=Monday.. + OnCalendar=-00:+00/-5 + OnCalendar=00:+00/-5 + OnCalendar=2016- 11- 24 12: 30: 00 + OnCalendar=*~29 + OnCalendar=*~16..31 + OnCalendar=00:00/60 + OnCalendar=00:00:2300 + OnCalendar=00:00:18446744073709551615 + OnCalendar=@88588582097858858 + OnCalendar=*:4,30:*,5 + OnCalendar=*:4,30:5,* + OnCalendar=Mo *-12-* 17:00 + OnCalendar=dialy + OnCalendar=*-*-* 24:00 + OnCalendar=*-13-01 + OnCalendar=1969-01-01 + OnCalendar=%e + OnCalendar=Mon Fri + OnCalendar=Mon, Tue + OnCalendar=*-*-* 12:00 Mon + OnCalendar=*-*-* 10:00 everyday + OnCalendar=*-*-* 10:00 daily + OnCalendar=daily UTC + OnCalendar=12:00 Europe/Berlin + OnCalendar=@5 UTC + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(33, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerMonotonicOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerMonotonicOptionValueTest.kt new file mode 100644 index 00000000..2e4c7d52 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerMonotonicOptionValueTest.kt @@ -0,0 +1,103 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTimerMonotonicOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Spellings from systemd's TEST(parse_sec) (src/test/test-time-util.c) and the Debian corpus, + // spread across all five keys that share config_parse_timer's parse_sec branch. + // language="unit file (systemd)" + val file = """ + [Timer] + OnActiveSec=5s + OnActiveSec=5s500ms + OnActiveSec= 5s 500ms + OnActiveSec=5.5s 0.5ms + OnBootSec=.22s + OnBootSec=.50y + OnBootSec=2.5 + OnBootSec=.7 + OnBootSec=23us + OnBootSec=23μs + OnBootSec=23µs + OnStartupSec=infinity + OnStartupSec=+3.1s + OnStartupSec=3.1s.2 + OnStartupSec=3.1 .2 + OnStartupSec=3.1 sec .2 sec + OnUnitActiveSec=1h30min + OnUnitActiveSec=5min 15s 100ms + OnUnitActiveSec=300ms20s 5day + OnUnitActiveSec=2 minutes + OnUnitActiveSec=1 min + OnUnitActiveSec=12 M + OnUnitActiveSec=1y2M3w4d5h6m7s + OnUnitInactiveSec=23434 + OnUnitInactiveSec=0 + OnUnitInactiveSec=15m + OnUnitInactiveSec=1 seconds + OnUnitInactiveSec=7 month + OnUnitInactiveSec=7hr 2hr + OnUnitInactiveSec=%i + OnUnitInactiveSec=%im + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testEmptyValueResets() { + // language="unit file (systemd)" + val file = """ + [Timer] + OnBootSec= + OnUnitActiveSec= + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Rejections from TEST(parse_sec), plus mis-pluralised units and an unknown specifier. + // language="unit file (systemd)" + val file = """ + [Timer] + OnActiveSec=xyz + OnActiveSec=5. + OnActiveSec=.s + OnBootSec=-5s + OnBootSec=0.-0s + OnBootSec=infinity .7 + OnStartupSec=.3 infinity + OnStartupSec=3.+1s + OnStartupSec=3. 1s + OnUnitActiveSec=3.s + OnUnitActiveSec=12.34.56 + OnUnitActiveSec=12..34 + OnUnitActiveSec=5mins + OnUnitInactiveSec=2hs + OnUnitInactiveSec=12foo 1 bar + OnUnitInactiveSec=min 1 sec + OnUnitInactiveSec=%e + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(17, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt new file mode 100644 index 00000000..8cf6bfa5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_timezone (syntax of verify_timezone). */ +class ConfigParseTimezoneOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Nowhere/Fake is well-formed; systemd only rejects it because /usr/share/zoneinfo/Nowhere/Fake does not exist, + // which depends on the installed tzdata (deliberate leniency). + // language="unit file (systemd)" + val file = """ + [DHCPServer] + Timezone=Europe/Berlin + Timezone=UTC + Timezone=America/Argentina/Buenos_Aires + Timezone=Etc/GMT+5 + Timezone=Etc/GMT-14 + Timezone=Nowhere/Fake + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + Timezone=/Europe/Berlin + Timezone=Europe/ + Timezone=Europe//Berlin + Timezone=../etc/passwd + Timezone=Europe/Berlin Time + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTmpfsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTmpfsOptionValueTest.kt new file mode 100644 index 00000000..2e2fc892 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTmpfsOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTmpfsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN; the option string is passed to the kernel unchecked. + // language="unit file (systemd)" + val file = """ + [Files] + TemporaryFileSystem=/tmp + TemporaryFileSystem=/opt/tmp + TemporaryFileSystem=/var:rw,nosuid,noexec + TemporaryFileSystem=/tmp: + TemporaryFileSystem=/tmp:mode=1777,size=10% + TemporaryFileSystem=/path\:x:mode=0700 + TemporaryFileSystem=/ + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // The path must be absolute. + // language="unit file (systemd)" + val file = """ + [Files] + TemporaryFileSystem=tmp + TemporaryFileSystem=:mode=0755 + TemporaryFileSystem=relative:size=1M + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt new file mode 100644 index 00000000..3e701359 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseUdevPropertyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Property=ID_NET_MANAGED_BY=io.systemd.Network + Property=ID_NET_NAME_MAC=* + Property=FOO=bar BAZ=qux + Property=FOO= + Property="FOO=bar baz" + Property=FOO="bar baz" + Property='FOO=bar baz' X=1 + Property=HOST_%H=1 + Property=_X1=a=b + Property=DEVPATHX=1 + Property=TAGS_EXTRA=1 + Property="FOO"=bar + Property=F"OO"=bar + Property=FOO\x41=1 + Property='ID_X'=1 "Y"=2 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Property=FOO + Property=1FOO=bar + Property=INTERFACE=eth0 + Property=SYNTH_ARG_X=1 + Property=FOO-BAR=1 + Property=ACTION=add + Property="ACTION"=add + Property=FOO\q=1 + Property=F"OO=bar + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(9, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUplinkOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUplinkOptionValueTest.kt new file mode 100644 index 00000000..4e076f37 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUplinkOptionValueTest.kt @@ -0,0 +1,69 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseUplinkOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // `:none` is only accepted in [DHCPServer]/[IPv6SendRA] and `:self` only in [DHCPPrefixDelegation]. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + UplinkInterface=:auto + UplinkInterface=:none + UplinkInterface=dummy98 + UplinkInterface= + [DHCPPrefixDelegation] + UplinkInterface=:self + UplinkInterface=auto + UplinkInterface=veth99 + UplinkInterface=12 + UplinkInterface=1eth + [IPv6SendRA] + UplinkInterface=this-is-a-long-alternative-interface-name + UplinkInterface=0x10 + UplinkInterface=:none + UplinkInterface=010 + UplinkInterface=2147483647 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + UplinkInterface=:bogus + UplinkInterface=0 + UplinkInterface=eth0:1 + [DHCPPrefixDelegation] + UplinkInterface=all + UplinkInterface=eth/0 + [IPv6SendRA] + UplinkInterface=eth0 eth1 + UplinkInterface=:self + UplinkInterface=99999999999 + UplinkInterface=2147483648 + UplinkInterface=08 + [DHCPServer] + UplinkInterface=:self + [DHCPPrefixDelegation] + UplinkInterface=:none + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVethExtraOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVethExtraOptionValueTest.kt new file mode 100644 index 00000000..4b88c25c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVethExtraOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVethExtraOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST-13-NSPAWN. An invalid container-side name is silently replaced by the host-side one, so it is accepted. + // language="unit file (systemd)" + val file = """ + [Network] + VirtualEthernetExtra=my-fancy-veth1 + VirtualEthernetExtra=fancy-veth2:my-fancy-veth2 + VirtualEthernetExtra=ve-host: + VirtualEthernetExtra=ve-host:name-that-is-far-too-long + VirtualEthernetExtra=ve-a:all + VirtualEthernetExtra=a:b\:c + VirtualEthernetExtra=ve-a:0x10 + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Empty/too long/reserved/ifindex-like host-side name, anything after a second colon (even empty), whitespace in + // the name. + // language="unit file (systemd)" + val file = """ + [Network] + VirtualEthernetExtra=:ve-b + VirtualEthernetExtra=a:b:c + VirtualEthernetExtra=a:b: + VirtualEthernetExtra=averyveryverylongname + VirtualEthernetExtra=default + VirtualEthernetExtra=host name:x + VirtualEthernetExtra=0x10:ve-b + VirtualEthernetExtra=a\ b:c + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt new file mode 100644 index 00000000..bd29db52 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt @@ -0,0 +1,59 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVlanQosMapsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + EgressQOSMaps=21-7 45-5 + IngressQOSMaps=1-1 + EgressQOSMaps=5 + IngressQOSMaps=0x10-0x3 "7-2" '8-1' + EgressQOSMaps=4294967295-7 + IngressQOSMaps=1-7 2-6 + EgressQOSMaps=1-"2" "1"-2 3 + IngressQOSMaps=5--0 + EgressQOSMaps=0-7 + IngressQOSMaps=1-5 + EgressQOSMaps=4294967295-1 + EgressQOSMaps=0b+1-2 + EgressQOSMaps=1-0b+10 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + EgressQOSMaps=21:7 + IngressQOSMaps=1- + EgressQOSMaps=-5 + IngressQOSMaps=1-2-3 + EgressQOSMaps=4294967296-1 + IngressQOSMaps=a-b + EgressQOSMaps=-0 + IngressQOSMaps=1-"2 + EgressQOSMaps=0b 1-2 + EgressQOSMaps=0b-0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(10, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt new file mode 100644 index 00000000..10f5f748 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVlanprotocolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + Protocol=802.1q + Protocol=802.1Q + Protocol=802.1ad + Protocol=802.1AD + [Bridge] + VLANProtocol=802.1ad + VLANProtocol=802.1Q + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + Protocol=802.1Ad + Protocol=8021q + Protocol=qinq + [Bridge] + VLANProtocol=802.1x + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVolatileModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVolatileModeOptionValueTest.kt new file mode 100644 index 00000000..192c522c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVolatileModeOptionValueTest.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVolatileModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Table names plus every parse_boolean spelling (case-insensitive). + // language="unit file (systemd)" + val file = """ + [Files] + Volatile=no + Volatile=yes + Volatile=state + Volatile=overlay + Volatile=true + Volatile=false + Volatile=1 + Volatile=0 + Volatile=on + Volatile=off + Volatile=Yes + Volatile=TRUE + Volatile=oFf + Volatile=NO + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Table names are matched case-sensitively. + // language="unit file (systemd)" + val file = """ + [Files] + Volatile=State + Volatile=overlayfs + Volatile=maybe + Volatile=OVERLAY + Volatile=stateful + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt new file mode 100644 index 00000000..db886ccc --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVrfTableOptionValueTest : AbstractUnitFileTest() { + + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VRF] + Table=42 + Table=main + Table=local + Table=default + Table=0x2a + Table=custom-table + Table=4294967295 + Table=yes + Table=0x0 + Table=mian + Table=main 42 + Table=1 2 + Table=my table + Table=0b 1 + Table=0b+1 + Table=0o 7 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VRF] + Table=0 + Table=4294967296 + Table=08 + Table=vrf:42 + Table=040000000000 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt new file mode 100644 index 00000000..590ef709 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt @@ -0,0 +1,72 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVxlanAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + Group=239.1.1.1 + Group=ff02::42 + Local=192.168.1.1 + Local=fe80::1 + Local=dhcp4 + Local=ipv6_link_local + Local=slaac + Local=dhcp_pd + Remote=10.0.0.2 + Remote=2001:db8::1 + Remote=::ffff:10.0.0.2 + Group=FF05::2 + Group=224.0.0.1 + Group=239.255.255.255 + Remote=223.255.255.255 + Remote=fe80::1 + Local=::ffff:239.1.1.1 + Local=dhcp6 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + Group=239.1.1 + Local=dhcp + Remote=example.com + Remote=10.0.0.256 + Local=192.168.1.1/24 + Group=fe80::1::2 + Group=dhcp4 + Remote=slaac + Group=192.168.1.1 + Group=240.0.0.1 + Group=ff::1 + Group=::ffff:239.1.1.1 + Remote=239.1.1.1 + Remote=ff02::1 + Local=239.1.1.1 + Local=ff02::1 + Local=DHCP4 + Local=Slaac + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(18, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt new file mode 100644 index 00000000..497fd9dc --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardAllowedIpsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + AllowedIPs= + AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24 + AllowedIPs=0.0.0.0/0 + AllowedIPs=::/0 + AllowedIPs=10.0.0.1 + AllowedIPs=fd8d:4d6d:3ccb:0500:900c:d437:ec27:8822/128 + AllowedIPs=10.0.0.0/8, 192.168.0.0/16 + AllowedIPs=10.0.0.0/8 fd00::/8, + AllowedIPs=10.0.0.0/0x8 fd00::/0100 + AllowedIPs=10.0.0.0/0b+1000 + AllowedIPs=10.0.0.0/0o10 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + AllowedIPs=10.0.0.0/33 + AllowedIPs=fd00::/129 + AllowedIPs=10.0.0.0/8;192.168.0.0/16 + AllowedIPs=any + AllowedIPs=10.0.0/8 + AllowedIPs=10.0.0.0/ + AllowedIPs=10.0.0.0/0b 1 + AllowedIPs=10.0.0.0/ 8 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt new file mode 100644 index 00000000..fe53bfba --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardEndpointOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + Endpoint=192.168.27.3:51820 + Endpoint=[2001:db8::1]:51820 + Endpoint=wg.example.com:51820 + Endpoint=@network.wireguard.peer0.endpoint + Endpoint=192.168.27.3 + Endpoint=fe80::1 + Endpoint=vpn.example.org.:0x1F90 + Endpoint=[fe80::1%eth0]:51820 + Endpoint=host:65535 + Endpoint=a\255b.example.com:51820 + Endpoint=:51820 + Endpoint=192.168.27.3:0b+1 + Endpoint=192.168.27.3:0b 1 + Endpoint=[2001:db8::1]:0o 7 + Endpoint=host.example:0b 1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + Endpoint=wg.example.com + Endpoint=192.168.27.3:0 + Endpoint=192.168.27.3:65536 + Endpoint=wg..example.com:51820 + Endpoint=[2001:db8::1]: + Endpoint=wg.example.com:port + Endpoint=a\999b:51820 + Endpoint=192.168.27.3: 80 + Endpoint=host.example: 80 + Endpoint=192.168.27.3:0b -0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(10, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt new file mode 100644 index 00000000..65f151e7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardPeerKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + PublicKeyFile=/run/systemd/network/25-wireguard-public-key.txt + PresharedKeyFile=/etc/systemd/network/wg psk.key + PresharedKeyFile=/etc//systemd/./network/psk/ + PublicKeyFile=/devices/key + PublicKeyFile=/run/credentials/%i.key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + PublicKeyFile=relative/key + PresharedKeyFile=/etc/../root/psk + PublicKeyFile=/proc/self/environ + PresharedKeyFile=/dev/stdin + PublicKeyFile=%d/key + PresharedKeyFile=//sys/key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt new file mode 100644 index 00000000..bbc40d13 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardPeerRouteTableOptionValueTest : AbstractUnitFileTest() { + + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + RouteTable=off + RouteTable=no + RouteTable=0 + RouteTable=false + RouteTable=OFF + RouteTable=No + RouteTable=fAlSe + RouteTable=F + RouteTable=1234 + RouteTable=main + RouteTable=local + RouteTable=default + RouteTable=0x10 + RouteTable=4294967295 + RouteTable=custom + RouteTable=vpn-table + RouteTable=main table + RouteTable=0b 1 + RouteTable=0b+1 + RouteTable=0o 7 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + RouteTable=4294967296 + RouteTable=08 + RouteTable=custom:100 + RouteTable=00 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt new file mode 100644 index 00000000..68830390 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardPrivateKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKeyFile=/run/systemd/network/25-wireguard-private-key.txt + PrivateKeyFile=/etc/wireguard/my key + PrivateKeyFile=/./devices/key + PrivateKeyFile=/.dev/key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKeyFile=wg.key + PrivateKeyFile=/proc/1/environ + PrivateKeyFile=/etc/wireguard/../../shadow + PrivateKeyFile=/./dev/shm/key + PrivateKeyFile=//sys/key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt new file mode 100644 index 00000000..85839cff --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardPrivateKeyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= + PrivateKey=Eor9QBsIoUG6C3ZKsKdqm4Vkt0n2N7qpSh2LzphQeho= + PrivateKey=@network.wireguard.private + PrivateKey=EEGlnEPYJV__kbvvIqxKkQwOiS-UENyPncC4bF46ong= + PrivateKey=EEGlnEPYJV//kbvvIqxK kQwOiS+UENyPncC4bF46ong= + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46onh= + PrivateKey=short= + PrivateKey=@bad/name + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong== + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt new file mode 100644 index 00000000..b8212f90 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt @@ -0,0 +1,64 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardRouteTableOptionValueTest : AbstractUnitFileTest() { + + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + RouteTable=off + RouteTable=no + RouteTable=0 + RouteTable=false + RouteTable=OFF + RouteTable=No + RouteTable=fAlSe + RouteTable=F + RouteTable=1234 + RouteTable=main + RouteTable=local + RouteTable=default + RouteTable=0x10 + RouteTable=4294967295 + RouteTable=custom + RouteTable=vpn-table + RouteTable=yes + RouteTable=main table + RouteTable=0b 1 + RouteTable=0b+1 + RouteTable=0o 7 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + RouteTable=4294967296 + RouteTable=08 + RouteTable=custom:100 + RouteTable=00 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt new file mode 100644 index 00000000..78a342df --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWolPasswordOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + WakeOnLanPassword=00:11:22:33:44:55 + WakeOnLanPassword=00-11-22-33-44-55 + WakeOnLanPassword=0011.2233.4455 + WakeOnLanPassword=/etc/systemd/network/wol.pass + WakeOnLanPassword=/run/credentials/wol pass + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + WakeOnLanPassword=00:11:22:33:44 + WakeOnLanPassword=secret + WakeOnLanPassword=/etc/../wol.pass + WakeOnLanPassword=00:11:22:33:44:55:66:77 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt new file mode 100644 index 00000000..68281e56 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseXattrOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first three are from systemd's test/units/TEST-74-AUX-UTILS.socket-xattr.sh and the + // systemd.socket(5) Varlink example. + // language="unit file (systemd)" + val file = """ + [Socket] + XAttrEntryPoint=user.varlink=entrypoint + XAttrListen=user.varlink=listen + XAttrAccept=user.varlink=server + XAttrEntryPoint=user.unit=%n + XAttrListen=user.%N.tag=a=b=c + XAttrAccept=user.empty= + XAttrAccept=user.percent=100%% + XAttrAccept=user.literal=50%-off + XAttrAccept=user.trailing=5% + XAttrAccept=user.spaces=hello world + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + XAttrEntryPoint=security.selinux=foo + XAttrListen=trusted.x=y + XAttrAccept=user.varlink + XAttrEntryPoint=User.varlink=entrypoint + XAttrListen=user.x=%z + XAttrAccept=user.%9=y + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkDhcpMiscSafeAtoVerdictTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkDhcpMiscSafeAtoVerdictTest.kt new file mode 100644 index 00000000..72e9b038 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkDhcpMiscSafeAtoVerdictTest.kt @@ -0,0 +1,97 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseBridgeVlanIdOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcp6SendOptionOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpOptionTlvOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpOrRaRouteTableOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpPdSubnetIdOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpRequestOptionsOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpServerAddressOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDuidTypeOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseFdbInterfaceOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseLinkGroupOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseUplinkOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** + * Verdicts of the network-dhcp-misc validators for values that are awkward to write in a unit-file fixture + * (a vertical tab, a no-break space) or whose verdict changed when the batch's own integer terminal was replaced by + * the shared SafeAtoTerminal (#563). Checked on both grammar engines at once. + * + * Every value here was run through systemd's real parsers at 0f0682b773 (steve-local/systemd-tools/bin/sd-check, + * one value per .network file). + */ +class NetworkDhcpMiscSafeAtoVerdictTest { + + private val vt = "\u000B" + private val nbsp = " " + + private fun accepted(grammar: Combinator, vararg values: String) { + for (value in values) { + assertEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(grammar, value)) + assertEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(grammar, value)) + } + } + + private fun rejected(grammar: Combinator, vararg values: String) { + for (value in values) { + assertNotEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(grammar, value)) + assertNotEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(grammar, value)) + } + } + + @Test + fun testWholeValueIntegersAcceptLeadingVerticalTab() { + // strtoul skips C isspace(), which includes \v; config_parse only strips " \t\n\r". + accepted(ConfigParseBridgeVlanIdOptionValue().combinator, "${vt}5", "0b${vt}1") + accepted(ConfigParseLinkGroupOptionValue().combinator, "${vt}5", "0b${vt}1") + accepted(ConfigParseDhcpPdSubnetIdOptionValue().combinator, "${vt}5", "-0", "0b1") + accepted(ConfigParseDuidTypeOptionValue().combinator, "${vt}5", "0b 1:2000-01-01") + accepted(ConfigParseDhcpOrRaRouteTableOptionValue().combinator, "${vt}5", "0b 1") + } + + @Test + fun testSignedGroupRefusesNegativeAfterPrefix() { + rejected(ConfigParseLinkGroupOptionValue().combinator, "0b -1", "0b-1") + } + + @Test + fun testUnsignedWrapAround() { + // strtoul negates modulo 2^64 when the '-' is not the first character, so this is 1 (DUID-LLT). + accepted(ConfigParseDuidTypeOptionValue().combinator, "0b -" + "1".repeat(64)) + } + + @Test + fun testFieldsAfterColonOrSlashTakeCWhitespaceOnly() { + accepted(ConfigParseDhcpServerAddressOptionValue().combinator, "192.168.5.1/${vt}24", "192.168.5.1/ 24", "192.168.5.1/0b 11000") + rejected(ConfigParseDhcpServerAddressOptionValue().combinator, "192.168.5.1/${nbsp}24") + + accepted(ConfigParseDhcpOptionTlvOptionValue().combinator, "12:uint8:${vt}1", "${vt}12:uint8:1", "0b 1100:uint8:1", "12:uint16: 0b 1") + rejected(ConfigParseDhcpOptionTlvOptionValue().combinator, "${nbsp}12:uint8:1", "12:uint8:${nbsp}1") + + accepted(ConfigParseDhcp6SendOptionOptionValue().combinator, "${vt}12:uint8:1", "${vt}1:12:uint8:1") + rejected(ConfigParseDhcp6SendOptionOptionValue().combinator, "${nbsp}12:uint8:1", "${nbsp}1:12:uint8:1") + + rejected(ConfigParseDuidTypeOptionValue().combinator, "${nbsp}1") + } + + @Test + fun testRequestOptionsSplitsOnWhitespaceBeforeParsing() { + // Each word reaches safe_atou32 alone, so "0b 1" is the invalid word "0b" followed by "1". + rejected(ConfigParseDhcpRequestOptionsOptionValue().combinator, "0b 1", "0o 7", "0b +1", "0b -0", "0b-0") + accepted(ConfigParseDhcpRequestOptionsOptionValue().combinator, "0b+1", "1 0b1 0x10 010") + } + + @Test + fun testInterfaceIndexIsTheWholeValue() { + // parse_ifindex() is safe_atoi on the whole value, whitespace included. + for (grammar in listOf(ConfigParseFdbInterfaceOptionValue().combinator, ConfigParseUplinkOptionValue().combinator)) { + accepted(grammar, "0b 1", "${vt}5", "0o 7", "0x10", "+5", "0x" + "0".repeat(130) + "1") + rejected(grammar, "eth 0", "0b 0", "0b -1", "eth${nbsp}0") + } + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt new file mode 100644 index 00000000..197c61f8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseIpv6AddressLabelOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseIpv6AddressLabelPrefixOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseNdiscRouteMetricOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRouteMetricInitCwndOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRouteMultiPathOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleFirewallMarkOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleIpProtocolOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleUidRangeOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseStackedNetdevOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** + * Values containing a vertical tab (`\v`), which config_parse() does not strip (it only strips " \t\n\r") and + * which strtoul() skips. They are hard to write in a fixture file, so they are checked here on the grammar, for + * both engines. Every verdict was confirmed with steve-local/systemd-tools/bin/sd-check at 0f0682b773, one value + * per .network file. + */ +class NetworkRouteAddrSafeAtoVerdictsTest { + + private fun accepted(optionValue: GrammarOptionValue, value: String) { + assertEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(optionValue.combinator, value)) + assertEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(optionValue.combinator, value)) + } + + private fun rejected(optionValue: GrammarOptionValue, value: String) { + assertNotEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(optionValue.combinator, value)) + assertNotEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(optionValue.combinator, value)) + } + + @Test + fun testLeadingVerticalTabReachesSafeAtou() { + accepted(ConfigParseRouteMetricInitCwndOptionValue(), "\u000B5") + accepted(ConfigParseIpv6AddressLabelOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleFirewallMarkOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleFirewallMarkOptionValue(), "1/\u000B5") + accepted(ConfigParseNdiscRouteMetricOptionValue(), "\u000B5") + accepted(ConfigParseRouteMultiPathOptionValue(), "@eth0 \u000B5") + accepted(ConfigParseIpv6AddressLabelPrefixOptionValue(), "::/\u000B64") + // safe_atoi, through parse_ip_protocol_full: 6 is tcp. + accepted(ConfigParseRoutingPolicyRuleIpProtocolOptionValue(), "\u000B6") + } + + @Test + fun testParseUidLetsVerticalTabThrough() { + // SAFE_ATO_REFUSE_LEADING_WHITESPACE only refuses " \t\n\r"; strtoul then skips the \v. + accepted(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "5-\u000B6") + // A space is refused by the same flag (sd-check: "Invalid uid or uid range '5- 6'"). + rejected(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "5- 6") + } + + @Test + fun testIfindexWithVerticalTabIsNotAName() { + // parse_ifindex("\v5") is 5, so ifname_valid() refuses it (sd-check: "Invalid netdev name in VLAN="). + rejected(ConfigParseStackedNetdevOptionValue(), "\u000B5") + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt new file mode 100644 index 00000000..5148c1f5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt @@ -0,0 +1,153 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Grammar-level tests for the shared helpers in BatchNetdevLinkCombinators.kt, run against BOTH matching + * engines: the classic SyntacticMatch/SemanticMatch pair and the list-of-successes [validate]. The cases + * come from systemd's own unit tests at 0f0682b773 (test-parse-util.c, test-time-util.c, + * test-cpu-set-util.c) where one exists, otherwise from sd-check. The integer and route-table terminals + * themselves are covered by SafeAtoTerminalTest and RouteTableTest. + */ +class BatchNetdevLinkCombinatorsTest { + + private fun classicAccepts(grammar: Combinator, value: String): Boolean = + grammar.SyntacticMatch(value, 0).matchResult == value.length && grammar.SemanticMatch(value, 0).matchResult == value.length + + private fun assertAccepts(grammar: Combinator, expected: Boolean, vararg values: String) { + val whole = SequenceCombinator(grammar, EOF()) + for (value in values) { + assertEquals("classic engine on '$value'", expected, classicAccepts(whole, value)) + assertEquals("parse engine on '$value'", expected, whole.validate(value) == ParseOutcome.Valid) + } + } + + @Test + fun testSiSizeMatchesParseSizeSiTests() { + // TEST(parse_size_si) successes. + assertAccepts(siSize(), true, "111", "111.4", "112 B", "112.6 B", "3.5 K", "3. K", "3.0 K", "4 M 11.5K", + "3.5G3B", "3.5G 4B", "4T3G3B", "4 T 3 G 3 B", "12P", "3E 2P", "1000E", "10.M", "+5K") + // TEST(parse_size_si) failures (bar 1000E, an overflow this grammar does not compute). + assertAccepts(siSize(), false, "3. 0 K", "3B3.5G", "3B3G4T", "12P12P", "12X", "12.5X", "12.5e3", "-1", "-1000E", + "-10B 20K", "1k", "1 2") + } + + @Test + fun testNsecDurationMatchesParseNsecTests() { + // TEST(parse_nsec) successes, minus the two "infinity" spellings this grammar deliberately excludes. + assertAccepts(NSEC_DURATION_FINITE, true, "5s", "5s500ms", "5s 500ms", "5.5s", "5.5s 0.5ms", ".22s", ".50y", + "2.5", ".7", "+3.1s", "3.1s.2", "3.1 .2s", "3.1 sec .2 sec", "3.1 sec 1.2 sec", "1.111111111111y", "100 ns", "1µs") + assertAccepts(NSEC_DURATION_FINITE, false, "xyz", ".", "5.", ".s", "-5s", "-0.3s", "0.-0s", "3.-0s", "3.+1s", + "3. 1s", "3.s", "12..34", "..1234", "1234..", "infinity") + } + + @Test + fun testCpuSetMatchesParseCpuSetTests() { + // TEST(parse_cpu_set) successes. + assertAccepts(CPU_SET, true, "0", "1 2 4", "0 1 2 3 8 9 10 11", "8 '9' 10 \"11\"", "0,1,2,3 8,9,10,11", + "0, 1, 2, 3, 4, 5, 6, 7, 63, ", "0-3,8-11", "36-39,44-47", "64-71", "0-3 8-11, ", "0-7 4-11", "0,2 4-11", + "8000-8191", "0x0-0x3", ",0") + // TEST(parse_cpu_set) failures (bar "3-0", a reversed range this grammar does not compare). + assertAccepts(CPU_SET, false, "0 1 2 3 garbage", "0-3 8-garbage", "0 1 2 3 \"4 5 6 7 ", "8192", "1-", "-1") + // EXTRACT_UNQUOTE removes quotes anywhere in a word, and without EXTRACT_CUNESCAPE a backslash just + // makes the next character literal. + assertAccepts(CPU_SET, true, "0-\"3\"", "'0'-'3'", "\"0\"-3, 4", "0-\\3", "\" 3\"", "1-\"0x3\"") + // The lower bound of a range is cut at the first `-`, so `-0` is an empty word there. + assertAccepts(CPU_SET, false, "-0", "-0-3", "0-\"9000\"", "\"0-3", "\"3 \"", "0-\"\"", "0-", "0- 3") + + // The bounds are SafeAtoTerminal numbers in a word split on whitespace and commas (sd-check, IRQAffinity=): + // a sign after 0b/0o is fine, but `0b 1` is two words and the lower bound of `0b-0` is the bare word `0b`. + assertAccepts(CPU_SET, true, "0b+1", "0-0b+1", "0-0b-0", "0o17", "\"0b 1\"") + assertAccepts(CPU_SET, false, "0b-0", "0b 1", "0-0b 1", "0b-1-3") + } + + @Test + fun testParseRangeAcceptsSplitsAtTheFirstDash() { + val lower = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 99L, + acceptsWhitespace = true, + acceptsMinus = false, + ) + val upper = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 99L, + acceptsWhitespace = true, + acceptsMinus = true, + ) + for (word in listOf("5", "5-7", "5--0", "0x5-+7", "0", " 5", "5- 7", "0b 1-0b+1")) { + assertEquals("parse_range accepts <$word>", true, netdevLinkParseRangeAccepts(word, lower, upper)) + } + for (word in listOf("", "-0", "-5", "5-", "5 -7", "100", "5-100", "5-0b-1", "0b-0")) { + assertEquals("parse_range refuses <$word>", false, netdevLinkParseRangeAccepts(word, lower, upper)) + } + } + + @Test + fun testExtractWordUnquotesAndUnescapes() { + assertEquals("FOO=bar baz", extractWord("\"FOO=bar baz\"", true)) + assertEquals("FOO=bar", extractWord("F\"OO\"=bar", true)) + assertEquals("FOOA=1", extractWord("FOO\\x41=1", true)) + assertEquals("a b", extractWord("a\\sb", true)) + assertEquals("a\u00e9", extractWord("a\\u00e9", true)) + assertEquals("a\u0001", extractWord("a\\001", true)) + assertEquals(null, extractWord("FOO\\q", true)) + assertEquals(null, extractWord("a\\x00", true)) + assertEquals(null, extractWord("a\\400", true)) + assertEquals(null, extractWord("a\\uD800", true)) + assertEquals(null, extractWord("\"abc", true)) + assertEquals(null, extractWord("abc\\", false)) + assertEquals("aq", extractWord("a\\q", false)) + } + + @Test + fun testKeyFilePathRefusesApiVfsAndDotDot() { + assertAccepts(NON_API_VFS_ABSOLUTE_PATH, true, "/etc/key", "/etc/my key", "/devices/x", "/sysroot/key", "/etc//a/./b/", + "/./devices/x", "/.dev/x", "/..dev/x") + assertAccepts(NON_API_VFS_ABSOLUTE_PATH, false, "key", "/dev", "/dev/null", "//sys/x", "/proc/1/environ", "/a/../b", "/a/..", + "/./dev/shm/key", "/.//./proc", "//./sys/", "/dev/") + } + + @Test + fun testWireguardKeyNeedsPaddingAndAValidLastCharacter() { + assertAccepts(WIREGUARD_BASE64_KEY, true, "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong=", + "EEGlnEPYJV__kbvvIqxKkQwOiS-UENyPncC4bF46ong=", "EEGlnEPYJV//kbvvIqxK kQwOiS+UENyPncC4bF46ong =") + assertAccepts(WIREGUARD_BASE64_KEY, false, "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong", + "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46onh=", "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong==", "AAAA") + } + + @Test + fun testSafePrefixLengthIsASafeAtou8WithinTheFamilyWidth() { + // sd-check (AllowedIPs=): `/0b+1000`, `/0o10` and `0.0.0.0/0b-0` are accepted; `/0b-1` is refused, and so are + // `/0b 1` and `/ 8`, which the list splits into two words. + assertAccepts(IP_ADDR_AND_SAFE_PREFIX, true, "10.0.0.0/8", "10.0.0.0/0x8", "10.0.0.0/0b+1000", "10.0.0.0/0o10", + "0.0.0.0/0b-0", "10.0.0.1", "fd00::/0x40", "::/128") + assertAccepts(IP_ADDR_AND_SAFE_PREFIX, false, "10.0.0.0/33", "10.0.0.0/0b-1", "10.0.0.0/0b 1", "10.0.0.0/ 8", + "fd00::/129", "10.0.0.0/") + } + + @Test + fun testCheckedSpanRejectsWhatItsCheckRefusesOnBothEngines() { + val multicast = NetdevLinkCheckedSpan(IP_ADDR, "multicast") { netdevLinkIsMulticast(it) } + assertAccepts(multicast, true, "224.0.0.1", "239.255.255.255", "ff02::1", "FF05::2") + assertAccepts(multicast, false, "223.255.255.255", "240.0.0.1", "ff::1", "::ffff:239.1.1.1", "10.0.0.1", "239.1.1") + } + + @Test + fun testParseSizeSiTotals() { + assertEquals(java.math.BigInteger("12000000000000000"), netdevLinkParseSizeSi("12P")) + assertEquals(java.math.BigInteger("3002000000000000000"), netdevLinkParseSizeSi("3E 2P")) + assertEquals(java.math.BigInteger("4011500"), netdevLinkParseSizeSi("4 M 11.5K")) + assertEquals(java.math.BigInteger("10000000"), netdevLinkParseSizeSi("10.M")) + assertEquals(java.math.BigInteger("111"), netdevLinkParseSizeSi("111.4")) + assertEquals(java.math.BigInteger("5000"), netdevLinkParseSizeSi("+5K")) + assertEquals(null, netdevLinkParseSizeSi("5k")) + } +}