Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,33 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
Validator("config_parse_socket_service", "0") to ConfigParseSocketServiceOptionValue() as OptionValueInformation,
Validator("config_parse_service_sockets", "0") to ConfigParseServiceSocketsOptionValue() as OptionValueInformation,

// Batch 2026-10: unit-core
// config_parse_timer: TIMER_CALENDAR goes to calendar_spec_from_string, every other ltype to parse_sec.
Validator("config_parse_timer", "TIMER_CALENDAR") to ConfigParseTimerCalendarOptionValue() as OptionValueInformation,
Validator("config_parse_timer", "TIMER_ACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation,
Validator("config_parse_timer", "TIMER_BOOT") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation,
Validator("config_parse_timer", "TIMER_STARTUP") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation,
Validator("config_parse_timer", "TIMER_UNIT_ACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation,
Validator("config_parse_timer", "TIMER_UNIT_INACTIVE") to ConfigParseTimerMonotonicOptionValue() as OptionValueInformation,
// config_parse_socket_listen: SOCKET_SOCKET covers four keys with different value types, so its
// validator dispatches on the key name, exactly as the C function does.
Validator("config_parse_socket_listen", "SOCKET_SOCKET") to ConfigParseSocketListenSocketOptionValue() as OptionValueInformation,
Validator("config_parse_socket_listen", "SOCKET_FIFO") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation,
Validator("config_parse_socket_listen", "SOCKET_MQUEUE") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation,
Validator("config_parse_socket_listen", "SOCKET_USB_FUNCTION") to ConfigParseSocketListenFileOptionValue() as OptionValueInformation,
Validator("config_parse_socket_listen", "SOCKET_SPECIAL") to ConfigParseSocketListenSpecialOptionValue() as OptionValueInformation,
Validator("config_parse_service_refresh_on_reload", "0") to ConfigParseServiceRefreshOnReloadOptionValue() as OptionValueInformation,
Validator("config_parse_pid_file", "0") to ConfigParsePidFileOptionValue() as OptionValueInformation,
Validator("config_parse_open_file", "0") to ConfigParseOpenFileOptionValue() as OptionValueInformation,
Validator("config_parse_job_timeout_sec", "0") to ConfigParseJobTimeoutSecOptionValue() as OptionValueInformation,
Validator("config_parse_bus_name", "0") to ConfigParseBusNameOptionValue() as OptionValueInformation,
// RequiresOverridable=/RequisiteOverridable= just log and forward to config_parse_unit_deps.
Validator("config_parse_obsolete_unit_deps", "UNIT_REQUIRES") to UnitDependencyOptionValue() as OptionValueInformation,
Validator("config_parse_obsolete_unit_deps", "UNIT_REQUISITE") to UnitDependencyOptionValue() as OptionValueInformation,
Validator("config_parse_path", "0") to ConfigParsePathOptionValue() as OptionValueInformation,
Validator("config_parse_long", "0") to ConfigParseLongOptionValue() as OptionValueInformation,
Validator("config_parse_iec_size_long", "0") to ConfigParseIecSizeLongOptionValue() as OptionValueInformation,

)

return allValidators
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai

import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.withUnitSpecifiers
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.DOT
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.OneOrMore
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator

/*
* [Service] BusName=
*
* man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#BusName=
* parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L2421-L2453 config_parse_bus_name (0)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/core/unit-printf.c#L179 unit_full_printf_full (specifiers expanded first)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-bus/bus-type.c#L144 sd_bus_service_name_is_valid
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/libsystemd/sd-bus/bus-internal.c#L119 service_name_is_valid
* linux https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-names-bus
*/

// One dot-separated element of a well-known name: letters, digits, `_` and `-`, not starting with a
// digit. The syntactic shape is wider so a digit-led element is reported as the bad token.
private val WELL_KNOWN_ELEMENT = RegexTerminal("[A-Za-z0-9_-]+", "[A-Za-z_-][A-Za-z0-9_-]*")

// In a unique name (leading `:`) elements may start with a digit.
private val UNIQUE_ELEMENT = RegexTerminal("[A-Za-z0-9_-]+", "[A-Za-z0-9_-]+")

/**
* Validator for `[Service] BusName=` (config_parse_bus_name, the only key using it).
*
* After specifier expansion the name must pass sd_bus_service_name_is_valid(): two or more
* dot-separated elements of `[A-Za-z0-9_-]`, none empty and none starting with a digit — or a unique
* name (`:1.42`), whose elements may start with a digit; systemd accepts that form too, odd as it is
* for this setting. The 255-character limit is not checked. Any value that uses a known unit
* specifier (`org.example.%i`) is accepted.
*/
class ConfigParseBusNameOptionValue : SimpleGrammarOptionValues(
"config_parse_bus_name",
withUnitSpecifiers(
AlternativeCombinator(
SequenceCombinator(LiteralChoiceTerminal(":"), UNIQUE_ELEMENT, OneOrMore(SequenceCombinator(DOT, UNIQUE_ELEMENT)), EOF()),
SequenceCombinator(WELL_KNOWN_ELEMENT, OneOrMore(SequenceCombinator(DOT, WELL_KNOWN_ELEMENT)), EOF()),
)
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai

import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.IEC_SIZE

/*
* [Socket] MessageQueueMessageSize=
*
* man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#MessageQueueMessageSize=
* parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1004-L1032 config_parse_iec_size_long (0)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L157-L283 parse_size (base 1024)
* tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-parse-util.c#L111 parse_size cases (1024 block)
*/

/**
* Validator for config_parse_iec_size_long, used only by `[Socket] MessageQueueMessageSize=`.
*
* A parse_size(…, 1024) byte size: `8192`, `8K`, `1.5M`, `3. K`, `4 M 11.5K`, `3.5G3B` — see
* [IEC_SIZE]. Suffixes are upper-case B/K/M/G/T/P/E only. systemd additionally requires the result to
* fit in a long and the suffixes of a multi-part size to be strictly descending; neither is checked
* here.
*
* The C parser has no empty-value case; the plugin does not validate empty assignments.
*/
class ConfigParseIecSizeLongOptionValue : SimpleGrammarOptionValues(
"config_parse_iec_size_long",
IEC_SIZE
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai

import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.TIMESPAN

/*
* [Unit] JobTimeoutSec=
*
* man https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html#JobTimeoutSec=
* parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L5594-L5630 config_parse_job_timeout_sec (0)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1293 parse_sec_fix_0 (parse_sec, then 0 means infinity)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1198-L1287 parse_time
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/time-util.c#L1147 extract_multiplier
* tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-time-util.c#L85 TEST(parse_sec_fix_0)
*/

/**
* Validator for `[Unit] JobTimeoutSec=` (config_parse_job_timeout_sec, the only key using it).
*
* The value goes straight to parse_sec_fix_0() — no specifier expansion — so it is a plain
* [TIMESPAN]: `infinity`, `0` (which this parser also turns into infinity), `90`, `5min 20s`, `1h30min`.
*
* There is no special case for an empty value in the C parser: parse_sec("") fails and the assignment
* is ignored with a warning. The plugin does not run value validators on empty assignments, so that
* case is not reported here.
*/
class ConfigParseJobTimeoutSecOptionValue : SimpleGrammarOptionValues(
"config_parse_job_timeout_sec",
TIMESPAN
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai

import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SafeAtoTerminal
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator

/*
* [Socket] MessageQueueMaxMessages=
*
* man https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#MessageQueueMaxMessages=
* parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L960 config_parse_long (DEFINE_PARSER(long, long, safe_atoli))
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.h#L114 safe_atoli (safe_atolli where long is 64-bit)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L560 safe_atolli (strtoll base 0)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/parse-util.c#L403 mangle_base (0b / 0o prefixes)
* linux https://man7.org/linux/man-pages/man7/mq_overview.7.html
* linux https://man7.org/linux/man-pages/man3/strtol.3.html
*/

/**
* Validator for config_parse_long, used only by `[Socket] MessageQueueMaxMessages=`.
*
* A signed `long` as safe_atoli() reads it ([SafeAtoTerminal], signed, base 0): optional leading
* whitespace and sign, then decimal, `0x` hex, leading-zero octal, or a `0b`/`0o` prefix — after which
* strtoll starts afresh, so `0b-1` and `0b -1` are -1 (sd-check accepts both) — anything from LONG_MIN
* to LONG_MAX on a 64-bit host.
* The setting itself only means something when positive (socket.c tests `mq_maxmsg > 0`), but the
* parser accepts any long and so does this.
*
* The C parser has no empty-value case (safe_atoli("") fails and the line is ignored with a warning);
* the plugin does not validate empty assignments, so that is not reported.
*/
class ConfigParseLongOptionValue : SimpleGrammarOptionValues(
"config_parse_long",
SequenceCombinator(
SafeAtoTerminal(
signed = true,
base = 0,
flags = emptySet(),
minInclusive = SafeAtoTerminal.INT64_MIN,
maxInclusive = SafeAtoTerminal.INT64_MAX,
acceptsWhitespace = true,
acceptsMinus = true,
),
EOF(),
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai

import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.AlternativeCombinator
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.COLON
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.LiteralChoiceTerminal
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.OneOrMore
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.RegexTerminal
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrMore
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.ZeroOrOne

/*
* [Service] OpenFile=
*
* man https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html#OpenFile=
* parser https://github.com/systemd/systemd/blob/0f0682b773/src/core/load-fragment.c#L6743-L6777 config_parse_open_file (0)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L12-L68 open_file_parse (a repeated flag is EINVAL, #L48-L49)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L70 open_file_validate (absolute path, valid fd name, at most one of read-only/append/truncate)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/open-file.c#L134 open_file_flags_table
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/fd-util.c#L554 fdname_is_valid (printable ASCII except ':', at most 255)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/extract-word.c#L10 extract_first_word (":" with EXTRACT_DONT_COALESCE_SEPARATORS|EXTRACT_CUNESCAPE; "," with 0)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/escape.c#L104 cunescape_one
* tests https://github.com/systemd/systemd/blob/0f0682b773/src/test/test-open-file.c#L7 TEST(open_file_parse)
*/

// A C escape as cunescape_one() understands it; any other backslash sequence makes open_file_parse fail.
private const val C_ESCAPE = """\\(?:[abfnrtv\\"'s]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|[0-7]{3})"""

// The path: everything up to the first ':' (a ':' inside it has to be escaped, e.g. \x3a). Must be
// absolute; path_is_valid() allows ".." here, unlike most unit-file paths. No specifier expansion.
private val OPEN_FILE_PATH = RegexTerminal("""(?:[^:\\]|\\[\s\S]?)+""", """(?:/|$C_ESCAPE)(?:[^:\\]|$C_ESCAPE)*(?=:|$)""")

// The fd name: printable ASCII other than ':'. Empty means "use the file name".
private val OPEN_FILE_FDNAME = RegexTerminal("""(?:[^:\\]|\\[\s\S]?)+""", """(?:[\x20-\x7E&&[^:\\]]|$C_ESCAPE){1,255}(?=:|$)""")

// open_file_parse refuses a flag given twice, and open_file_validate refuses more than one of
// read-only/append/truncate, so a valid list holds at most one access flag and at most one `graceful`.
// Exact literals rather than FlexibleLiteralChoiceTerminal: the two choices must not match each other's
// words syntactically, or the original (greedy, non-backtracking) engine commits to the wrong branch.
private val OPEN_FILE_ACCESS_FLAG = LiteralChoiceTerminal("read-only", "append", "truncate")
private val OPEN_FILE_GRACEFUL_FLAG = LiteralChoiceTerminal("graceful")
private val COMMA = LiteralChoiceTerminal(",")

// extract_first_word(…, ",", 0) coalesces separators, so stray commas anywhere are harmless (even a
// field of nothing but commas, which yields no flags).
private val OPEN_FILE_OPTIONS = SequenceCombinator(
ZeroOrMore(COMMA),
ZeroOrOne(
SequenceCombinator(
AlternativeCombinator(
SequenceCombinator(OPEN_FILE_ACCESS_FLAG, ZeroOrOne(SequenceCombinator(OneOrMore(COMMA), OPEN_FILE_GRACEFUL_FLAG))),
SequenceCombinator(OPEN_FILE_GRACEFUL_FLAG, ZeroOrOne(SequenceCombinator(OneOrMore(COMMA), OPEN_FILE_ACCESS_FLAG))),
),
ZeroOrMore(COMMA),
)
),
)

/**
* Validator for `[Service] OpenFile=` (config_parse_open_file, the only key using it).
*
* `PATH[:FD-NAME[:OPTIONS]]`, split on the first two colons with C escapes decoded (so a literal colon
* in the path is written `\x3a`) and nothing allowed after a third field except one trailing `:`:
*
* - PATH must be absolute;
* - FD-NAME, if given, is printable ASCII without `:` (at most 255 characters); an empty one defaults
* to the file name;
* - OPTIONS is a comma-separated list of at most one of `read-only`, `append`, `truncate` and at
* most one `graceful`, in either order (a repeated flag, or two of read-only/append/truncate, is
* refused by systemd).
*
* Not checked: a path such as `/` whose file name cannot serve as the default fd name. Unlike most
* settings this one performs no specifier expansion, so `%` is an ordinary character.
*
* An empty assignment resets the list and is handled before this grammar is consulted.
*/
class ConfigParseOpenFileOptionValue : SimpleGrammarOptionValues(
"config_parse_open_file",
SequenceCombinator(
OPEN_FILE_PATH,
ZeroOrOne(
SequenceCombinator(
COLON,
ZeroOrOne(OPEN_FILE_FDNAME),
// The third extract (EXTRACT_DONT_COALESCE_SEPARATORS) consumes the colon that ends it, so one
// trailing `:` after the options leaves nothing behind and passes the isempty(v) check.
ZeroOrOne(SequenceCombinator(COLON, OPEN_FILE_OPTIONS, ZeroOrOne(COLON))),
)
),
EOF(),
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai

import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.BatchUnitCoreGrammar.LITERAL_ABSOLUTE_PATH
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator

/*
* .nspawn [Exec] WorkingDirectory= and [Network] NamespacePath= (also journald.conf TTYPath=, a file
* type the plugin does not load).
*
* man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#WorkingDirectory=
* man https://www.freedesktop.org/software/systemd/man/latest/systemd.nspawn.html#NamespacePath=
* parser https://github.com/systemd/systemd/blob/0f0682b773/src/shared/conf-parser.c#L1378-L1414 config_parse_path (0)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/shared/parse-helpers.c#L34 path_simplify_and_warn (PATH_CHECK_ABSOLUTE)
* leaf https://github.com/systemd/systemd/blob/0f0682b773/src/basic/path-util.c#L1192 path_is_normalized (no ".." component)
*/

/**
* Validator for config_parse_path with ltype 0, used by `[Exec] WorkingDirectory=` and
* `[Network] NamespacePath=` in .nspawn files.
*
* The value is taken literally (no specifier expansion) and must be an absolute path with no `..`
* component; `.` components and repeated slashes are simplified away rather than refused. There is no
* API VFS restriction, so `NamespacePath=/proc/1/ns/net` is fine. A non-zero ltype would make the error
* fatal but accepts the same values; no gperf entry uses one today.
*
* An empty assignment clears the setting and is handled before this grammar is consulted.
*/
class ConfigParsePathOptionValue : SimpleGrammarOptionValues(
"config_parse_path",
SequenceCombinator(LITERAL_ABSOLUTE_PATH, EOF())
)
Loading
Loading