From fbe667fed31fc09860af1f28d4ec325742f77d7e Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 12:02:20 -0700 Subject: [PATCH 1/8] feat: validate exec sandboxing, image, credential and environment settings Add grammar-based value validation for: ActivatingConcurrencyMax=, BindPaths=, BindReadOnlyPaths=, ConcurrencyHardMax=, ConcurrencySoftMax=, CoredumpFilter=, Environment=, ExtensionImages=, ImportCredential=, LUOSession=, MountImages=, NUMAMask=, RootHash=, RootHashSignature=, RootImageOptions=, StandardInputData=, TemporaryFileSystem= Accepted values follow systemd's own parsers (derived from the systemd source at 0f0682b773 and checked against systemd's real parsers built at that commit). Values systemd accepts when parsing but rejects only at unit or container start are accepted; see #565. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 16 ++ .../ai/ConfigParseBindPathsOptionValue.kt | 55 ++++++ .../ConfigParseConcurrencyMaxOptionValue.kt | 46 +++++ .../ai/ConfigParseEnvironOptionValue.kt | 77 ++++++++ ...onfigParseExecCoredumpFilterOptionValue.kt | 52 +++++ .../ai/ConfigParseExecInputDataOptionValue.kt | 28 +++ .../ai/ConfigParseExecRootHashOptionValue.kt | 39 ++++ .../ConfigParseExecRootHashSigOptionValue.kt | 39 ++++ .../ConfigParseExtensionImagesOptionValue.kt | 44 +++++ .../ConfigParseImportCredentialOptionValue.kt | 53 ++++++ .../ai/ConfigParseLuoSessionsOptionValue.kt | 32 ++++ .../ai/ConfigParseMountImagesOptionValue.kt | 50 +++++ .../ai/ConfigParseNumaMaskOptionValue.kt | 100 ++++++++++ .../ConfigParseRootImageOptionsOptionValue.kt | 49 +++++ ...figParseTemporaryFilesystemsOptionValue.kt | 53 ++++++ .../grammar/BatchExecCombinators.kt | 178 ++++++++++++++++++ .../ai/ConfigParseBindPathsOptionValueTest.kt | 96 ++++++++++ ...onfigParseConcurrencyMaxOptionValueTest.kt | 72 +++++++ .../ai/ConfigParseEnvironOptionValueTest.kt | 97 ++++++++++ ...gParseExecCoredumpFilterOptionValueTest.kt | 56 ++++++ ...ConfigParseExecInputDataOptionValueTest.kt | 46 +++++ .../ConfigParseExecRootHashOptionValueTest.kt | 47 +++++ ...nfigParseExecRootHashSigOptionValueTest.kt | 51 +++++ ...nfigParseExtensionImagesOptionValueTest.kt | 50 +++++ ...figParseImportCredentialOptionValueTest.kt | 58 ++++++ .../ConfigParseLuoSessionsOptionValueTest.kt | 45 +++++ .../ConfigParseMountImagesOptionValueTest.kt | 56 ++++++ .../ai/ConfigParseNumaMaskOptionValueTest.kt | 68 +++++++ ...figParseRootImageOptionsOptionValueTest.kt | 55 ++++++ ...arseTemporaryFilesystemsOptionValueTest.kt | 51 +++++ 30 files changed, 1759 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindPathsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseConcurrencyMaxOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEnvironOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecCoredumpFilterOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecInputDataOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashSigOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExtensionImagesOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseImportCredentialOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLuoSessionsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMountImagesOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNumaMaskOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRootImageOptionsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTemporaryFilesystemsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchExecCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindPathsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseConcurrencyMaxOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEnvironOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecCoredumpFilterOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecInputDataOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashSigOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExtensionImagesOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseImportCredentialOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLuoSessionsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMountImagesOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNumaMaskOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRootImageOptionsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTemporaryFilesystemsOptionValueTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 36704512..66f1f09e 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,22 @@ 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: exec + Validator("config_parse_bind_paths", "0") to ConfigParseBindPathsOptionValue() as OptionValueInformation, + Validator("config_parse_temporary_filesystems", "0") to ConfigParseTemporaryFilesystemsOptionValue() as OptionValueInformation, + Validator("config_parse_root_image_options", "0") to ConfigParseRootImageOptionsOptionValue() as OptionValueInformation, + Validator("config_parse_numa_mask", "0") to ConfigParseNumaMaskOptionValue() as OptionValueInformation, + Validator("config_parse_mount_images", "0") to ConfigParseMountImagesOptionValue() as OptionValueInformation, + Validator("config_parse_import_credential", "0") to ConfigParseImportCredentialOptionValue() as OptionValueInformation, + Validator("config_parse_extension_images", "0") to ConfigParseExtensionImagesOptionValue() as OptionValueInformation, + Validator("config_parse_exec_root_hash_sig", "0") to ConfigParseExecRootHashSigOptionValue() as OptionValueInformation, + Validator("config_parse_exec_root_hash", "0") to ConfigParseExecRootHashOptionValue() as OptionValueInformation, + Validator("config_parse_exec_input_data", "0") to ConfigParseExecInputDataOptionValue() as OptionValueInformation, + Validator("config_parse_exec_coredump_filter", "0") to ConfigParseExecCoredumpFilterOptionValue() as OptionValueInformation, + Validator("config_parse_environ", "0") to ConfigParseEnvironOptionValue() as OptionValueInformation, + Validator("config_parse_concurrency_max", "0") to ConfigParseConcurrencyMaxOptionValue() as OptionValueInformation, + Validator("config_parse_luo_sessions", "0") to ConfigParseLuoSessionsOptionValue() as OptionValueInformation, + ) return allValidators diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindPathsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindPathsOptionValue.kt new file mode 100644 index 00000000..f5db980c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindPathsOptionValue.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAINED_ABSOLUTE_PATH_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAINED_OPTIONAL_ABSOLUTE_PATH_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] BindPaths= / BindReadOnlyPaths=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BindPaths= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5187-L5305 config_parse_bind_paths (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (UNQUOTE|RETAIN_ESCAPE, then ':' with CUNESCAPE|UNESCAPE_SEPARATORS|DONT_COALESCE_SEPARATORS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240 extract_many_words_internal (4 slots: a 4th field is "Too many parameters") + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-load-fragment.c#L459 TEST(config_parse_bind_paths) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-bindpaths.service#L27-L30 + */ + +/** + * Validator for `BindPaths=` and `BindReadOnlyPaths=` (config_parse_bind_paths, ltype 0; the only + * difference between the two keys is `strstr(lvalue, "ReadOnly")`). + * + * A whitespace-separated list of `[-]SOURCE[:DESTINATION[:OPTIONS]]` tuples: + * - SOURCE and DESTINATION are absolute paths after specifier expansion (so `%t/foo` is fine) with no + * `..` component; only SOURCE may carry the `-` "ignore if missing" prefix. + * - OPTIONS is `rbind`, `norbind`, or empty (`/a:/b:` is accepted — `isempty(options)` means rbind). + * - fields are split with EXTRACT_DONT_COALESCE_SEPARATORS, so an empty field is a field: `/a:` and + * `/a::rbind` have an empty, non-absolute destination and are rejected; a fourth field is too many. + * - a ':' inside a path is written `\:`; other backslashes must be valid C escapes. + * + * Every rejected tuple is logged by systemd and skipped ("…, ignoring"). Lenient corners: quoted runs + * are opaque (see BatchExecCombinators.kt), and `%` specifiers are not checked. + */ +class ConfigParseBindPathsOptionValue : SimpleGrammarOptionValues( + "config_parse_bind_paths", + whitespaceList( + SequenceCombinator( + RETAINED_OPTIONAL_ABSOLUTE_PATH_FIELD, + ZeroOrOne( + SequenceCombinator( + COLON, + RETAINED_ABSOLUTE_PATH_FIELD, + ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(FlexibleLiteralChoiceTerminal("rbind", "norbind")))), + ) + ), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseConcurrencyMaxOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseConcurrencyMaxOptionValue.kt new file mode 100644 index 00000000..f103ca9b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseConcurrencyMaxOptionValue.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Slice] ConcurrencyHardMax= / ConcurrencySoftMax= / ActivatingConcurrencyMax=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html#ConcurrencyHardMax= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.slice.html#ActivatingConcurrencyMax= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6091-L6111 config_parse_concurrency_max (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L966 config_parse_unsigned → safe_atou + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429 safe_atou_full (base 0, plus mangle_base 0b/0o at L403) + */ + +/** + * Validator for the [Slice] concurrency limits (config_parse_concurrency_max, ltype 0): + * `ConcurrencyHardMax=`, `ConcurrencySoftMax=` and `ActivatingConcurrencyMax=`. + * + * `infinity` (exact, streq), or an unsigned int as safe_atou() reads it (see [SafeAtoTerminal]), up to + * UINT_MAX (4294967295). The value is the whole (stripped) setting, so whitespace can still reach the parser: + * `0b 1` and a leading `\v` are accepted, as are `0b+1`, `0b-0`, a negative zero such as `-0`, and `+`-signed + * numbers (but not `+0b1`/`-0b0`, which mangle_base misses). `0` is legal (it blocks all activation). + */ +class ConfigParseConcurrencyMaxOptionValue : SimpleGrammarOptionValues( + "config_parse_concurrency_max", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("infinity"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEnvironOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEnvironOptionValue.kt new file mode 100644 index 00000000..c240a40c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEnvironOptionValue.kt @@ -0,0 +1,77 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.C_ESCAPE_BODY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] Environment=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#Environment= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2763-L2834 config_parse_environ (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one (an unknown escape is -EINVAL: the rest of the line is ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L265 unit_env_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L81 env_assignment_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L31 env_name_is_valid_n (ASCII letters, digits, '_'; not starting with a digit) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-environment.service#L8 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-unsetenvironment.service#L10 + * linux https://man7.org/linux/man-pages/man7/environ.7.html + */ + +// One raw chunk of a word: a double- or single-quoted run, a C escape, or an ordinary character. +// Inside quotes a backslash is an escape too (EXTRACT_CUNESCAPE applies there as well). +private const val QUOTED_SYNTAX = """"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'""" +private const val QUOTED_SEMANTIC = """"(?:[^"\\]|\\$C_ESCAPE_BODY)*"|'(?:[^'\\]|\\$C_ESCAPE_BODY)*'""" +private const val WORD_SYNTAX = """(?:$QUOTED_SYNTAX|\\\S|[^\s"'\\])+""" +private const val WORD_SEMANTIC = """(?:$QUOTED_SEMANTIC|\\$C_ESCAPE_BODY|[^\s"'\\])+""" + +// A specifier stands in for name characters; which specifier, and what it expands to, is not checked. +private const val NAME_SPECIFIER = """%[^\s="'\\]""" + +// The plain form NAME=VALUE, where NAME has no quotes or backslashes in it. The name may contain +// specifiers (systemd's own test-execute uses `X%b=%U`). +private val ENV_NAME = RegexTerminal( + """[^\s="'\\]+""", + """(?:[A-Za-z_]|$NAME_SPECIFIER)(?:[A-Za-z0-9_]|$NAME_SPECIFIER)*(?![^\s="'\\])""", +) +private val ENV_VALUE = RegexTerminal(WORD_SYNTAX, "$WORD_SEMANTIC(?!\\S)") + +// Any word with a quote or backslash before its first '=' — the documented `"NAME=value with spaces"` +// form above all. The assignment only exists after unquoting, so this is one token: the lookahead +// requires that, skipping quotes, the word starts with a valid name followed by '='. Disjoint from the +// plain form (whose name may not contain a quote), so the two never both claim a word. +private val QUOTED_ENV_ASSIGNMENT = RegexTerminal( + """(?=[^\s="'\\]*["'\\])$WORD_SYNTAX""", + """(?=["']*(?:[A-Za-z_]|$NAME_SPECIFIER|\\\S)(?:["']|[A-Za-z0-9_]|$NAME_SPECIFIER|\\\S)*=)$WORD_SEMANTIC(?!\S)""", +) + +/** + * Validator for `Environment=` (config_parse_environ, ltype 0). + * + * A whitespace-separated list of `NAME=VALUE` assignments, each unquoted (`"VAR1=word1 word2"`, + * `VAR="a b"`) and C-unescaped before it is checked. NAME is ASCII letters, digits and `_`, not + * starting with a digit; VALUE may be empty (`FOO=`) and may contain anything, including further `=`. + * A word without `=` (e.g. the stray `production` in `UNICORN_OPTS=-E production`) is logged and + * skipped. An unbalanced quote or an unknown escape such as `\q` makes systemd drop the rest of the line. + * + * The man page says non-printable characters in values are rejected; env_value_is_valid() at + * 0f0682b773 only requires valid UTF-8, so control characters (e.g. `\t`) are accepted, following the C. + * + * Lenient corners: `%` specifiers are not checked, and in a word that is quoted before its `=` the name + * is only checked roughly (quotes inside it are skipped over). + */ +class ConfigParseEnvironOptionValue : SimpleGrammarOptionValues( + "config_parse_environ", + whitespaceList( + AlternativeCombinator( + SequenceCombinator(ENV_NAME, LiteralChoiceTerminal("="), ZeroOrOne(ENV_VALUE)), + QUOTED_ENV_ASSIGNMENT, + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecCoredumpFilterOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecCoredumpFilterOptionValue.kt new file mode 100644 index 00000000..b5a1e31e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecCoredumpFilterOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] CoredumpFilter=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#CoredumpFilter= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L802-L838 config_parse_exec_coredump_filter (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/coredump-util.c#L38-L81 coredump_filter_mask_from_string (words split on WHITESPACE, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/coredump-util.c#L24-L34 coredump_filter_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L95 safe_atoux64 (base 16) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-coredump-util.c#L26-L70 TEST(coredump_filter_mask_from_string) + * linux https://man7.org/linux/man-pages/man5/core.5.html + */ + +// A raw mask: strtoull(…, 16) — an optional 0x prefix, at most 16 significant hex digits (uint64_t). +// strtoull also takes a sign: '+' is fine, and '-' is only refused when the value is non-zero, so "-0" +// and "-0x00" are accepted (safe_atollu_full: `l != 0 && s[0] == '-'`). +// The trailing lookahead keeps this from matching a hex-looking prefix of a longer word (the "de" of +// "default"), so it and the names never claim the same word. +private val COREDUMP_FILTER_MASK = RegexTerminal( + """[+-]?[0-9a-fA-FxX]+(?![a-zA-Z0-9-])""", + """(?:\+?(?:0[xX])?0*[0-9a-fA-F]{1,16}|-(?:0[xX])?0+)(?![a-zA-Z0-9-])""", +) + +/** + * Validator for `CoredumpFilter=` (config_parse_exec_coredump_filter, ltype 0). + * + * A whitespace-separated list of mapping-type names (`private-anonymous`, `shared-anonymous`, + * `private-file-backed`, `shared-file-backed`, `elf-headers`, `private-huge`, `shared-huge`, + * `private-dax`, `shared-dax`), the specials `default` and `all`, or raw masks, which are always + * hexadecimal (`11` is 0x11; `0x1101` is fine) and must fit 64 bits. Names are matched exactly + * (streq / string table, case-sensitive). One bad word makes systemd ignore the whole assignment. + */ +class ConfigParseExecCoredumpFilterOptionValue : SimpleGrammarOptionValues( + "config_parse_exec_coredump_filter", + whitespaceList( + AlternativeCombinator( + COREDUMP_FILTER_MASK, + FlexibleLiteralChoiceTerminal( + "default", "all", + "private-anonymous", "shared-anonymous", "private-file-backed", "shared-file-backed", + "elf-headers", "private-huge", "shared-huge", "private-dax", "shared-dax", + ), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecInputDataOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecInputDataOptionValue.kt new file mode 100644 index 00000000..d4e20f3a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecInputDataOptionValue.kt @@ -0,0 +1,28 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NON_EMPTY_BASE64_DATA +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Service]/[Socket]/[Mount]/[Swap] StandardInputData=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#StandardInputData= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1271-L1327 config_parse_exec_input_data (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755 unbase64mem_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L528 unbase64char (standard and URL-safe alphabets) + */ + +/** + * Validator for `StandardInputData=` (config_parse_exec_input_data, ltype 0). + * + * Base64 as unbase64mem() decodes it: whitespace anywhere is ignored (so a value continued over several + * lines with a trailing `\` works), standard (`+/`) or URL-safe (`-_`) alphabet, complete 4-character + * groups, `=` padding only in the last group and only where the discarded bits are zero. No escapes or + * specifiers are resolved. The 64 MiB cumulative cap (EXEC_STDIN_DATA_MAX) is not checked. + */ +class ConfigParseExecInputDataOptionValue : SimpleGrammarOptionValues( + "config_parse_exec_input_data", + SequenceCombinator(NON_EMPTY_BASE64_DATA, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashOptionValue.kt new file mode 100644 index 00000000..aed48bb8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Service]/[Socket]/[Mount]/[Swap] RootHash=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RootHash= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1715-L1771 config_parse_exec_root_hash (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.h#L30 path_is_absolute (just `p[0] == '/'`) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L109-L158 unhexmem_full / unhex_next (whitespace skipped, pairs of hex digits) + */ + +/** + * Validator for `RootHash=` (config_parse_exec_root_hash, ltype 0). + * + * Either a path to a file holding the root hash, or the hash itself in hexadecimal: + * - the path branch is taken whenever path_is_absolute(), i.e. the value starts with `/`; the rest is + * stored verbatim (no specifier expansion, no normalization check), so nothing more is checked; + * - otherwise unhexmem() must decode it: an even number of hex digits (either case), whitespace + * anywhere ignored, and at least sizeof(sd_id128_t) = 16 bytes (32 digits) — shorter is "too short". + * + * Because there is no specifier expansion, `%t/foo.roothash` is not absolute and fails as hex — so it is + * flagged, matching systemd. + */ +class ConfigParseExecRootHashOptionValue : SimpleGrammarOptionValues( + "config_parse_exec_root_hash", + SequenceCombinator( + AlternativeCombinator( + RegexTerminal("""/[\s\S]*""", """/[\s\S]*"""), + RegexTerminal("""[^/][\s\S]*""", """(?:\s*[0-9a-fA-F]\s*[0-9a-fA-F]){16,}\s*(?![\s\S])"""), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashSigOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashSigOptionValue.kt new file mode 100644 index 00000000..61f3377e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExecRootHashSigOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BASE64_DATA +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Service]/[Socket]/[Mount]/[Swap] RootHashSignature=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RootHashSignature= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1773-L1832 config_parse_exec_root_hash_sig (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.h#L30 path_is_absolute (just `p[0] == '/'`) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755 unbase64mem_full (after the "base64:" prefix) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L528 unbase64char (standard and URL-safe alphabets) + */ + +/** + * Validator for `RootHashSignature=` (config_parse_exec_root_hash_sig, ltype 0). + * + * Either a path to a DER-encoded PKCS#7 signature (any value starting with `/`, stored verbatim with no + * specifier expansion), or `base64:` followed by the signature in Base64 — whitespace ignored, standard + * (`+/`) or URL-safe (`-_`) alphabet, complete 4-character groups, padding only at the end. The prefix + * is matched case-sensitively (startswith). An empty payload (`base64:`) decodes to zero bytes and is + * accepted by the parser. + */ +class ConfigParseExecRootHashSigOptionValue : SimpleGrammarOptionValues( + "config_parse_exec_root_hash_sig", + SequenceCombinator( + AlternativeCombinator( + RegexTerminal("""/[\s\S]*""", """/[\s\S]*"""), + SequenceCombinator(LiteralChoiceTerminal("base64:"), BASE64_DATA), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExtensionImagesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExtensionImagesOptionValue.kt new file mode 100644 index 00000000..248e98d4 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExtensionImagesOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EXEC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EXTENSION_IMAGE_SOURCE_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.imageMountOptionsTail +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] ExtensionImages=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#ExtensionImages= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5457-L5592 config_parse_extension_images (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (UNQUOTE|RETAIN_ESCAPE words, then ':' with CUNESCAPE|UNESCAPE_SEPARATORS, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (source specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721 path_below_api_vfs (/dev, /sys, /proc) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/gpt.c#L174 partition_designator_from_string + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-50-DISSECT.mountfsd.sh#L135 + */ + +/** + * Validator for `ExtensionImages=` (config_parse_extension_images, ltype 0). + * + * A whitespace-separated list of `[-]SOURCE[:OPTIONS | :PARTITION:OPTIONS…]` words. SOURCE is absolute + * after specifier expansion, free of `..`, and — unlike MountImages= — may not be below /dev, /proc or + * /sys (PATH_CHECK_NON_API_VFS, no DEV_OK). The option tail is the same as MountImages=: a single + * root-partition option string, or `PARTITION:OPTIONS` pairs. ':' separators are coalesced; a ':' in + * the path is written `\:`. + * + * Lenient corners: quoted runs are opaque, `%` specifiers and the mount options are not checked. + */ +class ConfigParseExtensionImagesOptionValue : SimpleGrammarOptionValues( + "config_parse_extension_images", + whitespaceList( + SequenceCombinator( + ZeroOrOne(EXEC_COALESCED_COLONS), + EXTENSION_IMAGE_SOURCE_FIELD, + imageMountOptionsTail(), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseImportCredentialOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseImportCredentialOptionValue.kt new file mode 100644 index 00000000..d892495c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseImportCredentialOptionValue.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Service]/[Socket]/[Mount]/[Swap] ImportCredential=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#ImportCredential=GLOB + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L4868-L4926 config_parse_import_credential (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (':' with EXTRACT_DONT_COALESCE_SEPARATORS; no unquoting) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L55 credential_glob_valid (glob, after unit_cred_printf) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L49 credential_name_valid (rename; filename_is_valid && fdname_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/fd-util.c#L554 fdname_is_valid (ASCII 0x20-0x7E, no ':', <= 255) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-creds.c#L301-L328 TEST(credential_glob_valid) + */ + +// A credential glob: printable ASCII without '/' or ':' and without the glob characters `*?[]`, except +// one trailing '*'; a bare '*' matches everything. The prefix must itself be a valid name, so '.', '..', +// '.*' and '..*' are refused. +private val CREDENTIAL_GLOB = RegexTerminal( + "[^:]+", + """\*|(?!\.\.?\*?(?::|$))[\x20-\x7E&&[^/:*?\[\]]]{1,255}\*?(?![^:])""", +) + +// The rename target: a credential name, checked verbatim (no specifier expansion), and — being the +// whole remainder of the value — it may not contain another ':'. +private val CREDENTIAL_RENAME = RegexTerminal("[^:]+", """(?!\.\.?$)[\x20-\x7E&&[^/:]]{1,255}(?![^:])""") + +/** + * Validator for `ImportCredential=` (config_parse_import_credential, ltype 0). + * + * `GLOB[:NAME]`. GLOB is a credential name optionally ending in a single `*` (or just `*`); NAME, if + * present, is the credential name to rename matches to (`foo.*:bar.` is the documented prefix-rename + * form). The value is not split on whitespace and not unquoted — credential names may contain spaces + * — and an empty NAME (`foo:`) is accepted. Credential names are printable ASCII (0x20–0x7E) without + * `/` or `:`, at most 255 characters, and not `.` or `..`. + * + * Lenient corner: `%` specifiers in GLOB are not checked (they are ordinary characters as far as the + * name rules go). + */ +class ConfigParseImportCredentialOptionValue : SimpleGrammarOptionValues( + "config_parse_import_credential", + SequenceCombinator( + CREDENTIAL_GLOB, + ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(CREDENTIAL_RENAME))), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLuoSessionsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLuoSessionsOptionValue.kt new file mode 100644 index 00000000..e70e259d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLuoSessionsOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service] LUOSession=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#LUOSession= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6847-L6903 config_parse_luo_sessions (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE, flags 0: `\x` → x, quotes literal) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/luo-util.c#L119 luo_session_name_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/fd-util.c#L554 fdname_is_valid (ASCII 0x20-0x7E, no ':', <= 255) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-util.c#L1154 string_is_safe (STRING_DISALLOW_WHITESPACE: no blanks, backslashes, quotes or `*?[`) + */ + +// One name character: printable ASCII except ':' (fdname), and except backslash, quotes and the glob +// characters `*?[` (string_is_safe). The word is extracted with flags 0, so a raw `\x` contributes x. +private const val LUO_CHAR = """[\x21-\x7E&&[^:\\"'*?\[]]""" + +/** + * Validator for `LUOSession=` (config_parse_luo_sessions, ltype 0). + * + * A whitespace-separated list of session names, each used as an fd-store FDNAME=: 1–255 printable + * ASCII characters, none of them `:`, a backslash, a quote, or `*`, `?`, `[`. Invalid names are logged + * and skipped. Not checked: a name repeated in the same unit ("Duplicate LUO session name, ignoring"). + */ +class ConfigParseLuoSessionsOptionValue : SimpleGrammarOptionValues( + "config_parse_luo_sessions", + whitespaceList(RegexTerminal("""\S+""", """(?:$LUO_CHAR|\\$LUO_CHAR){1,255}(?!\S)""")) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMountImagesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMountImagesOptionValue.kt new file mode 100644 index 00000000..3804aff3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMountImagesOptionValue.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EXEC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MOUNT_IMAGE_DESTINATION_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MOUNT_IMAGE_SOURCE_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.imageMountOptionsTail +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] MountImages=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#MountImages= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5307-L5455 config_parse_mount_images (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (UNQUOTE|RETAIN_ESCAPE words, then ':' with CUNESCAPE|UNESCAPE_SEPARATORS, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (source/destination specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS_DEV_OK) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721 path_below_api_vfs (/dev, /sys, /proc) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/gpt.c#L174 partition_designator_from_string + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-50-DISSECT.mountfsd.sh#L161 + */ + +/** + * Validator for `MountImages=` (config_parse_mount_images, ltype 0). + * + * A whitespace-separated list of `[-]SOURCE:DESTINATION[:OPTIONS | :PARTITION:OPTIONS…]` words: + * - SOURCE (optionally `-`-prefixed) and DESTINATION are both required, absolute after specifier + * expansion, free of `..`, and not below /proc or /sys (/dev is allowed: DEV_OK); + * - then either a single mount-option string for the root partition, or `PARTITION:OPTIONS` pairs + * (an optional lone trailing option string still applies to root). A pair whose partition name is + * unknown is logged and skipped. + * - ':' separators are coalesced, so `a::b` equals `a:b`; a ':' inside a path is written `\:`. + * + * Lenient corners: quoted runs are opaque, `%` specifiers and the mount options themselves are not + * checked. + */ +class ConfigParseMountImagesOptionValue : SimpleGrammarOptionValues( + "config_parse_mount_images", + whitespaceList( + SequenceCombinator( + ZeroOrOne(EXEC_COALESCED_COLONS), + MOUNT_IMAGE_SOURCE_FIELD, + EXEC_COALESCED_COLONS, + MOUNT_IMAGE_DESTINATION_FIELD, + imageMountOptionsTail(), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNumaMaskOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNumaMaskOptionValue.kt new file mode 100644 index 00000000..17c0a1be --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNumaMaskOptionValue.kt @@ -0,0 +1,100 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Service]/[Socket]/[Mount]/[Swap] NUMAMask=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#NUMAMask= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1572-L1608 config_parse_numa_mask (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3790-L3819 config_parse_unit_cpu_set (unit_full_printf, then config_parse_cpu_set) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L220 config_parse_cpu_set (words split on WHITESPACE "," with EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305 parse_range (N or N-M, '-' not coalesced) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429 safe_atou_full (base 0, plus mangle_base 0b/0o at L403) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L17 CPU_SET_MAX_NCPU 8192 (cpu_set_add_range refuses end >= it) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-36-NUMAPOLICY.sh#L166 + */ + +// parse_range() cuts the word at its first '-', so the lower bound never sees a '-' (`-0` is an empty lower +// word, refused) while the upper bound is the rest of the word and may itself start with one (`0--0` is 0-0). +// Both are below CPU_SET_MAX_NCPU (8192). The word was split on whitespace, so no whitespace reaches either. +private val NODE_RANGE = SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = false, + ), + ZeroOrOne( + SequenceCombinator( + HYPHEN, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + ), +) + +private val DOUBLE_QUOTE = LiteralChoiceTerminal("\"") +private val SINGLE_QUOTE = LiteralChoiceTerminal("'") + +// unit_full_printf() runs over the whole value before it is split, so a word with a specifier in it +// could expand to anything; such words are accepted unchecked. Tried first: it needs a '%', which a +// plain range never contains, so it cannot shadow one. +private val SPECIFIER_WORD = RegexTerminal("""[^\s,]*%[^\s,]*""", """[^\s,]*%[^\s,]*""") + +private val NODE_WORD = AlternativeCombinator( + SPECIFIER_WORD, + SequenceCombinator(DOUBLE_QUOTE, NODE_RANGE, DOUBLE_QUOTE), + SequenceCombinator(SINGLE_QUOTE, NODE_RANGE, SINGLE_QUOTE), + NODE_RANGE, +) + +// WHITESPACE and ',' are both separators and are coalesced, so leading/trailing/doubled ones are fine. +private val NODE_SEPARATOR = RegexTerminal("""[\s,]+""", """[\s,]+""") + +/** + * Validator for `NUMAMask=` (config_parse_numa_mask, ltype 0). + * + * Either exactly `all` (compared with streq before anything else) or a CPU-set style list: node + * indexes `N` or ranges `N-M`, separated by whitespace and/or commas. Each number is read by + * safe_atou() (see [SafeAtoTerminal]), so `0x10`, `010` (octal), `0b11`, `0o7` and `0b+1` are all legal + * spellings, and must be below CPU_SET_MAX_NCPU (8192). The upper bound may be a negative zero (`0--0`); + * the lower bound may not, because parse_range() cuts at the first `-`. systemd does not check against the machine's actual NUMA node count at + * parse time. + * + * Not checked: a reversed range such as `3-1` (systemd logs "Invalid CPU range" and skips that word — + * the grammar framework cannot compare two numbers); and words containing a `%` specifier, which are + * accepted unchecked. + */ +class ConfigParseNumaMaskOptionValue : SimpleGrammarOptionValues( + "config_parse_numa_mask", + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("all"), EOF()), + SequenceCombinator( + ZeroOrOne(NODE_SEPARATOR), + NODE_WORD, + ZeroOrMore(SequenceCombinator(NODE_SEPARATOR, NODE_WORD)), + ZeroOrOne(NODE_SEPARATOR), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRootImageOptionsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRootImageOptionsOptionValue.kt new file mode 100644 index 00000000..230d212c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRootImageOptionsOptionValue.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EXEC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAINED_FREE_FIELD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.partitionDesignator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] RootImageOptions=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RootImageOptions= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1648-L1713 config_parse_root_image_options (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/strv.c#L481-L531 strv_split_colon_pairs (UNQUOTE|RETAIN_ESCAPE words, then ':' with CUNESCAPE|UNESCAPE_SEPARATORS; >2 fields is -EINVAL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/gpt.c#L156-L183 partition_designator_table / partition_designator_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L249 unit_full_printf (options) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-gpt.c#L136-L142 partition designator shorthands accepted/rejected + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-50-DISSECT.mountfsd.sh#L155-L156 + */ + +/** + * Validator for `RootImageOptions=` (config_parse_root_image_options, ltype 0). + * + * A whitespace-separated list of `[PARTITION:]OPTIONS` words. A word with two fields names a partition + * designator first; a word with one field is the root partition's options. A third field makes + * strv_split_colon_pairs() fail with -EINVAL, which discards the whole assignment. + * + * The man page lists only root, usr, home, srv, esp, xbootldr, tmp and var; the C table also accepts + * swap, root-verity, usr-verity, root-verity-sig, usr-verity-sig and the -vty/-vsig shorthands, so this + * follows the C. The ':' separators are coalesced (no EXTRACT_DONT_COALESCE_SEPARATORS), so `root::ro` + * and a trailing ':' are accepted. The mount options themselves are free-form (handed to mount(8) at + * runtime) and only their escapes are checked. + */ +class ConfigParseRootImageOptionsOptionValue : SimpleGrammarOptionValues( + "config_parse_root_image_options", + whitespaceList( + SequenceCombinator( + ZeroOrOne(EXEC_COALESCED_COLONS), + AlternativeCombinator( + SequenceCombinator(partitionDesignator(), EXEC_COALESCED_COLONS, RETAINED_FREE_FIELD), + RETAINED_FREE_FIELD, + ), + ZeroOrOne(EXEC_COALESCED_COLONS), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTemporaryFilesystemsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTemporaryFilesystemsOptionValue.kt new file mode 100644 index 00000000..781f1ba3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTemporaryFilesystemsOptionValue.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.whitespaceList + +/* + * [Service]/[Socket]/[Mount]/[Swap] TemporaryFileSystem=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#TemporaryFileSystem= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5116-L5185 config_parse_temporary_filesystems (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (EXTRACT_UNQUOTE, then ':' with EXTRACT_DONT_COALESCE_SEPARATORS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L3347 temporary_filesystem_add (options stored verbatim) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-temporaryfilesystem-options.service#L10 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-execute/exec-temporaryfilesystem-ro.service#L35 + */ + +// Words are split with EXTRACT_UNQUOTE and WITHOUT EXTRACT_RETAIN_ESCAPE, so (unlike BindPaths=) a +// backslash escapes any following character, including a blank, both inside and outside quotes. +private const val TMPFS_CHUNK = """(?:[^\s:"'\\]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'|\\[\s\S])""" +private const val TMPFS_OPTIONS_CHUNK = """(?:[^\s"'\\]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'|\\[\s\S])""" + +// The mount point: absolute after specifier expansion, with no ".." component. +private val TMPFS_MOUNT_POINT = RegexTerminal( + "$TMPFS_CHUNK+", + """(?!(?:[^\s:]*/)?\.\.(?:[/\s:"']|$))(?=["']*[/%])$TMPFS_CHUNK+(?![^\s:])""", +) + +// Everything after the first ':' is the option string, stored verbatim (it may itself contain ':'). +private val TMPFS_OPTIONS = RegexTerminal("$TMPFS_OPTIONS_CHUNK+", "$TMPFS_OPTIONS_CHUNK+") + +/** + * Validator for `TemporaryFileSystem=` (config_parse_temporary_filesystems, ltype 0). + * + * A whitespace-separated list of `PATH[:OPTIONS]`. PATH is cut at the first ':' and must be absolute + * after specifier expansion, with no `..` component; OPTIONS is everything after that ':' (mount + * options such as `ro,mode=0700`), stored verbatim and only interpreted at mount time, so it is not + * checked. `PATH:` (empty options) is accepted. An invalid mount point is logged and skipped. + * + * Lenient corners: quoted runs are opaque (a ':' inside quotes is really the separator), and `%` + * specifiers are not checked. + */ +class ConfigParseTemporaryFilesystemsOptionValue : SimpleGrammarOptionValues( + "config_parse_temporary_filesystems", + whitespaceList( + SequenceCombinator(TMPFS_MOUNT_POINT, ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(TMPFS_OPTIONS)))) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchExecCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchExecCombinators.kt new file mode 100644 index 00000000..2c10db38 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchExecCombinators.kt @@ -0,0 +1,178 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +/* + * Shared value grammars for the ExecContext sandboxing / image / credential / environment settings in + * systemd src/core/load-fragment.c (exec batch, 2026-10). Everything here is pinned to systemd + * 0f0682b773, the commit recorded in systemd-build/build/last_commit_hash. + * + * words https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word + * escape https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one + * path https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn + * b64 https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755 unbase64mem_full + * + * The grammar sees the RAW value text, before systemd unquotes or unescapes anything, so the terminals + * below model the raw spelling of a word. Most of these settings split their value twice: + * + * 1. into whitespace-separated words with extract_first_word(…, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) + * — quotes are removed (and may appear anywhere in a word), backslashes are kept verbatim; + * 2. each word into ':'-separated fields with EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS — so a + * backslash must start a valid C escape (cunescape_one) or escape the ':' separator (`\:`). + * + * Deliberate approximations, shared by every grammar built from these pieces: + * - a quoted run ("…" or '…') is treated as opaque field text. systemd unquotes BEFORE splitting on + * ':', so a ':' inside quotes is really a separator. The grammar keeps it inside the field, which is + * usually lenient, but NOT always: when such a run is followed by more ':'-fields outside the quotes + * (`BindPaths="/a:/b":rbind`, `MountImages="/a.raw:/mnt":ro`), or a literal field such as rbind or + * a partition designator is itself quoted (`BindPaths=/a:/b:"rbind"`, `RootImageOptions="root":ro`), + * the field count shifts and a value systemd accepts is flagged. None of these spellings occurs in + * the corpus; modelling them needs a combinator that matches against the unquoted text. + * Escapes inside quotes are not checked. + * - under EXTRACT_RETAIN_ESCAPE a backslash is not special in the outer split, so a raw `\"` keeps the + * backslash and then opens a quote. The grammar therefore does not treat `\"` / `\'` as escapes + * outside quotes (an unbalanced quote is refused by systemd, and is flagged here too). + * - '%' specifiers are accepted wherever a path may start, without checking that the specifier exists + * or that it expands to an absolute path. + */ + +// `\uXXXX`: not NUL, not a UTF-16 surrogate (D800-DFFF). Noncharacters such as FFFE are deliberately allowed. +private const val ESCAPE_U16 = """u(?!0000)(?![dD][89abAB])[0-9a-fA-F]{4}""" + +// `\UXXXXXXXX`: not NUL, and unichar_is_valid(): below 0x110000, not a surrogate, not FDD0-FDEF, and the +// low 16 bits not FFFE/FFFF. +private const val ESCAPE_U32 = "U(?!0{8})(?=00(?:0[0-9a-fA-F]|10)[0-9a-fA-F]{4})(?!0000[dD][89abAB])" + + "(?!0000[fF][dD][dDeE])(?![0-9a-fA-F]{4}[fF]{3}[eEfF])[0-9a-fA-F]{8}" + +private const val C_ESCAPE_COMMON = """x(?!00)[0-9a-fA-F]{2}|$ESCAPE_U16|$ESCAPE_U32|(?!000)[0-3][0-7]{2}""" + +/** + * A valid C escape for cunescape_one, minus the leading backslash. NUL bytes, octal > 0377, UTF-16 + * surrogates in `\u` and invalid code points in `\U` (unichar_is_valid) are refused. + */ +internal const val C_ESCAPE_BODY = """(?:[abfnrtv\\"'s]|$C_ESCAPE_COMMON)""" + +// As C_ESCAPE_BODY but without `\"` / `\'`, which under EXTRACT_RETAIN_ESCAPE are not escapes in the outer +// split (the backslash is kept and the quote opens a quoted run). +private const val RETAINED_ESCAPE_BODY = """(?:[abfnrtv\\s]|$C_ESCAPE_COMMON)""" + +// --------------------------------------------------------------------------------------------------- +// ':'-separated fields of a word that was split with EXTRACT_RETAIN_ESCAPE and is then re-split with +// EXTRACT_CUNESCAPE|EXTRACT_UNESCAPE_SEPARATORS (BindPaths=, MountImages=, ExtensionImages=, +// RootImageOptions=). + +// Lenient shape: any run of non-blank, non-':' characters, quoted runs, and backslash + non-blank. +// A backslash followed by whitespace is never valid here: the outer split keeps the backslash and cuts +// the word at the blank, leaving a trailing backslash that the inner split refuses with -EINVAL. +private const val RETAINED_FIELD_SYNTAX = """(?:[^\s:"'\\]|"[^"]*"|'[^']*'|\\[^\s"']|\\(?=["']))+""" + +// Strict body: as above, but a backslash must start a C escape or escape ':' itself. A backslash before a +// quote is kept verbatim by the outer split and the quote then opens a quoted run, which must be closed; +// the escape the backslash forms with the run's first character is not checked. +private const val RETAINED_FIELD_BODY = """(?:[^\s:"'\\]|"[^"]*"|'[^']*'|\\(?:$RETAINED_ESCAPE_BODY|:|(?=["'])))+""" + +// The unquoted field must start with '/' (absolute) or a specifier. Quotes are skipped over first. +private const val ABSOLUTE_HEAD = """(?=["']*[/%])""" + +// path_is_normalized() refuses a surviving ".." component (path_simplify() has already removed "." and +// "//"). The optional `[^\s:]*/` swallows leading directories so "/a/../b" and "/a/.." are caught but +// "/a/..b" and "/a/b.." are not. +private const val NO_DOT_DOT_COMPONENT = """(?!(?:[^\s:]*/)?\.\.(?:[/\s:"']|$))""" + +// path_below_api_vfs(): "/proc", "/sys" and (unless DEV_OK) "/dev", as whole leading components. +private const val NOT_PROC_OR_SYS = """(?!["']*-?["']*/+(?:proc|sys)(?:[/\s:"']|$))""" +private const val NOT_API_VFS = """(?!["']*-?["']*/+(?:proc|sys|dev)(?:[/\s:"']|$))""" + +// Every semantic regex in this file ends with a lookahead that stops it matching a strict prefix of the +// span its syntactic regex claimed. The classic engine localises a semantic failure by re-walking the +// syntactic token list, and a semantic match that ends mid-token makes it compute a highlight range past +// the end of the value (IntelliJ then logs "rangeInElement … must not exceed …"). +private const val FIELD_END = """(?![^\s:])""" + +private fun retainedField(head: String) = RegexTerminal(RETAINED_FIELD_SYNTAX, head + RETAINED_FIELD_BODY + FIELD_END) + +/** An absolute path field (after unit_path_printf), as path_simplify_and_warn(PATH_CHECK_ABSOLUTE) checks it. */ +val RETAINED_ABSOLUTE_PATH_FIELD = retainedField(NO_DOT_DOT_COMPONENT + ABSOLUTE_HEAD) + +/** An absolute path field optionally prefixed by `-` ("ignore if missing"), as BindPaths= sources are. */ +val RETAINED_OPTIONAL_ABSOLUTE_PATH_FIELD = retainedField(NO_DOT_DOT_COMPONENT + """(?=["']*-?["']*[/%])""") + +/** MountImages= source: `-`-prefixable absolute path, not below /proc or /sys (PATH_CHECK_NON_API_VFS_DEV_OK). */ +val MOUNT_IMAGE_SOURCE_FIELD = retainedField(NOT_PROC_OR_SYS + NO_DOT_DOT_COMPONENT + """(?=["']*-?["']*[/%])""") + +/** MountImages= destination: absolute path, not below /proc or /sys (PATH_CHECK_NON_API_VFS_DEV_OK). */ +val MOUNT_IMAGE_DESTINATION_FIELD = retainedField(NOT_PROC_OR_SYS + NO_DOT_DOT_COMPONENT + ABSOLUTE_HEAD) + +/** ExtensionImages= source: `-`-prefixable absolute path, not below /proc, /sys or /dev (PATH_CHECK_NON_API_VFS). */ +val EXTENSION_IMAGE_SOURCE_FIELD = retainedField(NOT_API_VFS + NO_DOT_DOT_COMPONENT + """(?=["']*-?["']*[/%])""") + +/** + * A free-form field — mount options, which systemd only runs through unit_full_printf() and hands to + * mount(8) later. Only the escape rules are checked. + */ +val RETAINED_FREE_FIELD = retainedField("") + +/** + * The ':' run between fields of a MountImages=/ExtensionImages=/RootImageOptions= word. Those three + * split WITHOUT EXTRACT_DONT_COALESCE_SEPARATORS, so `a::b` is the same as `a:b` and leading/trailing + * colons are skipped. + */ +val EXEC_COALESCED_COLONS = RegexTerminal(":+", ":+") + +/** + * A partition designator — partition_designator_from_string (src/shared/gpt.c#L156-L183). The table + * names, plus the "-vty"/"-vsig" shorthands that partition_name_expand() rewrites to "-verity" / + * "-verity-sig" (the "2" = "-secondary" shorthand expands to names the table does not contain, so it + * is never accepted here). Matching is case-sensitive (string_table_lookup_from_string uses streq). + */ +fun partitionDesignator() = FlexibleLiteralChoiceTerminal( + "root", "usr", "home", "srv", "esp", "xbootldr", "swap", + "root-verity", "usr-verity", "root-verity-sig", "usr-verity-sig", "tmp", "var", + "root-vty", "usr-vty", "root-vsig", "usr-vsig", +) + +/** + * The trailing `[:OPTIONS | (:PARTITION:OPTIONS)*[:OPTIONS]]` part of a MountImages=/ExtensionImages= + * word: the loop in config_parse_mount_images / config_parse_extension_images reads pairs with + * extract_many_words(partition, options); a lone trailing word is the root partition's options, while + * a pair whose first word is not a partition designator is logged and skipped. + */ +fun imageMountOptionsTail(): Combinator = SequenceCombinator( + ZeroOrMore(SequenceCombinator(EXEC_COALESCED_COLONS, partitionDesignator(), EXEC_COALESCED_COLONS, RETAINED_FREE_FIELD)), + optionalNoReprobe(SequenceCombinator(EXEC_COALESCED_COLONS, RETAINED_FREE_FIELD)), + ZeroOrOne(EXEC_COALESCED_COLONS), +) + +private val EMPTY = RegexTerminal("", "") + +/** + * Like [ZeroOrOne], but without its second probe. ZeroOrOne tries its inner combinator again after a + * match to report how far input "could" reach; when the inner can match twice in a row (`:OPTIONS` can), + * a semantic failure further left then reports a longest-match at the very end of the value, and the + * classic engine's error localisation indexes one past its token list (IndexOutOfBoundsException, + * logged as an internal error). An alternative with an empty branch has no such probe. + */ +fun optionalNoReprobe(inner: Combinator): Combinator = AlternativeCombinator(inner, EMPTY) + +/** A whitespace-separated list of [item]s ending the value. */ +fun whitespaceList(item: Combinator): Combinator = SequenceCombinator( + item, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), item)), + EOF(), +) + +// --------------------------------------------------------------------------------------------------- +// Base64 — unbase64mem_full / unbase64_next / unbase64char (src/basic/hexdecoct.c#L528-L850). +// +// Whitespace is skipped anywhere. Both the standard (+/) and URL-safe (-_) alphabets are accepted. The +// non-blank characters must form complete 4-character groups; padding may only appear as the last one +// or two characters of the last group, and then the bits it discards must be zero (b & 15 / c & 3). + +private const val B64 = """[A-Za-z0-9+/_-]""" +private const val B64_GROUPS = """(?:(?:\s*$B64){4})*""" +private const val B64_TAIL = + """(?:\s*$B64\s*[AQgw]\s*=\s*=|\s*$B64\s*$B64\s*[AEIMQUYcgkosw048]\s*=)?\s*""" + +/** Base64 as unbase64mem() accepts it, possibly empty. */ +val BASE64_DATA = RegexTerminal("""[\s\S]*""", """$B64_GROUPS$B64_TAIL(?![\s\S])""") + +/** Base64 as unbase64mem() accepts it, at least one group. */ +val NON_EMPTY_BASE64_DATA = RegexTerminal("""[\s\S]+""", """(?=\s*\S)$B64_GROUPS$B64_TAIL(?![\s\S])""") diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindPathsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindPathsOptionValueTest.kt new file mode 100644 index 00000000..61aaed36 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindPathsOptionValueTest.kt @@ -0,0 +1,96 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBindPathsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidServiceValues() { + // Values from systemd's test-load-fragment.c, test/test-execute/exec-bindpaths.service and the corpus. + // language="unit file (systemd)" + val file = """ + [Service] + BindPaths=/tmp:/tmp/test-exec-bindpaths + BindPaths="/tmp/test-exec-bindpaths source:/tmp/test-exec-bindpaths destination" /tmp/test-exec-bindpaths\:source:/tmp/test-exec-bindpaths\:destination + BindReadOnlyPaths= /usr/bin /usr/lib /lib64 + BindReadOnlyPaths=-/usr/lib -/usr/lib64 /usr/bin /usr/sbin + BindPaths=/etc /home:/mnt:norbind -/foo/bar/baz:/usr:rbind + BindPaths=%f:/tmp/scrub:norbind + BindPaths=%t/foo:%h/bar + BindPaths=/a:/b: + BindPaths=/tmp/markers/ + BindPaths=/a/./b//c:/d + BindPaths=/a\x20b:/c\sd + BindReadOnlyPaths=/dev/log:/var/lib/haproxy/dev/log + BindReadOnlyPaths='/srv/a b':/srv/c + BindPaths= + BindPaths=/a\"b":/c + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidInOtherUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BindPaths=/run/foo:/run/bar + BindReadOnlyPaths=-/etc/resolv.conf + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BindPaths=/run/foo:/run/bar:rbind + BindReadOnlyPaths=/etc/machine-id + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BindPaths=/run/foo + BindReadOnlyPaths=/run/dbus/system_bus_socket + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BindPaths=relative/path + BindPaths=/a:relative + BindPaths=/a: + BindPaths=/a::rbind + BindPaths=/a:/b:ro + BindPaths=/a:/b:rbind:extra + BindPaths=/a/../b + BindPaths=/a:-/b + BindPaths=/a\qb + BindReadOnlyPaths=/usr/bin foo + BindReadOnlyPaths=/usr/bin /a:/b:recursive + BindPaths=/a\" + BindPaths=/a\ud800 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(13, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseConcurrencyMaxOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseConcurrencyMaxOptionValueTest.kt new file mode 100644 index 00000000..f2605b38 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseConcurrencyMaxOptionValueTest.kt @@ -0,0 +1,72 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseConcurrencyMaxOptionValueTest : AbstractUnitFileTest() { + + // Every value below was checked with sd-check (systemd 0f0682b773). The 0b/0o forms with a sign or a space after + // the prefix are accepted because strtoul() starts afresh after mangle_base(); "0b -1" and "0b-1" are out of + // range (-ERANGE), "+0b1", "-0b0", "+0o7", "08" and "0x" are -EINVAL. (systemd also accepts a leading \v, but the + // unit-file lexer does not let one into a value, so SafeAtoTerminalTest covers that.) + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Slice] + ConcurrencySoftMax=infinity + ConcurrencyHardMax=10 + ActivatingConcurrencyMax=0 + ConcurrencySoftMax=4294967295 + ConcurrencyHardMax=0x10 + ActivatingConcurrencyMax=010 + ConcurrencySoftMax=0b101 + ConcurrencyHardMax=0o17 + ActivatingConcurrencyMax=+5 + ConcurrencySoftMax= + ConcurrencyHardMax=-0 + ActivatingConcurrencyMax=-0x0 + ConcurrencySoftMax=+0x10 + ConcurrencyHardMax=0b+1 + ActivatingConcurrencyMax=0b-0 + ConcurrencySoftMax=0b 1 + ConcurrencyHardMax=0o 7 + ActivatingConcurrencyMax=0b +1 + ConcurrencySoftMax=0b -0 + """.trimIndent() + + setupFileInEditor("file.slice", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Slice] + ConcurrencySoftMax=4294967296 + ConcurrencyHardMax=-1 + ActivatingConcurrencyMax=Infinity + ConcurrencySoftMax=unlimited + ConcurrencyHardMax=08 + ActivatingConcurrencyMax=10 20 + ConcurrencySoftMax=1.5 + ConcurrencyHardMax=+0b1 + ActivatingConcurrencyMax=+0o7 + ConcurrencySoftMax=-0b0 + ConcurrencyHardMax=0b-1 + ActivatingConcurrencyMax=0b -1 + ConcurrencySoftMax=0x + """.trimIndent() + + setupFileInEditor("file.slice", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(13, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEnvironOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEnvironOptionValueTest.kt new file mode 100644 index 00000000..26c55d1f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEnvironOptionValueTest.kt @@ -0,0 +1,97 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseEnvironOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From systemd's test/test-execute/exec-environment*.service, exec-unsetenvironment.service and the corpus. + // language="unit file (systemd)" + val file = """ + [Service] + Environment="VAR1=word1 word2" VAR2=word3 "VAR3=${'$'}word 5 6" + Environment="VAR3=foobar" + Environment=FOO=bar QUUX=waldo VAR3=value3 VAR4=value4 VAR5=value5 X%b=%U VAR6=value6 + Environment=TOP_SECRET=%d/very_top_secret + Environment='SHM_MEMORY=64' + Environment="TZ=:/etc/localtime" + Environment=SHELL_PROMPT_PREFIX="pre-udev " + Environment=PYTHONPATH='/path/to/mysite' + Environment=USER= CONFIG= HOST= PORT= EXTRAOPTS= + Environment=JAVA_OPTS=-Djava.awt.headless=true + Environment=USER=${'$'}{SERVICE_USER} HOME=${'$'}{SERVICE_HOME} + Environment="OPTIONS=" + Environment=_UNDERSCORE=1 lower_case=2 A1=3 + Environment=TAB=a\tb NL=x\ny HEX=\x41 OCT=\101 UNI=é SP=a\sb + Environment="QUOTED=say \"hi\"" + Environment="FOO"=bar + Environment=U=\uFFFE V=\U0010FFFD W=\U0001F600 + Environment= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidInOtherUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + Environment=FOO=bar + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + Environment="A=b c" + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + Environment=A=1 B=2 + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + Environment=UNICORN_OPTS=-E production + Environment=FOO + Environment=1FOO=bar + Environment==bar + Environment=FOO-BAR=1 + Environment="FOO=unbalanced + Environment=FOO=a\qb + Environment="=bar" + Environment="NOEQUALS" + Environment=FOO.BAR=1 + Environment=FOO=\ud800 + Environment=FOO=\U00110000 + Environment=FOO=\U0000FDD0 + Environment=FOO=\U0001FFFF + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(14, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecCoredumpFilterOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecCoredumpFilterOptionValueTest.kt new file mode 100644 index 00000000..be8f14bf --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecCoredumpFilterOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExecCoredumpFilterOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + CoredumpFilter=default + CoredumpFilter=all + CoredumpFilter=default private-dax shared-dax + CoredumpFilter=private-anonymous shared-anonymous private-file-backed shared-file-backed elf-headers private-huge shared-huge + CoredumpFilter=private-file-backed private-dax 0xF + CoredumpFilter=11 + CoredumpFilter=0x1101 + CoredumpFilter=0 + CoredumpFilter=ff deadbeef FFFFFFFFFFFFFFFF + CoredumpFilter=-0 + CoredumpFilter=-0x00 +1f + CoredumpFilter= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + CoredumpFilter=defaulta + CoredumpFilter=default defaulta default + CoredumpFilter=Default + CoredumpFilter=private + CoredumpFilter=0x + CoredumpFilter=1FFFFFFFFFFFFFFFF + CoredumpFilter=12g + CoredumpFilter=-1 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecInputDataOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecInputDataOptionValueTest.kt new file mode 100644 index 00000000..2f5ca4d0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecInputDataOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExecInputDataOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + StandardInputData=V2XigLJyZSBubyBzdHJhbmdlcnMgdG8gbG92ZQpZb3Uga25vdyB0aGUgcnVsZXMgYW5kIHNvIGRv \ + IEkKQSBmdWxsIGNvbW1pdG1lbnQncyB3aGF0IEnigLJtIHRoaW5raW5nIG9mCllvdSB3b3VsZG4n + StandardInputData=aGVsbG8= + StandardInputData=aGVsbA== + StandardInputData=aGVs + StandardInputData= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + StandardInputData=hello + StandardInputData=aGVsbG8 + StandardInputData=a!b= + StandardInputData==== + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashOptionValueTest.kt new file mode 100644 index 00000000..f0042d63 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExecRootHashOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootHash=e0e5e0b9e6d9bd4b8d2e2e3cbb6f0f4d0b0c0e5e0b9e6d9bd4b8d2e2e3cbb6f0 + RootHash=0123456789ABCDEF0123456789abcdef + RootHash=01234567 89abcdef 01234567 89abcdef + RootHash=/var/lib/images/foo.roothash + RootHash=/anything goes here + RootHash= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootHash=0123456789abcdef + RootHash=0123456789abcdef0123456789abcdef0 + RootHash=0123456789abcdefg123456789abcdef + RootHash=%S/foo.roothash + RootHash=foo.roothash + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashSigOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashSigOptionValueTest.kt new file mode 100644 index 00000000..059fcc46 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExecRootHashSigOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExecRootHashSigOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootHashSignature=/var/lib/images/foo.roothash.p7s + RootHashSignature=base64:MIIB + RootHashSignature=base64:MIIBAA== + RootHashSignature=base64:MIIBAAE= + RootHashSignature=base64:MI IB AA== + RootHashSignature=base64:-_-_ + RootHashSignature=base64: + RootHashSignature= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootHashSignature=MIIBAA== + RootHashSignature=base64:MII + RootHashSignature=base64:MI*B + RootHashSignature=base64:AB== + RootHashSignature=base64:=AAA + RootHashSignature=BASE64:MIIB + RootHashSignature=base64:AA==AAAA + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExtensionImagesOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExtensionImagesOptionValueTest.kt new file mode 100644 index 00000000..2ef1535d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExtensionImagesOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseExtensionImagesOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + ExtensionImages=/tmp/app0.raw + ExtensionImages=/tmp/app-data-only.raw + ExtensionImages=-/srv/ext.raw /srv/ext2.raw:root:ro + ExtensionImages=/srv/ext.raw:x-systemd.relax-extension-release-check + ExtensionImages=/srv/ext.raw:root:ro,nosuid:usr:nodev + ExtensionImages=/srv/ext.raw:nosuid: + ExtensionImages=%S/ext.raw + ExtensionImages=/srv/a\:b.raw + ExtensionImages= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + ExtensionImages=ext.raw + ExtensionImages=/dev/sda1 + ExtensionImages=/proc/x.raw + ExtensionImages=/srv/ext.raw:bogus:ro + ExtensionImages=/srv/../ext.raw + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseImportCredentialOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseImportCredentialOptionValueTest.kt new file mode 100644 index 00000000..56fa4eed --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseImportCredentialOptionValueTest.kt @@ -0,0 +1,58 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseImportCredentialOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + ImportCredential=fsck.* + ImportCredential=test.creds.* + ImportCredential=test.creds.*:renamed.creds. + ImportCredential=test.creds.first:renamed.creds.first + ImportCredential=brummbaer + ImportCredential=* + ImportCredential=x* + ImportCredential=my cred + ImportCredential=foo: + ImportCredential=%N.* + ImportCredential= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + ImportCredential=. + ImportCredential=.. + ImportCredential=foo/bar + ImportCredential=? + ImportCredential=*a + ImportCredential=a? + ImportCredential=a[abc] + ImportCredential=foo:bar:baz + ImportCredential=foo:.. + ImportCredential=foo:a/b + ImportCredential=:foo + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLuoSessionsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLuoSessionsOptionValueTest.kt new file mode 100644 index 00000000..ddecad52 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLuoSessionsOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseLuoSessionsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + LUOSession=mysession + LUOSession=vm-memory vm.state cache_1 + LUOSession=a]b + LUOSession= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + LUOSession=foo:bar + LUOSession=foo* + LUOSession="quoted" + LUOSession=what? + LUOSession=ok bad[1] + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMountImagesOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMountImagesOptionValueTest.kt new file mode 100644 index 00000000..4ff391b6 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMountImagesOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMountImagesOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + MountImages=/tmp/app0.raw:/var/tmp:noatime,nosuid + MountImages=/tmp/app0.raw:/var/tmp:nosuid + MountImages=-/srv/foo.raw:/srv/foo + MountImages=/srv/foo.raw:/srv/foo:root:ro,nosuid:usr:nodev + MountImages=/srv/foo.raw:/srv/foo:root:ro:nosuid + MountImages=/srv/foo.raw:/srv/foo:root: + MountImages=/srv/foo.raw::/srv/foo + MountImages=/dev/sda1:/mnt/disk + MountImages=%S/img.raw:%t/img + MountImages=/srv/a\:b.raw:/srv/c /srv/d.raw:/srv/e:ro + MountImages=/srv/foo.raw:/srv/foo:rootcontext=x + MountImages= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + MountImages=/srv/foo.raw + MountImages=/srv/foo.raw: + MountImages=relative.raw:/srv/foo + MountImages=/srv/foo.raw:relative + MountImages=/srv/foo.raw:/proc/foo + MountImages=/sys/foo.raw:/srv/foo + MountImages=/srv/foo.raw:/srv/foo:bogus:ro + MountImages=/srv/foo.raw:/srv/../foo + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNumaMaskOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNumaMaskOptionValueTest.kt new file mode 100644 index 00000000..c69f15c4 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNumaMaskOptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNumaMaskOptionValueTest : AbstractUnitFileTest() { + + // Every value below was checked with sd-check (systemd 0f0682b773). parse_range() cuts each word at its first + // '-', so the upper bound may be "-0" or "+1" ("0--0", "0-+1") while "-0" alone has an empty lower bound and + // "0b-0" has the lower bound "0b" (both "Failed to parse CPU range"). + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + NUMAMask=0 + NUMAMask=all + NUMAMask=0-3 + NUMAMask=0,2,4-7 + NUMAMask=0 1 2 + NUMAMask=0, 1,,2 + NUMAMask=0x10 010 0b11 0o7 + NUMAMask=8191 + NUMAMask="0-3" + NUMAMask=%i + NUMAMask= + NUMAMask=0b+1 + NUMAMask=0-0b+1 + NUMAMask=0--0 + NUMAMask=0-+1 + NUMAMask=+1 + NUMAMask=0-0b-0 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + NUMAMask=8192 + NUMAMask=0-8192 + NUMAMask=-1 + NUMAMask=1- + NUMAMask=all 1 + NUMAMask=ALL + NUMAMask=foo + NUMAMask=08 + NUMAMask=+0b1 + NUMAMask=-0 + NUMAMask=0b-0 + NUMAMask=1-2-3 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRootImageOptionsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRootImageOptionsOptionValueTest.kt new file mode 100644 index 00000000..10f17176 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRootImageOptionsOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRootImageOptionsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootImageOptions=root:ro,noatime home:ro,dev + RootImageOptions=root:ro,noatime,nosuid home:ro,dev nosuid,dev + RootImageOptions=home:ro,dev nosuid,dev,%%foo + RootImageOptions=root:nosuid + RootImageOptions=ro + RootImageOptions=usr:ro xbootldr:ro esp:ro srv:ro tmp:ro var:ro swap:ro + RootImageOptions=root-verity:ro usr-verity-sig:ro root-vty:ro usr-vsig:ro + RootImageOptions=root::ro + RootImageOptions=root:ro: + RootImageOptions=rootcontext=system_u\:object_r\:etc_t\:s0 + RootImageOptions=uid=0\:1 + RootImageOptions= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RootImageOptions=bogus:ro + RootImageOptions=root:ro:nosuid + RootImageOptions=ROOT:ro + RootImageOptions=root2:ro + RootImageOptions=esp-vty:ro + RootImageOptions=ro,x\qy + RootImageOptions=rootcontext=system_u:object_r:etc_t:s0 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTemporaryFilesystemsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTemporaryFilesystemsOptionValueTest.kt new file mode 100644 index 00000000..33500262 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTemporaryFilesystemsOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTemporaryFilesystemsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + TemporaryFileSystem=/var:ro + TemporaryFileSystem=/var:ro,mode=0700,nostrictatime + TemporaryFileSystem=/tmp:ro /usr:ro + TemporaryFileSystem=/var + TemporaryFileSystem=/var: + TemporaryFileSystem=%t/foo:size=10% + TemporaryFileSystem=/run/a\ b:ro + TemporaryFileSystem="/run/a b":ro + TemporaryFileSystem=/run/x:context=system_u:object_r:tmp_t:s0 + TemporaryFileSystem= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + TemporaryFileSystem=var:ro + TemporaryFileSystem=:ro + TemporaryFileSystem=/var/../etc + TemporaryFileSystem=/tmp relative + TemporaryFileSystem="/unbalanced + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} From b9d98be93f9eb3b97c6e7283cd1ded47c6873667 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 12:02:20 -0700 Subject: [PATCH 2/8] feat: validate seccomp, filesystem-restriction, BPF and socket device settings Add grammar-based value validation for: BPFDelegateAttachments=, BPFDelegateCommands=, BPFDelegateMaps=, BPFDelegatePrograms=, BPFProgram=, BindToDevice=, NFTSet=, PrivateBPF=, RestrictFileSystems=, SystemCallFilter=, SystemCallLog=, XAttrAccept=, XAttrEntryPoint=, XAttrListen= Accepted values follow systemd's own parsers (derived from the systemd source at 0f0682b773 and checked against systemd's real parsers built at that commit). Values systemd accepts when parsing but rejects only at unit or container start are accepted; see #565. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 14 ++ ...gParseBpfDelegateAttachmentsOptionValue.kt | 57 +++++ ...nfigParseBpfDelegateCommandsOptionValue.kt | 52 +++++ .../ConfigParseBpfDelegateMapsOptionValue.kt | 52 +++++ ...nfigParseBpfDelegateProgramsOptionValue.kt | 56 +++++ ...ConfigParseBpfForeignProgramOptionValue.kt | 61 +++++ .../ai/ConfigParseCgroupNftSetOptionValue.kt | 104 +++++++++ .../ai/ConfigParsePrivateBpfOptionValue.kt | 39 ++++ ...nfigParseRestrictFilesystemsOptionValue.kt | 172 ++++++++++++++ ...onfigParseSocketBindtodeviceOptionValue.kt | 111 ++++++++++ .../ai/ConfigParseSyscallFilterOptionValue.kt | 150 +++++++++++++ .../ai/ConfigParseSyscallLogOptionValue.kt | 62 ++++++ .../ai/ConfigParseXattrOptionValue.kt | 50 +++++ .../BatchSeccompBpfCgroupCombinators.kt | 209 ++++++++++++++++++ ...seBpfDelegateAttachmentsOptionValueTest.kt | 73 ++++++ ...ParseBpfDelegateCommandsOptionValueTest.kt | 73 ++++++ ...nfigParseBpfDelegateMapsOptionValueTest.kt | 73 ++++++ ...ParseBpfDelegateProgramsOptionValueTest.kt | 73 ++++++ ...igParseBpfForeignProgramOptionValueTest.kt | 102 +++++++++ .../ConfigParseCgroupNftSetOptionValueTest.kt | 90 ++++++++ .../ConfigParsePrivateBpfOptionValueTest.kt | 83 +++++++ ...ParseRestrictFilesystemsOptionValueTest.kt | 128 +++++++++++ ...gParseSocketBindtodeviceOptionValueTest.kt | 83 +++++++ ...ConfigParseSyscallFilterOptionValueTest.kt | 176 +++++++++++++++ .../ConfigParseSyscallLogOptionValueTest.kt | 77 +++++++ .../ai/ConfigParseXattrOptionValueTest.kt | 50 +++++ 26 files changed, 2270 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 66f1f09e..35dc68d4 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 @@ -428,6 +428,20 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_environ", "0") to ConfigParseEnvironOptionValue() as OptionValueInformation, Validator("config_parse_concurrency_max", "0") to ConfigParseConcurrencyMaxOptionValue() as OptionValueInformation, Validator("config_parse_luo_sessions", "0") to ConfigParseLuoSessionsOptionValue() as OptionValueInformation, + // Batch 2026-10: seccomp-bpf-cgroup + Validator("config_parse_syscall_filter", "0") to ConfigParseSyscallFilterOptionValue() as OptionValueInformation, + Validator("config_parse_syscall_log", "0") to ConfigParseSyscallLogOptionValue() as OptionValueInformation, + Validator("config_parse_restrict_filesystems", "0") to ConfigParseRestrictFilesystemsOptionValue() as OptionValueInformation, + Validator("config_parse_private_bpf", "0") to ConfigParsePrivateBpfOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_commands", "0") to ConfigParseBpfDelegateCommandsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_maps", "0") to ConfigParseBpfDelegateMapsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_programs", "0") to ConfigParseBpfDelegateProgramsOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_delegate_attachments", "0") to ConfigParseBpfDelegateAttachmentsOptionValue() as OptionValueInformation, + Validator("config_parse_cgroup_nft_set", "NFT_SET_PARSE_CGROUP") to ConfigParseCgroupNftSetOptionValue() as OptionValueInformation, + Validator("config_parse_bpf_foreign_program", "0") to ConfigParseBpfForeignProgramOptionValue() as OptionValueInformation, + Validator("config_parse_xattr", "0") to ConfigParseXattrOptionValue() as OptionValueInformation, + Validator("config_parse_socket_bindtodevice", "0") to ConfigParseSocketBindtodeviceOptionValue() as OptionValueInformation, + // END Batch 2026-10: seccomp-bpf-cgroup ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt new file mode 100644 index 00000000..6a596ce9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateAttachmentsOptionValue.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateAttachments= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateAttachments= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L181 config_parse_bpf_delegate_attachments (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L287 bpf_delegate_attachments_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4197 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_attach_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_attach_type_table from enum bpf_attach_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L52 BPFDelegateAttachments=BPFFlowDissector,BPFCgroupSysctl,BPFNetfilter + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateAttachments=` (the bpffs `delegate_attachs=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_attach_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateAttachments, Socket.BPFDelegateAttachments, Mount.BPFDelegateAttachments, Swap.BPFDelegateAttachments. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateAttachmentsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_attachments", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFCgroupInetIngress", "BPFCgroupInetEgress", "BPFCgroupInetSockCreate", "BPFCgroupSockOps", "BPFSkSkbStreamParser", + "BPFSkSkbStreamVerdict", "BPFCgroupDevice", "BPFSkMsgVerdict", "BPFCgroupInet4Bind", "BPFCgroupInet6Bind", + "BPFCgroupInet4Connect", "BPFCgroupInet6Connect", "BPFCgroupInet4PostBind", "BPFCgroupInet6PostBind", "BPFCgroupUdp4Sendmsg", + "BPFCgroupUdp6Sendmsg", "BPFLircMode2", "BPFFlowDissector", "BPFCgroupSysctl", "BPFCgroupUdp4Recvmsg", + "BPFCgroupUdp6Recvmsg", "BPFCgroupGetsockopt", "BPFCgroupSetsockopt", "BPFTraceRawTp", "BPFTraceFentry", + "BPFTraceFexit", "BPFModifyReturn", "BPFLsmMac", "BPFTraceIter", "BPFCgroupInet4Getpeername", + "BPFCgroupInet6Getpeername", "BPFCgroupInet4Getsockname", "BPFCgroupInet6Getsockname", "BPFXdpDevmap", "BPFCgroupInetSockRelease", + "BPFXdpCpumap", "BPFSkLookup", "BPFXdp", "BPFSkSkbVerdict", "BPFSkReuseportSelect", + "BPFSkReuseportSelectOrMigrate", "BPFPerfEvent", "BPFTraceKprobeMulti", "BPFLsmCgroup", "BPFStructOps", + "BPFNetfilter", "BPFTcxIngress", "BPFTcxEgress", "BPFTraceUprobeMulti", "BPFCgroupUnixConnect", + "BPFCgroupUnixSendmsg", "BPFCgroupUnixRecvmsg", "BPFCgroupUnixGetpeername", "BPFCgroupUnixGetsockname", "BPFNetkitPrimary", + "BPFNetkitPeer", "BPFTraceKprobeSession", "BPFTraceUprobeSession", "BPFTraceFsession", "BPFTraceFentryMulti", + "BPFTraceFexitMulti", "BPFTraceFsessionMulti", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt new file mode 100644 index 00000000..09f4167e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateCommandsOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateCommands= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateCommands= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L178 config_parse_bpf_delegate_commands (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L263 bpf_delegate_commands_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4194 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_cmd) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_cmd_table from enum bpf_cmd in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L49 BPFDelegateCommands=BPFObjPin,BPFBtfLoad,BPFMapFreeze,BPFLinkDetach + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateCommands=` (the bpffs `delegate_cmds=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_cmd` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateCommands, Socket.BPFDelegateCommands, Mount.BPFDelegateCommands, Swap.BPFDelegateCommands. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateCommandsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_commands", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFMapCreate", "BPFMapLookupElem", "BPFMapUpdateElem", "BPFMapDeleteElem", "BPFMapGetNextKey", + "BPFProgLoad", "BPFObjPin", "BPFObjGet", "BPFProgAttach", "BPFProgDetach", + "BPFProgTestRun", "BPFProgGetNextId", "BPFMapGetNextId", "BPFProgGetFdById", "BPFMapGetFdById", + "BPFObjGetInfoByFd", "BPFProgQuery", "BPFRawTracepointOpen", "BPFBtfLoad", "BPFBtfGetFdById", + "BPFTaskFdQuery", "BPFMapLookupAndDeleteElem", "BPFMapFreeze", "BPFBtfGetNextId", "BPFMapLookupBatch", + "BPFMapLookupAndDeleteBatch", "BPFMapUpdateBatch", "BPFMapDeleteBatch", "BPFLinkCreate", "BPFLinkUpdate", + "BPFLinkGetFdById", "BPFLinkGetNextId", "BPFEnableStats", "BPFIterCreate", "BPFLinkDetach", + "BPFProgBindMap", "BPFTokenCreate", "BPFProgStreamReadByFd", "BPFProgAssocStructOps", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt new file mode 100644 index 00000000..b9c66217 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateMapsOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegateMaps= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegateMaps= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L179 config_parse_bpf_delegate_maps (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L271 bpf_delegate_maps_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4195 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_map_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_map_type_table from enum bpf_map_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L50 BPFDelegateMaps=BPFMapTypeArray,BPFMapTypeCpumap,BPFMapTypeRingbuf + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegateMaps=` (the bpffs `delegate_maps=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_map_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Covers: Service.BPFDelegateMaps, Socket.BPFDelegateMaps, Mount.BPFDelegateMaps, Swap.BPFDelegateMaps. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateMapsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_maps", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFMapTypeUnspec", "BPFMapTypeHash", "BPFMapTypeArray", "BPFMapTypeProgArray", "BPFMapTypePerfEventArray", + "BPFMapTypePercpuHash", "BPFMapTypePercpuArray", "BPFMapTypeStackTrace", "BPFMapTypeCgroupArray", "BPFMapTypeLruHash", + "BPFMapTypeLruPercpuHash", "BPFMapTypeLpmTrie", "BPFMapTypeArrayOfMaps", "BPFMapTypeHashOfMaps", "BPFMapTypeDevmap", + "BPFMapTypeSockmap", "BPFMapTypeCpumap", "BPFMapTypeXskmap", "BPFMapTypeSockhash", "BPFMapTypeCgroupStorageDeprecated", + "BPFMapTypeReuseportSockarray", "BPFMapTypePercpuCgroupStorageDeprecated", "BPFMapTypeQueue", "BPFMapTypeStack", "BPFMapTypeSkStorage", + "BPFMapTypeDevmapHash", "BPFMapTypeStructOps", "BPFMapTypeRingbuf", "BPFMapTypeInodeStorage", "BPFMapTypeTaskStorage", + "BPFMapTypeBloomFilter", "BPFMapTypeUserRingbuf", "BPFMapTypeCgrpStorage", "BPFMapTypeArena", "BPFMapTypeInsnArray", + "BPFMapTypeRhash", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt new file mode 100644 index 00000000..646b2410 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfDelegateProgramsOptionValue.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.bpfDelegateList + +/* + * BPFDelegatePrograms= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#BPFDelegatePrograms= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L180 config_parse_bpf_delegate_programs (0), via DEFINE_CONFIG_PARSE_PTR + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.h#L279 bpf_delegate_programs_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4221-L4253 bpf_delegate_from_string ("any", or a "," list; unknown names warned and ignored) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4196 DEFINE_STRING_TABLE_LOOKUP(bpf_delegate_prog_type) — exact streq + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/generate-bpf-delegate-configs.py builds bpf_delegate_prog_type_table from enum bpf_prog_type in src/include/uapi/linux/bpf.h + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.private-bpf.sh#L51 BPFDelegatePrograms=BPFProgTypeTracepoint,BPFProgTypeXdp,BPFProgTypeTracing + * linux https://man7.org/linux/man-pages/man2/bpf.2.html + */ + +/** + * Validator for `BPFDelegatePrograms=` (the bpffs `delegate_progs=` mount option; only effective with `PrivateBPF=yes`). + * + * Either `any` (the whole value) or a comma-separated list of the names below — see [bpfDelegateList] + * for the splitting rules (commas only, no spaces; empty fields are skipped). The names are what + * src/core/generate-bpf-delegate-configs.py makes of each `BPF_…` enumerator of `enum bpf_prog_type` in the + * uapi bpf.h systemd bundles at 0f0682b773 (`BPF_FOO_BAR` → `BPFFooBar`; aliases written as + * `X = Y,` are skipped by its regex and therefore not accepted). Regenerate with + * + * python3 src/core/generate-bpf-delegate-configs.py code src/include/uapi/linux/bpf.h + * + * An unknown name is logged ("Unknown BPF delegate option, ignoring") and dropped, so it is flagged. + * The list tracks the bundled kernel header, so a newer systemd may know names this one does not. + * + * Quirk faithfully reproduced: the generator's `(\w+)\b,` full-match also skips enumerators followed + * by a comment, so `BPF_PROG_TYPE_SYSCALL, /* … */` (bpf.h#L1094) never becomes `BPFProgTypeSyscall` + * and that spelling is rejected by systemd (and every later program type is shifted down one bit — + * an upstream bug, see the batch QUESTIONS.md). + * + * Covers: Service.BPFDelegatePrograms, Socket.BPFDelegatePrograms, Mount.BPFDelegatePrograms, Swap.BPFDelegatePrograms. + * + * The empty assignment (mask 0, i.e. nothing delegated) never reaches the grammar. + */ +class ConfigParseBpfDelegateProgramsOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_delegate_programs", + bpfDelegateList( + FlexibleLiteralChoiceTerminal( + "BPFProgTypeUnspec", "BPFProgTypeSocketFilter", "BPFProgTypeKprobe", "BPFProgTypeSchedCls", "BPFProgTypeSchedAct", + "BPFProgTypeTracepoint", "BPFProgTypeXdp", "BPFProgTypePerfEvent", "BPFProgTypeCgroupSkb", "BPFProgTypeCgroupSock", + "BPFProgTypeLwtIn", "BPFProgTypeLwtOut", "BPFProgTypeLwtXmit", "BPFProgTypeSockOps", "BPFProgTypeSkSkb", + "BPFProgTypeCgroupDevice", "BPFProgTypeSkMsg", "BPFProgTypeRawTracepoint", "BPFProgTypeCgroupSockAddr", "BPFProgTypeLwtSeg6local", + "BPFProgTypeLircMode2", "BPFProgTypeSkReuseport", "BPFProgTypeFlowDissector", "BPFProgTypeCgroupSysctl", "BPFProgTypeRawTracepointWritable", + "BPFProgTypeCgroupSockopt", "BPFProgTypeTracing", "BPFProgTypeStructOps", "BPFProgTypeExt", "BPFProgTypeLsm", + "BPFProgTypeSkLookup", "BPFProgTypeNetfilter", + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt new file mode 100644 index 00000000..51ebc47d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBpfForeignProgramOptionValue.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UNIT_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * BPFProgram= ([Service]/[Socket]/[Mount]/[Swap]/[Slice]/[Scope]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html#BPFProgram=type:program-path + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5878-L5938 config_parse_bpf_foreign_program (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (":", no flags; separators coalesce) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/bpf-program.c#L25-L46 bpf_cgroup_attach_type_table (exact streq lookup) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE, normalized) + * linux https://man7.org/linux/man-pages/man2/bpf.2.html (BPF_PROG_ATTACH attach types) + */ + +/** bpf_cgroup_attach_type_table: the cgroup attach types, by their bpftool names. */ +private val BPF_CGROUP_ATTACH_TYPE = FlexibleLiteralChoiceTerminal( + "ingress", "egress", "sock_create", "sock_ops", "device", "bind4", "bind6", "connect4", "connect6", + "post_bind4", "post_bind6", "sendmsg4", "sendmsg6", "sysctl", "recvmsg4", "recvmsg6", "getsockopt", "setsockopt", +) + +/** + * Validator for `BPFProgram=TYPE:PATH`. + * + * config_parse_bpf_foreign_program() splits off the first `:`-delimited word (extract_first_word with + * `:` as the only separator; a run of colons coalesces, so `egress::/x` still leaves `/x`), requires + * something after it (leading colons are skipped too, so `:egress:/x` is the type `egress`), and looks the word up in bpf_cgroup_attach_type_table — one of `ingress`, + * `egress`, `sock_create`, `sock_ops`, `device`, `bind4`, `bind6`, `connect4`, `connect6`, + * `post_bind4`, `post_bind6`, `sendmsg4`, `sendmsg6`, `sysctl`, `recvmsg4`, `recvmsg6`, `getsockopt`, + * `setsockopt` (exact, case-sensitive). + * + * The rest is a path: specifiers are expanded (unit_path_printf), then path_simplify_and_warn() + * requires it to be absolute and normalized (no `..` component). That is [UNIT_PATH]: it must start + * with `/` or a specifier and contain no `..` component; the rest of the value belongs to the path, so + * further colons are part of it. Every failure is "…, ignoring", so it is flagged. + * + * Not checked: that the path is on bpffs (the man page says it generally starts `/sys/fs/bpf/`, but the + * parser does not require it — the attach simply fails at runtime). + * + * Covers: Service.BPFProgram, Socket.BPFProgram, Mount.BPFProgram, Swap.BPFProgram, Slice.BPFProgram, + * Scope.BPFProgram. + * + * The empty assignment removes all programs and never reaches the grammar. + */ +class ConfigParseBpfForeignProgramOptionValue : SimpleGrammarOptionValues( + "config_parse_bpf_foreign_program", + SequenceCombinator( + ZeroOrOne(RegexTerminal(":+", ":+")), + BPF_CGROUP_ATTACH_TYPE, + RegexTerminal(":+", ":+"), + UNIT_PATH, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt new file mode 100644 index 00000000..c4942d68 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCgroupNftSetOptionValue.kt @@ -0,0 +1,104 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * NFTSet= in the cgroup context ([Service]/[Socket]/[Mount]/[Swap]/[Slice]/[Scope]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html#NFTSet=family:table:set (term anchor as generated from the man page) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6779-L6795 config_parse_cgroup_nft_set (NFT_SET_PARSE_CGROUP) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1207-L1295 config_parse_nft_set + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1136 nft_set_source_table (only cgroup/user/group allowed for NFT_SET_PARSE_CGROUP, #L1258-L1260) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1125 nfproto_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L824 nft_identifier_valid (NFT_NAME_MAXLEN 256) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240 extract_many_words (":" , EXTRACT_CUNESCAPE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L39-L55 extract_first_word (skips leading separators; trailing ones coalesce at #L151-L158) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.nft.sh#L24 NFTSet=cgroup:inet:sd_test:c user:inet:sd_test:u group:inet:sd_test:g + * linux https://www.netfilter.org/projects/nftables/manpage.html (nft(8): address families, table/set names) + */ + +/** The NFT_SET_PARSE_CGROUP sources; address/prefix/ifindex belong to networkd's NFTSet= only. */ +private val NFT_CGROUP_SOURCE = FlexibleLiteralChoiceTerminal("cgroup", "user", "group") + +/** nfproto_table: the nft address families. */ +private val NFT_FAMILY = FlexibleLiteralChoiceTerminal("arp", "bridge", "inet", "ip", "ip6", "netdev") + +/** + * nft_identifier_valid: an ASCII letter, then letters, digits, `/`, `\`, `_` or `.`, shorter than + * NFT_NAME_MAXLEN (256). The lenient side takes anything up to the next `:` or whitespace so a bad + * name is located as one token. + */ +private val NFT_IDENTIFIER = RegexTerminal("""[^\s:"']+""", """[A-Za-z][A-Za-z0-9/\\_.]{0,254}""") + +/** + * extract_many_words() is extract_first_word() in a loop, so runs of `:` coalesce (`cgroup::inet:t:s` + * is still four fields). + */ +private val FIELD_SEPARATOR = RegexTerminal(":+", ":+") + +/** + * extract_first_word() skips leading separators, and after the 4th word it swallows any trailing run of + * separators up to the end of the string (leaving q == NULL, so the isempty(q) check passes) — hence + * `:cgroup:inet:t:s` and `cgroup:inet:t:s:` are both accepted. + */ +private val NFT_SET_TUPLE = SequenceCombinator( + ZeroOrOne(FIELD_SEPARATOR), + NFT_CGROUP_SOURCE, FIELD_SEPARATOR, NFT_FAMILY, FIELD_SEPARATOR, NFT_IDENTIFIER, FIELD_SEPARATOR, NFT_IDENTIFIER, + ZeroOrOne(FIELD_SEPARATOR), +) + +/** The outer split uses EXTRACT_UNQUOTE, so a whole tuple may be quoted. */ +private val NFT_SET_ENTRY = AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), NFT_SET_TUPLE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), NFT_SET_TUPLE, LiteralChoiceTerminal("'")), + NFT_SET_TUPLE, +) + +/** + * Validator for the unit-file `NFTSet=` (cgroup context). + * + * A whitespace-separated list of `SOURCE:FAMILY:TABLE:SET` tuples: + * + * - SOURCE is `cgroup`, `user` or `group`. The shared config_parse_nft_set() also knows `address`, + * `prefix` and `ifindex`, but rejects them for this ltype ("Unknown NFT source"). + * - FAMILY is one of `arp`, `bridge`, `inet`, `ip`, `ip6`, `netdev` (exact match). + * - TABLE and SET must satisfy nft_identifier_valid(). + * + * Exactly four fields: extract_many_words() is asked for four and anything left over fails the tuple. + * Any failure logs a warning and abandons the rest of the line, so one bad tuple is flagged. + * + * The man page's term reads `NFTSet=family:table:set`, but both its body text and the C parser require + * the leading source type; this grammar follows the C. + * + * Leading and trailing `:` around a tuple are accepted (see NFT_SET_TUPLE). + * + * Known over-rejection: the outer whitespace split uses EXTRACT_UNQUOTE, so quotes *inside* a tuple + * (`cgroup:inet:"t":s`) are removed by systemd and accepted; this grammar only accepts a tuple quoted + * as a whole. No real configuration writes that. + * + * Deliberately lenient: backslash escapes inside a name (decoded by EXTRACT_CUNESCAPE before the + * identifier check) are not decoded — `\` is simply accepted as an identifier character, as + * nft_identifier_valid() does after decoding. + * + * Covers: Service.NFTSet, Socket.NFTSet, Mount.NFTSet, Swap.NFTSet, Slice.NFTSet, Scope.NFTSet. (The + * [Network]/[Address]/… NFTSet= of networkd uses ltype NFT_SET_PARSE_NETWORK and is not covered.) + * + * The empty assignment clears the list and never reaches the grammar. + */ +class ConfigParseCgroupNftSetOptionValue : SimpleGrammarOptionValues( + "config_parse_cgroup_nft_set", + SequenceCombinator( + NFT_SET_ENTRY, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), NFT_SET_ENTRY)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt new file mode 100644 index 00000000..69b1094a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePrivateBpfOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * PrivateBPF= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#PrivateBPF= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L150 config_parse_private_bpf (0), via DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/namespace.c#L4185-L4190 private_bpf_table / DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23 parse_boolean (STRCASE_IN_SET) + */ + +/** + * Validator for `PrivateBPF=`. + * + * private_bpf_from_string() tries parse_boolean() first and then the table, whose only entries are + * `no` and `yes` — both already boolean spellings. So the accepted set is exactly parse_boolean's: + * `1 yes y true t on` / `0 no n false f off`, compared case-insensitively (STRCASE_IN_SET), so `Yes` + * and `TRUE` are fine. That is the shared [BOOLEAN]; there are no non-boolean names left to put in front + * of it, so no PreferValidAlternativeCombinator is needed. (Were the table to grow a name such as + * `auto`, it would be matched case-sensitively by streq, unlike the boolean spellings.) + * + * Note: DEFINE_CONFIG_PARSE_ENUM has no empty-string branch, so systemd actually logs `PrivateBPF=` as + * a parse error; the plugin never hands empty values to a validator, so that is not flagged. + * + * Covers: Service.PrivateBPF, Socket.PrivateBPF, Mount.PrivateBPF, Swap.PrivateBPF. + */ +class ConfigParsePrivateBpfOptionValue : SimpleGrammarOptionValues( + "config_parse_private_bpf", + SequenceCombinator( + BOOLEAN, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt new file mode 100644 index 00000000..e2884df5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictFilesystemsOptionValue.kt @@ -0,0 +1,172 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FILESYSTEM_GROUP_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FILESYSTEM_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * RestrictFileSystems= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RestrictFileSystems= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3645-L3712 config_parse_restrict_filesystems (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/bpf-restrict-fs.c#L212 bpf_restrict_fs_parse_filesystem + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/filesystems.c#L21 filesystem_set_find (group names) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/bpf-restrict-fs.c#L117 fs_type_from_string at unit start ("Invalid filesystem name, ignoring"), runtime only + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/filesystem-sets.py#L9 NAME_TO_MAGIC / FILESYSTEM_SETS (the name tables) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-07-PID1.exec-context.sh#L351-L358 ("foo", "@foo" are logged and ignored) + * linux https://man7.org/linux/man-pages/man2/statfs.2.html (the f_type magics the names map to) + * + * sd-check at 0f0682b773 (one value per file): accepted `EXT4`, `ext5`, `invalid`, `temporary`, + * `~proc,sysfs,foo`, `@auxiliary-api btrfs nfs @temporary unknown`, `ext"4"`, `ext4""`, `""`, `"a\"b"`, + * `ext4 ~proc`, `ext4 ~@foo`, `~ ~@bogus`, `"~@bogus"`, `\~proc`, `foo.bar!`; refused `@unknowngroup`, `@`, + * `@Basic-api`, `~@bogus`, `"@foo"`, `""@foo`, `\@foo`, `"@basic-api"x` ("Unknown filesystem group") and + * `ext"4`, `ext4 "unterminated` ("Trailing garbage"). + */ + +// Regex pieces for one word as extract_first_word(…, WHITESPACE ",", EXTRACT_UNQUOTE) reads it: unquoted +// characters, `\x` escapes (the backslash is dropped and any character may follow, also inside quotes) and +// '…' / "…" runs whose quotes are removed. An unterminated quote is "Trailing garbage", so quoted runs must close. + +/** Any one piece of a word. */ +private const val WORD_PIECE = """[^\s,"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*'""" + +/** + * The first piece of a word, which must not unquote to `@` (bpf_restrict_fs_parse_filesystem() would look the + * word up as a group and refuse an unknown one) and must not be a raw `~` (at the start of the value that is + * the deny-list marker; elsewhere [TILDE_WORD] covers it). + */ +private const val FIRST_PIECE_NO_AT_NO_TILDE = + """[^\s,"'\\@~]|\\[^@]|"(?:[^"\\@]|\\[^@])(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\@]|\\[^@])(?:[^'\\]|\\[\s\S])*'""" + +/** [FIRST_PIECE_NO_AT_NO_TILDE] after leading empty quotes, where a `~` is just a name character (`""~x` is `~x`). */ +private const val FIRST_PIECE_NO_AT = + """[^\s,"'\\@]|\\[^@]|"(?:[^"\\@]|\\[^@])(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\@]|\\[^@])(?:[^'\\]|\\[\s\S])*'""" + +/** + * The known file system names as exact literals (completion, enum coloring). Not the lenient + * [FILESYSTEM_NAME] itself: since any name is valid here, its "well-formed but unknown" token would only add + * a second, invalid parse for every unknown word, and a list of unknown words would then explode into + * 2^n parses on the new engine. + */ +private val KNOWN_FILESYSTEM_NAME = LiteralChoiceTerminal(*FILESYSTEM_NAME.choices) + +/** + * A known name, bare or quoted as a whole, filling a whole word. [ANY_FILESYSTEM_NAME] steps aside for these, + * so they are matched by [KNOWN_FILESYSTEM_NAME] and each word has a single parse. + */ +private val KNOWN_NAME_WORD = """(?["']?)(?:""" + KNOWN_FILESYSTEM_NAME.choices.joinToString("|") { Regex.escape(it) } + + """)\k(?=[\s,]|\z)""" + +/** + * Any word the unit-file parser stores as a plain file system name, i.e. one that does not unquote to a + * leading `@`. The parser keeps such names unchecked: an unknown one (`EXT4`, `ext5`, `foo`) is only logged + * and skipped when the unit starts and the BPF map is filled ("bpf-restrict-fs: Invalid filesystem name '%s', + * ignoring.", src/core/bpf-restrict-fs.c#L117-L121), so it is accepted here. Issue #565 tracks a separate + * warning for such names. A word that is wholly empty quotes (`""`) is stored as the empty name and accepted. + */ +private val ANY_FILESYSTEM_NAME = RegexTerminal( + "(?!$KNOWN_NAME_WORD)(?:(?:\"\"|'')+(?:(?:$FIRST_PIECE_NO_AT)(?:$WORD_PIECE)*)?|(?:$FIRST_PIECE_NO_AT_NO_TILDE)(?:$WORD_PIECE)*)", + "(?!$KNOWN_NAME_WORD)(?:(?:\"\"|'')+(?:(?:$FIRST_PIECE_NO_AT)(?:$WORD_PIECE)*)?|(?:$FIRST_PIECE_NO_AT_NO_TILDE)(?:$WORD_PIECE)*)", +) + +/** + * A word starting with a raw `~` anywhere except the very start of the value. Only `rvalue[0]` is the + * deny-list marker, so `ext4 ~proc` or `~ ~@bogus` store the literal name `~proc` / `~@bogus`, which never + * matches a file system at unit start. Accepted for the same reason as [ANY_FILESYSTEM_NAME] (#565). + */ +private val TILDE_WORD = RegexTerminal("~(?:$WORD_PIECE)*", "~(?:$WORD_PIECE)*") + +/** A word that does not start with a raw `~`: an unknown name, a known name or `@group`, either possibly quoted. */ +private val ENTRY_NOT_TILDE = AlternativeCombinator( + // First, so the classic engine does not take a known-name prefix (`ext4` of `ext4foo`) and strand the rest. + ANY_FILESYSTEM_NAME, + SequenceCombinator( + LiteralChoiceTerminal("\""), + AlternativeCombinator(SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), KNOWN_FILESYSTEM_NAME), + LiteralChoiceTerminal("\""), + ), + SequenceCombinator( + LiteralChoiceTerminal("'"), + AlternativeCombinator(SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), KNOWN_FILESYSTEM_NAME), + LiteralChoiceTerminal("'"), + ), + SequenceCombinator(LiteralChoiceTerminal("@"), FILESYSTEM_GROUP_NAME), + KNOWN_FILESYSTEM_NAME, +) + +/** Any word, including one starting with a literal `~`. */ +private val ENTRY = AlternativeCombinator(TILDE_WORD, ENTRY_NOT_TILDE) + +/** + * Validator for `RestrictFileSystems=`. + * + * An optional leading `~` (deny-list instead of allow-list), then words split on whitespace and commas + * (separators coalesce; stray ones at either end are fine, and `~` alone is an empty deny list). Words are + * unquoted with EXTRACT_UNQUOTE, so quotes and backslash escapes may appear anywhere in a word. + * + * - A word that unquotes to `@group` must be one of filesystem-sets.py's FILESYSTEM_SETS (`@basic-api`, + * `@common-block`, `@network`, `@known`, …). The parser refuses an unknown group ("Unknown filesystem + * group, ignoring"), so it is flagged. The lookup is exact and case-sensitive. + * - Any other word is a file system name. The parser stores it unchecked. An unknown name (`EXT4`, `ext5`, + * `foo`, a literal `~proc` after the first word) is only rejected when the unit starts ("Invalid + * filesystem name, ignoring"), so it is accepted here. Issue #565 tracks a separate inspection that + * warns about values like this. Known names stay a separate alternative for completion and coloring. + * + * Known over-rejection: a word that only becomes `@group` through escaping or partial quoting + * (`\@basic-api`, `'@'basic-api`, `""@basic-api`) is accepted by systemd but flagged here; `"@basic-api"` + * quoted as a whole is accepted. + * + * Covers: Service.RestrictFileSystems, Socket.RestrictFileSystems, Mount.RestrictFileSystems, + * Swap.RestrictFileSystems. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseRestrictFilesystemsOptionValue : SimpleGrammarOptionValues( + "config_parse_restrict_filesystems", + // `~` is a separate alternative rather than ZeroOrOne("~"): the classic engine's ZeroOrOne probes its + // inner combinator a second time and reports that as the furthest point reached, which on `~~x` + // produced a highlight range past the end of the value. + AlternativeCombinator( + // Deny list: `rvalue[0] == '~'`; every following word, even one starting with `~`, is a name. + SequenceCombinator( + LiteralChoiceTerminal("~"), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + // Allow list starting with a word: that first word cannot start with a raw `~` (it would be the marker). + SequenceCombinator( + ENTRY_NOT_TILDE, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + EOF(), + ), + // Allow list starting with separators (`,~proc`): every word may start with `~`. + SequenceCombinator( + WHITESPACE_OR_COMMA_SEPARATOR, + ZeroOrOne( + SequenceCombinator( + ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt new file mode 100644 index 00000000..d5afc0cb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketBindtodeviceOptionValue.kt @@ -0,0 +1,111 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MatchResult +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NoMatch +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Parse +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ParseStep +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.TerminalCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.UNIT_SPECIFIER_LETTERS + +/* + * BindToDevice= ([Socket]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#BindToDevice= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L1097-L1134 config_parse_socket_bindtodevice (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L249 unit_full_printf + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0: < IFNAMSIZ, not numeric, not ./../all/default) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L769 ifname_valid_char (33..126 except ':' '/' '%') + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L797-L799 parse_ifindex() >= 0 → refused (no IFNAME_VALID_NUMERIC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497 safe_atoi (via parse_ifindex: value must be > 0) + * linux https://man7.org/linux/man-pages/man7/socket.7.html (SO_BINDTODEVICE) + * linux https://man7.org/linux/man-pages/man7/netdevice.7.html (IFNAMSIZ) + */ + +// Characters ifname_valid_char accepts. +private const val IFNAME_CHAR = """[\x21-\x7E&&[^:/%]]""" + +// A specifier that can appear inside an interface name. `%%` is left out on purpose: it expands to a +// literal `%`, which ifname_valid_char refuses. +private val IFNAME_SPECIFIER = "%[${UNIT_SPECIFIER_LETTERS.replace("%", "")}]" + +/** + * One interface name, possibly built from specifiers. Without a `%` it must satisfy the same rules as + * [INTERFACE_NAME] (ifname_valid with no flags: 1–15 characters from ifname_valid_char, not all digits, + * not `.`, `..`, `all` or `default`). With specifiers the expanded length and content are unknown, so + * only the literal characters are checked and every `%x` must be a known specifier. + */ +private val IFNAME_WITH_SPECIFIERS = RegexTerminal( + """\S+""", + """(?:${INTERFACE_NAME.semanticMatch.pattern})(?!\S)|(?=\S*%)(?:$IFNAME_CHAR|$IFNAME_SPECIFIER)+(?!\S)""" +) + +/** + * True if parse_ifindex() would take [name] as an interface index: safe_atoi() reads all of it (sign, `0x`, + * `0b`, `0o`, leading-`0` octal, exactly as [SafeAtoTerminal] models it) and the result is a positive `int`. ifname_valid() with no flags + * refuses those, so `0x5`, `+5`, `0b101`, `0o7`, `010` and `0b+1` are invalid, while `-5`, `+0`, `0x0`, + * `+0b1` (no prefix stripping after a sign) and out-of-range `0x80000000` are ordinary names. + * Checked against systemd's own parser (sd-check) at 0f0682b773. + */ +private val IFINDEX = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, +) + +private fun bindToDeviceIsIfindex(name: String): Boolean = IFINDEX.valueOf(name) != null + +/** [IFNAME_WITH_SPECIFIERS], additionally refusing the spellings [bindToDeviceIsIfindex] matches. */ +private class BindToDeviceIfnameTerminal(private val inner: RegexTerminal) : TerminalCombinator { + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val result = inner.SemanticMatch(value, offset) + if (result.tokens.size == 1 && bindToDeviceIsIfindex(result.tokens[0])) return NoMatch.copy(longestMatch = offset) + return result + } + + override fun parse(value: String, offset: Int): Sequence = inner.parse(value, offset).map { step -> + if (step is Parse) { + step.copy(tokens = step.tokens.map { it.copy(terminal = this, valid = it.valid && !bindToDeviceIsIfindex(it.text)) }) + } else { + step + } + } + + override fun toString(): String = "BindToDeviceIfname($inner)" +} + +/** + * Validator for `BindToDevice=`. + * + * `*` (like the empty value) clears the setting. Anything else is run through unit_full_printf() and + * must then be a valid interface name per ifname_valid() with no flags — so alternative names longer + * than 15 characters are refused, unlike BindNetworkInterface=. An invalid name is "ignoring", so it is + * flagged. Specifiers are allowed (`BindToDevice=%i` in a template socket is a real pattern). + * + * Numeric spellings that parse_ifindex() takes as an index (`0x5`, `+5`, `0b101`) are refused like + * plain all-digit names, see [bindToDeviceIsIfindex]. + * + * Deliberately lenient: a name containing specifiers is not length-checked. + * + * Covers: Socket.BindToDevice. + */ +class ConfigParseSocketBindtodeviceOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_bindtodevice", + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("*"), EOF()), + SequenceCombinator(BindToDeviceIfnameTerminal(IFNAME_WITH_SPECIFIERS), EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt new file mode 100644 index 00000000..a8fc03a2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallFilterOptionValue.kt @@ -0,0 +1,150 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ERRNO_OR_SECCOMP_ACTION +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SYSCALL_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SYSCALL_OR_GROUP +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * SystemCallFilter= ([Service]/[Socket]/[Mount]/[Swap] and nspawn's [Exec]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#SystemCallFilter= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#SystemCallFilter= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3290-L3382 config_parse_syscall_filter (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/nspawn/nspawn-settings.c#L747-L790 config_parse_syscall_filter (0), nspawn's own function of the same name + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L2508 parse_syscall_and_errno + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L2688 seccomp_parse_errno_or_action + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L342 parse_errno + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1293 seccomp_parse_syscall_filter + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L334 syscall_filter_sets[] (group names) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1035 seccomp_add_syscall_filter_item (nspawn's consumer) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-seccomp.c#L56 TEST(parse_syscall_and_errno) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L729 TEST(parse_errno) + * linux https://man7.org/linux/man-pages/man2/syscalls.2.html + * linux https://man7.org/linux/man-pages/man3/errno.3.html + * + * sd-check at 0f0682b773 (one value per file), a `~` word after the first position: + * .nspawn `@system-service ~@mount`, `@system-service ~@bogus`, `~~@mount`, `@system-service ~@mount:EPERM`: accepted + * (nspawn's parser stores every word unchecked). + * .service `@system-service ~@mount`, `read ~write`, `~~@mount`, `@system-service ~@bogus`: "System call ~@mount is not + * known, ignoring." (the same libseccomp-lookup warning as `madeupcall`); `@system-service ~@mount:EPERM`: + * "Allow-listed system calls cannot take error number"; `~@bogus` (marker + unknown group): "Unknown system + * call group". + */ + +/** `@group` or a bare system call name. */ +private val NAME: Combinator = SYSCALL_OR_GROUP + +/** + * A word starting with `~` after the first position of the value (`@system-service ~@mount`). Only + * `rvalue[0]` is the deny-list marker in both parsers, so this word is taken literally as the name `~@mount`. + * + * - nspawn's parser stores it unchecked; at container start seccomp_add_syscall_filter_item() cannot resolve + * it and drops it with a debug message. That is a runtime-only rejection, so it is accepted here; issue #565 + * tracks a separate warning for it. + * - the unit-file parser hands it to libseccomp, which does not know it ("System call ~@mount is not known, + * ignoring."). That is the same unknown-name warning this grammar already leaves to the shape check for + * `madeupcall`. + * + * The validator registry is keyed only by (parser, ltype), and both files register (config_parse_syscall_filter, + * 0), so the two cannot be told apart; the lenient nspawn behaviour wins. After the `~` the name is not looked + * up as a group by either parser, so any group-shaped word is accepted (`~@bogus`). + */ +private val TILDE_NAME: Combinator = SequenceCombinator( + LiteralChoiceTerminal("~"), + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("@"), RegexTerminal("[^\\s,:]+", "[a-z0-9_-]+")), + SYSCALL_NAME, + ), +) + +/** One allow-list entry after the first position: a name or a literal `~` word, with no `:errno`. */ +private val ALLOW_ENTRY: Combinator = AlternativeCombinator(TILDE_NAME, NAME) + +/** One deny-list entry: a name or a literal `~` word, optionally `:errno`/`:kill`. */ +private val DENY_ENTRY: Combinator = SequenceCombinator( + AlternativeCombinator(TILDE_NAME, NAME), + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal(":"), ERRNO_OR_SECCOMP_ACTION)), +) + +/** + * Validator for `SystemCallFilter=` — both systemd's (exec context of service/socket/mount/swap units) + * and systemd-nspawn's (`[Exec]` in .nspawn files), which share the (config_parse_syscall_filter, 0) + * pair but are different C functions. + * + * Syntax (core): an optional leading `~` (deny-list instead of allow-list), then a list of entries + * split on whitespace and commas. extract_first_word skips leading separators and coalesces runs, so + * `~ @clock @debug`, `read,write` and a trailing separator are all fine; `~` on its own is an + * (empty) deny list. Each entry is + * + * - `@group` — one of the predefined sets in syscall_filter_sets[] (`@system-service`, `@mount`, …). + * The parser refuses an unknown group ("Unknown system call group, ignoring"), so it is flagged. + * - a system call name, resolved by libseccomp for the native architecture. Unknown names are only + * logged and the set is architecture-dependent, so names are only checked for shape (lower-case + * letters, digits, `_`); `DONOTEXIST` is flagged, `madeupcall` is not. + * - a word starting with `~` after the first position (`@system-service ~@mount`), which both parsers take + * as a literal, never-matching name. nspawn only drops it at container start, so it is accepted (see + * [TILDE_NAME]; issue #565 tracks a warning for it). + * - in deny-list mode only, any of those followed by `:` and an errno name (case-insensitive, + * `EPERM`), a number 0…4095 (decimal, `0x` hex or `0` octal) or `kill`. parse_syscall_and_errno + * rejects an empty name (`:EPERM`), an empty or junk errno (`uname:`, `uname:EINVALaaa`) and an + * out-of-range number (`uname:4096`). Without the `~`, config_parse_syscall_filter refuses any + * errno suffix ("Allow-listed system calls cannot take error number, ignoring"). + * + * nspawn: the same `~` and separators, but each word is stored verbatim and only handed to libseccomp / + * the group table at container start. nspawn does not understand `:errno` at all (`uname:EPERM` is just an + * unresolvable name there). Because the two parsers share one registration, this grammar applies the core + * rules above to nspawn files too, except for the non-first `~` word, where nspawn's lenient behaviour wins. + * + * Covers: Service.SystemCallFilter, Socket.SystemCallFilter, Mount.SystemCallFilter, + * Swap.SystemCallFilter (load-fragment-gperf) and Exec.SystemCallFilter (nspawn-gperf). + * + * The empty assignment resets the filter and never reaches the grammar. + */ +class ConfigParseSyscallFilterOptionValue : SimpleGrammarOptionValues( + "config_parse_syscall_filter", + AlternativeCombinator( + // Deny list: `rvalue[0] == '~'`; a further `~` word (`~~@mount`) is a literal name. + SequenceCombinator( + LiteralChoiceTerminal("~"), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + DENY_ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, DENY_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + // Allow list starting with a word: the first word cannot start with `~` (it would be the marker). + SequenceCombinator( + NAME, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ALLOW_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + EOF(), + ), + // Allow list starting with separators (`,~read`): every word may start with `~`. + SequenceCombinator( + WHITESPACE_OR_COMMA_SEPARATOR, + ZeroOrOne( + SequenceCombinator( + ALLOW_ENTRY, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, ALLOW_ENTRY)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt new file mode 100644 index 00000000..f2010a99 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSyscallLogOptionValue.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SYSCALL_OR_GROUP +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WHITESPACE_OR_COMMA_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * SystemCallLog= ([Service]/[Socket]/[Mount]/[Swap]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#SystemCallLog= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L3384-L3454 config_parse_syscall_log (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (WHITESPACE ",", no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L1293 seccomp_parse_syscall_filter (errno_num = -1) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/seccomp-util.c#L334 syscall_filter_sets[] (group names) + * linux https://man7.org/linux/man-pages/man2/syscalls.2.html + */ + +/** A possibly-empty whitespace/comma separated list, with stray separators allowed at either end. */ +private val LIST = SequenceCombinator( + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ZeroOrOne( + SequenceCombinator( + SYSCALL_OR_GROUP, + ZeroOrMore(SequenceCombinator(WHITESPACE_OR_COMMA_SEPARATOR, SYSCALL_OR_GROUP)), + ZeroOrOne(WHITESPACE_OR_COMMA_SEPARATOR), + ) + ), +) + +/** + * Validator for `SystemCallLog=`. + * + * Same list syntax as `SystemCallFilter=` (see [ConfigParseSyscallFilterOptionValue]): an optional + * leading `~` (log everything *except* the listed calls), then system call names and `@group`s split + * on whitespace and commas, with stray separators tolerated. Unlike SystemCallFilter=, each word goes + * straight to seccomp_parse_syscall_filter() with no parse_syscall_and_errno() step, so there is no + * `:errno` suffix — `uname:EPERM` would be looked up as a system call called `uname:EPERM`, fail to + * resolve, and be dropped with a warning; it is flagged here. + * + * Unknown `@group`s are flagged (warned and ignored by systemd). Bare names are only shape-checked + * (lower-case letters, digits, `_`), because libseccomp resolves them per native architecture. + * + * Covers: Service.SystemCallLog, Socket.SystemCallLog, Mount.SystemCallLog, Swap.SystemCallLog. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseSyscallLogOptionValue : SimpleGrammarOptionValues( + "config_parse_syscall_log", + // `~` is a separate alternative rather than ZeroOrOne("~"): the classic engine's ZeroOrOne probes its + // inner combinator a second time and reports that as the furthest point reached, which on `~~x` + // produced a highlight range past the end of the value. + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("~"), LIST, EOF()), + SequenceCombinator(LIST, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt new file mode 100644 index 00000000..fbab9d1d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseXattrOptionValue.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PRINTF_CHAR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.printfCharExcluding + +/* + * XAttrEntryPoint= / XAttrListen= / XAttrAccept= ([Socket]). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#XAttrEntryPoint= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2269-L2327 config_parse_xattr (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-util.c#L1049 split_pair (first "=") + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L249 unit_full_printf (name and value) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/specifier.c#L33 specifier_printf (unknown alphanumeric specifier = -EBADSLT) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/units/TEST-74-AUX-UTILS.socket-xattr.sh#L55-L57 user.varlink=entrypoint / listen / server + * linux https://man7.org/linux/man-pages/man7/xattr.7.html (the user. namespace) + */ + +/** + * Validator for the socket extended-attribute settings, `NAME=VALUE`. + * + * config_parse_xattr() splits at the first `=` (split_pair — no `=` at all is "Failed to parse extended + * attribute expression, ignoring"), expands specifiers in both halves, and then requires the expanded + * name to start with `user.` (the only xattr namespace an unprivileged socket inode can carry, see + * xattr(7)). Everything after the first `=` is the value and may contain further `=` or be empty. + * + * The only other failure is specifier expansion: an unknown alphanumeric `%x` is an error, while `%%`, + * a known specifier, or `%` before a non-alphanumeric character is fine ([PRINTF_CHAR]). + * + * Deliberately strict on one corner: the name must start with a literal `user.`. A name that only + * becomes `user.…` after specifier expansion is theoretically possible but not realistic. + * + * Covers: Socket.XAttrEntryPoint, Socket.XAttrListen, Socket.XAttrAccept. + * + * The empty assignment resets the list and never reaches the grammar. + */ +class ConfigParseXattrOptionValue : SimpleGrammarOptionValues( + "config_parse_xattr", + SequenceCombinator( + LiteralChoiceTerminal("user."), + RegexTerminal("[^=]*", "${printfCharExcluding("=")}*"), + LiteralChoiceTerminal("="), + RegexTerminal("[\\s\\S]*", "$PRINTF_CHAR*"), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt new file mode 100644 index 00000000..64b9fb21 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchSeccompBpfCgroupCombinators.kt @@ -0,0 +1,209 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +/* + * Shared terminals for the seccomp / BPF / cgroup validators (batch 2026-10). + * + * Everything here is pinned to systemd 0f0682b773, the commit in systemd-build/build/last_commit_hash + * that the plugin's gperf and man data are generated from. Browse it at + * https://github.com/systemd/systemd/blob/0f0682b773. + * + * These live in their own file rather than Combinators.kt because several validator families were + * written in parallel; the generally useful ones (ERRNO_OR_SECCOMP_ACTION, PRINTF_CHAR, the + * whitespace-or-comma separator) are candidates for promotion. + */ + +/** + * One run of separators for a list split with `extract_first_word(&p, &word, WHITESPACE ",", …)`. + * + * extract_first_word skips any leading separators and coalesces runs of them (unless + * EXTRACT_DONT_COALESCE_SEPARATORS is passed, which none of these callers do), so `a, b`, `a,,b` and + * `a b` all split into the same two words. + * + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 + */ +val WHITESPACE_OR_COMMA_SEPARATOR = RegexTerminal("[\\s,]+", "[\\s,]+") + +// --------------------------------------------------------------------------------------------------- +// errno names / numbers — parse_errno (src/basic/parse-util.c#L342) and +// seccomp_parse_errno_or_action (src/shared/seccomp-util.c#L2688). + +/** + * The errno names errno_from_name() knows: the `#define E…` macros of ``, as listed by + * src/basic/generate-errno-list.sh (which drops the parisc-only aliases ECANCELLED, EREFUSED, + * EFSBADCRC and EFSCORRUPTED). Regenerate with + * + * echo | gcc -dM -include errno.h -E - | grep -Ev '^#define[[:space:]]+(ECANCELLED|EREFUSED|EFSBADCRC|EFSCORRUPTED)' \ + * | awk '/^#define[ \t]+E[^ _]+[ \t]+/ { print $2; }' | sort + * + * The lookup table is built by gperf with `--ignore-case` (src/basic/meson.build#L168), so `eperm` + * resolves as well as `EPERM`. + */ +val ERRNO_NAMES = arrayOf( + "E2BIG", "EACCES", "EADDRINUSE", "EADDRNOTAVAIL", "EADV", "EAFNOSUPPORT", "EAGAIN", "EALREADY", "EBADE", "EBADF", + "EBADFD", "EBADMSG", "EBADR", "EBADRQC", "EBADSLT", "EBFONT", "EBUSY", "ECANCELED", "ECHILD", "ECHRNG", + "ECOMM", "ECONNABORTED", "ECONNREFUSED", "ECONNRESET", "EDEADLK", "EDEADLOCK", "EDESTADDRREQ", "EDOM", "EDOTDOT", "EDQUOT", + "EEXIST", "EFAULT", "EFBIG", "EHOSTDOWN", "EHOSTUNREACH", "EHWPOISON", "EIDRM", "EILSEQ", "EINPROGRESS", "EINTR", + "EINVAL", "EIO", "EISCONN", "EISDIR", "EISNAM", "EKEYEXPIRED", "EKEYREJECTED", "EKEYREVOKED", "EL2HLT", "EL2NSYNC", + "EL3HLT", "EL3RST", "ELIBACC", "ELIBBAD", "ELIBEXEC", "ELIBMAX", "ELIBSCN", "ELNRNG", "ELOOP", "EMEDIUMTYPE", + "EMFILE", "EMLINK", "EMSGSIZE", "EMULTIHOP", "ENAMETOOLONG", "ENAVAIL", "ENETDOWN", "ENETRESET", "ENETUNREACH", "ENFILE", + "ENOANO", "ENOBUFS", "ENOCSI", "ENODATA", "ENODEV", "ENOENT", "ENOEXEC", "ENOKEY", "ENOLCK", "ENOLINK", + "ENOMEDIUM", "ENOMEM", "ENOMSG", "ENONET", "ENOPKG", "ENOPROTOOPT", "ENOSPC", "ENOSR", "ENOSTR", "ENOSYS", + "ENOTBLK", "ENOTCONN", "ENOTDIR", "ENOTEMPTY", "ENOTNAM", "ENOTRECOVERABLE", "ENOTSOCK", "ENOTSUP", "ENOTTY", "ENOTUNIQ", + "ENXIO", "EOPNOTSUPP", "EOVERFLOW", "EOWNERDEAD", "EPERM", "EPFNOSUPPORT", "EPIPE", "EPROTO", "EPROTONOSUPPORT", "EPROTOTYPE", + "ERANGE", "EREMCHG", "EREMOTE", "EREMOTEIO", "ERESTART", "ERFKILL", "EROFS", "ESHUTDOWN", "ESOCKTNOSUPPORT", "ESPIPE", + "ESRCH", "ESRMNT", "ESTALE", "ESTRPIPE", "ETIME", "ETIMEDOUT", "ETOOMANYREFS", "ETXTBSY", "EUCLEAN", "EUNATCH", + "EUSERS", "EWOULDBLOCK", "EXDEV", "EXFULL", +) + +/** An errno name as errno_from_name() resolves it (case-insensitively). */ +val ERRNO_NAME = FlexibleLiteralChoiceTerminal(*ERRNO_NAMES, ignoreCase = true) + +/** + * The argument of a `syscall:ERRNO` entry: `kill`, an errno name, or a number. + * seccomp_parse_errno_or_action() special-cases `kill` (exact, lower case) and otherwise calls + * parse_errno(), which takes a name first and then falls back to safe_atoi() (sign, `0x`, `0b`, `0o`, + * leading-`0` octal, see [SafeAtoTerminal]), accepting 0 and 1…ERRNO_MAX (4095), so `-0` is fine but `-3` + * is not. + * + * The entry is a word from a whitespace split, so whitespace never reaches safe_atoi() here + * (`acceptsWhitespace = false`; sd-check rejects `SystemCallFilter=~uname:0b 1` because `0b` is a word of its + * own). A malformed number such as `08` is a syntax error located where strtol() stopped. + * + * The number comes before the name because ERRNO_NAME's lenient shape includes digits (E2BIG): under + * the classic engine, with the name first, `0x1F` would be taken as the one-character name `0` and the + * rest stranded. + */ +val ERRNO_OR_SECCOMP_ACTION = AlternativeCombinator( + LiteralChoiceTerminal("kill"), + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4095L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ERRNO_NAME, +) + +// --------------------------------------------------------------------------------------------------- +// System call names and groups — seccomp_parse_syscall_filter (src/shared/seccomp-util.c#L1293). + +/** + * The predefined system call groups, without their leading `@` (syscall_filter_sets[], + * src/shared/seccomp-util.c#L334; looked up by exact string compare in syscall_filter_set_find, + * #L1016). An unknown group is logged ("Unknown system call group, ignoring") and dropped. + */ +val SYSCALL_GROUP_NAME = FlexibleLiteralChoiceTerminal( + "default", "aio", "basic-io", "chown", "clock", "cpu-emulation", "debug", "file-system", "io-event", "ipc", + "keyring", "memlock", "module", "mount", "network-io", "obsolete", "pkey", "privileged", "process", "raw-io", + "reboot", "resources", "sandbox", "setuid", "signal", "swap", "sync", "system-service", "timer", "known", +) + +/** + * A bare system call name. These are resolved at runtime by libseccomp's + * seccomp_syscall_resolve_name() for the *native* architecture, so whether e.g. `arm_fadvise64_64` is + * "known" depends on the machine; an unknown name is logged and ignored. The set is therefore not + * enumerated here: the only check is the shape every libseccomp name has (lower case letters, digits + * and underscores — `_llseek`, `epoll_wait`, `s390_pci_mmio_read`). The lenient syntactic side takes + * anything up to a separator or `:`, so `DONOTEXIST` is located and flagged as a single token. It does not + * start at `@` or `~`, which belong to the `@group` and literal-`~` alternatives: otherwise each such word + * would also get a second (invalid) parse here, and a long invalid list would explode into 2^n parses on the + * new engine. + */ +val SYSCALL_NAME = RegexTerminal("[^\\s,:@~][^\\s,:]*", "[a-z_][a-z0-9_]*") + +/** `@group` or a bare system call name. */ +val SYSCALL_OR_GROUP = AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("@"), SYSCALL_GROUP_NAME), + SYSCALL_NAME, +) + +// --------------------------------------------------------------------------------------------------- +// File system names — bpf_restrict_fs_parse_filesystem (src/core/bpf-restrict-fs.c#L212). + +/** + * The file system names fs_type_from_string() resolves: the keys of NAME_TO_MAGIC in + * src/basic/filesystem-sets.py, compiled into a case-sensitive gperf table. A name outside this set is + * accepted by the unit-file parser and only logged and skipped when the unit starts and the BPF map is + * filled ("bpf-restrict-fs: Invalid filesystem name '%s', ignoring.", src/core/bpf-restrict-fs.c#L117-L121). + * Validators must therefore accept such names too (RestrictFileSystems= uses these choices as exact + * literals next to a lenient word); issue #565 tracks a separate warning for them. The list is kept for + * completion and coloring. + */ +val FILESYSTEM_NAME = FlexibleLiteralChoiceTerminal( + "apparmorfs", "adfs", "affs", "afs", "anon_inodefs", "autofs", "balloon-kvm", "bcachefs", "bdev", "binder", + "binfmt_misc", "bpf", "btrfs", "btrfs_test_fs", "cpuset", "ceph", "cgroup2", "cgroup", "cifs", "coda", + "configfs", "cramfs", "dax", "debugfs", "devmem", "devpts", "devtmpfs", "dmabuf", "ecryptfs", "efivarfs", + "efs", "erofs", "ext2", "ext3", "ext4", "exfat", "f2fs", "failfs", "fuseblk", "fuse", + "fusectl", "gfs", "gfs2", "gmem", "hostfs", "hpfs", "hugetlbfs", "iso9660", "jffs2", "minix", + "mqueue", "msdos", "ncp", "ncpfs", "nfs", "nfs4", "nilfs2", "nsfs", "ntfs", "ntfs3", + "nullfs", "ocfs2", "openpromfs", "orangefs", "overlay", "pidfs", "pipefs", "ppc-cmm", "proc", "pstore", + "pvfs2", "qnx4", "qnx6", "ramfs", "resctrl", "reiserfs", "rpc_pipefs", "secretmem", "securityfs", "selinuxfs", + "shiftfs", "smackfs", "smb3", "smbfs", "sockfs", "squashfs", "sysfs", "tmpfs", "tracefs", "udf", + "usbdevfs", "vboxsf", "vfat", "v9fs", "xenfs", "xfs", "z3fold", "zonefs", "zsmalloc", +) + +/** + * The file system groups, without the leading `@` (FILESYSTEM_SETS in src/basic/filesystem-sets.py, + * looked up by exact compare in filesystem_set_find, src/basic/filesystems.c#L21). The unit-file parser + * refuses an unknown group ("Unknown filesystem group, ignoring"), so validators flag it. + */ +val FILESYSTEM_GROUP_NAME = FlexibleLiteralChoiceTerminal( + "basic-api", "anonymous", "application", "auxiliary-api", "common-block", "historical-block", "network", + "privileged-api", "security", "temporary", "known", +) + +// --------------------------------------------------------------------------------------------------- +// Specifier expansion — unit_full_printf_full (src/core/unit-printf.c#L179). + +/** + * The specifier letters unit_full_printf() knows (src/core/unit-printf.c#L210-L238 plus + * COMMON_SYSTEM/CREDS/TMP_SPECIFIERS from src/shared/specifier.h#L76-L99), and `%` for a literal + * percent sign. Same set as ConfigParseUnitStringPrintfOptionValue uses. + */ +const val UNIT_SPECIFIER_LETTERS = "iIjJnNpPfyYcrRCdDELSthsaAbBHlqmMovwWgGuUTV%" + +/** + * A regex fragment for one character (or `%x` pair) of specifier-expanded text, per specifier_printf + * (src/shared/specifier.c#L33): a known specifier expands; an unknown *alphanumeric* one is an error + * (-EBADSLT, the assignment is dropped); `%` followed by anything else — or at the very end — is kept + * literally. Use inside a character-class-free regex, e.g. `(?:$PRINTF_CHAR)*`. + */ +const val PRINTF_CHAR = "(?:[^%]|%[$UNIT_SPECIFIER_LETTERS]|%(?![A-Za-z0-9]))" + +/** [PRINTF_CHAR], but also excluding [excluded] (a character-class body, e.g. `=` or `\\s`) as a plain character. */ +fun printfCharExcluding(excluded: String): String = "(?:[^%$excluded]|%[$UNIT_SPECIFIER_LETTERS]|%(?![A-Za-z0-9]))" + +// --------------------------------------------------------------------------------------------------- +// BPF token delegation lists — bpf_delegate_from_string (src/core/namespace.c#L4221). + +/** + * The value grammar shared by BPFDelegateCommands= / BPFDelegateMaps= / BPFDelegatePrograms= / + * BPFDelegateAttachments=: either the whole value `any`, or a comma-separated list of names from + * [names]. + * + * bpf_delegate_from_string() checks `streq(s, "any")` against the whole value, then splits on `,` + * *only* — no whitespace — with extract_first_word(..., ",", 0), which skips leading commas and + * coalesces runs of them. So `A, B` makes a word ` B` that is not in the table ("Unknown BPF delegate + * option, ignoring") and is flagged here, while `A,,B` and a trailing comma are fine. Each word is + * looked up with DEFINE_STRING_TABLE_LOOKUP, i.e. an exact, case-sensitive streq. + */ +fun bpfDelegateList(names: FlexibleLiteralChoiceTerminal): Combinator { + val commas = RegexTerminal(",+", ",+") + return AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("any"), EOF()), + SequenceCombinator( + ZeroOrOne(commas), + ZeroOrOne( + SequenceCombinator( + names, + ZeroOrMore(SequenceCombinator(commas, names)), + ZeroOrOne(commas), + ) + ), + EOF(), + ), + ) +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt new file mode 100644 index 00000000..f149d101 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateAttachmentsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateAttachmentsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateAttachments=BPFFlowDissector,BPFCgroupSysctl,BPFNetfilter + BPFDelegateAttachments=BPFCgroupInetIngress + BPFDelegateAttachments=any + BPFDelegateAttachments=BPFCgroupInetIngress,BPFCgroupInetEgress,BPFCgroupInetSockCreate + BPFDelegateAttachments=BPFTraceFsessionMulti + BPFDelegateAttachments=BPFLircMode2 + BPFDelegateAttachments=BPFCgroupInet4Bind,BPFCgroupInet6Bind + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateAttachments=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateAttachments=BPFCgroupInetIngress + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateAttachments=BPFTraceFsessionMulti,BPFCgroupInetIngress + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateAttachments=BPFCgroupInetSkbIngress + BPFDelegateAttachments=BPFFlowDissector, BPFNetfilter + BPFDelegateAttachments=BPF_NETFILTER + BPFDelegateAttachments=ingress + BPFDelegateAttachments=BPFTcxIngress BPFTcxEgress + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt new file mode 100644 index 00000000..be3529fa --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateCommandsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateCommandsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateCommands=BPFObjPin,BPFBtfLoad,BPFMapFreeze,BPFLinkDetach + BPFDelegateCommands=BPFProgLoad + BPFDelegateCommands=any + BPFDelegateCommands=BPFMapCreate,BPFMapLookupElem,BPFMapUpdateElem + BPFDelegateCommands=BPFMapCreate + BPFDelegateCommands=BPFProgAssocStructOps + BPFDelegateCommands=BPFProgLoad,BPFMapCreate,,BPFLinkCreate, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateCommands=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateCommands=BPFMapCreate + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateCommands=BPFProgAssocStructOps,BPFMapCreate + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateCommands=BPF_PROG_LOAD + BPFDelegateCommands=BPFProgload + BPFDelegateCommands=BPFProgLoad, BPFMapCreate + BPFDelegateCommands=BPFProgLoad BPFMapCreate + BPFDelegateCommands=any,BPFProgLoad + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt new file mode 100644 index 00000000..43b02250 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateMapsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateMapsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateMaps=BPFMapTypeArray,BPFMapTypeCpumap,BPFMapTypeRingbuf + BPFDelegateMaps=BPFMapTypeHash + BPFDelegateMaps=any + BPFDelegateMaps=BPFMapTypeUnspec,BPFMapTypeHash,BPFMapTypeArray + BPFDelegateMaps=BPFMapTypeUnspec + BPFDelegateMaps=BPFMapTypeCgroupStorageDeprecated + BPFDelegateMaps=BPFMapTypeRhash + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegateMaps=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegateMaps=BPFMapTypeUnspec + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegateMaps=BPFMapTypeRhash,BPFMapTypeUnspec + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegateMaps=BPFMapTypeCgroupStorage + BPFDelegateMaps=BPFMapTypeArray, BPFMapTypeHash + BPFDelegateMaps=BPF_MAP_TYPE_ARRAY + BPFDelegateMaps=Array + BPFDelegateMaps=Any + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt new file mode 100644 index 00000000..60ef19dd --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfDelegateProgramsOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfDelegateProgramsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first value is from systemd's test/units/TEST-07-PID1.private-bpf.sh (L49-L52). + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegatePrograms=BPFProgTypeTracepoint,BPFProgTypeXdp,BPFProgTypeTracing + BPFDelegatePrograms=BPFProgTypeKprobe + BPFDelegatePrograms=any + BPFDelegatePrograms=BPFProgTypeUnspec,BPFProgTypeSocketFilter,BPFProgTypeKprobe + BPFDelegatePrograms=BPFProgTypeUnspec + BPFDelegatePrograms=BPFProgTypeLwtSeg6local + BPFDelegatePrograms=BPFProgTypeNetfilter + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFDelegatePrograms=any + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFDelegatePrograms=BPFProgTypeUnspec + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFDelegatePrograms=BPFProgTypeNetfilter,BPFProgTypeUnspec + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFDelegatePrograms=BPFProgTypeXDP + BPFDelegatePrograms=BPFProgTypeXdp BPFProgTypeTracing + BPFDelegatePrograms=BPFProgTypeLwtSeg6Local + BPFDelegatePrograms=tracepoint + BPFDelegatePrograms=BPFProgTypeSyscall + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt new file mode 100644 index 00000000..fee4e7cf --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBpfForeignProgramOptionValueTest.kt @@ -0,0 +1,102 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBpfForeignProgramOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first two are the systemd.resource-control(5) examples. + // language="unit file (systemd)" + val file = """ + [Service] + BPFProgram=egress:/sys/fs/bpf/egress-hook + BPFProgram=bind6:/sys/fs/bpf/sock-addr-hook + BPFProgram=ingress:/sys/fs/bpf/ingress + BPFProgram=sock_create:/sys/fs/bpf/a + BPFProgram=sock_ops:/sys/fs/bpf/a + BPFProgram=device:/sys/fs/bpf/a + BPFProgram=bind4:/sys/fs/bpf/a + BPFProgram=connect4:/sys/fs/bpf/a + BPFProgram=connect6:/sys/fs/bpf/a + BPFProgram=post_bind4:/sys/fs/bpf/a + BPFProgram=post_bind6:/sys/fs/bpf/a + BPFProgram=sendmsg4:/sys/fs/bpf/a + BPFProgram=sendmsg6:/sys/fs/bpf/a + BPFProgram=sysctl:/sys/fs/bpf/a + BPFProgram=recvmsg4:/sys/fs/bpf/a + BPFProgram=recvmsg6:/sys/fs/bpf/a + BPFProgram=getsockopt:/sys/fs/bpf/a + BPFProgram=setsockopt:/sys/fs/bpf/a + BPFProgram=egress:/sys/fs/bpf/%N-egress + BPFProgram=egress:%t/bpf/prog + BPFProgram=egress::/sys/fs/bpf/double-colon + BPFProgram=egress:/sys/fs/bpf/with:colon + BPFProgram=:egress:/sys/fs/bpf/leading-colon + BPFProgram=::ingress::/sys/fs/bpf/x + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + BPFProgram=egress:/sys/fs/bpf/egress-hook + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + BPFProgram=ingress:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + BPFProgram=device:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val slice = """ + [Slice] + BPFProgram=sock_create:/sys/fs/bpf/x + """.trimIndent() + setupFileInEditor("file.slice", slice) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BPFProgram=/usr/lib/systemd/bpf/custom.bpf + BPFProgram=egress + BPFProgram=egress: + BPFProgram=xdp:/sys/fs/bpf/x + BPFProgram=Egress:/sys/fs/bpf/x + BPFProgram=egress:sys/fs/bpf/x + BPFProgram=egress:/sys/fs/bpf/../x + BPFProgram=cgroup_inet_egress:/sys/fs/bpf/x + BPFProgram=:/sys/fs/bpf/x + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt new file mode 100644 index 00000000..6eaacd02 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCgroupNftSetOptionValueTest.kt @@ -0,0 +1,90 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCgroupNftSetOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first two are from systemd's test/units/TEST-07-PID1.nft.sh and the man page example. + // language="unit file (systemd)" + val file = """ + [Service] + NFTSet=cgroup:inet:sd_test:c user:inet:sd_test:u group:inet:sd_test:g + NFTSet=cgroup:inet:filter:my_service user:inet:filter:serviceuser + NFTSet=cgroup:ip:t:s + NFTSet=user:ip6:my.table:set/v6 + NFTSet=group:arp:a:b group:bridge:a:b group:netdev:a:b + NFTSet="cgroup:inet:filter:quoted" + NFTSet=cgroup::inet:t:s + NFTSet=:cgroup:inet:t:s + NFTSet=cgroup:inet:t:s: + NFTSet=::cgroup:inet:t:s:: user:inet:t:u: + NFTSet="cgroup:inet:t:s:" + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + NFTSet=cgroup:inet:sd_test:c + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + NFTSet=user:inet:sd_test:u + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + NFTSet=group:inet:sd_test:g + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val slice = """ + [Slice] + NFTSet=cgroup:inet:sd_test:c + """.trimIndent() + setupFileInEditor("file.slice", slice) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + NFTSet=address:inet:sd_test:addr4 + NFTSet=cgroup:ipv4:t:s + NFTSet=cgroup:inet:1table:s + NFTSet=cgroup:inet:t + NFTSet=cgroup:inet:t:s:extra + NFTSet=inet:filter:my_service + NFTSet=cgroup:inet:t:s,user:inet:t:u + NFTSet=cgroup:inet:t-x:s + NFTSet=CGROUP:inet:t:s + NFTSet=cgroup:inet:t: + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt new file mode 100644 index 00000000..29e94048 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrivateBpfOptionValueTest.kt @@ -0,0 +1,83 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePrivateBpfOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + PrivateBPF=yes + PrivateBPF=no + PrivateBPF=true + PrivateBPF=false + PrivateBPF=on + PrivateBPF=off + PrivateBPF=1 + PrivateBPF=0 + PrivateBPF=y + PrivateBPF=n + PrivateBPF=t + PrivateBPF=f + PrivateBPF=Yes + PrivateBPF=TRUE + PrivateBPF=oFf + PrivateBPF=fAlSe + PrivateBPF=Y + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + PrivateBPF=yes + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + PrivateBPF=no + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + PrivateBPF=off + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + PrivateBPF=maybe + PrivateBPF=2 + PrivateBPF=private + PrivateBPF=yes no + PrivateBPF=Auto + PrivateBPF=Private + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt new file mode 100644 index 00000000..d7d7f5af --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictFilesystemsOptionValueTest.kt @@ -0,0 +1,128 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRestrictFilesystemsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first six are the man page examples and TEST-07-PID1.exec-context.sh values. + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=ext4 tmpfs + RestrictFileSystems=ext2 ext4 + RestrictFileSystems=~ext4 tmpfs + RestrictFileSystems=~tmpfs,ext4 + RestrictFileSystems=~@basic-api + RestrictFileSystems=~proc devtmpfs sysfs + RestrictFileSystems=~ + RestrictFileSystems=@common-block @network @known + RestrictFileSystems=btrfs_test_fs balloon-kvm ppc-cmm cgroup2 cgroup nfs4 nfs + RestrictFileSystems="ext4" 'xfs' + RestrictFileSystems=~ fuse, fuseblk, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + RestrictFileSystems=@temporary + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + RestrictFileSystems=~@privileged-api + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + RestrictFileSystems=@security @application + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testRuntimeOnlyRejectionsAreAccepted() { + // sd-check at 0f0682b773 accepts every one of these (one value per probe file). The unit-file parser + // stores any word that does not unquote to `@group` unchecked; unknown names are only logged and + // skipped when the unit starts ("bpf-restrict-fs: Invalid filesystem name '…', ignoring.", + // src/core/bpf-restrict-fs.c#L117-L121). Issue #565 tracks a separate warning for them. The kf6 + // autotest values ("invalid", "temporary", "@auxiliary-api btrfs nfs @temporary unknown") are here. + // A `~` word after the first position is the literal name `~proc` / `~@foo`, and quotes and + // backslash escapes are removed anywhere in a word (EXTRACT_UNQUOTE). + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=~proc,sysfs,foo + RestrictFileSystems=EXT4 + RestrictFileSystems=ext5 + RestrictFileSystems=invalid + RestrictFileSystems=temporary + RestrictFileSystems=@auxiliary-api btrfs nfs @temporary unknown + RestrictFileSystems=ext4foo nfs4x foo.bar! + RestrictFileSystems=ext4 ~proc + RestrictFileSystems=ext4 ~@foo + RestrictFileSystems=ext4 ~ + RestrictFileSystems=~ ~@bogus + RestrictFileSystems=~~proc + RestrictFileSystems="~@bogus" + RestrictFileSystems=\~proc + RestrictFileSystems=ext"4" + RestrictFileSystems=ext4"" + RestrictFileSystems="" + RestrictFileSystems="a\"b" + RestrictFileSystems='a\'b' + RestrictFileSystems="@basic-api" '@network' + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // "@foo" and "@bar" are TEST-07's deliberately unknown groups. The parser refuses unknown groups + // ("bpf-restrict-fs: Unknown filesystem group, ignoring: …", sd-check at 0f0682b773), also when + // the word only unquotes to `@…` ("@foo", ""@foo, \@foo) and when the `~` marker comes first. + // An unterminated quote is "Trailing garbage in RestrictFileSystems". + // language="unit file (systemd)" + val file = """ + [Service] + RestrictFileSystems=ext4 @foo @basic-api + RestrictFileSystems=~proc foo @bar @basic-api + RestrictFileSystems=@basic_api + RestrictFileSystems=@unknowngroup + RestrictFileSystems=@ + RestrictFileSystems=@Basic-api + RestrictFileSystems=~@bogus + RestrictFileSystems="@foo" + RestrictFileSystems=""@foo + RestrictFileSystems=\@foo + RestrictFileSystems="@basic-api"x + RestrictFileSystems=ext"4 + RestrictFileSystems=ext4 "unterminated + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(13, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt new file mode 100644 index 00000000..385c1fb3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketBindtodeviceOptionValueTest.kt @@ -0,0 +1,83 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSocketBindtodeviceOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // eth0/lo/wlan0 are from the Debian corpus and systemd's tests. The numeric-looking names after `**` + // are ones parse_ifindex() does not take as an index, so systemd accepts them (sd-check at 0f0682b773). + // language="unit file (systemd)" + val file = """ + [Socket] + BindToDevice=eth0 + BindToDevice=lo + BindToDevice=wlan0 + BindToDevice=* + BindToDevice=enp0s31f6 + BindToDevice=br-lan.100 + BindToDevice=abcdefghijklmno + BindToDevice=%i + BindToDevice=vrf-%i + BindToDevice=1a + BindToDevice=alliance + BindToDevice=** + BindToDevice=-5 + BindToDevice=+0 + BindToDevice=0x0 + BindToDevice=+0b1 + BindToDevice=0x+5 + BindToDevice=0x80000000 + BindToDevice=+2147483648 + BindToDevice=0b2 + BindToDevice=0xg + BindToDevice=00x5 + BindToDevice=0b-0 + BindToDevice=-0x0 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // 08 is all digits, which ifname_valid() refuses outright. + // 0x5 … 0o+7: parse_ifindex() reads them as an ifindex, which ifname_valid() refuses + // ("Invalid interface name, ignoring", sd-check at 0f0682b773). + // language="unit file (systemd)" + val file = """ + [Socket] + BindToDevice=abcdefghijklmnop + BindToDevice=eth0:1 + BindToDevice=eth/0 + BindToDevice=all + BindToDevice=default + BindToDevice=42 + BindToDevice=.. + BindToDevice=eth0 eth1 + BindToDevice=%z + BindToDevice=100%% + BindToDevice=0x5 + BindToDevice=0X5 + BindToDevice=+5 + BindToDevice=+0x5 + BindToDevice=0b101 + BindToDevice=0o7 + BindToDevice=0b+1 + BindToDevice=010 + BindToDevice=+010 + BindToDevice=0x7fffffff + BindToDevice=0o+7 + BindToDevice=08 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(22, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt new file mode 100644 index 00000000..adb46fc6 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallFilterOptionValueTest.kt @@ -0,0 +1,176 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSyscallFilterOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidAllowLists() { + // Real values from the Debian corpus and systemd's own units/tests. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=@system-service + SystemCallFilter=@system-service @mount + SystemCallFilter=@system-service @module @raw-io bpf + SystemCallFilter=@default @file-system @basic-io @system-service + SystemCallFilter=read write open execve + SystemCallFilter=ptrace,reboot swapoff + SystemCallFilter=read, write + SystemCallFilter=_llseek _newselect epoll_wait s390_pci_mmio_read + SystemCallFilter=@known + SystemCallFilter=,read, + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testUnknownSyscallNamesAreAcceptedDeliberately() { + // systemd warns "System call invalid is not known, ignoring." for these (sd-check at 0f0682b773), + // but whether a name resolves depends on libseccomp and the native architecture, so only the + // shape of a syscall name is checked. Documented leniency. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=invalid + SystemCallFilter=madeupcall + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidDenyListsAndErrno() { + // ~uname:EILSEQ, :kill, :255 come from systemd's test units; :4095/:0 from test-seccomp.c. + // language="unit file (systemd)" + val file = """ + [Socket] + SystemCallFilter=~@mount + SystemCallFilter=~ @clock @debug @module @mount @raw-io @reboot @swap @cpu-emulation @obsolete + SystemCallFilter=~@privileged @resources + SystemCallFilter=~uname:EILSEQ + SystemCallFilter=~uname:ENOENT uname:EILSEQ + SystemCallFilter=~uname:kill + SystemCallFilter=~uname:255 + SystemCallFilter=~@sync:4095 + SystemCallFilter=~uname:0 + SystemCallFilter=~uname:0x1F + SystemCallFilter=~uname:eperm + SystemCallFilter=~uname:ePerm uname:Eilseq + SystemCallFilter=~uname:0777 uname:0b11 uname:0o17 uname:+5 uname:-0 uname:+0x10 uname:0b+1 uname:0o+7 uname:0B-0 uname:-0x0 uname:0o-0 + SystemCallFilter=~@sync:EPERM,@mount:kill uname:E2BIG + SystemCallFilter=~ + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidOtherUnitTypesAndNspawn() { + // language="unit file (systemd)" + val mount = """ + [Mount] + SystemCallFilter=@system-service + """.trimIndent() + setupFileInEditor("file.mount", mount) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + SystemCallFilter=~@swap + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val nspawn = """ + [Exec] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=~@clock add_key + """.trimIndent() + setupFileInEditor("file.nspawn", nspawn) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testTildeWordAfterTheFirstPositionIsAccepted() { + // nspawn only honours `~` as the first character, so "~@mount" below is stored verbatim as a name. + // sd-check at 0f0682b773 accepts all of these in a .nspawn file (nspawn's parser stores words + // unchecked); the word is only dropped at container start by seccomp_add_syscall_filter_item(), a + // runtime-only rejection that issue #565 will warn about. + // language="unit file (systemd)" + val nspawn = """ + [Exec] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=@system-service ~@bogus + SystemCallFilter=~~@mount + SystemCallFilter=,~read + """.trimIndent() + setupFileInEditor("file.nspawn", nspawn) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // In a unit file sd-check says "System call ~@mount is not known, ignoring." for the same words: the + // libseccomp lookup warning that this grammar already leaves unflagged for names like "madeupcall". + // Both files share the (config_parse_syscall_filter, 0) registration, so the lenient nspawn + // behaviour applies here as well. Documented leniency. + // language="unit file (systemd)" + val service = """ + [Service] + SystemCallFilter=@system-service ~@mount + SystemCallFilter=read ~write + SystemCallFilter=~~@mount + SystemCallFilter=~read ~write:EPERM + """.trimIndent() + setupFileInEditor("file.service", service) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallFilter=@bogus-group + SystemCallFilter=~write open execve DONOTEXIST + SystemCallFilter=uname:EILSEQ + SystemCallFilter=~uname: + SystemCallFilter=~:255 + SystemCallFilter=~uname:4096 + SystemCallFilter=~uname:-3 + SystemCallFilter=~uname:12.3 + SystemCallFilter=~uname:EINVALaaa + SystemCallFilter=~uname:255:EILSEQ + SystemCallFilter=~uname:KILL + SystemCallFilter=~uname:Kill + SystemCallFilter=@service + SystemCallFilter=~uname:0b-1 + SystemCallFilter=~uname:+0b1 + SystemCallFilter=~uname:08 + SystemCallFilter=~uname:0x + SystemCallFilter=@system-service ~@mount:EPERM + SystemCallFilter=~@bogus + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(19, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt new file mode 100644 index 00000000..93f4a207 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSyscallLogOptionValueTest.kt @@ -0,0 +1,77 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSyscallLogOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // "~ some_name another_name" (kf6 autotest): systemd warns that both names are unknown, but syscall + // names are resolved by libseccomp per architecture, so only their shape is checked (deliberate). + // "@system-service", "read write open" and "ptrace,reboot swapoff" are from systemd's + // test/units/TEST-*.sh and the man page. + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallLog=@system-service + SystemCallLog=read write open + SystemCallLog=ptrace,reboot swapoff + SystemCallLog=~@sync @mount + SystemCallLog=~ read, write + SystemCallLog=~ + SystemCallLog=_llseek, + SystemCallLog=~ some_name another_name + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testValidAcrossUnitTypes() { + // language="unit file (systemd)" + val socket = """ + [Socket] + SystemCallLog=@network-io + """.trimIndent() + setupFileInEditor("file.socket", socket) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val mount = """ + [Mount] + SystemCallLog=~@mount + """.trimIndent() + setupFileInEditor("file.mount", mount) + assertSize(0, myFixture.doHighlighting()) + + // language="unit file (systemd)" + val swap = """ + [Swap] + SystemCallLog=swapon swapoff + """.trimIndent() + setupFileInEditor("file.swap", swap) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + SystemCallLog=@not-a-group + SystemCallLog=uname:EPERM + SystemCallLog=~uname:EPERM + SystemCallLog=read Write + SystemCallLog=~~read + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt new file mode 100644 index 00000000..68281e56 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseXattrOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseXattrOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // The first three are from systemd's test/units/TEST-74-AUX-UTILS.socket-xattr.sh and the + // systemd.socket(5) Varlink example. + // language="unit file (systemd)" + val file = """ + [Socket] + XAttrEntryPoint=user.varlink=entrypoint + XAttrListen=user.varlink=listen + XAttrAccept=user.varlink=server + XAttrEntryPoint=user.unit=%n + XAttrListen=user.%N.tag=a=b=c + XAttrAccept=user.empty= + XAttrAccept=user.percent=100%% + XAttrAccept=user.literal=50%-off + XAttrAccept=user.trailing=5% + XAttrAccept=user.spaces=hello world + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + XAttrEntryPoint=security.selinux=foo + XAttrListen=trusted.x=y + XAttrAccept=user.varlink + XAttrEntryPoint=User.varlink=entrypoint + XAttrListen=user.x=%z + XAttrAccept=user.%9=y + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} From ded9479918adc248fae57167767cd46185503071 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 12:02:20 -0700 Subject: [PATCH 3/8] feat: validate timer, socket listener and service/job settings Add grammar-based value validation for: BusName=, JobTimeoutSec=, ListenDatagram=, ListenFIFO=, ListenMessageQueue=, ListenNetlink=, ListenSequentialPacket=, ListenSpecial=, ListenStream=, ListenUSBFunction=, MessageQueueMaxMessages=, MessageQueueMessageSize=, NamespacePath=, OnActiveSec=, OnBootSec=, OnCalendar=, OnStartupSec=, OnUnitActiveSec=, OnUnitInactiveSec=, OpenFile=, PIDFile=, RefreshOnReload=, RequiresOverridable=, RequisiteOverridable=, TTYPath=, WorkingDirectory= Accepted values follow systemd's own parsers (derived from the systemd source at 0f0682b773 and checked against systemd's real parsers built at that commit). Values systemd accepts when parsing but rejects only at unit or container start are accepted; see #565. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 26 ++ .../ai/ConfigParseBusNameOptionValue.kt | 48 +++ .../ai/ConfigParseIecSizeLongOptionValue.kt | 28 ++ .../ai/ConfigParseJobTimeoutSecOptionValue.kt | 30 ++ .../ai/ConfigParseLongOptionValue.kt | 47 ++ .../ai/ConfigParseOpenFileOptionValue.kt | 94 ++++ .../ai/ConfigParsePathOptionValue.kt | 33 ++ .../ai/ConfigParsePidFileOptionValue.kt | 40 ++ ...gParseServiceRefreshOnReloadOptionValue.kt | 56 +++ .../ai/ConfigParseSocketListenOptionValues.kt | 252 +++++++++++ .../ai/ConfigParseTimerCalendarOptionValue.kt | 53 +++ .../ConfigParseTimerMonotonicOptionValue.kt | 34 ++ .../grammar/BatchUnitCoreCombinators.kt | 403 ++++++++++++++++++ .../ai/ConfigParseBusNameOptionValueTest.kt | 53 +++ .../ConfigParseIecSizeLongOptionValueTest.kt | 61 +++ ...ConfigParseJobTimeoutSecOptionValueTest.kt | 49 +++ .../ai/ConfigParseLongOptionValueTest.kt | 75 ++++ ...figParseObsoleteUnitDepsOptionValueTest.kt | 46 ++ .../ai/ConfigParseOpenFileOptionValueTest.kt | 73 ++++ .../ai/ConfigParsePathOptionValueTest.kt | 65 +++ .../ai/ConfigParsePidFileOptionValueTest.kt | 55 +++ ...seServiceRefreshOnReloadOptionValueTest.kt | 62 +++ ...figParseSocketListenFileOptionValueTest.kt | 55 +++ ...gParseSocketListenSocketOptionValueTest.kt | 242 +++++++++++ ...ParseSocketListenSpecialOptionValueTest.kt | 45 ++ ...ConfigParseTimerCalendarOptionValueTest.kt | 206 +++++++++ ...onfigParseTimerMonotonicOptionValueTest.kt | 103 +++++ 27 files changed, 2334 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBusNameOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIecSizeLongOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseJobTimeoutSecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLongOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOpenFileOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePathOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePidFileOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseServiceRefreshOnReloadOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketListenOptionValues.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerCalendarOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerMonotonicOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchUnitCoreCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBusNameOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIecSizeLongOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseJobTimeoutSecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLongOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseObsoleteUnitDepsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOpenFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePathOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePidFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseServiceRefreshOnReloadOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSocketOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSpecialOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerCalendarOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerMonotonicOptionValueTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 35dc68d4..8a8806ef 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 @@ -442,6 +442,32 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_xattr", "0") to ConfigParseXattrOptionValue() as OptionValueInformation, Validator("config_parse_socket_bindtodevice", "0") to ConfigParseSocketBindtodeviceOptionValue() as OptionValueInformation, // END Batch 2026-10: seccomp-bpf-cgroup + // Batch 2026-10: unit-core + // config_parse_timer: TIMER_CALENDAR goes to calendar_spec_from_string, every other ltype to parse_sec. + Validator("config_parse_timer", "TIMER_CALENDAR") to ConfigParseTimerCalendarOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_ACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_BOOT") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_STARTUP") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_UNIT_ACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + Validator("config_parse_timer", "TIMER_UNIT_INACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation, + // config_parse_socket_listen: SOCKET_SOCKET covers four keys with different value types, so its + // validator dispatches on the key name, exactly as the C function does. + Validator("config_parse_socket_listen", "SOCKET_SOCKET") to ConfigParseSocketListenSocketOptionValue() as OptionValueInformation, + Validator("config_parse_socket_listen", "SOCKET_FIFO") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation, + Validator("config_parse_socket_listen", "SOCKET_MQUEUE") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation, + Validator("config_parse_socket_listen", "SOCKET_USB_FUNCTION") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation, + Validator("config_parse_socket_listen", "SOCKET_SPECIAL") to ConfigParseSocketListenSpecialOptionValue() as OptionValueInformation, + Validator("config_parse_service_refresh_on_reload", "0") to ConfigParseServiceRefreshOnReloadOptionValue() as OptionValueInformation, + Validator("config_parse_pid_file", "0") to ConfigParsePidFileOptionValue() as OptionValueInformation, + Validator("config_parse_open_file", "0") to ConfigParseOpenFileOptionValue() as OptionValueInformation, + Validator("config_parse_job_timeout_sec", "0") to ConfigParseJobTimeoutSecOptionValue() as OptionValueInformation, + Validator("config_parse_bus_name", "0") to ConfigParseBusNameOptionValue() as OptionValueInformation, + // RequiresOverridable=/RequisiteOverridable= just log and forward to config_parse_unit_deps. + Validator("config_parse_obsolete_unit_deps", "UNIT_REQUIRES") to UnitDependencyOptionValue() as OptionValueInformation, + Validator("config_parse_obsolete_unit_deps", "UNIT_REQUISITE") to UnitDependencyOptionValue() as OptionValueInformation, + Validator("config_parse_path", "0") to ConfigParsePathOptionValue() as OptionValueInformation, + Validator("config_parse_long", "0") to ConfigParseLongOptionValue() as OptionValueInformation, + Validator("config_parse_iec_size_long", "0") to ConfigParseIecSizeLongOptionValue() as OptionValueInformation, ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBusNameOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBusNameOptionValue.kt new file mode 100644 index 00000000..71255292 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBusNameOptionValue.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.withUnitSpecifiers +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DOT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.OneOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Service] BusName= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#BusName= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2421-L2453 config_parse_bus_name (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L179 unit_full_printf_full (specifiers expanded first) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-bus/bus-type.c#L144 sd_bus_service_name_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-bus/bus-internal.c#L119 service_name_is_valid + * linux https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus + */ + +// One dot-separated element of a well-known name: letters, digits, `_` and `-`, not starting with a +// digit. The syntactic shape is wider so a digit-led element is reported as the bad token. +private val WELL_KNOWN_ELEMENT = RegexTerminal("[A-Za-z0-9_-]+", "[A-Za-z_-][A-Za-z0-9_-]*") + +// In a unique name (leading `:`) elements may start with a digit. +private val UNIQUE_ELEMENT = RegexTerminal("[A-Za-z0-9_-]+", "[A-Za-z0-9_-]+") + +/** + * Validator for `[Service] BusName=` (config_parse_bus_name, the only key using it). + * + * After specifier expansion the name must pass sd_bus_service_name_is_valid(): two or more + * dot-separated elements of `[A-Za-z0-9_-]`, none empty and none starting with a digit — or a unique + * name (`:1.42`), whose elements may start with a digit; systemd accepts that form too, odd as it is + * for this setting. The 255-character limit is not checked. Any value that uses a known unit + * specifier (`org.example.%i`) is accepted. + */ +class ConfigParseBusNameOptionValue : SimpleGrammarOptionValues( + "config_parse_bus_name", + withUnitSpecifiers( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal(":"), UNIQUE_ELEMENT, OneOrMore(SequenceCombinator(DOT, UNIQUE_ELEMENT)), EOF()), + SequenceCombinator(WELL_KNOWN_ELEMENT, OneOrMore(SequenceCombinator(DOT, WELL_KNOWN_ELEMENT)), EOF()), + ) + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIecSizeLongOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIecSizeLongOptionValue.kt new file mode 100644 index 00000000..1d2bb94c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIecSizeLongOptionValue.kt @@ -0,0 +1,28 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.IEC_SIZE + +/* + * [Socket] MessageQueueMessageSize= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#MessageQueueMessageSize= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1004-L1032 config_parse_iec_size_long (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L111 parse_size cases (1024 block) + */ + +/** + * Validator for config_parse_iec_size_long, used only by `[Socket] MessageQueueMessageSize=`. + * + * A parse_size(…, 1024) byte size: `8192`, `8K`, `1.5M`, `3. K`, `4 M 11.5K`, `3.5G3B` — see + * [IEC_SIZE]. Suffixes are upper-case B/K/M/G/T/P/E only. systemd additionally requires the result to + * fit in a long and the suffixes of a multi-part size to be strictly descending; neither is checked + * here. + * + * The C parser has no empty-value case; the plugin does not validate empty assignments. + */ +class ConfigParseIecSizeLongOptionValue : SimpleGrammarOptionValues( + "config_parse_iec_size_long", + IEC_SIZE +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseJobTimeoutSecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseJobTimeoutSecOptionValue.kt new file mode 100644 index 00000000..6ebad830 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseJobTimeoutSecOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.TIMESPAN + +/* + * [Unit] JobTimeoutSec= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#JobTimeoutSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5594-L5630 config_parse_job_timeout_sec (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1293 parse_sec_fix_0 (parse_sec, then 0 means infinity) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L85 TEST(parse_sec_fix_0) + */ + +/** + * Validator for `[Unit] JobTimeoutSec=` (config_parse_job_timeout_sec, the only key using it). + * + * The value goes straight to parse_sec_fix_0() — no specifier expansion — so it is a plain + * [TIMESPAN]: `infinity`, `0` (which this parser also turns into infinity), `90`, `5min 20s`, `1h30min`. + * + * There is no special case for an empty value in the C parser: parse_sec("") fails and the assignment + * is ignored with a warning. The plugin does not run value validators on empty assignments, so that + * case is not reported here. + */ +class ConfigParseJobTimeoutSecOptionValue : SimpleGrammarOptionValues( + "config_parse_job_timeout_sec", + TIMESPAN +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLongOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLongOptionValue.kt new file mode 100644 index 00000000..6aa7bad9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLongOptionValue.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Socket] MessageQueueMaxMessages= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#MessageQueueMaxMessages= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L960 config_parse_long (DEFINE_PARSER(long, long, safe_atoli)) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L114 safe_atoli (safe_atolli where long is 64-bit) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L560 safe_atolli (strtoll base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403 mangle_base (0b / 0o prefixes) + * linux https://man7.org/linux/man-pages/man7/mq_overview.7.html + * linux https://man7.org/linux/man-pages/man3/strtol.3.html + */ + +/** + * Validator for config_parse_long, used only by `[Socket] MessageQueueMaxMessages=`. + * + * A signed `long` as safe_atoli() reads it ([SafeAtoTerminal], signed, base 0): optional leading + * whitespace and sign, then decimal, `0x` hex, leading-zero octal, or a `0b`/`0o` prefix — after which + * strtoll starts afresh, so `0b-1` and `0b -1` are -1 (sd-check accepts both) — anything from LONG_MIN + * to LONG_MAX on a 64-bit host. + * The setting itself only means something when positive (socket.c tests `mq_maxmsg > 0`), but the + * parser accepts any long and so does this. + * + * The C parser has no empty-value case (safe_atoli("") fails and the line is ignored with a warning); + * the plugin does not validate empty assignments, so that is not reported. + */ +class ConfigParseLongOptionValue : SimpleGrammarOptionValues( + "config_parse_long", + SequenceCombinator( + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = SafeAtoTerminal.INT64_MIN, + maxInclusive = SafeAtoTerminal.INT64_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOpenFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOpenFileOptionValue.kt new file mode 100644 index 00000000..0d5b7a7d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOpenFileOptionValue.kt @@ -0,0 +1,94 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.OneOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Service] OpenFile= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#OpenFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6743-L6777 config_parse_open_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L12-L68 open_file_parse (a repeated flag is EINVAL, #L48-L49) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L70 open_file_validate (absolute path, valid fd name, at most one of read-only/append/truncate) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L134 open_file_flags_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/fd-util.c#L554 fdname_is_valid (printable ASCII except ':', at most 255) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (":" with EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_CUNESCAPE; "," with 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-open-file.c#L7 TEST(open_file_parse) + */ + +// A C escape as cunescape_one() understands it; any other backslash sequence makes open_file_parse fail. +private const val C_ESCAPE = """\\(?:[abfnrtv\\"'s]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-7]{3})""" + +// The path: everything up to the first ':' (a ':' inside it has to be escaped, e.g. \x3a). Must be +// absolute; path_is_valid() allows ".." here, unlike most unit-file paths. No specifier expansion. +private val OPEN_FILE_PATH = RegexTerminal("""(?:[^:\\]|\\[\s\S]?)+""", """(?:/|$C_ESCAPE)(?:[^:\\]|$C_ESCAPE)*(?=:|$)""") + +// The fd name: printable ASCII other than ':'. Empty means "use the file name". +private val OPEN_FILE_FDNAME = RegexTerminal("""(?:[^:\\]|\\[\s\S]?)+""", """(?:[\x20-\x7E&&[^:\\]]|$C_ESCAPE){1,255}(?=:|$)""") + +// open_file_parse refuses a flag given twice, and open_file_validate refuses more than one of +// read-only/append/truncate, so a valid list holds at most one access flag and at most one `graceful`. +// Exact literals rather than FlexibleLiteralChoiceTerminal: the two choices must not match each other's +// words syntactically, or the original (greedy, non-backtracking) engine commits to the wrong branch. +private val OPEN_FILE_ACCESS_FLAG = LiteralChoiceTerminal("read-only", "append", "truncate") +private val OPEN_FILE_GRACEFUL_FLAG = LiteralChoiceTerminal("graceful") +private val COMMA = LiteralChoiceTerminal(",") + +// extract_first_word(…, ",", 0) coalesces separators, so stray commas anywhere are harmless (even a +// field of nothing but commas, which yields no flags). +private val OPEN_FILE_OPTIONS = SequenceCombinator( + ZeroOrMore(COMMA), + ZeroOrOne( + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(OPEN_FILE_ACCESS_FLAG, ZeroOrOne(SequenceCombinator(OneOrMore(COMMA), OPEN_FILE_GRACEFUL_FLAG))), + SequenceCombinator(OPEN_FILE_GRACEFUL_FLAG, ZeroOrOne(SequenceCombinator(OneOrMore(COMMA), OPEN_FILE_ACCESS_FLAG))), + ), + ZeroOrMore(COMMA), + ) + ), +) + +/** + * Validator for `[Service] OpenFile=` (config_parse_open_file, the only key using it). + * + * `PATH[:FD-NAME[:OPTIONS]]`, split on the first two colons with C escapes decoded (so a literal colon + * in the path is written `\x3a`) and nothing allowed after a third field except one trailing `:`: + * + * - PATH must be absolute; + * - FD-NAME, if given, is printable ASCII without `:` (at most 255 characters); an empty one defaults + * to the file name; + * - OPTIONS is a comma-separated list of at most one of `read-only`, `append`, `truncate` and at + * most one `graceful`, in either order (a repeated flag, or two of read-only/append/truncate, is + * refused by systemd). + * + * Not checked: a path such as `/` whose file name cannot serve as the default fd name. Unlike most + * settings this one performs no specifier expansion, so `%` is an ordinary character. + * + * An empty assignment resets the list and is handled before this grammar is consulted. + */ +class ConfigParseOpenFileOptionValue : SimpleGrammarOptionValues( + "config_parse_open_file", + SequenceCombinator( + OPEN_FILE_PATH, + ZeroOrOne( + SequenceCombinator( + COLON, + ZeroOrOne(OPEN_FILE_FDNAME), + // The third extract (EXTRACT_DONT_COALESCE_SEPARATORS) consumes the colon that ends it, so one + // trailing `:` after the options leaves nothing behind and passes the isempty(v) check. + ZeroOrOne(SequenceCombinator(COLON, OPEN_FILE_OPTIONS, ZeroOrOne(COLON))), + ) + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePathOptionValue.kt new file mode 100644 index 00000000..18aa24d2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePathOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.LITERAL_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * .nspawn [Exec] WorkingDirectory= and [Network] NamespacePath= (also journald.conf TTYPath=, a file + * type the plugin does not load). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#WorkingDirectory= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#NamespacePath= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1378-L1414 config_parse_path (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.c#L1192 path_is_normalized (no ".." component) + */ + +/** + * Validator for config_parse_path with ltype 0, used by `[Exec] WorkingDirectory=` and + * `[Network] NamespacePath=` in .nspawn files. + * + * The value is taken literally (no specifier expansion) and must be an absolute path with no `..` + * component; `.` components and repeated slashes are simplified away rather than refused. There is no + * API VFS restriction, so `NamespacePath=/proc/1/ns/net` is fine. A non-zero ltype would make the error + * fatal but accepts the same values; no gperf entry uses one today. + * + * An empty assignment clears the setting and is handled before this grammar is consulted. + */ +class ConfigParsePathOptionValue : SimpleGrammarOptionValues( + "config_parse_path", + SequenceCombinator(LITERAL_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePidFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePidFileOptionValue.kt new file mode 100644 index 00000000..574dc291 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePidFileOptionValue.kt @@ -0,0 +1,40 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.NOT_API_VFS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.NO_DOT_DOT_COMPONENT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_TEXT_CHAR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Service] PIDFile= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#PIDFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5702-L5750 config_parse_pid_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers expanded first) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE | PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.c#L1192 path_is_normalized (no ".." component) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721 path_below_api_vfs (/dev, /sys, /proc) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L574 patch_var_run (/var/run/ is rewritten with a warning, not refused) + */ + +/** + * Validator for `[Service] PIDFile=` (config_parse_pid_file, the only key using it). + * + * After specifier expansion a relative path is made absolute by prefixing the runtime directory (/run, + * or $XDG_RUNTIME_DIR for user services), so both `foo.pid` and `/run/foo.pid` are fine. The result + * then goes through path_simplify_and_warn(PATH_CHECK_ABSOLUTE | PATH_CHECK_NON_API_VFS): no `..` + * component anywhere, and an absolute path may not be /dev, /sys, /proc or below them. Unknown + * specifiers make systemd ignore the setting, so those are flagged too. + * + * An empty assignment clears the setting and is handled before this grammar is consulted. + */ +class ConfigParsePidFileOptionValue : SimpleGrammarOptionValues( + "config_parse_pid_file", + SequenceCombinator( + RegexTerminal("""[\s\S]+""", """$NO_DOT_DOT_COMPONENT$NOT_API_VFS$UNIT_TEXT_CHAR+$"""), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseServiceRefreshOnReloadOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseServiceRefreshOnReloadOptionValue.kt new file mode 100644 index 00000000..a62ca7a2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseServiceRefreshOnReloadOptionValue.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Service] RefreshOnReload= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#RefreshOnReload= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L4928-L4975 config_parse_service_refresh_on_reload (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (STRCASE_IN_SET: any case) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/service.c#L6630-L6657 service_refresh_on_reload_from_string_many + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/service.c#L6615 service_refresh_on_reload_table ("extensions", "credentials"; streq, so case-sensitive) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (whitespace-separated, flags 0) + */ + +private val REFRESH_RESOURCE = FlexibleLiteralChoiceTerminal("extensions", "credentials") +private val OPTIONAL_WHITESPACE = ZeroOrOne(WhitespaceTerminal()) + +/** + * Validator for `[Service] RefreshOnReload=` (config_parse_service_refresh_on_reload, the only key using + * it). + * + * Either a boolean in any case (parse_boolean is tried first and is case-insensitive, so `Yes` and + * `TRUE` are fine), or a whitespace-separated list of `extensions` and + * `credentials`, optionally prefixed by a single `~` to invert it. A bare `~` is accepted by systemd + * (an empty inverted list, i.e. the default) and so is accepted here. A boolean cannot be inverted + * (`~yes` is an unknown resource name). + * + * Not modelled: extract_first_word() (flags 0) still decodes a backslash escape, so the obfuscated + * spelling `ext\ensions` is accepted by systemd but flagged here. + * + * An empty assignment resets to the default and is handled before this grammar is consulted. + */ +class ConfigParseServiceRefreshOnReloadOptionValue : SimpleGrammarOptionValues( + "config_parse_service_refresh_on_reload", + AlternativeCombinator( + SequenceCombinator(BOOLEAN, EOF()), + SequenceCombinator( + ZeroOrOne(LiteralChoiceTerminal("~")), + OPTIONAL_WHITESPACE, + ZeroOrOne( + SequenceCombinator(REFRESH_RESOURCE, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), REFRESH_RESOURCE)), OPTIONAL_WHITESPACE) + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketListenOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketListenOptionValues.kt new file mode 100644 index 00000000..ddd372c6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseSocketListenOptionValues.kt @@ -0,0 +1,252 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import com.intellij.codeInspection.ProblemsHolder +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_ABSOLUTE_NON_API_VFS_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_SPECIFIER_LETTERS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.UNIT_TEXT_CHAR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.withUnitSpecifiers + +/* + * [Socket] ListenStream= / ListenDatagram= / ListenSequentialPacket= / ListenNetlink= (SOCKET_SOCKET), + * ListenFIFO= (SOCKET_FIFO), ListenMessageQueue= (SOCKET_MQUEUE), ListenUSBFunction= (SOCKET_USB_FUNCTION) + * and ListenSpecial= (SOCKET_SPECIAL). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#ListenStream= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#ListenFIFO= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#ListenNetlink= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L603-L722 config_parse_socket_listen (SOCKET_SOCKET, SOCKET_FIFO, SOCKET_SPECIAL, SOCKET_MQUEUE, SOCKET_USB_FUNCTION) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L253 unit_path_printf (specifiers expanded first, every ltype) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/specifier.c#L33-L105 specifier_printf (unknown %X is an error, %% is a literal %) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (file ltypes: absolute, no "..", not below /dev /sys /proc unless SPECIAL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721 path_below_api_vfs + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L574-L601 patch_var_run (/var/run/ -> /run/ before the length check) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L24-L96 socket_address_parse + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1668 socket_address_parse_unix + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1352 sockaddr_un_set_path (2..107 bytes) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1754 socket_address_parse_vsock + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1708 vsock_parse_port + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L1731 vsock_parse_cid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L718 parse_ip_port (1..65535, safe_atou16_full with SAFE_ATO_REFUSE_LEADING_WHITESPACE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (vsock CID/port, netlink group) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L220 in_addr_port_ifindex_name_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785 ifname_valid_full (IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L119-L152 socket_address_parse_netlink + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L656 netlink_family_table (WITH_FALLBACK: a number up to INT_MAX also works) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L65-L80 string_table_lookup_from_string_fallback (safe_atou, then <= max) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-netlink.c#L40 TEST(socket_address_parse) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-netlink.c#L105 TEST(socket_address_parse_netlink) + * linux https://man7.org/linux/man-pages/man7/unix.7.html + * linux https://man7.org/linux/man-pages/man7/netlink.7.html + * linux https://man7.org/linux/man-pages/man7/vsock.7.html + * linux https://man7.org/linux/man-pages/man7/mq_overview.7.html + */ + +// --------------------------------------------------------------------------------------------------- +// Socket addresses (socket_address_parse), applied to the specifier-expanded text. + +private val PERCENT_START_SPECIFIER = """%[$UNIT_SPECIFIER_LETTERS]""" + +// AF_UNIX: '/' (file system) or '@' (abstract namespace), then anything. sockaddr_un_set_path insists on +// 2..107 bytes including the leading character. The length is only checked when no specifier is present, +// because a specifier's expansion length is unknown; a specifier may also stand at the start (%t/foo). +// config_parse_socket_listen runs patch_var_run() before parsing, which rewrites a /var/run/ prefix +// (path_startswith, so repeated slashes count as one) to /run/; the length limit applies to the result, +// so after /var/run/ up to 102 characters may follow. Length is counted in UTF-16 units, not bytes, so +// non-ASCII paths are checked leniently. +private val UNIX_SOCKET_ADDRESS = RegexTerminal( + """[/@%][\s\S]*""", + """(?:(?=[^%]*$)(?:/+var/+run/+[\s\S]{0,102}|[/@][\s\S]{1,106})|(?=[\s\S]*%)(?:[/@]|$PERCENT_START_SPECIFIER)$UNIT_TEXT_CHAR*)$""" +) + +// parse_ip_port: safe_atou16_full(…, SAFE_ATO_REFUSE_LEADING_WHITESPACE), then 0 is refused. The port is a +// whole value or the text after the last ':' (cut before a '%' scope), so a leading `\v` and `0b 1` reach +// the parser (accepted) while a leading space is refused (sd-check: `ListenStream=1.2.3.4: 80`). +private val IP_PORT = SafeAtoTerminal( + signed = false, + base = 0, + flags = setOf(SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// AF_VSOCK: vsock[-stream|-dgram|-seqpacket]:CID:PORT; the CID may be empty (VMADDR_CID_ANY), one of the +// named CIDs, "-1", or a number; the port is any unsigned except VMADDR_PORT_ANY (UINT32_MAX). Both numbers +// are plain safe_atou() on a field cut at the first ':', so whitespace reaches the parser +// (sd-check accepts `vsock:2: 80` and `vsock: 2:80`). +private val VSOCK_CID = AlternativeCombinator( + FlexibleLiteralChoiceTerminal("hypervisor", "local", "host", "any"), + LiteralChoiceTerminal("-1"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), +) +private val VSOCK_ADDRESS = SequenceCombinator( + LiteralChoiceTerminal("vsock-seqpacket:", "vsock-stream:", "vsock-dgram:", "vsock:"), + ZeroOrOne(VSOCK_CID), + COLON, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = true, + ), +) + +// An interface scope after the port. The `%` that introduces it has to be written `%%` in a unit file: +// unit_path_printf() runs first, and `%eth0` is an unknown specifier (an error), `%lo` would expand %l. +private val INTERFACE_SCOPE = SequenceCombinator( + LiteralChoiceTerminal("%%"), + AlternativeCombinator(ALTERNATIVE_INTERFACE_NAME, RegexTerminal("[1-9][0-9]*", "[1-9][0-9]*")), +) + +private val IPV6_SOCKET_ADDRESS = SequenceCombinator( + LiteralChoiceTerminal("["), IPV6_ADDR, LiteralChoiceTerminal("]"), COLON, IP_PORT, ZeroOrOne(INTERFACE_SCOPE), +) +private val IPV4_SOCKET_ADDRESS = SequenceCombinator(IPV4_ADDR, COLON, IP_PORT, ZeroOrOne(INTERFACE_SCOPE)) + +/** Anything socket_address_parse() accepts — the grammar for ListenStream= and ListenDatagram=. */ +private val SOCKET_ADDRESS = withUnitSpecifiers( + AlternativeCombinator( + SequenceCombinator(UNIX_SOCKET_ADDRESS, EOF()), + SequenceCombinator(VSOCK_ADDRESS, EOF()), + SequenceCombinator(IPV6_SOCKET_ADDRESS, EOF()), + SequenceCombinator(IPV4_SOCKET_ADDRESS, EOF()), + SequenceCombinator(IP_PORT, EOF()), + ) +) + +/** ListenSequentialPacket=: config_parse_socket_listen refuses every family but AF_UNIX for SOCK_SEQPACKET. */ +private val SEQPACKET_ADDRESS = withUnitSpecifiers(SequenceCombinator(UNIX_SOCKET_ADDRESS, EOF())) + +/** + * ListenNetlink=: `FAMILY [GROUP]`, the family a name from netlink_family_table or a number. + * + * socket_address_parse_netlink() takes the family as the first word (extract_first_word, split on + * `" \t\n\r"` only, so `route\v5` is one word and refused) and hands the whole REST to safe_atou(), so + * the group may contain whitespace that strtoul skips (sd-check accepts `route 0b 1` and `route \v5`). + */ +private val NETLINK_ADDRESS = withUnitSpecifiers( + SequenceCombinator( + // The number goes first: the name list contains digits and '-' (ip6-fw), so its syntactic shape also + // matches the `0b` of `0b+1`, and the classic engine commits to the first syntactic match. + AlternativeCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = false, + acceptsMinus = true, + ), + FlexibleLiteralChoiceTerminal( + "route", "firewall", "inet-diag", "nflog", "xfrm", "selinux", "iscsi", "audit", "fib-lookup", + "connector", "netfilter", "ip6-fw", "dnrtmsg", "kobject-uevent", "generic", "scsitransport", + "ecryptfs", "rdma", + ), + ), + ZeroOrOne( + SequenceCombinator( + RegexTerminal("[ \t\n\r]+", "[ \t\n\r]+"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), + EOF(), + ) +) + +/** + * Validator for the four `[Socket]` keys config_parse_socket_listen handles with ltype SOCKET_SOCKET. + * + * The ltype is shared but the value type is not — config_parse_socket_listen branches on the key name + * itself — so this dispatches on [UnitFilePropertyType.key]: + * + * - `ListenStream=` / `ListenDatagram=`: any socket_address_parse() address — an absolute path or + * `@abstract` name (2..107 bytes), `vsock:CID:PORT` (also `vsock-stream:` etc.), a bare port + * (1..65535; parse_ip_port, so `0x50` is port 80), `IPv4:PORT` or `[IPv6]:PORT`, the latter two + * optionally followed by an interface scope. Because unit_path_printf() runs first, that scope's + * `%` must be written `%%` (`[fe80::1]:80%%eth0`). + * - `ListenSequentialPacket=`: AF_UNIX only. + * - `ListenNetlink=`: a netlink family name or number, optionally followed by a multicast group. + * + * Any value that uses a known unit specifier is accepted. [combinator] (used for colouring, completion + * and anything key-agnostic) is the union of the per-key grammars. + */ +class ConfigParseSocketListenSocketOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_listen", + AlternativeCombinator(SOCKET_ADDRESS, NETLINK_ADDRESS) +) { + private val byKey: Map = mapOf( + "ListenStream" to SimpleGrammarOptionValues("config_parse_socket_listen", SOCKET_ADDRESS), + "ListenDatagram" to SimpleGrammarOptionValues("config_parse_socket_listen", SOCKET_ADDRESS), + "ListenSequentialPacket" to SimpleGrammarOptionValues("config_parse_socket_listen", SEQPACKET_ADDRESS), + "ListenNetlink" to SimpleGrammarOptionValues("config_parse_socket_listen", NETLINK_ADDRESS), + ) + + /** The grammar config_parse_socket_listen applies to [key]; the union for an unexpected key. */ + fun combinatorForKey(key: String): Combinator = byKey[key]?.combinator ?: combinator + + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + val delegate = byKey[property.key] + if (delegate != null) delegate.generateProblemDescriptors(property, holder) else super.generateProblemDescriptors(property, holder) + } +} + +/** + * Validator for `[Socket] ListenFIFO=`, `ListenMessageQueue=` and `ListenUSBFunction=` (ltypes + * SOCKET_FIFO, SOCKET_MQUEUE, SOCKET_USB_FUNCTION). + * + * After unit_path_printf() the value goes through path_simplify_and_warn(PATH_CHECK_ABSOLUTE | + * PATH_CHECK_NON_API_VFS): it must be absolute (or start with a specifier such as `%t`), contain no + * `..` component, and not be /dev, /sys, /proc or below. A message queue name is a single-component + * path like `/queue`; systemd does not enforce that at parse time, so neither does this. + */ +class ConfigParseSocketListenFileOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_listen", + SequenceCombinator(UNIT_ABSOLUTE_NON_API_VFS_PATH, EOF()) +) + +/** + * Validator for `[Socket] ListenSpecial=` (ltype SOCKET_SPECIAL): as + * [ConfigParseSocketListenFileOptionValue] but without PATH_CHECK_NON_API_VFS, because special files + * live precisely in /dev, /proc and /sys (`ListenSpecial=/dev/rfkill`). + */ +class ConfigParseSocketListenSpecialOptionValue : SimpleGrammarOptionValues( + "config_parse_socket_listen", + SequenceCombinator(UNIT_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerCalendarOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerCalendarOptionValue.kt new file mode 100644 index 00000000..5175d980 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerCalendarOptionValue.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.CALENDAR_SPEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.withUnitSpecifiers + +/* + * [Timer] OnCalendar= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html#OnCalendar= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html (section "Calendar Events") + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2043-L2114 config_parse_timer (TIMER_CALENDAR) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L179 unit_full_printf_full (specifiers expanded first) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L870-L1103 calendar_spec_from_string_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L375 parse_weekdays + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L719 parse_date + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L802 parse_calendar_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L619 prepend_component + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L500 parse_component_decimal + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L125 fix_year + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L169 chain_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L212 calendar_spec_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L887-L933 timezone suffix: " UTC" (any case), tzname[0..1], or last word if timezone_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/calendarspec.c#L480-L498 parse_one_number (@EPOCH: bare strtoul, so "@ +5" is fine) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1596 verify_timezone + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-calendarspec.c#L117 calendar_spec_one (accepted spellings) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-calendarspec.c#L231 calendar_spec_from_string (rejected spellings) + */ + +/** + * Validator for `[Timer] OnCalendar=`, the only key using config_parse_timer with TIMER_CALENDAR. + * + * After unit_full_printf() the value goes to calendar_spec_from_string(), which accepts either a + * shorthand keyword (`daily`, `weekly`, `semi-annually`, … — any case) or + * + * [WEEKDAYS] [[YEAR-]MONTH-DAY | [YEAR-]MONTH~DAY | @EPOCH] [HOUR:MINUTE[:SECOND]] + * + * optionally followed by a timezone after exactly one space (` UTC`, the local zone abbreviation or an + * IANA zone name; extra spaces are only tolerated after a weekday list or a bare date, which skip + * them). A weekday or keyword in the timezone position (`*-*-* 12:00 Mon`, `Mon Fri`) is refused, as + * systemd refuses it; see [BatchUnitCoreGrammar.CalendarTimezoneTerminal] for what counts as a zone. Each + * date/time field is `*` or a comma list of `value[..value][/repeat]`; seconds may carry fractions. + * See [CALENDAR_SPEC] for exactly which per-field bounds are checked and which cross-field checks + * (reversed ranges, overshooting repetitions, descending weekday ranges, zone existence) are left to + * systemd. A value that uses a unit specifier is accepted as long as the specifier is a known one. + * + * An empty assignment resets the timer list and is handled before this grammar is consulted. + */ +class ConfigParseTimerCalendarOptionValue : SimpleGrammarOptionValues( + "config_parse_timer", + withUnitSpecifiers(CALENDAR_SPEC) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerMonotonicOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerMonotonicOptionValue.kt new file mode 100644 index 00000000..3eff2f67 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimerMonotonicOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.TIMESPAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.withUnitSpecifiers + +/* + * [Timer] OnActiveSec= / OnBootSec= / OnStartupSec= / OnUnitActiveSec= / OnUnitInactiveSec= + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html#OnActiveSec= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html (section "Parsing Time Spans") + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2043-L2114 config_parse_timer (TIMER_ACTIVE, TIMER_BOOT, TIMER_STARTUP, TIMER_UNIT_ACTIVE, TIMER_UNIT_INACTIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L179 unit_full_printf_full (specifiers expanded first) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec, default unit seconds) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21 TEST(parse_sec) + */ + +/** + * Validator for the five monotonic `[Timer]` settings — `OnActiveSec=`, `OnBootSec=`, + * `OnStartupSec=`, `OnUnitActiveSec=` and `OnUnitInactiveSec=`. They share config_parse_timer, whose + * non-calendar branch hands the specifier-expanded value to parse_sec(). + * + * Accepted: `infinity`, or one or more `number[unit]` elements — `50`, `5min`, `1h30min`, `5s 500ms`, + * `.5y`, `+3.1s`, `3.1s.2` — exactly as described for [TIMESPAN]; any value using a known unit + * specifier. Refused: negative numbers, a dangling `.` (`5.`), unknown or mis-pluralised units + * (`5mins`, `2hs`) and two bare numbers glued together (`12.34.56`). + * + * An empty assignment resets the timer list and is handled before this grammar is consulted. + */ +class ConfigParseTimerMonotonicOptionValue : SimpleGrammarOptionValues( + "config_parse_timer", + withUnitSpecifiers(TIMESPAN) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchUnitCoreCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchUnitCoreCombinators.kt new file mode 100644 index 00000000..968a388d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchUnitCoreCombinators.kt @@ -0,0 +1,403 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigInteger + +/* + * Shared grammars for the unit-core validator batch ([Timer] timers, [Socket] Listen*=, assorted + * [Unit]/[Service] settings and the generic long / iec_size_long / path parsers). + * + * Everything here is modelled on systemd 0f0682b773, the commit recorded in + * systemd-build/build/last_commit_hash: https://github.com/systemd/systemd/blob/0f0682b773 + * + * These were written for one batch and live in their own file so parallel work on Combinators.kt does + * not collide; some of them (the time span, the unit-specifier escape hatch) are general enough to be + * promoted there. Integers read by the safe_ato* family use the shared [SafeAtoTerminal] instead. + * + * Every grammar here is careful about the classic engine: no repetition wraps something that can match + * the empty string (classic ZeroOrMore/OneOrMore would loop forever), and every alternative that + * could stop on a prefix of another is either ordered after it or terminated with EOF(). + * + * Everything is namespaced in [BatchUnitCoreGrammar] so that generic names like TIMESPAN cannot collide + * with top-level declarations other batches add to this package in parallel. + */ + +object BatchUnitCoreGrammar { + + // --------------------------------------------------------------------------------------------------- + // Unit specifiers — specifier_printf (src/shared/specifier.c) driven by the table in + // unit_full_printf_full (src/core/unit-printf.c), which also pulls in COMMON_SYSTEM_SPECIFIERS, + // COMMON_CREDS_SPECIFIERS and COMMON_TMP_SPECIFIERS (src/shared/specifier.h). + // + // `%%` is a literal percent sign. `%` followed by a letter or digit that is NOT in the table is a hard + // error (-EBADSLT, "Failed to resolve unit specifiers"), so the setting is ignored. `%` followed by + // anything else (or at the very end) is copied through verbatim. + + /** Every specifier letter unit_full_printf() knows about. */ + const val UNIT_SPECIFIER_LETTERS = "aAbBcCdDEfgGhHiIjJlLmMnNopPqrRsStTuUvVwWyY" + + /** One character of specifier-expanded text: anything but a `%`, or a `%` that expands to something. */ + const val UNIT_TEXT_CHAR = """(?:[^%]|%%|%[$UNIT_SPECIFIER_LETTERS]|%(?![A-Za-z0-9%]))""" + + // A run of text that contains at least one real (value-changing) specifier, and no unknown ones. + private const val NON_SPECIFIER_CHAR = """(?:[^%]|%%|%(?![A-Za-z0-9%]))""" + + /** + * A whole value that contains at least one unit specifier (`%i`, `%t`, …) and no unknown ones. + * + * What a specifier expands to is only known when the unit is loaded, so a value that uses one cannot + * be checked against a grammar for the expanded text. This terminal is the escape hatch: it accepts + * such a value as long as every `%X` in it is one systemd knows (an unknown one makes systemd ignore + * the whole assignment, so that is still flagged). + */ + val UNIT_SPECIFIED_VALUE = RegexTerminal( + """[\s\S]*%[A-Za-z0-9][\s\S]*""", + """(?=$NON_SPECIFIER_CHAR*%[$UNIT_SPECIFIER_LETTERS])$UNIT_TEXT_CHAR*$""" + ) + + /** + * [grammar] (which must itself end in [EOF]), or any value that uses a unit specifier — for settings + * whose C parser runs unit_full_printf()/unit_path_printf() before parsing the expanded text. + */ + fun withUnitSpecifiers(grammar: Combinator): Combinator = + AlternativeCombinator(grammar, SequenceCombinator(UNIT_SPECIFIED_VALUE, EOF())) + + + // --------------------------------------------------------------------------------------------------- + // Time spans — parse_time(…, USEC_PER_SEC) a.k.a. parse_sec (src/basic/time-util.c). + // + // - Leading/trailing whitespace is skipped. `infinity` stands alone (only whitespace may follow). + // - Each element is a number read by strtoll(…, 10) — so an optional `+`, never `-` — with an + // optional `.digits` fraction (a bare `.5` is fine, `5.` is not), then optional whitespace and an + // optional unit from extract_multiplier's table. With no unit the default (seconds) applies. + // - Elements may follow each other directly after a unit (`1h30min`, `3.1s.2`) but a number without + // a unit must be followed by whitespace or the end (`12.34.56`, `5+5` are refused). + // - A unit is matched by prefix in table order, so `5mins` reads `min` and then chokes on `s`. + // + // Overflow (ERANGE past UINT64_MAX µs) is not modelled. + + private val TS_OPTIONAL_WS = ZeroOrOne(WhitespaceTerminal()) + + /** One number of a time span, as strtoll(…, 10) plus parse_time's own `.fraction` handling reads it. */ + val TIMESPAN_NUMBER = RegexTerminal("""\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""", """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""") + + /** extract_multiplier's suffix table (case-sensitive: `m` is minutes, `M` is months). */ + val TIMESPAN_UNIT = FlexibleLiteralChoiceTerminal( + "seconds", "second", "sec", "s", + "minutes", "minute", "min", + "months", "month", "M", + "msec", "ms", "m", + "hours", "hour", "hr", "h", + "days", "day", "d", + "weeks", "week", "w", + "years", "year", "y", + "usec", "us", "μs", "µs", + ) + + private val TIMESPAN_ELEMENT = AlternativeCombinator( + SequenceCombinator(TIMESPAN_NUMBER, TS_OPTIONAL_WS, TIMESPAN_UNIT, TS_OPTIONAL_WS), + SequenceCombinator(TIMESPAN_NUMBER, AlternativeCombinator(WhitespaceTerminal(), EOF())), + ) + + /** A complete parse_sec() time span, `infinity` included, ending at [EOF]. */ + val TIMESPAN = AlternativeCombinator( + SequenceCombinator(TS_OPTIONAL_WS, LiteralChoiceTerminal("infinity"), TS_OPTIONAL_WS, EOF()), + SequenceCombinator(TS_OPTIONAL_WS, OneOrMore(TIMESPAN_ELEMENT), EOF()), + ) + + + // --------------------------------------------------------------------------------------------------- + // Calendar events — calendar_spec_from_string (src/shared/calendarspec.c). + // + // [WEEKDAYS] [DATE] [TIME] [TIMEZONE] or KEYWORD [TIMEZONE] + // + // What is checked: the shape the C parser walks (weekday list with `..`/`-` ranges and an optional + // trailing comma, a two- or three-part date with `-` or `~`, a two- or three-part time, `@epoch`), and + // the per-field ranges calendar_spec_valid() enforces after fix_year() (years 0..99 or 1970..2199, + // months 1..12, days 1..31 or 1..28 counted from the end of the month, hours 0..23, minutes and + // seconds 0..59), with repetitions bounded by `to - from` and never zero. + // + // What is deliberately left lenient: cross-field checks (reversed ranges like `12..1`, a repetition + // that overshoots the field like `20/4` hours, descending weekday ranges like `Fri..Mon`), rounding + // of second fractions, and whether a timezone actually exists on the machine (see + // [CalendarTimezoneTerminal] for what is accepted in its place). + + private val CAL_SPACES = RegexTerminal(" +", " +") + private val CAL_STAR = LiteralChoiceTerminal("*") + private val CAL_COMMA = LiteralChoiceTerminal(",") + private val CAL_RANGE = LiteralChoiceTerminal("..") + private val CAL_REPEAT = LiteralChoiceTerminal("/") + private val CAL_TILDE = LiteralChoiceTerminal("~") + /** + * A plain decimal number in `[minInclusive, maxExclusive)`, as calendarspec's parse_component_decimal + * reads one: it insists on a leading digit (so no sign, unlike [IntegerTerminal], which accepts `-0`) + * and then calls strtoul(…, 10). + */ + class DecimalNumberTerminal(private val minInclusive: Long, private val maxExclusive: Long) : TerminalCombinator { + + private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value + + private fun inRange(text: String): Boolean = + text.toBigInteger().let { it >= BigInteger.valueOf(minInclusive) && it < BigInteger.valueOf(maxExclusive) } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + if (!inRange(text)) return NoMatch.copy(longestMatch = offset) + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun parse(value: String, offset: Int): Sequence { + val text = span(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, inRange(text))))) + } + + override fun toString(): String = "Decimal($minInclusive,$maxExclusive)" + + private companion object { + val SHAPE = Regex("[0-9]+") + } + } + + private val CAL_FRACTION = SequenceCombinator(LiteralChoiceTerminal("."), RegexTerminal("[0-9]+", "[0-9]+")) + + /** One chain element: `value[..value][/repeat]` — prepend_component. */ + private fun calComponent(value: Combinator, repeat: Combinator): Combinator = SequenceCombinator( + value, + ZeroOrOne(SequenceCombinator(CAL_RANGE, value)), + ZeroOrOne(SequenceCombinator(CAL_REPEAT, repeat)), + ) + + /** `*`, or a comma-separated list of components — parse_chain. `*` never combines with a list. */ + private fun calChain(value: Combinator, repeat: Combinator): Combinator { + val component = calComponent(value, repeat) + return AlternativeCombinator(CAL_STAR, SequenceCombinator(component, ZeroOrMore(SequenceCombinator(CAL_COMMA, component)))) + } + + // fix_year() maps 0..69 to 2000..2069 and 70..99 to 1970..1999 before the 1970..2199 range check. + private val CAL_YEAR = AlternativeCombinator(DecimalNumberTerminal(1970, 2200), DecimalNumberTerminal(0, 100)) + private val CAL_YEAR_CHAIN = calChain(CAL_YEAR, DecimalNumberTerminal(1, 230)) + private val CAL_MONTH_CHAIN = calChain(DecimalNumberTerminal(1, 13), DecimalNumberTerminal(1, 12)) + private val CAL_DAY_CHAIN = calChain(DecimalNumberTerminal(1, 32), DecimalNumberTerminal(1, 31)) + // With `~` the day counts back from the end of the month and chain_valid() lowers the ceiling to 28. + private val CAL_END_OF_MONTH_DAY_CHAIN = calChain(DecimalNumberTerminal(1, 29), DecimalNumberTerminal(1, 28)) + private val CAL_HOUR_CHAIN = calChain(DecimalNumberTerminal(0, 24), DecimalNumberTerminal(1, 24)) + private val CAL_MINUTE_CHAIN = calChain(DecimalNumberTerminal(0, 60), DecimalNumberTerminal(1, 60)) + // Seconds are parsed with usec = true: a `.fraction` is allowed on values and repetitions alike. + private val CAL_SECOND_VALUE = SequenceCombinator(DecimalNumberTerminal(0, 60), ZeroOrOne(CAL_FRACTION)) + private val CAL_SECOND_REPEAT = AlternativeCombinator( + SequenceCombinator(DecimalNumberTerminal(0, 60), CAL_FRACTION), + DecimalNumberTerminal(1, 60), + ) + private val CAL_SECOND_CHAIN = calChain(CAL_SECOND_VALUE, CAL_SECOND_REPEAT) + + private val CAL_DAY_PART = AlternativeCombinator( + SequenceCombinator(HYPHEN, CAL_DAY_CHAIN), + SequenceCombinator(CAL_TILDE, CAL_END_OF_MONTH_DAY_CHAIN), + ) + + /** `[YEAR-]MONTH-DAY` or `[YEAR-]MONTH~DAY` — parse_date. A lone component is not a date. */ + private val CAL_DATE = AlternativeCombinator( + SequenceCombinator(CAL_YEAR_CHAIN, HYPHEN, CAL_MONTH_CHAIN, CAL_DAY_PART), + SequenceCombinator(CAL_MONTH_CHAIN, CAL_DAY_PART), + ) + + /** `HOUR:MINUTE[:SECOND]` — parse_calendar_time. */ + private val CAL_TIME = SequenceCombinator( + CAL_HOUR_CHAIN, COLON, CAL_MINUTE_CHAIN, + ZeroOrOne(SequenceCombinator(COLON, CAL_SECOND_CHAIN)), + ) + + // `@SECONDS` since the epoch; the resulting year must still pass the 1970..2199 check, i.e. the value + // must be below 2200-01-01T00:00:00Z. parse_one_number() is a bare strtoul(…, 10), so leading + // whitespace and a `+` are fine (`@ +5`). A `-` is wrapped modulo 2^64 by strtoul and lands before + // 1970, so is refused — except `@-0`, which is 0; that one spelling is not modelled. + private val CAL_EPOCH = SequenceCombinator( + LiteralChoiceTerminal("@"), + ZeroOrOne(WhitespaceTerminal()), + ZeroOrOne(LiteralChoiceTerminal("+")), + DecimalNumberTerminal(0L, 7_258_118_400L), + ) + + /** parse_weekdays' table; startswith_no_case, so any casing. */ + val CALENDAR_WEEKDAY = FlexibleLiteralChoiceTerminal( + "Monday", "Mon", "Tuesday", "Tue", "Wednesday", "Wed", "Thursday", "Thu", + "Friday", "Fri", "Saturday", "Sat", "Sunday", "Sun", + ignoreCase = true, + ) + + private val CAL_WEEKDAY_ITEM = SequenceCombinator( + CALENDAR_WEEKDAY, + ZeroOrOne(SequenceCombinator(LiteralChoiceTerminal("..", "-"), CALENDAR_WEEKDAY)), + ) + + // A trailing comma is allowed ("Wednesday,", "Wed, 17:48"), an open range is not ("Monday.."). + private val CAL_WEEKDAYS = SequenceCombinator( + CAL_WEEKDAY_ITEM, + ZeroOrMore(SequenceCombinator(CAL_COMMA, CAL_WEEKDAY_ITEM)), + ZeroOrOne(CAL_COMMA), + ) + + /** The shorthand keywords, matched with strcaseeq. `anually` is a kept-for-compatibility misspelling. */ + val CALENDAR_KEYWORD = FlexibleLiteralChoiceTerminal( + "minutely", "hourly", "daily", "monthly", "weekly", "yearly", "annually", "anually", "quarterly", + "biannually", "bi-annually", "semiannually", "semi-annually", + ignoreCase = true, + ) + + /** + * The timezone that may end a calendar event, as calendar_spec_from_string_full() recognises one. + * + * In C the suffix is tried three ways, each after exactly one space: ` UTC` in any case + * (endswith_no_case); the host's own zone abbreviations tzname[0]/tzname[1] in any case (`CET`, + * `CEST`, or numeric ones like `-03` for zones without a letter abbreviation); and otherwise the last + * space-separated word, if timezone_is_valid() finds a TZif file of that name under + * /usr/share/zoneinfo. Anything else is left in the expression, which then fails to parse. + * + * Which zones exist depends on the machine, so this accepts: `UTC` in any case; any zone ID the JDK + * knows (also under the `posix/` and `right/` trees); any `Area/Location` shape whose area is one of + * the tzdata regions (newer zones than the JDK's); a 2..6 letter word that is not a weekday or + * calendar keyword (a plausible host abbreviation); and `+NN`/`-NN`/`+NNNN`. What it refuses is the + * common mistake: a weekday or keyword written where systemd expects the date/time to have ended + * (`*-*-* 12:00 Mon`, `Mon Fri`), or a long word that is no zone (`everyday`). + */ + class CalendarTimezoneTerminal : TerminalCombinator { + + private fun span(value: String, offset: Int): String? = SHAPE.matchAt(value, offset)?.value + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + if (!isPlausibleZone(text)) return NoMatch.copy(longestMatch = offset) + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun parse(value: String, offset: Int): Sequence { + val text = span(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, isPlausibleZone(text))))) + } + + override fun toString(): String = "CalendarTimezone" + + companion object { + // verify_timezone's character set; the leading character excludes digits so a time is never read as a zone. + private val SHAPE = Regex("""[A-Za-z+\-][A-Za-z0-9_+\-]*(?:/[A-Za-z0-9_+\-]+)*""") + + private val KNOWN_ZONE_IDS: Set by lazy { + // "Factory" is a tzdata zone file the JDK does not ship (checked against a host's /usr/share/zoneinfo). + java.util.TimeZone.getAvailableIDs().toSet() + java.time.ZoneId.getAvailableZoneIds() + "Factory" + } + + private val TZDATA_AREAS = setOf( + "Africa", "America", "Antarctica", "Arctic", "Asia", "Atlantic", "Australia", "Brazil", "Canada", + "Chile", "Etc", "Europe", "Indian", "Mexico", "Pacific", "US", + ) + + private val ABBREVIATION = Regex("[A-Za-z]{2,6}") + private val NUMERIC_ABBREVIATION = Regex("[+-][0-9]{2}(?:[0-9]{2})?") + + private val NOT_A_ZONE: Set = ( + listOf("monday", "mon", "tuesday", "tue", "wednesday", "wed", "thursday", "thu", "friday", "fri", + "saturday", "sat", "sunday", "sun") + + listOf("minutely", "hourly", "daily", "monthly", "weekly", "yearly", "annually", "anually", + "quarterly", "biannually", "semiannually") + ).toSet() + + fun isPlausibleZone(text: String): Boolean { + if (text.equals("UTC", ignoreCase = true)) return true + val bare = text.removePrefix("posix/").removePrefix("right/") + if (bare in KNOWN_ZONE_IDS) return true + if (bare.contains('/') && bare.substringBefore('/') in TZDATA_AREAS && !bare.endsWith("/")) return true + if (ABBREVIATION.matches(text)) return text.lowercase() !in NOT_A_ZONE + return NUMERIC_ABBREVIATION.matches(text) + } + } + } + + private val CAL_TIMEZONE = CalendarTimezoneTerminal() + + // The zone is split off at exactly one space (endswith " UTC", or the last space for a zone name), so + // any extra spaces stay in the expression. parse_weekdays() and parse_date() skip a run of trailing + // spaces, so after a weekday list or a bare date they are harmless; after a keyword, a time or an + // `@epoch` they are left over and the value is refused. + private val CAL_ONE_SPACE = LiteralChoiceTerminal(" ") + private val CAL_TIMEZONE_AFTER_ONE_SPACE = ZeroOrOne(SequenceCombinator(CAL_ONE_SPACE, CAL_TIMEZONE)) + private val CAL_TIMEZONE_AFTER_SPACES = ZeroOrOne(SequenceCombinator(CAL_SPACES, CAL_TIMEZONE)) + + // `[WEEKDAYS ]` before each body is spelled out as two branches rather than a ZeroOrOne prefix: the + // classic engine's ZeroOrOne re-applies its operand after a match to compute longestMatch, so on + // `Mon Fri` it would read `Fri` as a second weekday prefix, report the whole value as matched and send + // GrammarOptionValue's error localisation past its last token. + private fun withOptionalWeekdays(vararg body: Combinator): List = listOf( + SequenceCombinator(CAL_WEEKDAYS, CAL_SPACES, *body), + SequenceCombinator(*body), + ) + + /** A complete calendar event expression, ending at [EOF]. Every branch is EOF-terminated. */ + val CALENDAR_SPEC = AlternativeCombinator( + *( + listOf(SequenceCombinator(CALENDAR_KEYWORD, CAL_TIMEZONE_AFTER_ONE_SPACE, EOF())) + + withOptionalWeekdays(CAL_EPOCH, CAL_TIMEZONE_AFTER_ONE_SPACE, EOF()) + + withOptionalWeekdays(CAL_DATE, CAL_SPACES, CAL_TIME, CAL_TIMEZONE_AFTER_ONE_SPACE, EOF()) + + withOptionalWeekdays(CAL_DATE, CAL_TIMEZONE_AFTER_SPACES, EOF()) + + withOptionalWeekdays(CAL_TIME, CAL_TIMEZONE_AFTER_ONE_SPACE, EOF()) + + SequenceCombinator(CAL_WEEKDAYS, CAL_TIMEZONE_AFTER_SPACES, EOF()) + ).toTypedArray() + ) + + + // --------------------------------------------------------------------------------------------------- + // Byte sizes — parse_size(…, 1024) (src/basic/parse-util.c). + // + // A run of `number[.fraction] [suffix]` elements, whitespace allowed before each element and between a + // number and its suffix. The number is read by strtoull(…, 10), so a leading `+` is fine and a `-` is + // ERANGE; the fraction may be empty (`10.M` is accepted). Suffixes are B, K, M, G, T, P, E (upper case + // only) or none, and an element without a suffix can only come last. systemd also insists suffixes + // appear in strictly descending order (`1G 512M`, not `512M 1G`) and that the sum fits — neither is + // modelled. + + private val SIZE_NUMBER = RegexTerminal("""\+?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""") + private val SIZE_SUFFIX = LiteralChoiceTerminal("B", "K", "M", "G", "T", "P", "E") + + /** A complete parse_size(…, 1024) value, ending at [EOF]. */ + val IEC_SIZE = SequenceCombinator( + TS_OPTIONAL_WS, + ZeroOrMore(SequenceCombinator(SIZE_NUMBER, TS_OPTIONAL_WS, SIZE_SUFFIX, TS_OPTIONAL_WS)), + ZeroOrOne(SequenceCombinator(SIZE_NUMBER, TS_OPTIONAL_WS)), + EOF(), + ) + + + // --------------------------------------------------------------------------------------------------- + // Paths as path_simplify_and_warn() (src/shared/parse-helpers.c) checks them after specifier expansion: + // PATH_CHECK_ABSOLUTE, then path_simplify() (which drops `.` components and duplicate slashes), then + // path_is_normalized() (which refuses any `..` component), and with PATH_CHECK_NON_API_VFS also + // path_below_api_vfs() (refuses /dev, /sys, /proc and anything below them). + + // Rejects a `..` component anywhere: the optional `(?:[\s\S]*/)` swallows any leading directories. + const val NO_DOT_DOT_COMPONENT = """(?!(?:[\s\S]*/)?\.\.(?:/|$))""" + + // path_startswith() is component-aware: /dev and /dev/x are API VFS, /devices is not. + const val NOT_API_VFS = """(?!/+(?:dev|sys|proc)(?:/|$))""" + + private const val SPECIFIER_OR_SLASH = """(?:/|%[$UNIT_SPECIFIER_LETTERS])""" + + /** An absolute, normalized path after unit_path_printf(); may begin with a specifier such as `%t`. */ + val UNIT_ABSOLUTE_PATH = RegexTerminal("""[\s\S]+""", """$NO_DOT_DOT_COMPONENT$SPECIFIER_OR_SLASH$UNIT_TEXT_CHAR*$""") + + /** As [UNIT_ABSOLUTE_PATH], but also not at or below /dev, /sys or /proc (PATH_CHECK_NON_API_VFS). */ + val UNIT_ABSOLUTE_NON_API_VFS_PATH = RegexTerminal( + """[\s\S]+""", + """$NO_DOT_DOT_COMPONENT$NOT_API_VFS$SPECIFIER_OR_SLASH$UNIT_TEXT_CHAR*$""" + ) + + /** An absolute, normalized path taken literally (no specifier expansion) — config_parse_path. */ + val LITERAL_ABSOLUTE_PATH = RegexTerminal("""[\s\S]+""", """$NO_DOT_DOT_COMPONENT/[\s\S]*""") +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBusNameOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBusNameOptionValueTest.kt new file mode 100644 index 00000000..e1f0fd1c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBusNameOptionValueTest.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBusNameOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Well-known names, a unique name (accepted by sd_bus_service_name_is_valid), and a specifier. + // language="unit file (systemd)" + val file = """ + [Service] + BusName=org.freedesktop.systemd1 + BusName=org.freedesktop.NetworkManager + BusName=com.example.my-service_2 + BusName=a.b + BusName=_foo.-bar + BusName=:1.42 + BusName=org.example.%i + BusName=%N.service + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + BusName=org + BusName=org. + BusName=.org.freedesktop + BusName=org..freedesktop + BusName=org.1freedesktop + BusName=org.free desktop + BusName=org.freedesktop/foo + BusName=org.example.%e + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIecSizeLongOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIecSizeLongOptionValueTest.kt new file mode 100644 index 00000000..2e1e04d1 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIecSizeLongOptionValueTest.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIecSizeLongOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // parse_size(…, 1024) spellings from TEST(parse_size) (src/test/test-parse-util.c). + // language="unit file (systemd)" + val file = """ + [Socket] + MessageQueueMessageSize=8192 + MessageQueueMessageSize=111.4 + MessageQueueMessageSize=112 B + MessageQueueMessageSize=112.6 B + MessageQueueMessageSize=3.5 K + MessageQueueMessageSize=3. K + MessageQueueMessageSize=3.0 K + MessageQueueMessageSize=4 M 11.5K + MessageQueueMessageSize=3.5G3B + MessageQueueMessageSize=3.5G 4B + MessageQueueMessageSize=4T3G3B + MessageQueueMessageSize=4 T 3 G 3 B + MessageQueueMessageSize=12P + MessageQueueMessageSize=3E 2P + MessageQueueMessageSize=+8K + MessageQueueMessageSize=1K5 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + MessageQueueMessageSize=3. 0 K + MessageQueueMessageSize=12X + MessageQueueMessageSize=12.5X + MessageQueueMessageSize=12.5e3 + MessageQueueMessageSize=-1 + MessageQueueMessageSize=-10B 20K + MessageQueueMessageSize=8k + MessageQueueMessageSize=8 KiB + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseJobTimeoutSecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseJobTimeoutSecOptionValueTest.kt new file mode 100644 index 00000000..1f19efa3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseJobTimeoutSecOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseJobTimeoutSecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // parse_sec_fix_0: any parse_sec time span, 0 meaning infinity. No specifier expansion. + // language="unit file (systemd)" + val file = """ + [Unit] + JobTimeoutSec=infinity + JobTimeoutSec=0 + JobTimeoutSec=90 + JobTimeoutSec=5min 20s + JobTimeoutSec=1h30min + JobTimeoutSec=.5s + JobTimeoutSec=+10 + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Unit] + JobTimeoutSec=-1 + JobTimeoutSec=5mins + JobTimeoutSec=forever + JobTimeoutSec=%i + JobTimeoutSec=5. + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLongOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLongOptionValueTest.kt new file mode 100644 index 00000000..aa4c466f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLongOptionValueTest.kt @@ -0,0 +1,75 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseLongOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import org.junit.Test + +class ConfigParseLongOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // safe_atoli: signed long, base 0 plus the 0b/0o prefixes mangle_base adds. strtoll starts afresh + // after the prefix, so a sign or whitespace may follow it (the last five confirmed accepted one per + // file with sd-check at 0f0682b773). + // language="unit file (systemd)" + val file = """ + [Socket] + MessageQueueMaxMessages=10 + MessageQueueMaxMessages=0 + MessageQueueMaxMessages=-5 + MessageQueueMaxMessages=+5 + MessageQueueMaxMessages=0x10 + MessageQueueMaxMessages=-0x10 + MessageQueueMaxMessages=010 + MessageQueueMaxMessages=0b101 + MessageQueueMaxMessages=0o17 + MessageQueueMaxMessages=9223372036854775807 + MessageQueueMaxMessages=-9223372036854775808 + MessageQueueMaxMessages=0b-1 + MessageQueueMaxMessages=0o-7 + MessageQueueMaxMessages=0b -1 + MessageQueueMaxMessages=0b+1 + MessageQueueMaxMessages=0b 1 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testVerticalTabSpellings() { + // strtoll skips a leading \v (C isspace); sd-check at 0f0682b773 accepts both. The plugin's lexer does + // not take a raw \v, so this checks the grammar directly on both engines. + val grammar = ConfigParseLongOptionValue().combinator + for (engine in listOf(GrammarEngineVerdict::classic, GrammarEngineVerdict::parseEngine)) { + assertEquals(GrammarEngineVerdict.VALID, engine(grammar, "\u000B5")) + assertEquals(GrammarEngineVerdict.VALID, engine(grammar, "\u000B-1")) + } + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + MessageQueueMaxMessages=ten + MessageQueueMaxMessages=08 + MessageQueueMaxMessages=0x + MessageQueueMaxMessages=9223372036854775808 + MessageQueueMaxMessages=-0b1 + MessageQueueMaxMessages=10K + MessageQueueMaxMessages=1.5 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseObsoleteUnitDepsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseObsoleteUnitDepsOptionValueTest.kt new file mode 100644 index 00000000..f0346af7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseObsoleteUnitDepsOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** + * RequiresOverridable=/RequisiteOverridable= (config_parse_obsolete_unit_deps) forward to + * config_parse_unit_deps, so they share the unit-dependency validator with Requires=/Requisite=. + */ +class ConfigParseObsoleteUnitDepsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Unit] + RequiresOverridable=foo.service + RequiresOverridable=foo.service bar.socket + RequisiteOverridable=network-online.target + RequisiteOverridable=getty@tty1.service + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Unit] + RequiresOverridable=foo + RequisiteOverridable=foo.notatype + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOpenFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOpenFileOptionValueTest.kt new file mode 100644 index 00000000..14244208 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOpenFileOptionValueTest.kt @@ -0,0 +1,73 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseOpenFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // From TEST(open_file_parse) (src/test/test-open-file.c) plus escape and comma edge cases. + // language="unit file (systemd)" + val file = """ + [Service] + OpenFile=/proc/1/ns/mnt:host-mount-namespace:read-only + OpenFile=/proc/1/ns/mnt + OpenFile=/proc/1/ns/mnt:host-mount-namespace + OpenFile=/proc/1/ns/mnt::read-only + OpenFile=/proc/1/ns/mnt:host-mount-namespace:append + OpenFile=/proc/1/ns/mnt:host-mount-namespace:truncate + OpenFile=/proc/1/ns/mnt:host-mount-namespace:graceful + OpenFile=/proc/1/ns/mnt:host-mount-namespace:read-only,graceful + OpenFile=/run/a\x3ab:name + OpenFile=/run/with space/file:my name + OpenFile=/run/x: + OpenFile=/run/x:: + OpenFile=/etc/foo:fd:read-only: + OpenFile=/run/x:fd:: + OpenFile=/run/x::,read-only,,graceful, + OpenFile=/run/x:fd:graceful,truncate + OpenFile=/run/x:fd:truncate,graceful + OpenFile=/run/x:fd:graceful,read-only + OpenFile=/run/x:fd:,,, + OpenFile=/run/%i + OpenFile=/run/../x + OpenFile= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // The last five are refused by open_file_parse/open_file_validate (repeated flag, or more than one of + // read-only/append/truncate); confirmed with systemd's own parser at 0f0682b773 (sd-check). + // language="unit file (systemd)" + val file = """ + [Service] + OpenFile=../file.dat:file:read-only + OpenFile=/proc/1/ns/mnt:host-mount-namespace:rw + OpenFile=/proc/1/ns/mnt:host-mount-namespace:read-only:other + OpenFile=relative + OpenFile=/run/x:name:read-only append + OpenFile=/run/x\q:name + OpenFile=/run/x:name:read-only:: + OpenFile=/run/x:fd:read-only,read-only + OpenFile=/run/x:fd:read-only,append + OpenFile=/run/x:fd:graceful,graceful + OpenFile=/run/x:fd:append,graceful,truncate + OpenFile=/some/path/:some-name:append,graceful,read-only,truncate + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePathOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePathOptionValueTest.kt new file mode 100644 index 00000000..ecfe59e8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePathOptionValueTest.kt @@ -0,0 +1,65 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePathOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // config_parse_path: literal absolute path, no '..' component, no API VFS restriction. + // language="unit file (systemd)" + val file = """ + [Exec] + WorkingDirectory=/ + WorkingDirectory=/root + WorkingDirectory=/home/user/work dir + WorkingDirectory=/srv//data/./x + WorkingDirectory=/..foo/bar.. + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Exec] + WorkingDirectory=root + WorkingDirectory=/srv/../etc + WorkingDirectory=.. + WorkingDirectory=%h/work + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } + + @Test + fun testNamespacePath() { + // The [Network] key shares config_parse_path; /proc is fine here (no PATH_CHECK_NON_API_VFS). + // language="unit file (systemd)" + val file = """ + [Network] + NamespacePath=/run/netns/foo + NamespacePath=/proc/1/ns/net + NamespacePath=netns/foo + NamespacePath=/run/netns/../foo + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePidFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePidFileOptionValueTest.kt new file mode 100644 index 00000000..148f9628 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePidFileOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePidFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Absolute and relative paths (relative ones are placed under /run), specifiers, and /var/run (rewritten, not refused). + // language="unit file (systemd)" + val file = """ + [Service] + PIDFile=/run/foo.pid + PIDFile=foo.pid + PIDFile=nginx/nginx.pid + PIDFile=./foo.pid + PIDFile=/var/run/foo.pid + PIDFile=%t/foo.pid + PIDFile=/run/%i.pid + PIDFile=/devices/foo.pid + PIDFile= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + // PidFileOptionWarning adds an informational note on every PIDFile= line; only value problems count here. + val highlights = myFixture.doHighlighting().filter { it.description?.startsWith("PID files should be avoided") != true } + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + PIDFile=../foo.pid + PIDFile=/run/../foo.pid + PIDFile=/run/foo/.. + PIDFile=/dev/foo.pid + PIDFile=/proc/self/foo.pid + PIDFile=/sys + PIDFile=/run/%e.pid + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + // PidFileOptionWarning adds an informational note on every PIDFile= line; only value problems count here. + val highlights = myFixture.doHighlighting().filter { it.description?.startsWith("PID files should be avoided") != true } + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseServiceRefreshOnReloadOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseServiceRefreshOnReloadOptionValueTest.kt new file mode 100644 index 00000000..465840ed --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseServiceRefreshOnReloadOptionValueTest.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseServiceRefreshOnReloadOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Booleans (parse_boolean is case-insensitive), resource lists, the ~ inversion (a bare ~ is an empty inverted list), and an empty reset. + // language="unit file (systemd)" + val file = """ + [Service] + RefreshOnReload=yes + RefreshOnReload=false + RefreshOnReload=1 + RefreshOnReload=Yes + RefreshOnReload=TRUE + RefreshOnReload=oN + RefreshOnReload=OFF + RefreshOnReload=oFf + RefreshOnReload=fAlSe + RefreshOnReload=T + RefreshOnReload=extensions + RefreshOnReload=credentials + RefreshOnReload=extensions credentials + RefreshOnReload=~credentials + RefreshOnReload=~ extensions + RefreshOnReload=~ + RefreshOnReload= + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Service] + RefreshOnReload=~yes + RefreshOnReload=Extensions + RefreshOnReload=extensions,credentials + RefreshOnReload=~~extensions + RefreshOnReload=extensions ~credentials + RefreshOnReload=all + RefreshOnReload=CREDENTIALS + RefreshOnReload=extensions Credentials + """.trimIndent() + + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenFileOptionValueTest.kt new file mode 100644 index 00000000..a46d4fca --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenFileOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSocketListenFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + ListenFIFO=/run/acpi_fakekey + ListenFIFO=/run/cloud-init/share/hook-hotplug-cmd + ListenFIFO=%t/wob.sock + ListenFIFO=/var/run/legacy.fifo + ListenFIFO=/run//double/./slash + ListenFIFO=/devices/not-api-vfs + ListenMessageQueue=/queue + ListenMessageQueue=/%i + ListenUSBFunction=/run/usb-ffs/adb + ListenUSBFunction=/some/path + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Relative paths, ".." components, API VFS paths (PATH_CHECK_NON_API_VFS) and an unknown specifier. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenFIFO=relative/path + ListenFIFO=/run/../etc/fifo + ListenFIFO=/dev/foo + ListenFIFO=/proc + ListenMessageQueue=queue + ListenMessageQueue=/sys/fs/mqueue/q + ListenUSBFunction=/dev/usb-ffs/adb + ListenUSBFunction=%e/ffs + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSocketOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSocketOptionValueTest.kt new file mode 100644 index 00000000..9223db2c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSocketOptionValueTest.kt @@ -0,0 +1,242 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseSocketListenSocketOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import org.junit.Test + +class ConfigParseSocketListenSocketOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidStreamAndDatagramAddresses() { + // From TEST(socket_address_parse) (src/test/test-socket-netlink.c) and the Debian corpus. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenStream=65535 + ListenStream=8888 + ListenStream=0x50 + ListenStream=[2001:0db8:0000:85a3:0000:0000:ac1f:8001]:8888 + ListenStream=[::1]:8888 + ListenStream=[::]:111 + ListenStream=[::1]:1234%%lo + ListenStream=[fe80::1]:80%%eth0 + ListenStream=[fe80::1]:80%%2 + ListenStream=192.168.1.254:8888 + ListenStream=0.0.0.0:80 + ListenStream=127.0.0.1:1234%%lo + ListenStream=/foo/bar + ListenStream=/run/foo bar.sock + ListenStream=@abstract + ListenStream=@/org/freedesktop/systemd1/logger + ListenStream=%t/pulse/native + ListenStream=%t/foot-%i.sock + ListenStream=%C/sigstore/gitsign/cache.sock + ListenStream=%i + ListenStream=vsock:2:1234 + ListenStream=vsock::1234 + ListenStream=vsock:host:22 + ListenStream=vsock:-1:22 + ListenStream=vsock-stream:any:22 + ListenStream=vsock:-0:5 + ListenDatagram=69 + ListenDatagram=[::]:53 + ListenDatagram=127.0.0.1:1153 + ListenDatagram=@fcm_clif + ListenDatagram=/path/to/socket + ListenDatagram=vsock-dgram:3:4000 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidStreamAndDatagramAddresses() { + // `0` and `65536` are ports out of range for ListenStream= (they would be netlink family numbers, + // which only ListenNetlink= takes); all 23 confirmed rejected by sd-check at 0f0682b773. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenStream=junk + ListenStream=192.168.1.1 + ListenStream=.168.1.1 + ListenStream=989.168.1.1:80 + ListenStream=192.168.1.1:65536 + ListenStream=192.168.1.1:0 + ListenStream=0 + ListenStream=65536 + ListenStream=[::1] + ListenStream=[::1]8888 + ListenStream=::1 + ListenStream=[::1]:0 + ListenStream=[a:b:1]:8888 + ListenStream=[::1]%%lo:1234 + ListenStream=[::1]:1234%%lo%%lo + ListenStream=/ + ListenStream=@ + ListenStream=vsock:2:1234x + ListenStream=vsock:2x:1234 + ListenStream=vsock:2 + ListenStream=[fe80::1]:80%eth0 + ListenDatagram=80/udp + ListenDatagram=localhost:53 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(23, highlights) + } + + @Test + fun testUnixSocketPathLength() { + // sockaddr_un_set_path: 107 bytes is the longest path that fits with its NUL terminator. The limit + // applies after patch_var_run() has rewritten /var/run/ to /run/, so /var/run/ paths get 4 more. + val ok = "/" + "a".repeat(106) + val tooLong = "/" + "a".repeat(107) + val file = """ + [Socket] + ListenStream=$ok + ListenStream=@${"a".repeat(106)} + ListenStream=$tooLong + ListenStream=/var/run/${"a".repeat(102)} + ListenStream=/var/run/${"a".repeat(103)} + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } + + @Test + fun testSequentialPacketIsUnixOnly() { + // The last three are refused by config_parse_socket_listen ("Address family not supported"), checked + // with sd-check at 0f0682b773. The inspection dispatches on the key; the key-agnostic union grammar + // (`combinator`) accepts them, which is why a harness that only reads `combinator` sees them as valid. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenSequentialPacket=/run/foo.sock + ListenSequentialPacket=@namespace + ListenSequentialPacket=%t/foo.sock + ListenSequentialPacket=1.2.3.4:1234 + ListenSequentialPacket=1234 + ListenSequentialPacket=vsock-seqpacket:2:1234 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } + + @Test + fun testNetlink() { + // From TEST(socket_address_parse_netlink) and the corpus. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenNetlink=route + ListenNetlink=route 10 + ListenNetlink=kobject-uevent 1 + ListenNetlink=kobject-uevent 10 + ListenNetlink=audit 1 + ListenNetlink=route 1361 + ListenNetlink=rdma 4 + ListenNetlink=16 + ListenNetlink=generic 0x10 + ListenNetlink=route -0 + ListenNetlink=junk + ListenNetlink=route ten + ListenNetlink=route 1 2 + ListenNetlink=/run/foo.sock + ListenNetlink=route -1 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } + + @Test + fun testSafeAtoSpellings() { + // The numbers go through the shared SafeAtoTerminal. Every value here was checked one per file with + // sd-check at 0f0682b773: the first 15 are accepted, the last 5 rejected. Ports are parse_ip_port + // (SAFE_ATO_REFUSE_LEADING_WHITESPACE, then 0 refused); vsock CID/port and the netlink group are plain + // safe_atou() on the rest of the text; the netlink family is the first " \t\n\r"-separated word. + // The plugin's lexer does not take a raw \v, so those spellings are in testVerticalTabSpellings. + // language="unit file (systemd)" + val file = """ + [Socket] + ListenStream=0b+1 + ListenStream=0b 1 + ListenStream=0b +1 + ListenStream=0o 7 + ListenStream=1.2.3.4:0b 1 + ListenStream=[::1]:0b+1 + ListenStream=1.2.3.4:0b+1%%lo + ListenStream=vsock:2: 80 + ListenStream=vsock: 2:80 + ListenStream=vsock:2:0b 1 + ListenStream=vsock:0b+1:80 + ListenNetlink=route 0b 1 + ListenNetlink=route 0b+1 + ListenNetlink=0b+1 5 + ListenNetlink=route 5 + ListenStream=0b-0 + ListenStream=1.2.3.4: 80 + ListenStream=+0b1 + ListenStream=vsock:2:0b -1 + ListenNetlink=route 0b -1 + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } + + @Test + fun testVerticalTabSpellings() { + // strtoul skips a leading \v, but neither SAFE_ATO_REFUSE_LEADING_WHITESPACE nor extract_first_word + // treat it as whitespace. sd-check at 0f0682b773, one value per file: the first three are accepted, + // `route\v5` is rejected (one word, not a netlink family). + val validator = ConfigParseSocketListenSocketOptionValue() + val stream = validator.combinatorForKey("ListenStream") + val netlink = validator.combinatorForKey("ListenNetlink") + for (engine in listOf(GrammarEngineVerdict::classic, GrammarEngineVerdict::parseEngine)) { + assertEquals(GrammarEngineVerdict.VALID, engine(stream, "\u000B80")) + assertEquals(GrammarEngineVerdict.VALID, engine(stream, "1.2.3.4:\u000B80")) + assertEquals(GrammarEngineVerdict.VALID, engine(netlink, "route \u000B5")) + assertEquals(GrammarEngineVerdict.Kind.SYNTAX, engine(netlink, "route\u000B5").kind) + } + } + + @Test + fun testEmptyValueResets() { + // language="unit file (systemd)" + val file = """ + [Socket] + ListenStream= + ListenNetlink= + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSpecialOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSpecialOptionValueTest.kt new file mode 100644 index 00000000..3237a435 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseSocketListenSpecialOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseSocketListenSpecialOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // ListenSpecial= is the one file ltype without PATH_CHECK_NON_API_VFS (systemd 8bd473ff25). + // language="unit file (systemd)" + val file = """ + [Socket] + ListenSpecial=/dev/rfkill + ListenSpecial=/proc/kmsg + ListenSpecial=/sys/kernel/debug/foo + ListenSpecial=/some/path + ListenSpecial=%t/special + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Socket] + ListenSpecial=dev/rfkill + ListenSpecial=/dev/../rfkill + ListenSpecial=%e + """.trimIndent() + + setupFileInEditor("file.socket", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerCalendarOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerCalendarOptionValueTest.kt new file mode 100644 index 00000000..f0c81ba8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerCalendarOptionValueTest.kt @@ -0,0 +1,206 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTimerCalendarOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesFromSystemdTests() { + // Every input of TEST(calendar_spec_one) in src/test/test-calendarspec.c, plus the man page examples. + // language="unit file (systemd)" + val file = """ + [Timer] + OnCalendar=Sat,Thu,Mon-Wed,Sat-Sun + OnCalendar=Sat,Thu,Mon..Wed,Sat..Sun + OnCalendar=Mon,Sun 12-*-* 2,1:23 + OnCalendar=Wed *-1 + OnCalendar=Wed-Wed,Wed *-1 + OnCalendar=Wed..Wed,Wed *-1 + OnCalendar=Wed, 17:48 + OnCalendar=Wednesday, + OnCalendar=Wed-Sat,Tue 12-10-15 1:2:3 + OnCalendar=Wed..Sat,Tue 12-10-15 1:2:3 + OnCalendar=*-*-7 0:0:0 + OnCalendar=10-15 + OnCalendar=monday *-12-* 17:00 + OnCalendar=Mon,Fri *-*-3,1,2 *:30:45 + OnCalendar=12,14,13,12:20,10,30 + OnCalendar=mon,fri *-1/2-1,3 *:30:45 + OnCalendar=03-05 08:05:40 + OnCalendar=08:05:40 + OnCalendar=05:40 + OnCalendar=Sat,Sun 12-05 08:05:40 + OnCalendar=Sat,Sun 08:05:40 + OnCalendar=2003-03-05 05:40 + OnCalendar=2003-03-05 + OnCalendar=03-05 + OnCalendar=hourly + OnCalendar=daily + OnCalendar=monthly + OnCalendar=weekly + OnCalendar=minutely + OnCalendar=quarterly + OnCalendar=semi-annually + OnCalendar=annually + OnCalendar=*:2/3 + OnCalendar=2015-10-25 01:00:00 uTc + OnCalendar=2015-10-25 01:00:00 Asia/Vladivostok + OnCalendar=weekly Pacific/Auckland + OnCalendar=2016-03-27 03:17:00.4200005 + OnCalendar=2016-03-27 03:17:00/0.42 + OnCalendar=9..11,13:00,30 + OnCalendar=1..3-1..3 1..3:1..3 + OnCalendar=00:00:1.125..2.125 + OnCalendar=00:00:1.0..3.8 + OnCalendar=00:00:01..03 + OnCalendar=00:00:01/2,02..03 + OnCalendar=*:4,30:0..3 + OnCalendar=*:4,30:0/1 + OnCalendar=*:4,30:0/1,3,5 + OnCalendar=*-*~1 Utc + OnCalendar=*-*~* 00:00:00 + OnCalendar=Monday + OnCalendar=Monday *-*-* + OnCalendar=*-*-* + OnCalendar=*:*:* + OnCalendar=*:* + OnCalendar=12:* + OnCalendar=*:30 + OnCalendar=93..00-*-* + OnCalendar=00..07-*-* + OnCalendar=*:20..39/5 + OnCalendar=00:00:20..40/1 + OnCalendar=*~03/1,03..05 + OnCalendar=@1493187147 + OnCalendar=@1493187147 UTC + OnCalendar=@0 + OnCalendar=*:05..05 + OnCalendar=*:05..10/6 + OnCalendar=Thu,Fri 2012-*-1,5 11:12:13 + OnCalendar=12..14:10,20,30 + OnCalendar=05:40:23.4200004/3.1700005 + OnCalendar=2003-02..04-05 + OnCalendar=daily UTC + OnCalendar=*-02~03 + OnCalendar=Mon *-05~07/1 + OnCalendar=Mon 2017-05~01..07 UTC + OnCalendar=Sun *-*-* 01:00:00 Europe/Dublin + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidValuesFromCorpus() { + // language="unit file (systemd)" + val file = """ + [Timer] + OnCalendar=Daily + OnCalendar=*-*-* 6:00 + OnCalendar=*:0/5 + OnCalendar=*-*-* 00,12:00:00 + OnCalendar=Sun *-*-1..7 0:45:00 + OnCalendar=Mon..Fri *-*-* 06:30:00 + OnCalendar=Mon,Tue,Wed,Thu,Fri,Sat + OnCalendar=Mon *-*-* 6:15:0 + OnCalendar=*-*-* 16,17,18,19,20,21,22,23,00,01,02,03,04,05,06:00:00 + OnCalendar=*-*-* *:00/10:00 + OnCalendar=Fri 2012-11-23 11:12:13 + OnCalendar=daily utc + OnCalendar=*-*-* %i:00 + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testValidTimezoneAndEpochSpellings() { + // Extra spaces before the zone survive only after a weekday list or a bare date (both skip a run of + // spaces); @EPOCH is read by strtoul, so whitespace and '+' may follow the '@'. CET/-03 stand for the + // host's tzname abbreviations, posix/ and Etc/ zones are real files under /usr/share/zoneinfo. + // Whether systemd accepts the zone depends on the host (sd-check at 0f0682b773): `daily CET` passes + // only with TZ=Europe/Berlin (or a tzdata that ships a CET file), `12:00 -03` only with + // TZ=America/Sao_Paulo, and posix/Europe/Berlin only where tzdata installs the posix/ tree. The plugin + // cannot know the target host, so it accepts these shapes. + // language="unit file (systemd)" + val file = """ + [Timer] + OnCalendar=Mon UTC + OnCalendar=2024-01-01 UTC + OnCalendar=Mon 2024-01-01 Europe/Berlin + OnCalendar=@+5 + OnCalendar=@ 5 + OnCalendar=daily CET + OnCalendar=12:00 -03 + OnCalendar=Mon 12:00 posix/Europe/Berlin + OnCalendar=daily Etc/GMT+5 + OnCalendar=daily Factory + OnCalendar=Mon,Fri UTC + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Rejections from TEST(calendar_spec_from_string) whose cause is a shape or a single-field bound, + // plus a few typos seen in the wild. + // language="unit file (systemd)" + val file = """ + [Timer] + OnCalendar=test + OnCalendar=7 + OnCalendar=121212:1:2 + OnCalendar=2000-03-05.23 00:00:00 + OnCalendar=2000-03-05 00:00.1:00 + OnCalendar=2016~11-22 + OnCalendar=Monday.. 12:00 + OnCalendar=Monday.. + OnCalendar=-00:+00/-5 + OnCalendar=00:+00/-5 + OnCalendar=2016- 11- 24 12: 30: 00 + OnCalendar=*~29 + OnCalendar=*~16..31 + OnCalendar=00:00/60 + OnCalendar=00:00:2300 + OnCalendar=00:00:18446744073709551615 + OnCalendar=@88588582097858858 + OnCalendar=*:4,30:*,5 + OnCalendar=*:4,30:5,* + OnCalendar=Mo *-12-* 17:00 + OnCalendar=dialy + OnCalendar=*-*-* 24:00 + OnCalendar=*-13-01 + OnCalendar=1969-01-01 + OnCalendar=%e + OnCalendar=Mon Fri + OnCalendar=Mon, Tue + OnCalendar=*-*-* 12:00 Mon + OnCalendar=*-*-* 10:00 everyday + OnCalendar=*-*-* 10:00 daily + OnCalendar=daily UTC + OnCalendar=12:00 Europe/Berlin + OnCalendar=@5 UTC + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(33, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerMonotonicOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerMonotonicOptionValueTest.kt new file mode 100644 index 00000000..2e4c7d52 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimerMonotonicOptionValueTest.kt @@ -0,0 +1,103 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTimerMonotonicOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Spellings from systemd's TEST(parse_sec) (src/test/test-time-util.c) and the Debian corpus, + // spread across all five keys that share config_parse_timer's parse_sec branch. + // language="unit file (systemd)" + val file = """ + [Timer] + OnActiveSec=5s + OnActiveSec=5s500ms + OnActiveSec= 5s 500ms + OnActiveSec=5.5s 0.5ms + OnBootSec=.22s + OnBootSec=.50y + OnBootSec=2.5 + OnBootSec=.7 + OnBootSec=23us + OnBootSec=23μs + OnBootSec=23µs + OnStartupSec=infinity + OnStartupSec=+3.1s + OnStartupSec=3.1s.2 + OnStartupSec=3.1 .2 + OnStartupSec=3.1 sec .2 sec + OnUnitActiveSec=1h30min + OnUnitActiveSec=5min 15s 100ms + OnUnitActiveSec=300ms20s 5day + OnUnitActiveSec=2 minutes + OnUnitActiveSec=1 min + OnUnitActiveSec=12 M + OnUnitActiveSec=1y2M3w4d5h6m7s + OnUnitInactiveSec=23434 + OnUnitInactiveSec=0 + OnUnitInactiveSec=15m + OnUnitInactiveSec=1 seconds + OnUnitInactiveSec=7 month + OnUnitInactiveSec=7hr 2hr + OnUnitInactiveSec=%i + OnUnitInactiveSec=%im + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testEmptyValueResets() { + // language="unit file (systemd)" + val file = """ + [Timer] + OnBootSec= + OnUnitActiveSec= + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // Rejections from TEST(parse_sec), plus mis-pluralised units and an unknown specifier. + // language="unit file (systemd)" + val file = """ + [Timer] + OnActiveSec=xyz + OnActiveSec=5. + OnActiveSec=.s + OnBootSec=-5s + OnBootSec=0.-0s + OnBootSec=infinity .7 + OnStartupSec=.3 infinity + OnStartupSec=3.+1s + OnStartupSec=3. 1s + OnUnitActiveSec=3.s + OnUnitActiveSec=12.34.56 + OnUnitActiveSec=12..34 + OnUnitActiveSec=5mins + OnUnitInactiveSec=2hs + OnUnitInactiveSec=12foo 1 bar + OnUnitInactiveSec=min 1 sec + OnUnitInactiveSec=%e + """.trimIndent() + + setupFileInEditor("file.timer", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(17, highlights) + } +} From 1bee9eb2a404a933604f94430271e6c5dd080fc0 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 12:02:20 -0700 Subject: [PATCH 4/8] feat: validate .nspawn container settings Add grammar-based value validation for: AmbientCapability=, Bind=, BindReadOnly=, BindUser=, Capability=, DropCapability=, IPVLAN=, Inaccessible=, Interface=, LinkJournal=, MACVLAN=, MachineID=, OOMScoreAdjust=, Overlay=, OverlayReadOnly=, PivotRoot=, Port=, ResolvConf=, RestrictAddressFamilies=, TemporaryFileSystem=, VirtualEthernetExtra=, Volatile=, Zone= Accepted values follow systemd's own parsers (derived from the systemd source at 0f0682b773 and checked against systemd's real parsers built at that commit). Values systemd accepts when parsing but rejects only at unit or container start are accepted; see #565. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 25 +++ .../ai/ConfigParseBindOptionValue.kt | 103 +++++++++ .../ai/ConfigParseBindUserOptionValue.kt | 37 ++++ .../ai/ConfigParseCapabilityOptionValue.kt | 69 ++++++ .../ai/ConfigParseExposePortOptionValue.kt | 57 +++++ .../ai/ConfigParseId128OptionValue.kt | 46 ++++ .../ai/ConfigParseInaccessibleOptionValue.kt | 32 +++ .../ConfigParseInterfacePairOptionValues.kt | 68 ++++++ .../ai/ConfigParseLinkJournalOptionValue.kt | 38 ++++ .../ai/ConfigParseNetworkZoneOptionValue.kt | 34 +++ .../ConfigParseOomScoreAdjustOptionValue.kt | 46 ++++ .../ai/ConfigParseOverlayOptionValue.kt | 68 ++++++ .../ai/ConfigParsePivotRootOptionValue.kt | 41 ++++ .../ai/ConfigParseResolvConfOptionValue.kt | 48 +++++ ...ParseRestrictAddressFamiliesOptionValue.kt | 66 ++++++ .../ai/ConfigParseTmpfsOptionValue.kt | 45 ++++ .../ai/ConfigParseVethExtraOptionValue.kt | 53 +++++ .../ai/ConfigParseVolatileModeOptionValue.kt | 41 ++++ .../grammar/BatchNspawnCombinators.kt | 202 ++++++++++++++++++ .../ai/ConfigParseBindOptionValueTest.kt | 86 ++++++++ .../ai/ConfigParseBindUserOptionValueTest.kt | 49 +++++ .../ConfigParseCapabilityOptionValueTest.kt | 57 +++++ .../ConfigParseExposePortOptionValueTest.kt | 87 ++++++++ .../ai/ConfigParseId128OptionValueTest.kt | 50 +++++ .../ConfigParseInaccessibleOptionValueTest.kt | 46 ++++ ...ConfigParseInterfacePairOptionValueTest.kt | 65 ++++++ .../ConfigParseLinkJournalOptionValueTest.kt | 57 +++++ .../ConfigParseNetworkZoneOptionValueTest.kt | 47 ++++ ...onfigParseOomScoreAdjustOptionValueTest.kt | 80 +++++++ .../ai/ConfigParseOverlayOptionValueTest.kt | 54 +++++ .../ai/ConfigParsePivotRootOptionValueTest.kt | 52 +++++ .../ConfigParseResolvConfOptionValueTest.kt | 65 ++++++ ...eRestrictAddressFamiliesOptionValueTest.kt | 74 +++++++ .../ai/ConfigParseTmpfsOptionValueTest.kt | 48 +++++ .../ai/ConfigParseVethExtraOptionValueTest.kt | 54 +++++ .../ConfigParseVolatileModeOptionValueTest.kt | 57 +++++ 36 files changed, 2147 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBindUserOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCapabilityOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseExposePortOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseId128OptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInaccessibleOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInterfacePairOptionValues.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkJournalOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetworkZoneOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOomScoreAdjustOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseOverlayOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePivotRootOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolvConfOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRestrictAddressFamiliesOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTmpfsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVethExtraOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVolatileModeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNspawnCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBindUserOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCapabilityOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseExposePortOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseId128OptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInaccessibleOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInterfacePairOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkJournalOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetworkZoneOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOomScoreAdjustOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseOverlayOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePivotRootOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolvConfOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRestrictAddressFamiliesOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTmpfsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVethExtraOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVolatileModeOptionValueTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 8a8806ef..1efc51a7 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 @@ -468,6 +468,31 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_path", "0") to ConfigParsePathOptionValue() as OptionValueInformation, Validator("config_parse_long", "0") to ConfigParseLongOptionValue() as OptionValueInformation, Validator("config_parse_iec_size_long", "0") to ConfigParseIecSizeLongOptionValue() as OptionValueInformation, + // Batch 2026-10: nspawn + // .nspawn settings (src/nspawn/nspawn-settings.c at 0f0682b773). None of these parsers is used by + // any other gperf table, and every (parser, ltype) below is shared only by keys of one value type. + Validator("config_parse_capability", "0") to ConfigParseCapabilityOptionValue() as OptionValueInformation, + Validator("config_parse_volatile_mode", "0") to ConfigParseVolatileModeOptionValue() as OptionValueInformation, + Validator("config_parse_veth_extra", "0") to ConfigParseVethExtraOptionValue() as OptionValueInformation, + Validator("config_parse_tmpfs", "0") to ConfigParseTmpfsOptionValue() as OptionValueInformation, + Validator("config_parse_restrict_address_families", "0") to ConfigParseRestrictAddressFamiliesOptionValue() as OptionValueInformation, + Validator("config_parse_resolv_conf", "0") to ConfigParseResolvConfOptionValue() as OptionValueInformation, + Validator("config_parse_pivot_root", "0") to ConfigParsePivotRootOptionValue() as OptionValueInformation, + // ltype only selects read-only for Overlay=/OverlayReadOnly= and Bind=/BindReadOnly=. + Validator("config_parse_overlay", "0") to ConfigParseOverlayOptionValue() as OptionValueInformation, + Validator("config_parse_overlay", "1") to ConfigParseOverlayOptionValue() as OptionValueInformation, + Validator("config_parse_oom_score_adjust", "0") to ConfigParseOomScoreAdjustOptionValue() as OptionValueInformation, + Validator("config_parse_network_zone", "0") to ConfigParseNetworkZoneOptionValue() as OptionValueInformation, + Validator("config_parse_network_iface_pair", "0") to ConfigParseNetworkIfacePairOptionValue() as OptionValueInformation, + Validator("config_parse_macvlan_iface_pair", "0") to ConfigParseMacvlanIfacePairOptionValue() as OptionValueInformation, + Validator("config_parse_ipvlan_iface_pair", "0") to ConfigParseIpvlanIfacePairOptionValue() as OptionValueInformation, + Validator("config_parse_link_journal", "0") to ConfigParseLinkJournalOptionValue() as OptionValueInformation, + Validator("config_parse_inaccessible", "0") to ConfigParseInaccessibleOptionValue() as OptionValueInformation, + Validator("config_parse_id128", "0") to ConfigParseId128OptionValue() as OptionValueInformation, + Validator("config_parse_expose_port", "0") to ConfigParseExposePortOptionValue() as OptionValueInformation, + Validator("config_parse_bind_user", "0") to ConfigParseBindUserOptionValue() as OptionValueInformation, + Validator("config_parse_bind", "0") to ConfigParseBindOptionValue() as OptionValueInformation, + Validator("config_parse_bind", "1") to ConfigParseBindOptionValue() as OptionValueInformation, ) 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) + } +} From 3bfc29c3e5cc36c42875d3d747313b8596376b71 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 12:02:20 -0700 Subject: [PATCH 5/8] feat: validate networkd .netdev and udev .link settings Add grammar-based value validation for: Advertise=, Alias=, AllowedIPs=, AlternativeName=, CPUAffinity=, DefaultPVID=, EgressQOSMaps=, Endpoint=, GatewayBandwidthDown=, GatewayBandwidthUp=, GatewayBandwithDown=, GatewayBandwithUp=, Group=, IPv6FlowLabel=, IRQAffinity=, IRQAffinityNUMA=, IRQAffinityPolicy=, IngressQOSMaps=, KeyFile=, KeyId=, LinkMode=, Local=, MACAddressPolicy=, Name=, PartitionKey=, Port=, PortRange=, Ports=, PresharedKeyFile=, PrivateKey=, PrivateKeyFile=, Property=, Protocol=, PublicKeyFile=, ReceivePacketSteeringCPUMask=, Remote=, RouteTable=, Table=, VLANProtocol=, WakeOnLanPassword= Accepted values follow systemd's own parsers (derived from the systemd source at 0f0682b773 and checked against systemd's real parsers built at that commit). Values systemd accepts when parsing but rejects only at unit or container start are accepted; see #565. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 40 ++ .../ai/ConfigParseAdvertiseOptionValue.kt | 75 +++ .../ConfigParseBadadvBandwidthOptionValue.kt | 52 ++ .../ai/ConfigParseCoalesceNsecOptionValue.kt | 32 ++ .../ai/ConfigParseCpuSetOptionValue.kt | 34 ++ ...ConfigParseDefaultPortVlanidOptionValue.kt | 50 ++ .../ai/ConfigParseGeneveAddressOptionValue.kt | 30 + .../ai/ConfigParseIfaliasOptionValue.kt | 27 + .../ai/ConfigParseIfnamesOptionValue.kt | 37 ++ .../ai/ConfigParseIpoibPkeyOptionValue.kt | 50 ++ .../ai/ConfigParseIpv6FlowlabelOptionValue.kt | 43 ++ .../ConfigParseIrqAffinityNumaOptionValue.kt | 44 ++ ...ConfigParseIrqAffinityPolicyOptionValue.kt | 26 + .../ConfigParseL2tpSessionNameOptionValue.kt | 30 + ...gParseL2tpTunnelLocalAddressOptionValue.kt | 51 ++ ...ParseL2tpTunnelRemoteAddressOptionValue.kt | 28 + .../ConfigParseMacAddressPolicyOptionValue.kt | 26 + .../ai/ConfigParseMacsecKeyFileOptionValue.kt | 30 + .../ai/ConfigParseMacsecKeyIdOptionValue.kt | 44 ++ .../ai/ConfigParsePortOptionValue.kt | 26 + .../ai/ConfigParsePortRangeOptionValue.kt | 61 +++ .../ai/ConfigParseRpsCpuMaskOptionValue.kt | 33 ++ .../ai/ConfigParseUdevPropertyOptionValue.kt | 115 ++++ .../ai/ConfigParseVlanQosMapsOptionValue.kt | 103 ++++ .../ai/ConfigParseVlanprotocolOptionValue.kt | 26 + .../ai/ConfigParseVrfTableOptionValue.kt | 34 ++ .../ai/ConfigParseVxlanAddressOptionValue.kt | 74 +++ ...nfigParseWireguardAllowedIpsOptionValue.kt | 44 ++ ...ConfigParseWireguardEndpointOptionValue.kt | 81 +++ ...figParseWireguardPeerKeyFileOptionValue.kt | 30 + ...ParseWireguardPeerRouteTableOptionValue.kt | 41 ++ ...ParseWireguardPrivateKeyFileOptionValue.kt | 30 + ...nfigParseWireguardPrivateKeyOptionValue.kt | 35 ++ ...nfigParseWireguardRouteTableOptionValue.kt | 41 ++ .../ai/ConfigParseWolPasswordOptionValue.kt | 35 ++ .../grammar/BatchNetdevLinkCombinators.kt | 517 ++++++++++++++++++ .../ai/ConfigParseAdvertiseOptionValueTest.kt | 44 ++ ...nfigParseBadadvBandwidthOptionValueTest.kt | 67 +++ .../ConfigParseCoalesceNsecOptionValueTest.kt | 52 ++ .../ai/ConfigParseCpuSetOptionValueTest.kt | 63 +++ ...igParseDefaultPortVlanidOptionValueTest.kt | 50 ++ ...ConfigParseGeneveAddressOptionValueTest.kt | 46 ++ .../ai/ConfigParseIfaliasOptionValueTest.kt | 42 ++ .../ai/ConfigParseIfnamesOptionValueTest.kt | 44 ++ .../ai/ConfigParseIpoibPkeyOptionValueTest.kt | 49 ++ ...ConfigParseIpv6FlowlabelOptionValueTest.kt | 47 ++ ...nfigParseIrqAffinityNumaOptionValueTest.kt | 48 ++ ...igParseIrqAffinityPolicyOptionValueTest.kt | 40 ++ ...nfigParseL2tpSessionNameOptionValueTest.kt | 45 ++ ...seL2tpTunnelLocalAddressOptionValueTest.kt | 57 ++ ...eL2tpTunnelRemoteAddressOptionValueTest.kt | 41 ++ ...figParseMacAddressPolicyOptionValueTest.kt | 42 ++ ...ConfigParseMacsecKeyFileOptionValueTest.kt | 43 ++ .../ConfigParseMacsecKeyIdOptionValueTest.kt | 52 ++ .../ai/ConfigParsePortOptionValueTest.kt | 44 ++ .../ai/ConfigParsePortRangeOptionValueTest.kt | 56 ++ .../ConfigParseRpsCpuMaskOptionValueTest.kt | 50 ++ .../ConfigParseUdevPropertyOptionValueTest.kt | 60 ++ .../ConfigParseVlanQosMapsOptionValueTest.kt | 59 ++ .../ConfigParseVlanprotocolOptionValueTest.kt | 48 ++ .../ai/ConfigParseVrfTableOptionValueTest.kt | 60 ++ .../ConfigParseVxlanAddressOptionValueTest.kt | 72 +++ ...ParseWireguardAllowedIpsOptionValueTest.kt | 55 ++ ...igParseWireguardEndpointOptionValueTest.kt | 61 +++ ...arseWireguardPeerKeyFileOptionValueTest.kt | 47 ++ ...eWireguardPeerRouteTableOptionValueTest.kt | 63 +++ ...eWireguardPrivateKeyFileOptionValueTest.kt | 45 ++ ...ParseWireguardPrivateKeyOptionValueTest.kt | 46 ++ ...ParseWireguardRouteTableOptionValueTest.kt | 64 +++ .../ConfigParseWolPasswordOptionValueTest.kt | 45 ++ .../grammar/BatchNetdevLinkCombinatorsTest.kt | 153 ++++++ 71 files changed, 3975 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index 1efc51a7..c41e1c78 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 @@ -493,6 +493,46 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_bind_user", "0") to ConfigParseBindUserOptionValue() as OptionValueInformation, Validator("config_parse_bind", "0") to ConfigParseBindOptionValue() as OptionValueInformation, Validator("config_parse_bind", "1") to ConfigParseBindOptionValue() as OptionValueInformation, + // Batch 2026-10: netdev-link + // systemd-networkd .netdev (src/network/netdev/) and udev .link (src/udev/net/) settings, pinned to + // systemd 0f0682b773. Every key using each (parser, ltype) pair was checked in the gperf files; where + // a parser branches on the key name (vxlan_address) the grammar is the union and says so. + Validator("config_parse_badadv_bandwidth", "0") to ConfigParseBadadvBandwidthOptionValue() as OptionValueInformation, + Validator("config_parse_vxlan_address", "0") to ConfigParseVxlanAddressOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_peer_key_file", "0") to ConfigParseWireguardPeerKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_vlanprotocol", "0") to ConfigParseVlanprotocolOptionValue() as OptionValueInformation, + Validator("config_parse_vlan_qos_maps", "0") to ConfigParseVlanQosMapsOptionValue() as OptionValueInformation, + Validator("config_parse_macsec_key_id", "0") to ConfigParseMacsecKeyIdOptionValue() as OptionValueInformation, + Validator("config_parse_macsec_key_file", "0") to ConfigParseMacsecKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_cpu_set", "0") to ConfigParseCpuSetOptionValue() as OptionValueInformation, + Validator("config_parse_wol_password", "0") to ConfigParseWolPasswordOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_route_table", "0") to ConfigParseWireguardRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_private_key_file", "0") to ConfigParseWireguardPrivateKeyFileOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_private_key", "0") to ConfigParseWireguardPrivateKeyOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_peer_route_table", "0") to ConfigParseWireguardPeerRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_endpoint", "0") to ConfigParseWireguardEndpointOptionValue() as OptionValueInformation, + Validator("config_parse_wireguard_allowed_ips", "0") to ConfigParseWireguardAllowedIpsOptionValue() as OptionValueInformation, + Validator("config_parse_vrf_table", "0") to ConfigParseVrfTableOptionValue() as OptionValueInformation, + Validator("config_parse_udev_property", "0") to ConfigParseUdevPropertyOptionValue() as OptionValueInformation, + Validator("config_parse_rps_cpu_mask", "0") to ConfigParseRpsCpuMaskOptionValue() as OptionValueInformation, + Validator("config_parse_port_range", "0") to ConfigParsePortRangeOptionValue() as OptionValueInformation, + Validator("config_parse_port", "0") to ConfigParsePortOptionValue() as OptionValueInformation, + Validator("config_parse_mac_address_policy", "0") to ConfigParseMacAddressPolicyOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_tunnel_remote_address", "0") to ConfigParseL2tpTunnelRemoteAddressOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_tunnel_local_address", "0") to ConfigParseL2tpTunnelLocalAddressOptionValue() as OptionValueInformation, + Validator("config_parse_l2tp_session_name", "0") to ConfigParseL2tpSessionNameOptionValue() as OptionValueInformation, + Validator("config_parse_irq_affinity_policy", "0") to ConfigParseIrqAffinityPolicyOptionValue() as OptionValueInformation, + Validator("config_parse_irq_affinity_numa", "0") to ConfigParseIrqAffinityNumaOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_flowlabel", "0") to ConfigParseIpv6FlowlabelOptionValue() as OptionValueInformation, + Validator("config_parse_ipoib_pkey", "0") to ConfigParseIpoibPkeyOptionValue() as OptionValueInformation, + Validator("config_parse_ifnames", "IFNAME_VALID_ALTERNATIVE") to ConfigParseIfnamesOptionValue() as OptionValueInformation, + Validator("config_parse_ifalias", "0") to ConfigParseIfaliasOptionValue() as OptionValueInformation, + Validator("config_parse_geneve_address", "0") to ConfigParseGeneveAddressOptionValue() as OptionValueInformation, + Validator("config_parse_default_port_vlanid", "0") to ConfigParseDefaultPortVlanidOptionValue() as OptionValueInformation, + Validator("config_parse_advertise", "0") to ConfigParseAdvertiseOptionValue() as OptionValueInformation, + // RxCoalesceCQENSec= registers config_parse_coalesce_sec with ltype `true` (parse_nsec instead of + // parse_sec); the `0` entry above (ConfigParseCoalesceSecOptionValue) covers the parse_sec keys. + Validator("config_parse_coalesce_sec", "true") to ConfigParseCoalesceNsecOptionValue() as OptionValueInformation, ) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt new file mode 100644 index 00000000..d5d929ee --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAdvertiseOptionValue.kt @@ -0,0 +1,75 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * [Link] Advertise= and [EnergyEfficientEthernet] LinkMode= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Advertise= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L1125-L1177 config_parse_advertise (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L162-L168 ethtool_link_mode_bit_table (DEFINE_STRING_TABLE_LOOKUP) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-link-mode.py (generates the table from enum ethtool_link_mode_bit_indices) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/ethtool.h#L1966-L2109 enum ethtool_link_mode_bit_indices + * linux https://man7.org/linux/man-pages/man8/ethtool.8.html + */ + +/** + * The link-mode names ethtool-link-mode.py derives from the vendored kernel header: each + * `ETHTOOL_LINK_MODE__BIT` lower-cased with `_` turned into `-`, and `autoneg` renamed + * `autonegotiation`. 125 names at 0f0682b773. + */ +private val LINK_MODE = FlexibleLiteralChoiceTerminal( + "10baset-half", "10baset-full", "100baset-half", "100baset-full", "1000baset-half", + "1000baset-full", "autonegotiation", "tp", "aui", "mii", "fibre", "bnc", "10000baset-full", + "pause", "asym-pause", "2500basex-full", "backplane", "1000basekx-full", "10000basekx4-full", + "10000basekr-full", "10000baser-fec", "20000basemld2-full", "20000basekr2-full", + "40000basekr4-full", "40000basecr4-full", "40000basesr4-full", "40000baselr4-full", + "56000basekr4-full", "56000basecr4-full", "56000basesr4-full", "56000baselr4-full", + "25000basecr-full", "25000basekr-full", "25000basesr-full", "50000basecr2-full", + "50000basekr2-full", "100000basekr4-full", "100000basesr4-full", "100000basecr4-full", + "100000baselr4-er4-full", "50000basesr2-full", "1000basex-full", "10000basecr-full", + "10000basesr-full", "10000baselr-full", "10000baselrm-full", "10000baseer-full", + "2500baset-full", "5000baset-full", "fec-none", "fec-rs", "fec-baser", "50000basekr-full", + "50000basesr-full", "50000basecr-full", "50000baselr-er-fr-full", "50000basedr-full", + "100000basekr2-full", "100000basesr2-full", "100000basecr2-full", "100000baselr2-er2-fr2-full", + "100000basedr2-full", "200000basekr4-full", "200000basesr4-full", "200000baselr4-er4-fr4-full", + "200000basedr4-full", "200000basecr4-full", "100baset1-full", "1000baset1-full", + "400000basekr8-full", "400000basesr8-full", "400000baselr8-er8-fr8-full", "400000basedr8-full", + "400000basecr8-full", "fec-llrs", "100000basekr-full", "100000basesr-full", + "100000baselr-er-fr-full", "100000basecr-full", "100000basedr-full", "200000basekr2-full", + "200000basesr2-full", "200000baselr2-er2-fr2-full", "200000basedr2-full", "200000basecr2-full", + "400000basekr4-full", "400000basesr4-full", "400000baselr4-er4-fr4-full", "400000basedr4-full", + "400000basecr4-full", "100basefx-half", "100basefx-full", "10baset1l-full", "800000basecr8-full", + "800000basekr8-full", "800000basedr8-full", "800000basedr8-2-full", "800000basesr8-full", + "800000basevr8-full", "10baset1s-full", "10baset1s-half", "10baset1s-p2mp-half", + "10baset1brr-full", "200000basecr-full", "200000basekr-full", "200000basedr-full", + "200000basedr-2-full", "200000basesr-full", "200000basevr-full", "400000basecr2-full", + "400000basekr2-full", "400000basedr2-full", "400000basedr2-2-full", "400000basesr2-full", + "400000basevr2-full", "800000basecr4-full", "800000basekr4-full", "800000basedr4-full", + "800000basedr4-2-full", "800000basesr4-full", "800000basevr4-full", "1600000basecr8-full", + "1600000basekr8-full", "1600000basedr8-full", "1600000basedr8-2-full" +) + +/** + * Validator for `[Link] Advertise=` and `[EnergyEfficientEthernet] LinkMode=` (.link). + * + * C function: config_parse_advertise. A whitespace-separated list (extract_first_word, no unquoting) of + * ethtool link-mode names, each looked up exactly (case-sensitive) in ethtool_link_mode_bit_table — + * speed/duplex modes like `1000baset-full` as well as the non-speed bits (`autonegotiation`, `pause`, + * `asym-pause`, `tp`, `fibre`, `fec-rs`, …). + * + * The table is generated at build time from the kernel's enum ethtool_link_mode_bit_indices in + * systemd's vendored copy of linux/ethtool.h (src/include/uapi, which meson puts first on the include + * path). That is wider than the ethtool-link-mode.xml table in the plugin's generated man data, which + * lists 102 names; this follows the header the C is built against. An unknown name is logged and + * skipped by udev; an empty assignment clears the list. + */ +class ConfigParseAdvertiseOptionValue : SimpleGrammarOptionValues( + "config_parse_advertise", + SequenceCombinator(LINK_MODE, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), LINK_MODE)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt new file mode 100644 index 00000000..4d6c601d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBadadvBandwidthOptionValue.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetdevLinkCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkParseSizeSi +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.siSize +import java.math.BigInteger + +/* + * [BatmanAdvanced] GatewayBandwidthDown= / GatewayBandwidthUp= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#GatewayBandwidthDown= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/batadv.c#L53-L89 config_parse_badadv_bandwidth (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/batadv.c#L81-L84 (the 100 kbit uint32_t range warning) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1000) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-batadv.netdev#L14-L15 + */ + +/** + * Validator for `[BatmanAdvanced] GatewayBandwidthDown=` and `GatewayBandwidthUp=` (.netdev), plus the + * misspelled `GatewayBandwithDown=` / `GatewayBandwithUp=` that systemd still accepts as aliases. + * + * C function: config_parse_badadv_bandwidth, which hands the whole value to parse_size(rvalue, 1000, …): + * a number with an optional case-sensitive SI suffix (`K`, `M`, `G`, `T`, `P`, `E`, `B`), a + * fraction allowed, and several elements with strictly descending suffixes (`1G 500M`) summed. See + * [siSize]. + * + * The total is stored in units of 100 kbit in a uint32_t, so a total of 4294967296 × 100000 + * (429496729600000, e.g. `12P` or `3E 2P`) or more is flagged. systemd logs that case ("is outside of + * 0...429496729500000 range") and then stores the total truncated to 32 bits, which is never what was + * meant, so it is treated as invalid here. Totals that overflow parse_size itself (`20E`) are refused + * outright. Confirmed against systemd's parser built at 0f0682b773: `429496729599999` is silent, + * `429496729600000` warns. Any other invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `GatewayBandwidthDown=` or `GatewayBandwidthUp=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. + */ +class ConfigParseBadadvBandwidthOptionValue : SimpleGrammarOptionValues( + "config_parse_badadv_bandwidth", + SequenceCombinator( + NetdevLinkCheckedSpan(siSize(), "total < 429496729600000") { text -> + netdevLinkParseSizeSi(text)?.let { it < BATADV_BANDWIDTH_LIMIT } == true + }, + EOF() + ) +) + +/** `k/1000/100 > UINT32_MAX` (batadv.c#L81) first holds at this total. */ +private val BATADV_BANDWIDTH_LIMIT: BigInteger = BigInteger.valueOf(4_294_967_296L) * BigInteger.valueOf(100_000L) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt new file mode 100644 index 00000000..5eaf1d1d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCoalesceNsecOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NSEC_DURATION_FINITE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] RxCoalesceCQENSec= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#RxCoalesceCQENSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L1370-L1415 config_parse_coalesce_sec (ltype "true", i.e. nsec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1374-L1466 parse_nsec + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L141 TEST(parse_nsec) + */ + +/** + * Validator for `[Link] RxCoalesceCQENSec=` (.link) — the one key that registers config_parse_coalesce_sec + * with ltype `true`, which switches the parser from parse_sec() to parse_nsec(). + * + * parse_nsec reads one or more `number[unit]` elements, a bare number meaning nanoseconds (`500`, + * `1us`, `2.5`, `1s500ms`, `.7`). See [NSEC_DURATION_FINITE]. + * + * `infinity` parses, but config_parse_coalesce_sec then refuses any result above UINT32_MAX ("Too large + * … value"), and NSEC_INFINITY is UINT64_MAX — so `infinity` is never a usable value here and is + * rejected. The same UINT32_MAX ceiling (about 4.29 seconds in nanoseconds) applies to every finite value + * too, but that compares the computed sum and is not modelled. An invalid value is logged and ignored. + */ +class ConfigParseCoalesceNsecOptionValue : SimpleGrammarOptionValues( + "config_parse_coalesce_sec", + SequenceCombinator(NSEC_DURATION_FINITE, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt new file mode 100644 index 00000000..1e2210b0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCpuSetOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CPU_SET +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] IRQAffinity= (.link) and [Exec] CPUAffinity= (.nspawn). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinity= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#CPUAffinity= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L220-L307 config_parse_cpu_set (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L186-L206 cpu_set_add_range (CPU_SET_MAX_NCPU) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-cpu-set-util.c#L36 TEST(parse_cpu_set) + * linux https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html + */ + +/** + * Validator for `[Link] IRQAffinity=` (.link) and `[Exec] CPUAffinity=` (.nspawn). + * + * C function: config_parse_cpu_set. CPU indices and `N-M` ranges, separated by any mix of whitespace + * and commas (separators coalesce; leading/trailing ones are fine), each word optionally quoted. Each + * bound is a safe_atou() number (so hex works) below CPU_SET_MAX_NCPU = 8192. See [CPU_SET]. + * + * Lenient on one point: a reversed range such as `3-0` is rejected by systemd after parsing (it + * compares the numbers); that is not modelled. An empty assignment clears the set. + */ +class ConfigParseCpuSetOptionValue : SimpleGrammarOptionValues( + "config_parse_cpu_set", + SequenceCombinator(CPU_SET, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt new file mode 100644 index 00000000..4d75d13e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDefaultPortVlanidOptionValue.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Bridge] DefaultPVID= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#DefaultPVID= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.c#L44-L67 config_parse_default_port_vlanid (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.c#L8-L23 parse_vlanid (safe_atou16, vlanid_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.h#L7-L13 VLANID_MAX (4094) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-bridge.netdev#L13 DefaultPVID=9 + */ + +/** + * Validator for `[Bridge] DefaultPVID=` (.netdev). + * + * C function: config_parse_default_port_vlanid. Either `none` (no PVID) or a VLAN id read by + * parse_vlanid (safe_atou16, so hex, octal and `0b`/`0o` work) and checked by vlanid_is_valid, i.e. 0…4094. + * + * The man page says "an integer in the range 1…4094", but vlanid_is_valid only checks `id <= 4094`, so + * `0` is accepted (and stored as 0, the same as `none`). This follows the C. An invalid value is logged + * and ignored by systemd. + * + * There is no isempty() branch, so an empty `DefaultPVID=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. + */ +class ConfigParseDefaultPortVlanidOptionValue : SimpleGrammarOptionValues( + "config_parse_default_port_vlanid", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("none"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4094L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt new file mode 100644 index 00000000..941564c0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGeneveAddressOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetdevLinkCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkIsMulticast + +/* + * [GENEVE] Remote= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Remote= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/geneve.c#L124-L163 config_parse_geneve_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L103-L113 in_addr_is_multicast + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[GENEVE] Remote=` (.netdev). + * + * C function: config_parse_geneve_address: an IPv4 or IPv6 literal (in_addr_from_string_auto → + * inet_pton) that is not a multicast address (224.0.0.0/4, ff00::/8). An invalid value is logged and + * ignored by systemd. + */ +class ConfigParseGeneveAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_geneve_address", + SequenceCombinator(NetdevLinkCheckedSpan(IP_ADDR, "not multicast") { !netdevLinkIsMulticast(it) }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt new file mode 100644 index 00000000..b4b05b86 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfaliasOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] Alias= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Alias= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L1944-L1980 config_parse_ifalias (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/if.h#L33 IFALIASZ (256) + */ + +/** + * Validator for `[Link] Alias=` (.link). + * + * C function: config_parse_ifalias. The value is free text, but it must be ASCII (ascii_is_valid) and + * shorter than IFALIASZ = 256 bytes, so at most 255 characters. Anything containing a non-ASCII + * character (`Café uplink`) or longer than that is logged and ignored by udev. An empty assignment + * clears the alias. + */ +class ConfigParseIfaliasOptionValue : SimpleGrammarOptionValues( + "config_parse_ifalias", + SequenceCombinator(RegexTerminal("[\\s\\S]+", "[\\x00-\\x7F]{1,255}"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt new file mode 100644 index 00000000..a3f6805b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIfnamesOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * [Link] AlternativeName= (.link) and [HSR] Ports= (.netdev). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#AlternativeName= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Ports= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1651-L1702 config_parse_ifnames (IFNAME_VALID_ALTERNATIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-hsr.netdev#L7 Ports=test1 dummy98 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-util.c#L29 TEST(ifname_valid) + */ + +/** + * Validator for `[Link] AlternativeName=` (.link) and `[HSR] Ports=` (.netdev). + * + * C function: config_parse_ifnames with ltype IFNAME_VALID_ALTERNATIVE: a whitespace-separated list + * (extract_first_word, no unquoting) of interface names, each checked by ifname_valid_full with + * IFNAME_VALID_ALTERNATIVE — up to 127 printable ASCII characters other than `:`, `/` and `%`, not + * entirely digits, and not `.`, `..`, `all` or `default`. See [ALTERNATIVE_INTERFACE_NAME]. An + * invalid name is logged and skipped; an empty assignment clears the list. + */ +class ConfigParseIfnamesOptionValue : SimpleGrammarOptionValues( + "config_parse_ifnames", + SequenceCombinator( + ALTERNATIVE_INTERFACE_NAME, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), ALTERNATIVE_INTERFACE_NAME)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt new file mode 100644 index 00000000..b75098ef --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpoibPkeyOptionValue.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [IPoIB] PartitionKey= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PartitionKey= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/ipoib.c#L100-L140 config_parse_ipoib_pkey (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L599-L613 safe_atou16_full (base 0) + */ + +/** + * Validator for `[IPoIB] PartitionKey=` (.netdev). + * + * C function: config_parse_ipoib_pkey. A uint16 read with safe_atou16() (so `0x8001` works, which is + * how partition keys are usually written) in 1…0xffff, except 0x8000 (the full-membership bit with an + * empty key). Spelled as two ranges either side of 0x8000. An empty assignment unsets it; an invalid + * value is logged and ignored by systemd. + */ +class ConfigParseIpoibPkeyOptionValue : SimpleGrammarOptionValues( + "config_parse_ipoib_pkey", + SequenceCombinator( + AlternativeCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 0x7fffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0x8001L, + maxInclusive = 0xffffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt new file mode 100644 index 00000000..2b7c51d3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6FlowlabelOptionValue.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Tunnel] IPv6FlowLabel= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#IPv6FlowLabel= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/tunnel.c#L907-L942 config_parse_ipv6_flowlabel (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2151-L2182 config_parse_unsigned_bounded → safe_atou_bounded + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Tunnel] IPv6FlowLabel=` (.netdev). + * + * C function: config_parse_ipv6_flowlabel. Either `inherit` (copy the inner packet's flow label) or a + * 20-bit flow label 0…0xFFFFF read by config_parse_uint32_bounded (safe_atou, so hex works, as the man + * page's own range notation suggests). An out-of-range value is logged and ignored by systemd. + */ +class ConfigParseIpv6FlowlabelOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_flowlabel", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("inherit"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 0xfffffL, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt new file mode 100644 index 00000000..ac5afd41 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityNumaOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] IRQAffinityNUMA= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinityNUMA= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2157-L2201 config_parse_irq_affinity_numa (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.h#L35-L36 IRQ_AFFINITY_NUMA_UNSET / IRQ_AFFINITY_NUMA_LOCAL + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Link] IRQAffinityNUMA=` (.link). + * + * C function: config_parse_irq_affinity_numa. Either `local` (the device's own NUMA node) or a NUMA + * node number read with safe_atou() (so hex works). UINT_MAX and UINT_MAX - 1 are reserved as the + * "unset" and "local" markers, so the number must be at most 4294967293. An empty assignment unsets it. + * An out-of-range number is logged and ignored by udev. + */ +class ConfigParseIrqAffinityNumaOptionValue : SimpleGrammarOptionValues( + "config_parse_irq_affinity_numa", + SequenceCombinator( + AlternativeCombinator( + FlexibleLiteralChoiceTerminal("local"), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_293L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt new file mode 100644 index 00000000..726caac6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIrqAffinityPolicyOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] IRQAffinityPolicy= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#IRQAffinityPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2209-L2213 config_parse_irq_affinity_policy (0), DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2203-L2206 irq_affinity_policy_table + */ + +/** + * Validator for `[Link] IRQAffinityPolicy=` (.link). + * + * C function: config_parse_irq_affinity_policy, generated by DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT over + * irq_affinity_policy_table: exactly `single` or `spread` (case-sensitive). An empty assignment unsets + * it. An invalid value is logged and ignored by udev. + */ +class ConfigParseIrqAffinityPolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_irq_affinity_policy", + SequenceCombinator(FlexibleLiteralChoiceTerminal("single", "spread"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt new file mode 100644 index 00000000..c063fc5f --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpSessionNameOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [L2TPSession] Name= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Name= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L727-L765 config_parse_l2tp_session_name (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0, via ifname_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-util.c#L29 TEST(ifname_valid) + */ + +/** + * Validator for `[L2TPSession] Name=` (.netdev). + * + * C function: config_parse_l2tp_session_name, which requires ifname_valid(): 1…15 printable ASCII + * characters other than `:`, `/` and `%`, not entirely digits, and not `.`, `..`, `all` or + * `default`. See [INTERFACE_NAME]. An invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `Name=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. + */ +class ConfigParseL2tpSessionNameOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_session_name", + SequenceCombinator(INTERFACE_NAME, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt new file mode 100644 index 00000000..94ce597e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelLocalAddressOptionValue.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [L2TP] Local= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Local= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L477-L569 config_parse_l2tp_tunnel_local_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L38-L42 l2tp_local_address_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("@", no flags; leading-separator skip at #L39-L56) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + */ + +/** + * Validator for `[L2TP] Local=` (.netdev). + * + * C function: config_parse_l2tp_tunnel_local_address. `ADDRESS[@INTERFACE]`, where ADDRESS is one of + * the l2tp_local_address_type_table names `auto`, `static`, `dynamic` or an IPv4/IPv6 literal, and the + * optional interface (everything after the first `@`) must pass ifname_valid_full with + * IFNAME_VALID_ALTERNATIVE (up to 127 characters). + * + * The `@` is cut with extract_first_word(…, "@", 0), which coalesces a run of separators, so `auto@@eth0` + * and even a trailing `auto@` (no interface) are accepted; it also skips leading separators, so `@auto` + * is `auto` (a lone `@` is an empty word and refused). Not modelled: systemd refuses the null + * address and an address family that conflicts with `Remote=`. An invalid value is logged and ignored. + */ +class ConfigParseL2tpTunnelLocalAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_tunnel_local_address", + SequenceCombinator( + // extract_first_word() skips leading separators, so `@auto` reads as `auto`. + ZeroOrOne(RegexTerminal("@+", "@+")), + AlternativeCombinator( + IPV6_ADDR, + IPV4_ADDR, + FlexibleLiteralChoiceTerminal("auto", "static", "dynamic"), + ), + ZeroOrOne(SequenceCombinator(RegexTerminal("@+", "@+"), ZeroOrOne(ALTERNATIVE_INTERFACE_NAME))), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt new file mode 100644 index 00000000..97aa2f17 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseL2tpTunnelRemoteAddressOptionValue.kt @@ -0,0 +1,28 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [L2TP] Remote= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Remote= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/l2tp-tunnel.c#L571-L623 config_parse_l2tp_tunnel_remote_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[L2TP] Remote=` (.netdev). + * + * C function: config_parse_l2tp_tunnel_remote_address: an IPv4 or IPv6 literal (in_addr_from_string_auto + * → inet_pton). Not modelled: systemd also refuses the null address (`0.0.0.0`, `::`) and an address + * whose family differs from an earlier `Local=`; both depend on the parsed value or on other keys. An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseL2tpTunnelRemoteAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_l2tp_tunnel_remote_address", + SequenceCombinator(IP_ADDR, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt new file mode 100644 index 00000000..d96abc78 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacAddressPolicyOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] MACAddressPolicy= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#MACAddressPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2145-L2149 config_parse_mac_address_policy (0), DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2138-L2142 mac_address_policy_table + */ + +/** + * Validator for `[Link] MACAddressPolicy=` (.link). + * + * C function: config_parse_mac_address_policy, generated by DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT over + * mac_address_policy_table: exactly one of `persistent`, `random` or `none` (case-sensitive). An empty + * assignment selects the default, `none`. An invalid value is logged and ignored by udev. + */ +class ConfigParseMacAddressPolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_mac_address_policy", + SequenceCombinator(FlexibleLiteralChoiceTerminal("persistent", "random", "none"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt new file mode 100644 index 00000000..9d999f8c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [MACsecTransmitAssociation] / [MACsecReceiveAssociation] KeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#KeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/macsec.c#L770-L821 config_parse_macsec_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `KeyFile=` in both `[MACsecTransmitAssociation]` and `[MACsecReceiveAssociation]` (.netdev). + * + * C function: config_parse_macsec_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseMacsecKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_macsec_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt new file mode 100644 index 00000000..8b0af0d6 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMacsecKeyIdOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Repeat +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [MACsecTransmitAssociation] / [MACsecReceiveAssociation] KeyId= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#KeyId= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/macsec.c#L823-L879 config_parse_macsec_key_id (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L109-L158 unhexmem_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/if_macsec.h#L23 MACSEC_KEYID_LEN (16) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-macsec.netdev#L12 KeyId=01 … KeyId=0203040506070809 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-hexdecoct.c#L110 TEST(unhexmem) + */ + +// One byte: two hex digits. unhex_next() skips whitespace around every digit, so a space may even +// split a byte. +private val KEY_ID_BYTE = RegexTerminal("[0-9a-fA-F][ \\t]*[0-9a-fA-F]", "[0-9a-fA-F][ \\t]*[0-9a-fA-F]") + +/** + * Validator for `KeyId=` in `[MACsecTransmitAssociation]` and `[MACsecReceiveAssociation]` (.netdev). + * + * C function: config_parse_macsec_key_id, which decodes the value with unhexmem(): an even number of + * hex digits (case-insensitive, whitespace between digits ignored) encoding at most MACSEC_KEYID_LEN = + * 16 bytes, i.e. 2 to 32 digits. Shorter ids are zero-padded. + * + * The man page still says "Takes a number between 0-255", but the parser is hex-only: `KeyId=1` (odd + * length) and `KeyId=255` are refused, while systemd's own fixtures write `KeyId=01` and + * `KeyId=0203040506070809`. This follows the C. An invalid value is logged and ignored by systemd. + */ +class ConfigParseMacsecKeyIdOptionValue : SimpleGrammarOptionValues( + "config_parse_macsec_key_id", + SequenceCombinator( + KEY_ID_BYTE, + Repeat(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), KEY_ID_BYTE), 0, 15), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt new file mode 100644 index 00000000..9c1bf25f --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] Port= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Port= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L84 config_parse_port (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ethtool-util.c#L75-L81 port_table + */ + +/** + * Validator for `[Link] Port=` (.link). + * + * C function: config_parse_port, generated by DEFINE_CONFIG_PARSE_ENUM over port_table: exactly one of + * `tp`, `aui`, `mii`, `fibre` or `bnc` (case-sensitive string-table lookup). An invalid value is + * logged and ignored by udev. + */ +class ConfigParsePortOptionValue : SimpleGrammarOptionValues( + "config_parse_port", + SequenceCombinator(FlexibleLiteralChoiceTerminal("tp", "aui", "mii", "fibre", "bnc"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt new file mode 100644 index 00000000..ec880400 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePortRangeOptionValue.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [VXLAN] PortRange= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PortRange= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vxlan.c#L299-L324 config_parse_port_range (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L736-L760 parse_ip_port_range (allow_zero=false) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + */ + +/** + * Validator for `[VXLAN] PortRange=` (.netdev). + * + * C function: config_parse_port_range → parse_ip_port_range(…, allow_zero = false): a single port or a + * `low-high` range, each bound a safe_atou() number (hex, octal and `0b`/`0o` work) in 1…65535. + * parse_range() cuts the whole value at the first `-`, so the lower bound never sees a minus (`-0` is an empty + * word) and the upper bound is the rest. Whitespace inside the value reaches safe_atou(), which skips it before + * the number, so `1000- 2000`, `0b 1-5` and `1-0b 1` are legal (but `1000 -2000` is not: the lower word keeps + * its trailing space). + * A range with `high < low` is refused by systemd after parsing; that compares numbers and is not + * modelled. An invalid value is logged and ignored by systemd. There is no isempty() branch, so `PortRange=` + * is a parse error in systemd rather than a reset; the plugin never runs a grammar on an empty value, so it + * is not flagged here. + */ +class ConfigParsePortRangeOptionValue : SimpleGrammarOptionValues( + "config_parse_port_range", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = false, + ), + ZeroOrOne( + SequenceCombinator( + HYPHEN, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt new file mode 100644 index 00000000..ad9921de --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRpsCpuMaskOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CPU_SET +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] ReceivePacketSteeringCPUMask= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#ReceivePacketSteeringCPUMask= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2095-L2136 config_parse_rps_cpu_mask (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/cpu-set-util.c#L220-L307 config_parse_cpu_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-rps-cpu-multi.link#L6-L7 ReceivePacketSteeringCPUMask=0 1 / =2,3 + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/24-rps-cpu-invalid.link#L6 ReceivePacketSteeringCPUMask=0 3 8-invalid (deliberately invalid) + */ + +/** + * Validator for `[Link] ReceivePacketSteeringCPUMask=` (.link). + * + * C function: config_parse_rps_cpu_mask. The whole value `disable` (an empty mask) or `all` (every + * online CPU), compared with streq, or else a CPU set exactly as config_parse_cpu_set reads it — see + * [CPU_SET]. An empty assignment clears the mask. An invalid word is logged and skipped by systemd. + */ +class ConfigParseRpsCpuMaskOptionValue : SimpleGrammarOptionValues( + "config_parse_rps_cpu_mask", + AlternativeCombinator( + SequenceCombinator(FlexibleLiteralChoiceTerminal("disable", "all"), EOF()), + SequenceCombinator(CPU_SET, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt new file mode 100644 index 00000000..81994454 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUdevPropertyOptionValue.kt @@ -0,0 +1,115 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ExtractedWordTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WordEnd +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Link] Property= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Property= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L1810-L1878 config_parse_udev_property (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/specifier.c#L33-L105 specifier_printf (link_specifier_table) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L81-L102 env_assignment_is_valid (via udev_property_assignment_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-device/device-util.c#L219-L237 device_property_can_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/85-unmanaged.link#L18 Property=ID_NET_MANAGED_BY=io.systemd.Network + */ + +// The properties udev reserves for itself (device_property_can_set), refused as assignment targets, plus +// the kernel's SYNTH_ARG_ prefix. The trailing lookahead makes each reserved word match only as the whole +// name, so `DEVPATHX` or `TAGS_EXTRA` are still fine. +private const val RESERVED_PROPERTY = + """(?!(?:ACTION|SEQNUM|SYNTH_UUID|DEVPATH|DEVPATH_OLD|SUBSYSTEM|DEVTYPE|DRIVER|MODALIAS|DEVNAME|DEVMODE|""" + + """DEVUID|DEVGID|MAJOR|MINOR|DISKSEQ|PARTN|IFINDEX|INTERFACE|INTERFACE_OLD|DEVLINKS|TAGS|CURRENT_TAGS|""" + + """USEC_INITIALIZED|UDEV_DATABASE_VERSION)(?![A-Za-z0-9_%]))(?!SYNTH_ARG_)""" + +// A link_specifier_table specifier (COMMON_SYSTEM_SPECIFIERS + COMMON_TMP_SPECIFIERS). `%%` is left out on +// purpose: it expands to a literal `%`, which no property name may contain. +private const val SPECIFIER = """%[aAbBHlqmMovwWTV]""" + +// The property name: env_name_is_valid — bash-style, i.e. letters, digits and `_`, not starting with a +// digit. Specifiers are expanded before the check, so one may appear in the name too. +private val PROPERTY_NAME = RegexTerminal( + """[A-Za-z0-9_%]+""", + """$RESERVED_PROPERTY(?:[A-Za-z_]|$SPECIFIER)(?:[A-Za-z0-9_]|$SPECIFIER)*""", +) + +private val EQUALS = LiteralChoiceTerminal("=") + +// The value, in its raw (still-quoted, still-escaped) form. env_value_is_valid only requires valid UTF-8, +// so any character is fine; whitespace ends the word unless it is quoted or backslash-escaped. +private val RAW_VALUE = RegexTerminal( + """(?:[^\s"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""", + """(?:[^\s"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""", +) +private val DOUBLE_QUOTED_INNER = RegexTerminal("""(?:[^"\\]|\\[\s\S])+""", """(?:[^"\\]|\\[\s\S])+""") +private val SINGLE_QUOTED_INNER = RegexTerminal("""(?:[^'\\]|\\[\s\S])+""", """(?:[^'\\]|\\[\s\S])+""") + +private fun quotedAssignment(quote: String, inner: RegexTerminal) = SequenceCombinator( + LiteralChoiceTerminal(quote), PROPERTY_NAME, EQUALS, ZeroOrOne(inner), LiteralChoiceTerminal(quote), ZeroOrOne(RAW_VALUE), +) + +// The name check of PROPERTY_NAME, applied to an already unquoted and unescaped word. +private val UNQUOTED_PROPERTY_NAME = Regex("""$RESERVED_PROPERTY(?:[A-Za-z_]|$SPECIFIER)(?:[A-Za-z0-9_]|$SPECIFIER)*""") + +private fun assignmentNameIsValid(word: String): Boolean { + val eq = word.indexOf('=') + return eq > 0 && UNQUOTED_PROPERTY_NAME.matches(word.substring(0, eq)) +} + +// One word: NAME=VALUE, either plain (the value may still contain quoted runs) or with the whole +// assignment quoted from the start of the name. Anything else extract_first_word could turn into a valid +// assignment — a quote or C escape inside the name (`F"OO"=bar`, `"FOO"=bar`, `FOO\x41=1`) — falls +// through to the ExtractedWordTerminal, which unquotes and unescapes the word first and then applies the +// same name check. WordEnd stops the classic engine committing to a structured branch that only matched +// a prefix of the word. +private val PROPERTY_ASSIGNMENT = AlternativeCombinator( + SequenceCombinator( + AlternativeCombinator( + quotedAssignment("\"", DOUBLE_QUOTED_INNER), + quotedAssignment("'", SINGLE_QUOTED_INNER), + SequenceCombinator(PROPERTY_NAME, EQUALS, ZeroOrOne(RAW_VALUE)), + ), + WordEnd("""\s"""), + ), + ExtractedWordTerminal("""\s""", cunescape = true, "udev property assignment", ::assignmentNameIsValid), +) + +/** + * Validator for `[Link] Property=` (.link). + * + * C function: config_parse_udev_property. A whitespace-separated list of `NAME=VALUE` udev property + * assignments. Each word is split with extract_first_word(…, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE), so it may + * be quoted (`"FOO=bar baz"` or `FOO="bar baz"`) or contain C escapes; specifiers from the link specifier + * table (`%H`, `%v`, …) are expanded; then the assignment must pass env_assignment_is_valid — a + * bash-style name (letters, digits, `_`, not starting with a digit), `=`, and any UTF-8 value, which may be + * empty — and the name must not be one udev reserves (device_property_can_set: `ACTION`, `DEVPATH`, + * `INTERFACE`, `TAGS`, …, or anything starting `SYNTH_ARG_`). + * + * Quotes and C escapes may appear anywhere in the word, including inside the name (`F"OO"=bar`, + * `"FOO"=bar`, `FOO\x41=1` are all `FOO…=…` after extract_first_word); such words are unquoted and + * unescaped before the name is checked. + * + * Lenient corners: in the common `NAME=VALUE` spelling the escapes inside the value are not checked + * against cunescape_one's list (an unknown escape such as `\q` makes systemd ignore the whole + * assignment), the value's UTF-8 validity is not checked, and an unknown `%` specifier in the value is + * not flagged. An invalid word is logged and + * skipped by udev; an empty assignment clears the list. + */ +class ConfigParseUdevPropertyOptionValue : SimpleGrammarOptionValues( + "config_parse_udev_property", + SequenceCombinator( + PROPERTY_ASSIGNMENT, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), PROPERTY_ASSIGNMENT)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt new file mode 100644 index 00000000..66fccd7e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanQosMapsOptionValue.kt @@ -0,0 +1,103 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ExtractedWordTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WordEnd +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkParseRangeAccepts +import java.math.BigInteger + +/* + * [VLAN] EgressQOSMaps= / IngressQOSMaps= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#EgressQOSMaps= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vlan.c#L120-L180 config_parse_vlan_qos_maps (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340 parse_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +// parse_range() bounds as safe_atou() reads them (any unsigned int). The word was split on whitespace, so no +// whitespace reaches safe_atou(). The lower bound is cut at the first `-`, so it never sees a minus; the upper +// bound is the rest of the word, which may (`1--0`). +private val QOS_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = false, + acceptsMinus = false, +) +private val QOS_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = false, + acceptsMinus = true, +) +private val QOS_RANGE = SequenceCombinator(QOS_LOWER, ZeroOrOne(SequenceCombinator(HYPHEN, QOS_UPPER))) + +// The same bounds for a word whose quotes and escapes have been removed: quoted or escaped whitespace does reach +// safe_atou(). +private val QOS_UNQUOTED_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = false, +) +private val QOS_UNQUOTED_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// One word: the structured `from[-to]` grammar, bare or quoted as a whole, for highlighting; then an +// [ExtractedWordTerminal] fallback for quotes and C escapes anywhere in the word. +private val QOS_MAPPING = AlternativeCombinator( + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), QOS_RANGE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), QOS_RANGE, LiteralChoiceTerminal("'")), + QOS_RANGE, + ), + WordEnd("""\s"""), + ), + ExtractedWordTerminal("""\s""", cunescape = true, "range 0..4294967295") { word -> + netdevLinkParseRangeAccepts(word, QOS_UNQUOTED_LOWER, QOS_UNQUOTED_UPPER) + }, +) + +/** + * Validator for `[VLAN] EgressQOSMaps=` and `IngressQOSMaps=` (.netdev). + * + * C function: config_parse_vlan_qos_maps. A whitespace-separated list of words (each may be quoted, and + * may contain C escapes — the quotes and escapes are removed before parsing, see [ExtractedWordTerminal]); + * every word goes to parse_range(), so it is `from-to` or a single number meaning `from == to`, each + * side an unsigned int as safe_atou() reads it ([SafeAtoTerminal]: decimal, `0x` hex, octal, `0b`/`0o`). + * + * The man page says each integer is in 1…4294967294 and that `from` must be >= `to`; the parser + * enforces neither (it stores whatever parse_range returns), so this follows the C and accepts the full + * unsigned range and any order. A malformed word is logged and skipped by systemd. + */ +class ConfigParseVlanQosMapsOptionValue : SimpleGrammarOptionValues( + "config_parse_vlan_qos_maps", + SequenceCombinator(QOS_MAPPING, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), QOS_MAPPING)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt new file mode 100644 index 00000000..1eafd4bc --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVlanprotocolOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [VLAN] Protocol= and [Bridge] VLANProtocol= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Protocol= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1832-L1862 config_parse_vlanprotocol (0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-bridge.netdev#L17 + */ + +/** + * Validator for `[VLAN] Protocol=` and `[Bridge] VLANProtocol=` (.netdev). + * + * C function: config_parse_vlanprotocol, which compares the value with STR_IN_SET against exactly four + * spellings: `802.1q` / `802.1Q` (ETH_P_8021Q) and `802.1ad` / `802.1AD` (ETH_P_8021AD). Mixed case + * such as `802.1Ad` is refused. An empty assignment resets to the kernel default. + */ +class ConfigParseVlanprotocolOptionValue : SimpleGrammarOptionValues( + "config_parse_vlanprotocol", + SequenceCombinator(FlexibleLiteralChoiceTerminal("802.1q", "802.1Q", "802.1ad", "802.1AD"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt new file mode 100644 index 00000000..fbb9ea6a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVrfTableOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING + +/* + * [VRF] Table= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Table= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vrf.c#L10-L39 config_parse_vrf_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + */ + +/** + * Validator for `[VRF] Table=` (.netdev). + * + * C function: config_parse_vrf_table → manager_get_route_table_from_string: `default`, `main`, + * `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 (safe_atou32, so hex, + * octal and `0b`/`0o` work; `0` is refused with ERANGE). See [ROUTE_TABLE_FROM_STRING]. + * + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name. What is flagged: `0`, out-of-range or + * malformed all-digit numbers (`00`, `08`), and anything containing `:` (`name:number` is networkd.conf + * syntax, not a table reference). An invalid value is logged and ignored by systemd. + * + * There is no isempty() branch, so an empty `Table=` is a parse error in systemd (logged and ignored), not + * a reset. The plugin never runs a grammar on an empty value, so it is not flagged here. + */ +class ConfigParseVrfTableOptionValue : SimpleGrammarOptionValues( + "config_parse_vrf_table", + ROUTE_TABLE_FROM_STRING +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt new file mode 100644 index 00000000..f70ff004 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseVxlanAddressOptionValue.kt @@ -0,0 +1,74 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import com.intellij.codeInspection.ProblemsHolder +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetdevLinkCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.netdevLinkIsMulticast + +/* + * [VXLAN] Group= / Local= / Remote= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Local= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/vxlan.c#L220-L297 config_parse_vxlan_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/netdev-util.c#L9-L16 netdev_local_address_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L103-L113 in_addr_is_multicast + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +private const val VXLAN_VALIDATOR = "config_parse_vxlan_address" + +private val VXLAN_LOCAL_TYPE = FlexibleLiteralChoiceTerminal("ipv4_link_local", "ipv6_link_local", "dhcp4", "dhcp6", "slaac", "dhcp_pd") + +/** An address literal that is (or, with [multicast] false, is not) a multicast address. */ +private fun vxlanAddress(multicast: Boolean): Combinator = AlternativeCombinator( + NetdevLinkCheckedSpan(IPV6_ADDR, if (multicast) "multicast" else "not multicast") { netdevLinkIsMulticast(it) == multicast }, + NetdevLinkCheckedSpan(IPV4_ADDR, if (multicast) "multicast" else "not multicast") { netdevLinkIsMulticast(it) == multicast }, +) + +/** `Group=`: a multicast address. */ +private val VXLAN_GROUP = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(vxlanAddress(multicast = true), EOF())) + +/** `Remote=`: a unicast address. */ +private val VXLAN_REMOTE = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(vxlanAddress(multicast = false), EOF())) + +/** `Local=`: a local-address type name, or a unicast address. Addresses first, so the name terminal cannot take a prefix. */ +private val VXLAN_LOCAL = GrammarOptionValue(VXLAN_VALIDATOR, SequenceCombinator(AlternativeCombinator(vxlanAddress(multicast = false), VXLAN_LOCAL_TYPE), EOF())) + +/** + * Validator for `[VXLAN] Group=`, `Local=` and `Remote=` (.netdev). + * + * C function: config_parse_vxlan_address, which branches on the key name. The value is an IPv4 or IPv6 + * literal (in_addr_from_string_auto → inet_pton). For `Local=` only, one of the + * netdev_local_address_type_table names (`ipv4_link_local`, `ipv6_link_local`, `dhcp4`, `dhcp6`, `slaac`, + * `dhcp_pd`) is accepted first, telling networkd to pick up the address that mechanism assigned. `Group=` + * must be a multicast address; `Local=` and `Remote=` must not be. + * + * Validators are looked up by (parser, ltype), not by key, so the key-specific rules are applied by + * choosing a per-key grammar in [generateProblemDescriptors]. [combinator] (used for colouring and + * completion) stays the union of the three. All of this was confirmed against systemd's parser built at + * 0f0682b773: `Group=dhcp4`, `Remote=slaac`, `Group=192.168.1.1`, `Remote=239.1.1.1` and + * `Local=239.1.1.1` are each logged and ignored. + */ +class ConfigParseVxlanAddressOptionValue : SimpleGrammarOptionValues( + VXLAN_VALIDATOR, + SequenceCombinator(AlternativeCombinator(IPV6_ADDR, IPV4_ADDR, VXLAN_LOCAL_TYPE), EOF()) +) { + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + when (property.key) { + "Group" -> VXLAN_GROUP.generateProblemDescriptors(property, holder) + "Remote" -> VXLAN_REMOTE.generateProblemDescriptors(property, holder) + "Local" -> VXLAN_LOCAL.generateProblemDescriptors(property, holder) + else -> super.generateProblemDescriptors(property, holder) + } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt new file mode 100644 index 00000000..eb33a27d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardAllowedIpsOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_SAFE_PREFIX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [WireGuardPeer] AllowedIPs= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#AllowedIPs= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L673-L749 config_parse_wireguard_allowed_ips (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word ("," WHITESPACE, no flags) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L845-L897 in_addr_prefix_from_string_auto_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L779-L797 in_addr_parse_prefixlen (safe_atou8) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L15 AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24 + */ + +// One extract_first_word() separator run; "," and whitespace both separate and runs coalesce. +private val ALLOWED_IPS_SEPARATOR = RegexTerminal("[\\s,]+", "[\\s,]+") + +/** + * Validator for `[WireGuardPeer] AllowedIPs=` (.netdev). + * + * C function: config_parse_wireguard_allowed_ips. A list of IPv4/IPv6 addresses with optional prefix + * lengths, separated by commas and/or whitespace. Each entry goes to in_addr_prefix_from_string_auto: a + * missing length means the full width, `/0` is allowed, and the length is a safe_atou8 number, so `/0x18` + * works (see [IP_ADDR_AND_SAFE_PREFIX]). Separators coalesce, and leading or trailing ones are harmless. + * An address that is not properly masked (`192.168.26.3/24`) is accepted and masked with a warning. + * An invalid entry is logged and skipped by systemd; an empty assignment clears the list. + */ +class ConfigParseWireguardAllowedIpsOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_allowed_ips", + SequenceCombinator( + ZeroOrOne(ALLOWED_IPS_SEPARATOR), + IP_ADDR_AND_SAFE_PREFIX, + ZeroOrMore(SequenceCombinator(ALLOWED_IPS_SEPARATOR, IP_ADDR_AND_SAFE_PREFIX)), + ZeroOrOne(ALLOWED_IPS_SEPARATOR), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt new file mode 100644 index 00000000..2526f660 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardEndpointOptionValue.kt @@ -0,0 +1,81 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CREDENTIAL_REFERENCE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [WireGuardPeer] Endpoint= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#Endpoint= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L751-L862 config_parse_wireguard_endpoint (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/socket-netlink.c#L220-L354 in_addr_port_ifindex_name_from_string_auto (no ifindex, no server name) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L718-L734 parse_ip_port + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape (via dns_name_is_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L156-L180 read_credential (credential_name_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-socket-netlink.c#L356 TEST(in_addr_port_ifindex_name_from_string_auto) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L17 Endpoint=@network.wireguard.peer0.endpoint + */ + +// parse_ip_port: safe_atou16_full(…, SAFE_ATO_REFUSE_LEADING_WHITESPACE) (base 0), then 0 is refused. The port is +// everything after the last `:`, so whitespace can reach the parser: `\v80` and `0b 1` are accepted, ` 80` is not. +private val ENDPOINT_PORT = SafeAtoTerminal( + signed = false, + base = 0, + flags = setOf(SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE), + minInclusive = 1L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// The hostname fallback: everything up to the LAST colon (strrchr), checked with dns_name_is_valid(): +// dot-separated labels of at most 63 characters, each character printable (>= ' ', not DEL) or one of +// the escapes `\.`, `\\` or `\DDD` with DDD at most 255 (dns_label_unescape refuses `k > 255`). A single trailing dot is allowed, and dns_name_is_valid() also +// accepts the root name — `.` or nothing at all — so `:51820` passes the parser too. +private const val DNS_LABEL = """(?:[^.\\\x00-\x1F\x7F]|\\[.\\]|\\(?:[01][0-9]{2}|2[0-4][0-9]|25[0-5])){1,63}""" +private val ENDPOINT_HOST = RegexTerminal( + """[^\x00-\x1F\x7F]+(?=:[^:]*$)""", + """(?:$DNS_LABEL(?:\.$DNS_LABEL)*\.?|\.)(?=:[^:]*$)""", +) + +/** + * Validator for `[WireGuardPeer] Endpoint=` (.netdev). + * + * C function: config_parse_wireguard_endpoint. In the order systemd tries them: + * + * - `@name` reads the endpoint from a credential; + * - in_addr_port_ifindex_name_from_string_auto with no interface and no server name: `[IPv6]:port`, + * `IPv4:port`, or — because that helper makes the port optional — a bare IPv4 or IPv6 address (the + * man page says a port is required; the parser stores port 0); + * - otherwise `host:port`, split at the last colon, where the host only has to pass + * dns_name_is_valid(), which is permissive (any printable characters, labels up to 63 bytes). + * + * The port is a parse_ip_port() number, 1…65535 (safe_atou16 base 0, so hex, octal and `0b`/`0o` work, and + * a leading space is refused while a leading `\v` is not). Because the host + * fallback is so permissive, something like `[fe80::1%eth0]:51820` is accepted (as a hostname), as it + * is by systemd at parse time. What is flagged: a missing or invalid port, empty labels, over-long labels + * and control characters. Lenient: the overall name length (DNS_HOSTNAME_MAX, enforced by + * dns_name_concat) and the 63-byte label limit, which is counted here in characters, not UTF-8 bytes. An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardEndpointOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_endpoint", + AlternativeCombinator( + SequenceCombinator(CREDENTIAL_REFERENCE, EOF()), + SequenceCombinator(LiteralChoiceTerminal("["), IPV6_ADDR, LiteralChoiceTerminal("]"), COLON, ENDPOINT_PORT, EOF()), + SequenceCombinator(IPV4_ADDR, COLON, ENDPOINT_PORT, EOF()), + SequenceCombinator(IPV6_ADDR, EOF()), + SequenceCombinator(IPV4_ADDR, EOF()), + SequenceCombinator(ZeroOrOne(ENDPOINT_HOST), COLON, ENDPOINT_PORT, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt new file mode 100644 index 00000000..daf299e9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuardPeer] PublicKeyFile= / PresharedKeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PublicKeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L623-L671 config_parse_wireguard_peer_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `[WireGuardPeer] PublicKeyFile=` and `PresharedKeyFile=` (.netdev). + * + * C function: config_parse_wireguard_peer_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseWireguardPeerKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_peer_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt new file mode 100644 index 00000000..14ab1c47 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPeerRouteTableOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_FALSE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuardPeer] RouteTable= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#RouteTable= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L941-L990 config_parse_wireguard_peer_route_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L10 + */ + +/** + * Validator for `[WireGuardPeer] RouteTable=` (.netdev). + * + * C function: config_parse_wireguard_peer_route_table. A value that parse_boolean() reads as false (`no`, `off`, `0`, …) + * disables route configuration; anything else is a route table for manager_get_route_table_from_string: + * `default`, `main`, `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 + * (safe_atou32, so hex, octal and `0b`/`0o` work). See [ROUTE_TABLE_FROM_STRING]. An empty assignment falls back to the `[WireGuard]` section's table. + * + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name — including `yes`, which is only valid if + * networkd.conf defines it. What is flagged: out-of-range or malformed all-digit numbers (`4294967296`, + * `08`) and anything containing `:` (`name:number` is networkd.conf syntax, not a table reference). An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardPeerRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_peer_route_table", + AlternativeCombinator( + SequenceCombinator(BOOLEAN_FALSE, EOF()), + ROUTE_TABLE_FROM_STRING, + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt new file mode 100644 index 00000000..4e818fc7 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyFileOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NON_API_VFS_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuard] PrivateKeyFile= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PrivateKeyFile= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L563-L591 config_parse_wireguard_private_key_file (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34-L88 path_simplify_and_warn (PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/mountpoint-util.c#L721-L726 path_below_api_vfs + */ + +/** + * Validator for `[WireGuard] PrivateKeyFile=` (.netdev). + * + * C function: config_parse_wireguard_private_key_file. The value is strdup'd verbatim — no specifier expansion and no unquoting, so + * `%` and quotes are ordinary characters and a path may contain spaces — and checked with + * path_simplify_and_warn(PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS): it must be absolute, must not keep + * a `..` component after simplification, and must not be at or below `/dev`, `/sys` or `/proc`. + * See [NON_API_VFS_ABSOLUTE_PATH]. An empty assignment clears the setting. An invalid value is logged + * and ignored by systemd. + */ +class ConfigParseWireguardPrivateKeyFileOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_private_key_file", + SequenceCombinator(NON_API_VFS_ABSOLUTE_PATH, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt new file mode 100644 index 00000000..db993546 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardPrivateKeyOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CREDENTIAL_REFERENCE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WIREGUARD_BASE64_KEY + +/* + * [WireGuard] PrivateKey= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#PrivateKey= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L546-L561 config_parse_wireguard_private_key (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L488-L544 wireguard_decode_key_and_warn + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L755-L851 unbase64mem_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hexdecoct.c#L528-L555 unbase64char (also accepts the URL-safe '-' and '_') + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/creds-util.c#L156-L180 read_credential (credential_name_valid) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard-23-peers.netdev#L12 PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev.d/private-key.conf#L2 (same key, drop-in) + */ + +/** + * Validator for `[WireGuard] PrivateKey=` (.netdev). + * + * C function: config_parse_wireguard_private_key → wireguard_decode_key_and_warn. Either `@name`, + * which reads the key from the named credential (the name must pass credential_name_valid), or the key + * itself in base64, which must decode to exactly 32 bytes: 43 characters plus one mandatory `=` of + * padding, the 43rd character restricted to those whose low two bits are clear. See + * [WIREGUARD_BASE64_KEY]. An invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardPrivateKeyOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_private_key", + SequenceCombinator(AlternativeCombinator(CREDENTIAL_REFERENCE, WIREGUARD_BASE64_KEY), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt new file mode 100644 index 00000000..0c1e49e5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWireguardRouteTableOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_FALSE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [WireGuard] RouteTable= in a .netdev file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.netdev.html#RouteTable= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/netdev/wireguard.c#L905-L939 config_parse_wireguard_route_table (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L427-L431 route_table_table (default, main, local) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-wireguard.netdev#L10 + */ + +/** + * Validator for `[WireGuard] RouteTable=` (.netdev). + * + * C function: config_parse_wireguard_route_table. A value that parse_boolean() reads as false (`no`, `off`, `0`, …) + * disables route configuration; anything else is a route table for manager_get_route_table_from_string: + * `default`, `main`, `local`, a name defined with `RouteTable=` in networkd.conf, or a non-zero uint32 + * (safe_atou32, so hex, octal and `0b`/`0o` work). See [ROUTE_TABLE_FROM_STRING]. An empty assignment also disables it. + * + * Because table names are user-defined (networkd.conf can even define `my\ table:100`), any value without + * `:` that is not all digits is accepted as a potential name — including `yes`, which is only valid if + * networkd.conf defines it. What is flagged: out-of-range or malformed all-digit numbers (`4294967296`, + * `08`) and anything containing `:` (`name:number` is networkd.conf syntax, not a table reference). An + * invalid value is logged and ignored by systemd. + */ +class ConfigParseWireguardRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_wireguard_route_table", + AlternativeCombinator( + SequenceCombinator(BOOLEAN_FALSE, EOF()), + ROUTE_TABLE_FROM_STRING, + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt new file mode 100644 index 00000000..761800da --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseWolPasswordOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.MAC_ADDRESS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SAFE_ABSOLUTE_PATH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] WakeOnLanPassword= in a .link file. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#WakeOnLanPassword= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L2050-L2093 config_parse_wol_password (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/udev/net/link-config.c#L134-L155 link_parse_wol_password → parse_ether_addr + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/ether-addr-util.c#L282-L295 parse_ether_addr (parse_hw_addr_full, ETH_ALEN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.h#L152-L154 path_is_safe + */ + +/** + * Validator for `[Link] WakeOnLanPassword=` (.link). + * + * C function: config_parse_wol_password. If the value is an absolute path that path_is_safe() accepts + * (no `..` component) it names a file to read the password from; otherwise it must be the 6-byte + * SecureOn password written like an Ethernet address (parse_ether_addr: `00:11:22:33:44:55`, + * `00-11-22-33-44-55` or `0011.2233.4455`). The path is taken verbatim (no specifiers, spaces are + * ordinary characters). An invalid value is logged and ignored by systemd. + */ +class ConfigParseWolPasswordOptionValue : SimpleGrammarOptionValues( + "config_parse_wol_password", + AlternativeCombinator( + SequenceCombinator(MAC_ADDRESS, EOF()), + SequenceCombinator(SAFE_ABSOLUTE_PATH, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt new file mode 100644 index 00000000..10ad2f15 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinators.kt @@ -0,0 +1,517 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigInteger + +/* + * Shared grammars for the systemd-networkd .netdev (src/network/netdev/) and udev .link (src/udev/net/) + * validators added in the 2026-10 netdev-link batch. + * + * Everything here is pinned to systemd 0f0682b773, the commit recorded in + * systemd-build/build/last_commit_hash. Browse it at https://github.com/systemd/systemd/blob/0f0682b773. + * + * These live in their own file (rather than Combinators.kt) only because several batches were written + * in parallel. Several are general-purpose and are good candidates for promotion — see the batch's + * RECOMMENDATIONS.md. + */ + +// --------------------------------------------------------------------------------------------------- +// parse_range() — src/basic/parse-util.c. Its bounds are safe_atou() numbers, modelled by [SafeAtoTerminal]. + +/** + * Whether parse_range() (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L305-L340) + * accepts the already-extracted [word]: the text before the first `-` must satisfy [lower] and, if there is a `-`, + * the non-empty rest must satisfy [upper]. A predicate for an [ExtractedWordTerminal], whose words have had their + * quotes and escapes removed first; the structured grammar spells the same check out with the terminals. + */ +fun netdevLinkParseRangeAccepts(word: String, lower: SafeAtoTerminal, upper: SafeAtoTerminal): Boolean { + val dash = word.indexOf('-') + if (dash < 0) return lower.valueOf(word) != null + val upperText = word.substring(dash + 1) + return lower.valueOf(word.substring(0, dash)) != null && upperText.isNotEmpty() && upper.valueOf(upperText) != null +} + + +// --------------------------------------------------------------------------------------------------- +// Words as extract_first_word() (src/basic/extract-word.c) returns them. + +/** + * A zero-width check that the input ends here or continues with one of [separators] (a regex character + * class body such as `\s,`). Put after a structured word so that, under the classic first-match engine, + * an [AlternativeCombinator] does not commit to a branch that matched only a prefix of the word. + */ +class WordEnd(separators: String) : Combinator { + private val separator = Regex("[$separators]") + + private fun at(value: String, offset: Int): Boolean = + offset == value.length || separator.matchesAt(value, offset) + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = + if (at(value, offset)) MatchResult(emptyList(), offset, emptyList(), offset) else NoMatch + + override fun SemanticMatch(value: String, offset: Int): MatchResult = SyntacticMatch(value, offset) + + override fun parse(value: String, offset: Int): Sequence = + if (at(value, offset)) sequenceOf(Parse(offset, emptyList())) else sequenceOf(Stuck(offset, setOf(this))) + + override fun toStringIndented(indent: Int): String = "WordEnd" +} + +/** + * One raw word of an extract_first_word(…, separators, EXTRACT_UNQUOTE[|EXTRACT_CUNESCAPE]) list, checked + * the way systemd checks it: AFTER the quotes (which may open and close anywhere in the word, as in + * `0-"3"`) and backslash escapes are removed. + * + * It matches a run of non-separator characters, backslash escapes and balanced quoted runs (quoted runs + * may contain separators) — but ONLY when that word contains a quote or backslash, [extractWord] succeeds + * on it and [check] accepts the result. Use it as the last alternative after a structured grammar for the + * common spelling: the common spelling keeps its highlighting and error localization, and this only + * rescues the exotic quoting and escaping systemd also accepts. + * + * It deliberately has no "syntactically right but semantically wrong" state. Under the classic engine + * GrammarOptionValue locates a semantic error by lining the semantic match's longestMatch up against the + * syntactic tokens; a whole-word token that only fails semantically would be misaligned with the + * structured branch's partial progress and produce an out-of-range highlight. So an invalid word simply + * does not match, and the value is reported as not matching the expected format. + * + * @param separators a regex character-class body for the separator set, e.g. `\s` or `\s,` + * @param cunescape whether the caller passes EXTRACT_CUNESCAPE + */ +class ExtractedWordTerminal( + separators: String, + private val cunescape: Boolean, + private val description: String, + private val check: (String) -> Boolean, +) : TerminalCombinator { + + private val shape = Regex("""(?:[^$separators"'\\]|\\[\s\S]|"(?:[^"\\]|\\[\s\S])*"|'(?:[^'\\]|\\[\s\S])*')+""") + + /** The word at [offset] if it is one this terminal accepts, else null. */ + private fun word(value: String, offset: Int): String? { + val text = shape.matchAt(value, offset)?.value ?: return null + if (text.none { it == '"' || it == '\'' || it == '\\' }) return null + return text.takeIf { extractWord(it, cunescape)?.let(check) == true } + } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = word(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult = SyntacticMatch(value, offset) + + override fun parse(value: String, offset: Int): Sequence { + val text = word(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, true)))) + } + + override fun toString(): String = "ExtractedWord($description)" +} + +/** + * The text extract_first_word() returns for the single raw word [raw] under EXTRACT_UNQUOTE (plus + * EXTRACT_CUNESCAPE when [cunescape]), or null where it fails: an unbalanced quote, a trailing + * backslash, or — with [cunescape] — an escape cunescape_one() (src/basic/escape.c) refuses. Without + * [cunescape], a backslash simply makes the next character literal. + */ +fun extractWord(raw: String, cunescape: Boolean): String? { + val out = StringBuilder() + var quote: Char? = null + var i = 0 + while (i < raw.length) { + val c = raw[i] + if (c == '\\') { + if (i + 1 >= raw.length) return null + if (cunescape) { + val (text, length) = cunescapeOne(raw, i + 1) ?: return null + out.append(text) + i += 1 + length + } else { + out.append(raw[i + 1]) + i += 2 + } + continue + } + when { + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> out.append(c) + } + i++ + } + return if (quote == null) out.toString() else null +} + +/** cunescape_one(…, accept_nul = false) on the escape starting at [p]: the decoded text and its length. */ +private fun cunescapeOne(s: String, p: Int): Pair? { + fun digits(count: Int, radix: Int): Int? = + if (p + 1 + count > s.length) null else s.substring(p + 1, p + 1 + count).let { d -> if (d.all { Character.digit(it, radix) >= 0 }) d.toInt(radix) else null } + + return when (val c = s[p]) { + 'a' -> "\u0007" to 1 + 'b' -> "\b" to 1 + 'f' -> "\u000C" to 1 + 'n' -> "\n" to 1 + 'r' -> "\r" to 1 + 't' -> "\t" to 1 + 'v' -> "\u000B" to 1 + '\\', '"', '\'' -> c.toString() to 1 + 's' -> " " to 1 + 'x' -> digits(2, 16)?.takeIf { it != 0 }?.let { it.toChar().toString() to 3 } + 'u' -> digits(4, 16)?.takeIf { it != 0 && it !in 0xD800..0xDFFF }?.let { it.toChar().toString() to 5 } + 'U' -> { + if (p + 9 > s.length) return null + val v = s.substring(p + 1, p + 9).toLongOrNull(16) ?: return null + if (v == 0L || v > 0x10FFFF || v in 0xD800..0xDFFF) return null + String(Character.toChars(v.toInt())) to 9 + } + in '0'..'7' -> { + if (p + 3 > s.length) return null + val d = s.substring(p, p + 3) + if (!d.all { it in '0'..'7' }) return null + val v = d.toInt(8) + if (v == 0 || v > 255) null else v.toChar().toString() to 3 + } + else -> null + } +} + +// --------------------------------------------------------------------------------------------------- +// CPU sets — config_parse_cpu_set (src/shared/cpu-set-util.c). + +// One extract_first_word() separator run: any mix of whitespace and commas (separators coalesce). +private val CPU_SET_SEPARATOR = RegexTerminal("""[\s,]+""", """[\s,]+""") + +// parse_range() bounds as safe_atou() reads them, below CPU_SET_MAX_NCPU (8192, cpu_set_add_range). The word was +// split on whitespace and commas, so no whitespace can reach safe_atou(). The lower bound is cut at the first `-` +// by parse_range(), so it never sees a minus; the upper bound is the rest of the word, which may (`0--0`). +private val CPU_SET_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = false, +) +private val CPU_SET_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = false, + acceptsMinus = true, +) +private val CPU_SET_RANGE = SequenceCombinator(CPU_SET_LOWER, ZeroOrOne(SequenceCombinator(HYPHEN, CPU_SET_UPPER))) + +// The same bounds for a word whose quotes have been removed: quoted whitespace does reach safe_atou() (`" 3"`). +private val CPU_SET_UNQUOTED_LOWER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = true, + acceptsMinus = false, +) +private val CPU_SET_UNQUOTED_UPPER = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 8191L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// One word: the structured `N[-M]` grammar, bare or quoted as a whole, for highlighting; then an +// [ExtractedWordTerminal] fallback for quotes and escapes anywhere in the word (`0-"3"`, `'0'-'3'`). +private val CPU_SET_ITEM = AlternativeCombinator( + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(LiteralChoiceTerminal("\""), CPU_SET_RANGE, LiteralChoiceTerminal("\"")), + SequenceCombinator(LiteralChoiceTerminal("'"), CPU_SET_RANGE, LiteralChoiceTerminal("'")), + CPU_SET_RANGE, + ), + WordEnd("""\s,"""), + ), + ExtractedWordTerminal("""\s,""", cunescape = false, "range 0..8191") { word -> + netdevLinkParseRangeAccepts(word, CPU_SET_UNQUOTED_LOWER, CPU_SET_UNQUOTED_UPPER) + }, +) + +/** + * A CPU set: CPU indices and `N-M` ranges separated by whitespace and/or commas. + * + * config_parse_cpu_set splits with extract_first_word(…, WHITESPACE ",", EXTRACT_UNQUOTE), so separators + * coalesce, leading and trailing separators are harmless (`0, 1, 2, 3, 4, 5, 6, 7, 63, ` is in + * test-cpu-set-util.c), and a word may be quoted — anywhere in the word, not only around it (`0-"3"` is + * `0-3`; the [ExtractedWordTerminal] fallback handles that). Each word goes to parse_range(), whose bounds are + * safe_atou() numbers ([SafeAtoTerminal]: hex, octal, `0b`/`0o`) — and cpu_set_add_range() refuses anything at or above + * CPU_SET_MAX_NCPU (8192). A range whose lower end exceeds its upper end (`3-0`) is rejected by systemd + * after parsing; that compares numbers and is not modelled. + */ +val CPU_SET: Combinator = SequenceCombinator( + ZeroOrOne(CPU_SET_SEPARATOR), + CPU_SET_ITEM, + ZeroOrMore(SequenceCombinator(CPU_SET_SEPARATOR, CPU_SET_ITEM)), + ZeroOrOne(CPU_SET_SEPARATOR), +) + + +// --------------------------------------------------------------------------------------------------- +// Sizes — parse_size() (src/basic/parse-util.c). + +// strtoull(…, 10) after parse_size has skipped whitespace: an optional `+` (a `-` is refused), digits, +// then an optional `.` and fraction. parse_size steps over the `.` unconditionally and ignores a +// missing fraction ("E.g. 10.M is valid"), so `10.` and `10.M` are both legal. +// Labeled so the number colours as a literal, like ByteSizeTerminal does. +private val SIZE_NUMBER = Labeled(Role.LITERAL, RegexTerminal("""\+?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""")) + +/** + * A parse_size() value with base 1000 (the SI table: `E`, `P`, `T`, `G`, `M`, `K`, `B` and bare). + * + * parse_size accepts a run of `number[suffix]` elements whose suffixes strictly descend through the + * table (`start_pos = i + 1`), with optional whitespace before each number and between a number and its + * suffix. The bare (suffix-less) element is the last table entry, so it can only come last. That + * ordering is spelled out structurally: [sizeChain] for table index `i` offers every suffix from `i` + * onwards, each optionally followed by a chain starting after it. + * + * Suffixes are case-sensitive: `1k` is not a size. + */ +fun siSize(): Combinator = sizeChain(listOf("E", "P", "T", "G", "M", "K", "B")) + +private fun sizeChain(suffixes: List): Combinator { + val ws = ZeroOrOne(WhitespaceTerminal()) + // Built from the last suffix backwards so each chain reuses the shorter one after it. + var chainAfter: Combinator? = null + val chains = ArrayList() + // The bare element (no suffix) is always the final alternative of every chain. + val bare: Combinator = SequenceCombinator(ws, SIZE_NUMBER) + for (i in suffixes.indices.reversed()) { + val element = SequenceCombinator(ws, SIZE_NUMBER, ws, LiteralChoiceTerminal(suffixes[i])) + val withRest = if (chainAfter == null) SequenceCombinator(element, ZeroOrOne(bare)) + else SequenceCombinator(element, ZeroOrOne(chainAfter)) + chains.add(0, withRest) + chainAfter = AlternativeCombinator(*chains.toTypedArray(), bare) + } + return chainAfter ?: bare +} + + +// --------------------------------------------------------------------------------------------------- +// Durations — parse_nsec() (src/basic/time-util.c). + +// strtoll(…, 10) as parse_nsec drives it: an optional `+` (a leading `-` is refused), then digits with +// an optional fraction — or a bare fraction like `.5`, which parse_nsec accepts because it checks for +// `.` before it checks whether strtoll consumed anything. A trailing `.` with no digits (`3.`) is refused. +private val DURATION_NUMBER = Labeled( + Role.LITERAL, + RegexTerminal("""\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""", """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+"""), +) + +/** extract_nsec_multiplier's table (minus its empty default entry, which is the bare-number case). */ +private val NSEC_SUFFIX = LiteralChoiceTerminal( + "seconds", "second", "sec", "s", "minutes", "minute", "min", "months", "month", "M", "msec", "ms", "m", + "hours", "hour", "hr", "h", "days", "day", "d", "weeks", "week", "w", "years", "year", "y", + "usec", "us", "μs", "µs", "nsec", "ns", +) + +/** + * A parse_nsec() duration WITHOUT the `infinity` spelling: one or more `number[unit]` elements, an + * element without a unit meaning nanoseconds. Whitespace may separate elements and may sit between a + * number and its unit; an element with a unit may be followed directly by the next one (`1s500ms`). + * + * Lenient on one corner: parse_nsec refuses a bare number followed immediately by a `.` fraction + * (`12.34.56`), which this grammar would read as `12.34` + `.56`. + */ +val NSEC_DURATION_FINITE: Combinator = run { + val ws = ZeroOrOne(WhitespaceTerminal()) + val element = AlternativeCombinator( + SequenceCombinator(DURATION_NUMBER, ws, NSEC_SUFFIX), + DURATION_NUMBER, + ) + SequenceCombinator(element, ZeroOrMore(SequenceCombinator(ws, element))) +} + + +// --------------------------------------------------------------------------------------------------- +// Paths. + +// Rejects a `..` component anywhere (see NO_DOT_DOT in Combinators.kt). +private const val NO_DOT_DOT_COMPONENT = """(?!(?:[\s\S]*/)?\.\.(?:/|$))""" + +/** + * An absolute path checked by path_simplify_and_warn(…, PATH_CHECK_ABSOLUTE|PATH_CHECK_NON_API_VFS) + * (src/shared/parse-helpers.c), taken verbatim — no specifier expansion, no unquoting — as the netdev + * `*KeyFile=` settings do. After path_simplify() the path must be normalized (so no `..` component) and + * must not be at or below one of the API VFS mounts `/dev`, `/sys`, `/proc` (path_below_api_vfs, which + * compares whole components, so `/devices` is fine). Because path_simplify() runs first, `.` components + * and repeated slashes in front of those names do not hide them (`/./dev/shm/key`, `//sys`). Spaces + * are ordinary path characters. Lenient: path_is_valid()'s NAME_MAX/PATH_MAX length limits. + */ +val NON_API_VFS_ABSOLUTE_PATH = RegexTerminal( + """[\s\S]+""", + """(?!/(?:\.?/)*(?:dev|sys|proc)(?:/|$))$NO_DOT_DOT_COMPONENT/[\s\S]*""", +) + +/** + * An absolute path accepted by path_is_absolute() && path_is_safe() (src/basic/path-util.h): it starts + * with `/` and has no `..` component. Taken verbatim. + */ +val SAFE_ABSOLUTE_PATH = RegexTerminal( + """/[\s\S]*""", + """$NO_DOT_DOT_COMPONENT/[\s\S]*""", +) + + +// --------------------------------------------------------------------------------------------------- +// WireGuard keys and credentials — wireguard_decode_key_and_warn (src/network/netdev/wireguard.c) → +// unbase64mem_full (src/basic/hexdecoct.c). + +/** + * A 32-byte WireGuard key in base64, as unbase64mem_full() decodes it. + * + * 32 bytes is ten full 4-character groups plus a final 3-character group padded with one `=`; the + * padding is mandatory — without it unbase64_next() runs out of input mid-group and fails. The third + * character of a padded group must have its low two bits clear (`c & 3`), which limits the 43rd + * character to `AEIMQUYcgkosw048`. unbase64char() accepts the URL-safe alphabet too (`-` for `+`, + * `_` for `/`), and unbase64_next() skips whitespace anywhere in the input. + */ +val WIREGUARD_BASE64_KEY = RegexTerminal( + """[A-Za-z0-9+/_=\-]+(?:[ \t]+[A-Za-z0-9+/_=\-]+)*""", + """(?:[A-Za-z0-9+/_\-][ \t]*){42}[AEIMQUYcgkosw048][ \t]*=""", +) + +/** + * `@name` — a credential reference resolved by read_credential(), which refuses any name that fails + * credential_name_valid() (src/shared/creds-util.c). [CONDITION_CREDENTIAL] models that check. + */ +val CREDENTIAL_REFERENCE: Combinator = SequenceCombinator(LiteralChoiceTerminal("@"), CONDITION_CREDENTIAL) + + +// --------------------------------------------------------------------------------------------------- +// Address prefixes — in_addr_prefix_from_string_auto_full (src/basic/in-addr-util.c). + +/** + * An IPv4 or IPv6 address with an optional `/prefixlen`, as in_addr_prefix_from_string_auto (mode + * PREFIXLEN_FULL) reads it. Like [IP_ADDR_AND_ANY_PREFIX], but the length goes through + * in_addr_parse_prefixlen → safe_atou8, so it is a base-0 number (`/0x18` is 24, `/0b+1000` is 8) up to the + * family width. Its only user, AllowedIPs=, splits the list on whitespace first, so no whitespace reaches + * safe_atou8 (`/0b 1` is the two words `…/0b` and `1`). IPv6 is tried first so the IPv4 branch never strands + * part of an IPv6 literal. + */ +val IP_ADDR_AND_SAFE_PREFIX: Combinator = AlternativeCombinator( + SequenceCombinator( + IPV6_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + ), + ), + SequenceCombinator( + IPV4_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 32L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + ), + ), +) + + +// --------------------------------------------------------------------------------------------------- +// Checks on a whole matched span — for rules systemd applies to the parsed value rather than its shape. + +/** + * Matches exactly what [inner] matches, but the match is only semantically valid when [check] accepts + * the matched text. For limits systemd applies after parsing, which a grammar cannot spell out: a + * magnitude bound on a parse_size() total, or "must (not) be a multicast address". + * + * Under the parse engine a rejected span marks its first token invalid; under the classic engine the + * semantic match fails with no progress at the span's start. Either way the first token of the span is + * highlighted, and the value keeps the structured grammar's colouring. + */ +class NetdevLinkCheckedSpan( + private val inner: Combinator, + private val description: String, + private val check: (String) -> Boolean, +) : Combinator { + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val match = inner.SemanticMatch(value, offset) + if (match.matchResult == -1 || check(value.substring(offset, match.matchResult))) return match + return NoMatch.copy(longestMatch = offset) + } + + override fun parse(value: String, offset: Int): Sequence = + inner.parse(value, offset).map { step -> + if (step is Parse && step.tokens.isNotEmpty() && !check(value.substring(offset, step.end))) { + step.copy(tokens = listOf(step.tokens.first().copy(valid = false)) + step.tokens.drop(1)) + } else { + step + } + } + + override fun toString(): String = "Checked($description)" + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "Checked($description,\n" + inner.toStringIndented(indent + 1) + ")" +} + +/** + * in_addr_is_multicast() (src/basic/in-addr-util.c) on an address literal [IP_ADDR] has matched: IPv4 + * in 224.0.0.0/4 (IN_MULTICAST), IPv6 in ff00::/8 (IN6_IS_ADDR_MULTICAST). The first IPv6 byte is 0xff + * only when the first group is written with four hex digits starting `ff` — `ff::1` is 0x00ff::1, and an + * address starting `::` (including IPv4-mapped `::ffff:239.1.1.1`) has a zero first byte. + */ +fun netdevLinkIsMulticast(address: String): Boolean { + if (':' in address) { + val first = address.substringBefore(':') + return first.length == 4 && first.startsWith("ff", ignoreCase = true) + } + return address.substringBefore('.').toIntOrNull() in 224..239 +} + +/** + * The total parse_size(text, 1000, …) (src/basic/parse-util.c) computes for a value [siSize] has + * matched: each element contributes `l * factor + (unsigned long long) (frac * factor)`. Exact decimal + * arithmetic stands in for the C double, which only matters within a unit of the truncated fraction. + * Null if [text] does not have [siSize]'s shape. + */ +fun netdevLinkParseSizeSi(text: String): BigInteger? { + val factors = mapOf("E" to 18, "P" to 15, "T" to 12, "G" to 9, "M" to 6, "K" to 3, "B" to 0, "" to 0) + val element = Regex("""\s*\+?([0-9]+)(?:\.([0-9]*))?\s*([EPTGMKB]?)""") + var total = BigInteger.ZERO + var pos = 0 + while (pos < text.length) { + val m = element.matchAt(text, pos) ?: return null + if (m.value.isEmpty()) return null + val factor = BigInteger.TEN.pow(factors.getValue(m.groupValues[3])) + val fraction = m.groupValues[2].takeIf { it.isNotEmpty() } + ?.let { java.math.BigDecimal("0.$it").multiply(java.math.BigDecimal(factor)).toBigInteger() } ?: BigInteger.ZERO + total += BigInteger(m.groupValues[1]) * factor + fraction + pos = m.range.last + 1 + } + return total +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt new file mode 100644 index 00000000..e8af0df8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAdvertiseOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseAdvertiseOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Advertise=1000baset-full + Advertise=10baset-half 100baset-full autonegotiation pause asym-pause + Advertise=10000baser-fec fec-rs + Advertise=1600000basedr8-2-full + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Advertise=hoge foo + Advertise=1000BaseT-Full + Advertise=autoneg + Advertise=1000baset-full,100baset-full + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt new file mode 100644 index 00000000..8b5eef20 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBadadvBandwidthOptionValueTest.kt @@ -0,0 +1,67 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBadadvBandwidthOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [BatmanAdvanced] + GatewayBandwidthDown=205M + GatewayBandwidthUp=2G + GatewayBandwidthDown=1000 + GatewayBandwidthUp=1.5G + GatewayBandwidthDown=10.M + GatewayBandwidthUp=3. K + GatewayBandwidthDown=4 M 11.5K + GatewayBandwidthUp=3.5G3B + GatewayBandwidthDown=4T3G3B + GatewayBandwidthUp=+5K + GatewayBandwidthDown=429496729599999 + GatewayBandwidthUp=429T 496G 729M 599K 999 + GatewayBandwidthDown=429.496729599999T + GatewayBandwithDown=100K + GatewayBandwithUp=1T + GatewayBandwidthDown=111.4 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BatmanAdvanced] + GatewayBandwidthDown=5k + GatewayBandwidthUp=-1 + GatewayBandwidthDown=12X + GatewayBandwidthUp=3B3.5G + GatewayBandwidthDown=12P12P + GatewayBandwidthUp=3. 0 K + GatewayBandwidthDown=fast + GatewayBandwithUp=1 Mbit + GatewayBandwidthDown=12P + GatewayBandwidthUp=3E 2P + GatewayBandwidthDown=429496729600000 + GatewayBandwidthUp=429.4967296T + GatewayBandwidthDown=0.5P + GatewayBandwidthUp=20E + GatewayBandwidthDown=18446744073709551616 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(15, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt new file mode 100644 index 00000000..e22f59a9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCoalesceNsecOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCoalesceNsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RxCoalesceCQENSec=500 + RxCoalesceCQENSec=1us + RxCoalesceCQENSec=2.5 + RxCoalesceCQENSec=1s500ms + RxCoalesceCQENSec=.7 + RxCoalesceCQENSec=3.1 sec .2 sec + RxCoalesceCQENSec=+3.1s + RxCoalesceCQENSec=3.1s.2 + RxCoalesceCQENSec=100 ns + RxCoalesceCQENSec=1µs + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RxCoalesceCQENSec=infinity + RxCoalesceCQENSec=-5 + RxCoalesceCQENSec=5. + RxCoalesceCQENSec=3.s + RxCoalesceCQENSec=xyz + RxCoalesceCQENSec=12..34 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt new file mode 100644 index 00000000..06a3074c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCpuSetOptionValueTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCpuSetOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinity= + IRQAffinity=0 + IRQAffinity=1 2 4 + IRQAffinity=0-3,8-11 + IRQAffinity=0, 1, 2, 3, 4, 5, 6, 7, 63, + IRQAffinity=8 '9' 10 "11" + IRQAffinity=0-3 8-11, + IRQAffinity=8000-8191 + IRQAffinity=0x0-0x3 + IRQAffinity=,0 + IRQAffinity=0-"3" + IRQAffinity='0'-'3', 4 + IRQAffinity=0b+1 + IRQAffinity=0-0b+1 + IRQAffinity=0-0b-0 + IRQAffinity="0b 1" + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinity=0 1 2 3 garbage + IRQAffinity=0-3 8-garbage + IRQAffinity=8192 + IRQAffinity=1- + IRQAffinity=-1 + IRQAffinity=0 1 2 3 "4 5 6 7 + IRQAffinity=-0 + IRQAffinity=0-"9000" + IRQAffinity=0b-0 + IRQAffinity=0b 1 + IRQAffinity=0-0b 1 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt new file mode 100644 index 00000000..44b4c1b6 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDefaultPortVlanidOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDefaultPortVlanidOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Bridge] + DefaultPVID=9 + DefaultPVID=none + DefaultPVID=1 + DefaultPVID=4094 + DefaultPVID=0 + DefaultPVID=0xFFE + DefaultPVID=0b 1 + DefaultPVID=0b+1 + DefaultPVID=0o 7 + DefaultPVID=0b -0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Bridge] + DefaultPVID=4095 + DefaultPVID=None + DefaultPVID=-1 + DefaultPVID=1-10 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt new file mode 100644 index 00000000..124aa131 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGeneveAddressOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseGeneveAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [GENEVE] + Remote=10.0.0.2 + Remote=2001:db8::1 + Remote=223.255.255.255 + Remote=240.0.0.1 + Remote=ff::1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [GENEVE] + Remote=10.0.0 + Remote=geneve.example.com + Remote=10.0.0.2/24 + Remote=239.1.1.1 + Remote=ff02::1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt new file mode 100644 index 00000000..4fb1e872 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfaliasOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIfaliasOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Alias= + Alias=uplink to core switch + Alias=eth0 (management) + Alias=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Alias=Café uplink + Alias=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt new file mode 100644 index 00000000..8cf16f95 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIfnamesOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIfnamesOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + AlternativeName=dummy98-2-altname + AlternativeName=dummyalt hogehogehogehogehogehoge + AlternativeName=a-very-long-alternative-interface-name-that-is-fine + AlternativeName=sim99 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + AlternativeName=eth:0 + AlternativeName=all + AlternativeName=123 + AlternativeName=ok bad/name + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt new file mode 100644 index 00000000..7b252817 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpoibPkeyOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIpoibPkeyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPoIB] + PartitionKey=1 + PartitionKey=0x8001 + PartitionKey=0xffff + PartitionKey=0x7fff + PartitionKey=32769 + PartitionKey=0b1 + PartitionKey=0b 1 + PartitionKey=0b+1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [IPoIB] + PartitionKey=0 + PartitionKey=0x8000 + PartitionKey=32768 + PartitionKey=0x10000 + PartitionKey=default + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt new file mode 100644 index 00000000..e115519f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6FlowlabelOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIpv6FlowlabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Tunnel] + IPv6FlowLabel=inherit + IPv6FlowLabel=0 + IPv6FlowLabel=1048575 + IPv6FlowLabel=0xFFFFF + IPv6FlowLabel=0x12345 + IPv6FlowLabel=0b 1 + IPv6FlowLabel=0b-0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Tunnel] + IPv6FlowLabel=1048576 + IPv6FlowLabel=0x100000 + IPv6FlowLabel=Inherit + IPv6FlowLabel=-1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt new file mode 100644 index 00000000..2b368796 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityNumaOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIrqAffinityNumaOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityNUMA=local + IRQAffinityNUMA=0 + IRQAffinityNUMA=3 + IRQAffinityNUMA=0x1 + IRQAffinityNUMA=4294967293 + IRQAffinityNUMA=0b 1 + IRQAffinityNUMA=0b+1 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityNUMA=4294967294 + IRQAffinityNUMA=-1 + IRQAffinityNUMA=remote + IRQAffinityNUMA=1.5 + IRQAffinityNUMA=Local + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt new file mode 100644 index 00000000..32344a7a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIrqAffinityPolicyOptionValueTest.kt @@ -0,0 +1,40 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIrqAffinityPolicyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityPolicy=single + IRQAffinityPolicy=spread + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + IRQAffinityPolicy=Single + IRQAffinityPolicy=balanced + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt new file mode 100644 index 00000000..651d76a0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpSessionNameOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseL2tpSessionNameOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TPSession] + Name=l2tp-ses1 + Name=sess.a + Name=1a + Name=abcdefghijklmno + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TPSession] + Name=abcdefghijklmnop + Name=123 + Name=all + Name=a:b + Name=a/b + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt new file mode 100644 index 00000000..daa73eba --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelLocalAddressOptionValueTest.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseL2tpTunnelLocalAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Local=192.168.30.100 + Local=2001:db8::1 + Local=auto + Local=static + Local=dynamic + Local=auto@eth0 + Local=dynamic@test1 + Local=192.168.30.100@eth0 + Local=auto@@eth0 + Local=auto@ + Local=static@a-very-long-alternative-interface-name + Local=@auto + Local=@@192.168.30.100@eth0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Local=any + Local=auto@all + Local=auto@eth/0 + Local=192.168.30.256 + Local=auto@123 + Local=@ + Local=Auto + Local=STATIC@eth0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt new file mode 100644 index 00000000..6d7d654d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseL2tpTunnelRemoteAddressOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseL2tpTunnelRemoteAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Remote=192.168.30.101 + Remote=2001:db8::2 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [L2TP] + Remote=auto + Remote=192.168.30 + Remote=peer.example.com + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt new file mode 100644 index 00000000..54f08a4c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacAddressPolicyOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMacAddressPolicyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + MACAddressPolicy=persistent + MACAddressPolicy=random + MACAddressPolicy=none + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + MACAddressPolicy=Persistent + MACAddressPolicy=keep + MACAddressPolicy=yes + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt new file mode 100644 index 00000000..97c1be19 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyFileOptionValueTest.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMacsecKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyFile=/etc/systemd/network/macsec.key + [MACsecReceiveAssociation] + KeyFile=/run/keys/rx key.hex + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyFile=macsec.key + KeyFile=/sys/kernel/key + [MACsecReceiveAssociation] + KeyFile=/etc/keys/../secret + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt new file mode 100644 index 00000000..7be9e45d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMacsecKeyIdOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMacsecKeyIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyId=01 + KeyId=0203 + KeyId=0203040506070809 + KeyId=ABCDEF + KeyId=00112233445566778899aabbccddeeff + KeyId=01 02 03 + [MACsecReceiveAssociation] + KeyId=020304 + KeyId=ff + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [MACsecTransmitAssociation] + KeyId=1 + KeyId=255 + KeyId=0 + KeyId=0x01 + KeyId=00112233445566778899aabbccddeeff00 + [MACsecReceiveAssociation] + KeyId=zz + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt new file mode 100644 index 00000000..0be26b67 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePortOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Port=tp + Port=aui + Port=mii + Port=fibre + Port=bnc + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Port=fiber + Port=TP + Port=da + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt new file mode 100644 index 00000000..96e428c8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePortRangeOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePortRangeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + PortRange=1-65535 + PortRange=4789 + PortRange=0x100-0x200 + PortRange=32768-61000 + PortRange=1000- 2000 + PortRange=0b 1-5 + PortRange=0b+1-5 + PortRange=1-0b 1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + PortRange=0-100 + PortRange=1-65536 + PortRange=100:200 + PortRange=-5 + PortRange=5- + PortRange=1000 -2000 + PortRange=+0b1-2 + PortRange=-0b0-2 + PortRange=5--0 + PortRange=-0-5 + PortRange=0b-0 + PortRange=1-0b -1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt new file mode 100644 index 00000000..6a8927d8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRpsCpuMaskOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRpsCpuMaskOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ReceivePacketSteeringCPUMask=0 1 + ReceivePacketSteeringCPUMask=2,3 + ReceivePacketSteeringCPUMask=all + ReceivePacketSteeringCPUMask=disable + ReceivePacketSteeringCPUMask=0-7 + ReceivePacketSteeringCPUMask=1 + ReceivePacketSteeringCPUMask=0-"3" + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ReceivePacketSteeringCPUMask=invalid + ReceivePacketSteeringCPUMask=0 3 8-invalid + ReceivePacketSteeringCPUMask=all 1 + ReceivePacketSteeringCPUMask=none + ReceivePacketSteeringCPUMask=-0 + ReceivePacketSteeringCPUMask=All + ReceivePacketSteeringCPUMask=Disable + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt new file mode 100644 index 00000000..3e701359 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUdevPropertyOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseUdevPropertyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Property=ID_NET_MANAGED_BY=io.systemd.Network + Property=ID_NET_NAME_MAC=* + Property=FOO=bar BAZ=qux + Property=FOO= + Property="FOO=bar baz" + Property=FOO="bar baz" + Property='FOO=bar baz' X=1 + Property=HOST_%H=1 + Property=_X1=a=b + Property=DEVPATHX=1 + Property=TAGS_EXTRA=1 + Property="FOO"=bar + Property=F"OO"=bar + Property=FOO\x41=1 + Property='ID_X'=1 "Y"=2 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Property=FOO + Property=1FOO=bar + Property=INTERFACE=eth0 + Property=SYNTH_ARG_X=1 + Property=FOO-BAR=1 + Property=ACTION=add + Property="ACTION"=add + Property=FOO\q=1 + Property=F"OO=bar + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(9, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt new file mode 100644 index 00000000..bd29db52 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanQosMapsOptionValueTest.kt @@ -0,0 +1,59 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVlanQosMapsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + EgressQOSMaps=21-7 45-5 + IngressQOSMaps=1-1 + EgressQOSMaps=5 + IngressQOSMaps=0x10-0x3 "7-2" '8-1' + EgressQOSMaps=4294967295-7 + IngressQOSMaps=1-7 2-6 + EgressQOSMaps=1-"2" "1"-2 3 + IngressQOSMaps=5--0 + EgressQOSMaps=0-7 + IngressQOSMaps=1-5 + EgressQOSMaps=4294967295-1 + EgressQOSMaps=0b+1-2 + EgressQOSMaps=1-0b+10 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + EgressQOSMaps=21:7 + IngressQOSMaps=1- + EgressQOSMaps=-5 + IngressQOSMaps=1-2-3 + EgressQOSMaps=4294967296-1 + IngressQOSMaps=a-b + EgressQOSMaps=-0 + IngressQOSMaps=1-"2 + EgressQOSMaps=0b 1-2 + EgressQOSMaps=0b-0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(10, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt new file mode 100644 index 00000000..10f5f748 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVlanprotocolOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVlanprotocolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + Protocol=802.1q + Protocol=802.1Q + Protocol=802.1ad + Protocol=802.1AD + [Bridge] + VLANProtocol=802.1ad + VLANProtocol=802.1Q + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VLAN] + Protocol=802.1Ad + Protocol=8021q + Protocol=qinq + [Bridge] + VLANProtocol=802.1x + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt new file mode 100644 index 00000000..db886ccc --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVrfTableOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVrfTableOptionValueTest : AbstractUnitFileTest() { + + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VRF] + Table=42 + Table=main + Table=local + Table=default + Table=0x2a + Table=custom-table + Table=4294967295 + Table=yes + Table=0x0 + Table=mian + Table=main 42 + Table=1 2 + Table=my table + Table=0b 1 + Table=0b+1 + Table=0o 7 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VRF] + Table=0 + Table=4294967296 + Table=08 + Table=vrf:42 + Table=040000000000 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt new file mode 100644 index 00000000..590ef709 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseVxlanAddressOptionValueTest.kt @@ -0,0 +1,72 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseVxlanAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + Group=239.1.1.1 + Group=ff02::42 + Local=192.168.1.1 + Local=fe80::1 + Local=dhcp4 + Local=ipv6_link_local + Local=slaac + Local=dhcp_pd + Remote=10.0.0.2 + Remote=2001:db8::1 + Remote=::ffff:10.0.0.2 + Group=FF05::2 + Group=224.0.0.1 + Group=239.255.255.255 + Remote=223.255.255.255 + Remote=fe80::1 + Local=::ffff:239.1.1.1 + Local=dhcp6 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [VXLAN] + Group=239.1.1 + Local=dhcp + Remote=example.com + Remote=10.0.0.256 + Local=192.168.1.1/24 + Group=fe80::1::2 + Group=dhcp4 + Remote=slaac + Group=192.168.1.1 + Group=240.0.0.1 + Group=ff::1 + Group=::ffff:239.1.1.1 + Remote=239.1.1.1 + Remote=ff02::1 + Local=239.1.1.1 + Local=ff02::1 + Local=DHCP4 + Local=Slaac + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(18, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt new file mode 100644 index 00000000..497fd9dc --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardAllowedIpsOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardAllowedIpsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + AllowedIPs= + AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24 + AllowedIPs=0.0.0.0/0 + AllowedIPs=::/0 + AllowedIPs=10.0.0.1 + AllowedIPs=fd8d:4d6d:3ccb:0500:900c:d437:ec27:8822/128 + AllowedIPs=10.0.0.0/8, 192.168.0.0/16 + AllowedIPs=10.0.0.0/8 fd00::/8, + AllowedIPs=10.0.0.0/0x8 fd00::/0100 + AllowedIPs=10.0.0.0/0b+1000 + AllowedIPs=10.0.0.0/0o10 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + AllowedIPs=10.0.0.0/33 + AllowedIPs=fd00::/129 + AllowedIPs=10.0.0.0/8;192.168.0.0/16 + AllowedIPs=any + AllowedIPs=10.0.0/8 + AllowedIPs=10.0.0.0/ + AllowedIPs=10.0.0.0/0b 1 + AllowedIPs=10.0.0.0/ 8 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt new file mode 100644 index 00000000..fe53bfba --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardEndpointOptionValueTest.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardEndpointOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + Endpoint=192.168.27.3:51820 + Endpoint=[2001:db8::1]:51820 + Endpoint=wg.example.com:51820 + Endpoint=@network.wireguard.peer0.endpoint + Endpoint=192.168.27.3 + Endpoint=fe80::1 + Endpoint=vpn.example.org.:0x1F90 + Endpoint=[fe80::1%eth0]:51820 + Endpoint=host:65535 + Endpoint=a\255b.example.com:51820 + Endpoint=:51820 + Endpoint=192.168.27.3:0b+1 + Endpoint=192.168.27.3:0b 1 + Endpoint=[2001:db8::1]:0o 7 + Endpoint=host.example:0b 1 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + Endpoint=wg.example.com + Endpoint=192.168.27.3:0 + Endpoint=192.168.27.3:65536 + Endpoint=wg..example.com:51820 + Endpoint=[2001:db8::1]: + Endpoint=wg.example.com:port + Endpoint=a\999b:51820 + Endpoint=192.168.27.3: 80 + Endpoint=host.example: 80 + Endpoint=192.168.27.3:0b -0 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(10, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt new file mode 100644 index 00000000..65f151e7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerKeyFileOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardPeerKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + PublicKeyFile=/run/systemd/network/25-wireguard-public-key.txt + PresharedKeyFile=/etc/systemd/network/wg psk.key + PresharedKeyFile=/etc//systemd/./network/psk/ + PublicKeyFile=/devices/key + PublicKeyFile=/run/credentials/%i.key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + PublicKeyFile=relative/key + PresharedKeyFile=/etc/../root/psk + PublicKeyFile=/proc/self/environ + PresharedKeyFile=/dev/stdin + PublicKeyFile=%d/key + PresharedKeyFile=//sys/key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt new file mode 100644 index 00000000..bbc40d13 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPeerRouteTableOptionValueTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardPeerRouteTableOptionValueTest : AbstractUnitFileTest() { + + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + RouteTable=off + RouteTable=no + RouteTable=0 + RouteTable=false + RouteTable=OFF + RouteTable=No + RouteTable=fAlSe + RouteTable=F + RouteTable=1234 + RouteTable=main + RouteTable=local + RouteTable=default + RouteTable=0x10 + RouteTable=4294967295 + RouteTable=custom + RouteTable=vpn-table + RouteTable=main table + RouteTable=0b 1 + RouteTable=0b+1 + RouteTable=0o 7 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuardPeer] + RouteTable=4294967296 + RouteTable=08 + RouteTable=custom:100 + RouteTable=00 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt new file mode 100644 index 00000000..68830390 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyFileOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardPrivateKeyFileOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKeyFile=/run/systemd/network/25-wireguard-private-key.txt + PrivateKeyFile=/etc/wireguard/my key + PrivateKeyFile=/./devices/key + PrivateKeyFile=/.dev/key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKeyFile=wg.key + PrivateKeyFile=/proc/1/environ + PrivateKeyFile=/etc/wireguard/../../shadow + PrivateKeyFile=/./dev/shm/key + PrivateKeyFile=//sys/key + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt new file mode 100644 index 00000000..85839cff --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardPrivateKeyOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardPrivateKeyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= + PrivateKey=Eor9QBsIoUG6C3ZKsKdqm4Vkt0n2N7qpSh2LzphQeho= + PrivateKey=@network.wireguard.private + PrivateKey=EEGlnEPYJV__kbvvIqxKkQwOiS-UENyPncC4bF46ong= + PrivateKey=EEGlnEPYJV//kbvvIqxK kQwOiS+UENyPncC4bF46ong= + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46onh= + PrivateKey=short= + PrivateKey=@bad/name + PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong== + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt new file mode 100644 index 00000000..b8212f90 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWireguardRouteTableOptionValueTest.kt @@ -0,0 +1,64 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWireguardRouteTableOptionValueTest : AbstractUnitFileTest() { + + // Values with whitespace are possible table names (networkd.conf can define `my\ table:100`), so they are not + // flagged even though sd-check, which loads no networkd.conf, reports them. `0b 1`, `0b+1` and `0o 7` are + // safe_atou32 numbers (sd-check accepts them). + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + RouteTable=off + RouteTable=no + RouteTable=0 + RouteTable=false + RouteTable=OFF + RouteTable=No + RouteTable=fAlSe + RouteTable=F + RouteTable=1234 + RouteTable=main + RouteTable=local + RouteTable=default + RouteTable=0x10 + RouteTable=4294967295 + RouteTable=custom + RouteTable=vpn-table + RouteTable=yes + RouteTable=main table + RouteTable=0b 1 + RouteTable=0b+1 + RouteTable=0o 7 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [WireGuard] + RouteTable=4294967296 + RouteTable=08 + RouteTable=custom:100 + RouteTable=00 + """.trimIndent() + + setupFileInEditor("file.netdev", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt new file mode 100644 index 00000000..78a342df --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseWolPasswordOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseWolPasswordOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + WakeOnLanPassword=00:11:22:33:44:55 + WakeOnLanPassword=00-11-22-33-44-55 + WakeOnLanPassword=0011.2233.4455 + WakeOnLanPassword=/etc/systemd/network/wol.pass + WakeOnLanPassword=/run/credentials/wol pass + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + WakeOnLanPassword=00:11:22:33:44 + WakeOnLanPassword=secret + WakeOnLanPassword=/etc/../wol.pass + WakeOnLanPassword=00:11:22:33:44:55:66:77 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt new file mode 100644 index 00000000..5148c1f5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetdevLinkCombinatorsTest.kt @@ -0,0 +1,153 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import org.junit.Assert.assertEquals +import org.junit.Test + +/** + * Grammar-level tests for the shared helpers in BatchNetdevLinkCombinators.kt, run against BOTH matching + * engines: the classic SyntacticMatch/SemanticMatch pair and the list-of-successes [validate]. The cases + * come from systemd's own unit tests at 0f0682b773 (test-parse-util.c, test-time-util.c, + * test-cpu-set-util.c) where one exists, otherwise from sd-check. The integer and route-table terminals + * themselves are covered by SafeAtoTerminalTest and RouteTableTest. + */ +class BatchNetdevLinkCombinatorsTest { + + private fun classicAccepts(grammar: Combinator, value: String): Boolean = + grammar.SyntacticMatch(value, 0).matchResult == value.length && grammar.SemanticMatch(value, 0).matchResult == value.length + + private fun assertAccepts(grammar: Combinator, expected: Boolean, vararg values: String) { + val whole = SequenceCombinator(grammar, EOF()) + for (value in values) { + assertEquals("classic engine on '$value'", expected, classicAccepts(whole, value)) + assertEquals("parse engine on '$value'", expected, whole.validate(value) == ParseOutcome.Valid) + } + } + + @Test + fun testSiSizeMatchesParseSizeSiTests() { + // TEST(parse_size_si) successes. + assertAccepts(siSize(), true, "111", "111.4", "112 B", "112.6 B", "3.5 K", "3. K", "3.0 K", "4 M 11.5K", + "3.5G3B", "3.5G 4B", "4T3G3B", "4 T 3 G 3 B", "12P", "3E 2P", "1000E", "10.M", "+5K") + // TEST(parse_size_si) failures (bar 1000E, an overflow this grammar does not compute). + assertAccepts(siSize(), false, "3. 0 K", "3B3.5G", "3B3G4T", "12P12P", "12X", "12.5X", "12.5e3", "-1", "-1000E", + "-10B 20K", "1k", "1 2") + } + + @Test + fun testNsecDurationMatchesParseNsecTests() { + // TEST(parse_nsec) successes, minus the two "infinity" spellings this grammar deliberately excludes. + assertAccepts(NSEC_DURATION_FINITE, true, "5s", "5s500ms", "5s 500ms", "5.5s", "5.5s 0.5ms", ".22s", ".50y", + "2.5", ".7", "+3.1s", "3.1s.2", "3.1 .2s", "3.1 sec .2 sec", "3.1 sec 1.2 sec", "1.111111111111y", "100 ns", "1µs") + assertAccepts(NSEC_DURATION_FINITE, false, "xyz", ".", "5.", ".s", "-5s", "-0.3s", "0.-0s", "3.-0s", "3.+1s", + "3. 1s", "3.s", "12..34", "..1234", "1234..", "infinity") + } + + @Test + fun testCpuSetMatchesParseCpuSetTests() { + // TEST(parse_cpu_set) successes. + assertAccepts(CPU_SET, true, "0", "1 2 4", "0 1 2 3 8 9 10 11", "8 '9' 10 \"11\"", "0,1,2,3 8,9,10,11", + "0, 1, 2, 3, 4, 5, 6, 7, 63, ", "0-3,8-11", "36-39,44-47", "64-71", "0-3 8-11, ", "0-7 4-11", "0,2 4-11", + "8000-8191", "0x0-0x3", ",0") + // TEST(parse_cpu_set) failures (bar "3-0", a reversed range this grammar does not compare). + assertAccepts(CPU_SET, false, "0 1 2 3 garbage", "0-3 8-garbage", "0 1 2 3 \"4 5 6 7 ", "8192", "1-", "-1") + // EXTRACT_UNQUOTE removes quotes anywhere in a word, and without EXTRACT_CUNESCAPE a backslash just + // makes the next character literal. + assertAccepts(CPU_SET, true, "0-\"3\"", "'0'-'3'", "\"0\"-3, 4", "0-\\3", "\" 3\"", "1-\"0x3\"") + // The lower bound of a range is cut at the first `-`, so `-0` is an empty word there. + assertAccepts(CPU_SET, false, "-0", "-0-3", "0-\"9000\"", "\"0-3", "\"3 \"", "0-\"\"", "0-", "0- 3") + + // The bounds are SafeAtoTerminal numbers in a word split on whitespace and commas (sd-check, IRQAffinity=): + // a sign after 0b/0o is fine, but `0b 1` is two words and the lower bound of `0b-0` is the bare word `0b`. + assertAccepts(CPU_SET, true, "0b+1", "0-0b+1", "0-0b-0", "0o17", "\"0b 1\"") + assertAccepts(CPU_SET, false, "0b-0", "0b 1", "0-0b 1", "0b-1-3") + } + + @Test + fun testParseRangeAcceptsSplitsAtTheFirstDash() { + val lower = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 99L, + acceptsWhitespace = true, + acceptsMinus = false, + ) + val upper = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 99L, + acceptsWhitespace = true, + acceptsMinus = true, + ) + for (word in listOf("5", "5-7", "5--0", "0x5-+7", "0", " 5", "5- 7", "0b 1-0b+1")) { + assertEquals("parse_range accepts <$word>", true, netdevLinkParseRangeAccepts(word, lower, upper)) + } + for (word in listOf("", "-0", "-5", "5-", "5 -7", "100", "5-100", "5-0b-1", "0b-0")) { + assertEquals("parse_range refuses <$word>", false, netdevLinkParseRangeAccepts(word, lower, upper)) + } + } + + @Test + fun testExtractWordUnquotesAndUnescapes() { + assertEquals("FOO=bar baz", extractWord("\"FOO=bar baz\"", true)) + assertEquals("FOO=bar", extractWord("F\"OO\"=bar", true)) + assertEquals("FOOA=1", extractWord("FOO\\x41=1", true)) + assertEquals("a b", extractWord("a\\sb", true)) + assertEquals("a\u00e9", extractWord("a\\u00e9", true)) + assertEquals("a\u0001", extractWord("a\\001", true)) + assertEquals(null, extractWord("FOO\\q", true)) + assertEquals(null, extractWord("a\\x00", true)) + assertEquals(null, extractWord("a\\400", true)) + assertEquals(null, extractWord("a\\uD800", true)) + assertEquals(null, extractWord("\"abc", true)) + assertEquals(null, extractWord("abc\\", false)) + assertEquals("aq", extractWord("a\\q", false)) + } + + @Test + fun testKeyFilePathRefusesApiVfsAndDotDot() { + assertAccepts(NON_API_VFS_ABSOLUTE_PATH, true, "/etc/key", "/etc/my key", "/devices/x", "/sysroot/key", "/etc//a/./b/", + "/./devices/x", "/.dev/x", "/..dev/x") + assertAccepts(NON_API_VFS_ABSOLUTE_PATH, false, "key", "/dev", "/dev/null", "//sys/x", "/proc/1/environ", "/a/../b", "/a/..", + "/./dev/shm/key", "/.//./proc", "//./sys/", "/dev/") + } + + @Test + fun testWireguardKeyNeedsPaddingAndAValidLastCharacter() { + assertAccepts(WIREGUARD_BASE64_KEY, true, "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong=", + "EEGlnEPYJV__kbvvIqxKkQwOiS-UENyPncC4bF46ong=", "EEGlnEPYJV//kbvvIqxK kQwOiS+UENyPncC4bF46ong =") + assertAccepts(WIREGUARD_BASE64_KEY, false, "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong", + "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46onh=", "EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong==", "AAAA") + } + + @Test + fun testSafePrefixLengthIsASafeAtou8WithinTheFamilyWidth() { + // sd-check (AllowedIPs=): `/0b+1000`, `/0o10` and `0.0.0.0/0b-0` are accepted; `/0b-1` is refused, and so are + // `/0b 1` and `/ 8`, which the list splits into two words. + assertAccepts(IP_ADDR_AND_SAFE_PREFIX, true, "10.0.0.0/8", "10.0.0.0/0x8", "10.0.0.0/0b+1000", "10.0.0.0/0o10", + "0.0.0.0/0b-0", "10.0.0.1", "fd00::/0x40", "::/128") + assertAccepts(IP_ADDR_AND_SAFE_PREFIX, false, "10.0.0.0/33", "10.0.0.0/0b-1", "10.0.0.0/0b 1", "10.0.0.0/ 8", + "fd00::/129", "10.0.0.0/") + } + + @Test + fun testCheckedSpanRejectsWhatItsCheckRefusesOnBothEngines() { + val multicast = NetdevLinkCheckedSpan(IP_ADDR, "multicast") { netdevLinkIsMulticast(it) } + assertAccepts(multicast, true, "224.0.0.1", "239.255.255.255", "ff02::1", "FF05::2") + assertAccepts(multicast, false, "223.255.255.255", "240.0.0.1", "ff::1", "::ffff:239.1.1.1", "10.0.0.1", "239.1.1") + } + + @Test + fun testParseSizeSiTotals() { + assertEquals(java.math.BigInteger("12000000000000000"), netdevLinkParseSizeSi("12P")) + assertEquals(java.math.BigInteger("3002000000000000000"), netdevLinkParseSizeSi("3E 2P")) + assertEquals(java.math.BigInteger("4011500"), netdevLinkParseSizeSi("4 M 11.5K")) + assertEquals(java.math.BigInteger("10000000"), netdevLinkParseSizeSi("10.M")) + assertEquals(java.math.BigInteger("111"), netdevLinkParseSizeSi("111.4")) + assertEquals(java.math.BigInteger("5000"), netdevLinkParseSizeSi("+5K")) + assertEquals(null, netdevLinkParseSizeSi("5k")) + } +} From eb7c737f8c24ae315327f5519395c4ad353d90c8 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 12:02:20 -0700 Subject: [PATCH 6/8] feat: validate networkd route, routing-policy, address and RA settings Add grammar-based value validation for: APN=, Address=, BindCarrier=, BootFilename=, Driver=, Environment=, FirewallMark=, HomeAgentLifetimeSec=, IPProtocol=, IPVLAN=, IPVTAP=, IPoIB=, IPv6PrefixDelegation=, IPv6ProxyNDPAddress=, IPv6SendRA=, IPv6StableSecretAddress=, InitialCongestionWindow=, KillExcludeUsers=, KillOnlyUsers=, Kind=, Label=, LifetimeSec=, MACVLAN=, MACVTAP=, MACsec=, MTUBytes=, MultiPathRoute=, NFTSet=, NetLabel=, OperatorId=, PIN=, Parameters=, Password=, Path=, Prefix=, Property=, ProxyNeighbor=, ReachableTimeSec=, RestartSec=, RetransmitSec=, Route=, RouteMetric=, RouterLifetimeSec=, SSID=, TCPAdvertisedMaximumSegmentSize=, TCPRetransmissionTimeoutSec=, Table=, Timezone=, Token=, Tunnel=, Type=, User=, VLAN=, VXLAN=, WLANInterfaceType=, Xfrm= Accepted values follow systemd's own parsers (derived from the systemd source at 0f0682b773 and checked against systemd's real parsers built at that commit). Values systemd accepts when parsing but rejects only at unit or container start are accepted; see #565. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 55 +++ .../ai/ConfigParseAddressLabelOptionValue.kt | 27 ++ .../ConfigParseCanRestartUsecOptionValue.kt | 34 ++ ...ConfigParseInAddrNonNullIpv6OptionValue.kt | 35 ++ .../ConfigParseIpv6AddressLabelOptionValue.kt | 36 ++ ...gParseIpv6AddressLabelPrefixOptionValue.kt | 55 +++ ...gedOomMemPressureDurationSecOptionValue.kt | 2 +- .../ai/ConfigParseMatchPropertyOptionValue.kt | 67 ++++ .../ai/ConfigParseMatchStrvOptionValue.kt | 34 ++ .../ConfigParseNdiscRouteMetricOptionValue.kt | 54 +++ ...seNeighborDestinationAddressOptionValue.kt | 29 ++ ...figParseNeighborProxyAddressOptionValue.kt | 41 ++ .../ai/ConfigParseNftSetNetworkOptionValue.kt | 131 +++++++ ...figParsePref64PrefixLifetimeOptionValue.kt | 35 ++ ...ConfigParseRouteMetricAdvMssOptionValue.kt | 37 ++ ...nfigParseRouteMetricInitCwndOptionValue.kt | 41 ++ .../ConfigParseRouteMetricMtuOptionValue.kt | 35 ++ ...ConfigParseRouteMetricTcpRtoOptionValue.kt | 39 ++ .../ConfigParseRouteMultiPathOptionValue.kt | 85 ++++ ...nfigParseRoutePrefixLifetimeOptionValue.kt | 34 ++ .../ai/ConfigParseRoutePrefixOptionValue.kt | 30 ++ .../ai/ConfigParseRouteTableOptionValue.kt | 33 ++ ...ParseRouterHomeAgentLifetimeOptionValue.kt | 34 ++ .../ConfigParseRouterLifetimeOptionValue.kt | 29 ++ ...gParseRouterPrefixDelegationOptionValue.kt | 64 +++ ...figParseRouterUint32MsecUsecOptionValue.kt | 36 ++ ...outingPolicyRuleFirewallMarkOptionValue.kt | 57 +++ ...eRoutingPolicyRuleIpProtocolOptionValue.kt | 77 ++++ ...rseRoutingPolicyRuleUidRangeOptionValue.kt | 97 +++++ .../ai/ConfigParseStackedNetdevOptionValue.kt | 54 +++ .../ConfigParseStringSafeAsciiOptionValue.kt | 34 ++ .../ai/ConfigParseStrvOptionValue.kt | 32 ++ .../ai/ConfigParseTimezoneOptionValue.kt | 38 ++ .../ai/RoutingPolicyRuleOptionValues.kt | 7 +- .../BatchNetworkRouteAddrCombinators.kt | 365 ++++++++++++++++++ .../ConfigParseAddressLabelOptionValueTest.kt | 39 ++ ...nfigParseAddressNetLabelOptionValueTest.kt | 44 +++ ...onfigParseCanRestartUsecOptionValueTest.kt | 41 ++ ...igParseInAddrNonNullIpv6OptionValueTest.kt | 42 ++ ...figParseIpv6AddressLabelOptionValueTest.kt | 46 +++ ...seIpv6AddressLabelPrefixOptionValueTest.kt | 47 +++ ...ConfigParseMatchPropertyOptionValueTest.kt | 76 ++++ .../ai/ConfigParseMatchStrvOptionValueTest.kt | 78 ++++ ...figParseNdiscRouteMetricOptionValueTest.kt | 55 +++ ...ighborDestinationAddressOptionValueTest.kt | 41 ++ ...arseNeighborProxyAddressOptionValueTest.kt | 45 +++ ...ConfigParseNftSetNetworkOptionValueTest.kt | 61 +++ ...arsePref64PrefixLifetimeOptionValueTest.kt | 41 ++ .../ConfigParsePrefixTokenOptionValueTest.kt | 42 ++ ...igParseRouteMetricAdvMssOptionValueTest.kt | 41 ++ ...ParseRouteMetricInitCwndOptionValueTest.kt | 48 +++ ...onfigParseRouteMetricMtuOptionValueTest.kt | 46 +++ ...igParseRouteMetricTcpRtoOptionValueTest.kt | 42 ++ ...onfigParseRouteMultiPathOptionValueTest.kt | 57 +++ ...ParseRoutePrefixLifetimeOptionValueTest.kt | 42 ++ .../ConfigParseRoutePrefixOptionValueTest.kt | 45 +++ .../ConfigParseRouteTableOptionValueTest.kt | 56 +++ ...eRouterHomeAgentLifetimeOptionValueTest.kt | 42 ++ ...onfigParseRouterLifetimeOptionValueTest.kt | 45 +++ ...seRouterPrefixDelegationOptionValueTest.kt | 55 +++ ...arseRouterUint32MsecUsecOptionValueTest.kt | 50 +++ ...ngPolicyRuleFirewallMarkOptionValueTest.kt | 51 +++ ...tingPolicyRuleIpProtocolOptionValueTest.kt | 62 +++ ...outingPolicyRuleUidRangeOptionValueTest.kt | 50 +++ ...ConfigParseStackedNetdevOptionValueTest.kt | 60 +++ ...nfigParseStringSafeAsciiOptionValueTest.kt | 43 +++ .../ai/ConfigParseStrvOptionValueTest.kt | 68 ++++ .../ai/ConfigParseTimezoneOptionValueTest.kt | 47 +++ .../ai/NetworkRouteAddrSafeAtoVerdictsTest.kt | 63 +++ 69 files changed, 3600 insertions(+), 4 deletions(-) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index c41e1c78..d5bce785 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt @@ -412,6 +412,61 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_unit_path_strv_printf", "0") to ConfigParseUnitPathStrvPrintfOptionValue() as OptionValueInformation, Validator("config_parse_socket_service", "0") to ConfigParseSocketServiceOptionValue() as OptionValueInformation, Validator("config_parse_service_sockets", "0") to ConfigParseServiceSocketsOptionValue() as OptionValueInformation, + // Batch 2026-10: network-route-addr + // Generic word-list / string / match parsers. + Validator("config_parse_match_strv", "0") to ConfigParseMatchStrvOptionValue() as OptionValueInformation, + Validator("config_parse_strv", "0") to ConfigParseStrvOptionValue() as OptionValueInformation, + Validator("config_parse_match_property", "0") to ConfigParseMatchPropertyOptionValue() as OptionValueInformation, + Validator("config_parse_string", "CONFIG_PARSE_STRING_SAFE_AND_ASCII") to ConfigParseStringSafeAsciiOptionValue() as OptionValueInformation, + Validator("config_parse_nft_set", "NFT_SET_PARSE_NETWORK") to ConfigParseNftSetNetworkOptionValue() as OptionValueInformation, + Validator("config_parse_timezone", "0") to ConfigParseTimezoneOptionValue() as OptionValueInformation, + Validator("config_parse_in_addr_non_null", "AF_INET6") to ConfigParseInAddrNonNullIpv6OptionValue() as OptionValueInformation, + Validator("config_parse_neighbor_proxy_address", "0") to ConfigParseNeighborProxyAddressOptionValue() as OptionValueInformation, + // [Network] stacked netdevs: the ltype only records the expected kind; every slot takes netdev names. + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPOIB") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPVLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_IPVTAP") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACSEC") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACVLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_MACVTAP") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_VLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_VXLAN") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "NETDEV_KIND_XFRM") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + Validator("config_parse_stacked_netdev", "_NETDEV_KIND_TUNNEL") to ConfigParseStackedNetdevOptionValue() as OptionValueInformation, + // [IPv6SendRA] / [IPv6Prefix] / [IPv6RoutePrefix] / [IPv6PREF64Prefix] / [IPv6AcceptRA]. + Validator("config_parse_router_prefix_delegation", "0") to ConfigParseRouterPrefixDelegationOptionValue() as OptionValueInformation, + Validator("config_parse_router_uint32_msec_usec", "0") to ConfigParseRouterUint32MsecUsecOptionValue() as OptionValueInformation, + Validator("config_parse_router_lifetime", "0") to ConfigParseRouterLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_router_home_agent_lifetime", "0") to ConfigParseRouterHomeAgentLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_route_prefix", "0") to ConfigParseRoutePrefixOptionValue() as OptionValueInformation, + Validator("config_parse_route_prefix_lifetime", "0") to ConfigParseRoutePrefixLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_pref64_prefix_lifetime", "0") to ConfigParsePref64PrefixLifetimeOptionValue() as OptionValueInformation, + // config_parse_prefix_token hands its value straight to config_parse_address_generation_type. + Validator("config_parse_prefix_token", "0") to ConfigParseAddressGenerationTypeOptionValue() as OptionValueInformation, + Validator("config_parse_ndisc_route_metric", "0") to ConfigParseNdiscRouteMetricOptionValue() as OptionValueInformation, + Validator("config_parse_ndisc_route_metric", "AF_INET6") to ConfigParseNdiscRouteMetricOptionValue() as OptionValueInformation, + Validator("config_parse_can_restart_usec", "0") to ConfigParseCanRestartUsecOptionValue() as OptionValueInformation, + // [Route] table slots (each ltype mirrors its own ConfigSectionParser entry). + Validator("config_parse_route_section", "ROUTE_TABLE") to ConfigParseRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_MULTIPATH") to ConfigParseRouteMultiPathOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_MTU") to ConfigParseRouteMetricMtuOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_ADVMSS") to ConfigParseRouteMetricAdvMssOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_INITCWND") to ConfigParseRouteMetricInitCwndOptionValue() as OptionValueInformation, + Validator("config_parse_route_section", "ROUTE_METRIC_RTO_MIN") to ConfigParseRouteMetricTcpRtoOptionValue() as OptionValueInformation, + // [RoutingPolicyRule] table slots. + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_TABLE") to ConfigParseRouteTableOptionValue("config_parse_routing_policy_rule") as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_FWMARK") to ConfigParseRoutingPolicyRuleFirewallMarkOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_IP_PROTOCOL") to ConfigParseRoutingPolicyRuleIpProtocolOptionValue() as OptionValueInformation, + Validator("config_parse_routing_policy_rule", "ROUTING_POLICY_RULE_UID_RANGE") to ConfigParseRoutingPolicyRuleUidRangeOptionValue() as OptionValueInformation, + // [Address] / [Neighbor] / [IPv6AddressLabel] table slots. + Validator("config_parse_address_section", "ADDRESS_LABEL") to ConfigParseAddressLabelOptionValue() as OptionValueInformation, + // ADDRESS_NET_LABEL's table entry is config_parse_string with CONFIG_PARSE_STRING_SAFE. + Validator("config_parse_address_section", "ADDRESS_NET_LABEL") to ConfigParseStringOptionValue() as OptionValueInformation, + // ADDRESS_NFT_SET's table entry is config_parse_nft_set with NFT_SET_PARSE_NETWORK. + Validator("config_parse_address_section", "ADDRESS_NFT_SET") to ConfigParseNftSetNetworkOptionValue() as OptionValueInformation, + Validator("config_parse_neighbor_section", "NEIGHBOR_DESTINATION_ADDRESS") to ConfigParseNeighborDestinationAddressOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_address_label_section", "IPV6_ADDRESS_LABEL_PREFIX") to ConfigParseIpv6AddressLabelPrefixOptionValue() as OptionValueInformation, + Validator("config_parse_ipv6_address_label_section", "IPV6_ADDRESS_LABEL") to ConfigParseIpv6AddressLabelOptionValue() as OptionValueInformation, // Batch 2026-10: exec Validator("config_parse_bind_paths", "0") to ConfigParseBindPathsOptionValue() as OptionValueInformation, diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt new file mode 100644 index 00000000..c05602ad --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseAddressLabelOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Address] Label= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Label= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2298-L2363 config_parse_address_section (ADDRESS_LABEL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2174-L2200 config_parse_address_label + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L832-L852 address_label_valid + */ + +/** + * Validator for `[Address] Label=` (.network) — the ADDRESS_LABEL slot, whose table entry is + * config_parse_address_label → address_label_valid(). + * + * One to 15 characters (shorter than IFNAMSIZ), each a byte in 32…126 — printable ASCII, space + * included. A longer or non-ASCII label is ignored with "Interface label is too long or invalid". + */ +class ConfigParseAddressLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_address_section", + SequenceCombinator(RegexTerminal("""[\s\S]+""", """[\x20-\x7E]{1,15}"""), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt new file mode 100644 index 00000000..c0f64684 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCanRestartUsecOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [CAN] RestartSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RestartSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-can.c#L210-L245 config_parse_can_restart_usec (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[CAN] RestartSec=` (.network). + * + * config_parse_can_restart_usec reads a time span with parse_sec() and ignores it unless it is + * `infinity` or rounds up to at most UINT32_MAX milliseconds (the kernel's IFLA_CAN_RESTART_MS is a + * u32). `0` (disable automatic restart) is accepted. Note this parser has no isempty() branch of its + * own; an empty assignment is not something this grammar is asked about. + */ +class ConfigParseCanRestartUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_can_restart_usec", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || divRoundUp(it, USEC_PER_MSEC) <= UINT32_MAX }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt new file mode 100644 index 00000000..0dd53b0c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseInAddrNonNullIpv6OptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsNull +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes + +/* + * [Network] IPv6StableSecretAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6StableSecretAddress= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2036-L2082 config_parse_in_addr_non_null (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for config_parse_in_addr_non_null with ltype AF_INET6 — `[Network] + * IPv6StableSecretAddress=` (.network). + * + * The value goes through in_addr_from_string(AF_INET6), i.e. inet_pton(AF_INET6): an IPv6 literal, no + * prefix length and no `%zone`. An IPv4 literal is not accepted. The parser then refuses the + * all-zero address (`::`, `0:0:0:0:0:0:0:0`, …) with "cannot be the ANY address", which is flagged + * here as well. + */ +class ConfigParseInAddrNonNullIpv6OptionValue : SimpleGrammarOptionValues( + "config_parse_in_addr_non_null", + SequenceCombinator( + Checked(IPV6_ADDR) { text -> ipLiteralBytes(text)?.let { !ipIsNull(it) } ?: false }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt new file mode 100644 index 00000000..258aac7d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelOptionValue.kt @@ -0,0 +1,36 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [IPv6AddressLabel] Label= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Label= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L419-L461 config_parse_ipv6_address_label_section (IPV6_ADDRESS_LABEL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L386-L417 config_parse_ipv6_address_label + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[IPv6AddressLabel] Label=` (.network): safe_atou32() — decimal, `0x` hex, `0o`/leading + * zero octal, `0b` binary — in 0…4294967294. 4294967295 (0xffffffff) is reserved and refused. The value is + * whole, so whitespace after a `0b`/`0o` prefix (`0b 1`) and a leading `\v` reach safe_atou32 and are fine. + */ +class ConfigParseIpv6AddressLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_address_label_section", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt new file mode 100644 index 00000000..f0470d82 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv6AddressLabelPrefixOptionValue.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_PREFIX_SAFE_LEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes + +/* + * [IPv6AddressLabel] Prefix= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6AddressLabel%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L419-L461 config_parse_ipv6_address_label_section (IPV6_ADDRESS_LABEL_PREFIX) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address-label.c#L346-L384 config_parse_ipv6_address_label_prefix + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L799-L843 in_addr_prefix_from_string (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L165-L171 in6_addr_is_ipv4_mapped_address + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html (kernel net/ipv6/addrlabel.c ip6addrlbl_alloc) + */ + +/** True for an IPv4-mapped address (`::ffff:a.b.c.d`) whose prefix length exceeds 96. */ +private fun isMappedTooLong(text: String): Boolean { + val slash = text.indexOf('/') + val bytes = ipLiteralBytes(if (slash >= 0) text.substring(0, slash) else text) ?: return false + val mapped = (0 until 10).all { bytes[it].toInt() == 0 } && bytes[10].toInt() == -1 && bytes[11].toInt() == -1 + // The same safe_atou8 reading as the length in IPV6_PREFIX_SAFE_LEN, so `/0b1100001` counts as 97. + val length = if (slash >= 0) { + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(text.substring(slash + 1))?.toInt() ?: return false + } else { + 128 + } + return mapped && length > 96 +} + +/** + * Validator for `[IPv6AddressLabel] Prefix=` (.network). + * + * An IPv6 prefix as in_addr_prefix_from_string(AF_INET6) reads it — an IPv6 literal with an optional + * `/LEN` in 0…128 (no slash means /128). config_parse_ipv6_address_label_prefix then refuses an + * IPv4-mapped address (`::ffff:0:0/96`) with a prefix length above 96, mirroring the kernel's + * ip6addrlbl_alloc(); note that this includes an IPv4-mapped address written without a slash. + */ +class ConfigParseIpv6AddressLabelPrefixOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv6_address_label_section", + SequenceCombinator(Checked(IPV6_PREFIX_SAFE_LEN) { !isMappedTooLong(it) }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt index cf7679ba..4b338096 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseManagedOomMemPressureDurationSecOptionValue.kt @@ -7,7 +7,7 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * Validator for ManagedOOMMemoryPressureDurationSec=. * * C function: config_parse_managed_oom_mem_pressure_duration_sec in src/core/load-fragment.c. - * Empty resets to USEC_INFINITY. Otherwise calls parse_sec(rvalue) and additionally requires + * Empty resets to NETWORK_ROUTE_ADDR_USEC_INFINITY. Otherwise calls parse_sec(rvalue) and additionally requires * the result to be at least 1s and strictly less than infinity. * * The grammar matches parse_sec syntax (which is the existing TIME_VALUE: "infinity" or a diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt new file mode 100644 index 00000000..fcac5eeb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchPropertyOptionValue.kt @@ -0,0 +1,67 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BANG +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CUNESCAPE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.unquoteWord + +/* + * [Match] Property= (.network and .link). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Property= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Property= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/net-condition.c#L344-L403 config_parse_match_property (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/env-util.c#L81-L102 env_assignment_is_valid + */ + +// env_name_is_valid_n: one or more of VALID_BASH_ENV_NAME_CHARS (letters, digits, `_`), not starting +// with a digit. +private val ENV_ASSIGNMENT = Regex("""[A-Za-z_][A-Za-z0-9_]*=[\s\S]*""") + +/** + * Validator for `[Match] Property=` in .network and .link files. + * + * config_parse_match_property strips one optional leading `!` (inverting the match) and splits the rest + * with extract_first_word(EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE): quotes group, and a backslash starts a C + * escape (`\"`, `\\`, `\n`, `\x41`, …) both inside and outside quotes — an unknown escape such as `\q`, + * or an unterminated quote, makes the whole assignment "Invalid syntax, ignoring". Each resulting word + * must then satisfy env_assignment_is_valid(): `NAME=VALUE` where NAME is letters, digits and `_` not + * starting with a digit, and VALUE is anything (including empty). A word that fails is logged and + * skipped — flagged here. + * + * The man page's own example is accepted as written: + * `Property=ID_MODEL_ID=9999 "ID_VENDOR_FROM_DATABASE=vendor name" "KEY=with \"quotation\""`. + * + * The NAME check runs on the unquoted word, so quoting the pair, quoting just the value, or even + * quoting the name are all judged exactly as systemd judges them. Lenient corners: `\x00`/`\u0000` + * (refused by cunescape_one as NUL) and UTF-16 surrogates in `\u` escapes are not singled out, and + * VALUE's UTF-8 validity / ARG_MAX length limits are not checked. + */ +class ConfigParseMatchPropertyOptionValue : SimpleGrammarOptionValues( + "config_parse_match_property", + AlternativeCombinator( + // `p += invert` and then extract_first_word() skips leading whitespace, so `! A=b` == `!A=b`. + SequenceCombinator(BANG, ZeroOrOne(WhitespaceTerminal()), PROPERTY_LIST), + // A lone `!` yields no words at all, which is accepted. + SequenceCombinator(BANG, EOF()), + PROPERTY_LIST, + ) +) { + private companion object { + val PROPERTY_WORD = Checked(CUNESCAPE_WORD) { ENV_ASSIGNMENT.matches(unquoteWord(it)) } + val PROPERTY_LIST = SequenceCombinator( + PROPERTY_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), PROPERTY_WORD)), + EOF(), + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt new file mode 100644 index 00000000..dba2916a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMatchStrvOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAIN_ESCAPE_WORD_LIST + +/* + * [Match] Path= / Driver= / Type= / Kind= (.network and .link), WLANInterfaceType= / SSID= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Path= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.link.html#Path= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/net-condition.c#L228-L281 config_parse_match_strv (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) + */ + +/** + * Validator for the glob-list `[Match]` settings: `Path=`, `Driver=`, `Type=`, `Kind=` (.network and + * .link) and `WLANInterfaceType=`, `SSID=` (.network). + * + * config_parse_match_strv strips one optional leading `!` (inverting the whole list) and then splits the + * rest with extract_first_word(EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE). Each word is an fnmatch() glob + * stored verbatim — the kind, type and driver names are open-ended (the man page itself says the lists of + * examples are "not comprehensive"), so no word is checked against a list. + * + * What *is* checked is the one thing systemd rejects: an unterminated `"` or `'` quote makes + * extract_first_word fail, and the parser then logs "Invalid syntax, ignoring" and drops the whole + * assignment. Quoted pieces may be glued to bare text (`"pci-0000 00"*`), and because of + * EXTRACT_RETAIN_ESCAPE a backslash is an ordinary character, even inside quotes. The `!` needs no + * special handling: it is an ordinary first character of the first word, and `!` on its own is accepted + * (it yields an empty list). + */ +class ConfigParseMatchStrvOptionValue : SimpleGrammarOptionValues( + "config_parse_match_strv", + RETAIN_ESCAPE_WORD_LIST, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt new file mode 100644 index 00000000..5c06ad71 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNdiscRouteMetricOptionValue.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [IPv6AcceptRA] RouteMetric= and the legacy [DHCPv6] RouteMetric= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RouteMetric= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L451-L507 config_parse_ndisc_route_metric (0, AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/systemd-networkd-tests.py#L8124 (RouteMetric=100:200:300) + */ + +// safe_atou32() on the whole value, or on one field of the `:`-separated triple. Either way whitespace +// inside the text reaches safe_atou32, which skips it at the start (`100: 200: 300`) and after a +// `0b`/`0o` prefix (`0b 1`). +private val METRIC = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// extract_many_words(…, EXTRACT_DONT_COALESCE_SEPARATORS) leaves p at "" after a single trailing `:`, +// which passes the isempty(p) check, so `100:200:300:` is accepted too. +// high < medium < low, compared on the parsed numbers. +private val METRIC_TRIPLE = Checked(SequenceCombinator(METRIC, COLON, METRIC, COLON, METRIC, ZeroOrOne(COLON))) { text -> + val v = text.removeSuffix(":").split(':').map { METRIC.valueOf(it) } + v.size == 3 && v.all { it != null } && v[0]!! < v[1]!! && v[1]!! < v[2]!! +} + +/** + * Validator for config_parse_ndisc_route_metric: `[IPv6AcceptRA] RouteMetric=` (ltype 0) and the legacy + * `[DHCPv6] RouteMetric=` alias (ltype AF_INET6; the function ignores its ltype). + * + * Either one safe_atou32() metric used for all three router preferences, or `HIGH:MEDIUM:LOW` — exactly + * three safe_atou32() fields split on `:` — which systemd only accepts when strictly increasing + * (HIGH < MEDIUM < LOW, e.g. the default `512:1024:2048`). An empty field, a fourth field or a + * non-increasing triple is ignored with a warning and flagged here. + */ +class ConfigParseNdiscRouteMetricOptionValue : SimpleGrammarOptionValues( + "config_parse_ndisc_route_metric", + SequenceCombinator(AlternativeCombinator(METRIC_TRIPLE, METRIC), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt new file mode 100644 index 00000000..33ad8a2a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborDestinationAddressOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Neighbor] Address= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BNeighbor%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-neighbor.c#L747-L781 config_parse_neighbor_section (NEIGHBOR_DESTINATION_ADDRESS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2084-L2112 config_parse_in_addr_data + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[Neighbor] Address=` (.network) — the NEIGHBOR_DESTINATION_ADDRESS slot of + * config_parse_neighbor_section, whose table entry is config_parse_in_addr_data. + * + * That reads one address of either family with in_addr_from_string_auto (inet_pton(AF_INET), then + * inet_pton(AF_INET6)): no prefix length and no `%zone`. Unlike some address settings it does not + * refuse the ANY address at parse time, so neither does this grammar. + */ +class ConfigParseNeighborDestinationAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_neighbor_section", + SequenceCombinator(IP_ADDR, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt new file mode 100644 index 00000000..dc867038 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNeighborProxyAddressOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsMulticast +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipIsNull +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ipLiteralBytes + +/* + * [Network] ProxyNeighbor= and its deprecated alias IPv6ProxyNDPAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ProxyNeighbor= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-neighbor-proxy.c#L157-L216 config_parse_neighbor_proxy_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[Network] ProxyNeighbor=` and the deprecated `IPv6ProxyNDPAddress=` (.network). + * + * config_parse_neighbor_proxy_address reads one address of either family with in_addr_from_string_auto + * (inet_pton(AF_INET) then inet_pton(AF_INET6) — no prefix length, no zone index) and then ignores it + * with a warning if it is + * - the ANY address (`0.0.0.0`, `::`), + * - multicast (224.0.0.0/4 or ff00::/8), or + * - the IPv4 limited broadcast address `255.255.255.255`, + * because the kernel would refuse such a proxy entry. All three are flagged here. + */ +class ConfigParseNeighborProxyAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_neighbor_proxy_address", + SequenceCombinator( + Checked(IP_ADDR) { text -> + val bytes = ipLiteralBytes(text) + bytes != null && !ipIsNull(bytes) && !ipIsMulticast(bytes) && !(bytes.size == 4 && bytes.all { it.toInt() == -1 }) + }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt new file mode 100644 index 00000000..7fbb1d93 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNftSetNetworkOptionValue.kt @@ -0,0 +1,131 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.OneOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * NFTSet= in [Address], [DHCPv4], [DHCPv6], [IPv6AcceptRA] and [DHCPPrefixDelegation] (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#NFTSet=source:family:table:set + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1207-L1295 config_parse_nft_set (NFT_SET_PARSE_NETWORK) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-address.c#L2298-L2363 config_parse_address_section (ADDRESS_NFT_SET → config_parse_nft_set, NFT_SET_PARSE_NETWORK) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/firewall-util.c#L1125-L1145 nfproto_table, nft_set_source_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L824-L835 nft_identifier_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (leading separators skipped L39-L56, coalescing L145-L159) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L240-L281 extract_many_words_internal + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv4-only.network + * linux https://www.netfilter.org/projects/nftables/manpage.html (NFT_NAME_MAXLEN = 256, include/uapi/linux/netfilter/nf_tables.h) + */ + +// nft_identifier_valid: a letter, then letters, digits, `/`, `\`, `_` and `.`; shorter than +// NFT_NAME_MAXLEN (256). The tuple is split with EXTRACT_CUNESCAPE, so a backslash in the file starts a C +// escape; any escaped character is accepted here rather than decoding it. +private val NFT_IDENTIFIER = RegexTerminal( + """[^\s:"']+""", + """[A-Za-z](?:[A-Za-z0-9/_.]|\\[\s\S]){0,254}""" +) + +private val NFT_SOURCES = arrayOf("address", "prefix", "ifindex") +private val NFT_FAMILIES = arrayOf("arp", "bridge", "inet", "ip", "ip6", "netdev") +private val NFT_IDENTIFIER_REGEX = Regex("""[A-Za-z](?:[A-Za-z0-9/_.]|\\[\s\S]){0,254}""") + +// extract_many_words(":") is called without EXTRACT_DONT_COALESCE_SEPARATORS, so a run of colons is one +// separator, and leading / trailing colons are skipped. +private val COLONS = OneOrMore(COLON) + +/** + * One unquoted `source:family:table:set` tuple. `source` is restricted by NFT_SET_PARSE_NETWORK to + * `address`, `prefix` or `ifindex` (the cgroup/user/group sources are for unit files). + */ +private val NFT_TUPLE = SequenceCombinator( + ZeroOrMore(COLON), + FlexibleLiteralChoiceTerminal(*NFT_SOURCES), + COLONS, + FlexibleLiteralChoiceTerminal(*NFT_FAMILIES), + COLONS, + NFT_IDENTIFIER, + COLONS, + NFT_IDENTIFIER, + ZeroOrMore(COLON), +) + +/** + * The tuple text after the first-level EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE split: quotes removed + * wherever they are, backslashes kept. + */ +private fun stripQuotesRetainEscape(text: String): String { + val sb = StringBuilder() + var quote: Char? = null + for (c in text) { + when { + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> sb.append(c) + } + } + return sb.toString() +} + +/** Whether the (already unquoted) tuple is valid: four non-empty `:`-separated fields, each acceptable. */ +private fun nftTupleValid(tuple: String): Boolean { + val fields = tuple.split(':').filter { it.isNotEmpty() } + return fields.size == 4 && + fields[0] in NFT_SOURCES && + fields[1] in NFT_FAMILIES && + NFT_IDENTIFIER_REGEX.matches(fields[2]) && + NFT_IDENTIFIER_REGEX.matches(fields[3]) +} + +// A word that contains a quote somewhere (`"address:inet:t:s"`, `address:"inet":t:s`, ...). The first +// split removes the quotes wherever they are, so the word is judged as a whole after unquoting; it is a +// single token because the quotes may cut across the tuple's fields. An unterminated quote does not +// match here and is then flagged by the unquoted branch. +private val QUOTED_NFT_WORD = Checked( + RegexTerminal( + """(?=[^\s"']*["'])(?:[^\s"']+|"[^"]*"|'[^']*')+""", + """(?=[^\s"']*["'])(?:[^\s"']+|"[^"]*"|'[^']*')+""" + ) +) { nftTupleValid(stripQuotesRetainEscape(it)) } + +// The quoted form first: it requires a quote, so it never shadows a plain tuple. +private val NFT_WORD = AlternativeCombinator( + QUOTED_NFT_WORD, + NFT_TUPLE, +) + +/** + * Validator for the .network `NFTSet=` settings: `[Address]`, `[DHCPv4]`, `[DHCPv6]`, + * `[IPv6AcceptRA]` and `[DHCPPrefixDelegation]` — config_parse_nft_set with NFT_SET_PARSE_NETWORK, + * reached either directly or through config_parse_address_section's ADDRESS_NFT_SET table slot. + * + * The value is a whitespace-separated list (split with EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE, so quotes + * anywhere in a word are removed) of `source:family:table:set` tuples, each split on `:` into exactly + * four fields (extract_many_words with EXTRACT_CUNESCAPE and without DONT_COALESCE_SEPARATORS: runs of + * `:` count as one separator and leading/trailing `:` are skipped; a fifth field is an error): + * - source: `address`, `prefix` or `ifindex`; + * - family: nfproto_table — `arp`, `bridge`, `inet`, `ip`, `ip6`, `netdev`; + * - table and set: nft_identifier_valid — a letter followed by letters, digits, `/`, `\`, `_`, `.`, + * at most 255 characters. + * Any invalid tuple aborts the rest of the assignment with a warning. + * + * Lenient / strict corners: C escapes inside a table or set name are accepted without being decoded. + * A C escape that spells part of the source or family (`addr\x65ss`) is flagged, although systemd + * would decode it; nobody writes that. + */ +class ConfigParseNftSetNetworkOptionValue : SimpleGrammarOptionValues( + "config_parse_nft_set", + SequenceCombinator( + NFT_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), NFT_WORD)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt new file mode 100644 index 00000000..867cc632 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePref64PrefixLifetimeOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6PREF64Prefix] LifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6PREF64Prefix%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1240-L1283 config_parse_pref64_prefix_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + */ + +private val EIGHT_SECONDS: BigInteger = BigInteger.valueOf(8).multiply(USEC_PER_SEC) +private val SCALED_LIFETIME_LIMIT: BigInteger = BigInteger.valueOf(1L shl 13) + +/** + * Validator for `[IPv6PREF64Prefix] LifetimeSec=` (.network). + * + * The PREF64 option (RFC 8781) carries the lifetime as a 13-bit count of 8-second units, so + * config_parse_pref64_prefix_lifetime ignores ("PREF64 lifetime is too long") `infinity` and any value + * where DIV_ROUND_UP(usec, 8s) ≥ 8192 — i.e. it accepts parse_sec() values up to 8191 × 8 = 65528 + * seconds (18h 12min 8s). `0` is accepted. + */ +class ConfigParsePref64PrefixLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_pref64_prefix_lifetime", + SequenceCombinator(timeSpan { it != NETWORK_ROUTE_ADDR_USEC_INFINITY && divRoundUp(it, EIGHT_SECONDS) < SCALED_LIFETIME_LIMIT }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt new file mode 100644 index 00000000..9fe3e29c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricAdvMssOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SIZE_IEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseSizeIec +import java.math.BigInteger + +/* + * [Route] TCPAdvertisedMaximumSegmentSize= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#TCPAdvertisedMaximumSegmentSize= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_ADVMSS) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_ADVMSS) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L221-L251 config_parse_route_metric_advmss + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-static.network + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] TCPAdvertisedMaximumSegmentSize=` (.network) — ROUTE_METRIC_ADVMSS, which + * config_parse_route_metric dispatches to config_parse_route_metric_advmss. + * + * A parse_size(…, 1024) byte count (`1460`, `1K`, `1.5K`, …) that must be in 1…UINT32_MAX. The man page + * says 1…4294967294; the C bound is `u == 0 || u > UINT32_MAX`, so 4294967295 is accepted here. + */ +class ConfigParseRouteMetricAdvMssOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + Checked(PARSE_SIZE_IEC) { text -> parseSizeIec(text)?.let { it.signum() > 0 && it <= UINT32_MAX } ?: false }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt new file mode 100644 index 00000000..c3fb87b9 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricInitCwndOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Route] InitialCongestionWindow= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#InitialCongestionWindow= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_INITCWND) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_INITCWND) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L284-L313 config_parse_tcp_window + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-static.network#L37 + */ + +/** + * Validator for `[Route] InitialCongestionWindow=` (.network) — ROUTE_METRIC_INITCWND, which + * config_parse_route_metric dispatches to config_parse_tcp_window: safe_atou32() (so `0x10`, `0o20`, + * `020` are numbers too, and so are `0b 1` and `0b+1`, since safe_atou32 sees the whole value) and then 1…1023. + * + * (`InitialAdvertisedReceiveWindow=` uses the same leaf and is already covered by + * ConfigParseRouteSectionInitRwndOptionValue.) + */ +class ConfigParseRouteMetricInitCwndOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 1023L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt new file mode 100644 index 00000000..9f3baa4c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricMtuOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SIZE_IEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.parseSizeIec +import java.math.BigInteger + +/* + * [Route] MTUBytes= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MTUBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_MTU) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_MTU) + * https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1737-L1766 config_parse_mtu (ltype 0 = AF_UNSPEC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L126-L155 parse_mtu + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024) + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] MTUBytes=` (.network) — ROUTE_METRIC_MTU, which config_parse_route_metric + * dispatches to config_parse_mtu with ltype 0 (AF_UNSPEC, so no family minimum). + * + * parse_mtu() reads the value with parse_size(…, 1024) — a byte count with optional `K`/`M`/`G`/… + * suffixes (base 1024), fractions and multi-part sums like `1K 512` — and refuses anything above + * UINT32_MAX. With AF_UNSPEC there is no lower bound, so `0` is accepted. + */ +class ConfigParseRouteMetricMtuOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator(Checked(PARSE_SIZE_IEC) { text -> parseSizeIec(text)?.let { it <= UINT32_MAX } ?: false }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt new file mode 100644 index 00000000..c843ec2c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMetricTcpRtoOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [Route] TCPRetransmissionTimeoutSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#TCPRetransmissionTimeoutSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_METRIC_RTO_MIN) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L373-L419 config_parse_route_metric (RTAX_RTO_MIN) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-metric.c#L315-L346 config_parse_route_metric_tcp_rto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.h#L106-L108 timestamp_is_set + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-route-congctl.network + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[Route] TCPRetransmissionTimeoutSec=` (.network) — ROUTE_METRIC_RTO_MIN, which + * config_parse_route_metric dispatches to config_parse_route_metric_tcp_rto. + * + * A parse_sec() time span that must be set (neither `0` nor `infinity`) and must round up to at most + * UINT32_MAX milliseconds, since RTAX_RTO_MIN is a u32 of milliseconds. + */ +class ConfigParseRouteMetricTcpRtoOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + timeSpan { it.signum() > 0 && it != NETWORK_ROUTE_ADDR_USEC_INFINITY && divRoundUp(it, USEC_PER_MSEC) <= UINT32_MAX }, + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt new file mode 100644 index 00000000..c9473999 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteMultiPathOptionValue.kt @@ -0,0 +1,85 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ALTERNATIVE_INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.WhitespaceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Route] MultiPathRoute= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MultiPathRoute= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_MULTIPATH) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-nexthop.c#L1041-L1142 config_parse_multipath_route + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex (safe_atoi, > 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L531-L551 in_addr_from_string_auto + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (weight, base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/network/test-networkd-conf.c#L352-L440 TEST(config_parse_multipath_route) + */ + +private val AT = LiteralChoiceTerminal("@") + +// After `@`: parse_ifindex() (a positive int) or, failing that, an alternative-length interface name. +// The name branch comes first because its lenient shape also matches digits but it only *validates* +// non-numeric names; under the classic matcher a number then falls through to the index branch. +// parse_ifindex() is safe_atoi() refusing anything below 1. The device is part of the first word, which +// extract_first_word() cut at whitespace, so no whitespace may follow the `@`. +private val DEVICE = AlternativeCombinator( + ALTERNATIVE_INTERFACE_NAME, + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = false, + acceptsMinus = true, + ), +) + +/** + * Validator for `[Route] MultiPathRoute=` (.network) — the ROUTE_MULTIPATH slot, whose table entry is + * config_parse_multipath_route. + * + * The first whitespace-separated word is `[GATEWAY][@DEVICE]` with at least one part present: + * - GATEWAY is an IPv4 or IPv6 literal (in_addr_from_string_auto — no prefix, no zone); + * - DEVICE, after the first `@`, is an interface index (parse_ifindex: a positive integer) or an + * interface name valid with IFNAME_VALID_ALTERNATIVE (up to 127 characters, not all digits). + * Anything after that word is the weight, read as a whole with safe_atou32() and accepted in 1…256 — + * a second word, or anything else trailing, is an error. Because the weight is the whole rest of the value, + * `@eth0 0b 1` is weight 1. + * + * Examples from systemd's own unit test: `@wg0 10`, `@1 15`, `10.0.0.1@eth0 20`, + * `2001:db8::1@wg0 15`, `192.168.1.1 100`, `fe80::1`, `@999999 10`. + */ +class ConfigParseRouteMultiPathOptionValue : SimpleGrammarOptionValues( + "config_parse_route_section", + SequenceCombinator( + AlternativeCombinator( + SequenceCombinator(IP_ADDR, ZeroOrOne(SequenceCombinator(AT, DEVICE))), + SequenceCombinator(AT, DEVICE), + ), + ZeroOrOne( + SequenceCombinator( + WhitespaceTerminal(), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 256L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt new file mode 100644 index 00000000..505dce41 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixLifetimeOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.divRoundUp +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6RoutePrefix] LifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#%5BIPv6RoutePrefix%5D%20Section%20Options + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1116-L1159 config_parse_route_prefix_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipv6ra-prefix.network#L24-L26 + */ + +private val UINT32_MAX: BigInteger = BigInteger.valueOf(4_294_967_295L) + +/** + * Validator for `[IPv6RoutePrefix] LifetimeSec=` (.network). + * + * config_parse_route_prefix_lifetime reads a time span with parse_sec() and ignores it ("Lifetime is + * too long") unless it is `infinity` or rounds up to fewer than UINT32_MAX whole seconds — the Route + * Information option carries a 32-bit seconds field in which all-ones means infinity. + */ +class ConfigParseRoutePrefixLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_route_prefix_lifetime", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || divRoundUp(it, USEC_PER_SEC) < UINT32_MAX }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt new file mode 100644 index 00000000..0853a577 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutePrefixOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_PREFIX_SAFE_LEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [IPv6RoutePrefix] Route= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Route= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1076-L1114 config_parse_route_prefix (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L799-L843 in_addr_prefix_from_string (AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L779-L797 in_addr_parse_prefixlen (safe_atou8, ≤ 128) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipv6ra-prefix.network#L24-L26 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[IPv6RoutePrefix] Route=` (.network). + * + * config_parse_route_prefix reads an IPv6 prefix with in_addr_prefix_from_string(AF_INET6): an IPv6 + * literal (inet_pton — no IPv4, no zone) and an optional `/LEN` in 0…128, LEN read with safe_atou8 so + * hex/octal spellings count too. The man page asks for the slash, but without it the C simply uses a + * /128, so that is accepted. Host bits are masked off silently, so `2001:db8::1/64` is fine. + */ +class ConfigParseRoutePrefixOptionValue : SimpleGrammarOptionValues( + "config_parse_route_prefix", + SequenceCombinator(IPV6_PREFIX_SAFE_LEN, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt new file mode 100644 index 00000000..c99d5808 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouteTableOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING + +/* + * [Route] Table= and [RoutingPolicyRule] Table= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Table= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1967-L2038 config_parse_route_section (ROUTE_TABLE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route.c#L1862-L1886 config_parse_route_table + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_TABLE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1397-L1424 config_parse_routing_policy_rule_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may look like) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[Route] Table=` and `[RoutingPolicyRule] Table=` (.network). Both table slots hand the whole + * value to manager_get_route_table_from_string(), so they use the shared [ROUTE_TABLE_FROM_STRING] grammar. + * + * That tries, in order: the predefined names `default`, `main`, `local`; the names defined by `RouteTable=` in + * networkd.conf; and finally safe_atou32(), refusing 0. The user-defined names live in a file the plugin cannot + * see, so every value that could be such a name is accepted: anything non-empty without a `:` that is not all + * digits. That includes `0x0`, `-0`, and names with whitespace such as `main table` (networkd.conf can define + * `main\ table:100`). An all-digit value must be a table number in 1…4294967295, so `0`, `00`, `08` and + * `4294967296` are flagged, as is any value containing `:`. + */ +class ConfigParseRouteTableOptionValue(parserName: String = "config_parse_route_section") : SimpleGrammarOptionValues( + parserName, + ROUTE_TABLE_FROM_STRING, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt new file mode 100644 index 00000000..7bc1c829 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterHomeAgentLifetimeOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_SEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6SendRA] HomeAgentLifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#HomeAgentLifetimeSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1583-L1624 config_parse_router_home_agent_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.h#L106-L108 timestamp_is_set + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L72 RADV_HOME_AGENT_MAX_LIFETIME_USEC + */ + +private val RADV_HOME_AGENT_MAX_LIFETIME_USEC: BigInteger = BigInteger.valueOf(65_535L).multiply(USEC_PER_SEC) + +/** + * Validator for `[IPv6SendRA] HomeAgentLifetimeSec=` (.network). + * + * config_parse_router_home_agent_lifetime reads a time span with parse_sec() and ignores it unless + * timestamp_is_set() (neither `0` nor `infinity`) and it is at most RADV_HOME_AGENT_MAX_LIFETIME_USEC = + * 65535 seconds. Sub-second values such as `500ms` pass the C check (the man page's "1…65535" is in + * whole seconds, but the comparison is on microseconds), so they are accepted here too. + */ +class ConfigParseRouterHomeAgentLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_router_home_agent_lifetime", + SequenceCombinator(timeSpan { it.signum() > 0 && it != NETWORK_ROUTE_ADDR_USEC_INFINITY && it <= RADV_HOME_AGENT_MAX_LIFETIME_USEC }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt new file mode 100644 index 00000000..396db662 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterLifetimeOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan + +/* + * [IPv6SendRA] RouterLifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RouterLifetimeSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1463-L1512 config_parse_router_lifetime (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L38-L39 RADV_MIN/MAX_ROUTER_LIFETIME_USEC + */ + +/** + * Validator for `[IPv6SendRA] RouterLifetimeSec=` (.network). + * + * config_parse_router_lifetime reads a time span with parse_sec(). Only a parse failure ignores the + * assignment: `0` is kept (the host is not a default router), and any other value outside + * RADV_MIN_ROUTER_LIFETIME_USEC…9000s — `infinity` included — is *clamped* into that range with a + * warning, not rejected. So every parse_sec() spelling is accepted here; the clamping is not flagged. + */ +class ConfigParseRouterLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_router_lifetime", + SequenceCombinator(timeSpan(), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt new file mode 100644 index 00000000..b5b0fd58 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterPrefixDelegationOptionValue.kt @@ -0,0 +1,64 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import com.intellij.codeInspection.ProblemsHolder +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] IPv6SendRA= and the deprecated [Network] IPv6PrefixDelegation= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6SendRA= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1418-L1461 config_parse_router_prefix_delegation (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1406-L1416 radv_prefix_delegation_table (DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (case-insensitive) + */ + +/** + * Validator for config_parse_router_prefix_delegation: `[Network] IPv6SendRA=` and its deprecated + * predecessor `[Network] IPv6PrefixDelegation=`. + * + * The parser branches on the key name. For `IPv6SendRA=` it takes a plain parse_boolean() value. For + * the backward-compatible `IPv6PrefixDelegation=` it uses radv_prefix_delegation_from_string(), a + * string table with boolean fallback: any boolean, or `static` / `dhcpv6`. + * + * Both keys share one (parser, ltype) registration, so the registered grammar is the union — a boolean, + * or `static` / `dhcpv6` — which is exact for `IPv6PrefixDelegation=`. For `IPv6SendRA=` + * [generateProblemDescriptors] branches on the key, as the C does, and checks against a boolean-only + * grammar, so `IPv6SendRA=static` is flagged. + * + * string_table_lookup_from_string_with_boolean() runs parse_boolean() first (STRCASE_IN_SET, so `Yes`, + * `TRUE`, `oFf` are fine), then a case-sensitive streq() lookup, so `Static` is refused. The table's + * `no` / `yes` entries are left out of the names because [BOOLEAN] already covers them in any case. + */ +class ConfigParseRouterPrefixDelegationOptionValue : SimpleGrammarOptionValues( + "config_parse_router_prefix_delegation", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("static", "dhcpv6"), + BOOLEAN, + ), + EOF() + ) +) { + + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + if (property.key == "IPv6SendRA") { + SEND_RA_BOOLEAN_ONLY.generateProblemDescriptors(property, holder) + } else { + super.generateProblemDescriptors(property, holder) + } + } + + private companion object { + // streq(lvalue, "IPv6SendRA") → parse_boolean() only. + val SEND_RA_BOOLEAN_ONLY = SimpleGrammarOptionValues( + "config_parse_router_prefix_delegation", + SequenceCombinator(BOOLEAN, EOF()) + ) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt new file mode 100644 index 00000000..3da8d2ea --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRouterUint32MsecUsecOptionValue.kt @@ -0,0 +1,36 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_ROUTE_ADDR_USEC_INFINITY +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.USEC_PER_MSEC +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.timeSpan +import java.math.BigInteger + +/* + * [IPv6SendRA] ReachableTimeSec= / RetransmitSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ReachableTimeSec= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RetransmitSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1514-L1555 config_parse_router_uint32_msec_usec (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time (via parse_sec) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/radv-internal.h#L44 RADV_MAX_UINT32_MSEC_USEC + */ + +private val RADV_MAX_UINT32_MSEC_USEC: BigInteger = BigInteger.valueOf(4_294_967_295L).multiply(USEC_PER_MSEC) + +/** + * Validator for `[IPv6SendRA] ReachableTimeSec=` and `RetransmitSec=` (.network). + * + * config_parse_router_uint32_msec_usec reads a time span with parse_sec() (default unit seconds, every + * suffix extract_multiplier() knows, `infinity`) and ignores it unless it is `infinity` or at most + * RADV_MAX_UINT32_MSEC_USEC = UINT32_MAX milliseconds (~49.7 days), since the value is sent as a 32-bit + * millisecond count. The man page documents only the 0…4294967295 ms range; the C also lets `infinity` + * through, so it is accepted here. + */ +class ConfigParseRouterUint32MsecUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_router_uint32_msec_usec", + SequenceCombinator(timeSpan { it == NETWORK_ROUTE_ADDR_USEC_INFINITY || it <= RADV_MAX_UINT32_MSEC_USEC }, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt new file mode 100644 index 00000000..8c3fe01b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValue.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [RoutingPolicyRule] FirewallMark= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#FirewallMark= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_FWMARK) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1426-L1450 config_parse_routing_policy_rule_fwmark + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1298-L1330 parse_fwmark_fwmask + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + */ + +/** + * Validator for `[RoutingPolicyRule] FirewallMark=` (.network) — the ROUTING_POLICY_RULE_FWMARK slot, + * whose table entry is config_parse_routing_policy_rule_fwmark → parse_fwmark_fwmask. + * + * `MARK` or `MARK/MASK`, split at the first `/`, each read with safe_atou32() — so decimal, `0x` hex, + * leading-zero / `0o` octal and `0b` binary are all accepted, up to 4294967295. A missing number on + * either side of the slash is an error. Both parts reach safe_atou32 with any whitespace they contain, so a + * leading `\v` or `0b 1` is fine, but whitespace before the slash (`1 /2`) is trailing junk for the mark. + */ +class ConfigParseRoutingPolicyRuleFirewallMarkOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_294_967_295L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt new file mode 100644 index 00000000..21f3a51c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValue.kt @@ -0,0 +1,77 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [RoutingPolicyRule] IPProtocol= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPProtocol= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_IP_PROTOCOL) + * https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2257-L2287 config_parse_ip_protocol (ltype 0 = not relaxed) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ip-protocol-list.c#L36-L70 parse_ip_protocol_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/generate-ip-protocol-list.sh every `#define IPPROTO_*` cpp sees for + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/meson.build#L328-L333 ... run with system_include_args, so is systemd's override + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/override/netinet/in.h#L4-L6 which includes the bundled uapi , + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/in.h#L27-L92 IPPROTO_IP ... IPPROTO_MPTCP (incl. AGGFRAG = 144) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/in6.h#L131-L139 IPPROTO_HOPOPTS ... IPPROTO_MH + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/ip-protocol-to-name.awk number -> name table (HOPOPTS skipped; 0 is "ip") + * linux https://man7.org/linux/man-pages/man5/protocols.5.html + */ + +/** + * The IPPROTO_* names the generated gperf table knows, lower-cased. The table is generated from + * systemd's own vendored uapi headers (the override includes and + * , not glibc's), so it is fixed per systemd commit. `smc` (256) and `mptcp` (262) are left + * out because config_parse_ip_protocol refuses anything above 255. + */ +private val IP_PROTOCOL_NAMES = arrayOf( + "ip", "hopopts", "icmp", "igmp", "ipip", "tcp", "egp", "pup", "udp", "idp", "tp", "dccp", "ipv6", + "routing", "fragment", "rsvp", "gre", "esp", "ah", "icmpv6", "none", "dstopts", "mtp", "beetph", + "encap", "pim", "comp", "l2tp", "mh", "sctp", "udplite", "mpls", "ethernet", "aggfrag", "raw", +) + +/** The protocol numbers ip_protocol_to_name() has a name for (and that fit in a byte). */ +private val IP_PROTOCOL_NUMBERS = setOf( + 0, 1, 2, 4, 6, 8, 12, 17, 22, 29, 33, 41, 43, 44, 46, 47, 50, 51, 58, 59, 60, 92, 94, 98, 103, 108, 115, + 132, 135, 136, 137, 143, 144, 255, +) + +// parse_ip_protocol_full reads a number with safe_atoi() and refuses a negative one with -ERANGE; +// config_parse_ip_protocol then refuses anything above UINT8_MAX. The value is whole, so `\v6` and +// `0b 110` reach safe_atoi too. +private val IP_PROTOCOL_NUMBER = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 255L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +/** + * Validator for `[RoutingPolicyRule] IPProtocol=` (.network) — the ROUTING_POLICY_RULE_IP_PROTOCOL slot, + * whose table entry is config_parse_ip_protocol with ltype 0. + * + * parse_ip_protocol_full(…, relaxed = false) accepts an IPPROTO_* name looked up case-insensitively + * (the gperf table is built with `--ignore-case`, and the value is also retried lower-cased), or a + * number read with safe_atoi() (base 0, so `0x11` works) that ip_protocol_to_name() has a name for + * (otherwise -EPROTONOSUPPORT; e.g. `3` is refused). config_parse_ip_protocol then refuses anything + * above 255, because the netlink attribute is one byte. + */ +class ConfigParseRoutingPolicyRuleIpProtocolOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal(*IP_PROTOCOL_NAMES, ignoreCase = true), + Checked(IP_PROTOCOL_NUMBER) { text -> IP_PROTOCOL_NUMBER.valueOf(text)?.toInt() in IP_PROTOCOL_NUMBERS }, + ), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt new file mode 100644 index 00000000..147a8b1c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValue.kt @@ -0,0 +1,97 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.HYPHEN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [RoutingPolicyRule] User= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#User= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1582-L1633 config_parse_routing_policy_rule (ROUTING_POLICY_RULE_UID_RANGE) + * https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-routing-policy-rule.c#L1478-L1508 config_parse_routing_policy_rule_uid_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L285-L369 get_user_creds + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L97-L135 parse_uid_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L66-L95 parse_uid (base 10, no sign, no leading zero, no whitespace) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/user-util.c#L51-L64 uid_is_valid (not 65535, not 4294967295) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-fibrule-uidrange.network + */ + +// parse_uid: safe_atou32_full(s, 10 | SAFE_ATO_REFUSE_PLUS_MINUS | SAFE_ATO_REFUSE_LEADING_ZERO | +// SAFE_ATO_REFUSE_LEADING_WHITESPACE), so strict decimal, no `+`/`-`, no leading zero (except `0` itself) and +// no leading " \t\n\r" (a leading `\v` still gets through, as in C). 4294967295 is refused by uid_is_valid(). +// The lower bound is the text before the first `-` (extract_first_word with "-"), so it cannot contain one. +private val UID_LOWER = SafeAtoTerminal( + signed = false, + base = 10, + flags = setOf( + SafeAtoTerminal.Flag.REFUSE_PLUS_MINUS, + SafeAtoTerminal.Flag.REFUSE_LEADING_ZERO, + SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE, + ), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = false, +) + +// The upper bound is the rest of the value after that `-`. +private val UID_UPPER = SafeAtoTerminal( + signed = false, + base = 10, + flags = setOf( + SafeAtoTerminal.Flag.REFUSE_PLUS_MINUS, + SafeAtoTerminal.Flag.REFUSE_LEADING_ZERO, + SafeAtoTerminal.Flag.REFUSE_LEADING_WHITESPACE, + ), + minInclusive = 0L, + maxInclusive = 4_294_967_294L, + acceptsWhitespace = true, + acceptsMinus = true, +) + +// uid_is_valid() also refuses the 16-bit -1, 65535. +private val UID_RANGE = Checked( + SequenceCombinator( + Checked(UID_LOWER) { UID_LOWER.valueOf(it)?.toLong() != 65_535L }, + ZeroOrOne(SequenceCombinator(HYPHEN, Checked(UID_UPPER) { UID_UPPER.valueOf(it)?.toLong() != 65_535L })), + ) +) { text -> + val dash = text.indexOf('-') + val lo = if (dash >= 0) UID_LOWER.valueOf(text.substring(0, dash)) else null + val hi = if (dash >= 0) UID_UPPER.valueOf(text.substring(dash + 1)) else null + dash < 0 || lo == null || hi == null || lo <= hi +} + +// A user name for getpwnam(): NSS is the judge, so anything without whitespace or `:` is accepted, as +// long as it does not look like a UID or UID range (those are judged by the numeric branch). The lenient +// shape stops only at systemd's WHITESPACE (" \t\n\r"), so under the classic engine a range such as +// `5-\v6` (a `\v` that parse_uid lets through) is not cut short at the `\v` and falls through to UID_RANGE. +private val USER_NAME = RegexTerminal("""[^ \t\n\r]+""", """(?![0-9][0-9-]*(?:\s|$))[^\s:]+""") + +/** + * Validator for `[RoutingPolicyRule] User=` (.network) — the ROUTING_POLICY_RULE_UID_RANGE slot. + * + * config_parse_routing_policy_rule_uid_range first tries get_user_creds() (a user name, or a numeric UID + * that exists in the user database), and falls back to parse_uid_range(): `UID` or `UID-UID`, each + * read by parse_uid() — decimal only, no sign, no leading zeros, no whitespace, and not one of the + * invalid UIDs 65535 / 4294967295 — with the lower bound not above the upper. + * + * User names can only be checked at runtime, so any token that is not purely digits and dashes is + * accepted as a possible name. A digits-and-dashes token is judged strictly as a UID or UID range. + */ +class ConfigParseRoutingPolicyRuleUidRangeOptionValue : SimpleGrammarOptionValues( + "config_parse_routing_policy_rule", + SequenceCombinator( + // Name first: its lenient shape also covers numbers, but it only validates non-numeric names, so + // the classic matcher falls through to the numeric branch for those. + AlternativeCombinator(USER_NAME, UID_RANGE), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt new file mode 100644 index 00000000..49b7cac8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStackedNetdevOptionValue.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Checked +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.INTERFACE_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.plainNameList + +/* + * [Network] IPoIB= / IPVLAN= / IPVTAP= / MACsec= / MACVLAN= / MACVTAP= / Tunnel= / VLAN= / VXLAN= / + * Xfrm= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPoIB= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L945-L1008 config_parse_stacked_netdev (NETDEV_KIND_IPOIB, …_IPVLAN, …_IPVTAP, …_MACSEC, …_MACVLAN, …_MACVTAP, …_VLAN, …_VXLAN, …_XFRM, _NETDEV_KIND_TUNNEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (flags 0, via ifname_valid) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex (safe_atoi base 0, > 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-macvlan.network#L7 (MACVLAN=macvlan99) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipvlan.network#L7 (IPVLAN=ipvlan99) + */ + +// ifname_valid() first refuses anything parse_ifindex() accepts: safe_atoi() with base 0 (so hex, `0b`, +// `0o` and a leading `+` count) giving a value in 1…INT_MAX. INTERFACE_NAME only refuses all-digit names. +// The name is a whole word handed to safe_atoi, so leading whitespace that survived the split (`\v`) is +// skipped as C does. +private val NOT_AN_IFINDEX_NAME = Checked(INTERFACE_NAME) { name -> + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(name) == null +} + +/** + * Validator for the stacked-netdev settings of a .network `[Network]` section: `IPoIB=`, `IPVLAN=`, + * `IPVTAP=`, `MACsec=`, `MACVLAN=`, `MACVTAP=`, `Tunnel=`, `VLAN=`, `VXLAN=` and `Xfrm=`. All ten use + * config_parse_stacked_netdev; the ltype only records which netdev kind is expected, so they share a + * grammar. + * + * The value is a whitespace-separated list (extract_first_word with no flags: no quote handling) of + * netdev names, each checked with ifname_valid(): 1–15 printable ASCII characters other than `:`, `/` + * and `%`, not all digits, not something parse_ifindex() reads as a positive ifindex (`0x10`, `+5`, + * `0b1`), and not `.`, `..`, `all` or `default`. An invalid name is logged and skipped; + * it is flagged here. An unmatched backslash at the very end of the value would also make + * extract_first_word fail, but is not singled out. + */ +class ConfigParseStackedNetdevOptionValue : SimpleGrammarOptionValues( + "config_parse_stacked_netdev", + plainNameList(NOT_AN_IFINDEX_NAME), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt new file mode 100644 index 00000000..cb64c294 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStringSafeAsciiOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPServer] BootFilename=, [MobileNetwork] APN= / OperatorId= / User= / Password= / PIN= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#BootFilename= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#APN= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1253-L1300 config_parse_string (CONFIG_PARSE_STRING_SAFE_AND_ASCII) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-util.c#L1154-L1198 string_is_safe (STRING_ALLOW_GLOBS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/utf8.h#L17 ascii_is_valid + */ + +/** + * Validator for config_parse_string with CONFIG_PARSE_STRING_SAFE_AND_ASCII + * (= CONFIG_PARSE_STRING_SAFE | CONFIG_PARSE_STRING_ASCII): `[DHCPServer] BootFilename=` and the + * `[MobileNetwork]` `APN=`, `OperatorId=`, `User=`, `Password=` and `PIN=` settings of .network files. + * + * Both checks run on the raw value. string_is_safe(…, STRING_ALLOW_GLOBS) refuses control characters + * (0x01–0x1F, 0x7F), backslashes and the quote characters `"` and `'` (globs `*?[` are fine); and + * ascii_is_valid() refuses any byte of 0x80 or above. Either failure logs a warning and ignores the + * assignment. What remains is printable ASCII, space included, minus `"`, `'` and `\`. + */ +class ConfigParseStringSafeAsciiOptionValue : SimpleGrammarOptionValues( + "config_parse_string", + SequenceCombinator( + RegexTerminal("""[\s\S]+""", """[\x20-\x7E&&[^"'\\]]+"""), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt new file mode 100644 index 00000000..942eeb56 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseStrvOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RETAIN_ESCAPE_WORD_LIST + +/* + * [Network] BindCarrier= (.network), [Exec] Parameters= / Environment= (.nspawn). + * (logind.conf's KillOnlyUsers= / KillExcludeUsers= share the parser but are not a file type the plugin + * handles.) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#BindCarrier= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Parameters= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#Environment= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1416-L1460 config_parse_strv (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) + */ + +/** + * Validator for config_parse_strv with ltype 0: `[Network] BindCarrier=` in .network files and + * `[Exec] Parameters=` / `Environment=` in .nspawn files. + * + * config_parse_strv splits the value with extract_first_word(EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) and + * appends every word verbatim; it validates nothing about the words themselves (BindCarrier= globs are + * matched later, nspawn's Environment= entries are not even checked for an `=`). The only input it + * refuses is one extract_first_word cannot split — an unterminated `"` or `'` quote — and for that it + * logs a parse error and ignores the assignment. That is all this grammar checks; see + * [RETAIN_ESCAPE_WORD_LIST] for the word shape. + */ +class ConfigParseStrvOptionValue : SimpleGrammarOptionValues( + "config_parse_strv", + RETAIN_ESCAPE_WORD_LIST, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt new file mode 100644 index 00000000..2cd08bb1 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTimezoneOptionValue.kt @@ -0,0 +1,38 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * [DHCPServer] Timezone= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Timezone= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L2222-L2255 config_parse_timezone (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1596-L1655 verify_timezone + * linux https://man7.org/linux/man-pages/man5/tzfile.5.html + */ + +// One path component of a zoneinfo name: verify_timezone allows ASCII letters, digits, `-`, `_`, `+`. +private val TZ_COMPONENT = RegexTerminal("""[^/]+""", """[A-Za-z0-9_+-]+""") + +/** + * Validator for `[DHCPServer] Timezone=` (.network). + * + * config_parse_timezone calls verify_timezone(), which accepts `UTC` unconditionally and otherwise + * requires a relative zoneinfo path: components of ASCII letters, digits, `-`, `_` and `+`, separated by + * single `/`, with no leading or trailing `/` (so no `.` or `..` components either). It then opens + * `/usr/share/zoneinfo/` and checks the TZif magic — that runtime check cannot be done here, so + * a well-formed but non-existent zone such as `Mars/Olympus_Mons` is accepted. + */ +class ConfigParseTimezoneOptionValue : SimpleGrammarOptionValues( + "config_parse_timezone", + SequenceCombinator( + TZ_COMPONENT, + ZeroOrMore(SequenceCombinator(CIDR_SEPARATOR, TZ_COMPONENT)), + EOF() + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt index 1aa294f2..a73d2188 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/RoutingPolicyRuleOptionValues.kt @@ -33,9 +33,10 @@ import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gram * actually reads the value. The classes below mirror that table entry by entry, so each one is * registered under the matching ROUTING_POLICY_RULE_* ltype. * - * Not covered here: ROUTING_POLICY_RULE_TABLE (route table names are user-defined via RouteTable= in - * networkd.conf, so there is no closed set to check), ROUTING_POLICY_RULE_FWMARK and - * ROUTING_POLICY_RULE_IP_PROTOCOL. + * Covered in their own files instead: ROUTING_POLICY_RULE_TABLE (ConfigParseRouteTableOptionValue), + * ROUTING_POLICY_RULE_FWMARK (ConfigParseRoutingPolicyRuleFirewallMarkOptionValue), + * ROUTING_POLICY_RULE_IP_PROTOCOL (ConfigParseRoutingPolicyRuleIpProtocolOptionValue) and + * ROUTING_POLICY_RULE_UID_RANGE (ConfigParseRoutingPolicyRuleUidRangeOptionValue). */ private const val RULE = "config_parse_routing_policy_rule" diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt new file mode 100644 index 00000000..48154b00 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkRouteAddrCombinators.kt @@ -0,0 +1,365 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigInteger + +/* + * Shared helpers for the network-route-addr validator batch (2026-10): [Route], [RoutingPolicyRule], + * [Address], [Neighbor], [IPv6SendRA] / [IPv6Prefix] / [IPv6RoutePrefix] / [IPv6PREF64Prefix] and the + * generic strv / string / match parsers. + * + * Everything here is pinned to systemd 0f0682b773, the commit recorded in + * systemd-build/build/last_commit_hash. Browse it at https://github.com/systemd/systemd/blob/0f0682b773. + * + * Kept in its own file (rather than Combinators.kt) because several validator batches were written in + * parallel; the candidates for promotion are listed in the batch RECOMMENDATIONS.md. + */ + +// --------------------------------------------------------------------------------------------------- +// Checked — a value-level check on top of a structural grammar. + +/** + * Matches exactly what [inner] matches, but additionally requires [predicate] to hold for the text it + * consumed. + * + * Use it when systemd's acceptance depends on the *value* a well-formed span denotes rather than on its + * shape — a time span that has to fit in a uint32 of milliseconds, a metric triple that has to be + * strictly increasing, an address that may not be the ANY address. The structural grammar still drives + * tokenization and highlighting; only validity changes. + * + * When the predicate fails the first token of the span is reported as the bad one (under both engines), + * since there is no single token to blame for a property of the whole span. + */ +class Checked(private val inner: Combinator, private val predicate: (String) -> Boolean) : Combinator { + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val match = inner.SemanticMatch(value, offset) + if (match.matchResult == -1) return match + if (!predicate(value.substring(offset, match.matchResult))) return NoMatch.copy(longestMatch = offset) + return match + } + + override fun parse(value: String, offset: Int): Sequence = + inner.parse(value, offset).map { step -> + // The predicate only runs on spans whose tokens are all individually valid, so predicates may assume + // well-formed input (e.g. a digit run that fits its range) and need not be total. + if (step is Parse && step.tokens.isNotEmpty() && step.end > offset && step.tokens.all { it.valid } && + !predicate(value.substring(offset, step.end))) { + step.copy(tokens = listOf(step.tokens[0].copy(valid = false)) + step.tokens.drop(1)) + } else { + step + } + } + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "Checked(\n" + inner.toStringIndented(indent + 1) + "\n" + " ".repeat(indent) + ")" + + override fun toString(): String = toStringIndented(0) +} + + +// --------------------------------------------------------------------------------------------------- +// parse_sec() — parse_time(t, &usec, USEC_PER_SEC) in src/basic/time-util.c. +// +// The grammar mirrors parse_time()'s loop: a run of `NUMBER [whitespace] [UNIT]` elements, where +// - NUMBER is strtoll() base 10 (so an optional `+`) with an optional `.fraction`; `.5` is fine, `5.` +// is not ("Don't allow … 3.sec"); +// - elements may abut (`1h30min`, `5s+3s`) or be separated by whitespace; +// - a number with no unit defaults to seconds and must then be followed by whitespace or the end — +// extract_multiplier() refuses `12.34.56` but accepts `12.34 .56`; +// - `infinity` stands alone. +// Units are matched by a FlexibleLiteralChoiceTerminal: a unit is always followed by a digit, a sign, +// whitespace or the end, so the terminal's greedy letter run lines up with extract_multiplier()'s +// longest-prefix-first table and a misspelt unit is highlighted as one token. + +/** extract_multiplier()'s table, in microseconds. */ +private val TIME_UNIT_USEC: Map = run { + val sec = BigInteger.valueOf(1_000_000L) + val min = sec.multiply(BigInteger.valueOf(60)) + val hour = min.multiply(BigInteger.valueOf(60)) + val day = hour.multiply(BigInteger.valueOf(24)) + mapOf( + "seconds" to sec, "second" to sec, "sec" to sec, "s" to sec, + "minutes" to min, "minute" to min, "min" to min, + "months" to sec.multiply(BigInteger.valueOf(2_629_800L)), "month" to sec.multiply(BigInteger.valueOf(2_629_800L)), + "M" to sec.multiply(BigInteger.valueOf(2_629_800L)), + "msec" to BigInteger.valueOf(1000), "ms" to BigInteger.valueOf(1000), + "m" to min, + "hours" to hour, "hour" to hour, "hr" to hour, "h" to hour, + "days" to day, "day" to day, "d" to day, + "weeks" to day.multiply(BigInteger.valueOf(7)), "week" to day.multiply(BigInteger.valueOf(7)), "w" to day.multiply(BigInteger.valueOf(7)), + "years" to sec.multiply(BigInteger.valueOf(31_557_600L)), "year" to sec.multiply(BigInteger.valueOf(31_557_600L)), + "y" to sec.multiply(BigInteger.valueOf(31_557_600L)), + "usec" to BigInteger.ONE, "us" to BigInteger.ONE, "μs" to BigInteger.ONE, "µs" to BigInteger.ONE, + ) +} + +/** NETWORK_ROUTE_ADDR_USEC_INFINITY, i.e. UINT64_MAX. */ +val NETWORK_ROUTE_ADDR_USEC_INFINITY: BigInteger = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE) +val USEC_PER_SEC: BigInteger = BigInteger.valueOf(1_000_000L) +val USEC_PER_MSEC: BigInteger = BigInteger.valueOf(1_000L) + +// `+.5` is refused: strtoll() reads no digits after the sign, so e == p and *e is not '.'. +private val TIME_NUMBER = RegexTerminal("""[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]*)""", """(?:\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+)""") +private val TIME_UNIT = FlexibleLiteralChoiceTerminal(*TIME_UNIT_USEC.keys.toTypedArray()) +private val TIME_INFINITY = FlexibleLiteralChoiceTerminal("infinity") + +private val TIME_ELEMENT = AlternativeCombinator( + SequenceCombinator(TIME_NUMBER, ZeroOrOne(WhitespaceTerminal()), TIME_UNIT), + SequenceCombinator(TIME_NUMBER, AlternativeCombinator(WhitespaceTerminal(), EOF())), +) + +/** Every spelling parse_sec() accepts, structurally; not range-checked. Not followed by EOF. */ +val NETWORK_ROUTE_ADDR_PARSE_SEC = AlternativeCombinator( + TIME_INFINITY, + OneOrMore(SequenceCombinator(TIME_ELEMENT, ZeroOrOne(WhitespaceTerminal()))), +) + +private val TIME_ELEMENT_REGEX = Regex("""\s*(?:\+(?=[0-9]))?([0-9]*)(?:\.([0-9]+))?\s*(seconds|second|sec|s|minutes|minute|min|months|month|M|msec|ms|m|hours|hour|hr|h|days|day|d|weeks|week|w|years|year|y|usec|us|μs|µs)?""") + +/** + * The value parse_sec() gives [text] in microseconds — [NETWORK_ROUTE_ADDR_USEC_INFINITY] for `infinity` — or null if + * parse_sec() would fail (including its -ERANGE overflow checks). [text] is expected to be something + * [NETWORK_ROUTE_ADDR_PARSE_SEC] matched. + */ +fun parseSecUsec(text: String): BigInteger? { + val t = text.trim() + if (t == "infinity") return NETWORK_ROUTE_ADDR_USEC_INFINITY + var pos = 0 + var usec = BigInteger.ZERO + var something = false + while (pos < t.length) { + val m = TIME_ELEMENT_REGEX.matchAt(t, pos) ?: return null + if (m.value.isEmpty()) return null + val whole = m.groupValues[1] + val frac = m.groupValues[2] + if (whole.isEmpty() && frac.isEmpty()) return null + val multiplier = TIME_UNIT_USEC[m.groupValues[3]] ?: USEC_PER_SEC + val l = if (whole.isEmpty()) BigInteger.ZERO else BigInteger(whole) + if (l >= NETWORK_ROUTE_ADDR_USEC_INFINITY.divide(multiplier)) return null + usec = usec.add(l.multiply(multiplier)) + var scale = multiplier.divide(BigInteger.TEN) + for (c in frac) { + usec = usec.add(BigInteger.valueOf((c - '0').toLong()).multiply(scale)) + scale = scale.divide(BigInteger.TEN) + } + if (usec >= NETWORK_ROUTE_ADDR_USEC_INFINITY) return null + something = true + pos = m.range.last + 1 + } + return if (something) usec else null +} + +/** + * A parse_sec() value whose microsecond count must satisfy [accept] (which receives [NETWORK_ROUTE_ADDR_USEC_INFINITY] for + * `infinity`). Not followed by EOF. + */ +fun timeSpan(accept: (BigInteger) -> Boolean = { true }): Combinator = + Checked(NETWORK_ROUTE_ADDR_PARSE_SEC) { text -> parseSecUsec(text)?.let(accept) ?: false } + +/** DIV_ROUND_UP(a, b) for non-negative a. */ +fun divRoundUp(a: BigInteger, b: BigInteger): BigInteger = a.add(b).subtract(BigInteger.ONE).divide(b) + + +// --------------------------------------------------------------------------------------------------- +// parse_size(t, 1024, …) — src/basic/parse-util.c. +// +// A run of `NUMBER [whitespace] [SUFFIX]` elements where each suffix must be strictly smaller than the +// previous one (start_pos), a bare number (the "" suffix, bytes) ends the run, and NUMBER is strtoull() +// base 10 with an optional fraction (`10.M` is accepted: "Ignore failure"). + +private val SIZE_NUMBER = RegexTerminal("""[+-]?[0-9]+(?:\.[0-9]*)?""", """\+?[0-9]+(?:\.[0-9]*)?""") +private val SIZE_SUFFIX = LiteralChoiceTerminal("E", "P", "T", "G", "M", "K", "B") +private val SIZE_ELEMENT = SequenceCombinator(SIZE_NUMBER, ZeroOrOne(WhitespaceTerminal()), ZeroOrOne(SIZE_SUFFIX)) + +/** Every spelling parse_size() (base 1024) accepts, structurally. Not followed by EOF. */ +val PARSE_SIZE_IEC = SequenceCombinator(SIZE_ELEMENT, ZeroOrMore(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), SIZE_ELEMENT))) + +private val SIZE_ELEMENT_REGEX = Regex("""\s*\+?([0-9]+)(?:\.([0-9]*))?\s*([EPTGMKB]?)""") +private val SIZE_ORDER = listOf("E", "P", "T", "G", "M", "K", "B", "") + +/** The byte count parse_size(text, 1024) yields, or null if it would fail. */ +fun parseSizeIec(text: String): BigInteger? { + val t = text.trim() + var pos = 0 + var total = BigInteger.ZERO + var startPos = 0 + val max = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE) + if (t.isEmpty()) return null + while (pos < t.length) { + val m = SIZE_ELEMENT_REGEX.matchAt(t, pos) ?: return null + val suffix = m.groupValues[3] + val index = SIZE_ORDER.indexOf(suffix) + if (index < startPos) return null + val factor = BigInteger.valueOf(1024).pow(6 - minOf(index, 6)) + val whole = BigInteger(m.groupValues[1]) + val fracDigits = m.groupValues[2] + var bytes = whole.multiply(factor) + if (fracDigits.isNotEmpty()) { + bytes = bytes.add(factor.multiply(BigInteger(fracDigits)).divide(BigInteger.TEN.pow(fracDigits.length))) + } + total = total.add(bytes) + if (total > max) return null + startPos = index + 1 + pos = m.range.last + 1 + if (m.value.isEmpty()) return null + } + return total +} + + +// --------------------------------------------------------------------------------------------------- +// extract_first_word() word lists (src/basic/extract-word.c). + +/** + * One word as extract_first_word(…, EXTRACT_UNQUOTE|EXTRACT_RETAIN_ESCAPE) reads it: runs of bare + * characters and `"…"` / `'…'` quoted pieces, glued together (`a"b c"d` is the single word `ab cd`). + * With EXTRACT_RETAIN_ESCAPE a backslash is an ordinary character, even inside quotes, so the only way + * such a word can fail is an unterminated quote. The quoted pieces match leniently without their + * closing quote so the unterminated piece is the token highlighted. + */ +val RETAIN_ESCAPE_WORD: Combinator = OneOrMore( + AlternativeCombinator( + RegexTerminal("\"[^\"]*\"?", "\"[^\"]*\""), + RegexTerminal("'[^']*'?", "'[^']*'"), + RegexTerminal("[^\\s\"']+", "[^\\s\"']+"), + ) +) + +/** A whitespace-separated list of [RETAIN_ESCAPE_WORD]s, followed by EOF. */ +val RETAIN_ESCAPE_WORD_LIST: Combinator = SequenceCombinator( + RETAIN_ESCAPE_WORD, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), RETAIN_ESCAPE_WORD)), + EOF(), +) + +// The C escapes cunescape_one() understands (src/basic/escape.c). NUL and surrogate code points, which +// it refuses, are not singled out. +private const val C_ESCAPE = """\\(?:[abfnrtv\\"'s]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-3][0-7]{2})""" + +/** + * One word as extract_first_word(…, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) reads it: like + * [RETAIN_ESCAPE_WORD], but a backslash starts a C escape both inside and outside quotes, and an + * unknown escape is an error. + */ +val CUNESCAPE_WORD: Combinator = OneOrMore( + AlternativeCombinator( + RegexTerminal("\"(?:[^\"\\\\]|\\\\[\\s\\S])*\"?", "\"(?:[^\"\\\\]|$C_ESCAPE)*\""), + RegexTerminal("'(?:[^'\\\\]|\\\\[\\s\\S])*'?", "'(?:[^'\\\\]|$C_ESCAPE)*'"), + RegexTerminal("(?:[^\\s\"'\\\\]|\\\\[\\s\\S])+", "(?:[^\\s\"'\\\\]|$C_ESCAPE)+"), + ) +) + +/** + * [text] with its quotes removed and its escapes collapsed to the escaped character — enough to look at + * the word's structure (where its `=` is, what precedes it). Escapes are not decoded faithfully. + */ +fun unquoteWord(text: String): String { + val sb = StringBuilder() + var quote: Char? = null + var i = 0 + while (i < text.length) { + val c = text[i] + when { + c == '\\' && i + 1 < text.length -> { + sb.append(text[i + 1]); i += 2; continue + } + quote != null && c == quote -> quote = null + quote == null && (c == '"' || c == '\'') -> quote = c + else -> sb.append(c) + } + i++ + } + return sb.toString() +} + +/** + * A whitespace-separated list of interface names as extract_first_word(…, 0) splits it — no quote + * handling, so a name is simply a run of non-whitespace characters — each checked with [name]. + */ +fun plainNameList(name: Combinator): Combinator = SequenceCombinator( + name, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), name)), + EOF(), +) + + +// --------------------------------------------------------------------------------------------------- +// Address literals, as bytes — for the value checks inet_pton() parsing alone cannot express. + +/** The 4 or 16 bytes an IPv4 / IPv6 literal (as matched by [IP_ADDR]) denotes, or null. */ +fun ipLiteralBytes(text: String): ByteArray? = if (':' in text) ipv6Bytes(text) else ipv4Bytes(text) + +private fun ipv4Bytes(text: String): ByteArray? { + val parts = text.split('.') + if (parts.size != 4) return null + val out = ByteArray(4) + for ((i, p) in parts.withIndex()) { + val v = p.toIntOrNull() ?: return null + if (v !in 0..255) return null + out[i] = v.toByte() + } + return out +} + +private fun ipv6Bytes(text: String): ByteArray? { + var s = text + var tail = ByteArray(0) + // An embedded dotted-quad supplies the last two groups. + val lastColon = s.lastIndexOf(':') + if ('.' in s.substring(lastColon + 1)) { + tail = ipv4Bytes(s.substring(lastColon + 1)) ?: return null + s = s.substring(0, lastColon + 1) + "0:0" + } + val dc = s.indexOf("::") + val groups: List = if (dc >= 0) { + val head = s.substring(0, dc).split(':').filter { it.isNotEmpty() } + val rest = s.substring(dc + 2).split(':').filter { it.isNotEmpty() } + val missing = 8 - head.size - rest.size + if (missing < 1) return null + head + List(missing) { "0" } + rest + } else { + s.split(':') + } + if (groups.size != 8) return null + val out = ByteArray(16) + for ((i, g) in groups.withIndex()) { + if (g.isEmpty() || g.length > 4) return null + val v = g.toIntOrNull(16) ?: return null + out[2 * i] = (v shr 8).toByte() + out[2 * i + 1] = v.toByte() + } + if (tail.isNotEmpty()) tail.copyInto(out, 12) + return out +} + +/** in_addr_is_null(): every byte zero. */ +fun ipIsNull(bytes: ByteArray): Boolean = bytes.all { it.toInt() == 0 } + +/** in_addr_is_multicast(): 224.0.0.0/4 for IPv4, ff00::/8 for IPv6. */ +fun ipIsMulticast(bytes: ByteArray): Boolean = + if (bytes.size == 4) (bytes[0].toInt() and 0xF0) == 0xE0 else (bytes[0].toInt() and 0xFF) == 0xFF + + +// --------------------------------------------------------------------------------------------------- +// IPv6 prefixes — in_addr_prefix_from_string(…, AF_INET6, …) (src/basic/in-addr-util.c): an IPv6 literal +// and an optional `/LEN`, where LEN goes through in_addr_parse_prefixlen → safe_atou8 (base 0) and must +// be ≤ 128. The length is everything after the `/`, so whitespace reaches safe_atou8 (`::/ 64` and +// `::/0b 1` are fine). Without a slash the prefix length is 128. Not followed by EOF. +val IPV6_PREFIX_SAFE_LEN: Combinator = SequenceCombinator( + IPV6_ADDR, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 128L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ) + ), +) diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt new file mode 100644 index 00000000..ed63fe98 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressLabelOptionValueTest.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Address] Label= (ADDRESS_LABEL). */ +class ConfigParseAddressLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Address] + Label=eth0:1 + Label=with space + Label=123456789012345 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + Label=1234567890123456 + Label=café + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt new file mode 100644 index 00000000..b82bc4a3 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseAddressNetLabelOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Address] NetLabel= (ADDRESS_NET_LABEL -> config_parse_string(CONFIG_PARSE_STRING_SAFE)). */ +class ConfigParseAddressNetLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // CONFIG_PARSE_STRING_SAFE uses STRING_ALLOW_GLOBS and does not require ASCII, so globs and UTF-8 are fine + // (systemd oracle check 2026-10-24). + // language="unit file (systemd)" + val file = """ + [Address] + NetLabel=system_u:object_r:root_t:s0 + NetLabel=unconfined_u:object_r:* + NetLabel=foo*? + NetLabel=café + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // systemd logs these at warning level, but it drops the value and with it the whole [Address] section + // (config_section_parse returns 0, so address_unref_or_set_invalidp marks the section invalid). + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + NetLabel="system_u:object_r:root_t:s0" + NetLabel=back\\slash + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt new file mode 100644 index 00000000..6fe8125e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCanRestartUsecOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_can_restart_usec. */ +class ConfigParseCanRestartUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [CAN] + RestartSec=0 + RestartSec=100ms + RestartSec=1.5s + RestartSec=infinity + RestartSec=49d + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [CAN] + RestartSec=50d + RestartSec=never + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt new file mode 100644 index 00000000..30321237 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseInAddrNonNullIpv6OptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_in_addr_non_null(AF_INET6). */ +class ConfigParseInAddrNonNullIpv6OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv6StableSecretAddress=2001:db8::1 + IPv6StableSecretAddress=::1 + IPv6StableSecretAddress=fe80::1234:5678 + IPv6StableSecretAddress=::ffff:192.168.1.1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + IPv6StableSecretAddress=:: + IPv6StableSecretAddress=0:0:0:0:0:0:0:0 + IPv6StableSecretAddress=192.168.1.1 + IPv6StableSecretAddress=2001:db8::/64 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt new file mode 100644 index 00000000..dec188ef --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [IPv6AddressLabel] Label=. */ +class ConfigParseIpv6AddressLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Label=0 + Label=100 + Label=4294967294 + Label=0xfffffffe + Label=0b 1 + Label=0b+1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1 and -0b0 (EINVAL) and accepts 0b 1 and 0b+1. + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Label=4294967295 + Label=0xffffffff + Label=-1 + Label=label + Label=+0b1 + Label=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt new file mode 100644 index 00000000..52452cc4 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv6AddressLabelPrefixOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [IPv6AddressLabel] Prefix=. */ +class ConfigParseIpv6AddressLabelPrefixOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Prefix=2001:db8::/32 + Prefix=::/0 + Prefix=::ffff:0:0/96 + Prefix=fe80::1 + Prefix=2001:db8::/ 32 + Prefix=::/0b 1 + Prefix=::ffff:0:0/0b1100000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects ::/+0b1 (EINVAL) and ::ffff:0:0/0b1100001 (97 > 96), and accepts ::/0b 1. + // language="unit file (systemd)" + val file = """ + [IPv6AddressLabel] + Prefix=::ffff:0:0/97 + Prefix=::ffff:1.2.3.4 + Prefix=10.0.0.0/8 + Prefix=2001:db8::/200 + Prefix=::/+0b1 + Prefix=::ffff:0:0/0b1100001 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt new file mode 100644 index 00000000..31e08f81 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchPropertyOptionValueTest.kt @@ -0,0 +1,76 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_match_property: NAME=VALUE words with CUNESCAPE|UNQUOTE. */ +class ConfigParseMatchPropertyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Match] + Property=ID_MODEL_ID=9999 "ID_VENDOR_FROM_DATABASE=vendor name" "KEY=with \"quotation\"" + Property=HOGE=foo BAR=baz + Property=ID_NET_MANAGED_BY=io.systemd.Network + Property=!ID_BUS=usb + Property=! ID_BUS=usb + Property=! + Property=EMPTY= + Property=KEY="value in quotes" + Property=_UNDERSCORE1=x\x41 + Property=GLOB=foo* + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Property=NOEQUALS + Property=1BAD=x + Property=BAD-NAME=x + Property="KEY=unterminated + Property=KEY=bad\qescape + Property=GOOD=1 !BAD=2 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } + @Test + fun testValidValuesLink() { + // language="unit file (systemd)" + val file = """ + [Match] + Property=ID_PATH=pci-0000:00:1f.6 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesLink() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Property==value + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt new file mode 100644 index 00000000..9d58d26d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMatchStrvOptionValueTest.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_match_strv: glob lists with only quote balance checked. */ +class ConfigParseMatchStrvOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Match] + Path=pci-0000:02:00.0-* + Driver=dummy + Driver=!e1000e igb + Type=ether wlan + Type=!loopback + Kind=dummy veth + Kind=! + WLANInterfaceType=ap station + SSID="My Home Wifi" 'Other Net' + SSID=Cafe\ Wifi + SSID=pre"fix mid"dle + Path= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + SSID="unterminated + Kind=bond 'veth + Driver=a"b + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } + @Test + fun testValidValuesLink() { + // language="unit file (systemd)" + val file = """ + [Match] + Path=pci-0000:00:1f.6 + Driver=e1000e + Type=ether + Kind=!* + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesLink() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Match] + Path="pci-0000 + """.trimIndent() + + setupFileInEditor("file.link", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt new file mode 100644 index 00000000..67dcf434 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNdiscRouteMetricOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_ndisc_route_metric. */ +class ConfigParseNdiscRouteMetricOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6AcceptRA] + RouteMetric=256 + RouteMetric=512:1024:2048 + RouteMetric=100:200:300 + RouteMetric=0 + RouteMetric=0x10:0x20:0x30 + RouteMetric=100: 200: 300 + RouteMetric=100:200:300: + RouteMetric=0b 1 + RouteMetric=0b1:0b 10:0b11 + [DHCPv6] + RouteMetric=1024 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1 and 1:+0b10:3, and accepts 0b 1 and 0b1:0b 10:0b11. + // language="unit file (systemd)" + val file = """ + [IPv6AcceptRA] + RouteMetric=hoge + RouteMetric=300:200:100 + RouteMetric=100:100:200 + RouteMetric=1:2 + RouteMetric=1:2:3:4 + RouteMetric=1::3 + RouteMetric=1:2:3:: + RouteMetric=100 :200:300 + RouteMetric=+0b1 + RouteMetric=1:+0b10:3 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt new file mode 100644 index 00000000..0a98a141 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborDestinationAddressOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Neighbor] Address= (NEIGHBOR_DESTINATION_ADDRESS). */ +class ConfigParseNeighborDestinationAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Neighbor] + Address=192.168.10.1 + Address=2004:da8:1:0::1 + Address=0.0.0.0 + Address=fe80::1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Neighbor] + Address=192.168.10.1/24 + Address=not-an-address + Address=256.1.1.1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt new file mode 100644 index 00000000..fa88ef74 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNeighborProxyAddressOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_neighbor_proxy_address. */ +class ConfigParseNeighborProxyAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + ProxyNeighbor=192.168.1.10 + ProxyNeighbor=2001:db8::1 + ProxyNeighbor=::ffff:10.0.0.1 + ProxyNeighbor=255.255.255.254 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + ProxyNeighbor=0.0.0.0 + ProxyNeighbor=:: + ProxyNeighbor=224.0.0.1 + ProxyNeighbor=ff02::1 + ProxyNeighbor=255.255.255.255 + ProxyNeighbor=192.168.1.0/24 + ProxyNeighbor=fe80::1%eth0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt new file mode 100644 index 00000000..ef45133c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNftSetNetworkOptionValueTest.kt @@ -0,0 +1,61 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_nft_set(NFT_SET_PARSE_NETWORK) and [Address] NFTSet=. */ +class ConfigParseNftSetNetworkOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Address] + NFTSet=address:inet:sd_test:addr4 prefix:inet:sd_test:network4 ifindex:inet:sd_test:ifindex + NFTSet=prefix:netdev:filter:eth_ipv4_prefix + NFTSet="address:ip6:my.table:set/one" + [DHCPv4] + NFTSet=address:ip:filter:dhcp4 + [DHCPv6] + NFTSet=prefix:ip6:filter:dhcp6_prefix + [IPv6AcceptRA] + NFTSet=address:inet:t:s + [DHCPPrefixDelegation] + NFTSet=prefix:bridge:t_1:s_2 + NFTSet=address::inet:t:s + NFTSet=:address:inet:t:s + NFTSet=address:inet:t:s: + NFTSet=address:"inet":t:s + NFTSet='address':inet:"t":s prefix:ip:t:s + NFTSet=address:inet:"t:s" + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Address] + NFTSet=cgroup:inet:filter:set + NFTSet=address:ipx:filter:set + NFTSet=address:inet:1table:set + NFTSet=address:inet:table + NFTSet=address:inet:table:set:extra + [DHCPv4] + NFTSet=address:inet:filter:bad-name + NFTSet=address:"inet:t + NFTSet=address:"ipx":t:s + NFTSet="address:inet:t:s:extra" + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt new file mode 100644 index 00000000..b283ebe1 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePref64PrefixLifetimeOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_pref64_prefix_lifetime: at most 8191 * 8s, not infinity. */ +class ConfigParsePref64PrefixLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6PREF64Prefix] + LifetimeSec=1800 + LifetimeSec=0 + LifetimeSec=65528 + LifetimeSec=18h 12min 8s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6PREF64Prefix] + LifetimeSec=65529 + LifetimeSec=infinity + LifetimeSec=1d + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt new file mode 100644 index 00000000..551a212f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePrefixTokenOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [IPv6Prefix] Token= (config_parse_prefix_token -> config_parse_address_generation_type). */ +class ConfigParsePrefixTokenOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6Prefix] + Token=eui64 + Token=::1a:2b:3c:4d + Token=static:::fa:de:ca:fe + Token=prefixstable + Token=prefixstable:2002:da8:1::,86b123b969ba4b7eb8b3d8605123525a + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6Prefix] + Token=prefixstable@ + Token=prefixstable,00000000000000000000000000000000 + Token=static: + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt new file mode 100644 index 00000000..d728ab06 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricAdvMssOptionValueTest.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] TCPAdvertisedMaximumSegmentSize=. */ +class ConfigParseRouteMetricAdvMssOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + TCPAdvertisedMaximumSegmentSize=30 + TCPAdvertisedMaximumSegmentSize=1460 + TCPAdvertisedMaximumSegmentSize=1K + TCPAdvertisedMaximumSegmentSize=4294967295 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + TCPAdvertisedMaximumSegmentSize=0 + TCPAdvertisedMaximumSegmentSize=4G + TCPAdvertisedMaximumSegmentSize=big + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt new file mode 100644 index 00000000..50a7ff2b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricInitCwndOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] InitialCongestionWindow=. */ +class ConfigParseRouteMetricInitCwndOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // safe_atou32 sees the whole value, so whitespace or a sign after a 0b/0o prefix is fine (sd-check accepts 0b 1, 0b+1). + // language="unit file (systemd)" + val file = """ + [Route] + InitialCongestionWindow=20 + InitialCongestionWindow=1 + InitialCongestionWindow=1023 + InitialCongestionWindow=0x10 + InitialCongestionWindow=0b 1 + InitialCongestionWindow=0b+1 + InitialCongestionWindow=0o 7 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. +0b1 and -0b0 are EINVAL: systemd turns off glibc's C23 0b parsing (sd-check rejects both). + // language="unit file (systemd)" + val file = """ + [Route] + InitialCongestionWindow=0 + InitialCongestionWindow=1024 + InitialCongestionWindow=ten + InitialCongestionWindow=+0b1 + InitialCongestionWindow=-0b0 + InitialCongestionWindow=08 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt new file mode 100644 index 00000000..f94ca474 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricMtuOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] MTUBytes= (config_parse_mtu, AF_UNSPEC). */ +class ConfigParseRouteMetricMtuOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + MTUBytes=1480 + MTUBytes=9000 + MTUBytes=0 + MTUBytes=1K + MTUBytes=1.5K + MTUBytes=1 K + MTUBytes=1K 512 + MTUBytes=4294967295 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + MTUBytes=4G + MTUBytes=1500X + MTUBytes=512 1K + MTUBytes=-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt new file mode 100644 index 00000000..18841fde --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMetricTcpRtoOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] TCPRetransmissionTimeoutSec=. */ +class ConfigParseRouteMetricTcpRtoOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + TCPRetransmissionTimeoutSec=300s + TCPRetransmissionTimeoutSec=200ms + TCPRetransmissionTimeoutSec=1 + TCPRetransmissionTimeoutSec=1us + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + TCPRetransmissionTimeoutSec=0 + TCPRetransmissionTimeoutSec=infinity + TCPRetransmissionTimeoutSec=50d + TCPRetransmissionTimeoutSec=fast + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt new file mode 100644 index 00000000..38143c82 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteMultiPathOptionValueTest.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] MultiPathRoute=. */ +class ConfigParseRouteMultiPathOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Route] + MultiPathRoute=10.0.0.1@eth0 20 + MultiPathRoute=192.168.1.1 50 + MultiPathRoute=@wg0 15 + MultiPathRoute=2001:db8::1@eth0 + MultiPathRoute=2001:1234:5:9fff:ff:ff:ff:ff@dummy98 5 + MultiPathRoute=@1 15 + MultiPathRoute=@999999 10 + MultiPathRoute=10.0.0.1@5 + MultiPathRoute=fe80::1 + MultiPathRoute=@eth0 256 + MultiPathRoute=@eth0 1 + MultiPathRoute=@eth0 0b 1 + MultiPathRoute=@+0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects the weight +0b1 and the empty device in `@ 5`; it accepts the + // weight 0b 1 and the device name +0b1 (parse_ifindex refuses it, ifname_valid accepts it). + // language="unit file (systemd)" + val file = """ + [Route] + MultiPathRoute=@eth0 0 + MultiPathRoute=@eth0 257 + MultiPathRoute=@ + MultiPathRoute=@0 + MultiPathRoute=10.0.0.1/24@eth0 + MultiPathRoute=@eth0 10 20 + MultiPathRoute=eth0 + MultiPathRoute=@eth0 +0b1 + MultiPathRoute=@ 5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(9, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt new file mode 100644 index 00000000..69b8e838 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixLifetimeOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_route_prefix_lifetime. */ +class ConfigParseRoutePrefixLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + LifetimeSec=1000 + LifetimeSec=infinity + LifetimeSec=0 + LifetimeSec=4294967294 + LifetimeSec=1w + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + LifetimeSec=4294967295 + LifetimeSec=200y + LifetimeSec=forever + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt new file mode 100644 index 00000000..4f269c92 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutePrefixOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [IPv6RoutePrefix] Route=. */ +class ConfigParseRoutePrefixOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + Route=2001:db0:fff::/64 + Route=2001:db2:fff::/48 + Route=::/0 + Route=2001:db8::1 + Route=2001:db8::/0x40 + Route=2001:db8::/ 64 + Route=2001:db8::/0b 1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6RoutePrefix] + Route=192.168.0.0/24 + Route=2001:db8::/129 + Route=2001:db8::/ + Route=2001:db8::/+0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt new file mode 100644 index 00000000..cce75651 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouteTableOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [Route] Table= and [RoutingPolicyRule] Table=. */ +class ConfigParseRouteTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // custom-table, my_table, 0x0 and `main table` are deliberate leniencies: they are legal networkd.conf RouteTable= + // names (only all-digit names and names with `:` are refused; `main\ table:100` defines one with a space), which the + // plugin cannot see. systemd without such a definition rejects them. 0b 1 is safe_atou32 = 1 (sd-check accepts it). + // language="unit file (systemd)" + val file = """ + [Route] + Table=main + Table=local + Table=default + Table=1011 + Table=4294967295 + Table=custom-table + Table=0x0 + [RoutingPolicyRule] + Table=7 + Table=my_table + Table=0x10 + Table=0b 1 + Table=main table + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Route] + Table=0 + Table=4294967296 + Table=a:b + [RoutingPolicyRule] + Table=00 + Table=08 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt new file mode 100644 index 00000000..5c782dde --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterHomeAgentLifetimeOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_router_home_agent_lifetime: 0 < t <= 65535s. */ +class ConfigParseRouterHomeAgentLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + HomeAgentLifetimeSec=1 + HomeAgentLifetimeSec=65535 + HomeAgentLifetimeSec=18h 12min 15s + HomeAgentLifetimeSec=500ms + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + HomeAgentLifetimeSec=0 + HomeAgentLifetimeSec=65536 + HomeAgentLifetimeSec=infinity + HomeAgentLifetimeSec=1day + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt new file mode 100644 index 00000000..afcadc96 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterLifetimeOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_router_lifetime: any parse_sec() value (out-of-range values are clamped). */ +class ConfigParseRouterLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + RouterLifetimeSec=1800 + RouterLifetimeSec=0 + RouterLifetimeSec=30min + RouterLifetimeSec=1s + RouterLifetimeSec=10000 + RouterLifetimeSec=infinity + RouterLifetimeSec=.5 + RouterLifetimeSec=+5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + RouterLifetimeSec=forever + RouterLifetimeSec=30 minuts + RouterLifetimeSec=+.5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(3, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt new file mode 100644 index 00000000..80f2a7e9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterPrefixDelegationOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_router_prefix_delegation (IPv6SendRA= and deprecated IPv6PrefixDelegation=). */ +class ConfigParseRouterPrefixDelegationOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv6SendRA=yes + IPv6SendRA=no + IPv6SendRA=True + IPv6SendRA=0 + IPv6SendRA=Yes + IPv6SendRA=TRUE + IPv6SendRA=oFf + IPv6PrefixDelegation=static + IPv6PrefixDelegation=dhcpv6 + IPv6PrefixDelegation=yes + IPv6PrefixDelegation=Yes + IPv6PrefixDelegation=TRUE + IPv6PrefixDelegation=oFf + IPv6PrefixDelegation=No + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + IPv6SendRA=maybe + IPv6SendRA=both + IPv6SendRA=static + IPv6SendRA=dhcpv6 + IPv6PrefixDelegation=Static + IPv6PrefixDelegation=DHCPv6 + IPv6PrefixDelegation=maybe + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt new file mode 100644 index 00000000..7dcd2384 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRouterUint32MsecUsecOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_router_uint32_msec_usec. */ +class ConfigParseRouterUint32MsecUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + ReachableTimeSec=30s + ReachableTimeSec=0 + ReachableTimeSec=1h30min + ReachableTimeSec=1500ms + ReachableTimeSec=4294967295ms + ReachableTimeSec=infinity + RetransmitSec=1.5 + RetransmitSec=.5s + RetransmitSec=2 min + RetransmitSec=1d 2h + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + ReachableTimeSec=4294967296ms + ReachableTimeSec=50d + RetransmitSec=5x + RetransmitSec=-1s + RetransmitSec=5. + RetransmitSec=12.34.56 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(6, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt new file mode 100644 index 00000000..dc724aa2 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [RoutingPolicyRule] FirewallMark=. */ +class ConfigParseRoutingPolicyRuleFirewallMarkOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + FirewallMark=1011 + FirewallMark=7/255 + FirewallMark=0x10/0xff + FirewallMark=0 + FirewallMark=4294967295/4294967295 + FirewallMark=0b101 + FirewallMark=7/ 255 + FirewallMark=0b 1 + FirewallMark=1/0b 1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b1, 1/+0b1 and 1 /2, and accepts 0b 1 and 1/0b 1. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + FirewallMark=4294967296 + FirewallMark=7/ + FirewallMark=/255 + FirewallMark=mark + FirewallMark=-1 + FirewallMark=+0b1 + FirewallMark=1/+0b1 + FirewallMark=1 /2 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(8, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt new file mode 100644 index 00000000..689f742d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [RoutingPolicyRule] IPProtocol=. */ +class ConfigParseRoutingPolicyRuleIpProtocolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + IPProtocol=tcp + IPProtocol=UDP + IPProtocol=sctp + IPProtocol=icmpv6 + IPProtocol=6 + IPProtocol=17 + IPProtocol=0x11 + IPProtocol=255 + IPProtocol=aggfrag + IPProtocol=AGGFRAG + IPProtocol=144 + IPProtocol=0 + IPProtocol=58 + IPProtocol=TcP + IPProtocol=IcMpV6 + IPProtocol=L2TP + IPProtocol=0b 110 + IPProtocol=-0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. sd-check rejects +0b110 (EINVAL) and 0b-1 (-1: ERANGE), and accepts 0b 110 (tcp) and -0. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + IPProtocol=tcpp + IPProtocol=256 + IPProtocol=mptcp + IPProtocol=-6 + IPProtocol=3 + IPProtocol=200 + IPProtocol=TcPx + IPProtocol=Yes + IPProtocol=+0b110 + IPProtocol=0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(10, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt new file mode 100644 index 00000000..a9caaa4d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRoutingPolicyRuleUidRangeOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** [RoutingPolicyRule] User=. */ +class ConfigParseRoutingPolicyRuleUidRangeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // systemd-network and +1000 are accepted as possible user names: systemd resolves them via NSS at runtime + // (get_user_creds), so they are only rejected on a machine without that user. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + User=100-200 + User=1000 + User=0 + User=root + User=systemd-network + User=1000-1000 + User=+1000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [RoutingPolicyRule] + User=200-100 + User=65535 + User=4294967295 + User=01000 + User=100- + User=1000-99999999999999999999 + User=99999999999999999999-1000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt new file mode 100644 index 00000000..32d3a47e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStackedNetdevOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_stacked_netdev (all ten kinds). */ +class ConfigParseStackedNetdevOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // +0b1 is not an ifindex (safe_atoi gives EINVAL because systemd turns off glibc's C23 0b parsing), so it is a + // valid name; 0b-1 is the ifindex -1, which is not positive. sd-check accepts both names. + // language="unit file (systemd)" + val file = """ + [Network] + IPoIB=ib0.8001 + IPVLAN=ipvlan99 + IPVTAP=ipvtap99 + MACsec=macsec99 + MACVLAN=macvlan99 macvlan98 + MACVTAP=macvtap99 + Tunnel=sittun99 + VLAN=vlan99 + VLAN=vlan-1 vlan-2 + VXLAN=vxlan-slaac + Xfrm=xfrm98 + VLAN= + VLAN=0x0 + VLAN=-5 + VLAN=0xvlan + VLAN=+0b1 + VLAN=0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + VLAN=vlan:1 + VXLAN=this-name-is-way-too-long + Tunnel=123 + MACVLAN=ok all + IPVLAN=0x10 + VLAN=+5 + VLAN=0b1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(7, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt new file mode 100644 index 00000000..7b22ecc5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStringSafeAsciiOptionValueTest.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_string(CONFIG_PARSE_STRING_SAFE_AND_ASCII). */ +class ConfigParseStringSafeAsciiOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + BootFilename=pxelinux.0 + BootFilename=efi/grubx64.efi + BootFilename=boot file with spaces*.efi + BootFilename=p@ss:w0rd!#[x]? + BootFilename=foo*bar[1]? + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + BootFilename=café.efi + BootFilename=back\\slash + BootFilename="quoted" + BootFilename=it's + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(4, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt new file mode 100644 index 00000000..c209d662 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseStrvOptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_strv(0): word lists with only quote balance checked. */ +class ConfigParseStrvOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValuesNetwork() { + // language="unit file (systemd)" + val file = """ + [Network] + BindCarrier=eth0 eth1 + BindCarrier=en* + BindCarrier= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNetwork() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Network] + BindCarrier=eth0 "eth1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(1, myFixture.doHighlighting()) + } + @Test + fun testValidValuesNspawn() { + // language="unit file (systemd)" + val file = """ + [Exec] + Parameters=/bin/sh -c "echo hello world" + Parameters='single quoted' plain + Environment=FOO=bar "BAZ=with space" + Environment=PATH=/usr/bin\:/bin + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValuesNspawn() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [Exec] + Parameters=/bin/sh -c "echo + Environment=FOO='bar + """.trimIndent() + + setupFileInEditor("file.nspawn", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(2, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt new file mode 100644 index 00000000..8cf6bfa5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTimezoneOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** config_parse_timezone (syntax of verify_timezone). */ +class ConfigParseTimezoneOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Nowhere/Fake is well-formed; systemd only rejects it because /usr/share/zoneinfo/Nowhere/Fake does not exist, + // which depends on the installed tzdata (deliberate leniency). + // language="unit file (systemd)" + val file = """ + [DHCPServer] + Timezone=Europe/Berlin + Timezone=UTC + Timezone=America/Argentina/Buenos_Aires + Timezone=Etc/GMT+5 + Timezone=Etc/GMT-14 + Timezone=Nowhere/Fake + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(0, myFixture.doHighlighting()) + } + + @Test + fun testInvalidValues() { + // One highlight per line. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + Timezone=/Europe/Berlin + Timezone=Europe/ + Timezone=Europe//Berlin + Timezone=../etc/passwd + Timezone=Europe/Berlin Time + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + assertSize(5, myFixture.doHighlighting()) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt new file mode 100644 index 00000000..197c61f8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkRouteAddrSafeAtoVerdictsTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseIpv6AddressLabelOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseIpv6AddressLabelPrefixOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseNdiscRouteMetricOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRouteMetricInitCwndOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRouteMultiPathOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleFirewallMarkOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleIpProtocolOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseRoutingPolicyRuleUidRangeOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseStackedNetdevOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** + * Values containing a vertical tab (`\v`), which config_parse() does not strip (it only strips " \t\n\r") and + * which strtoul() skips. They are hard to write in a fixture file, so they are checked here on the grammar, for + * both engines. Every verdict was confirmed with steve-local/systemd-tools/bin/sd-check at 0f0682b773, one value + * per .network file. + */ +class NetworkRouteAddrSafeAtoVerdictsTest { + + private fun accepted(optionValue: GrammarOptionValue, value: String) { + assertEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(optionValue.combinator, value)) + assertEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(optionValue.combinator, value)) + } + + private fun rejected(optionValue: GrammarOptionValue, value: String) { + assertNotEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(optionValue.combinator, value)) + assertNotEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(optionValue.combinator, value)) + } + + @Test + fun testLeadingVerticalTabReachesSafeAtou() { + accepted(ConfigParseRouteMetricInitCwndOptionValue(), "\u000B5") + accepted(ConfigParseIpv6AddressLabelOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleFirewallMarkOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleFirewallMarkOptionValue(), "1/\u000B5") + accepted(ConfigParseNdiscRouteMetricOptionValue(), "\u000B5") + accepted(ConfigParseRouteMultiPathOptionValue(), "@eth0 \u000B5") + accepted(ConfigParseIpv6AddressLabelPrefixOptionValue(), "::/\u000B64") + // safe_atoi, through parse_ip_protocol_full: 6 is tcp. + accepted(ConfigParseRoutingPolicyRuleIpProtocolOptionValue(), "\u000B6") + } + + @Test + fun testParseUidLetsVerticalTabThrough() { + // SAFE_ATO_REFUSE_LEADING_WHITESPACE only refuses " \t\n\r"; strtoul then skips the \v. + accepted(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "\u000B5") + accepted(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "5-\u000B6") + // A space is refused by the same flag (sd-check: "Invalid uid or uid range '5- 6'"). + rejected(ConfigParseRoutingPolicyRuleUidRangeOptionValue(), "5- 6") + } + + @Test + fun testIfindexWithVerticalTabIsNotAName() { + // parse_ifindex("\v5") is 5, so ifname_valid() refuses it (sd-check: "Invalid netdev name in VLAN="). + rejected(ConfigParseStackedNetdevOptionValue(), "\u000B5") + } +} From 13f0889de2be136b64ae56d461753fccc10c87e1 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 12:02:20 -0700 Subject: [PATCH 7/8] feat: validate networkd DHCP, DUID, DNS, NTP and LLDP settings Add grammar-based value validation for: ActivationPolicy=, DHCPRelay=, DNS=, DNSOverTLS=, DNSSECNegativeTrustAnchors=, DUIDRawData=, DUIDType=, Domains=, EmitLLDP=, ExtraOption=, FallbackLeaseLifetimeSec=, Group=, Hostname=, IPv4LL=, IPv4LLStartAddress=, IPv6OnlyPreferredSec=, IgnoreCarrierLoss=, LLMNR=, Label=, MUDURL=, MulticastDNS=, NTP=, OutgoingInterface=, PVID=, RequestOptions=, RequiredForOnline=, RouteTable=, SendOption=, SendVendorOption=, ServerAddress=, SubnetId=, UnassignedSubnetPolicy=, UplinkInterface=, UserClass=, VendorClass= Accepted values follow systemd's own parsers (derived from the systemd source at 0f0682b773 and checked against systemd's real parsers built at that commit). Values systemd accepts when parsing but rejects only at unit or container start are accepted; see #565. Includes the [DHCPv4]/[DHCPv6]/[IPv6AcceptRA] RouteTable= validators using the shared route-table grammar (#563). Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 41 ++ .../ConfigParseActivationPolicyOptionValue.kt | 27 ++ .../ai/ConfigParseBridgeVlanIdOptionValue.kt | 43 ++ .../ConfigParseDhcp4UserClassOptionValue.kt | 35 ++ .../ConfigParseDhcp6SendOptionOptionValue.kt | 90 ++++ ...gParseDhcp6UserOrVendorClassOptionValue.kt | 27 ++ ...rseDhcpFallbackLeaseLifetimeOptionValue.kt | 22 + .../ai/ConfigParseDhcpLabelOptionValue.kt | 24 + .../ai/ConfigParseDhcpOptionTlvOptionValue.kt | 62 +++ .../ConfigParseDhcpOptionTypedOptionValue.kt | 44 ++ ...onfigParseDhcpOrRaRouteTableOptionValue.kt | 36 ++ ...igParseDhcpPdPrefixRouteTypeOptionValue.kt | 30 ++ .../ConfigParseDhcpPdSubnetIdOptionValue.kt | 48 ++ ...gParseDhcpRelayInterfaceModeOptionValue.kt | 24 + ...onfigParseDhcpRequestOptionsOptionValue.kt | 44 ++ ...ConfigParseDhcpServerAddressOptionValue.kt | 59 +++ ...eDhcpServerIpv6OnlyPreferredOptionValue.kt | 39 ++ ...ParseDhcpStaticLeaseHostnameOptionValue.kt | 26 ++ .../ConfigParseDnsOverTlsModeOptionValue.kt | 34 ++ ...seDnssecNegativeTrustAnchorsOptionValue.kt | 27 ++ .../ai/ConfigParseDomainsOptionValue.kt | 55 +++ .../ai/ConfigParseDuidRawdataOptionValue.kt | 57 +++ .../ai/ConfigParseDuidTypeOptionValue.kt | 92 ++++ .../ai/ConfigParseFdbInterfaceOptionValue.kt | 24 + ...ConfigParseIgnoreCarrierLossOptionValue.kt | 33 ++ .../ai/ConfigParseIpv4llAddressOptionValue.kt | 41 ++ .../ai/ConfigParseIpv4llOptionValue.kt | 23 + .../ai/ConfigParseLinkGroupOptionValue.kt | 35 ++ ...ConfigParseLldpMulticastModeOptionValue.kt | 34 ++ .../ai/ConfigParseMudUrlOptionValue.kt | 43 ++ .../ai/ConfigParseNtpOptionValue.kt | 32 ++ .../ai/ConfigParseRadvDnsOptionValue.kt | 35 ++ ...ConfigParseRequiredForOnlineOptionValue.kt | 46 ++ .../ConfigParseResolveSupportOptionValue.kt | 37 ++ .../ai/ConfigParseUplinkOptionValue.kt | 49 ++ .../BatchNetworkDhcpMiscCombinators.kt | 436 ++++++++++++++++++ ...figParseActivationPolicyOptionValueTest.kt | 45 ++ .../ConfigParseBridgeVlanIdOptionValueTest.kt | 51 ++ ...onfigParseDhcp4UserClassOptionValueTest.kt | 51 ++ ...nfigParseDhcp6SendOptionOptionValueTest.kt | 63 +++ ...seDhcp6UserOrVendorClassOptionValueTest.kt | 44 ++ ...hcpFallbackLeaseLifetimeOptionValueTest.kt | 42 ++ .../ai/ConfigParseDhcpLabelOptionValueTest.kt | 44 ++ ...ConfigParseDhcpOptionTlvOptionValueTest.kt | 76 +++ ...nfigParseDhcpOptionTypedOptionValueTest.kt | 60 +++ ...gParseDhcpOrRaRouteTableOptionValueTest.kt | 77 ++++ ...rseDhcpPdPrefixRouteTypeOptionValueTest.kt | 48 ++ ...onfigParseDhcpPdSubnetIdOptionValueTest.kt | 49 ++ ...seDhcpRelayInterfaceModeOptionValueTest.kt | 42 ++ ...gParseDhcpRequestOptionsOptionValueTest.kt | 51 ++ ...igParseDhcpServerAddressOptionValueTest.kt | 54 +++ ...pServerIpv6OnlyPreferredOptionValueTest.kt | 78 ++++ ...eDhcpStaticLeaseHostnameOptionValueTest.kt | 49 ++ ...onfigParseDnsOverTlsModeOptionValueTest.kt | 47 ++ ...ssecNegativeTrustAnchorsOptionValueTest.kt | 48 ++ .../ai/ConfigParseDomainsOptionValueTest.kt | 65 +++ .../ConfigParseDuidRawdataOptionValueTest.kt | 54 +++ .../ai/ConfigParseDuidTypeOptionValueTest.kt | 56 +++ .../ConfigParseFdbInterfaceOptionValueTest.kt | 53 +++ ...igParseIgnoreCarrierLossOptionValueTest.kt | 68 +++ ...ConfigParseIpv4llAddressOptionValueTest.kt | 45 ++ .../ai/ConfigParseIpv4llOptionValueTest.kt | 46 ++ .../ai/ConfigParseLinkGroupOptionValueTest.kt | 49 ++ ...igParseLldpMulticastModeOptionValueTest.kt | 50 ++ .../ai/ConfigParseMudUrlOptionValueTest.kt | 51 ++ .../ai/ConfigParseNtpOptionValueTest.kt | 54 +++ .../ai/ConfigParseRadvDnsOptionValueTest.kt | 45 ++ ...igParseRequiredForOnlineOptionValueTest.kt | 56 +++ ...onfigParseResolveSupportOptionValueTest.kt | 51 ++ .../ai/ConfigParseUplinkOptionValueTest.kt | 69 +++ .../ai/NetworkDhcpMiscSafeAtoVerdictTest.kt | 97 ++++ 71 files changed, 3782 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseActivationPolicyOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBridgeVlanIdOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp4UserClassOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6SendOptionOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6UserOrVendorClassOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpLabelOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTlvOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTypedOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOrRaRouteTableOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdSubnetIdOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRelayInterfaceModeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRequestOptionsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpStaticLeaseHostnameOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnsOverTlsModeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDomainsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidRawdataOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidTypeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFdbInterfaceOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIgnoreCarrierLossOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkGroupOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLldpMulticastModeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMudUrlOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNtpOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRadvDnsOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRequiredForOnlineOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolveSupportOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUplinkOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkDhcpMiscCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseActivationPolicyOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBridgeVlanIdOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp4UserClassOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6SendOptionOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6UserOrVendorClassOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpLabelOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTlvOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTypedOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOrRaRouteTableOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdSubnetIdOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRelayInterfaceModeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRequestOptionsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpStaticLeaseHostnameOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnsOverTlsModeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDomainsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidRawdataOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidTypeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFdbInterfaceOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIgnoreCarrierLossOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llAddressOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkGroupOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLldpMulticastModeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMudUrlOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNtpOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRadvDnsOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRequiredForOnlineOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolveSupportOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUplinkOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkDhcpMiscSafeAtoVerdictTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index d5bce785..a3eaf99c 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 @@ -589,6 +589,47 @@ fun getAllAIGeneratedValidators(): Map { // parse_sec); the `0` entry above (ConfigParseCoalesceSecOptionValue) covers the parse_sec keys. Validator("config_parse_coalesce_sec", "true") to ConfigParseCoalesceNsecOptionValue() as OptionValueInformation, + // Batch 2026-10: network-dhcp-misc + Validator("config_parse_dhcp_option_tlv", "0") to ConfigParseDhcpOptionTlvOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_option", "true") to ConfigParseDhcpOptionTypedOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp6_send_option", "0") to ConfigParseDhcp6SendOptionOptionValue() as OptionValueInformation, + Validator("config_parse_uplink", "0") to ConfigParseUplinkOptionValue() as OptionValueInformation, + Validator("config_parse_fdb_interface", "0") to ConfigParseFdbInterfaceOptionValue() as OptionValueInformation, + Validator("config_parse_mud_url", "0") to ConfigParseMudUrlOptionValue() as OptionValueInformation, + Validator("config_parse_resolve_support", "0") to ConfigParseResolveSupportOptionValue() as OptionValueInformation, + Validator("config_parse_dns_over_tls_mode", "0") to ConfigParseDnsOverTlsModeOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_pd_prefix_route_type", "0") to ConfigParseDhcpPdPrefixRouteTypeOptionValue() as OptionValueInformation, + Validator("config_parse_lldp_multicast_mode", "0") to ConfigParseLldpMulticastModeOptionValue() as OptionValueInformation, + Validator("config_parse_activation_policy", "0") to ConfigParseActivationPolicyOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_relay_interface_mode", "0") to ConfigParseDhcpRelayInterfaceModeOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_fallback_lease_lifetime", "0") to ConfigParseDhcpFallbackLeaseLifetimeOptionValue() as OptionValueInformation, + Validator("config_parse_ipv4ll", "0") to ConfigParseIpv4llOptionValue() as OptionValueInformation, + Validator("config_parse_ignore_carrier_loss", "0") to ConfigParseIgnoreCarrierLossOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_server_ipv6_only_preferred", "0") to ConfigParseDhcpServerIpv6OnlyPreferredOptionValue() as OptionValueInformation, + Validator("config_parse_bridge_vlan_id", "0") to ConfigParseBridgeVlanIdOptionValue() as OptionValueInformation, + Validator("config_parse_link_group", "0") to ConfigParseLinkGroupOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_pd_subnet_id", "0") to ConfigParseDhcpPdSubnetIdOptionValue() as OptionValueInformation, + Validator("config_parse_required_for_online", "0") to ConfigParseRequiredForOnlineOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_server_address", "0") to ConfigParseDhcpServerAddressOptionValue() as OptionValueInformation, + Validator("config_parse_ipv4ll_address", "0") to ConfigParseIpv4llAddressOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_label", "0") to ConfigParseDhcpLabelOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_static_lease_hostname", "0") to ConfigParseDhcpStaticLeaseHostnameOptionValue() as OptionValueInformation, + Validator("config_parse_domains", "0") to ConfigParseDomainsOptionValue() as OptionValueInformation, + Validator("config_parse_dnssec_negative_trust_anchors", "0") to ConfigParseDnssecNegativeTrustAnchorsOptionValue() as OptionValueInformation, + Validator("config_parse_ntp", "0") to ConfigParseNtpOptionValue() as OptionValueInformation, + Validator("config_parse_radv_dns", "0") to ConfigParseRadvDnsOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_request_options", "AF_INET") to ConfigParseDhcpRequestOptionsOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_request_options", "AF_INET6") to ConfigParseDhcpRequestOptionsOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp4_user_class", "0") to ConfigParseDhcp4UserClassOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp6_user_or_vendor_class", "0") to ConfigParseDhcp6UserOrVendorClassOptionValue() as OptionValueInformation, + Validator("config_parse_duid_type", "0") to ConfigParseDuidTypeOptionValue("config_parse_duid_type") as OptionValueInformation, + Validator("config_parse_network_duid_type", "0") to ConfigParseDuidTypeOptionValue("config_parse_network_duid_type") as OptionValueInformation, + Validator("config_parse_duid_rawdata", "0") to ConfigParseDuidRawdataOptionValue("config_parse_duid_rawdata") as OptionValueInformation, + Validator("config_parse_network_duid_rawdata", "0") to ConfigParseDuidRawdataOptionValue("config_parse_network_duid_rawdata") as OptionValueInformation, + Validator("config_parse_dhcp_or_ra_route_table", "NETWORK_CONFIG_SOURCE_DHCP4") to ConfigParseDhcpOrRaRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_or_ra_route_table", "NETWORK_CONFIG_SOURCE_DHCP6") to ConfigParseDhcpOrRaRouteTableOptionValue() as OptionValueInformation, + Validator("config_parse_dhcp_or_ra_route_table", "NETWORK_CONFIG_SOURCE_NDISC") to ConfigParseDhcpOrRaRouteTableOptionValue() as OptionValueInformation, + ) return allValidators diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseActivationPolicyOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseActivationPolicyOptionValue.kt new file mode 100644 index 00000000..98285bf0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseActivationPolicyOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] ActivationPolicy= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ActivationPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1201 config_parse_activation_policy (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1191-L1198 activation_policy_table + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-activation-policy.network.d/always-down.conf#L2 + */ + +/** + * Validator for `[Link] ActivationPolicy=`: exactly one of `up`, `always-up`, `manual`, `always-down`, + * `down`, `bound` (plain DEFINE_STRING_TABLE_LOOKUP — case-sensitive, no boolean). + */ +class ConfigParseActivationPolicyOptionValue : SimpleGrammarOptionValues( + "config_parse_activation_policy", + SequenceCombinator( + FlexibleLiteralChoiceTerminal("up", "always-up", "manual", "always-down", "down", "bound"), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBridgeVlanIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBridgeVlanIdOptionValue.kt new file mode 100644 index 00000000..c9df8f0a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBridgeVlanIdOptionValue.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN_FALSE +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [BridgeVLAN] PVID= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PVID= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-bridge-vlan.c#L341-L381 config_parse_bridge_vlan_id (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/vlan-util.c#L8-L23 parse_vlanid (safe_atou16, <= VLANID_MAX = 4094) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/26-bridge-vlan-master.network.d/30-override.conf#L3 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/26-bridge-vlan-master.network#L9 + */ + +/** + * Validator for `[BridgeVLAN] PVID=` (config_parse_bridge_vlan_id): a *false* boolean (removes the PVID; + * a true one falls through to parse_vlanid and is refused), or a VLAN id 0…4094 read by safe_atou16 + * (base 0, so `0x10` is legal). + */ +class ConfigParseBridgeVlanIdOptionValue : SimpleGrammarOptionValues( + "config_parse_bridge_vlan_id", + AlternativeCombinator( + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 4_094L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), + SequenceCombinator(BOOLEAN_FALSE, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp4UserClassOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp4UserClassOptionValue.kt new file mode 100644 index 00000000..922bad0d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp4UserClassOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CUNESCAPE_UNQUOTE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [DHCPv4] UserClass= (and the old [DHCP] UserClass=) (.network). + * + * Note: the plugin reports the deprecated [DHCP] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#UserClass= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L655-L698 config_parse_dhcp4_user_class (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + */ + +/** + * Validator for `[DHCPv4] UserClass=`: a whitespace-separated list of user-class strings, split by + * extract_first_word with EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE — so a class may be quoted to contain spaces + * and may use C escapes ([CUNESCAPE_UNQUOTE_WORD]). An unknown escape, a trailing backslash or an + * unterminated quote fails the assignment. + * + * Each class must also be 1…255 bytes long once unquoted/unescaped. An empty one (`""`, `''`) is + * dropped with a warning and flagged ([NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD]); the upper bound is not + * modelled (lenient). + */ +class ConfigParseDhcp4UserClassOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp4_user_class", + SequenceCombinator(wordList(NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6SendOptionOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6SendOptionOptionValue.kt new file mode 100644 index 00000000..d2ffe214 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6SendOptionOptionValue.kt @@ -0,0 +1,90 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.KeyOrSectionDispatchingGrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.dhcpOptionTypedData +import java.math.BigInteger + +/* + * [DHCPv6] SendOption= / SendVendorOption= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#SendOption= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#SendVendorOption= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L745-L942 config_parse_dhcp6_send_option (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L781 streq(lvalue, "SendVendorOption") — the key decides the shape + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1176-L1183 dhcp_option_data_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L599-L613 safe_atou16_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (safe_atou32, base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +// safe_atou16/safe_atou32 skip leading whitespace, and extract_first_word(…, ":", 0) leaves any that +// follows a colon inside the next word, so `123: 1:string:x` is fine: hence acceptsWhitespace = true. +// CODE is safe_atou16 with `u16 < 1 || u16 >= UINT16_MAX` refused, so 1…65534. +private val CODE_TYPE_DATA = SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 65_534L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + NETWORK_DHCP_MISC_COALESCED_COLONS, + dhcpOptionTypedData(), + EOF(), +) + +private val SEND_OPTION = SequenceCombinator(ZeroOrMore(COLON), CODE_TYPE_DATA) + +private val SEND_VENDOR_OPTION = SequenceCombinator( + ZeroOrMore(COLON), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + NETWORK_DHCP_MISC_COALESCED_COLONS, + CODE_TYPE_DATA, +) + +/** + * Validator for `[DHCPv6] SendOption=` (`CODE:TYPE:DATA`) and `[DHCPv6] SendVendorOption=` + * (`ENTERPRISE:CODE:TYPE:DATA`), both parsed by config_parse_dhcp6_send_option. + * + * The parser branches on the *key name*: only for SendVendorOption= does it first read an enterprise + * identifier (safe_atou32, any uint32 — the man page says 1…4294967294 but C accepts 0 and 4294967295 + * too). The validator is registered per (parser, ltype) but dispatches on the key it is checking, so + * `SendOption=` takes exactly the three-field form and `SendVendorOption=` exactly the four-field one + * (`SendVendorOption=12:string:x` is refused, as systemd's safe_atou16("string") refuses it). + * + * CODE is safe_atou16 in 1…65534 — the C check is `u16 < 1 || u16 >= UINT16_MAX`, so 65535 is refused + * although the man page says "1…65536". The TYPE:DATA tail is [dhcpOptionTypedData]; all six types are + * accepted. Every separator coalesces, so leading and doubled colons are harmless. + */ +class ConfigParseDhcp6SendOptionOptionValue : KeyOrSectionDispatchingGrammarOptionValue( + VALIDATOR_NAME, + AlternativeCombinator(SEND_VENDOR_OPTION, SEND_OPTION), + { key, _ -> if (key == "SendVendorOption") VENDOR else PLAIN }, +) { + private companion object { + const val VALIDATOR_NAME = "config_parse_dhcp6_send_option" + val PLAIN = SimpleGrammarOptionValues(VALIDATOR_NAME, SEND_OPTION) + val VENDOR = SimpleGrammarOptionValues(VALIDATOR_NAME, SEND_VENDOR_OPTION) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6UserOrVendorClassOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6UserOrVendorClassOptionValue.kt new file mode 100644 index 00000000..d28876f8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcp6UserOrVendorClassOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [DHCPv6] UserClass= / VendorClass= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#UserClass= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#VendorClass= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L700-L743 config_parse_dhcp6_user_or_vendor_class (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + */ + +/** + * Validator for `[DHCPv6] UserClass=` and `[DHCPv6] VendorClass=`: the same quoted/escaped word list as + * [ConfigParseDhcp4UserClassOptionValue], with a 1…65535-byte length limit per entry: empty + * entries are flagged, the upper bound is not modelled. + */ +class ConfigParseDhcp6UserOrVendorClassOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp6_user_or_vendor_class", + SequenceCombinator(wordList(NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValue.kt new file mode 100644 index 00000000..d1c7154b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValue.kt @@ -0,0 +1,22 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPv4] FallbackLeaseLifetimeSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#FallbackLeaseLifetimeSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp4.c#L2098-L2133 config_parse_dhcp_fallback_lease_lifetime (0) + */ + +/** + * Validator for `[DHCPv4] FallbackLeaseLifetimeSec=`. Despite the `Sec` suffix this is not a time span: + * the parser accepts only `forever` or `infinity` (STR_IN_SET, case-sensitive), as the man page says. + */ +class ConfigParseDhcpFallbackLeaseLifetimeOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_fallback_lease_lifetime", + SequenceCombinator(FlexibleLiteralChoiceTerminal("forever", "infinity"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpLabelOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpLabelOptionValue.kt new file mode 100644 index 00000000..5e5678a2 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpLabelOptionValue.kt @@ -0,0 +1,24 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPv4] Label= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Label= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp4.c#L2135-L2165 config_parse_dhcp_label (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L832-L852 address_label_valid + */ + +/** + * Validator for `[DHCPv4] Label=` (config_parse_dhcp_label → address_label_valid): 1…15 characters + * (shorter than IFNAMSIZ), each printable ASCII 0x20…0x7E — so, unlike an interface name, a label may + * contain `:`, `/`, `%` and inner spaces, and may be all digits. + */ +class ConfigParseDhcpLabelOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_label", + SequenceCombinator(RegexTerminal("""[\s\S]+""", """[\x20-\x7E]{1,15}\z"""), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTlvOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTlvOptionValue.kt new file mode 100644 index 00000000..fa2dd445 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTlvOptionValue.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.dhcpOptionTypedData + +/* + * [DHCPv4] SendOption= / SendVendorOption=, [DHCPServer] SendOption= / SendVendorOption=, + * [DHCPRelay] ExtraOption= (.network and networkd.conf). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#SendOption= + * https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#ExtraOption= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1062-L1108 config_parse_dhcp_option_tlv (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L944-L1060 config_parse_dhcp_option (TYPE:DATA tail, check_length=false) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1176-L1183 dhcp_option_data_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L583-L597 safe_atou8_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv4-only.network#L21 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for the `CODE:TYPE:DATA` raw-option settings parsed by config_parse_dhcp_option_tlv: + * `[DHCPv4] SendOption=`, `[DHCPv4] SendVendorOption=`, `[DHCPServer] SendOption=`, + * `[DHCPServer] SendVendorOption=` and `[DHCPRelay] ExtraOption=` (in both .network files and + * networkd.conf). + * + * CODE is read by safe_atou8 (so `0x0c` and `014` are 12 too, and leading whitespace, `0b 1` or `\v5` are + * fine because the field is cut on `:`, not on whitespace) and must be 1…254. The rest is handed to + * config_parse_dhcp_option and has the [dhcpOptionTypedData] shape: `uint8`/`uint16`/`uint32` take a + * number in range, `ipv4address`/`ipv6address` a bare address, `string` any C-escaped text (which may + * itself contain colons). Every separator is extract_first_word(…, ":", 0), so leading and repeated + * colons are coalesced and accepted. + * + * The man page lists only uint8/uint16/uint32/ipv4address/string for [DHCPv4]; the C parser shares + * the full type table, so `ipv6address` is accepted everywhere (C wins). + */ +class ConfigParseDhcpOptionTlvOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_option_tlv", + SequenceCombinator( + ZeroOrMore(COLON), + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 254L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + NETWORK_DHCP_MISC_COALESCED_COLONS, + dhcpOptionTypedData(), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTypedOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTypedOptionValue.kt new file mode 100644 index 00000000..7b2c14d5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOptionTypedOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.dhcpOptionTypedData + +/* + * [DHCPRelay] CircuitId= / RemoteId= / VirtualSubnetSelection= and the deprecated + * [DHCPServer] RelayAgentCircuitId= / RelayAgentRemoteId= (.network; RemoteId= also in networkd.conf). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#CircuitId= + * https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#RemoteId= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L944-L1060 config_parse_dhcp_option (true = check_length) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1176-L1183 dhcp_option_data_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-relay-downstream.network#L13-L14 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-agent-bridge.network#L12 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for config_parse_dhcp_option with ltype `true` (the gperf writes `/* check_length= */ true`): + * `[DHCPRelay] CircuitId=`, `RemoteId=`, `VirtualSubnetSelection=` and the deprecated + * `[DHCPServer] RelayAgentCircuitId=` / `RelayAgentRemoteId=`. + * + * The value is `TYPE:DATA` — see [dhcpOptionTypedData]: `uint8`/`uint16`/`uint32` with a number in range + * (base 0, so hex/octal are legal), `ipv4address`/`ipv6address` with a bare address, or `string` with any + * C-escaped text. With check_length set, systemd additionally refuses a string longer than 255 bytes once + * unescaped; that length limit is not modelled (lenient). + */ +class ConfigParseDhcpOptionTypedOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_option", + SequenceCombinator( + ZeroOrMore(COLON), + dhcpOptionTypedData(), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOrRaRouteTableOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOrRaRouteTableOptionValue.kt new file mode 100644 index 00000000..4a8a1a75 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpOrRaRouteTableOptionValue.kt @@ -0,0 +1,36 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ROUTE_TABLE_FROM_STRING + +/* + * [DHCPv4] RouteTable=, [DHCPv6] RouteTable=, [IPv6AcceptRA] RouteTable= (and the old [DHCP] RouteTable=) (.network). + * + * Note: the plugin reports the deprecated [DHCP] section as not allowed in .network files, so this validator never + * actually runs for the key there; it is listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RouteTable=num + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L560-L606 config_parse_dhcp_or_ra_route_table (NETWORK_CONFIG_SOURCE_DHCP4 / _DHCP6 / _NDISC) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L435-L464 manager_get_route_table_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L496-L616 config_parse_route_table_names (what a user-defined name may be) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (safe_atou32, base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv4-only.network#L17 + */ + +/** + * Validator for `RouteTable=` in [DHCPv4], [DHCPv6] and [IPv6AcceptRA] (config_parse_dhcp_or_ra_route_table). The + * ltype only picks which Network field receives the table. The parser hands the whole value to + * manager_get_route_table_from_string() and adds nothing of its own: no special empty value and no extra bounds. + * So the grammar is the shared [ROUTE_TABLE_FROM_STRING]: `default`, `main` or `local` (case-sensitive), a table + * number 1…4294967295 read by safe_atou32 (so `0x10`, `010` and `0b 1` are numbers too, and `0` is refused), or + * anything networkd.conf could have defined as a table name (non-empty, no `:`, not all digits). + * + * The name branch is lenient by design: networkd.conf is not visible to the plugin, so `mytable` is accepted + * although systemd refuses it when networkd.conf does not define it. What is flagged is what systemd refuses + * whatever networkd.conf says: `0`, `00`, `08`, `4294967296` and anything containing `:` (checked with + * systemd's parser at 0f0682b773 on all three keys). + */ +class ConfigParseDhcpOrRaRouteTableOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_or_ra_route_table", + ROUTE_TABLE_FROM_STRING, +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValue.kt new file mode 100644 index 00000000..0dc04000 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPv4] UnassignedSubnetPolicy=, [DHCPv6] UnassignedSubnetPolicy= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#UnassignedSubnetPolicy= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-prefix-delegation.c#L1472-L1518 config_parse_dhcp_pd_prefix_route_type (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L341-L356 route_type_table / route_type_from_string + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-route-util.c#L47-L49 route_type_is_reject + */ + +/** + * Validator for `UnassignedSubnetPolicy=` (config_parse_dhcp_pd_prefix_route_type). + * + * Accepts `none`, or a route type that route_type_is_reject() accepts: `unreachable`, `prohibit`, + * `blackhole` or `throw`. Other route types (`unicast`, `local`, …) parse but are then refused. No + * boolean; case-sensitive (streq / string table). + */ +class ConfigParseDhcpPdPrefixRouteTypeOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_pd_prefix_route_type", + SequenceCombinator( + FlexibleLiteralChoiceTerminal("none", "unreachable", "prohibit", "blackhole", "throw"), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdSubnetIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdSubnetIdOptionValue.kt new file mode 100644 index 00000000..81c70072 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpPdSubnetIdOptionValue.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import java.math.BigInteger + +/* + * [DHCPPrefixDelegation] SubnetId= (and the old [DHCPv6PrefixDelegation] SubnetId=) (.network). + * + * Note: the plugin reports the deprecated [DHCPv6PrefixDelegation] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#SubnetId= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-prefix-delegation.c#L1428-L1470 config_parse_dhcp_pd_subnet_id (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L522-L558 safe_atollu_full (safe_atoux64: explicit base 16) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp6pd-upstream.network#L16 + */ + +/** + * Validator for `SubnetId=` (config_parse_dhcp_pd_subnet_id): `auto`, or a *hexadecimal* number (always + * base 16 — `SubnetId=10` means 0x10 — with an optional `0x`) no larger than INT64_MAX, i.e. + * 0x7fffffffffffffff. + * + * `auto` comes first, but `a`, `add` etc. are hex numbers too; each branch ends in EOF so the classic + * engine can't strand one in the other. + */ +class ConfigParseDhcpPdSubnetIdOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_pd_subnet_id", + AlternativeCombinator( + SequenceCombinator(FlexibleLiteralChoiceTerminal("auto"), EOF()), + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 16, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.INT64_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRelayInterfaceModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRelayInterfaceModeOptionValue.kt new file mode 100644 index 00000000..ed768a8c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRelayInterfaceModeOptionValue.kt @@ -0,0 +1,24 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] DHCPRelay= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DHCPRelay= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-relay.c#L376-L380 config_parse_dhcp_relay_interface_mode (0), DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-relay.c#L369-L372 dhcp_relay_interface_mode_table + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-relay-downstream.network + */ + +/** + * Validator for `[Network] DHCPRelay=`: `upstream` or `downstream` (case-sensitive, no boolean). The + * empty assignment resets to "unset" and never reaches the grammar. + */ +class ConfigParseDhcpRelayInterfaceModeOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_relay_interface_mode", + SequenceCombinator(FlexibleLiteralChoiceTerminal("upstream", "downstream"), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRequestOptionsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRequestOptionsOptionValue.kt new file mode 100644 index 00000000..dfbccf8d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpRequestOptionsOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [DHCPv4] RequestOptions= (AF_INET), [DHCPv6] RequestOptions= (AF_INET6) (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RequestOptions= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1110-L1174 config_parse_dhcp_request_options (AF_INET / AF_INET6) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (safe_atou32, base 0) + */ + +/** + * Validator for `RequestOptions=` in both [DHCPv4] and [DHCPv6] (config_parse_dhcp_request_options; + * the two ltypes only pick which set is filled): a whitespace-separated list of option codes, each read + * by safe_atou32 and required to be 1…254 — for DHCPv6 too, although its option space is 16-bit; the C + * check is `i < 1 || i >= UINT8_MAX` for both families. + * + * Each code is a word from a whitespace split, so whitespace never reaches safe_atou32 + * (acceptsWhitespace = false): `RequestOptions=0b 1` is the words `0b` and `1`, and `0b` is refused + * ("DHCP request option is invalid", checked with systemd's parser at 0f0682b773). + */ +class ConfigParseDhcpRequestOptionsOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_request_options", + SequenceCombinator( + wordList( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 254L, + acceptsWhitespace = false, + acceptsMinus = true, + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerAddressOptionValue.kt new file mode 100644 index 00000000..664b198d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerAddressOptionValue.kt @@ -0,0 +1,59 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.CIDR_SEPARATOR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkDhcpMiscCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV4_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [DHCPServer] ServerAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ServerAddress= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server.c#L889-L932 config_parse_dhcp_server_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L799-L843 in_addr_prefix_from_string (AF_INET) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server.c#L922-L927 localhost / link-local → ignored + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L43-L47 in4_addr_is_link_local (169.254.0.0/16) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L121-L126 in4_addr_is_localhost (127.0.0.0/8) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L779-L797 in_addr_parse_prefixlen (safe_atou8, <= 32) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-server.network + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[DHCPServer] ServerAddress=`: an IPv4 address with an optional `/PREFIXLEN` of 0…32 + * (read by safe_atou8, so base-0 spellings like `0x18` are legal, and whitespace after the `/` reaches + * the parser and is skipped), e.g. `192.168.5.1/24`. + * + * A localhost (127.0.0.0/8) or link-local (169.254.0.0/16) address is flagged: systemd ignores it with + * "DHCP server address cannot be a localhost or link-local address" (checked with systemd's parser at + * 0f0682b773). + */ +class ConfigParseDhcpServerAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_server_address", + SequenceCombinator( + NetworkDhcpMiscCheckedSpan(IPV4_ADDR, "NotLoopbackOrLinkLocal") { addr -> + val o = addr.split('.').map { it.toIntOrNull() } + !(o.getOrNull(0) == 127 || (o.getOrNull(0) == 169 && o.getOrNull(1) == 254)) + }, + ZeroOrOne( + SequenceCombinator( + CIDR_SEPARATOR, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 32L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValue.kt new file mode 100644 index 00000000..e90e3194 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValue.kt @@ -0,0 +1,39 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkDhcpMiscCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.networkDhcpMiscParseSecUsec +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SEC_TIMESPAN + +/* + * [DHCPServer] IPv6OnlyPreferredSec= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv6OnlyPreferredSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server.c#L934-L974 config_parse_dhcp_server_ipv6_only_preferred (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server.c#L966-L970 t < MIN_V6ONLY_WAIT_USEC && !network_test_mode_enabled() + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/dhcp-protocol.h#L15 MIN_V6ONLY_WAIT_USEC (300 s) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289-L1291 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L82 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-server-ipv6-only-mode.network#L16 + */ + +/** + * Validator for `[DHCPServer] IPv6OnlyPreferredSec=`: a time span as parse_sec() reads it (`30min`, + * `1h30min`, `.5h`, `infinity`) — see [PARSE_SEC_TIMESPAN]. + * + * The duration must also be at least MIN_V6ONLY_WAIT_USEC, 300 s (`5min`, `300`, `4min 60s`); anything + * shorter, `0` included, is ignored with "Invalid [DHCPServer] IPv6OnlyPreferredSec=" (checked with + * systemd's parser at 0f0682b773). The only exception is networkd's test mode + * (`SYSTEMD_NETWORK_TEST_MODE`), which systemd's own test suite uses for its `20s` fixture; that is not a + * production setting, so short values are flagged. + */ +class ConfigParseDhcpServerIpv6OnlyPreferredOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_server_ipv6_only_preferred", + SequenceCombinator( + NetworkDhcpMiscCheckedSpan(PARSE_SEC_TIMESPAN, "AtLeast300s") { (networkDhcpMiscParseSecUsec(it) ?: Long.MAX_VALUE) >= 300_000_000L }, + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpStaticLeaseHostnameOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpStaticLeaseHostnameOptionValue.kt new file mode 100644 index 00000000..78069204 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDhcpStaticLeaseHostnameOptionValue.kt @@ -0,0 +1,26 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LDH_DNS_NAME +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPServerStaticLease] Hostname= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Hostname= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-server-static-lease.c#L222-L262 config_parse_dhcp_static_lease_hostname (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.h#L38-L47 dns_name_is_valid_ldh (DNS_LABEL_LDH|DNS_LABEL_NO_ESCAPES) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hostname-util.c#L42-L48 valid_ldh_char + */ + +/** + * Validator for `[DHCPServerStaticLease] Hostname=`: a DNS name made of LDH labels — letters, digits and + * hyphens, no leading or trailing hyphen, 1…63 characters each — joined by dots, with an optional single + * trailing dot; no escapes (see [LDH_DNS_NAME]). The 253-character total is not enforced. + */ +class ConfigParseDhcpStaticLeaseHostnameOptionValue : SimpleGrammarOptionValues( + "config_parse_dhcp_static_lease_hostname", + SequenceCombinator(LDH_DNS_NAME, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnsOverTlsModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnsOverTlsModeOptionValue.kt new file mode 100644 index 00000000..cbcd0a40 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnsOverTlsModeOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] DNSOverTLS= (.network) and [Resolve] DNSOverTLS= (resolved.conf). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DNSOverTLS= + * https://www.freedesktop.org/software/systemd/man/latest/resolved.conf.html#DNSOverTLS= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/resolve-util.c#L10 config_parse_dns_over_tls_mode (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/resolve-util.c#L26-L31 dns_over_tls_mode_table (DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32-L43 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + */ + +/** + * Validator for `DNSOverTLS=` (config_parse_dns_over_tls_mode): `opportunistic`, or any boolean + * (parse_boolean first, case-insensitively; then the case-sensitive table `no`/`opportunistic`/`yes`). + */ +class ConfigParseDnsOverTlsModeOptionValue : SimpleGrammarOptionValues( + "config_parse_dns_over_tls_mode", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("opportunistic"), + BOOLEAN, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValue.kt new file mode 100644 index 00000000..8c9c26f3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValue.kt @@ -0,0 +1,27 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DNS_NAME_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [Network] DNSSECNegativeTrustAnchors= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DNSSECNegativeTrustAnchors= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dns.c#L270-L319 config_parse_dnssec_negative_trust_anchors (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.h#L26-L36 dns_name_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape + */ + +/** + * Validator for `[Network] DNSSECNegativeTrustAnchors=`: a whitespace-separated list of DNS names, each + * checked by dns_name_is_valid() (non-LDH: any printable label characters, single dots, optional + * trailing dot, `.` for the root). See [DNS_NAME_WORD]. + */ +class ConfigParseDnssecNegativeTrustAnchorsOptionValue : SimpleGrammarOptionValues( + "config_parse_dnssec_negative_trust_anchors", + SequenceCombinator(wordList(DNS_NAME_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDomainsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDomainsOptionValue.kt new file mode 100644 index 00000000..b17cdd76 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDomainsOptionValue.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DNS_NAME_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NetworkDhcpMiscCheckedSpan +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.networkDhcpMiscIsLocalhost +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [Network] Domains= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Domains= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dns.c#L134-L209 config_parse_domains (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L742-L749 dns_name_is_root + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape (via dns_name_normalize, no LDH) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hostname-util.c#L145-L161 is_localhost + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dns.c#L192-L197 is_localhost(domain) → ignored + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-state-file-tests.network#L13 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/24-search-domain.network#L9 + */ + +// One Domains= word: an optional `~` (route-only), then the root domain `.`, the wildcard `*`, or a +// DNS name. `~` on its own is the root routing domain (dns_name_is_root("") is true). `*` needs no +// branch of its own: it is already a valid one-label non-LDH DNS name, and a separate `*` literal would +// strand the classic engine on a label such as `*foo`. +// +// The localhost check runs on the whole word, minus exactly one leading `~` as in the C: checking only +// the name part would let the parse engine read `~localhost` as a one-label name that starts with `~`. +private val DOMAIN_WORD = NetworkDhcpMiscCheckedSpan( + SequenceCombinator( + ZeroOrOne(LiteralChoiceTerminal("~")), + ZeroOrOne(DNS_NAME_WORD), + ), + "NotLocalhost", +) { !networkDhcpMiscIsLocalhost(it.removePrefix("~")) } + +/** + * Validator for `[Network] Domains=` (config_parse_domains): a whitespace-separated list of search + * domains, each optionally prefixed with `~` to make it a routing-only domain; `.`, `~.` and `*` select + * the root/wildcard routing domain. Names are validated as non-LDH DNS names ([DNS_NAME_WORD]), so + * underscores and most punctuation are fine but empty labels (`a..b`, `.a`) are not. + * + * `localhost`, `localhost.localdomain` and their subdomains (is_localhost, any case, with or without `~`) + * are flagged: systemd ignores them with "'localhost' domain may not be configured as search or route + * domain" (checked with systemd's parser at 0f0682b773). + */ +class ConfigParseDomainsOptionValue : SimpleGrammarOptionValues( + "config_parse_domains", + SequenceCombinator(wordList(DOMAIN_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidRawdataOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidRawdataOptionValue.kt new file mode 100644 index 00000000..e54a791c --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidRawdataOptionValue.kt @@ -0,0 +1,57 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Repeat +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * DUIDRawData= — [DHCPv6] DUIDRawData= (.network, config_parse_duid_rawdata), [DHCPv4] DUIDRawData= and + * the old [DHCP] DUIDRawData= (.network, config_parse_network_duid_rawdata), and [DHCPv4]/[DHCPv6] + * DUIDRawData= in networkd.conf (config_parse_duid_rawdata). + * + * Note: the plugin reports the deprecated [DHCP] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#DUIDRawData= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DUIDRawData= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1321-L1392 config_parse_duid_rawdata (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1418-L1439 config_parse_network_duid_rawdata (0) → config_parse_duid_rawdata + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd-network/dhcp-duid-internal.h#L26 MAX_DUID_DATA_LEN (128) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/network/test-networkd-conf.c#L108-L122 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv6-only-custom-client-identifier.network#L15 + */ + +// One byte: one or two hex digits (`0` is 0x00, `ab` is 0xab). The shape is wider so `000` / `abc` +// highlight as a single bad byte. +private val DUID_BYTE = RegexTerminal("""[0-9a-zA-Z]+""", """[0-9a-fA-F]{1,2}(?![0-9a-zA-Z])""") + +/** + * Validator for `DUIDRawData=`, registered for both config_parse_duid_rawdata and + * config_parse_network_duid_rawdata. + * + * Colon-separated bytes `NN:NN:…`, each one or two hex digits, at most MAX_DUID_DATA_LEN = 128 of them. + * The separator is extract_first_word(…, ":", 0), so runs of colons coalesce and leading/trailing colons + * are accepted — test-networkd-conf.c accepts `00:11:22:` and `11::`, and rejects `000:11:22`, + * `00:111:22` and `abcdef`. A value of nothing but colons parses to an empty DUID and is accepted too. + */ +class ConfigParseDuidRawdataOptionValue(validatorName: String = "config_parse_duid_rawdata") : SimpleGrammarOptionValues( + validatorName, + SequenceCombinator( + ZeroOrMore(COLON), + ZeroOrOne( + SequenceCombinator( + DUID_BYTE, + Repeat(SequenceCombinator(NETWORK_DHCP_MISC_COALESCED_COLONS, DUID_BYTE), 0, 127), + ZeroOrMore(COLON), + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidTypeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidTypeOptionValue.kt new file mode 100644 index 00000000..8fe24079 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDuidTypeOptionValue.kt @@ -0,0 +1,92 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.NETWORK_DHCP_MISC_COALESCED_COLONS +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore + +/* + * DUIDType= — [DHCPv6] DUIDType= (.network, config_parse_duid_type), [DHCPv4] DUIDType= and the old + * [DHCP] DUIDType= (.network, config_parse_network_duid_type), and [DHCPv4]/[DHCPv6] DUIDType= in + * networkd.conf (config_parse_duid_type). + * + * Note: the plugin reports the deprecated [DHCP] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/networkd.conf.html#DUIDType= + * https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DUIDType= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1195-L1272 config_parse_duid_type (0) + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1298-L1319 config_parse_network_duid_type (0) → config_parse_duid_type + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1187-L1193 duid_type_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (":" separator, coalescing) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L599-L613 safe_atou16_full (base 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1083-L1145 parse_timestamp + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/network/test-networkd-conf.c#L25-L37 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-ipv6-only-custom-client-identifier.network#L14 + */ + +private val DUID_TYPE_NAME = FlexibleLiteralChoiceTerminal("link-layer-time", "vendor", "link-layer", "uuid") + +// parse_timestamp() takes the whole remainder (it may itself contain spaces and colons, e.g. +// "2000-01-01 00:00:00 UTC", "@1700000000", "yesterday"); its grammar is large and is not modelled. +private val DUID_TIMESTAMP = RegexTerminal(""".+""", """.+""") + +private fun duidType(): Combinator = SequenceCombinator( + ZeroOrMore(COLON), + AlternativeCombinator( + // Only DUID-LLT (type 1) may carry a time; spelled either by name or as the number 1. + SequenceCombinator(FlexibleLiteralChoiceTerminal("link-layer-time"), NETWORK_DHCP_MISC_COALESCED_COLONS, DUID_TIMESTAMP, EOF()), + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = 1L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + NETWORK_DHCP_MISC_COALESCED_COLONS, + DUID_TIMESTAMP, + EOF(), + ), + SequenceCombinator(DUID_TYPE_NAME, ZeroOrMore(COLON), EOF()), + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 65_535L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ZeroOrMore(COLON), + EOF(), + ), + ), +) + +/** + * Validator for `DUIDType=`, registered for both config_parse_duid_type and config_parse_network_duid_type + * (the latter only forwards to the former, twice). + * + * The value is `TYPE[:TIME]`. TYPE is one of `link-layer-time`, `vendor`, `link-layer`, `uuid` + * (case-sensitive) or any number 0…65535 read by safe_atou16 (so `0x42`, as systemd's own test data + * writes, is legal). A `:TIME` suffix — anything parse_timestamp() accepts — is allowed only when the type + * is DUID-LLT, i.e. `link-layer-time` or the number 1; `vendor:2000-01-01` is refused, as + * test-networkd-conf.c checks. The separator coalesces, so leading or trailing colons are harmless. + * + * The timestamp text itself is accepted as-is (lenient). + */ +class ConfigParseDuidTypeOptionValue(validatorName: String = "config_parse_duid_type") : SimpleGrammarOptionValues( + validatorName, + duidType(), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFdbInterfaceOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFdbInterfaceOptionValue.kt new file mode 100644 index 00000000..4ac2c4a0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFdbInterfaceOptionValue.kt @@ -0,0 +1,24 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IFNAME_OR_IFINDEX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [BridgeFDB] OutgoingInterface= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#OutgoingInterface= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-bridge-fdb.c#L485-L539 config_parse_fdb_interface (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + */ + +/** + * Validator for `[BridgeFDB] OutgoingInterface=` (config_parse_fdb_interface): a positive interface + * index, or an alternative interface name of up to 127 characters — see [IFNAME_OR_IFINDEX]. + */ +class ConfigParseFdbInterfaceOptionValue : SimpleGrammarOptionValues( + "config_parse_fdb_interface", + SequenceCombinator(IFNAME_OR_IFINDEX, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIgnoreCarrierLossOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIgnoreCarrierLossOptionValue.kt new file mode 100644 index 00000000..beb82af7 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIgnoreCarrierLossOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PARSE_SEC_TIMESPAN + +/* + * [Network] IgnoreCarrierLoss= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IgnoreCarrierLoss= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1096-L1138 config_parse_ignore_carrier_loss (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289-L1291 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L82 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-bridge99-ignore-carrier-loss.network#L9 + */ + +/** + * Validator for `[Network] IgnoreCarrierLoss=`: a boolean (case-insensitive; yes = infinity, no = 0), or + * a time span as parse_sec() reads it (`5s`, `1min 30s`, `1h30min`, `.5s`, `infinity`) — see + * [PARSE_SEC_TIMESPAN]. + */ +class ConfigParseIgnoreCarrierLossOptionValue : SimpleGrammarOptionValues( + "config_parse_ignore_carrier_loss", + AlternativeCombinator( + SequenceCombinator(BOOLEAN, EOF()), + SequenceCombinator(PARSE_SEC_TIMESPAN, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llAddressOptionValue.kt new file mode 100644 index 00000000..6b213a61 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llAddressOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DOT +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IntegerTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Labeled +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Role +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] IPv4LLStartAddress= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IPv4LLStartAddress= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-ipv4ll.c#L389-L429 config_parse_ipv4ll_address (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (inet_pton, AF_INET) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L49-L61 in4_addr_is_link_local_dynamic + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-client-with-ipv4ll.network#L9 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[Network] IPv4LLStartAddress=`: an IPv4 address usable for dynamic IPv4 link-local + * assignment — inside 169.254.0.0/16 but outside its first and last /24 (RFC 3927 §2.1), i.e. + * 169.254.1.0 … 169.254.254.255. + * + * Every octet matches any number so a wrong address still highlights as an address; the ranges are + * then enforced per octet. + */ +class ConfigParseIpv4llAddressOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv4ll_address", + SequenceCombinator( + Labeled( + Role.LITERAL, + SequenceCombinator( + IntegerTerminal(169, 170), DOT, IntegerTerminal(254, 255), DOT, IntegerTerminal(1, 255), DOT, IntegerTerminal(0, 256), + ), + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llOptionValue.kt new file mode 100644 index 00000000..722e0840 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseIpv4llOptionValue.kt @@ -0,0 +1,23 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] IPv4LL= (.network; deprecated in favour of LinkLocalAddressing=). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#LinkLocalAddressing= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-ipv4ll.c#L348-L387 config_parse_ipv4ll (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + */ + +/** + * Validator for the deprecated `[Network] IPv4LL=`: a boolean (parse_boolean, case-insensitive). systemd + * logs a deprecation warning for every assignment; the value itself is still applied. + */ +class ConfigParseIpv4llOptionValue : SimpleGrammarOptionValues( + "config_parse_ipv4ll", + SequenceCombinator(BOOLEAN, EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkGroupOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkGroupOptionValue.kt new file mode 100644 index 00000000..e235d344 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLinkGroupOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Link] Group= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Group= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1054-L1094 config_parse_link_group (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497-L520 safe_atoi (safe_atoi32, base 0 + mangle_base) + */ + +/** + * Validator for `[Link] Group=` (config_parse_link_group): an integer 0…2147483647, read by safe_atoi32 + * — so `0x10`, `0o17`, `0b101`, `010` (octal) and `+5` are all legal, and negatives are refused by the + * explicit `group < 0` check. + */ +class ConfigParseLinkGroupOptionValue : SimpleGrammarOptionValues( + "config_parse_link_group", + SequenceCombinator( + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 2_147_483_647L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLldpMulticastModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLldpMulticastModeOptionValue.kt new file mode 100644 index 00000000..1891dd2d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseLldpMulticastModeOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] EmitLLDP= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#EmitLLDP= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-lldp-tx.c#L134-L174 config_parse_lldp_multicast_mode (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-lldp-tx.c#L126-L132 lldp_multicast_mode_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/23-emit-lldp.network#L7 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/26-bridge-vlan-slave-issue-20373.network#L9 + */ + +/** + * Validator for `EmitLLDP=` (config_parse_lldp_multicast_mode): a boolean (case-insensitive; true means + * `nearest-bridge`) or one of `nearest-bridge`, `non-tpmr-bridge`, `customer-bridge` (case-sensitive). + */ +class ConfigParseLldpMulticastModeOptionValue : SimpleGrammarOptionValues( + "config_parse_lldp_multicast_mode", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("nearest-bridge", "non-tpmr-bridge", "customer-bridge"), + BOOLEAN, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMudUrlOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMudUrlOptionValue.kt new file mode 100644 index 00000000..504f609b --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseMudUrlOptionValue.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.C_ESCAPE_REGEX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPv4] MUDURL=, [DHCPv6] MUDURL=, [LLDP] MUDURL= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MUDURL= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-util.c#L199-L238 config_parse_mud_url (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/web-util.c#L10-L24 http_url_is_valid + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one (via cunescape, flags 0) + */ + +// After the scheme: at least one character, every one of them ASCII (ascii_is_valid), with backslashes +// only as valid C escapes (the whole value goes through cunescape first). +private val MUD_URL_REST = RegexTerminal( + """(?:[^\\]|\\[\s\S]?)+""", + """(?:[\x01-\x5B\x5D-\x7F]|$C_ESCAPE_REGEX)+\z""", +) + +/** + * Validator for the three `MUDURL=` settings (config_parse_mud_url). + * + * The value is cunescape()d and must then satisfy http_url_is_valid(): it starts with `http://` or + * `https://` (case-sensitive, STARTSWITH_SET), has something after the scheme, and is pure ASCII. The + * 255-byte limit on the unescaped URL is not modelled, and an escape that decodes to a non-ASCII byte + * (`\xff`) is accepted although systemd would then reject it (both lenient). + */ +class ConfigParseMudUrlOptionValue : SimpleGrammarOptionValues( + "config_parse_mud_url", + SequenceCombinator( + // An exact LiteralChoiceTerminal, not a Flexible one: the flexible shape regex would run on into the + // host name ("http://e") and never match a choice under the list-of-successes engine. + LiteralChoiceTerminal("http://", "https://"), + MUD_URL_REST, + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNtpOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNtpOptionValue.kt new file mode 100644 index 00000000..12533e1a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNtpOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DNS_NAME_WORD +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [Network] NTP= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#NTP= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-ntp.c#L48-L104 config_parse_ntp (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L1331-L1341 dns_name_is_valid_or_address + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-state-file-tests.network#L12 + */ + +/** + * Validator for `[Network] NTP=` (config_parse_ntp): a whitespace-separated list of NTP servers, each a + * DNS name or an IP address (dns_name_is_valid_or_address). + * + * Every IP literal systemd accepts here is also a syntactically valid non-LDH DNS name (`192.168.5.1` is + * four labels, `2001:db8::1` one label containing colons), so the grammar is simply a list of + * [DNS_NAME_WORD]s; what it catches is empty labels (`pool..ntp.org`, `.org`), double trailing dots, + * over-long labels and stray control characters. The MAX_NTP_SERVERS cap is not modelled. + */ +class ConfigParseNtpOptionValue : SimpleGrammarOptionValues( + "config_parse_ntp", + SequenceCombinator(wordList(DNS_NAME_WORD), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRadvDnsOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRadvDnsOptionValue.kt new file mode 100644 index 00000000..5be47953 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRadvDnsOptionValue.kt @@ -0,0 +1,35 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IPV6_ADDR +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.wordList + +/* + * [IPv6SendRA] DNS= (and the old [IPv6PrefixDelegation] DNS=) (.network). + * + * Note: the plugin reports the deprecated [IPv6PrefixDelegation] section as not allowed in .network files, so this + * validator never actually runs for keys there; they are listed only because the gperf shares the parser. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DNS= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-radv.c#L1285-L1347 config_parse_radv_dns (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word (whitespace, flags 0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/in-addr-util.c#L517-L529 in_addr_from_string (AF_INET6, inet_pton) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-ipv6-prefix.network#L10 + * linux https://man7.org/linux/man-pages/man3/inet_pton.3.html + */ + +/** + * Validator for `[IPv6SendRA] DNS=` (config_parse_radv_dns): a whitespace-separated list of IPv6 + * addresses (inet_pton AF_INET6 — no brackets, ports or `%scope`) or the special word `_link_local`. + * + * systemd also drops the null address (`::`) with a warning; spellings of it are not singled out here + * (lenient). + */ +class ConfigParseRadvDnsOptionValue : SimpleGrammarOptionValues( + "config_parse_radv_dns", + SequenceCombinator(wordList(AlternativeCombinator(LiteralChoiceTerminal("_link_local"), IPV6_ADDR)), EOF()) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRequiredForOnlineOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRequiredForOnlineOptionValue.kt new file mode 100644 index 00000000..6fa9ef75 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseRequiredForOnlineOptionValue.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne + +/* + * [Link] RequiredForOnline= (.network). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RequiredForOnline= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-network.c#L1010-L1052 config_parse_required_for_online (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-network/network-util.c#L94-L137 parse_operational_state_range + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-network/network-util.c#L44-L56 link_operstate_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-state-file-tests.network#L6 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-activation-policy.network.d/required-yes.conf#L2 + */ + +// link_operstate_table, compared with streq (case-sensitive). +private val OPERSTATE = FlexibleLiteralChoiceTerminal( + "missing", "off", "no-carrier", "dormant", "degraded-carrier", "carrier", "degraded", "enslaved", "routable", +) + +/** + * Validator for `[Link] RequiredForOnline=` (config_parse_required_for_online). + * + * Tried first as an operational-state range — `MIN`, `MIN:`, `MIN:MAX` or `:MAX` over the names in + * link_operstate_table (split on the first `:`; a missing MIN means `missing`, a missing MAX means + * `routable`) — and failing that as a boolean (case-insensitive). + * + * parse_operational_state_range also refuses a range whose MIN is above its MAX (`routable:off`); that + * compares table positions rather than spelling and is not modelled (lenient). + */ +class ConfigParseRequiredForOnlineOptionValue : SimpleGrammarOptionValues( + "config_parse_required_for_online", + AlternativeCombinator( + SequenceCombinator(OPERSTATE, ZeroOrOne(SequenceCombinator(COLON, ZeroOrOne(OPERSTATE))), EOF()), + SequenceCombinator(COLON, OPERSTATE, EOF()), + SequenceCombinator(BOOLEAN, EOF()), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolveSupportOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolveSupportOptionValue.kt new file mode 100644 index 00000000..5f43fc67 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseResolveSupportOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BOOLEAN +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.PreferValidAlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [Network] LLMNR= / MulticastDNS= (.network) and [Resolve] LLMNR= / MulticastDNS= (resolved.conf). + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#LLMNR= + * https://www.freedesktop.org/software/systemd/man/latest/resolved.conf.html#LLMNR= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/resolve-util.c#L8 config_parse_resolve_support (0), DEFINE_CONFIG_PARSE_ENUM + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/resolve-util.c#L12-L17 resolve_support_table (DEFINE_STRING_TABLE_LOOKUP_WITH_BOOLEAN) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32-L43 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-state-file-tests.network#L14-L15 + */ + +/** + * Validator for `LLMNR=` and `MulticastDNS=` (config_parse_resolve_support): `resolve`, or any boolean. + * + * The lookup tries parse_boolean() first — case-insensitive, all twelve spellings — and only then the + * case-sensitive table (`no`, `yes`, `resolve`), so `Yes` is fine but `Resolve` is not. + */ +class ConfigParseResolveSupportOptionValue : SimpleGrammarOptionValues( + "config_parse_resolve_support", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("resolve"), + BOOLEAN, + ), + EOF(), + ) +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUplinkOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUplinkOptionValue.kt new file mode 100644 index 00000000..552d3792 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseUplinkOptionValue.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.FlexibleLiteralChoiceTerminal +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IFNAME_OR_IFINDEX +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.KeyOrSectionDispatchingGrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +/* + * [DHCPServer] UplinkInterface=, [DHCPPrefixDelegation] UplinkInterface=, [IPv6SendRA] UplinkInterface=. + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#UplinkInterface= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1441-L1516 config_parse_uplink (0) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L1463-L1475 section → accept_none (:none vs :self) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full (IFNAME_VALID_ALTERNATIVE) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp6pd-upstream.network#L15 + * https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-dhcp-server-downstream.network#L13 + */ + +private fun uplink(vararg special: String) = AlternativeCombinator( + SequenceCombinator(FlexibleLiteralChoiceTerminal(*special), EOF()), + SequenceCombinator(IFNAME_OR_IFINDEX, EOF()), +) + +/** + * Validator for the three `UplinkInterface=` settings, all parsed by config_parse_uplink. + * + * Accepts `:auto`, a positive interface index, or an alternative interface name (see + * [IFNAME_OR_IFINDEX]), plus one section-specific word: `:none` in [DHCPServer] and [IPv6SendRA], `:self` + * in [DHCPPrefixDelegation] (the C sets `accept_none = false` for that section and its deprecated + * [DHCPv6PrefixDelegation] alias, which the plugin does not recognise as a section). The validator + * dispatches on the section of the property it checks. The special values are compared with streq, so + * they are case-sensitive. A `:none` in [DHCPPrefixDelegation] then fails ifname_valid_full (`:` is not + * a name character) and is ignored with a warning — flagged here. + */ +class ConfigParseUplinkOptionValue : KeyOrSectionDispatchingGrammarOptionValue( + VALIDATOR_NAME, + uplink(":auto", ":none", ":self"), + { _, section -> if (section == "DHCPPrefixDelegation" || section == "DHCPv6PrefixDelegation") SELF else NONE }, +) { + private companion object { + const val VALIDATOR_NAME = "config_parse_uplink" + val NONE = SimpleGrammarOptionValues(VALIDATOR_NAME, uplink(":auto", ":none")) + val SELF = SimpleGrammarOptionValues(VALIDATOR_NAME, uplink(":auto", ":self")) + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkDhcpMiscCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkDhcpMiscCombinators.kt new file mode 100644 index 00000000..315ea413 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchNetworkDhcpMiscCombinators.kt @@ -0,0 +1,436 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.util.PsiTreeUtil +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionGroups +import java.math.BigInteger + +/* + * Shared grammar pieces for the network-dhcp-misc validator batch (systemd-networkd DHCPv4/DHCPv6/DHCP + * server/DUID/DNS/NTP/LLDP and assorted [Network]/[Link] settings). + * + * Everything here is pinned to systemd 0f0682b773 (systemd-build/build/last_commit_hash); browse it at + * https://github.com/systemd/systemd/blob/0f0682b773. These live in their own file rather than in + * Combinators.kt so that parallel batches don't collide; several of them are candidates for promotion + * (see the batch's RECOMMENDATIONS.md). + * + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L112-L124 parse_ifindex + * + * Integers (safe_atou8/16/32, safe_atoi, safe_atoux64, parse_ifindex) are read with the shared [SafeAtoTerminal]. + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104-L296 cunescape_one + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10-L188 extract_first_word + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/socket-util.c#L785-L830 ifname_valid_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L24-L150 dns_label_unescape + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L304-L387 dns_name_concat + */ + +/** One or more `:` — extract_first_word(…, ":", 0) coalesces runs of its separator. */ +val NETWORK_DHCP_MISC_COALESCED_COLONS = OneOrMore(COLON) + +/** + * One C escape sequence as cunescape_one() accepts it: `\a \b \f \n \r \t \v \\ \" \' \s`, `\xHH`, + * `\uHHHH`, `\UHHHHHHHH`, or a three-digit octal `\OOO` no greater than 0377. (cunescape_one also refuses + * NUL unless asked, UTF-16 surrogates and out-of-range code points; those corners are left lenient.) + */ +const val C_ESCAPE_REGEX = """\\(?:[abfnrtv\\"'s]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-3][0-7]{2})""" + +/** + * A run of text that cunescape() must be able to decode: any characters, with every backslash starting a + * valid [C_ESCAPE_REGEX]. A stray or trailing backslash is an error (no UNESCAPE_RELAX). Non-empty. + */ +val C_ESCAPED_TEXT = RegexTerminal("""(?:[^\\]|\\[\s\S]?)+""", """(?:[^\\]|$C_ESCAPE_REGEX)+\z""") + +/** + * One word as extract_first_word(…, NULL, EXTRACT_CUNESCAPE|EXTRACT_UNQUOTE) splits it: unquoted text, + * `"…"` and `'…'` sections (which may contain whitespace) glued together, with backslash escapes — + * inside or outside quotes — decoded by cunescape_one. An escape that cunescape_one doesn't know (`\q`, + * `\ `), a trailing backslash or an unterminated quote fails the whole assignment. + */ +val CUNESCAPE_UNQUOTE_WORD = RegexTerminal( + """(?:[^\s"'\\]|\\[\s\S]?|"(?:[^"\\]|\\[\s\S]?)*"?|'(?:[^'\\]|\\[\s\S]?)*'?)+""", + """(?:[^\s"'\\]|$C_ESCAPE_REGEX|"(?:[^"\\]|$C_ESCAPE_REGEX)*"|'(?:[^'\\]|$C_ESCAPE_REGEX)*')+(?=\s|\z)""", +) + +/** + * [CUNESCAPE_UNQUOTE_WORD] minus the words that unquote to the empty string (`""`, `''`, `""''`): + * config_parse_dhcp4_user_class and config_parse_dhcp6_user_or_vendor_class drop those with "The length of + * the … entry '' is not in the range 1…255/65535, ignoring" (checked with systemd's parser at 0f0682b773). + * The upper length bound is not modelled (lenient). + * + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L687-L692 len == 0 check (DHCPv4) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/networkd-dhcp-common.c#L732-L737 len == 0 check (DHCPv6) + */ +val NETWORK_DHCP_MISC_NONEMPTY_UNQUOTE_WORD: Combinator = NetworkDhcpMiscCheckedSpan(CUNESCAPE_UNQUOTE_WORD, "NonEmptyWord") { + !NETWORK_DHCP_MISC_EMPTY_QUOTES.matches(it) +} + +private val NETWORK_DHCP_MISC_EMPTY_QUOTES = Regex("""(?:""|'')+""") + +/** A whitespace-separated list of [word]s (the value arrives stripped, so no leading/trailing space). */ +fun wordList(word: Combinator): Combinator = SequenceCombinator( + word, + ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), word)), +) + +// A note on the RegexTerminals below: each semantic regex either matches the *whole* span its syntactic +// regex matched or nothing (trailing lookaheads / \z). Under the classic engine a semantic match that +// stops short inside the syntactic span misaligns GrammarOptionValue's token bookkeeping and can index +// past the end of its token list (seen as an IndexOutOfBoundsException on DUIDRawData=000:11:22). + +// --------------------------------------------------------------------------------------------------- +// Interface name or index — `parse_ifindex(s) > 0 || ifname_valid_full(s, IFNAME_VALID_ALTERNATIVE)`, +// the pattern used by config_parse_uplink and config_parse_fdb_interface. +private const val IFNAME_CHAR = """[\x21-\x7E&&[^:/%]]""" + +private val IFNAME_ALTERNATIVE = Regex("""(?!(?:\.\.?|all|default)\z)$IFNAME_CHAR{1,127}""") + +/** + * A terminal that takes the span [shape] matches and accepts it iff [valid] says so, for leaves whose + * acceptance is a computation rather than a regular language. + */ +class CheckedTerminal(private val shape: Regex, private val name: String, private val valid: (String) -> Boolean) : TerminalCombinator { + + private fun span(value: String, offset: Int): String? = shape.matchAt(value, offset)?.value?.takeIf { it.isNotEmpty() } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val text = span(value, offset) ?: return NoMatch + if (!valid(text)) return NoMatch.copy(longestMatch = offset) + return MatchResult(listOf(text), offset + text.length, listOf(this), offset + text.length) + } + + override fun parse(value: String, offset: Int): Sequence { + val text = span(value, offset) ?: return sequenceOf(Stuck(offset, setOf(this))) + return sequenceOf(Parse(offset + text.length, listOf(ParsedToken(offset, offset + text.length, text, this, valid(text))))) + } + + override fun toString(): String = name +} + +/** + * A positive interface index, or an alternative interface name: `parse_ifindex(s) > 0 || + * ifname_valid_full(s, IFNAME_VALID_ALTERNATIVE)`. + * + * parse_ifindex() is safe_atoi() (base 0) plus `> 0`, so `010` is index 8, `0x10` is 16 and `+5` is 5, while + * `0`, `08` and `99999999999` are refused. ifname_valid_full() then takes 1…127 printable ASCII characters + * other than `:`, `/`, `%`, but not `.`, `..`, `all`, `default`, or a spelling made only of digits (those + * failed parse_ifindex above, so they are zero, too large or malformed octal). + * + * The span is the whole value, whitespace included, because safe_atoi sees all of it: `0b 1`, `0o 7` and `\v5` + * are indexes 1, 7 and 5, while `eth 0` and `0b 0` are refused (checked with systemd's parser at 0f0682b773). + */ +val IFNAME_OR_IFINDEX = CheckedTerminal(Regex("""[\s\S]+"""), "IfnameOrIfindex") { text -> + SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 1L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, + ).valueOf(text) != null || + (!text.all { it in '0'..'9' } && IFNAME_ALTERNATIVE.matches(text)) +} + +// --------------------------------------------------------------------------------------------------- +// DNS names. + +// A character that may appear unescaped in a non-LDH DNS label (dns_label_unescape without +// DNS_LABEL_LDH): anything >= 0x20 except DEL, the `.` separator and the `\` escape introducer. In a +// whitespace-separated word list whitespace is also excluded (it ends the word). +private const val DNS_PLAIN = """[^\s.\\\p{Cntrl}]""" + +/** + * One non-LDH DNS label inside a whitespace-separated word. + * + * The words are cut by extract_first_word(…, NULL, 0), which strips one level of backslash (a raw `\x` + * becomes `x`), and the result then goes through dns_label_unescape, which treats a remaining `\` as the + * start of a `\\`, `\.` or `\DDD` (000…255) escape. So in the raw text: + * - `\x` for any `x` other than a backslash is just `x` (accepted as label content); + * - a doubled backslash `\\` survives as `\` and must start a valid dns_label_unescape escape: it must + * be followed by `\\` or `\.` (an escaped backslash/dot), by three digits no greater than 255, or by + * the `.` separator (raw `foo\\.bar` is `foo\.bar`, one label with a literal dot — accepted either + * way). `foo\\bar`, `foo\\256` and a trailing `foo\\` are refused, as systemd refuses them. + * + * Still lenient: a raw `\.` is treated as label content although extract_first_word turns it into a + * separator, so `a\..b` and `\.a` (empty labels once unescaped) are accepted. A label of 64 or more + * plain characters is refused: that is over DNS_LABEL_MAX whatever follows. + * + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/dns-domain.c#L60-L100 dns_label_unescape escapes + */ +val DNS_WORD_LABEL = RegexTerminal( + """(?:[^\s.\\]|\\[\s\S])+""", + """(?!$DNS_PLAIN{64})(?:$DNS_PLAIN|\\[^\\]|\\\\(?:\\[\\.]|[01][0-9]{2}|2[0-4][0-9]|25[0-5]|(?=\.)))+(?=[\s.]|\z)""", +) + +/** + * A DNS name as dns_name_is_valid() accepts it, written as one whitespace-separated word: labels joined + * by single dots with an optional single trailing dot, or the root domain `.` on its own. Empty labels + * (`a..b`, `.a`) and a double trailing dot are rejected, as in dns_label_unescape. The 253-character + * whole-name limit is not enforced. + */ +val DNS_NAME_WORD = AlternativeCombinator( + SequenceCombinator(DNS_WORD_LABEL, ZeroOrMore(SequenceCombinator(DOT, DNS_WORD_LABEL)), ZeroOrOne(DOT)), + DOT, +) + +/** + * is_localhost(): `localhost`, `localhost.localdomain` and any name ending in `.localhost` or + * `.localhost.localdomain`, case-insensitively, with or without a trailing dot. [name] is a raw + * [DNS_NAME_WORD]; single-backslash escapes are dropped first, as extract_first_word drops them. + * + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/hostname-util.c#L145-L161 is_localhost + */ +fun networkDhcpMiscIsLocalhost(name: String): Boolean { + val n = name.replace(Regex("""\\(.)"""), "$1").lowercase().removeSuffix(".") + return n == "localhost" || n == "localhost.localdomain" || n.endsWith(".localhost") || n.endsWith(".localhost.localdomain") +} + +/** An LDH label: letters, digits and inner hyphens, 1…63 characters (dns_label_unescape with DNS_LABEL_LDH). */ +val LDH_DNS_LABEL = RegexTerminal( + """[A-Za-z0-9-]+""", + """[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?![A-Za-z0-9-])""", +) + +/** + * A DNS name as dns_name_is_valid_ldh() accepts it: [LDH_DNS_LABEL]s joined by single dots, an optional + * trailing dot, no escapes (DNS_LABEL_NO_ESCAPES), or the root domain `.`. The 253-character limit is not + * enforced. + */ +val LDH_DNS_NAME = AlternativeCombinator( + SequenceCombinator(LDH_DNS_LABEL, ZeroOrMore(SequenceCombinator(DOT, LDH_DNS_LABEL)), ZeroOrOne(DOT)), + DOT, +) + +// --------------------------------------------------------------------------------------------------- +// Typed DHCP option payloads — config_parse_dhcp_option / the tail of config_parse_dhcp6_send_option. + +/** + * [IPV6_ADDR] minus the two forms that begin with `::` (`::1`, `::ffff:1.2.3.4`, `::`) — for positions + * where a preceding coalescing `:` separator has already swallowed any leading colons. + */ +val IPV6_ADDR_NO_LEADING_COLON = Labeled(Role.LITERAL, tag = SemanticTag.IPV6, inner = AlternativeCombinator( + IPV6_IPV4_SUFFIX_FULL, + IPV6_IPV4_SUFFIX_ONE_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_TWO_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_THREE_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_FOUR_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_FIVE_HEXTET_BEFORE_ZERO_COMP, + IPV6_FULL_SPECIFIED, + IPV6_SEVEN_HEXTET_BEFORE_ZERO_COMP, + IPV6_SIX_HEXTET_BEFORE_ZERO_COMP, + IPV6_FIVE_HEXTET_BEFORE_ZERO_COMP, + IPV6_FOUR_HEXTET_BEFORE_ZERO_COMP, + IPV6_THREE_HEXTET_BEFORE_ZERO_COMP, + IPV6_TWO_HEXTET_BEFORE_ZERO_COMP, + IPV6_ONE_HEXTET_BEFORE_ZERO_COMP, +)) + +/** + * `TYPE:DATA`, the shape config_parse_dhcp_option() parses and config_parse_dhcp_option_tlv() / + * config_parse_dhcp6_send_option() parse after their numeric prefix(es). TYPE is one of + * dhcp_option_data_type_table and decides how the remainder is read: + * + * - `uint8` / `uint16` / `uint32` — safe_atou8/16/32 ([SafeAtoTerminal], base 0); + * - `ipv4address` / `ipv6address` — inet_pton() of that family, no prefix, no scope; + * - `string` — cunescape(…, UNESCAPE_ACCEPT_NUL): anything, with valid C escapes; may contain `:`. + * + * The separator is extract_first_word(…, ":", 0), which coalesces a run of colons, so `uint8::5` is + * fine. That coalescing also eats the leading colons of the data itself: for `ipv6address` an address + * beginning with `::` therefore cannot be written at all (`ipv6address:::1` hands inet_pton just `1` + * and fails), while `ipv6address::2001:db8::1` is fine. Hence [IPV6_ADDR_NO_LEADING_COLON] after + * the colon run. Likewise a `string` value cannot start with `:`. + * + * The number after `uintN:` is a field cut on `:`, so whitespace reaches safe_atou_full, which skips it + * (acceptsWhitespace = true). The string length check (`check_length`, <= 255 bytes once unescaped) is not + * modelled. + */ +fun dhcpOptionTypedData(): Combinator = AlternativeCombinator( + SequenceCombinator( + LiteralChoiceTerminal("uint8"), + NETWORK_DHCP_MISC_COALESCED_COLONS, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT8_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + SequenceCombinator( + LiteralChoiceTerminal("uint16"), + NETWORK_DHCP_MISC_COALESCED_COLONS, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT16_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + SequenceCombinator( + LiteralChoiceTerminal("uint32"), + NETWORK_DHCP_MISC_COALESCED_COLONS, + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + ), + SequenceCombinator(LiteralChoiceTerminal("ipv4address"), NETWORK_DHCP_MISC_COALESCED_COLONS, IPV4_ADDR), + SequenceCombinator(LiteralChoiceTerminal("ipv6address"), NETWORK_DHCP_MISC_COALESCED_COLONS, IPV6_ADDR_NO_LEADING_COLON), + SequenceCombinator(LiteralChoiceTerminal("string"), NETWORK_DHCP_MISC_COALESCED_COLONS, C_ESCAPED_TEXT), +) + +// --------------------------------------------------------------------------------------------------- +// Time spans — parse_sec() / parse_time(…, USEC_PER_SEC). +// +// leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1146-L1196 extract_multiplier +// leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time +// tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c + +// extract_multiplier's table, in table order; the first prefix that matches wins (startswith, no +// longest-match), hence the atomic group. Note: no `ns`/`nsec` — those belong to parse_nsec only. +private const val PARSE_SEC_UNIT_REGEX = + "(?>seconds|second|sec|s|minutes|minute|min|months|month|M|msec|ms|m|hours|hour|hr|h|days|day|d|weeks|week|w|years|year|y|usec|us|\u03bcs|\u00b5s)" + +/** + * One number of a parse_time() element: strtoll() digits (an optional `+`, never `-`) with an optional + * `.FRACTION`, or a bare `.FRACTION` (strtoll converts nothing, then the `.` branch runs: `.5s` is + * legal). A trailing `.` without digits (`5.`, `5.s`) is refused. + * + * The lookahead encodes the one adjacency rule of parse_time(): when no unit follows a number, the next + * element must be separated by whitespace (`5.5.5` and `5+5` fail, `5 .5` and `5s.5` are fine). So a + * number may only be followed by whitespace, the end, or the first letter of a unit. + */ +private val PARSE_SEC_NUMBER = RegexTerminal( + """\+?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]*)""", + """(?:\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+)(?=[ \t\n\r]|\z|[smMhdwyu\u03bc\u00b5])""", +) + +private val PARSE_SEC_UNIT = RegexTerminal(PARSE_SEC_UNIT_REGEX, PARSE_SEC_UNIT_REGEX) + +private val PARSE_SEC_ELEMENT = SequenceCombinator( + PARSE_SEC_NUMBER, + ZeroOrOne(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), PARSE_SEC_UNIT)), +) + +/** + * A time span exactly as parse_sec() reads it: `infinity`, or one or more `NUMBER [UNIT]` elements whose + * values are summed, with optional whitespace anywhere between a number and its unit and between + * elements — elements may also be written back to back after a unit (`1h30min`, `55s500ms`, `5s.5`). + * The default unit is seconds. A `-` anywhere at the start of an element is refused (`-0` too). + * + * Overflow (a sum >= USEC_INFINITY) is not modelled. The shared TIME_VALUE in Combinators.kt is both + * stricter (it needs whitespace between elements and a digit before `.`) and looser (it accepts the + * `ns`/`nsec` units parse_sec doesn't know); this one follows the C. + */ +val PARSE_SEC_TIMESPAN = AlternativeCombinator( + LiteralChoiceTerminal("infinity"), + SequenceCombinator(PARSE_SEC_ELEMENT, ZeroOrMore(SequenceCombinator(ZeroOrOne(WhitespaceTerminal()), PARSE_SEC_ELEMENT))), +) + +private val NETWORK_DHCP_MISC_PARSE_SEC_MULTIPLIERS: Map = mapOf( + "seconds" to 1_000_000L, "second" to 1_000_000L, "sec" to 1_000_000L, "s" to 1_000_000L, + "minutes" to 60_000_000L, "minute" to 60_000_000L, "min" to 60_000_000L, "m" to 60_000_000L, + "months" to 2_629_800_000_000L, "month" to 2_629_800_000_000L, "M" to 2_629_800_000_000L, + "msec" to 1_000L, "ms" to 1_000L, + "hours" to 3_600_000_000L, "hour" to 3_600_000_000L, "hr" to 3_600_000_000L, "h" to 3_600_000_000L, + "days" to 86_400_000_000L, "day" to 86_400_000_000L, "d" to 86_400_000_000L, + "weeks" to 604_800_000_000L, "week" to 604_800_000_000L, "w" to 604_800_000_000L, + "years" to 31_557_600_000_000L, "year" to 31_557_600_000_000L, "y" to 31_557_600_000_000L, + "usec" to 1L, "us" to 1L, "\u03bcs" to 1L, "\u00b5s" to 1L, +) + +private val NETWORK_DHCP_MISC_PARSE_SEC_STEP = + Regex("""[ \t\n\r]*\+?([0-9]*(?:\.[0-9]*)?)[ \t\n\r]*($PARSE_SEC_UNIT_REGEX)?""") + +/** + * The duration, in microseconds, that parse_sec() computes for a [PARSE_SEC_TIMESPAN]-shaped [text] + * (the elements summed, the default unit seconds, `infinity` as [Long.MAX_VALUE]), or null if [text] + * isn't of that shape. Fractions are evaluated exactly rather than digit-by-digit as the C does; the two + * differ by less than a microsecond per element. + */ +fun networkDhcpMiscParseSecUsec(text: String): Long? { + if (text == "infinity") return Long.MAX_VALUE + var total = java.math.BigDecimal.ZERO + var i = 0 + while (i < text.length) { + val m = NETWORK_DHCP_MISC_PARSE_SEC_STEP.matchAt(text, i) ?: return null + val number = m.groupValues[1] + if (m.value.isEmpty() || number.isEmpty() || number == ".") return null + val multiplier = m.groups[2]?.value?.let { NETWORK_DHCP_MISC_PARSE_SEC_MULTIPLIERS[it] ?: return null } ?: 1_000_000L + total += java.math.BigDecimal(if (number.startsWith(".")) "0$number" else number.trimEnd('.')) * java.math.BigDecimal(multiplier) + i = m.range.last + 1 + } + return total.toBigInteger().min(java.math.BigInteger.valueOf(Long.MAX_VALUE)).toLong() +} + +/** + * Wraps [inner] and additionally requires the whole span it matched to satisfy [valid] — for rules + * that are a computation over a structured value (a minimum duration, an excluded address range, a + * reserved name) rather than part of its shape. Matching, colouring and completion are [inner]'s; when + * [valid] fails, the first token of the span is reported as the semantic error (both engines). + */ +class NetworkDhcpMiscCheckedSpan( + private val inner: Combinator, + private val name: String, + private val valid: (String) -> Boolean, +) : Combinator { + + override fun SyntacticMatch(value: String, offset: Int): MatchResult = inner.SyntacticMatch(value, offset) + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val m = inner.SemanticMatch(value, offset) + if (m.matchResult == -1 || valid(value.substring(offset, m.matchResult))) return m + return NoMatch.copy(longestMatch = offset) + } + + override fun parse(value: String, offset: Int): Sequence = inner.parse(value, offset).map { step -> + if (step !is Parse || step.tokens.isEmpty() || valid(value.substring(offset, step.end))) step + else step.copy(tokens = listOf(step.tokens[0].copy(valid = false)) + step.tokens.drop(1)) + } + + override fun toString(): String = "$name(${inner})" + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "$name(\n" + inner.toStringIndented(indent + 1) + "\n" + " ".repeat(indent) + ")" +} + +// --------------------------------------------------------------------------------------------------- +// Parsers whose accepted syntax depends on the key or section (they test `lvalue` / `section`). + +/** + * A grammar validator for a (parser, ltype) pair whose C code branches on the key name or the section + * name — e.g. config_parse_dhcp6_send_option (`streq(lvalue, "SendVendorOption")`) and + * config_parse_uplink (`:none` vs `:self` by section). + * + * The registration is still per (parser, ltype), but [generateProblemDescriptors] receives the PSI + * property, so it asks [select] for the grammar that applies to that (key, section) and validates with + * it. [combinator] (the [lenientUnion] of all shapes) is what the key-independent features — value + * colouring, completion, the deprecation annotator — see; it should accept the union of the shapes. + */ +open class KeyOrSectionDispatchingGrammarOptionValue( + validatorName: String, + lenientUnion: Combinator, + private val select: (key: String, section: String?) -> GrammarOptionValue, +) : GrammarOptionValue(validatorName, lenientUnion) { + + override fun generateProblemDescriptors(property: UnitFilePropertyType, holder: ProblemsHolder) { + val section = PsiTreeUtil.getParentOfType(property, UnitFileSectionGroups::class.java)?.sectionName + select(property.key, section).generateProblemDescriptors(property, holder) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseActivationPolicyOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseActivationPolicyOptionValueTest.kt new file mode 100644 index 00000000..f210dba0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseActivationPolicyOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseActivationPolicyOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ActivationPolicy=up + ActivationPolicy=always-up + ActivationPolicy=manual + ActivationPolicy=always-down + ActivationPolicy=down + ActivationPolicy=bound + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + ActivationPolicy=yes + ActivationPolicy=Up + ActivationPolicy=always + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBridgeVlanIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBridgeVlanIdOptionValueTest.kt new file mode 100644 index 00000000..491e3fee --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBridgeVlanIdOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBridgeVlanIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [BridgeVLAN] + PVID=1020 + PVID=0 + PVID=4094 + PVID=0x10 + PVID=no + PVID=False + PVID=oFf + PVID=FALSE + PVID= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BridgeVLAN] + PVID=4095 + PVID=yes + PVID=-1 + PVID=10-20 + PVID=TRUE + PVID=Yes + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp4UserClassOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp4UserClassOptionValueTest.kt new file mode 100644 index 00000000..06c3ac1f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp4UserClassOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcp4UserClassOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + UserClass=foo + UserClass=foo bar baz + UserClass="with space" 'single quoted' + UserClass=esc\x41ped tab\tbed + UserClass=mid"dle quo"te + UserClass="a" + UserClass= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + UserClass="unterminated + UserClass=bad\qescape + UserClass=bad\ escaped-space + UserClass="" + UserClass='' + UserClass=a "" + UserClass="" b + UserClass=""'' + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6SendOptionOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6SendOptionOptionValueTest.kt new file mode 100644 index 00000000..322b6ba9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6SendOptionOptionValueTest.kt @@ -0,0 +1,63 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcp6SendOptionOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + SendOption=1:string:\x41 + SendOption=65534:uint8:1 + SendOption=0x10:uint16:0x1234 + SendOption=23:ipv6address:2001:db8::53 + SendOption=23:ipv4address:10.0.0.1 + SendOption=12:uint32:4294967295 + SendOption=1:ipv6address::2001:db8::1 + SendOption=1:uint8:0b+1 + SendVendorOption=1: 2:uint8:0b 1 + SendVendorOption=123: 1:string:x + SendVendorOption=123:1:string:hello + SendVendorOption=0:1:uint8:2 + SendVendorOption=4294967295:65534:string:x + SendVendorOption= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // An unknown TYPE (`bytes`, `int8`, `text`) makes systemd drop the WHOLE .network file ("Failed to parse + // file: Invalid argument"): DHCPOptionDataType has no negative enumerator, so the C's `type < 0` check + // never fires and the switch's `default: return -EINVAL` does (sd-check, 0f0682b773). + // language="unit file (systemd)" + val file = """ + [DHCPv6] + SendOption=0:string:x + SendOption=65535:string:x + SendOption=1:uint8: + SendOption=1:ipv6address:52:54:00:b9:b5:61 + SendOption=1:string:\U + SendVendorOption=4294967296:1:string:x + SendVendorOption=123:1:bytes:x + SendVendorOption=12:string:x + SendOption=123:1:string:x + SendOption=1:ipv6address:::1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(10, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6UserOrVendorClassOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6UserOrVendorClassOptionValueTest.kt new file mode 100644 index 00000000..2f1a3ff0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcp6UserOrVendorClassOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcp6UserOrVendorClassOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + UserClass=foo bar + UserClass="quoted value" + VendorClass=vendor-1 vendor\s2 + VendorClass= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + UserClass='unterminated + VendorClass=bad\zescape + UserClass="" + VendorClass="" + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest.kt new file mode 100644 index 00000000..284222df --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpFallbackLeaseLifetimeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + FallbackLeaseLifetimeSec=forever + FallbackLeaseLifetimeSec=infinity + FallbackLeaseLifetimeSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + FallbackLeaseLifetimeSec=5min + FallbackLeaseLifetimeSec=never + FallbackLeaseLifetimeSec=Infinity + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpLabelOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpLabelOptionValueTest.kt new file mode 100644 index 00000000..3c72db9e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpLabelOptionValueTest.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpLabelOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + Label=label1 + Label=eth0:dhcp + Label=4444 + Label=a b/c%d + Label=exactly15chars! + Label= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + Label=sixteen-chars-xx + Label=café + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(2, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTlvOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTlvOptionValueTest.kt new file mode 100644 index 00000000..86c9b32f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTlvOptionValueTest.kt @@ -0,0 +1,76 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpOptionTlvOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // [DHCPRelay] ExtraOption= shares this parser, but the plugin's section data doesn't know [DHCPRelay] + // yet, so it can't be exercised here. + // language="unit file (systemd)" + val file = """ + [DHCPv4] + SendOption=12:string:test + SendOption=1:uint8:255 + SendOption=0x0c:uint16:0xffff + SendOption=254:uint32:4294967295 + SendOption=014:ipv4address:192.168.1.1 + SendOption=17:ipv6address:2001:db8::1 + SendOption=60:string:has:colons:inside + SendOption=60:string:tab\there\x41\u00e9\101 é + SendOption=12::uint8::5 + SendOption=12:uint8: 7 + SendOption=12:uint8:0b+1 + SendOption=12:uint8:0b 1 + SendOption=12:ipv6address::2001:db8::1 + SendOption=12:ipv6address:::2001:db8::1 + SendOption= + SendVendorOption=43:string:vendor + [DHCPServer] + SendOption=42:ipv4address:10.0.0.1 + SendVendorOption=43:uint8:1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // An unknown TYPE (`bytes`, `int8`, `text`) makes systemd drop the WHOLE .network file ("Failed to parse + // file: Invalid argument"): DHCPOptionDataType has no negative enumerator, so the C's `type < 0` check + // never fires and the switch's `default: return -EINVAL` does (sd-check, 0f0682b773). + // language="unit file (systemd)" + val file = """ + [DHCPv4] + SendOption=0:string:test + SendOption=255:string:test + SendOption=12:uint8:256 + SendOption=12:uint16:65536 + SendOption=12:int8:5 + SendOption=12:ipv4address:300.1.1.1 + SendOption=12:string: + SendOption=12:string:bad\qescape + SendOption=12 + SendOption=abc:string:x + [DHCPServer] + SendVendorOption=12:ipv6address:1.2.3.4 + SendVendorOption=12:uint32:-1 + SendOption=12:ipv6address:::1 + SendOption=12:ipv6address:: + SendOption=12:uint8:0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(15, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTypedOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTypedOptionValueTest.kt new file mode 100644 index 00000000..c8caec50 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOptionTypedOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpOptionTypedOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // [DHCPRelay] CircuitId=/RemoteId=/VirtualSubnetSelection= share this parser, but the plugin's + // section data doesn't know [DHCPRelay] yet; the deprecated [DHCPServer] spellings exercise it. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + RelayAgentCircuitId=string:sample_circuit_id + RelayAgentRemoteId=string:aabbccdd + RelayAgentRemoteId=uint8:0xff + RelayAgentRemoteId=uint16:65535 + RelayAgentRemoteId=uint32:0 + RelayAgentRemoteId=ipv4address:192.0.2.1 + RelayAgentRemoteId=ipv6address:fe80::1 + RelayAgentRemoteId=ipv6address::2001:db8::1 + RelayAgentRemoteId=string:with\x00nul\ttab + RelayAgentRemoteId=::string:leading-colons + RelayAgentCircuitId= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // An unknown TYPE (`bytes`, `int8`, `text`) makes systemd drop the WHOLE .network file ("Failed to parse + // file: Invalid argument"): DHCPOptionDataType has no negative enumerator, so the C's `type < 0` check + // never fires and the switch's `default: return -EINVAL` does (sd-check, 0f0682b773). + // language="unit file (systemd)" + val file = """ + [DHCPServer] + RelayAgentCircuitId=sample_circuit_id + RelayAgentCircuitId=text:hello + RelayAgentCircuitId=uint8:256 + RelayAgentCircuitId=uint16:-1 + RelayAgentCircuitId=ipv4address:192.0.2.1/24 + RelayAgentCircuitId=string + RelayAgentRemoteId=ipv6address:::1 + RelayAgentRemoteId=ipv6address::ffff:192.0.2.1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOrRaRouteTableOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOrRaRouteTableOptionValueTest.kt new file mode 100644 index 00000000..7023d304 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpOrRaRouteTableOptionValueTest.kt @@ -0,0 +1,77 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +/** + * RouteTable= in [DHCPv4], [DHCPv6] and [IPv6AcceptRA] (config_parse_dhcp_or_ra_route_table). Every value was checked + * with systemd's parser at 0f0682b773 on all three keys, which agree. `mytable` and `Main` are rejected there only + * because sd-check loads no networkd.conf; they are accepted here on purpose (a networkd.conf RouteTable=NAME:NUMBER + * could define them). + */ +class ConfigParseDhcpOrRaRouteTableOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + RouteTable=211 + RouteTable=main + RouteTable=default + RouteTable=local + RouteTable=4294967295 + RouteTable=0x10 + RouteTable=010 + RouteTable=0b 1 + RouteTable=mytable + RouteTable=Main + RouteTable= + [DHCPv6] + RouteTable=211 + RouteTable=local + RouteTable=0x10 + RouteTable=mytable + [IPv6AcceptRA] + RouteTable=211 + RouteTable=main + RouteTable=0b 1 + RouteTable=mytable + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + RouteTable=0 + RouteTable=00 + RouteTable=08 + RouteTable=4294967296 + RouteTable=a:b + RouteTable=1:2 + [DHCPv6] + RouteTable=0 + RouteTable=4294967296 + RouteTable=a:b + [IPv6AcceptRA] + RouteTable=0 + RouteTable=08 + RouteTable=1:2 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValueTest.kt new file mode 100644 index 00000000..898c56f5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdPrefixRouteTypeOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpPdPrefixRouteTypeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + UnassignedSubnetPolicy=none + UnassignedSubnetPolicy=unreachable + UnassignedSubnetPolicy= + [DHCPv6] + UnassignedSubnetPolicy=blackhole + UnassignedSubnetPolicy=prohibit + UnassignedSubnetPolicy=throw + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + UnassignedSubnetPolicy=unicast + UnassignedSubnetPolicy=no + [DHCPv6] + UnassignedSubnetPolicy=local + UnassignedSubnetPolicy=Blackhole + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdSubnetIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdSubnetIdOptionValueTest.kt new file mode 100644 index 00000000..61dc584e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpPdSubnetIdOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpPdSubnetIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // SubnetId= is always hexadecimal (safe_atoux64), so `ff` and `add` are numbers. + // language="unit file (systemd)" + val file = """ + [DHCPPrefixDelegation] + SubnetId=auto + SubnetId=0 + SubnetId=10 + SubnetId=ff + SubnetId=0xabcd + SubnetId=add + SubnetId=7fffffffffffffff + SubnetId= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPPrefixDelegation] + SubnetId=8000000000000000 + SubnetId=Auto + SubnetId=xyz + SubnetId=1g + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRelayInterfaceModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRelayInterfaceModeOptionValueTest.kt new file mode 100644 index 00000000..47179f70 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRelayInterfaceModeOptionValueTest.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpRelayInterfaceModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DHCPRelay=upstream + DHCPRelay=downstream + DHCPRelay= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DHCPRelay=yes + DHCPRelay=both + DHCPRelay=Upstream + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRequestOptionsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRequestOptionsOptionValueTest.kt new file mode 100644 index 00000000..43659785 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpRequestOptionsOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpRequestOptionsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + RequestOptions=59 60 + RequestOptions=17 + RequestOptions=1 254 0xfe 012 + RequestOptions=0b+1 0b1 + RequestOptions= + [DHCPv6] + RequestOptions=23 24 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + RequestOptions=0 + RequestOptions=255 + RequestOptions=59,60 + RequestOptions=0b 1 + RequestOptions=0o 7 + [DHCPv6] + RequestOptions=dns + RequestOptions=0b-0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerAddressOptionValueTest.kt new file mode 100644 index 00000000..4284b30e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerAddressOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpServerAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + ServerAddress=192.168.5.1/24 + ServerAddress=0.0.0.0/24 + ServerAddress=10.0.0.1 + ServerAddress=10.0.0.1/0 + ServerAddress=10.0.0.1/32 + ServerAddress=10.0.0.1/0x18 + ServerAddress=10.0.0.1/ 24 + ServerAddress=169.253.255.255/16 + ServerAddress=128.0.0.1 + ServerAddress= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + ServerAddress=192.168.5.1/33 + ServerAddress=2001:db8::1/64 + ServerAddress=192.168.5 + ServerAddress=server.example.com + ServerAddress=127.0.0.1/8 + ServerAddress=127.255.0.1 + ServerAddress=169.254.1.1/16 + ServerAddress=169.254.0.0/16 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest.kt new file mode 100644 index 00000000..a797d59b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpServerIpv6OnlyPreferredOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Anything >= MIN_V6ONLY_WAIT_USEC (300s) is accepted by systemd's parser (sd-check, 0f0682b773). + // language="unit file (systemd)" + val file = """ + [DHCPServer] + IPv6OnlyPreferredSec=5min + IPv6OnlyPreferredSec=5m + IPv6OnlyPreferredSec=4min 60s + IPv6OnlyPreferredSec=300000000us + IPv6OnlyPreferredSec=1M + IPv6OnlyPreferredSec=300 + IPv6OnlyPreferredSec=1h 5min + IPv6OnlyPreferredSec=1h30min + IPv6OnlyPreferredSec=.5h + IPv6OnlyPreferredSec=10min5s + IPv6OnlyPreferredSec=infinity + IPv6OnlyPreferredSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + IPv6OnlyPreferredSec=yes + IPv6OnlyPreferredSec=5 fortnights + IPv6OnlyPreferredSec=-5min + IPv6OnlyPreferredSec=1h. + IPv6OnlyPreferredSec=1.2.3 + IPv6OnlyPreferredSec=Infinity + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + + @Test + fun testBelowMinimumIsInvalid() { + // systemd: "Invalid [DHCPServer] IPv6OnlyPreferredSec=, ignoring assignment" for every value below + // 300s outside its test mode (sd-check, 0f0682b773). 20s is systemd's own test-suite value. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + IPv6OnlyPreferredSec=20s + IPv6OnlyPreferredSec=0 + IPv6OnlyPreferredSec=299 + IPv6OnlyPreferredSec=299.999999s + IPv6OnlyPreferredSec=4min + IPv6OnlyPreferredSec=299999999us + IPv6OnlyPreferredSec=0.0833333h + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpStaticLeaseHostnameOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpStaticLeaseHostnameOptionValueTest.kt new file mode 100644 index 00000000..e6781889 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDhcpStaticLeaseHostnameOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDhcpStaticLeaseHostnameOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServerStaticLease] + Hostname=test-hostname + Hostname=fqdn.example.com + Hostname=fqdn.example.com. + Hostname=0123 + Hostname=a + Hostname=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com + Hostname= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServerStaticLease] + Hostname=-leading + Hostname=trailing- + Hostname=under_score + Hostname=double..dot + Hostname=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + Hostname=with\.escape + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnsOverTlsModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnsOverTlsModeOptionValueTest.kt new file mode 100644 index 00000000..17f9289e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnsOverTlsModeOptionValueTest.kt @@ -0,0 +1,47 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDnsOverTlsModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DNSOverTLS=opportunistic + DNSOverTLS=yes + DNSOverTLS=no + DNSOverTLS=TRUE + DNSOverTLS=1 + DNSOverTLS=Yes + DNSOverTLS=oFf + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DNSOverTLS=Opportunistic + DNSOverTLS=strict + DNSOverTLS=allow-downgrade + DNSOverTLS=OPPORTUNISTIC + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValueTest.kt new file mode 100644 index 00000000..bb226b27 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDnssecNegativeTrustAnchorsOptionValueTest.kt @@ -0,0 +1,48 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDnssecNegativeTrustAnchorsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DNSSECNegativeTrustAnchors=example.com + DNSSECNegativeTrustAnchors=test local. corp.example + DNSSECNegativeTrustAnchors=. + DNSSECNegativeTrustAnchors=_tcp.example + DNSSECNegativeTrustAnchors=foo\.bar + DNSSECNegativeTrustAnchors=foo\\\.bar + DNSSECNegativeTrustAnchors=localhost + DNSSECNegativeTrustAnchors= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + DNSSECNegativeTrustAnchors=a..b + DNSSECNegativeTrustAnchors=good.example .. + DNSSECNegativeTrustAnchors=~example.com.. + DNSSECNegativeTrustAnchors=foo\\bar + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDomainsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDomainsOptionValueTest.kt new file mode 100644 index 00000000..7c876458 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDomainsOptionValueTest.kt @@ -0,0 +1,65 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDomainsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + Domains=example.com + Domains=hogehoge ~foofoo + Domains=one two three four five six seven eight nine ten + Domains=~. + Domains=~ + Domains=. + Domains=* + Domains=~* + Domains=example.com. + Domains=_srv.example.com + Domains=foo\.bar + Domains=foo\\\\bar + Domains=foo\\.bar + Domains=foo\\\.bar + Domains=foo\\065bar + Domains=localhost.example.com + Domains= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + Domains=example..com + Domains=.example.com + Domains=example.com.. + Domains=ok.example xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.com + Domains=localhost + Domains=foo.localhost + Domains=~localhost + Domains=Foo.LocalHost. + Domains=localhost.localdomain + Domains=x.localhost.localdomain + Domains=foo\\bar + Domains=foo\\256bar + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidRawdataOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidRawdataOptionValueTest.kt new file mode 100644 index 00000000..d9e9db1d --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidRawdataOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDuidRawdataOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // [DHCPv6] uses config_parse_duid_rawdata and [DHCPv4] config_parse_network_duid_rawdata. + // Accepted/rejected spellings mirror systemd's test-networkd-conf.c. + // language="unit file (systemd)" + val file = """ + [DHCPv6] + DUIDRawData=00:00:ab:11:f9:2a:c2:77:29:f9:5c:00 + DUIDRawData=00:11:22:33:44:55:66:77 + DUIDRawData=00:11:22: + DUIDRawData=0:1:2:3:4:5:6:7 + DUIDRawData=11:: + DUIDRawData=ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab + DUIDRawData= + [DHCPv4] + DUIDRawData=AB:cd + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + DUIDRawData=000:11:22 + DUIDRawData=00:111:22 + DUIDRawData=abcdef + DUIDRawData=ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab:ab + [DHCPv4] + DUIDRawData=00:gg + DUIDRawData=00: 11 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidTypeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidTypeOptionValueTest.kt new file mode 100644 index 00000000..703d8e4e --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDuidTypeOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDuidTypeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // [DHCPv6] uses config_parse_duid_type and [DHCPv4] config_parse_network_duid_type; same grammar. + // Values from systemd's test-networkd-conf.c and test-network fixtures. + // language="unit file (systemd)" + val file = """ + [DHCPv6] + DUIDType=link-layer-time + DUIDType=link-layer-time:2000-01-01 00:00:00 UTC + DUIDType=vendor + DUIDType=link-layer + DUIDType=uuid + DUIDType=0x42 + DUIDType=1:2000-01-01 00:00:00 UTC + DUIDType=vendor: + [DHCPv4] + DUIDType=uuid + DUIDType=link-layer-time:@1700000000 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv6] + DUIDType=foo + DUIDType=vendor:2000-01-01 00:00:00 UTC + DUIDType=uuid:2000-01-01 00:00:00 UTC + DUIDType=65536 + [DHCPv4] + DUIDType=Vendor + DUIDType=2:2000-01-01 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFdbInterfaceOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFdbInterfaceOptionValueTest.kt new file mode 100644 index 00000000..d98cb471 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFdbInterfaceOptionValueTest.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFdbInterfaceOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [BridgeFDB] + OutgoingInterface=test1 + OutgoingInterface=dummy98 + OutgoingInterface=7 + OutgoingInterface=010 + OutgoingInterface=0b 1 + OutgoingInterface=0o 7 + OutgoingInterface=an-alternative-name-longer-than-fifteen + OutgoingInterface= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BridgeFDB] + OutgoingInterface=0 + OutgoingInterface=default + OutgoingInterface=a%d + OutgoingInterface=.. + OutgoingInterface=08 + OutgoingInterface=eth 0 + OutgoingInterface=0b -1 + OutgoingInterface=4294967297 + OutgoingInterface=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(9, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIgnoreCarrierLossOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIgnoreCarrierLossOptionValueTest.kt new file mode 100644 index 00000000..6f104590 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIgnoreCarrierLossOptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIgnoreCarrierLossOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IgnoreCarrierLoss=true + IgnoreCarrierLoss=false + IgnoreCarrierLoss=No + IgnoreCarrierLoss=Yes + IgnoreCarrierLoss=TRUE + IgnoreCarrierLoss=oFf + IgnoreCarrierLoss=5s + IgnoreCarrierLoss=1min 30s + IgnoreCarrierLoss=infinity + IgnoreCarrierLoss=0 + IgnoreCarrierLoss=1h30min + IgnoreCarrierLoss=55s500ms + IgnoreCarrierLoss=.5s + IgnoreCarrierLoss=5s.5 + IgnoreCarrierLoss=+3.1s + IgnoreCarrierLoss=3.1 sec .2 sec + IgnoreCarrierLoss=2.5 + IgnoreCarrierLoss=23µs + IgnoreCarrierLoss=5 min + IgnoreCarrierLoss= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IgnoreCarrierLoss=sometimes + IgnoreCarrierLoss=5 parsecs + IgnoreCarrierLoss=-1s + IgnoreCarrierLoss=5. + IgnoreCarrierLoss=12.34.56 + IgnoreCarrierLoss=3.s + IgnoreCarrierLoss=3. 1s + IgnoreCarrierLoss=..1234 + IgnoreCarrierLoss=5ns + IgnoreCarrierLoss=infinity .7 + IgnoreCarrierLoss=5+5 + IgnoreCarrierLoss=Infinity + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llAddressOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llAddressOptionValueTest.kt new file mode 100644 index 00000000..93e6e58f --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llAddressOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIpv4llAddressOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv4LLStartAddress=169.254.133.11 + IPv4LLStartAddress=169.254.1.0 + IPv4LLStartAddress=169.254.254.255 + IPv4LLStartAddress= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv4LLStartAddress=169.254.0.5 + IPv4LLStartAddress=169.254.255.1 + IPv4LLStartAddress=192.168.1.1 + IPv4LLStartAddress=169.254.1.256 + IPv4LLStartAddress=fe80::1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llOptionValueTest.kt new file mode 100644 index 00000000..5189d038 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseIpv4llOptionValueTest.kt @@ -0,0 +1,46 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseIpv4llOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv4LL=yes + IPv4LL=no + IPv4LL=True + IPv4LL=Yes + IPv4LL=TRUE + IPv4LL=Off + IPv4LL=oFf + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + IPv4LL=ipv4 + IPv4LL=both + IPv4LL=Yess + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(3, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkGroupOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkGroupOptionValueTest.kt new file mode 100644 index 00000000..b1df989c --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLinkGroupOptionValueTest.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseLinkGroupOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Group=0 + Group=2147483647 + Group=0x10 + Group=010 + Group=0b101 + Group=0o17 + Group=+5 + Group= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + Group=-1 + Group=2147483648 + Group=root + Group=08 + Group=1.5 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLldpMulticastModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLldpMulticastModeOptionValueTest.kt new file mode 100644 index 00000000..8ea15587 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseLldpMulticastModeOptionValueTest.kt @@ -0,0 +1,50 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseLldpMulticastModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + EmitLLDP=nearest-bridge + EmitLLDP=non-tpmr-bridge + EmitLLDP=customer-bridge + EmitLLDP=yes + EmitLLDP=no + EmitLLDP=On + EmitLLDP=Yes + EmitLLDP=TRUE + EmitLLDP=oFf + EmitLLDP= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + EmitLLDP=nearest + EmitLLDP=Customer-Bridge + EmitLLDP=routers-only + EmitLLDP=Nearest-Bridge + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMudUrlOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMudUrlOptionValueTest.kt new file mode 100644 index 00000000..176ab8e1 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseMudUrlOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseMudUrlOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + MUDURL=https://www.example.com/mud.json + MUDURL=http://example.com/a b + MUDURL=https://example.com/\x41 + MUDURL= + [DHCPv6] + MUDURL=https://example.com/ + [LLDP] + MUDURL=http://x + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPv4] + MUDURL=ftp://example.com/mud.json + MUDURL=https:// + MUDURL=HTTPS://example.com/ + [DHCPv6] + MUDURL=https://example.com/café + [LLDP] + MUDURL=https://example.com/\q + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNtpOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNtpOptionValueTest.kt new file mode 100644 index 00000000..1de65ec4 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNtpOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNtpOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + NTP=0.fedora.pool.ntp.org 1.fedora.pool.ntp.org + NTP=192.168.5.1 + NTP=2001:db8::123 + NTP=ntp.example.com. + NTP=time + NTP=. + NTP=foo\.bar + NTP=foo\qbar + NTP=foo\065bar + NTP=foo\\\\bar + NTP=foo\\.bar + NTP=foo\\065bar + NTP=localhost.localdomain + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + NTP=pool..ntp.org + NTP=.ntp.org + NTP=ok.example bad..example + NTP=foo\\bar + NTP=foo\\256bar + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRadvDnsOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRadvDnsOptionValueTest.kt new file mode 100644 index 00000000..50460140 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRadvDnsOptionValueTest.kt @@ -0,0 +1,45 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRadvDnsOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + DNS=_link_local 2002:da8:1:0::1 + DNS=2001:db8:1:1::2 + DNS=fe80::1 fe80::2 + DNS=::ffff:192.0.2.1 + DNS= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [IPv6SendRA] + DNS=192.168.1.1 + DNS=[2001:db8::1]:53 + DNS=_link_locals + DNS=2001:db8::1%eth0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(4, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRequiredForOnlineOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRequiredForOnlineOptionValueTest.kt new file mode 100644 index 00000000..09199dbb --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseRequiredForOnlineOptionValueTest.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseRequiredForOnlineOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RequiredForOnline=routable + RequiredForOnline=no + RequiredForOnline=yes + RequiredForOnline=False + RequiredForOnline=Yes + RequiredForOnline=TRUE + RequiredForOnline=oFf + RequiredForOnline=degraded-carrier + RequiredForOnline=carrier:routable + RequiredForOnline=off: + RequiredForOnline=:degraded + RequiredForOnline=no-carrier + RequiredForOnline=enslaved + RequiredForOnline= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Link] + RequiredForOnline=online + RequiredForOnline=: + RequiredForOnline=routable:bogus + RequiredForOnline=carrier:routable:off + RequiredForOnline=Routable + RequiredForOnline=No-Carrier + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolveSupportOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolveSupportOptionValueTest.kt new file mode 100644 index 00000000..4a73ffd2 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseResolveSupportOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseResolveSupportOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + LLMNR=resolve + LLMNR=yes + LLMNR=no + LLMNR=Yes + LLMNR=0 + MulticastDNS=resolve + MulticastDNS=true + MulticastDNS=OFF + MulticastDNS=TRUE + MulticastDNS=oFf + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [Network] + LLMNR=Resolve + MulticastDNS=RESOLVE + LLMNR=maybe + MulticastDNS=allow-downgrade + MulticastDNS=yes no + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUplinkOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUplinkOptionValueTest.kt new file mode 100644 index 00000000..4e076f37 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseUplinkOptionValueTest.kt @@ -0,0 +1,69 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseUplinkOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // `:none` is only accepted in [DHCPServer]/[IPv6SendRA] and `:self` only in [DHCPPrefixDelegation]. + // language="unit file (systemd)" + val file = """ + [DHCPServer] + UplinkInterface=:auto + UplinkInterface=:none + UplinkInterface=dummy98 + UplinkInterface= + [DHCPPrefixDelegation] + UplinkInterface=:self + UplinkInterface=auto + UplinkInterface=veth99 + UplinkInterface=12 + UplinkInterface=1eth + [IPv6SendRA] + UplinkInterface=this-is-a-long-alternative-interface-name + UplinkInterface=0x10 + UplinkInterface=:none + UplinkInterface=010 + UplinkInterface=2147483647 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DHCPServer] + UplinkInterface=:bogus + UplinkInterface=0 + UplinkInterface=eth0:1 + [DHCPPrefixDelegation] + UplinkInterface=all + UplinkInterface=eth/0 + [IPv6SendRA] + UplinkInterface=eth0 eth1 + UplinkInterface=:self + UplinkInterface=99999999999 + UplinkInterface=2147483648 + UplinkInterface=08 + [DHCPServer] + UplinkInterface=:self + [DHCPPrefixDelegation] + UplinkInterface=:none + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(12, highlights) + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkDhcpMiscSafeAtoVerdictTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkDhcpMiscSafeAtoVerdictTest.kt new file mode 100644 index 00000000..72e9b038 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/NetworkDhcpMiscSafeAtoVerdictTest.kt @@ -0,0 +1,97 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseBridgeVlanIdOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcp6SendOptionOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpOptionTlvOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpOrRaRouteTableOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpPdSubnetIdOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpRequestOptionsOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDhcpServerAddressOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseDuidTypeOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseFdbInterfaceOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseLinkGroupOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai.ConfigParseUplinkOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.Combinator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarEngineVerdict +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test + +/** + * Verdicts of the network-dhcp-misc validators for values that are awkward to write in a unit-file fixture + * (a vertical tab, a no-break space) or whose verdict changed when the batch's own integer terminal was replaced by + * the shared SafeAtoTerminal (#563). Checked on both grammar engines at once. + * + * Every value here was run through systemd's real parsers at 0f0682b773 (steve-local/systemd-tools/bin/sd-check, + * one value per .network file). + */ +class NetworkDhcpMiscSafeAtoVerdictTest { + + private val vt = "\u000B" + private val nbsp = " " + + private fun accepted(grammar: Combinator, vararg values: String) { + for (value in values) { + assertEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(grammar, value)) + assertEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(grammar, value)) + } + } + + private fun rejected(grammar: Combinator, vararg values: String) { + for (value in values) { + assertNotEquals("classic engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.classic(grammar, value)) + assertNotEquals("parse engine on <$value>", GrammarEngineVerdict.VALID, GrammarEngineVerdict.parseEngine(grammar, value)) + } + } + + @Test + fun testWholeValueIntegersAcceptLeadingVerticalTab() { + // strtoul skips C isspace(), which includes \v; config_parse only strips " \t\n\r". + accepted(ConfigParseBridgeVlanIdOptionValue().combinator, "${vt}5", "0b${vt}1") + accepted(ConfigParseLinkGroupOptionValue().combinator, "${vt}5", "0b${vt}1") + accepted(ConfigParseDhcpPdSubnetIdOptionValue().combinator, "${vt}5", "-0", "0b1") + accepted(ConfigParseDuidTypeOptionValue().combinator, "${vt}5", "0b 1:2000-01-01") + accepted(ConfigParseDhcpOrRaRouteTableOptionValue().combinator, "${vt}5", "0b 1") + } + + @Test + fun testSignedGroupRefusesNegativeAfterPrefix() { + rejected(ConfigParseLinkGroupOptionValue().combinator, "0b -1", "0b-1") + } + + @Test + fun testUnsignedWrapAround() { + // strtoul negates modulo 2^64 when the '-' is not the first character, so this is 1 (DUID-LLT). + accepted(ConfigParseDuidTypeOptionValue().combinator, "0b -" + "1".repeat(64)) + } + + @Test + fun testFieldsAfterColonOrSlashTakeCWhitespaceOnly() { + accepted(ConfigParseDhcpServerAddressOptionValue().combinator, "192.168.5.1/${vt}24", "192.168.5.1/ 24", "192.168.5.1/0b 11000") + rejected(ConfigParseDhcpServerAddressOptionValue().combinator, "192.168.5.1/${nbsp}24") + + accepted(ConfigParseDhcpOptionTlvOptionValue().combinator, "12:uint8:${vt}1", "${vt}12:uint8:1", "0b 1100:uint8:1", "12:uint16: 0b 1") + rejected(ConfigParseDhcpOptionTlvOptionValue().combinator, "${nbsp}12:uint8:1", "12:uint8:${nbsp}1") + + accepted(ConfigParseDhcp6SendOptionOptionValue().combinator, "${vt}12:uint8:1", "${vt}1:12:uint8:1") + rejected(ConfigParseDhcp6SendOptionOptionValue().combinator, "${nbsp}12:uint8:1", "${nbsp}1:12:uint8:1") + + rejected(ConfigParseDuidTypeOptionValue().combinator, "${nbsp}1") + } + + @Test + fun testRequestOptionsSplitsOnWhitespaceBeforeParsing() { + // Each word reaches safe_atou32 alone, so "0b 1" is the invalid word "0b" followed by "1". + rejected(ConfigParseDhcpRequestOptionsOptionValue().combinator, "0b 1", "0o 7", "0b +1", "0b -0", "0b-0") + accepted(ConfigParseDhcpRequestOptionsOptionValue().combinator, "0b+1", "1 0b1 0x10 010") + } + + @Test + fun testInterfaceIndexIsTheWholeValue() { + // parse_ifindex() is safe_atoi on the whole value, whitespace included. + for (grammar in listOf(ConfigParseFdbInterfaceOptionValue().combinator, ConfigParseUplinkOptionValue().combinator)) { + accepted(grammar, "0b 1", "${vt}5", "0o 7", "0x10", "+5", "0x" + "0".repeat(130) + "1") + rejected(grammar, "eth 0", "0b 0", "0b -1", "eth${nbsp}0") + } + } +} From 8504501835c8ff10ac4ca731dcbec697e583bd23 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 24 Oct 2026 12:02:20 -0700 Subject: [PATCH 8/8] feat: validate networkd traffic-control qdisc and class settings Add grammar-based value validation for: AckFilter=, Bandwidth=, CEThresholdSec=, CeilRate=, DefaultClass=, DefaultVirtualQueue=, DelayJitterSec=, DelaySec=, DuplicateRate=, ECN=, FlowIsolationMode=, Flows=, GenericRIO=, Id=, IntervalSec=, LimitBytes=, LossRate=, MaxPacketBytes=, MaximumRate=, PacketLimit=, PeakRate=, Priority=, PriorityMap=, QuantumBytes=, Rate=, RateToQuantum=, TargetSec=, VirtualQueues= Accepted values follow systemd's own parsers (derived from the systemd source at 0f0682b773 and checked against systemd's real parsers built at that commit). Values systemd accepts when parsing but rejects only at unit or container start are accepted; see #565. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../semanticdata/optionvalues/AiGenerated.kt | 26 ++ .../ai/ConfigParseBfifoSizeOptionValue.kt | 29 ++ .../ai/ConfigParseCakeAckFilterOptionValue.kt | 37 ++ .../ai/ConfigParseCakeBandwidthOptionValue.kt | 29 ++ ...igParseCakeFlowIsolationModeOptionValue.kt | 28 ++ .../ai/ConfigParseDrrSizeOptionValue.kt | 30 ++ .../ai/ConfigParseEtsPrioOptionValue.kt | 51 +++ .../ai/ConfigParseEtsQuantaOptionValue.kt | 41 +++ .../ai/ConfigParseFqCodelBoolOptionValue.kt | 29 ++ .../ai/ConfigParseFqCodelU32OptionValue.kt | 44 +++ .../ai/ConfigParseFqCodelUsecOptionValue.kt | 36 ++ .../ai/ConfigParseFqMaxRateOptionValue.kt | 32 ++ .../ai/ConfigParseFqUsecOptionValue.kt | 32 ++ .../ai/ConfigParseGredBoolOptionValue.kt | 29 ++ .../ai/ConfigParseGredU32OptionValue.kt | 49 +++ .../ai/ConfigParseHtbClassRateOptionValue.kt | 30 ++ .../ai/ConfigParseHtbClassU32OptionValue.kt | 42 +++ .../ConfigParseHtbDefaultClassOptionValue.kt | 42 +++ .../ai/ConfigParseHtbU32OptionValue.kt | 42 +++ .../ai/ConfigParseNetemDelayOptionValue.kt | 33 ++ .../ai/ConfigParseNetemRateOptionValue.kt | 34 ++ .../ConfigParsePiePacketLimitOptionValue.kt | 43 +++ .../ai/ConfigParseQfqMaxPacketOptionValue.kt | 32 ++ .../ai/ConfigParseTbfRateOptionValue.kt | 32 ++ .../ai/ConfigParseTeqlIdOptionValue.kt | 44 +++ .../grammar/BatchQdiscCombinators.kt | 324 ++++++++++++++++++ .../ai/ConfigParseBfifoSizeOptionValueTest.kt | 52 +++ ...ConfigParseCakeAckFilterOptionValueTest.kt | 62 ++++ ...ConfigParseCakeBandwidthOptionValueTest.kt | 53 +++ ...rseCakeFlowIsolationModeOptionValueTest.kt | 52 +++ .../ai/ConfigParseDrrSizeOptionValueTest.kt | 51 +++ .../ai/ConfigParseEtsPrioOptionValueTest.kt | 58 ++++ .../ai/ConfigParseEtsQuantaOptionValueTest.kt | 54 +++ .../ConfigParseFqCodelBoolOptionValueTest.kt | 64 ++++ .../ConfigParseFqCodelU32OptionValueTest.kt | 96 ++++++ .../ConfigParseFqCodelUsecOptionValueTest.kt | 130 +++++++ .../ai/ConfigParseFqMaxRateOptionValueTest.kt | 51 +++ .../ai/ConfigParseFqUsecOptionValueTest.kt | 51 +++ .../ai/ConfigParseGredBoolOptionValueTest.kt | 64 ++++ .../ai/ConfigParseGredU32OptionValueTest.kt | 88 +++++ .../ConfigParseHtbClassRateOptionValueTest.kt | 78 +++++ .../ConfigParseHtbClassU32OptionValueTest.kt | 68 ++++ ...nfigParseHtbDefaultClassOptionValueTest.kt | 55 +++ .../ai/ConfigParseHtbU32OptionValueTest.kt | 68 ++++ .../ConfigParseNetemDelayOptionValueTest.kt | 100 ++++++ .../ai/ConfigParseNetemRateOptionValueTest.kt | 94 +++++ ...onfigParsePiePacketLimitOptionValueTest.kt | 68 ++++ .../ConfigParseQfqMaxPacketOptionValueTest.kt | 55 +++ .../ai/ConfigParseTbfRateOptionValueTest.kt | 78 +++++ .../ai/ConfigParseTeqlIdOptionValueTest.kt | 60 ++++ 50 files changed, 2870 insertions(+) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBfifoSizeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeAckFilterOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeBandwidthOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeFlowIsolationModeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDrrSizeOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsPrioOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsQuantaOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelBoolOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelU32OptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelUsecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqMaxRateOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqUsecOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredBoolOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredU32OptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassRateOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassU32OptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbDefaultClassOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbU32OptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemDelayOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemRateOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePiePacketLimitOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseQfqMaxPacketOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTbfRateOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTeqlIdOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchQdiscCombinators.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBfifoSizeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeAckFilterOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeBandwidthOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeFlowIsolationModeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDrrSizeOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsPrioOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsQuantaOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelBoolOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelU32OptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelUsecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqMaxRateOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqUsecOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredBoolOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredU32OptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassRateOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassU32OptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbDefaultClassOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbU32OptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemDelayOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemRateOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePiePacketLimitOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseQfqMaxPacketOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTbfRateOptionValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTeqlIdOptionValueTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt index a3eaf99c..07099f0e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt @@ -630,6 +630,32 @@ fun getAllAIGeneratedValidators(): Map { Validator("config_parse_dhcp_or_ra_route_table", "NETWORK_CONFIG_SOURCE_DHCP6") to ConfigParseDhcpOrRaRouteTableOptionValue() as OptionValueInformation, Validator("config_parse_dhcp_or_ra_route_table", "NETWORK_CONFIG_SOURCE_NDISC") to ConfigParseDhcpOrRaRouteTableOptionValue() as OptionValueInformation, + // Batch 2026-10: qdisc + Validator("config_parse_fq_codel_usec", "QDISC_KIND_FQ_CODEL") to ConfigParseFqCodelUsecOptionValue() as OptionValueInformation, + Validator("config_parse_tbf_rate", "QDISC_KIND_TBF") to ConfigParseTbfRateOptionValue() as OptionValueInformation, + Validator("config_parse_netem_rate", "QDISC_KIND_NETEM") to ConfigParseNetemRateOptionValue() as OptionValueInformation, + Validator("config_parse_netem_delay", "QDISC_KIND_NETEM") to ConfigParseNetemDelayOptionValue() as OptionValueInformation, + Validator("config_parse_htb_class_rate", "TCLASS_KIND_HTB") to ConfigParseHtbClassRateOptionValue() as OptionValueInformation, + Validator("config_parse_gred_u32", "QDISC_KIND_GRED") to ConfigParseGredU32OptionValue() as OptionValueInformation, + Validator("config_parse_fq_codel_u32", "QDISC_KIND_FQ_CODEL") to ConfigParseFqCodelU32OptionValue() as OptionValueInformation, + Validator("config_parse_teql_id", "QDISC_KIND_TEQL") to ConfigParseTeqlIdOptionValue() as OptionValueInformation, + Validator("config_parse_qfq_max_packet", "TCLASS_KIND_QFQ") to ConfigParseQfqMaxPacketOptionValue() as OptionValueInformation, + Validator("config_parse_pie_packet_limit", "QDISC_KIND_PIE") to ConfigParsePiePacketLimitOptionValue() as OptionValueInformation, + Validator("config_parse_htb_u32", "QDISC_KIND_HTB") to ConfigParseHtbU32OptionValue() as OptionValueInformation, + Validator("config_parse_htb_default_class", "QDISC_KIND_HTB") to ConfigParseHtbDefaultClassOptionValue() as OptionValueInformation, + Validator("config_parse_htb_class_u32", "TCLASS_KIND_HTB") to ConfigParseHtbClassU32OptionValue() as OptionValueInformation, + Validator("config_parse_gred_bool", "QDISC_KIND_GRED") to ConfigParseGredBoolOptionValue() as OptionValueInformation, + Validator("config_parse_fq_usec", "QDISC_KIND_FQ") to ConfigParseFqUsecOptionValue() as OptionValueInformation, + Validator("config_parse_fq_max_rate", "QDISC_KIND_FQ") to ConfigParseFqMaxRateOptionValue() as OptionValueInformation, + Validator("config_parse_fq_codel_bool", "QDISC_KIND_FQ_CODEL") to ConfigParseFqCodelBoolOptionValue() as OptionValueInformation, + Validator("config_parse_ets_quanta", "QDISC_KIND_ETS") to ConfigParseEtsQuantaOptionValue() as OptionValueInformation, + Validator("config_parse_ets_prio", "QDISC_KIND_ETS") to ConfigParseEtsPrioOptionValue() as OptionValueInformation, + Validator("config_parse_drr_size", "TCLASS_KIND_DRR") to ConfigParseDrrSizeOptionValue() as OptionValueInformation, + Validator("config_parse_cake_flow_isolation_mode", "QDISC_KIND_CAKE") to ConfigParseCakeFlowIsolationModeOptionValue() as OptionValueInformation, + Validator("config_parse_cake_bandwidth", "QDISC_KIND_CAKE") to ConfigParseCakeBandwidthOptionValue() as OptionValueInformation, + Validator("config_parse_cake_ack_filter", "QDISC_KIND_CAKE") to ConfigParseCakeAckFilterOptionValue() as OptionValueInformation, + Validator("config_parse_bfifo_size", "QDISC_KIND_BFIFO") to ConfigParseBfifoSizeOptionValue() as OptionValueInformation, + ) return allValidators diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBfifoSizeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBfifoSizeOptionValue.kt new file mode 100644 index 00000000..b6c02e8e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseBfifoSizeOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [BFIFO] LimitBytes= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#LimitBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fifo.c#L103-L160 config_parse_bfifo_size (QDISC_KIND_BFIFO) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L42 LimitBytes=1000000 + */ + +/** + * Validator for [BFIFO] LimitBytes=. + * + * C function: config_parse_bfifo_size (QDISC_KIND_BFIFO). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A byte size as parse_size(rvalue, 1024) reads it (see [parseSize]) that must not exceed UINT32_MAX: + * above it the parser logs "Invalid 'LimitBytes=', ignoring assignment". So `4G` is rejected. + */ +class ConfigParseBfifoSizeOptionValue : SimpleGrammarOptionValues( + "config_parse_bfifo_size", + SequenceCombinator(parseSize(1024, maxInclusive = SIZE_UINT32_MAX), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeAckFilterOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeAckFilterOptionValue.kt new file mode 100644 index 00000000..67911f6d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeAckFilterOptionValue.kt @@ -0,0 +1,37 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [CAKE] AckFilter= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#AckFilter= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L680-L730 config_parse_cake_ack_filter (QDISC_KIND_CAKE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L672-L678 cake_ack_filter_table + DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING_WITH_BOOLEAN + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/string-table.c#L32-L43 string_table_lookup_from_string_with_boolean + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (case-insensitive) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L13-L37 TEST(parse_boolean) — "YES", "TRUE", "ON" accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-cake.network#L25 AckFilter=aggressive + */ + +/** + * Validator for [CAKE] AckFilter=. + * + * C function: config_parse_cake_ack_filter (QDISC_KIND_CAKE). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A boolean (parse_boolean() is tried first, case-insensitively: `yes` -> filter, `no` -> off) or the + * table word `aggressive`, which is matched exactly (case-sensitive streq). The table's `no`/`yes` + * entries are not listed as names: parse_boolean() already takes them, in any case. + */ +class ConfigParseCakeAckFilterOptionValue : SimpleGrammarOptionValues( + "config_parse_cake_ack_filter", + SequenceCombinator( + PreferValidAlternativeCombinator( + FlexibleLiteralChoiceTerminal("aggressive"), + BOOLEAN, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeBandwidthOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeBandwidthOptionValue.kt new file mode 100644 index 00000000..e6299ca0 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeBandwidthOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [CAKE] Bandwidth= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Bandwidth= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L141-L193 config_parse_cake_bandwidth (QDISC_KIND_CAKE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-cake.network#L12 Bandwidth=500M + */ + +/** + * Validator for [CAKE] Bandwidth=. + * + * C function: config_parse_cake_bandwidth (QDISC_KIND_CAKE). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A rate in bits per second as parse_size(rvalue, 1000) reads it (see [parseSize]); the result is + * divided by 8 and stored as a uint64 with no further bound. + */ +class ConfigParseCakeBandwidthOptionValue : SimpleGrammarOptionValues( + "config_parse_cake_bandwidth", + SequenceCombinator(parseSize(1000), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeFlowIsolationModeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeFlowIsolationModeOptionValue.kt new file mode 100644 index 00000000..ca8ecee8 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseCakeFlowIsolationModeOptionValue.kt @@ -0,0 +1,28 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [CAKE] FlowIsolationMode= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#FlowIsolationMode= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L442-L492 config_parse_cake_flow_isolation_mode (QDISC_KIND_CAKE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L429-L438 cake_flow_isolation_mode_table + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/cake.c#L440 DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING (exact, case-sensitive) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-cake.network#L18 FlowIsolationMode=dual-dst-host + */ + +/** + * Validator for [CAKE] FlowIsolationMode=. + * + * C function: config_parse_cake_flow_isolation_mode (QDISC_KIND_CAKE). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * One of `none`, `src-host`, `dst-host`, `hosts`, `flows`, `dual-src-host`, `dual-dst-host`, `triple`, + * matched exactly (string_table_lookup is case-sensitive, and this table has no boolean fallback). + */ +class ConfigParseCakeFlowIsolationModeOptionValue : SimpleGrammarOptionValues( + "config_parse_cake_flow_isolation_mode", + SequenceCombinator(FlexibleLiteralChoiceTerminal("none", "src-host", "dst-host", "hosts", "flows", "dual-src-host", "dual-dst-host", "triple"), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDrrSizeOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDrrSizeOptionValue.kt new file mode 100644 index 00000000..cd52278d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseDrrSizeOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [DeficitRoundRobinSchedulerClass] QuantumBytes= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#QuantumBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/drr.c#L45-L102 config_parse_drr_size (TCLASS_KIND_DRR) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-drr.network#L16 QuantumBytes=2000 + */ + +/** + * Validator for [DeficitRoundRobinSchedulerClass] QuantumBytes=. + * + * C function: config_parse_drr_size (TCLASS_KIND_DRR). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A byte size as parse_size(rvalue, 1024) reads it (see [parseSize]) that must not exceed UINT32_MAX: + * above it the parser logs "Invalid 'QuantumBytes=', ignoring assignment". So `4G` (4294967296) is + * rejected and `4095M` is accepted. + */ +class ConfigParseDrrSizeOptionValue : SimpleGrammarOptionValues( + "config_parse_drr_size", + SequenceCombinator(parseSize(1024, maxInclusive = SIZE_UINT32_MAX), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsPrioOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsPrioOptionValue.kt new file mode 100644 index 00000000..2e27d6cb --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsPrioOptionValue.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [EnhancedTransmissionSelection] PriorityMap= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PriorityMap= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/ets.c#L227-L302 config_parse_ets_prio (QDISC_KIND_ETS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L45-L47 safe_atou8 + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L583-L597 safe_atou8_full (rejects > UINT8_MAX) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/pkt_sched.h#L28 TC_PRIO_MAX = 15 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-ets.network#L18-L21 PriorityMap=8 7 6 5 / PriorityMap= / PriorityMap=3 4 5 + */ + +private val ETS_BAND = SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT8_MAX, + acceptsWhitespace = false, + acceptsMinus = true, +) + +/** + * Validator for [EnhancedTransmissionSelection] PriorityMap=. + * + * C function: config_parse_ets_prio (QDISC_KIND_ETS). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A whitespace-separated list of band numbers, each read with safe_atou8 (base 0, 0…255), at most + * TC_PRIO_MAX + 1 (16) entries per line. Each entry is a word from extract_first_word(), so the number + * cannot contain a blank: `0b+1` is 1, but `0b 1` is the words `0b` (invalid) and `1`. Out-of-range or unparsable entries are logged and skipped by + * the C, and so are flagged here; so is a 17th entry. + * + * Lenient: enhanced_transmission_selection_verify later rejects the whole section if any entry is not + * below the number of bands (at most 16), so values 16…255 can never actually work; they are accepted + * here because the man page documents 0…255 and that is what the parser itself checks. The 16-entry + * cap is cumulative across lines; only the per-line count is checked. + */ +class ConfigParseEtsPrioOptionValue : SimpleGrammarOptionValues( + "config_parse_ets_prio", + SequenceCombinator(ETS_BAND, Repeat(SequenceCombinator(WhitespaceTerminal(), ETS_BAND), 0, 15), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsQuantaOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsQuantaOptionValue.kt new file mode 100644 index 00000000..c6d3a587 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseEtsQuantaOptionValue.kt @@ -0,0 +1,41 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import java.math.BigInteger +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [EnhancedTransmissionSelection] QuantumBytes= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#QuantumBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/ets.c#L144-L225 config_parse_ets_quanta (QDISC_KIND_ETS) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/pkt_sched.h#L1210 TCQ_ETS_MAX_BANDS = 16 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-ets.network#L14-L17 QuantumBytes=2 4 6 / QuantumBytes= / QuantumBytes=1001 1002 1003 + */ + +private val ETS_QUANTUM = parseSize(1024, minInclusive = BigInteger.ONE, maxInclusive = SIZE_UINT32_MAX, allowInnerWhitespace = false) + +/** + * Validator for [EnhancedTransmissionSelection] QuantumBytes=. + * + * C function: config_parse_ets_quanta (QDISC_KIND_ETS). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A whitespace-separated list (extract_first_word, no flags) of byte sizes, each read with + * parse_size(word, 1024) and required to be 1…4294967295; at most TCQ_ETS_MAX_BANDS (16) entries. A + * zero, oversized or unparsable entry is logged and skipped by the C ("ignoring assignment"), and so is + * flagged here; so is a 17th entry on one line. + * + * Because the list is split on whitespace first, each entry is a single word: `1K` or `1K512B` is one + * entry, but `1K 512B` is two. + * + * Lenient: the 16-entry cap is cumulative across repeated QuantumBytes= lines (until an empty + * assignment resets the list); only the per-line count is checked. + */ +class ConfigParseEtsQuantaOptionValue : SimpleGrammarOptionValues( + "config_parse_ets_quanta", + SequenceCombinator(ETS_QUANTUM, Repeat(SequenceCombinator(WhitespaceTerminal(), ETS_QUANTUM), 0, 15), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelBoolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelBoolOptionValue.kt new file mode 100644 index 00000000..b04ad462 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelBoolOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [FairQueueingControlledDelay] ECN= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#ECN= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq-codel.c#L222-L265 config_parse_fq_codel_bool (QDISC_KIND_FQ_CODEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L48-L64 parse_tristate_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (STRCASE_IN_SET: case-insensitive) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L13-L37 TEST(parse_boolean) — "YES", "TRUE", "ON" accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq_codel.network#L18 ECN=yes + */ + +/** + * Validator for [FairQueueingControlledDelay] ECN=. + * + * C function: config_parse_fq_codel_bool (QDISC_KIND_FQ_CODEL). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A boolean via parse_tristate(): any parse_boolean() spelling, case-insensitively. Empty resets to + * "unset" (handled before the grammar). + */ +class ConfigParseFqCodelBoolOptionValue : SimpleGrammarOptionValues( + "config_parse_fq_codel_bool", + SequenceCombinator(BOOLEAN, EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelU32OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelU32OptionValue.kt new file mode 100644 index 00000000..6357d525 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelU32OptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [FairQueueingControlledDelay] PacketLimit=, Flows= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PacketLimit= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Flows= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq-codel.c#L97-L155 config_parse_fq_codel_u32 (QDISC_KIND_FQ_CODEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq_codel.network#L12-L14 PacketLimit=20480, Flows=2048 + */ + +/** + * Validator for [FairQueueingControlledDelay] PacketLimit=, Flows=. + * + * C function: config_parse_fq_codel_u32 (QDISC_KIND_FQ_CODEL). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned 32-bit integer read with safe_atou32 (base 0: decimal, `0x` hex, `0o`/leading-zero + * octal, `0b` binary, optional `+`), 0…4294967295. No further bound in the parser. strtoul starts + * afresh after a `0b`/`0o` prefix, so `0b+1` and `0b 1` are 1 (see [SafeAtoTerminal]). + */ +class ConfigParseFqCodelU32OptionValue : SimpleGrammarOptionValues( + "config_parse_fq_codel_u32", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelUsecOptionValue.kt new file mode 100644 index 00000000..c6ba5d11 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqCodelUsecOptionValue.kt @@ -0,0 +1,36 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [FairQueueingControlledDelay] TargetSec=, IntervalSec=, CEThresholdSec= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#TargetSec= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#IntervalSec= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#CEThresholdSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq-codel.c#L157-L220 config_parse_fq_codel_usec (QDISC_KIND_FQ_CODEL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L83 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq_codel.network#L15-L19 TargetSec=10ms, IntervalSec=200ms, CEThresholdSec=100ms + */ + +/** + * Validator for [FairQueueingControlledDelay] TargetSec=, IntervalSec=, CEThresholdSec=. + * + * C function: config_parse_fq_codel_usec (QDISC_KIND_FQ_CODEL). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A time span as parse_sec() reads it: `infinity`, or one or more `number[unit]` elements such as + * `10ms`, `1min30s`, `5s 500ms`, `.5s`, `+3.1s` (see [parseSec]). There is no extra bound: the parsed + * usec_t is stored as is (CEThresholdSec= additionally treats an empty value as USEC_INFINITY). + * + * Overflow is rejected exactly as parse_time() does (ERANGE): an integer part above LLONG_MAX, or a finite + * total reaching USEC_INFINITY (e.g. `18446744073709551615us`). + */ +class ConfigParseFqCodelUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_fq_codel_usec", + SequenceCombinator(parseSec(), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqMaxRateOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqMaxRateOptionValue.kt new file mode 100644 index 00000000..381bbb81 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqMaxRateOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import java.math.BigInteger +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [FairQueueing] MaximumRate= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MaximumRate= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq.c#L344-L402 config_parse_fq_max_rate (QDISC_KIND_FQ) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq.network#L16 MaximumRate=1M + */ + +/** + * Validator for [FairQueueing] MaximumRate=. + * + * C function: config_parse_fq_max_rate (QDISC_KIND_FQ). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A rate in bits per second as parse_size(rvalue, 1000) reads it (see [parseSize]) whose byte rate + * (value / 8) must fit a uint32: above it the parser logs "Specified 'MaximumRate=' is too large, + * ignoring assignment". So the largest accepted value is 34359738367 bits/s (≈ 34.35G); `34G` is + * legal and `35G` is not. + */ +class ConfigParseFqMaxRateOptionValue : SimpleGrammarOptionValues( + "config_parse_fq_max_rate", + SequenceCombinator(parseSize(1000, maxInclusive = SIZE_UINT32_MAX * BigInteger.valueOf(8) + BigInteger.valueOf(7)), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqUsecOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqUsecOptionValue.kt new file mode 100644 index 00000000..d48539d4 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseFqUsecOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [FairQueueing] CEThresholdSec= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#CEThresholdSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/fq.c#L284-L342 config_parse_fq_usec (QDISC_KIND_FQ) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L83 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-fq.network#L20 CEThresholdSec=100ms + */ + +/** + * Validator for [FairQueueing] CEThresholdSec=. + * + * C function: config_parse_fq_usec (QDISC_KIND_FQ). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A time span as parse_sec() reads it (see [parseSec]) that must not exceed UINT32_MAX microseconds + * (about 71min 34.967s): above it the parser logs "Specified 'CEThresholdSec=' is too large, ignoring + * assignment". In particular `infinity` (USEC_INFINITY) is rejected — leave the value empty to get the + * "unset/infinity" default. + */ +class ConfigParseFqUsecOptionValue : SimpleGrammarOptionValues( + "config_parse_fq_usec", + SequenceCombinator(parseSec(UINT32_MAX_USEC), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredBoolOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredBoolOptionValue.kt new file mode 100644 index 00000000..7fa893b7 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredBoolOptionValue.kt @@ -0,0 +1,29 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [GenericRandomEarlyDetection] GenericRIO= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#GenericRIO= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/gred.c#L134-L177 config_parse_gred_bool (QDISC_KIND_GRED) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L48-L64 parse_tristate_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L23-L46 parse_boolean (STRCASE_IN_SET: case-insensitive) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L13-L37 TEST(parse_boolean) — "YES", "TRUE", "ON" accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-gred.network#L14 GenericRIO=yes + */ + +/** + * Validator for [GenericRandomEarlyDetection] GenericRIO=. + * + * C function: config_parse_gred_bool (QDISC_KIND_GRED). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A boolean via parse_tristate(): any parse_boolean() spelling (`yes`/`no`/`1`/`0`/`true`/`false`/ + * `on`/`off`/`y`/`n`/`t`/`f`), case-insensitively. Empty resets to "unset" (handled before the grammar). + */ +class ConfigParseGredBoolOptionValue : SimpleGrammarOptionValues( + "config_parse_gred_bool", + SequenceCombinator(BOOLEAN, EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredU32OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredU32OptionValue.kt new file mode 100644 index 00000000..fbf483a5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseGredU32OptionValue.kt @@ -0,0 +1,49 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [GenericRandomEarlyDetection] VirtualQueues=, DefaultVirtualQueue= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#VirtualQueues= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DefaultVirtualQueue= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/gred.c#L68-L133 config_parse_gred_u32 (QDISC_KIND_GRED) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/include/uapi/linux/pkt_sched.h#L305 MAX_DPs = 16 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-gred.network#L12-L13 VirtualQueues=12, DefaultVirtualQueue=10 + */ + +/** + * Validator for [GenericRandomEarlyDetection] VirtualQueues=, DefaultVirtualQueue=. + * + * C function: config_parse_gred_u32 (QDISC_KIND_GRED). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned integer read with safe_atou32 (base 0: decimal, `0x` hex, `0o`/leading-zero octal, `0b` + * binary, optional `+`; `0b+1` and `0b 1` are 1, see [SafeAtoTerminal]) that must not exceed MAX_DPs (16). Above 16 the parser logs "Invalid ..., ignoring + * assignment" (although, at 0f0682b773, it still stores the value — a C bug; the kernel refuses more than + * 16 DPs anyway), so it is flagged here. + * + * Lenient: both keys share the parser, so 0..16 is accepted for both, although VirtualQueues=0 and + * DefaultVirtualQueue=16 always fail generic_random_early_detection_verify (DefaultVirtualQueue= must + * be less than VirtualQueues=). The man page documents VirtualQueues= as 1…16. + */ +class ConfigParseGredU32OptionValue : SimpleGrammarOptionValues( + "config_parse_gred_u32", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = 16L, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassRateOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassRateOptionValue.kt new file mode 100644 index 00000000..4f027c87 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassRateOptionValue.kt @@ -0,0 +1,30 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [HierarchyTokenBucketClass] Rate=, CeilRate= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Rate= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#CeilRate= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/htb.c#L378-L436 config_parse_htb_class_rate (TCLASS_KIND_HTB) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L19-L20 Rate=1M, CeilRate=0.5M + */ + +/** + * Validator for [HierarchyTokenBucketClass] Rate=, CeilRate=. + * + * C function: config_parse_htb_class_rate (TCLASS_KIND_HTB). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A rate in bits per second as parse_size(rvalue, 1000) reads it (see [parseSize]): `1M`, `0.5M`, + * `1G 500M`, `125000`. The result is divided by 8 and stored as a uint64; no further bound. + */ +class ConfigParseHtbClassRateOptionValue : SimpleGrammarOptionValues( + "config_parse_htb_class_rate", + SequenceCombinator(parseSize(1000), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassU32OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassU32OptionValue.kt new file mode 100644 index 00000000..da722eda --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbClassU32OptionValue.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [HierarchyTokenBucketClass] Priority= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Priority= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/htb.c#L242-L293 config_parse_htb_class_u32 (TCLASS_KIND_HTB) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L18 Priority=1 + */ + +/** + * Validator for [HierarchyTokenBucketClass] Priority=. + * + * C function: config_parse_htb_class_u32 (TCLASS_KIND_HTB). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned 32-bit integer read with safe_atou32 (base 0, so `0b+1` and `0b 1` are 1; see + * [SafeAtoTerminal]), 0…4294967295. No further bound. + */ +class ConfigParseHtbClassU32OptionValue : SimpleGrammarOptionValues( + "config_parse_htb_class_u32", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbDefaultClassOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbDefaultClassOptionValue.kt new file mode 100644 index 00000000..9ab738f3 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbDefaultClassOptionValue.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [HierarchyTokenBucket] DefaultClass= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DefaultClass= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/htb.c#L48-L98 config_parse_htb_default_class (QDISC_KIND_HTB) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L61-L64 safe_atou32_full(rvalue, 16, ..) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full (explicit base: mangle_base is skipped) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L12 DefaultClass=30 (i.e. 0x30) + */ + +/** + * Validator for [HierarchyTokenBucket] DefaultClass=. + * + * C function: config_parse_htb_default_class (QDISC_KIND_HTB). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * The default class's minor id, always read as HEXADECIMAL (safe_atou32_full(.., 16)): `30` is 0x30, `0x30` + * is the same, `ff` is legal, `0b1` is read as hex 0xb1, and `0o7` is rejected (with an explicit base, + * mangle_base does not run). strtoul skips a leading vertical tab, so `\v5` is 5 (see [SafeAtoTerminal]). + * Range 0…0xffffffff. + */ +class ConfigParseHtbDefaultClassOptionValue : SimpleGrammarOptionValues( + "config_parse_htb_default_class", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 16, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbU32OptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbU32OptionValue.kt new file mode 100644 index 00000000..5470f63a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseHtbU32OptionValue.kt @@ -0,0 +1,42 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [HierarchyTokenBucket] RateToQuantum= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#RateToQuantum= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/htb.c#L100-L150 config_parse_htb_u32 (QDISC_KIND_HTB) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-htb-fifo.network#L13 RateToQuantum=20 + */ + +/** + * Validator for [HierarchyTokenBucket] RateToQuantum=. + * + * C function: config_parse_htb_u32 (QDISC_KIND_HTB). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned 32-bit integer read with safe_atou32 (base 0, so `0b+1` and `0b 1` are 1; see + * [SafeAtoTerminal]), 0…4294967295. No further bound. + */ +class ConfigParseHtbU32OptionValue : SimpleGrammarOptionValues( + "config_parse_htb_u32", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemDelayOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemDelayOptionValue.kt new file mode 100644 index 00000000..1989c2a5 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemDelayOptionValue.kt @@ -0,0 +1,33 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [NetworkEmulator] DelaySec=, DelayJitterSec= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DelaySec= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DelayJitterSec= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/netem.c#L51-L110 config_parse_netem_delay (QDISC_KIND_NETEM) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1289 parse_sec + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147-L1196 extract_multiplier + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L21-L83 TEST(parse_sec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-netem.network#L12-L13 DelaySec=50ms, DelayJitterSec=10ms + */ + +/** + * Validator for [NetworkEmulator] DelaySec=, DelayJitterSec=. + * + * C function: config_parse_netem_delay (QDISC_KIND_NETEM). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A time span as parse_sec() reads it (see [parseSec]). No further bound in the parser. + * + * Lenient: a delay too large for the kernel's tick conversion only fails later, in + * network_emulator_fill_message, and is not checked here. + */ +class ConfigParseNetemDelayOptionValue : SimpleGrammarOptionValues( + "config_parse_netem_delay", + SequenceCombinator(parseSec(), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemRateOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemRateOptionValue.kt new file mode 100644 index 00000000..64e87776 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseNetemRateOptionValue.kt @@ -0,0 +1,34 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [NetworkEmulator] LossRate=, DuplicateRate= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#LossRate= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DuplicateRate= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/netem.c#L112-L170 config_parse_netem_rate (QDISC_KIND_NETEM) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/tc-util.c#L63-L75 parse_tc_percent + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L135-L157 parse_permyriad / parse_permyriad_unbounded + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L57-L99 parse_parts_value_with_hundredths_place + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L497-L519 safe_atoi (base 0) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-percent-util.c#L87-L130 TEST(parse_permyriad) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-netem.network#L14 LossRate=20% + */ + +/** + * Validator for [NetworkEmulator] LossRate=, DuplicateRate=. + * + * C function: config_parse_netem_rate (QDISC_KIND_NETEM). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A percentage as parse_tc_percent() -> parse_permyriad() reads it: an integer followed by `%` (up to + * two decimals), `‰` (up to one decimal) or `‱` (no decimals), at most 100% (see [TC_PERCENT]). A + * bare number without a symbol is rejected. The integer part goes through safe_atoi (base 0), so + * `0x10%` is legal; that corner is accepted. + */ +class ConfigParseNetemRateOptionValue : SimpleGrammarOptionValues( + "config_parse_netem_rate", + SequenceCombinator(TC_PERCENT, EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePiePacketLimitOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePiePacketLimitOptionValue.kt new file mode 100644 index 00000000..1e4354ac --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParsePiePacketLimitOptionValue.kt @@ -0,0 +1,43 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [PIE] PacketLimit= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PacketLimit= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/pie.c#L40-L90 config_parse_pie_packet_limit (QDISC_KIND_PIE) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-pie.network#L12 PacketLimit=200000 + */ + +/** + * Validator for [PIE] PacketLimit=. + * + * C function: config_parse_pie_packet_limit (QDISC_KIND_PIE). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned 32-bit integer read with safe_atou32 (base 0, so `0b+1` and `0b 1` are 1; see + * [SafeAtoTerminal]), 0…4294967295. No further bound in the + * parser (the man page's "1…4294967294" is not enforced by it). + */ +class ConfigParsePiePacketLimitOptionValue : SimpleGrammarOptionValues( + "config_parse_pie_packet_limit", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.UINT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseQfqMaxPacketOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseQfqMaxPacketOptionValue.kt new file mode 100644 index 00000000..5b8f40de --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseQfqMaxPacketOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import java.math.BigInteger +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [QuickFairQueueingClass] MaxPacketBytes= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#MaxPacketBytes= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/qfq.c#L115-L173 config_parse_qfq_max_packet (TCLASS_KIND_QFQ) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/qfq.c#L14-L15 QFQ_MIN_MAX_PACKET 512, QFQ_MAX_MAX_PACKET 1<<16 + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-qfq.network#L17 MaxPacketBytes=16000 + */ + +/** + * Validator for [QuickFairQueueingClass] MaxPacketBytes=. + * + * C function: config_parse_qfq_max_packet (TCLASS_KIND_QFQ). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A byte size as parse_size(rvalue, 1024) reads it (IEC suffixes `K`/`M`/…, see [parseSize]) that + * must lie in 512…65536 bytes (QFQ_MIN_MAX_PACKET…QFQ_MAX_MAX_PACKET); outside it the parser logs + * "Invalid ..., ignoring assignment". So `64K` is accepted and `65K` or `511` are not. + */ +class ConfigParseQfqMaxPacketOptionValue : SimpleGrammarOptionValues( + "config_parse_qfq_max_packet", + SequenceCombinator(parseSize(1024, BigInteger.valueOf(512), BigInteger.valueOf(65_536)), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTbfRateOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTbfRateOptionValue.kt new file mode 100644 index 00000000..4a8bd014 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTbfRateOptionValue.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +/* + * [TokenBucketFilter] Rate=, PeakRate= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Rate= + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#PeakRate= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/tbf.c#L184-L243 config_parse_tbf_rate (QDISC_KIND_TBF) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L108-L176 TEST(parse_size_iec) + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L178-L246 TEST(parse_size_si) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-tbf.network#L12-L16 Rate=1G, PeakRate=100G + */ + +/** + * Validator for [TokenBucketFilter] Rate=, PeakRate=. + * + * C function: config_parse_tbf_rate (QDISC_KIND_TBF). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * A rate in bits per second as parse_size(rvalue, 1000) reads it: a number with an optional SI suffix + * `K`/`M`/`G`/`T`/`P`/`E` (upper case only, powers of 1000) or `B`, optionally several descending + * elements (`1G 500M`). The result is divided by 8 and stored as a uint64; there is no further bound. + * Note `10Mbit` / `10mbit` (tc(8) spellings) are NOT accepted by systemd. + */ +class ConfigParseTbfRateOptionValue : SimpleGrammarOptionValues( + "config_parse_tbf_rate", + SequenceCombinator(parseSize(1000), EOF()), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTeqlIdOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTeqlIdOptionValue.kt new file mode 100644 index 00000000..47b289ab --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/ai/ConfigParseTeqlIdOptionValue.kt @@ -0,0 +1,44 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* +import java.math.BigInteger + +/* + * [TrivialLinkEqualizer] Id= (.network) + * + * man https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#Id= + * parser https://github.com/systemd/systemd/blob/0f0682b773/src/network/tc/teql.c#L44-L100 config_parse_teql_id (QDISC_KIND_TEQL) + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429-L478 safe_atou_full + * leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403-L427 mangle_base + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L489 safe_atou16("+12345") accepted + * tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L466-L470 0o6 / 0B101 prefixes (safe_atolli, same mangle_base) + * tests https://github.com/systemd/systemd/blob/0f0682b773/test/test-network/conf/25-qdisc-teql.network#L12 Id=1 + */ + +/** + * Validator for [TrivialLinkEqualizer] Id=. + * + * C function: config_parse_teql_id (QDISC_KIND_TEQL). An empty assignment resets the setting and is + * handled before this grammar runs. + * + * An unsigned integer read with safe_atou (base 0, so `0b+1` and `0b 1` are 1; see [SafeAtoTerminal]) + * that must not exceed INT_MAX: above it the parser + * logs "'Id=' is too large, ignoring assignment" (and, at 0f0682b773, still stores it — the qdisc kind + * then becomes e.g. "teql4294967295"), so 0…2147483647 is accepted. + */ +class ConfigParseTeqlIdOptionValue : SimpleGrammarOptionValues( + "config_parse_teql_id", + SequenceCombinator( + SafeAtoTerminal( + signed = false, + base = 0, + flags = emptySet(), + minInclusive = BigInteger.ZERO, + maxInclusive = SafeAtoTerminal.INT32_MAX, + acceptsWhitespace = true, + acceptsMinus = true, + ), + EOF(), + ), +) diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchQdiscCombinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchQdiscCombinators.kt new file mode 100644 index 00000000..e1797519 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/BatchQdiscCombinators.kt @@ -0,0 +1,324 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar + +import java.math.BigDecimal +import java.math.BigInteger + +/* + * Shared grammars for the systemd-networkd traffic-control validators (.network [QDisc]-family and + * [...Class] sections, systemd src/network/tc/), written for the 2026-10 qdisc batch. + * + * Everything here is pinned to systemd 0f0682b773 (systemd-build/build/last_commit_hash): + * https://github.com/systemd/systemd/blob/0f0682b773 + * + * The tc parsers lean on four leaf helpers. safe_atou / safe_atou32 / safe_atou8 / + * safe_atou32_full(.., 16) (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L429) + * are the shared [SafeAtoTerminal], written out at each call site. The other three are modelled here as + * closely to the C as the combinator framework allows: + * + * - parse_sec / parse_time -> [parseSec] + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198 + * - parse_size(.., 1000 | 1024) -> [parseSize] + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157 + * - parse_permyriad (via parse_tc_percent) -> [TC_PERCENT] + * https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L149 + * + * These are more faithful than the older shared terminals in Combinators.kt (TIME_VALUE rejects + * "1min30s"; ByteSizeTerminal rejects "1G 512M"). They live in this file only so the batch does not + * collide with parallel edits to Combinators.kt; see RECOMMENDATIONS.md for promoting + * them. + */ + +// --------------------------------------------------------------------------------------------------- +// Generic helpers + +private val OPT_WS = ZeroOrOne(WhitespaceTerminal()) + +/** + * Wraps [inner] and additionally requires the text it matched to satisfy [accept]. + * + * Used for value-level range checks that no single terminal can see — e.g. a parse_size() value + * written as several elements (`1G 512M`), where the bound applies to the sum. Matching (and so + * syntax highlighting and error localisation) is delegated to [inner] unchanged; only validity is + * narrowed. When [accept] returns null ("could not evaluate") the value is accepted: the grammar has + * already decided it is well-formed, and an evaluator gap must never flag a valid value. + * + * Empty matches are never accepted, so a grammar built only of optional parts (like [parseSize]'s + * chain of optional elements) cannot match the empty string. + */ +class ValueChecked(private val inner: Combinator, private val accept: (String) -> Boolean?) : Combinator { + + private fun ok(text: String): Boolean = accept(text) ?: true + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + val m = inner.SyntacticMatch(value, offset) + return if (m.matchResult == offset) NoMatch.copy(longestMatch = offset) else m + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + val m = inner.SemanticMatch(value, offset) + if (m.matchResult == -1) return m + if (m.matchResult == offset) return NoMatch.copy(longestMatch = offset) + return if (ok(value.substring(offset, m.matchResult))) m else NoMatch.copy(longestMatch = offset) + } + + override fun parse(value: String, offset: Int): Sequence = + inner.parse(value, offset).map { step -> + when (step) { + is Stuck -> step + is Parse -> when { + step.end == offset -> Stuck(offset, setOf(this)) + ok(value.substring(offset, step.end)) -> step + // Out of range: keep the tokens (so the span still highlights) but mark them invalid. + else -> Parse(step.end, step.tokens.map { it.copy(valid = false) }, step.regions) + } + } + } + + override fun toStringIndented(indent: Int): String = " ".repeat(indent) + "Checked(\n" + inner.toStringIndented(indent + 1) + "\n" + " ".repeat(indent) + ")" + + override fun toString(): String = toStringIndented(0) +} + +// --------------------------------------------------------------------------------------------------- +// Bounds + +/** UINT32_MAX, for the size/time range checks. */ +private val UINT32_MAX_BIG: BigInteger = SafeAtoTerminal.UINT32_MAX + +/** ULLONG_MAX: parse_size() and parse_time() return ERANGE above it. */ +private val UINT64_MAX_BIG: BigInteger = SafeAtoTerminal.UINT64_MAX + +// --------------------------------------------------------------------------------------------------- +// parse_sec / parse_time + +// extract_multiplier()'s table, in table order: the C takes the first entry the text starts with, +// and a regex alternation does the same (time-util.c#L1147-L1196). +private const val SEC_SUFFIX_ALTERNATION = + "seconds|second|sec|s|minutes|minute|min|months|month|M|msec|ms|m|hours|hour|hr|h|days|day|d|" + + "weeks|week|w|years|year|y|usec|us|μs|µs" + +private val SEC_SUFFIX = Labeled(Role.ENUM, RegexTerminal(SEC_SUFFIX_ALTERNATION, SEC_SUFFIX_ALTERNATION)) + +// strtoll() digits with an optional '+', then an optional '.digits'; or a bare '.digits' (".5s" is +// legal: strtoll consumes nothing, and the '.' branch takes over). "5." and "+.5" are not. +private const val SEC_NUMBER_RE = """\+?[0-9]+(?:\.[0-9]+)?|\.[0-9]+""" +private val SEC_NUMBER = Labeled(Role.LITERAL, RegexTerminal(SEC_NUMBER_RE, SEC_NUMBER_RE)) + +// After a number comes either a unit (optionally surrounded by whitespace), or whitespace, or the end. +// A unit may be followed directly by the next number ("5s500ms", "3.1s.2"); a bare number may not +// ("12.34.56" is EINVAL). +private val SEC_ELEMENT = SequenceCombinator( + SEC_NUMBER, + AlternativeCombinator( + SequenceCombinator(OPT_WS, SEC_SUFFIX, OPT_WS), + WhitespaceTerminal(), + EOF(), + ), +) + +private val PARSE_SEC = AlternativeCombinator( + LiteralChoiceTerminal("infinity"), + OneOrMore(SEC_ELEMENT), +) + +private val USEC_INFINITY: BigInteger = UINT64_MAX_BIG + +private val LLONG_MAX_BIG: BigInteger = BigInteger.valueOf(Long.MAX_VALUE) + +/** Returned by [evaluateParseSec] when parse_time() would fail with ERANGE; above every accepted bound. */ +private val PARSE_SEC_ERANGE: BigInteger = USEC_INFINITY + BigInteger.ONE + +private val SEC_MULTIPLIERS: Map = run { + val sec = 1_000_000L + val minute = 60 * sec + val hour = 60 * minute + val day = 24 * hour + mapOf( + "seconds" to sec, "second" to sec, "sec" to sec, "s" to sec, + "minutes" to minute, "minute" to minute, "min" to minute, "m" to minute, + "months" to 2_629_800L * sec, "month" to 2_629_800L * sec, "M" to 2_629_800L * sec, + "msec" to 1000L, "ms" to 1000L, + "hours" to hour, "hour" to hour, "hr" to hour, "h" to hour, + "days" to day, "day" to day, "d" to day, + "weeks" to 7 * day, "week" to 7 * day, "w" to 7 * day, + "years" to 31_557_600L * sec, "year" to 31_557_600L * sec, "y" to 31_557_600L * sec, + "usec" to 1L, "us" to 1L, "μs" to 1L, "µs" to 1L, + ).mapValues { BigInteger.valueOf(it.value) } +} + +private val SEC_EVAL_ELEMENT = Regex("""\s*\+?([0-9]*)(?:\.([0-9]+))?\s*($SEC_SUFFIX_ALTERNATION)?""") + +/** + * parse_time(t, USEC_PER_SEC) as a value in microseconds, or null if [text] is not evaluable. Overflow + * (C's ERANGE: an integer part above LLONG_MAX, or a finite total reaching USEC_INFINITY) yields a value + * greater than USEC_INFINITY so that every bound check rejects it. + */ +fun evaluateParseSec(text: String): BigInteger? { + val t = text.trim() + if (t == "infinity") return USEC_INFINITY + var pos = 0 + var total = BigInteger.ZERO + while (pos < t.length) { + while (pos < t.length && t[pos].isWhitespace()) pos++ + if (pos >= t.length) break + val m = SEC_EVAL_ELEMENT.matchAt(t, pos) ?: return null + if (m.value.isEmpty()) return null + pos += m.value.length + val multiplier = m.groupValues[3].takeIf { it.isNotEmpty() }?.let { SEC_MULTIPLIERS[it] } ?: SEC_MULTIPLIERS.getValue("s") + val whole = BigInteger(m.groupValues[1].ifEmpty { "0" }) + // strtoll() sets ERANGE above LLONG_MAX; then `(usec_t) l >= USEC_INFINITY / multiplier` and + // `k >= USEC_INFINITY - usec` are ERANGE too, so a finite total can never reach USEC_INFINITY. + if (whole > LLONG_MAX_BIG || whole >= USEC_INFINITY / multiplier) return PARSE_SEC_ERANGE + val k = whole * multiplier + if (k >= USEC_INFINITY - total) return PARSE_SEC_ERANGE + total += k + // The C adds the fractional digits one by one, each times a truncated multiplier/10^k. + var step = multiplier / BigInteger.TEN + for (digit in m.groupValues[2]) { + val kf = BigInteger.valueOf((digit - '0').toLong()) * step + if (kf >= USEC_INFINITY - total) return PARSE_SEC_ERANGE + total += kf + step /= BigInteger.TEN + } + } + return total +} + +/** + * A whole parse_sec() value: `infinity`, or one or more `number[unit]` elements. + * + * Mirrors parse_time() (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287) + * including the forms its unit test asserts are legal (test-time-util.c#L21): `5s500ms`, `.22s`, + * `+3.1s`, `3.1s.2`, `3.1 .2`, `23µs`; and rejects what it asserts are not: `5.`, `.s`, `-5s`, + * `3.s`, `3. 1s`, `12.34.56`, `infinity .7`. + * + * @param maxUsec when set, the value (in microseconds, `infinity` counting as USEC_INFINITY) must not + * exceed it. + */ +fun parseSec(maxUsec: BigInteger? = null): Combinator = + ValueChecked(PARSE_SEC) { text -> + evaluateParseSec(text)?.let { it <= (maxUsec ?: USEC_INFINITY) } + } + +/** [maxUsec] for parsers that refuse anything above UINT32_MAX microseconds (and so `infinity`). */ +val UINT32_MAX_USEC: BigInteger = UINT32_MAX_BIG + +// --------------------------------------------------------------------------------------------------- +// parse_size + +// strtoull(base 10) digits with an optional '+', then an optional '.' with optional digits: "3." and +// "10.M" are legal (parse-util.c "Ignore failure. E.g. 10.M is valid"); ".5" is not. +private const val SIZE_NUMBER_RE = """\+?[0-9]+(?:\.[0-9]*)?""" +private val SIZE_NUMBER = Labeled(Role.LITERAL, RegexTerminal(SIZE_NUMBER_RE, SIZE_NUMBER_RE)) + +private val SIZE_SUFFIXES = listOf("E", "P", "T", "G", "M", "K", "B") + +private fun sizeChain(allowInnerWhitespace: Boolean): Combinator { + val parts = mutableListOf() + for (suffix in SIZE_SUFFIXES) { + val element = if (allowInnerWhitespace) { + SequenceCombinator(SIZE_NUMBER, OPT_WS, LiteralChoiceTerminal(suffix), OPT_WS) + } else { + SequenceCombinator(SIZE_NUMBER, LiteralChoiceTerminal(suffix)) + } + parts.add(ZeroOrOne(element)) + } + // The "" entry of the table: a bare number, which can only be the last element. + parts.add(ZeroOrOne(SIZE_NUMBER)) + return SequenceCombinator(*parts.toTypedArray()) +} + +private val SIZE_EVAL_ELEMENT = Regex("""\s*\+?([0-9]+)(?:\.([0-9]*))?\s*([EPTGMKB]?)\s*""") + +/** parse_size(text, base) as a byte count, or null if [text] is not evaluable. */ +fun evaluateParseSize(text: String, base: Int): BigInteger? { + val factorBase = BigInteger.valueOf(base.toLong()) + var pos = 0 + var total = BigInteger.ZERO + while (pos < text.length) { + val m = SIZE_EVAL_ELEMENT.matchAt(text, pos) ?: return null + if (m.value.isEmpty()) return null + pos += m.value.length + val power = when (m.groupValues[3]) { + "E" -> 6; "P" -> 5; "T" -> 4; "G" -> 3; "M" -> 2; "K" -> 1; else -> 0 + } + val factor = factorBase.pow(power) + total += BigInteger(m.groupValues[1]) * factor + val frac = m.groupValues[2] + if (frac.isNotEmpty()) total += BigDecimal("0.$frac").multiply(BigDecimal(factor)).toBigInteger() + } + return total +} + +/** + * A parse_size() value (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283): + * one or more `number[.fraction][suffix]` elements whose suffixes strictly descend through + * `E P T G M K B ""` — `4M 11.5K`, `3.5G3B` and `4T3G3B` are legal; `3B3.5G`, `12P12P`, `12X`, `5k` + * (suffixes are upper-case only) and `-1` are not (test-parse-util.c#L108, #L178). A bare number (the + * `""` entry) must be the last element. + * + * @param base 1000 (SI, for rates) or 1024 (IEC, for sizes); only affects the range check. + * @param minInclusive / maxInclusive bounds on the resulting byte (or bit) count. The upper bound + * defaults to ULLONG_MAX, above which parse_size() itself returns ERANGE. + * @param allowInnerWhitespace false for an element of a whitespace-separated list (each word is + * parsed on its own, so a space would split the size into two words). + */ +fun parseSize( + base: Int, + minInclusive: BigInteger = BigInteger.ZERO, + maxInclusive: BigInteger = UINT64_MAX_BIG, + allowInnerWhitespace: Boolean = true, +): Combinator = + ValueChecked(sizeChain(allowInnerWhitespace)) { text -> + evaluateParseSize(text, base)?.let { it >= minInclusive && it <= maxInclusive } + } + +/** UINT32_MAX, the cap several tc size parsers apply after parse_size(). */ +val SIZE_UINT32_MAX: BigInteger = UINT32_MAX_BIG + +// --------------------------------------------------------------------------------------------------- +// parse_permyriad / parse_tc_percent + +// The integer part goes through safe_atoi() (base 0, mangle_base), so `0x10%` is 16% and `0b 1%` is 1%, +// and parse_parts_value_*() return ERANGE when it is negative (`-0` is fine, and so is `0b-0`). Its +// magnitude is capped by the 100% check below, so int's range on the terminal itself is enough. The +// whole value reaches safe_atoi() up to the '.' or the symbol, so whitespace can reach the parser. +// https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L7-L23 +private val PCT_INT = SafeAtoTerminal( + signed = true, + base = 0, + flags = emptySet(), + minInclusive = 0L, + maxInclusive = Int.MAX_VALUE.toLong(), + acceptsWhitespace = true, + acceptsMinus = true, +) + +/** + * A percentage as parse_permyriad() reads it + * (https://github.com/systemd/systemd/blob/0f0682b773/src/basic/percent-util.c#L135-L157): an integer + * followed by `%` (up to two decimals), `‰` (up to one decimal) or `‱` (no decimals), at most 100%. + * test-percent-util.c#L87 asserts `55.53%`, `555.5‰` and `1000‱` legal and `3.212%`, `3.22‰`, + * `3.2‱`, `107%`, `-7%` and a bare `50` illegal. + */ +val TC_PERCENT: Combinator = ValueChecked( + AlternativeCombinator( + SequenceCombinator(PCT_INT, ZeroOrOne(SequenceCombinator(DOT, RegexTerminal("[0-9]{1,2}", "[0-9]{1,2}"))), LiteralChoiceTerminal("%")), + SequenceCombinator(PCT_INT, ZeroOrOne(SequenceCombinator(DOT, RegexTerminal("[0-9]", "[0-9]"))), LiteralChoiceTerminal("‰")), + SequenceCombinator(PCT_INT, LiteralChoiceTerminal("‱")), + ), +) { text -> evaluatePermyriad(text)?.let { it in 0..10_000 } } + +private val PCT_EVAL = Regex("""(.*?)(?:\.([0-9]+))?([%‰‱])""") + +/** parse_permyriad_unbounded(text), or null if not evaluable (the integer part is read like [PCT_INT]). */ +fun evaluatePermyriad(text: String): Long? { + val m = PCT_EVAL.matchEntire(text) ?: return null + val n = PCT_INT.valueOf(m.groupValues[1])?.toLong() ?: return null + val frac = m.groupValues[2] + return when (m.groupValues[3]) { + "%" -> n * 100 + (frac.padEnd(2, '0').toLongOrNull() ?: 0) + "‰" -> n * 10 + (frac.toLongOrNull() ?: 0) + else -> n + } +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBfifoSizeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBfifoSizeOptionValueTest.kt new file mode 100644 index 00000000..27920cc0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseBfifoSizeOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseBfifoSizeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [BFIFO] + LimitBytes=1000000 + LimitBytes=1K + LimitBytes=1M + LimitBytes=1.5M + LimitBytes=4095M + LimitBytes=4294967295 + LimitBytes=3G 1023M + LimitBytes= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [BFIFO] + LimitBytes=4G + LimitBytes=4294967296 + LimitBytes=1k + LimitBytes=-1 + LimitBytes=abc + LimitBytes=1M 1M + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeAckFilterOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeAckFilterOptionValueTest.kt new file mode 100644 index 00000000..78625491 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeAckFilterOptionValueTest.kt @@ -0,0 +1,62 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCakeAckFilterOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [CAKE] + AckFilter=aggressive + AckFilter=yes + AckFilter=no + AckFilter=true + AckFilter=false + AckFilter=1 + AckFilter=0 + AckFilter=on + AckFilter=off + AckFilter=YES + AckFilter=No + AckFilter=Yes + AckFilter=TRUE + AckFilter=oFf + AckFilter=tRUE + AckFilter=N + AckFilter= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [CAKE] + AckFilter=Aggressive + AckFilter=maybe + AckFilter=aggresive + AckFilter=2 + AckFilter=filter + AckFilter=AGGRESSIVE + AckFilter=aggressivE + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeBandwidthOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeBandwidthOptionValueTest.kt new file mode 100644 index 00000000..2dcedf88 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeBandwidthOptionValueTest.kt @@ -0,0 +1,53 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCakeBandwidthOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [CAKE] + Bandwidth=500M + Bandwidth=1G + Bandwidth=100K + Bandwidth=1.5M + Bandwidth=1G 500M + Bandwidth=100000 + Bandwidth= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [CAKE] + Bandwidth=10Mbit + Bandwidth=1k + Bandwidth=-1M + Bandwidth=3B3.5G + Bandwidth=12P12P + Bandwidth=1M 1M + Bandwidth=abc + Bandwidth=.5M + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeFlowIsolationModeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeFlowIsolationModeOptionValueTest.kt new file mode 100644 index 00000000..a97157ad --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseCakeFlowIsolationModeOptionValueTest.kt @@ -0,0 +1,52 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseCakeFlowIsolationModeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [CAKE] + FlowIsolationMode=none + FlowIsolationMode=src-host + FlowIsolationMode=dst-host + FlowIsolationMode=hosts + FlowIsolationMode=flows + FlowIsolationMode=dual-src-host + FlowIsolationMode=dual-dst-host + FlowIsolationMode=triple + FlowIsolationMode= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [CAKE] + FlowIsolationMode=None + FlowIsolationMode=dual + FlowIsolationMode=triple-isolate + FlowIsolationMode=src_host + FlowIsolationMode=yes + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(5, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDrrSizeOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDrrSizeOptionValueTest.kt new file mode 100644 index 00000000..21931169 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseDrrSizeOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseDrrSizeOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [DeficitRoundRobinSchedulerClass] + QuantumBytes=2000 + QuantumBytes=1K + QuantumBytes=1.5K + QuantumBytes=4095M + QuantumBytes=4294967295 + QuantumBytes=3G 1023M + QuantumBytes= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [DeficitRoundRobinSchedulerClass] + QuantumBytes=4G + QuantumBytes=4294967296 + QuantumBytes=1k + QuantumBytes=-1 + QuantumBytes=abc + QuantumBytes=1K 1K + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsPrioOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsPrioOptionValueTest.kt new file mode 100644 index 00000000..5796bcb5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsPrioOptionValueTest.kt @@ -0,0 +1,58 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseEtsPrioOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts 0b+1 and 0b-0 (a sign may follow the 0b prefix). + // language="unit file (systemd)" + val file = """ + [EnhancedTransmissionSelection] + PriorityMap=8 7 6 5 + PriorityMap=3 4 5 + PriorityMap=6 7 + PriorityMap=0 + PriorityMap=255 + PriorityMap=0x0f 017 0b1 + PriorityMap=0b+1 + PriorityMap=1 0b-0 2 + PriorityMap=0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + PriorityMap= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects 0b 1 and 0b +1 (the list splits on blanks, so the word is "0b") and 0b-1 (ERANGE). + // language="unit file (systemd)" + val file = """ + [EnhancedTransmissionSelection] + PriorityMap=256 + PriorityMap=-1 + PriorityMap=1,2 + PriorityMap=a b + PriorityMap=0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 + PriorityMap=0b 1 + PriorityMap=0b +1 + PriorityMap=0b-1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsQuantaOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsQuantaOptionValueTest.kt new file mode 100644 index 00000000..e836feb7 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseEtsQuantaOptionValueTest.kt @@ -0,0 +1,54 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseEtsQuantaOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [EnhancedTransmissionSelection] + QuantumBytes=2 4 6 + QuantumBytes=1001 1002 1003 + QuantumBytes=1004 1005 + QuantumBytes=1K + QuantumBytes=1K512B + QuantumBytes=1.5K 2M + QuantumBytes=4294967295 + QuantumBytes=1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 + QuantumBytes= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [EnhancedTransmissionSelection] + QuantumBytes=0 + QuantumBytes=4G + QuantumBytes=1 0 2 + QuantumBytes=1K 512 B + QuantumBytes=abc + QuantumBytes=1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 + QuantumBytes=1k + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelBoolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelBoolOptionValueTest.kt new file mode 100644 index 00000000..7163924a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelBoolOptionValueTest.kt @@ -0,0 +1,64 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqCodelBoolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + ECN=yes + ECN=no + ECN=1 + ECN=0 + ECN=true + ECN=false + ECN=on + ECN=off + ECN=y + ECN=n + ECN=t + ECN=f + ECN=YES + ECN=False + ECN=On + ECN=Yes + ECN=TRUE + ECN=oFf + ECN=Y + ECN= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + ECN=maybe + ECN=2 + ECN=enable + ECN=nope + ECN=Maybe + ECN=ENABLE + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelU32OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelU32OptionValueTest.kt new file mode 100644 index 00000000..bd8d3047 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelU32OptionValueTest.kt @@ -0,0 +1,96 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqCodelU32OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + PacketLimit=0 + Flows=0 + PacketLimit=1 + Flows=1 + PacketLimit=20480 + Flows=20480 + PacketLimit=4294967295 + Flows=4294967295 + PacketLimit=0x10 + Flows=0x10 + PacketLimit=0o17 + Flows=0o17 + PacketLimit=0b101 + Flows=0b101 + PacketLimit=017 + Flows=017 + PacketLimit=+5 + Flows=+5 + PacketLimit=-0 + Flows=-0 + PacketLimit=0b+1 + Flows=0b+1 + PacketLimit=0b-0 + Flows=0b-0 + PacketLimit=0b 1 + Flows=0b 1 + PacketLimit=0o 7 + Flows=0o 7 + PacketLimit=0b +1 + Flows=0b +1 + PacketLimit=0b -0 + Flows=0b -0 + PacketLimit= + Flows= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + PacketLimit=4294967296 + Flows=4294967296 + PacketLimit=-1 + Flows=-1 + PacketLimit=08 + Flows=08 + PacketLimit=0x + Flows=0x + PacketLimit=1.5 + Flows=1.5 + PacketLimit=abc + Flows=abc + PacketLimit=+0b1 + Flows=+0b1 + PacketLimit=12 13 + Flows=12 13 + PacketLimit=0b-1 + Flows=0b-1 + PacketLimit=0b -1 + Flows=0b -1 + PacketLimit=-0b0 + Flows=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(22, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelUsecOptionValueTest.kt new file mode 100644 index 00000000..8ceaf4d8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqCodelUsecOptionValueTest.kt @@ -0,0 +1,130 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqCodelUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + TargetSec=10ms + IntervalSec=10ms + CEThresholdSec=10ms + TargetSec=200ms + IntervalSec=200ms + CEThresholdSec=200ms + TargetSec=1min30s + IntervalSec=1min30s + CEThresholdSec=1min30s + TargetSec=5s 500ms + IntervalSec=5s 500ms + CEThresholdSec=5s 500ms + TargetSec=5s500ms + IntervalSec=5s500ms + CEThresholdSec=5s500ms + TargetSec=.5s + IntervalSec=.5s + CEThresholdSec=.5s + TargetSec=+3.1s + IntervalSec=+3.1s + CEThresholdSec=+3.1s + TargetSec=3.1s.2 + IntervalSec=3.1s.2 + CEThresholdSec=3.1s.2 + TargetSec=3.1 .2 + IntervalSec=3.1 .2 + CEThresholdSec=3.1 .2 + TargetSec=1.5h + IntervalSec=1.5h + CEThresholdSec=1.5h + TargetSec=23us + IntervalSec=23us + CEThresholdSec=23us + TargetSec=23µs + IntervalSec=23µs + CEThresholdSec=23µs + TargetSec=2 weeks + IntervalSec=2 weeks + CEThresholdSec=2 weeks + TargetSec=100 + IntervalSec=100 + CEThresholdSec=100 + # Largest finite values parse_time() accepts (total < USEC_INFINITY, each integer <= LLONG_MAX). + TargetSec=9223372036854775807us 9223372036854775807us + IntervalSec=9223372036854775807us 9223372036854775807us + CEThresholdSec=9223372036854775807us 9223372036854775807us + TargetSec=18446744073708s + IntervalSec=18446744073708s + CEThresholdSec=18446744073708s + TargetSec=infinity + IntervalSec=infinity + CEThresholdSec=infinity + TargetSec= + IntervalSec= + CEThresholdSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [FairQueueingControlledDelay] + TargetSec=5. + IntervalSec=5. + CEThresholdSec=5. + TargetSec=-5s + IntervalSec=-5s + CEThresholdSec=-5s + TargetSec=3.s + IntervalSec=3.s + CEThresholdSec=3.s + TargetSec=12.34.56 + IntervalSec=12.34.56 + CEThresholdSec=12.34.56 + TargetSec=5mins + IntervalSec=5mins + CEThresholdSec=5mins + TargetSec=10 foo + IntervalSec=10 foo + CEThresholdSec=10 foo + TargetSec=infinity 5s + IntervalSec=infinity 5s + CEThresholdSec=infinity 5s + TargetSec=Infinity + IntervalSec=Infinity + CEThresholdSec=Infinity + # parse_time() ERANGE: integer above LLONG_MAX, or total reaching USEC_INFINITY. + TargetSec=9223372036854775808us + IntervalSec=9223372036854775808us + CEThresholdSec=9223372036854775808us + TargetSec=18446744073709551615us + IntervalSec=18446744073709551615us + CEThresholdSec=18446744073709551615us + TargetSec=9223372036854775807us 9223372036854775807us 1us + IntervalSec=9223372036854775807us 9223372036854775807us 1us + CEThresholdSec=9223372036854775807us 9223372036854775807us 1us + TargetSec=18446744073709s + IntervalSec=18446744073709s + CEThresholdSec=18446744073709s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(36, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqMaxRateOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqMaxRateOptionValueTest.kt new file mode 100644 index 00000000..dc9792fe --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqMaxRateOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqMaxRateOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [FairQueueing] + MaximumRate=1M + MaximumRate=0.5G + MaximumRate=34G + MaximumRate=34359738367 + MaximumRate=34359738367B + MaximumRate=1G 500M + MaximumRate= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [FairQueueing] + MaximumRate=34359738368 + MaximumRate=35G + MaximumRate=1T + MaximumRate=10mbit + MaximumRate=-1M + MaximumRate=1M 2G + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqUsecOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqUsecOptionValueTest.kt new file mode 100644 index 00000000..818697c5 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseFqUsecOptionValueTest.kt @@ -0,0 +1,51 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseFqUsecOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [FairQueueing] + CEThresholdSec=100ms + CEThresholdSec=1s + CEThresholdSec=4294967295us + CEThresholdSec=71min 34s + CEThresholdSec=1h 11min 34.967295s + CEThresholdSec=0 + CEThresholdSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [FairQueueing] + CEThresholdSec=4294967296us + CEThresholdSec=72min + CEThresholdSec=infinity + CEThresholdSec=1h 12min + CEThresholdSec=5x + CEThresholdSec=-1s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredBoolOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredBoolOptionValueTest.kt new file mode 100644 index 00000000..0f81298a --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredBoolOptionValueTest.kt @@ -0,0 +1,64 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseGredBoolOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [GenericRandomEarlyDetection] + GenericRIO=yes + GenericRIO=no + GenericRIO=1 + GenericRIO=0 + GenericRIO=true + GenericRIO=false + GenericRIO=on + GenericRIO=off + GenericRIO=y + GenericRIO=n + GenericRIO=t + GenericRIO=f + GenericRIO=YES + GenericRIO=False + GenericRIO=On + GenericRIO=Yes + GenericRIO=TRUE + GenericRIO=oFf + GenericRIO=Y + GenericRIO= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [GenericRandomEarlyDetection] + GenericRIO=maybe + GenericRIO=2 + GenericRIO=enable + GenericRIO=nope + GenericRIO=Maybe + GenericRIO=ENABLE + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(6, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredU32OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredU32OptionValueTest.kt new file mode 100644 index 00000000..657eee48 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseGredU32OptionValueTest.kt @@ -0,0 +1,88 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseGredU32OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [GenericRandomEarlyDetection] + VirtualQueues=12 + DefaultVirtualQueue=12 + VirtualQueues=10 + DefaultVirtualQueue=10 + VirtualQueues=0 + DefaultVirtualQueue=0 + VirtualQueues=16 + DefaultVirtualQueue=16 + VirtualQueues=0x10 + DefaultVirtualQueue=0x10 + VirtualQueues=0b1 + DefaultVirtualQueue=0b1 + VirtualQueues=020 + DefaultVirtualQueue=020 + VirtualQueues=+3 + DefaultVirtualQueue=+3 + VirtualQueues=0b+1 + DefaultVirtualQueue=0b+1 + VirtualQueues=0b-0 + DefaultVirtualQueue=0b-0 + VirtualQueues=0b 1 + DefaultVirtualQueue=0b 1 + VirtualQueues=0o 7 + DefaultVirtualQueue=0o 7 + VirtualQueues=0b +1 + DefaultVirtualQueue=0b +1 + VirtualQueues=0b -0 + DefaultVirtualQueue=0b -0 + VirtualQueues= + DefaultVirtualQueue= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [GenericRandomEarlyDetection] + VirtualQueues=17 + DefaultVirtualQueue=17 + VirtualQueues=0x11 + DefaultVirtualQueue=0x11 + VirtualQueues=-1 + DefaultVirtualQueue=-1 + VirtualQueues=abc + DefaultVirtualQueue=abc + VirtualQueues=4294967296 + DefaultVirtualQueue=4294967296 + VirtualQueues=1.0 + DefaultVirtualQueue=1.0 + VirtualQueues=0b-1 + DefaultVirtualQueue=0b-1 + VirtualQueues=0b -1 + DefaultVirtualQueue=0b -1 + VirtualQueues=-0b0 + DefaultVirtualQueue=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(18, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassRateOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassRateOptionValueTest.kt new file mode 100644 index 00000000..a4f3475b --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassRateOptionValueTest.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseHtbClassRateOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucketClass] + Rate=1M + CeilRate=1M + Rate=0.5M + CeilRate=0.5M + Rate=1G + CeilRate=1G + Rate=100G + CeilRate=100G + Rate=1G 500M + CeilRate=1G 500M + Rate=3.5G3B + CeilRate=3.5G3B + Rate=4T3G3B + CeilRate=4T3G3B + Rate=10.M + CeilRate=10.M + Rate=125000 + CeilRate=125000 + Rate=+5K + CeilRate=+5K + Rate=1000 B + CeilRate=1000 B + Rate= + CeilRate= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucketClass] + Rate=10Mbit + CeilRate=10Mbit + Rate=1k + CeilRate=1k + Rate=-1M + CeilRate=-1M + Rate=3B3.5G + CeilRate=3B3.5G + Rate=12P12P + CeilRate=12P12P + Rate=1M 1M + CeilRate=1M 1M + Rate=abc + CeilRate=abc + Rate=.5M + CeilRate=.5M + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(16, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassU32OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassU32OptionValueTest.kt new file mode 100644 index 00000000..6401d690 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbClassU32OptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseHtbClassU32OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucketClass] + Priority=0 + Priority=1 + Priority=20480 + Priority=4294967295 + Priority=0x10 + Priority=0o17 + Priority=0b101 + Priority=017 + Priority=+5 + Priority=-0 + Priority=0b+1 + Priority=0b-0 + Priority=0b 1 + Priority=0o 7 + Priority=0b +1 + Priority=0b -0 + Priority= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucketClass] + Priority=4294967296 + Priority=-1 + Priority=08 + Priority=0x + Priority=1.5 + Priority=abc + Priority=+0b1 + Priority=12 13 + Priority=0b-1 + Priority=0b -1 + Priority=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbDefaultClassOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbDefaultClassOptionValueTest.kt new file mode 100644 index 00000000..c97135b9 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbDefaultClassOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseHtbDefaultClassOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucket] + DefaultClass=30 + DefaultClass=0x30 + DefaultClass=ff + DefaultClass=FFFF + DefaultClass=ffffffff + DefaultClass=0 + DefaultClass=+a + DefaultClass=0b1 + DefaultClass= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucket] + DefaultClass=100000000 + DefaultClass=0x100000000 + DefaultClass=0o7 + DefaultClass=g + DefaultClass=-1 + DefaultClass=1 2 + DefaultClass=0b 1 + DefaultClass=0x 1 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbU32OptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbU32OptionValueTest.kt new file mode 100644 index 00000000..a09ee769 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseHtbU32OptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseHtbU32OptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucket] + RateToQuantum=0 + RateToQuantum=1 + RateToQuantum=20480 + RateToQuantum=4294967295 + RateToQuantum=0x10 + RateToQuantum=0o17 + RateToQuantum=0b101 + RateToQuantum=017 + RateToQuantum=+5 + RateToQuantum=-0 + RateToQuantum=0b+1 + RateToQuantum=0b-0 + RateToQuantum=0b 1 + RateToQuantum=0o 7 + RateToQuantum=0b +1 + RateToQuantum=0b -0 + RateToQuantum= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [HierarchyTokenBucket] + RateToQuantum=4294967296 + RateToQuantum=-1 + RateToQuantum=08 + RateToQuantum=0x + RateToQuantum=1.5 + RateToQuantum=abc + RateToQuantum=+0b1 + RateToQuantum=12 13 + RateToQuantum=0b-1 + RateToQuantum=0b -1 + RateToQuantum=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemDelayOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemDelayOptionValueTest.kt new file mode 100644 index 00000000..b9573441 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemDelayOptionValueTest.kt @@ -0,0 +1,100 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNetemDelayOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [NetworkEmulator] + DelaySec=10ms + DelayJitterSec=10ms + DelaySec=200ms + DelayJitterSec=200ms + DelaySec=1min30s + DelayJitterSec=1min30s + DelaySec=5s 500ms + DelayJitterSec=5s 500ms + DelaySec=5s500ms + DelayJitterSec=5s500ms + DelaySec=.5s + DelayJitterSec=.5s + DelaySec=+3.1s + DelayJitterSec=+3.1s + DelaySec=3.1s.2 + DelayJitterSec=3.1s.2 + DelaySec=3.1 .2 + DelayJitterSec=3.1 .2 + DelaySec=1.5h + DelayJitterSec=1.5h + DelaySec=23us + DelayJitterSec=23us + DelaySec=23µs + DelayJitterSec=23µs + DelaySec=2 weeks + DelayJitterSec=2 weeks + DelaySec=100 + DelayJitterSec=100 + # Largest finite values parse_time() accepts (total < USEC_INFINITY, each integer <= LLONG_MAX). + DelaySec=9223372036854775807us 9223372036854775807us + DelayJitterSec=9223372036854775807us 9223372036854775807us + DelaySec=18446744073708s + DelayJitterSec=18446744073708s + DelaySec=infinity + DelayJitterSec=infinity + DelaySec= + DelayJitterSec= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [NetworkEmulator] + DelaySec=5. + DelayJitterSec=5. + DelaySec=-5s + DelayJitterSec=-5s + DelaySec=3.s + DelayJitterSec=3.s + DelaySec=12.34.56 + DelayJitterSec=12.34.56 + DelaySec=5mins + DelayJitterSec=5mins + DelaySec=10 foo + DelayJitterSec=10 foo + DelaySec=infinity 5s + DelayJitterSec=infinity 5s + DelaySec=Infinity + DelayJitterSec=Infinity + # parse_time() ERANGE: integer above LLONG_MAX, or total reaching USEC_INFINITY. + DelaySec=9223372036854775808us + DelayJitterSec=9223372036854775808us + DelaySec=18446744073709551615us + DelayJitterSec=18446744073709551615us + DelaySec=9223372036854775807us 9223372036854775807us 1us + DelayJitterSec=9223372036854775807us 9223372036854775807us 1us + DelaySec=18446744073709s + DelayJitterSec=18446744073709s + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(24, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemRateOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemRateOptionValueTest.kt new file mode 100644 index 00000000..5a4ca6ee --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseNetemRateOptionValueTest.kt @@ -0,0 +1,94 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseNetemRateOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts the 0b/0o spellings: the integer part goes through safe_atoi(). + // language="unit file (systemd)" + val file = """ + [NetworkEmulator] + LossRate=20% + DuplicateRate=20% + LossRate=0% + DuplicateRate=0% + LossRate=100% + DuplicateRate=100% + LossRate=55.5% + DuplicateRate=55.5% + LossRate=55.53% + DuplicateRate=55.53% + LossRate=555.5‰ + DuplicateRate=555.5‰ + LossRate=1000‰ + DuplicateRate=1000‰ + LossRate=1000‱ + DuplicateRate=1000‱ + LossRate=0x10% + DuplicateRate=0x10% + LossRate=0b 1% + DuplicateRate=0b 1% + LossRate=0b+1% + DuplicateRate=0b+1% + LossRate=0b-0% + DuplicateRate=0b-0% + LossRate=-0% + DuplicateRate=-0% + LossRate=0b 1.5% + DuplicateRate=0b 1.5% + LossRate=0o 7‰ + DuplicateRate=0o 7‰ + LossRate= + DuplicateRate= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects 0b-1% and 0b -1% (the integer part is -1: ERANGE). + // language="unit file (systemd)" + val file = """ + [NetworkEmulator] + LossRate=20 + DuplicateRate=20 + LossRate=107% + DuplicateRate=107% + LossRate=-7% + DuplicateRate=-7% + LossRate=3.212% + DuplicateRate=3.212% + LossRate=3.22‰ + DuplicateRate=3.22‰ + LossRate=3.2‱ + DuplicateRate=3.2‱ + LossRate=abc% + DuplicateRate=abc% + LossRate=% + DuplicateRate=% + LossRate=20 % + DuplicateRate=20 % + LossRate=0b-1% + DuplicateRate=0b-1% + LossRate=0b -1% + DuplicateRate=0b -1% + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(22, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePiePacketLimitOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePiePacketLimitOptionValueTest.kt new file mode 100644 index 00000000..298306d2 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParsePiePacketLimitOptionValueTest.kt @@ -0,0 +1,68 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParsePiePacketLimitOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [PIE] + PacketLimit=0 + PacketLimit=1 + PacketLimit=20480 + PacketLimit=4294967295 + PacketLimit=0x10 + PacketLimit=0o17 + PacketLimit=0b101 + PacketLimit=017 + PacketLimit=+5 + PacketLimit=-0 + PacketLimit=0b+1 + PacketLimit=0b-0 + PacketLimit=0b 1 + PacketLimit=0o 7 + PacketLimit=0b +1 + PacketLimit=0b -0 + PacketLimit= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [PIE] + PacketLimit=4294967296 + PacketLimit=-1 + PacketLimit=08 + PacketLimit=0x + PacketLimit=1.5 + PacketLimit=abc + PacketLimit=+0b1 + PacketLimit=12 13 + PacketLimit=0b-1 + PacketLimit=0b -1 + PacketLimit=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(11, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseQfqMaxPacketOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseQfqMaxPacketOptionValueTest.kt new file mode 100644 index 00000000..5fd5c2b8 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseQfqMaxPacketOptionValueTest.kt @@ -0,0 +1,55 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseQfqMaxPacketOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [QuickFairQueueingClass] + MaxPacketBytes=512 + MaxPacketBytes=16000 + MaxPacketBytes=8000 + MaxPacketBytes=65536 + MaxPacketBytes=64K + MaxPacketBytes=1K + MaxPacketBytes=0.5K + MaxPacketBytes=1K 512B + MaxPacketBytes=63K1023B + MaxPacketBytes= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [QuickFairQueueingClass] + MaxPacketBytes=511 + MaxPacketBytes=65537 + MaxPacketBytes=65K + MaxPacketBytes=1M + MaxPacketBytes=0 + MaxPacketBytes=16000b + MaxPacketBytes=abc + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(7, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTbfRateOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTbfRateOptionValueTest.kt new file mode 100644 index 00000000..59fd41fd --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTbfRateOptionValueTest.kt @@ -0,0 +1,78 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTbfRateOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // language="unit file (systemd)" + val file = """ + [TokenBucketFilter] + Rate=1M + PeakRate=1M + Rate=0.5M + PeakRate=0.5M + Rate=1G + PeakRate=1G + Rate=100G + PeakRate=100G + Rate=1G 500M + PeakRate=1G 500M + Rate=3.5G3B + PeakRate=3.5G3B + Rate=4T3G3B + PeakRate=4T3G3B + Rate=10.M + PeakRate=10.M + Rate=125000 + PeakRate=125000 + Rate=+5K + PeakRate=+5K + Rate=1000 B + PeakRate=1000 B + Rate= + PeakRate= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // language="unit file (systemd)" + val file = """ + [TokenBucketFilter] + Rate=10Mbit + PeakRate=10Mbit + Rate=1k + PeakRate=1k + Rate=-1M + PeakRate=-1M + Rate=3B3.5G + PeakRate=3B3.5G + Rate=12P12P + PeakRate=12P12P + Rate=1M 1M + PeakRate=1M 1M + Rate=abc + PeakRate=abc + Rate=.5M + PeakRate=.5M + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(16, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTeqlIdOptionValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTeqlIdOptionValueTest.kt new file mode 100644 index 00000000..55c99220 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/ai/ConfigParseTeqlIdOptionValueTest.kt @@ -0,0 +1,60 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest +import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection +import org.junit.Test + +class ConfigParseTeqlIdOptionValueTest : AbstractUnitFileTest() { + + @Test + fun testValidValues() { + // Fixture values from systemd test/test-network/conf plus tricky spellings the C parser accepts. + // sd-check at 0f0682b773 accepts these: strtoul starts afresh after the 0b/0o prefix, so a sign or blank may follow it. + // language="unit file (systemd)" + val file = """ + [TrivialLinkEqualizer] + Id=0 + Id=1 + Id=2147483647 + Id=0x7fffffff + Id=0o7 + Id=0b+1 + Id=0b-0 + Id=0b 1 + Id=0o 7 + Id=0b +1 + Id=0b -0 + Id= + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(0, highlights) + } + + @Test + fun testInvalidValues() { + // sd-check at 0f0682b773 rejects these: 0b-1 and 0b -1 are -1 (ERANGE), -0b0 is -0 followed by junk (EINVAL). + // language="unit file (systemd)" + val file = """ + [TrivialLinkEqualizer] + Id=2147483648 + Id=4294967295 + Id=-1 + Id=abc + Id=teql1 + Id=0b-1 + Id=0b -1 + Id=-0b0 + """.trimIndent() + + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + assertSize(8, highlights) + } + +}