Skip to content

Commit d2ad9cb

Browse files
SJrXSteve Ramageclaude
authored
feat: add 6 validators - cpu_quota, htb_class_size, delegate, syscall_errno, address_families, pass_environ (Resolves #446) (#447)
Batch 2/5 of the burn-down extension. Each grammar mirrors the systemd C parser: - config_parse_cpu_quota: parse_permyriad_unbounded, percent form unbounded (200% valid) - config_parse_htb_class_size (TCLASS_KIND_HTB): IEC byte size, same shape as tbf_size/fq_size - config_parse_delegate: boolean OR whitespace-separated cgroup controller names - config_parse_syscall_errno: "kill" OR errno name (E*) OR integer - config_parse_address_families: "none" OR optional ~ + list of AF_* names - config_parse_pass_environ: list of env var names (specifier-aware) Note for future validators: when an Alternative contains both a FlexibleLiteralChoiceTerminal (like BOOLEAN) and a more specific structured branch, put the structured branch first. FlexibleLiteralChoice's syntactic phase accepts any short alphanumeric token in its character class, so a single-token input like "cpu" would syntactically pass BOOLEAN and AlternativeCombinator would short-circuit before reaching the structured branch. OptionValueTest missing-function count drops 394 -> 388; found-key count rises 1780 -> 1812. Co-authored-by: Steve Ramage <gitcommits@sjrx.net> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c50f7b5 commit d2ad9cb

13 files changed

Lines changed: 478 additions & 0 deletions

src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/AiGenerated.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
1212
Validator("config_parse_6rd_prefix", "0") to ConfigParse6rdPrefixOptionValue() as OptionValueInformation,
1313
Validator("config_parse_ad_actor_sys_prio", "0") to ConfigParseAdActorSysPrioOptionValue() as OptionValueInformation,
1414
Validator("config_parse_ad_user_port_key", "0") to ConfigParseAdUserPortKeyOptionValue() as OptionValueInformation,
15+
Validator("config_parse_address_families", "0") to ConfigParseAddressFamiliesOptionValue() as OptionValueInformation,
1516
Validator("config_parse_address_section", "ADDRESS_ADD_PREFIX_ROUTE") to ConfigParseAddressSectionOptionValue() as OptionValueInformation,
1617
Validator("config_parse_address_section", "ADDRESS_AUTO_JOIN") to ConfigParseAddressSectionOptionValue() as OptionValueInformation,
1718
Validator("config_parse_address_section", "ADDRESS_DAD") to ConfigParseAddressSectionOptionValue() as OptionValueInformation,
@@ -54,7 +55,9 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
5455
Validator("config_parse_codel_bool", "QDISC_KIND_CODEL") to ConfigParseCodelBoolOptionValue() as OptionValueInformation,
5556
Validator("config_parse_codel_u32", "QDISC_KIND_CODEL") to ConfigParseCodelU32OptionValue() as OptionValueInformation,
5657
Validator("config_parse_collect_mode", "0") to ConfigParseCollectModeOptionValue() as OptionValueInformation,
58+
Validator("config_parse_cpu_quota", "0") to ConfigParseCpuQuotaOptionValue() as OptionValueInformation,
5759
Validator("config_parse_cpuset_partition", "0") to ConfigParseCpusetPartitionOptionValue() as OptionValueInformation,
60+
Validator("config_parse_delegate", "0") to ConfigParseDelegateOptionValue() as OptionValueInformation,
5861
Validator("config_parse_df", "0") to ConfigParseDfOptionValue() as OptionValueInformation,
5962
Validator("config_parse_dhcp6_client_start_mode", "0") to ConfigParseDhcp6ClientStartModeOptionValue() as OptionValueInformation,
6063
Validator("config_parse_dhcp6_pd_prefix_hint", "0") to ConfigParseDhcp6PdPrefixHintOptionValue() as OptionValueInformation,
@@ -96,6 +99,7 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
9699
Validator("config_parse_geneve_ttl", "0") to ConfigParseGeneveTtlOptionValue() as OptionValueInformation,
97100
Validator("config_parse_geneve_vni", "0") to ConfigParseGeneveVniOptionValue() as OptionValueInformation,
98101
Validator("config_parse_hhf_packet_limit", "QDISC_KIND_HHF") to ConfigParseHhfPacketLimitOptionValue() as OptionValueInformation,
102+
Validator("config_parse_htb_class_size", "TCLASS_KIND_HTB") to ConfigParseHtbClassSizeOptionValue() as OptionValueInformation,
99103
Validator("config_parse_iaid", "AF_INET6") to ConfigParseIaidOptionValue() as OptionValueInformation,
100104
Validator("config_parse_iaid", "AF_INET") to ConfigParseIaidOptionValue() as OptionValueInformation,
101105
Validator("config_parse_in_addr_non_null", "AF_INET") to ConfigParseInAddrNonNullOptionValue() as OptionValueInformation,
@@ -138,6 +142,7 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
138142
Validator("config_parse_nexthop_section", "NEXTHOP_ID") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation,
139143
Validator("config_parse_nexthop_section", "NEXTHOP_ONLINK") to ConfigParseNexthopSectionOptionValue() as OptionValueInformation,
140144
Validator("config_parse_nsec", "0") to ConfigParseNsecOptionValue() as OptionValueInformation,
145+
Validator("config_parse_pass_environ", "0") to ConfigParsePassEnvironOptionValue() as OptionValueInformation,
141146
Validator("config_parse_permille", "0") to ConfigParsePermilleOptionValue() as OptionValueInformation,
142147
Validator("config_parse_pfifo_size", "QDISC_KIND_PFIFO") to ConfigParsePfifoSizeOptionValue() as OptionValueInformation,
143148
Validator("config_parse_pfifo_size", "QDISC_KIND_PFIFO_HEAD_DROP") to ConfigParsePfifoSizeOptionValue() as OptionValueInformation,
@@ -175,6 +180,7 @@ fun getAllAIGeneratedValidators(): Map<Validator, OptionValueInformation> {
175180
Validator("config_parse_sr_iov_uint32", "0") to ConfigParseSrIovUint32OptionValue() as OptionValueInformation,
176181
Validator("config_parse_sr_iov_vlan_proto", "0") to ConfigParseSrIovVlanProtoOptionValue() as OptionValueInformation,
177182
Validator("config_parse_swap_priority", "0") to ConfigParseSwapPriorityOptionValue() as OptionValueInformation,
183+
Validator("config_parse_syscall_errno", "0") to ConfigParseSyscallErrnoOptionValue() as OptionValueInformation,
178184
Validator("config_parse_tasks_max", "0") to ConfigParseTasksMaxOptionValue() as OptionValueInformation,
179185
Validator("config_parse_tbf_size", "QDISC_KIND_TBF") to ConfigParseTbfSizeOptionValue() as OptionValueInformation,
180186
Validator("config_parse_tcp_window", "0") to ConfigParseTcpWindowOptionValue() as OptionValueInformation,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for RestrictAddressFamilies=.
8+
*
9+
* C function: config_parse_address_families in src/core/load-fragment.c → parse_address_families
10+
* in src/shared/parse-helpers.c:90. Accepts:
11+
* - "none" (clears the set, sets allowlist)
12+
* - optional leading "~" (invert / denylist mode), followed by a whitespace-separated list
13+
* of address family names from af_from_name (AF_UNIX, AF_INET, AF_INET6, AF_NETLINK,
14+
* AF_PACKET, …)
15+
*
16+
* The grammar matches the "AF_" prefix loosely (any uppercase/digit/underscore tail); unknown
17+
* names slip past the grammar but fail at runtime. This is the same tradeoff as syscall_errno.
18+
*/
19+
class ConfigParseAddressFamiliesOptionValue : SimpleGrammarOptionValues(
20+
"config_parse_address_families",
21+
SequenceCombinator(
22+
AlternativeCombinator(
23+
LiteralChoiceTerminal("none"),
24+
SequenceCombinator(
25+
ZeroOrOne(LiteralChoiceTerminal("~")),
26+
RegexTerminal("AF_[A-Z0-9_]+", "AF_[A-Z0-9_]+"),
27+
ZeroOrMore(SequenceCombinator(
28+
WhitespaceTerminal(),
29+
RegexTerminal("AF_[A-Z0-9_]+", "AF_[A-Z0-9_]+")
30+
))
31+
)
32+
),
33+
EOF()
34+
)
35+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for CPUQuota=.
8+
*
9+
* C function: config_parse_cpu_quota in src/core/load-fragment.c. After the empty-input check
10+
* it calls parse_permyriad_unbounded(rvalue), which accepts N% / N.N% / N.NN% (with no upper
11+
* bound — values over 100% are meaningful here, e.g. "200%" means 2 cores). The result must
12+
* be strictly greater than zero, so "0%", "0.0%", "0.00%" are rejected at the C semantic
13+
* layer; the grammar accepts them but the runtime parser will warn.
14+
*
15+
* Only the ASCII percent form is matched here; the ‰/‱ Unicode suffixes are vanishingly rare
16+
* in unit files.
17+
*/
18+
class ConfigParseCpuQuotaOptionValue : SimpleGrammarOptionValues(
19+
"config_parse_cpu_quota",
20+
SequenceCombinator(
21+
RegexTerminal("[0-9]+(\\.[0-9]{1,2})?", "[0-9]+(\\.[0-9]{1,2})?"),
22+
LiteralChoiceTerminal("%"),
23+
EOF()
24+
)
25+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for Delegate=.
8+
*
9+
* C function: config_parse_delegate in src/core/load-fragment.c. Either a boolean (toggles
10+
* delegation for all controllers) or a whitespace-separated list of cgroup controller names
11+
* from cgroup_controller_table in src/basic/cgroup-util.c.
12+
*
13+
* Note: the controller list is the first alternative because BOOLEAN is a
14+
* FlexibleLiteralChoiceTerminal whose syntactic phase accepts any short alphanumeric token —
15+
* if it ran first, single-controller inputs like "cpu" would syntactically consume the value
16+
* as a (fake) boolean and AlternativeCombinator would short-circuit there.
17+
*/
18+
class ConfigParseDelegateOptionValue : SimpleGrammarOptionValues(
19+
"config_parse_delegate",
20+
SequenceCombinator(
21+
AlternativeCombinator(
22+
SequenceCombinator(
23+
LiteralChoiceTerminal(
24+
"cpu", "cpuacct", "cpuset", "io", "blkio", "memory", "devices", "pids",
25+
"bpf-firewall", "bpf-devices", "bpf-foreign", "bpf-socket-bind",
26+
"bpf-restrict-network-interfaces", "bpf-bind-network-interface"
27+
),
28+
ZeroOrMore(SequenceCombinator(
29+
WhitespaceTerminal(),
30+
LiteralChoiceTerminal(
31+
"cpu", "cpuacct", "cpuset", "io", "blkio", "memory", "devices", "pids",
32+
"bpf-firewall", "bpf-devices", "bpf-foreign", "bpf-socket-bind",
33+
"bpf-restrict-network-interfaces", "bpf-bind-network-interface"
34+
)
35+
))
36+
),
37+
BOOLEAN
38+
),
39+
EOF()
40+
)
41+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for the HTB class size-style options: QuantumBytes=, MTUBytes=, OverheadBytes=,
8+
* BufferBytes=, CeilBufferBytes=.
9+
*
10+
* C function: config_parse_htb_class_size(TCLASS_KIND_HTB) in src/network/tc/htb.c. After
11+
* branching on the lvalue, it calls parse_size(rvalue, 1024, &v), so the value is a decimal
12+
* byte count optionally suffixed with an IEC unit. Same shape as config_parse_tbf_size and
13+
* config_parse_fq_size.
14+
*
15+
* The per-lvalue OverheadBytes <= UINT16_MAX bound and the general v <= UINT32_MAX bound are
16+
* semantic checks that can't be expressed at the grammar level without lvalue access.
17+
*/
18+
class ConfigParseHtbClassSizeOptionValue : SimpleGrammarOptionValues(
19+
"config_parse_htb_class_size",
20+
SequenceCombinator(
21+
OptionalWhitespacePrefix(BYTES),
22+
EOF()
23+
)
24+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for PassEnvironment=.
8+
*
9+
* C function: config_parse_pass_environ in src/core/load-fragment.c. Whitespace-separated list
10+
* of environment variable names. Each word is first run through unit_env_printf (resolves
11+
* %p / %n / etc.), then env_name_is_valid: must not be empty, must not start with a digit,
12+
* and may contain only characters in VALID_BASH_ENV_NAME_CHARS (alphanumerics and underscore).
13+
*
14+
* The grammar tolerates "%X" specifiers inline by including "%" in the allowed-character set,
15+
* since the C parser resolves them before the validity check. This means a few unresolved
16+
* specifier-heavy inputs that the runtime parser would reject will pass here — preferred over
17+
* false positives for legitimate `PassEnvironment=%n_LOG` style usage.
18+
*/
19+
class ConfigParsePassEnvironOptionValue : SimpleGrammarOptionValues(
20+
"config_parse_pass_environ",
21+
SequenceCombinator(
22+
RegexTerminal("[A-Za-z_%][A-Za-z0-9_%]*", "[A-Za-z_%][A-Za-z0-9_%]*"),
23+
ZeroOrMore(SequenceCombinator(
24+
WhitespaceTerminal(),
25+
RegexTerminal("[A-Za-z_%][A-Za-z0-9_%]*", "[A-Za-z_%][A-Za-z0-9_%]*")
26+
)),
27+
EOF()
28+
)
29+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.SimpleGrammarOptionValues
4+
import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.*
5+
6+
/**
7+
* Validator for SystemCallErrorNumber=.
8+
*
9+
* C function: config_parse_syscall_errno in src/core/load-fragment.c → parse_errno in
10+
* src/basic/parse-util.c. Accepts:
11+
* - "kill" (special, resets to SECCOMP_ERROR_NUMBER_KILL)
12+
* - an errno name (uppercase "E"-prefixed, e.g. EPERM, ENOENT) via errno_from_name
13+
* - an integer that maps to a valid errno via errno_is_valid (or 0)
14+
*
15+
* The grammar matches the errno-name pattern loosely (any uppercase E-prefixed token);
16+
* unknown names like "ENOIENT" would slip past the grammar but fail at runtime. Numeric
17+
* range here is 0..4095 — wider than typical valid errnos but enough to catch obvious typos.
18+
*/
19+
class ConfigParseSyscallErrnoOptionValue : SimpleGrammarOptionValues(
20+
"config_parse_syscall_errno",
21+
SequenceCombinator(
22+
AlternativeCombinator(
23+
LiteralChoiceTerminal("kill"),
24+
RegexTerminal("E[A-Z][A-Z0-9]*", "E[A-Z][A-Z0-9]*"),
25+
IntegerTerminal(0, 4096)
26+
),
27+
EOF()
28+
)
29+
)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
4+
import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection
5+
import org.junit.Test
6+
7+
class ConfigParseAddressFamiliesOptionValueTest : AbstractUnitFileTest() {
8+
9+
@Test
10+
fun testValidValues() {
11+
// language="unit file (systemd)"
12+
val file = """
13+
[Service]
14+
RestrictAddressFamilies=none
15+
RestrictAddressFamilies=AF_INET
16+
RestrictAddressFamilies=AF_INET AF_INET6
17+
RestrictAddressFamilies=AF_UNIX AF_NETLINK
18+
RestrictAddressFamilies=~AF_PACKET
19+
RestrictAddressFamilies=~AF_INET AF_INET6
20+
RestrictAddressFamilies=AF_BRIDGE AF_X25 AF_AX25
21+
""".trimIndent()
22+
23+
setupFileInEditor("file.service", file)
24+
enableInspection(InvalidValueInspection::class.java)
25+
val highlights = myFixture.doHighlighting()
26+
27+
assertSize(0, highlights)
28+
}
29+
30+
@Test
31+
fun testInvalidValues() {
32+
// language="unit file (systemd)"
33+
val file = """
34+
[Service]
35+
RestrictAddressFamilies=<error descr="Invalid value">inet</error>
36+
RestrictAddressFamilies=<error descr="Invalid value">AF_inet</error>
37+
RestrictAddressFamilies=<error descr="Invalid value">AF_INET, AF_INET6</error>
38+
RestrictAddressFamilies=<error descr="Invalid value">~ AF_PACKET</error>
39+
RestrictAddressFamilies=<error descr="Invalid value">NONE</error>
40+
""".trimIndent()
41+
42+
setupFileInEditor("file.service", file)
43+
enableInspection(InvalidValueInspection::class.java)
44+
val highlights = myFixture.doHighlighting()
45+
46+
assertSize(5, highlights)
47+
}
48+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
4+
import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection
5+
import org.junit.Test
6+
7+
class ConfigParseCpuQuotaOptionValueTest : AbstractUnitFileTest() {
8+
9+
@Test
10+
fun testValidValues() {
11+
// language="unit file (systemd)"
12+
val file = """
13+
[Service]
14+
CPUQuota=50%
15+
CPUQuota=100%
16+
CPUQuota=200%
17+
CPUQuota=12.34%
18+
CPUQuota=1.5%
19+
CPUQuota=0.01%
20+
CPUQuota=99.99%
21+
CPUQuota=1000%
22+
""".trimIndent()
23+
24+
setupFileInEditor("file.service", file)
25+
enableInspection(InvalidValueInspection::class.java)
26+
val highlights = myFixture.doHighlighting()
27+
28+
assertSize(0, highlights)
29+
}
30+
31+
@Test
32+
fun testInvalidValues() {
33+
// language="unit file (systemd)"
34+
val file = """
35+
[Service]
36+
CPUQuota=<error descr="Invalid value">50</error>
37+
CPUQuota=<error descr="Invalid value">abc</error>
38+
CPUQuota=<error descr="Invalid value">-5%</error>
39+
CPUQuota=<error descr="Invalid value">12.345%</error>
40+
CPUQuota=<error descr="Invalid value">50% 60%</error>
41+
""".trimIndent()
42+
43+
setupFileInEditor("file.service", file)
44+
enableInspection(InvalidValueInspection::class.java)
45+
val highlights = myFixture.doHighlighting()
46+
47+
assertSize(5, highlights)
48+
}
49+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package net.sjrx.intellij.plugins.systemdunitfiles.inspections.ai
2+
3+
import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest
4+
import net.sjrx.intellij.plugins.systemdunitfiles.inspections.InvalidValueInspection
5+
import org.junit.Test
6+
7+
class ConfigParseDelegateOptionValueTest : AbstractUnitFileTest() {
8+
9+
@Test
10+
fun testValidValues() {
11+
// language="unit file (systemd)"
12+
val file = """
13+
[Service]
14+
Delegate=yes
15+
Delegate=no
16+
Delegate=true
17+
Delegate=false
18+
Delegate=cpu
19+
Delegate=cpu memory io
20+
Delegate=cpu cpuacct cpuset io blkio memory devices pids
21+
Delegate=bpf-firewall bpf-devices bpf-foreign
22+
Delegate=bpf-socket-bind bpf-restrict-network-interfaces bpf-bind-network-interface
23+
""".trimIndent()
24+
25+
setupFileInEditor("file.service", file)
26+
enableInspection(InvalidValueInspection::class.java)
27+
val highlights = myFixture.doHighlighting()
28+
29+
assertSize(0, highlights)
30+
}
31+
32+
@Test
33+
fun testInvalidValues() {
34+
// language="unit file (systemd)"
35+
val file = """
36+
[Service]
37+
Delegate=<error descr="Invalid value">maybe</error>
38+
Delegate=<error descr="Invalid value">cpu unknown</error>
39+
Delegate=<error descr="Invalid value">CPU</error>
40+
Delegate=<error descr="Invalid value">cpu,memory</error>
41+
""".trimIndent()
42+
43+
setupFileInEditor("file.service", file)
44+
enableInspection(InvalidValueInspection::class.java)
45+
val highlights = myFixture.doHighlighting()
46+
47+
assertSize(4, highlights)
48+
}
49+
}

0 commit comments

Comments
 (0)