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..7dac176c 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt @@ -413,6 +413,32 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_socket_service", "0") to ConfigParseSocketServiceOptionValue() as OptionValueInformation, Validator("config_parse_service_sockets", "0") to ConfigParseServiceSocketsOptionValue() as OptionValueInformation, + // Batch 2026-10: 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, + ) return allValidators 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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) + } +}