From a7a32c74fb28c0f240d291cfa88ae27d3e036438 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Sat, 7 Jun 2025 13:46:11 -0700 Subject: [PATCH] feat: validation for IP addresses in Unit and Network files (Resolves #361) --- .../IPAddressAllowOnlyInspection.kt | 88 ++++++++++ .../intentions/AddPropertyAndValueQuickFix.kt | 32 ++++ .../semanticdata/SemanticDataRepository.kt | 2 + .../optionvalues/InAddrPrefixesOptionValue.kt | 17 ++ .../optionvalues/NetworkAddressOptionValue.kt | 20 +++ .../grammar/AlternativeCombinator.kt | 17 ++ .../optionvalues/grammar/Combinator.kt | 1 + .../optionvalues/grammar/Combinators.kt | 91 ++++++++++ .../semanticdata/optionvalues/grammar/EOF.kt | 4 + .../grammar/FlexibleLiteralChoiceTerminal.kt | 10 ++ .../optionvalues/grammar/IntegerTerminal.kt | 4 + .../grammar/LiteralChoiceTerminal.kt | 8 + .../optionvalues/grammar/OneOrMore.kt | 15 ++ .../grammar/OptionalWhitespacePrefix.kt | 31 +--- .../optionvalues/grammar/RegexTerminal.kt | 4 + .../optionvalues/grammar/Repeat.kt | 79 +++++++++ .../grammar/SequenceCombinator.kt | 17 ++ .../grammar/TerminalCombinator.kt | 6 +- .../grammar/WhitespaceTerminal.kt | 8 + .../optionvalues/grammar/ZeroOrMore.kt | 15 ++ .../optionvalues/grammar/ZeroOrOne.kt | 15 ++ src/main/resources/META-INF/plugin.xml | 4 + .../IPAddressAllowOnly.html | 22 +++ .../IPAddressAllowOnlyInspection.kt | 56 ++++++ .../InAddrPrefixOptionalValueTest.kt | 121 +++++++++++++ .../InvalidValueForNetworkAddressesTest.kt | 138 +++++++++++++++ .../optionvalues/grammar/Grammar.kt | 164 ++++++++++++++++++ 27 files changed, 962 insertions(+), 27 deletions(-) create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/AddPropertyAndValueQuickFix.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/InAddrPrefixesOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt create mode 100644 src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Repeat.kt create mode 100644 src/main/resources/inspectionDescriptions/IPAddressAllowOnly.html create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InAddrPrefixOptionalValueTest.kt create mode 100644 src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt new file mode 100644 index 00000000..ef2f0642 --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt @@ -0,0 +1,88 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import com.intellij.codeInspection.LocalInspectionTool +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiFile +import com.intellij.psi.util.PsiTreeUtil +import net.sjrx.intellij.plugins.systemdunitfiles.intentions.AddPropertyAndValueQuickFix +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFilePropertyType +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionGroups +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileVisitor +import net.sjrx.intellij.plugins.systemdunitfiles.intentions.AddPropertyQuickFix +import java.util.* + +/** + * This inspection warns when IPAddressAllow is specified without IPAddressDeny in certain sections, + * as this configuration does not block traffic (the default action is to permit). + */ +class IPAddressAllowOnlyInspection : LocalInspectionTool() { + + // Sections where this inspection applies + private val targetSections = setOf("Slice", "Scope", "Service", "Socket") + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + return object : UnitFileVisitor() { + // Map to track properties by section + private val sectionProperties = mutableMapOf>() + + override fun visitFile(file: PsiFile) { + super.visitFile(file) + + // After visiting the file, check each section for the condition + for ((section, properties) in sectionProperties) { + if (section in targetSections && + "IPAddressAllow" in properties && + "IPAddressDeny" !in properties) { + + // Find all IPAddressAllow properties in this section to highlight + val sectionElement = findSectionElement(file, section) + if (sectionElement != null) { + val ipAddressAllowProperties = findPropertiesInSection(sectionElement, "IPAddressAllow") + + for (property in ipAddressAllowProperties) { + holder.registerProblem( + property, + "Specifying IPAddressAllow without IPAddressDeny does not block traffic as the default action is to permit", + com.intellij.codeInspection.ProblemHighlightType.WEAK_WARNING, + AddPropertyAndValueQuickFix(section, "IPAddressDeny", "any") + ) + } + } + } + } + } + + override fun visitPropertyType(property: UnitFilePropertyType) { + super.visitPropertyType(property) + + val section = PsiTreeUtil.getParentOfType(property, UnitFileSectionGroups::class.java) + if (section != null && section.sectionName in targetSections) { + // Add this property to the section's property set + val sectionName = section.sectionName + if (!sectionProperties.containsKey(sectionName)) { + sectionProperties[sectionName] = mutableSetOf() + } + sectionProperties[sectionName]?.add(property.key) + } + } + + /** + * Find a section element by name in the file + */ + private fun findSectionElement(file: PsiFile, sectionName: String): UnitFileSectionGroups? { + val sections = PsiTreeUtil.findChildrenOfType(file, UnitFileSectionGroups::class.java) + return sections.find { it.sectionName == sectionName } + } + + /** + * Find all properties with a specific key in a section + */ + private fun findPropertiesInSection(section: UnitFileSectionGroups, propertyKey: String): List { + val properties = PsiTreeUtil.findChildrenOfType(section, UnitFilePropertyType::class.java) + return properties.filter { it.key == propertyKey } + } + } + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/AddPropertyAndValueQuickFix.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/AddPropertyAndValueQuickFix.kt new file mode 100644 index 00000000..1741d90a --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/intentions/AddPropertyAndValueQuickFix.kt @@ -0,0 +1,32 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.intentions + +import com.intellij.codeInspection.LocalQuickFix +import com.intellij.codeInspection.ProblemDescriptor +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import com.intellij.pom.Navigatable +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.childrenOfType +import com.intellij.psi.util.endOffset +import net.sjrx.intellij.plugins.systemdunitfiles.psi.UnitFileSectionGroups + +class AddPropertyAndValueQuickFix(val section: String, val key: String, val value: String) : LocalQuickFix { + override fun getFamilyName(): String { + return "Add ${key}=${value} to ${section}" + } + + override fun applyFix(project: Project, descriptor: ProblemDescriptor) { + + descriptor.psiElement.containingFile ?: return + val sectionGroup = PsiTreeUtil.getParentOfType(descriptor.psiElement, UnitFileSectionGroups::class.java) ?: return + val dummyFile = UnitElementFactory.createFile(project, sectionGroup.text + "\n${key}=${value}") + + val newElement = sectionGroup.replace(dummyFile.firstChild) + + (dummyFile.lastChild.navigationElement as? Navigatable)?.navigate(true) + + FileEditorManager.getInstance(project).selectedTextEditor?.caretModel?.moveToOffset(newElement.endOffset) + } + +} + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt index 48493864..a9539c7e 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/SemanticDataRepository.kt @@ -152,6 +152,8 @@ class SemanticDataRepository private constructor() { validatorMap.putAll(CPUSharesOptionValue.validators) validatorMap.putAll(CgroupSocketBindOptionValue.validators) validatorMap.putAll(RlimitOptionValue.validators) // Scopes are not supported since they aren't standard unit files. + validatorMap.putAll(NetworkAddressOptionValue.validators) + validatorMap.putAll(InAddrPrefixesOptionValue.validators) fileClassToSectionNameToKeyValuesFromDoc["unit"]?.remove(SCOPE_KEYWORD) fileClassToSectionToKeyAndValidatorMap["unit"]?.remove(SCOPE_KEYWORD) } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/InAddrPrefixesOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/InAddrPrefixesOptionValue.kt new file mode 100644 index 00000000..4a74659d --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/InAddrPrefixesOptionValue.kt @@ -0,0 +1,17 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.* + +class InAddrPrefixesOptionValue(combinator: Combinator) : GrammarOptionValue("config_parse_in_addr_prefixes", combinator) { + + companion object { + + val validators = mapOf( + Validator("config_parse_in_addr_prefixes", "AF_UNSPEC") to InAddrPrefixesOptionValue(SequenceCombinator(IP_ADDR_PREFIX_LIST, EOF())), + Validator("config_parse_in_addr_prefixes", "AF_INET") to InAddrPrefixesOptionValue(SequenceCombinator(IPV4_ADDR_PREFIX_LIST, EOF())), + Validator("config_parse_in_addr_prefixes", "AF_INET6") to InAddrPrefixesOptionValue(SequenceCombinator(IPV6_ADDR_PREFIX_LIST, EOF())) + ) + } +} + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt new file mode 100644 index 00000000..c7bac63e --- /dev/null +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/NetworkAddressOptionValue.kt @@ -0,0 +1,20 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues + +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.Validator +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.EOF +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.GrammarOptionValue +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.IP_ADDR_AND_PREFIX_LENGTH +import net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar.SequenceCombinator + +class NetworkAddressOptionValue() : GrammarOptionValue("config_parse_address_section", GRAMMAR) { + + companion object { + val GRAMMAR = SequenceCombinator( + IP_ADDR_AND_PREFIX_LENGTH, EOF()) + + val validators = mapOf( + Validator("config_parse_address_section", "ADDRESS_ADDRESS") to NetworkAddressOptionValue() + ) + } +} + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt index 69d68a58..76320d71 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/AlternativeCombinator.kt @@ -37,4 +37,21 @@ open class AlternativeCombinator(vararg val tokens: Combinator) : Combinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset, Combinator::SemanticMatch) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("Alt(\n") + for (token in tokens) { + if (token is SequenceCombinator || token is AlternativeCombinator || token is Repeat || token is ZeroOrOne || token is ZeroOrMore || token is OneOrMore) { + sb.append(token.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(token.toString()).append("\n") + } + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt index eac35737..29757dac 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinator.kt @@ -32,4 +32,5 @@ interface Combinator { */ fun SemanticMatch(value : String, offset: Int): MatchResult + fun toStringIndented(indent: Int): String } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt index feefbec4..23c47383 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Combinators.kt @@ -3,3 +3,94 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.gra val BYTES = RegexTerminal("[0-9]+[a-zA-Z]*\\s*", "[0-9]+[KMGT]?\\s*") val DEVICE = RegexTerminal("\\S+\\s*", "/[^\\u0000. ]+\\s*") val IOPS = RegexTerminal("[0-9]+[a-zA-Z]*\\s*", "[0-9]+[KMGT]?\\s*") + +var IPV4_OCTET = IntegerTerminal(0, 256) +val DOT = LiteralChoiceTerminal(".") +var IPV4_ADDR = SequenceCombinator(IPV4_OCTET, DOT, IPV4_OCTET, DOT, IPV4_OCTET, DOT, IPV4_OCTET) + +val CIDR_SEPARATOR = LiteralChoiceTerminal("/") + +val IPV4_ADDR_AND_PREFIX_LENGTH = SequenceCombinator(IPV4_ADDR, CIDR_SEPARATOR, IntegerTerminal(8, 33)) +val IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH = SequenceCombinator(IPV4_ADDR, ZeroOrOne(SequenceCombinator( CIDR_SEPARATOR, IntegerTerminal(8, 33)))) + +var IPV6_HEXTET = RegexTerminal("[0-9a-fA-F]{1,4}", "[0-9a-fA-F]{1,4}") +val COLON = LiteralChoiceTerminal(":") +val DOUBLE_COLON = LiteralChoiceTerminal("::") + + +val IPV6_FULL_SPECIFIED = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET) +val IPV6_ZERO_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 8), IPV6_HEXTET))) +val IPV6_ONE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator( Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 7), IPV6_HEXTET))) +val IPV6_TWO_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 6), IPV6_HEXTET))) +val IPV6_THREE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 5), IPV6_HEXTET))) +val IPV6_FOUR_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 4), IPV6_HEXTET))) +val IPV6_FIVE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 3), IPV6_HEXTET))) +val IPV6_SIX_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 2), IPV6_HEXTET))) +val IPV6_SEVEN_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, COLON,IPV6_HEXTET, DOUBLE_COLON, ZeroOrOne(IPV6_HEXTET)) + + +val IPV6_IPV4_SUFFIX_FULL = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV4_ADDR) +val IPV6_IPV4_SUFFIX_ZERO_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(DOUBLE_COLON,SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 6), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_ONE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, DOUBLE_COLON, SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 5), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_TWO_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON, SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 4), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_THREE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON,SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 3), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_FOUR_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON, SequenceCombinator(Repeat(SequenceCombinator(IPV6_HEXTET, COLON), 0, 2), IPV4_ADDR)) +val IPV6_IPV4_SUFFIX_FIVE_HEXTET_BEFORE_ZERO_COMP = SequenceCombinator(IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, COLON, IPV6_HEXTET, DOUBLE_COLON, IPV4_ADDR) + +//val IPV6_ALL_ZEROS = DOUBLE_COLON + +val IPV6_ADDR = AlternativeCombinator( + IPV6_IPV4_SUFFIX_FULL, + IPV6_IPV4_SUFFIX_ZERO_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_ONE_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_TWO_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_THREE_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_FOUR_HEXTET_BEFORE_ZERO_COMP, + IPV6_IPV4_SUFFIX_FIVE_HEXTET_BEFORE_ZERO_COMP, + IPV6_FULL_SPECIFIED, + IPV6_SEVEN_HEXTET_BEFORE_ZERO_COMP, + IPV6_SIX_HEXTET_BEFORE_ZERO_COMP, + IPV6_FIVE_HEXTET_BEFORE_ZERO_COMP, + IPV6_FOUR_HEXTET_BEFORE_ZERO_COMP, + IPV6_THREE_HEXTET_BEFORE_ZERO_COMP, + IPV6_TWO_HEXTET_BEFORE_ZERO_COMP, + IPV6_ONE_HEXTET_BEFORE_ZERO_COMP, + // Must go last because it's the most general and can match :: + IPV6_ZERO_HEXTET_BEFORE_ZERO_COMP, + + // I suspect maybe that this one is redundant + //IPV6_ALL_ZEROS, +) + +val IPV6_ADDR_AND_PREFIX_LENGTH = SequenceCombinator(IPV6_ADDR, CIDR_SEPARATOR, IntegerTerminal(64, 129)) +val IPV6_ADDR_AND_OPTIONAL_PREFIX_LENGTH = SequenceCombinator(IPV6_ADDR, ZeroOrOne(SequenceCombinator(CIDR_SEPARATOR, IntegerTerminal(64, 129)))) + + +var IP_ADDR_AND_PREFIX_LENGTH = AlternativeCombinator( + IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IPV6_ADDR_AND_PREFIX_LENGTH) + +var IN_ADDR_PREFIX_SPECIAL_VALUES = LiteralChoiceTerminal("any", "localhost", "link-local", "multicast") + +var IPV4_ADDR_AND_PREFIX_OR_SPECIAL = AlternativeCombinator( + IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IN_ADDR_PREFIX_SPECIAL_VALUES, +) + +var IPV6_ADDR_AND_PREFIX_OR_SPECIAL = AlternativeCombinator( + IPV6_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IN_ADDR_PREFIX_SPECIAL_VALUES, +) + + +var IP_ADDR_AND_PREFIX_OR_SPECIAL = AlternativeCombinator( + IPV4_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IPV6_ADDR_AND_OPTIONAL_PREFIX_LENGTH, + IN_ADDR_PREFIX_SPECIAL_VALUES, +) + +var IP_ADDR_PREFIX_LIST = SequenceCombinator(IP_ADDR_AND_PREFIX_OR_SPECIAL, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), IP_ADDR_AND_PREFIX_OR_SPECIAL))) +var IPV4_ADDR_PREFIX_LIST = SequenceCombinator(IPV4_ADDR_AND_PREFIX_OR_SPECIAL, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), IPV4_ADDR_AND_PREFIX_OR_SPECIAL))) +var IPV6_ADDR_PREFIX_LIST = SequenceCombinator(IPV6_ADDR_AND_PREFIX_OR_SPECIAL, ZeroOrMore(SequenceCombinator(WhitespaceTerminal(), IPV6_ADDR_AND_PREFIX_OR_SPECIAL))) + + diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt index c0316686..770b4ef7 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/EOF.kt @@ -16,4 +16,8 @@ class EOF : Combinator { NoMatch } } + + override fun toStringIndented(indent: Int): String { + return "EOF" + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt index 6e94fa00..4876bfde 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/FlexibleLiteralChoiceTerminal.kt @@ -91,5 +91,15 @@ class FlexibleLiteralChoiceTerminal(vararg val choices: String) : TerminalCombin return NoMatch.copy(longestMatch = offset) } + override fun toString(): String { + return if (choices.size == 1) { + "Literal(\"${choices[0]}\")" + } else { + "FlexLitChoice(" + choices.joinToString(",") { "\"$it\"" } + ")" + } + } + override fun toStringIndented(indent: Int): String { + return toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt index a9b3e6e6..f1107238 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/IntegerTerminal.kt @@ -22,4 +22,8 @@ class IntegerTerminal(private val minInclusive: Int,private val maxExclusive: In return MatchResult(listOf(matchResult.value), offset + matchResult.value.length, listOf(this), offset + matchResult.value.length) } + + override fun toString(): String { + return "Int($minInclusive,$maxExclusive)" + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt index 08ee0d2a..870db3d1 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/LiteralChoiceTerminal.kt @@ -23,4 +23,12 @@ class LiteralChoiceTerminal(vararg var choices: String) : TerminalCombinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset) } + + override fun toString(): String { + return if (choices.size == 1) { + "Literal(\"${choices[0]}\")" + } else { + "LitChoice(" + choices.joinToString(",") { "\"$it\"" } + ")" + } + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt index 476dfb82..f56a0f1b 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OneOrMore.kt @@ -39,4 +39,19 @@ class OneOrMore(val combinator : Combinator) : Combinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset, combinator::SemanticMatch) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("OneOrMore(\n") + if (combinator is SequenceCombinator || combinator is AlternativeCombinator || combinator is Repeat || combinator is ZeroOrOne || combinator is ZeroOrMore || combinator is OneOrMore) { + sb.append(combinator.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(combinator.toString()).append("\n") + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OptionalWhitespacePrefix.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OptionalWhitespacePrefix.kt index 53fab5ec..c7576495 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OptionalWhitespacePrefix.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/OptionalWhitespacePrefix.kt @@ -5,30 +5,9 @@ class OptionalWhitespacePrefix(val combinator: Combinator): SequenceCombinator(WhitespaceTerminal(), combinator), combinator ) { -// -// override fun SyntacticMatch(value: String, offset: Int): MatchResult { -// var newOffset = offset -// for(o in offset..= 0") + } + if (maxExclusive < minInclusive) { + throw IllegalArgumentException("maxExclusive must be >= minInclusive") + } + } + + + private fun match(value: String, offset: Int, f: (String, Int) -> MatchResult): MatchResult { + var index = offset + val tokens = mutableListOf() + val terminals = mutableListOf() + + var match = f(value, index) + var matches = 0 + + if (match.matchResult == -1) { + if (minInclusive != 0) { + // This will return a match result = -1 + return match + } + + return MatchResult(tokens, offset, terminals, match.longestMatch) + } + + var maxLength = match.longestMatch + + + while (match.matchResult != -1 && matches < maxExclusive) { + matches++ + index = match.matchResult + tokens.addAll(match.tokens) + terminals.addAll(match.terminals) + + match = f(value, index) + maxLength = max(maxLength, match.longestMatch) + } + + if (matches < minInclusive) { + return MatchResult(emptyList(), -1, emptyList(), maxLength) + } else { + return MatchResult(tokens, index, terminals, maxLength) + } + } + + override fun SyntacticMatch(value: String, offset: Int): MatchResult { + return match(value, offset, combinator::SyntacticMatch) + } + + override fun SemanticMatch(value: String, offset: Int): MatchResult { + return match(value, offset, combinator::SemanticMatch) + } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("Repeat($minInclusive,$maxExclusive\n") + if (combinator is SequenceCombinator || combinator is AlternativeCombinator || combinator is Repeat || combinator is ZeroOrOne || combinator is ZeroOrMore || combinator is OneOrMore) { + sb.append(combinator.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(combinator.toString()).append("\n") + } + sb.append(prefix).append(")") + return sb.toString() + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt index 8d674d75..ddab6a61 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/SequenceCombinator.kt @@ -54,4 +54,21 @@ open class SequenceCombinator(vararg val tokens: Combinator) : Combinator { } return MatchResult(resultTokens, index, resultTerminals, maxLength) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("Seq(\n") + for (token in tokens) { + if (token is SequenceCombinator || token is AlternativeCombinator || token is Repeat || token is ZeroOrOne || token is ZeroOrMore || token is OneOrMore) { + sb.append(token.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(token.toString()).append("\n") + } + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt index 8e01cf76..8d1705af 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/TerminalCombinator.kt @@ -1,3 +1,7 @@ package net.sjrx.intellij.plugins.systemdunitfiles.semanticdata.optionvalues.grammar -interface TerminalCombinator : Combinator +interface TerminalCombinator : Combinator { + override fun toStringIndented(indent: Int): String { + return toString() + } +} diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/WhitespaceTerminal.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/WhitespaceTerminal.kt index aa932e22..180e5eb4 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/WhitespaceTerminal.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/WhitespaceTerminal.kt @@ -26,4 +26,12 @@ class WhitespaceTerminal : TerminalCombinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset) } + + override fun toString(): String { + return "\\s+" + } + + override fun toStringIndented(indent: Int): String { + return toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt index f935ea48..beeced13 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrMore.kt @@ -41,4 +41,19 @@ class ZeroOrMore(val combinator : Combinator) : Combinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset, combinator::SemanticMatch) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("ZeroOrMore(\n") + if (combinator is SequenceCombinator || combinator is AlternativeCombinator || combinator is Repeat || combinator is ZeroOrOne || combinator is ZeroOrMore || combinator is OneOrMore) { + sb.append(combinator.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(combinator.toString()).append("\n") + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrOne.kt b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrOne.kt index 7db1faef..1751b24c 100644 --- a/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrOne.kt +++ b/src/main/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/ZeroOrOne.kt @@ -39,4 +39,19 @@ class ZeroOrOne(val combinator : Combinator) : Combinator { override fun SemanticMatch(value: String, offset: Int): MatchResult { return match(value, offset, combinator::SemanticMatch) } + + override fun toString(): String = toStringIndented(0) + + override fun toStringIndented(indent: Int): String { + val prefix = " ".repeat(indent) + val sb = StringBuilder() + sb.append(prefix).append("ZeroOrOne(\n") + if (combinator is SequenceCombinator || combinator is AlternativeCombinator || combinator is Repeat || combinator is ZeroOrOne || combinator is ZeroOrMore || combinator is OneOrMore) { + sb.append(combinator.toStringIndented(indent + 1)).append("\n") + } else { + sb.append(" ".repeat(indent + 1)).append(combinator.toString()).append("\n") + } + sb.append(prefix).append(")") + return sb.toString() + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 86d8fa14..063172d3 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -80,6 +80,10 @@ groupPath="Unit files (systemd)" language="Unit File (systemd)" shortName="MissingRequiredKey" displayName="Missing required key" groupName="Validity" enabledByDefault="true" level="ERROR"/> + diff --git a/src/main/resources/inspectionDescriptions/IPAddressAllowOnly.html b/src/main/resources/inspectionDescriptions/IPAddressAllowOnly.html new file mode 100644 index 00000000..9a7dcc17 --- /dev/null +++ b/src/main/resources/inspectionDescriptions/IPAddressAllowOnly.html @@ -0,0 +1,22 @@ + + + +

This inspection warns when IPAddressAllow= is specified without IPAddressDeny= in Slice, Scope, Service, or Socket sections.

+

When IPAddressAllow= is used without IPAddressDeny=, it does not block traffic as the default action is to permit.

+

To properly restrict traffic, both IPAddressAllow= and IPAddressDeny= should be specified.

+ +

Note: This warning may be safely ignored in cases where:

+
    +
  • The unit is part of a slice that already has IPAddressDeny= configured at a higher level
  • +
  • The unit inherits IP access restrictions from a parent slice unit (such as -.slice or system.slice)
  • +
  • You are implementing a hierarchical firewall structure where IPAddressDeny=any is set on an upper-level slice unit, and individual services only need to specify IPAddressAllow= entries
  • +
+ +

systemd applies IP access lists hierarchically, combining lists from parent slice units with those defined in the current unit. The access control rules are applied in this order:

+
    +
  1. Access is granted when the IP address matches an entry in IPAddressAllow=
  2. +
  3. Otherwise, access is denied when the IP address matches an entry in IPAddressDeny=
  4. +
  5. Otherwise, access is granted
  6. +
+ + diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt new file mode 100644 index 00000000..79ecaa94 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/IPAddressAllowOnlyInspection.kt @@ -0,0 +1,56 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class IPAddressAllowOnlyInspectionTest: AbstractUnitFileTest() { + + fun testServiceThrowsWarningWhenIPAddressAllowIsUsedByNoIPAddressDeny() { + + // Fixture Setup + // language="unit file (systemd)" + val file = """ + # SPDX-License-Identifier: LGPL-2.1-or-later + [Unit] + Description=test + + [Service] + IPAddressAllow=192.168.0.0/24 + + """.trimIndent() + + // Exercise SUT + setupFileInEditor("file.service", file) + enableInspection(IPAddressAllowOnlyInspection::class.java) + + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(1, highlights) + } + + fun testServiceThrowsNoWarningWhenIPAddressAllowIsUsedByIPAddressDeny() { + + // Fixture Setup + // language="unit file (systemd)" + val file = """ + # SPDX-License-Identifier: LGPL-2.1-or-later + [Unit] + Description=test + + [Service] + IPAddressAllow=192.168.0.0/24 + IPAddressDeny=any + + """.trimIndent() + + // Exercise SUT + setupFileInEditor("file.service", file) + enableInspection(IPAddressAllowOnlyInspection::class.java) + + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InAddrPrefixOptionalValueTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InAddrPrefixOptionalValueTest.kt new file mode 100644 index 00000000..922efb87 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InAddrPrefixOptionalValueTest.kt @@ -0,0 +1,121 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class InAddrPrefixOptionalValueTest : AbstractUnitFileTest() { + + fun testNoWarningWhenValidIPAddressesInServiceSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Service] + IPAddressAllow=244.178.44.111/32 + IPAddressAllow=any + IPAddressAllow=localhost + IPAddressAllow=link-local + IPAddressAllow=multicast + IPAddressAllow=127.0.0.1 + IPAddressAllow=1.2.3.4 5.6.7.8/23 + IPAddressAllow=::FFFF/64 + IPAddressAllow=::/64 + IPAddressAllow=::1/128 + IPAddressAllow=2001:db8::1/128 2001:db8:ABCD:0012::0/96 2001:0db8:85a3:0000:0000:8a2e:0370:7334 multicast + IPAddressAllow=fe80::/64 + IPAddressAllow=::/128 + IPAddressAllow=::ffff:192.0.2.128/96 + IPAddressAllow=2001:0db8:85a3::8a2e:0370:7334/124 + IPAddressDeny=any + IPAddressDeny=localhost + IPAddressDeny=link-local + IPAddressDeny=multicast + IPAddressDeny=64.2.3.4 1.2.3.5 6.9.0.1 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWarningsWhenInvalidIPAddressesInServiceSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Service] + # Not a valid option + IPAddressAllow=all + # First octet is too high + IPAddressAllow=256.178.44.111 + # Too few octets + IPAddressAllow=245.124.2/12 + # Too many octets + IPAddressDeny=1.2.3.4.5/8 + # Invalid Prefix Length x 2 but valid IPs in between + IPAddressAllow=244.25.2.1/33 4.2.3.5 244.25.2.1/7 any + IPAddressDeny=2001:0db8:85a3:0000:0000:8a2e:0370:7334:1234/64 + # Too few hextets (only 2) + # Invalid character (g is not a hex digit) + IPAddressDeny=abcd:1234/64 2001:db8:85a3:0:0:8a2e:370g:7334/64 2001:db8::85a3::7334/64 + # Prefix too large (>128) + IPAddressDeny=2001:db8::1/129 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.service", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(8, highlights) + } + + fun testNoWarningWhenValidIPv4AndIPv6AddressesSetInNetwork() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [DHCPv4] + AllowList=1.2.3.4/32 + AllowList=any + + [IPv6AcceptRA] + RouterAllowList=2001:0db8:85a3::8a2e:0370:7334/124 + RouterAllowList=link-local + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWarningWhenValidIPv4AndIPv6AddressesSetInNetwork() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [DHCPv4] + AllowList=2001:0db8:85a3::8a2e:0370:7334/124 + AllowList=any + + [IPv6AcceptRA] + RouterAllowList=1.2.3.4 + RouterAllowList=link-local + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(2, highlights) + } + + + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt new file mode 100644 index 00000000..0f3656d0 --- /dev/null +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/inspections/InvalidValueForNetworkAddressesTest.kt @@ -0,0 +1,138 @@ +package net.sjrx.intellij.plugins.systemdunitfiles.inspections + +import net.sjrx.intellij.plugins.systemdunitfiles.AbstractUnitFileTest + +class InvalidValueForNetworkAddressesTest : AbstractUnitFileTest() { + + fun testNoWarningWhenValidIPv4NetworkAddressSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Network] + Address=244.178.44.111/32 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWarningWhenSomeInvalidIPv4NetworkAddressSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Network] + # The first octet is two high + Address=256.178.44.111 + # Too few octets + Address=245.124.2/12 + # Too many octets + Address=1.2.3.4.5/8 + # Invalid Prefix Length + Address=244.25.2.1/33 + # Invalid Prefix Length + Address=244.25.2.1/7 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(5, highlights) + + } + + fun testNoWarningWhenValidIPv6NetworkAddressSet() { + // Fixture Setup + + // language="unit file (systemd)" + val file=""" + [Network] + Address=::ffff/64 + Address=::FFFF/64 + Address=::/64 + Address=::1/128 + Address=2001:db8::1/128 + Address=2001:db8:0:0:0:0:2:1/127 + Address=2001:db8:ABCD:0012::0/96 + Address=fe80::/64 + Address=::/128 + Address=::ffff:192.0.2.128/96 + Address=2001:0db8:85a3:0000:0000:8a2e:0370:7334/124 + Address=2001:0db8:85a3::8a2e:0370:7334/124 + Address=fd00:1234:5678:9abc:def0:1234:5678:9abc/100 + Address=2001:db8:0:0:0:0:0:1/126 + Address=2001:db8:85a3::8a2e:370:7334/120 + Address=::ABCD/112 + Address=::1/127 + Address=2001:db8::/65 + Address=ff02::1/128 + # Honestly I don't know what matches this + Address=2001:0db8:85a3:0000:0000:8a2e:192.168.0.1/96 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(0, highlights) + } + + fun testWarningWhenSomeInvalidIPv6NetworkAddressSet() { + // Fixture Setup + // language="unit file (systemd)" + val file=""" + [Network] + # Too many hextets (9 instead of max 8) + Address=2001:0db8:85a3:0000:0000:8a2e:0370:7334:1234/64 + # Too few hextets (only 2) + Address=abcd:1234/64 + # Invalid character (g is not a hex digit) + Address=2001:db8:85a3:0:0:8a2e:370g:7334/64 + # Double '::' not allowed + Address=2001:db8::85a3::7334/64 + # Prefix too large (>128) + Address=2001:db8::1/129 + # Prefix too small (<0) + Address=2001:db8::1/-1 + # Missing prefix + Address=2001:db8::1 + # Empty address + Address=/64 + # Trailing colon + Address=2001:db8:85a3:0:0:8a2e:370:7334:/64 + # Leading colon (not part of '::') + Address=:2001:db8:85a3:0:0:8a2e:370:7334/64 + # Too many consecutive colons (illegal ':::') + Address=2001:db8:::1/64 + # Embedded IPv4 with too many octets + Address=::ffff:192.168.1.1.1/96 + # Embedded IPv4 with invalid octet (>255) + Address=::ffff:300.168.1.1/96 + # Hextet too long (more than 4 hex digits) + Address=2001:db8:12345::1/64 + # Non-hex character in hextet + Address=2001:db8:zzzz::1/64 + # Invalid number of octets (missing one with no zero compression) + Address=2001:0db8:85a3:0000:0000:192.168.0.1/96 + """.trimIndent() + + // Execute SUT + setupFileInEditor("file.network", file) + enableInspection(InvalidValueInspection::class.java) + val highlights = myFixture.doHighlighting() + + // Verification + assertSize(16, highlights) + + } + +} diff --git a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt index 15f9eb70..84ecab43 100644 --- a/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt +++ b/src/test/kotlin/net/sjrx/intellij/plugins/systemdunitfiles/semanticdata/optionvalues/grammar/Grammar.kt @@ -827,4 +827,168 @@ class GrammarTest : TestCase() { assertEquals(NoMatch, optionalWhitespacePrefix.SyntacticMatch(invalidFromOffset, garbage.length)) } + fun testRepeatCombinatorMatchesNonZeroMin() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + val fizzOrBuzz = RegexTerminal("[a-z]{4}", "fizz|buzz") + + val repeatCombinator = Repeat(fizzOrBuzz, 2, 4) + + val semValid = "fizzbuzzfizz" + val synValid = "blehblehbleh" + //val invalid = "fizz" + val tooShort = "fizz" + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val synValidFromOffset = "${garbage}${synValid}" + val tooShortFromOffset = "${garbage}${tooShort}" + + /** + * Execute SUT & Verification + */ + var match = repeatCombinator.SyntacticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(semValid, 0) + assertEquals(semValid.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SyntacticMatch(synValid, 0) + assertEquals(synValid.length, match.matchResult) + assertEquals(listOf("bleh", "bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, repeatCombinator.SemanticMatch(synValid, 0)) + + match = repeatCombinator.SyntacticMatch(tooShort, 0) + assertEquals(-1, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(4, match.longestMatch) + + match = repeatCombinator.SemanticMatch(tooShort, 0) + assertEquals(-1, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(4, match.longestMatch) + + match = repeatCombinator.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(semValidFromOffset.length, match.matchResult) + assertEquals(listOf("fizz", "buzz", "fizz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SyntacticMatch(synValidFromOffset, garbage.length) + assertEquals(synValidFromOffset.length, match.matchResult) + assertEquals(listOf("bleh", "bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + assertEquals(NoMatch, repeatCombinator.SemanticMatch(synValidFromOffset, garbage.length)) + + match = repeatCombinator.SyntacticMatch(tooShortFromOffset, garbage.length) + assertEquals(-1, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(6, match.longestMatch) + + match = repeatCombinator.SemanticMatch(tooShortFromOffset, garbage.length) + assertEquals(-1, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(6, match.longestMatch) + + } + + fun testRepeatCombinatorMatchesZeroMinAndExtraAtEnd() { + /** + * Fixture Setup + */ + + // This combinator should match the options passed in. + val fizzOrBuzz = RegexTerminal("[a-z]{4}", "fizz|buzz") + + val repeatCombinator = Repeat(fizzOrBuzz, 0, 2) + + val semValid = "fizzbuzzfizz" + val synValid = "blehblehbleh" + //val invalid = "fizz" + val emptyString = "" + val garbage = "XX" + + val semValidFromOffset = "${garbage}${semValid}" + val synValidFromOffset = "${garbage}${synValid}" + val emptyStringFromOffset = "${garbage}${emptyString}" + + /** + * Execute SUT & Verification + */ + var match = repeatCombinator.SyntacticMatch(semValid, 0) + assertEquals(8, match.matchResult) + assertEquals(listOf("fizz", "buzz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(semValid, 0) + assertEquals(8, match.matchResult) + assertEquals(listOf("fizz", "buzz" ), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SyntacticMatch(synValid, 0) + assertEquals(8, match.matchResult) + assertEquals(listOf("bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(synValid, 0) + assertEquals(0, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SyntacticMatch(emptyString, 0) + assertEquals(0, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SemanticMatch(emptyString, 0) + assertEquals(0, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SyntacticMatch(semValidFromOffset, garbage.length) + assertEquals(10, match.matchResult) + assertEquals(listOf("fizz", "buzz" ), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(semValidFromOffset, garbage.length) + assertEquals(10, match.matchResult) + assertEquals(listOf("fizz", "buzz"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SyntacticMatch(synValidFromOffset, garbage.length) + assertEquals(10, match.matchResult) + assertEquals(listOf("bleh", "bleh"), match.tokens) + assertEquals(listOf("RegexTerminal", "RegexTerminal"), TerminalTypes(match.terminals)) + + match = repeatCombinator.SemanticMatch(synValidFromOffset, garbage.length) + assertEquals(2, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SyntacticMatch(emptyStringFromOffset, garbage.length) + assertEquals(2, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + match = repeatCombinator.SemanticMatch(emptyStringFromOffset, garbage.length) + assertEquals(2, match.matchResult) + assertEquals(emptyList(), match.tokens) + assertEquals(0, match.longestMatch) + + } + }