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) + } +}