-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This PR implements support for INI files by providing a new `Language` and `LanguageFrontend`.
- Loading branch information
1 parent
5155fff
commit 35e1704
Showing
12 changed files
with
385 additions
and
0 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,3 +28,5 @@ cpg-neo4j @peckto | |
|
||
build.gradle.kts @oxisto | ||
.github @oxisto | ||
|
||
cpg-language-ini @maximiliankaul |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* Copyright (c) 2021, Fraunhofer AISEC. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* $$$$$$\ $$$$$$$\ $$$$$$\ | ||
* $$ __$$\ $$ __$$\ $$ __$$\ | ||
* $$ / \__|$$ | $$ |$$ / \__| | ||
* $$ | $$$$$$$ |$$ |$$$$\ | ||
* $$ | $$ ____/ $$ |\_$$ | | ||
* $$ | $$\ $$ | $$ | $$ | | ||
* \$$$$$ |$$ | \$$$$$ | | ||
* \______/ \__| \______/ | ||
* | ||
*/ | ||
plugins { | ||
id("cpg.frontend-conventions") | ||
} | ||
|
||
publishing { | ||
publications { | ||
named<MavenPublication>("cpg-language-ini") { | ||
pom { | ||
artifactId = "cpg-language-ini" | ||
name.set("Code Property Graph - INI Frontend") | ||
description.set("An INI configuration file frontend for the CPG") | ||
} | ||
} | ||
} | ||
} | ||
|
||
dependencies { | ||
// ini4j for parsing ini files | ||
implementation(libs.ini4j) | ||
|
||
// to evaluate some test cases | ||
testImplementation(project(":cpg-analysis")) | ||
} |
163 changes: 163 additions & 0 deletions
163
...guage-ini/src/main/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileFrontend.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
/* | ||
* Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* $$$$$$\ $$$$$$$\ $$$$$$\ | ||
* $$ __$$\ $$ __$$\ $$ __$$\ | ||
* $$ / \__|$$ | $$ |$$ / \__| | ||
* $$ | $$$$$$$ |$$ |$$$$\ | ||
* $$ | $$ ____/ $$ |\_$$ | | ||
* $$ | $$\ $$ | $$ | $$ | | ||
* \$$$$$ |$$ | \$$$$$ | | ||
* \______/ \__| \______/ | ||
* | ||
*/ | ||
package de.fraunhofer.aisec.cpg.frontend.configfiles | ||
|
||
import de.fraunhofer.aisec.cpg.TranslationContext | ||
import de.fraunhofer.aisec.cpg.frontends.Language | ||
import de.fraunhofer.aisec.cpg.frontends.LanguageFrontend | ||
import de.fraunhofer.aisec.cpg.frontends.TranslationException | ||
import de.fraunhofer.aisec.cpg.graph.* | ||
import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration | ||
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration | ||
import de.fraunhofer.aisec.cpg.graph.types.Type | ||
import de.fraunhofer.aisec.cpg.sarif.PhysicalLocation | ||
import de.fraunhofer.aisec.cpg.sarif.Region | ||
import java.io.File | ||
import java.io.FileInputStream | ||
import java.net.URI | ||
import org.ini4j.Ini | ||
import org.ini4j.Profile | ||
|
||
/** | ||
* The INI file frontend. This frontend utilizes the [ini4j library](https://ini4j.sourceforge.net/) | ||
* to parse the config file. The result consists of | ||
* - a [TranslationUnitDeclaration] wrapping the entire result | ||
* - a [de.fraunhofer.aisec.cpg.graph.declarations.NamespaceDeclaration] wrapping the INI file and | ||
* thus preventing collisions with other symbols which might have the same name | ||
* - a [RecordDeclaration] per `Section` (a section refers to a block of INI values marked with a | ||
* line `[SectionName]`) | ||
* - a [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration] per entry in a section. The | ||
* [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration.name] matches the `entry`s `name` | ||
* field and the [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration.initializer] is set | ||
* to a [statements.expressions.Literal] with the corresponding `entry`s `value`. | ||
* | ||
* Note: | ||
* - the "ini4j" library does not provide any super type for all nodes. Thus, the frontend accepts | ||
* `Any` | ||
* - [typeOf] has to be implemented, but as there are no types always returns the builtin `string` | ||
* type | ||
* - [codeOf] has to accept `Any` (because of the limitations stated above) and simply returns | ||
* `.toString()` | ||
* - [locationOf] always returns `null` as the "ini4j" library does not provide any means of getting | ||
* a location given a node | ||
* - [setComment] not implemented as this is not used (no | ||
* [de.fraunhofer.aisec.cpg.frontends.Handler] pattern implemented) | ||
* - Comments in general are not supported. | ||
*/ | ||
class IniFileFrontend(language: Language<IniFileFrontend>, ctx: TranslationContext) : | ||
LanguageFrontend<Any, Any?>(language, ctx) { | ||
|
||
private lateinit var uri: URI | ||
private lateinit var region: Region | ||
|
||
override fun parse(file: File): TranslationUnitDeclaration { | ||
uri = file.toURI() | ||
region = Region() | ||
|
||
val ini = Ini() | ||
try { | ||
ini.load(FileInputStream(file)) | ||
} catch (ex: Exception) { | ||
throw TranslationException("Parsing failed with exception: $ex") | ||
} | ||
|
||
/* | ||
* build a namespace name relative to the configured | ||
* [de.fraunhofer.aisec.cpg.TranslationConfiguration.topLevel] using | ||
* [Language.namespaceDelimiter] as a separator | ||
*/ | ||
val topLevel = config.topLevel?.let { file.relativeToOrNull(it) } ?: file | ||
val parentDir = topLevel.parent | ||
|
||
val namespace = | ||
if (parentDir != null) { | ||
val pathSegments = parentDir.toString().split(File.separator) | ||
(pathSegments + file.nameWithoutExtension).joinToString(language.namespaceDelimiter) | ||
} else { | ||
file.nameWithoutExtension | ||
} | ||
|
||
val tud = newTranslationUnitDeclaration(name = file.name, rawNode = ini) | ||
scopeManager.resetToGlobal(tud) | ||
val nsd = newNamespaceDeclaration(name = namespace, rawNode = ini) | ||
scopeManager.addDeclaration(nsd) | ||
scopeManager.enterScope(nsd) | ||
|
||
ini.values.forEach { handleSection(it) } | ||
|
||
scopeManager.enterScope(nsd) | ||
return tud | ||
} | ||
|
||
/** | ||
* Translates a `Section` into a [RecordDeclaration] and handles all `entries` using | ||
* [handleEntry]. | ||
*/ | ||
private fun handleSection(section: Profile.Section) { | ||
val record = newRecordDeclaration(name = section.name, kind = "section", rawNode = section) | ||
scopeManager.addDeclaration(record) | ||
scopeManager.enterScope(record) | ||
section.entries.forEach { handleEntry(it) } | ||
scopeManager.leaveScope(record) | ||
} | ||
|
||
/** | ||
* Translates an `MutableEntry` to a new | ||
* [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration] with the | ||
* [de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration.initializer] being set to the | ||
* `entry`s value. | ||
*/ | ||
private fun handleEntry(entry: MutableMap.MutableEntry<String?, String?>) { | ||
val field = | ||
newFieldDeclaration(name = entry.key, type = primitiveType("string"), rawNode = entry) | ||
.apply { initializer = newLiteral(value = entry.value, rawNode = entry) } | ||
scopeManager.addDeclaration(field) | ||
} | ||
|
||
override fun typeOf(type: Any?): Type { | ||
return primitiveType("string") | ||
} | ||
|
||
override fun codeOf(astNode: Any): String? { | ||
return astNode.toString() | ||
} | ||
|
||
/** | ||
* Return the entire file as the location of any node. The parsing library in use does not | ||
* provide more fine granular access to a node's location. | ||
*/ | ||
override fun locationOf(astNode: Any): PhysicalLocation? { | ||
return PhysicalLocation( | ||
uri, | ||
region | ||
) // currently, the line number / column cannot be accessed given an Ini object -> we only | ||
// provide a precise uri | ||
} | ||
|
||
override fun setComment(node: Node, astNode: Any) { | ||
return // not used as this function does not implement [Handler] | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
...guage-ini/src/main/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileLanguage.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* $$$$$$\ $$$$$$$\ $$$$$$\ | ||
* $$ __$$\ $$ __$$\ $$ __$$\ | ||
* $$ / \__|$$ | $$ |$$ / \__| | ||
* $$ | $$$$$$$ |$$ |$$$$\ | ||
* $$ | $$ ____/ $$ |\_$$ | | ||
* $$ | $$\ $$ | $$ | $$ | | ||
* \$$$$$ |$$ | \$$$$$ | | ||
* \______/ \__| \______/ | ||
* | ||
*/ | ||
package de.fraunhofer.aisec.cpg.frontend.configfiles | ||
|
||
import de.fraunhofer.aisec.cpg.frontends.Language | ||
import de.fraunhofer.aisec.cpg.graph.types.StringType | ||
import de.fraunhofer.aisec.cpg.graph.types.Type | ||
import kotlin.reflect.KClass | ||
|
||
/** | ||
* A simple language representing classical [INI files](https://en.wikipedia.org/wiki/INI_file). As | ||
* there are conflicting definitions of an INI file, we go with: | ||
* - the file extension is `.ini` or `.conf` | ||
* - all entries live in a unique `section` | ||
* - all `key`s are unique per section | ||
* - the file is accepted by the [ini4j library](https://ini4j.sourceforge.net/) | ||
*/ | ||
class IniFileLanguage : Language<IniFileFrontend>() { | ||
override val fileExtensions = listOf("ini", "conf") | ||
override val namespaceDelimiter: String = "." // no such thing | ||
|
||
@Transient override val frontend: KClass<out IniFileFrontend> = IniFileFrontend::class | ||
override val builtInTypes: Map<String, Type> = | ||
mapOf("string" to StringType("string", language = this)) // everything is a string | ||
|
||
override val compoundAssignmentOperators: Set<String> = emptySet() // no such thing | ||
} |
92 changes: 92 additions & 0 deletions
92
cpg-language-ini/src/test/kotlin/de/fraunhofer/aisec/cpg/frontend/configfiles/IniFileTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
* Copyright (c) 2024, Fraunhofer AISEC. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
* | ||
* $$$$$$\ $$$$$$$\ $$$$$$\ | ||
* $$ __$$\ $$ __$$\ $$ __$$\ | ||
* $$ / \__|$$ | $$ |$$ / \__| | ||
* $$ | $$$$$$$ |$$ |$$$$\ | ||
* $$ | $$ ____/ $$ |\_$$ | | ||
* $$ | $$\ $$ | $$ | $$ | | ||
* \$$$$$ |$$ | \$$$$$ | | ||
* \______/ \__| \______/ | ||
* | ||
*/ | ||
package de.fraunhofer.aisec.cpg.frontend.configfiles | ||
|
||
import de.fraunhofer.aisec.cpg.graph.declarations.FieldDeclaration | ||
import de.fraunhofer.aisec.cpg.graph.declarations.RecordDeclaration | ||
import de.fraunhofer.aisec.cpg.graph.declarations.TranslationUnitDeclaration | ||
import de.fraunhofer.aisec.cpg.graph.get | ||
import de.fraunhofer.aisec.cpg.graph.records | ||
import de.fraunhofer.aisec.cpg.test.BaseTest | ||
import de.fraunhofer.aisec.cpg.test.analyzeAndGetFirstTU | ||
import de.fraunhofer.aisec.cpg.test.assertFullName | ||
import de.fraunhofer.aisec.cpg.test.assertLiteralValue | ||
import java.nio.file.Path | ||
import kotlin.test.Test | ||
import kotlin.test.assertEquals | ||
import kotlin.test.assertIs | ||
import kotlin.test.assertNotNull | ||
|
||
class IniFileTest : BaseTest() { | ||
|
||
@Test | ||
fun testSimpleINIFile() { | ||
val topLevel = Path.of("src", "test", "resources") | ||
val tu = | ||
analyzeAndGetFirstTU(listOf(topLevel.resolve("config.ini").toFile()), topLevel, true) { | ||
it.registerLanguage<IniFileLanguage>() | ||
} | ||
assertIs<TranslationUnitDeclaration>(tu) | ||
|
||
val namespace = tu.namespaces.firstOrNull() | ||
assertNotNull(namespace) | ||
assertFullName( | ||
"config", | ||
namespace, | ||
"Namespace name mismatch." | ||
) // analyzeAndGetFirstTU does not provide the full path | ||
|
||
assertEquals(2, tu.records.size, "Expected two records") | ||
|
||
val sectionA = tu.records["SectionA"] | ||
assertIs<RecordDeclaration>(sectionA) | ||
assertEquals(2, sectionA.fields.size, "Expected two fields") | ||
|
||
val sectionAEntry1 = sectionA.fields["key1"] | ||
assertIs<FieldDeclaration>(sectionAEntry1) | ||
assertLiteralValue("value1", sectionAEntry1.initializer) | ||
|
||
val sectionAEntry2 = sectionA.fields["key2"] | ||
assertIs<FieldDeclaration>(sectionAEntry2) | ||
assertLiteralValue("value2", sectionAEntry2.initializer) | ||
|
||
val sectionB = tu.records["SectionB"] | ||
assertIs<RecordDeclaration>(sectionB) | ||
assertEquals(3, sectionB.fields.size, "Expected three fields") | ||
|
||
val sectionBEntry1 = sectionB.fields["key1"] | ||
assertIs<FieldDeclaration>(sectionBEntry1) | ||
assertLiteralValue("123", sectionBEntry1.initializer) | ||
|
||
val sectionBEntry2 = sectionB.fields["key2"] | ||
assertIs<FieldDeclaration>(sectionBEntry2) | ||
assertLiteralValue("1.2.3.4", sectionBEntry2.initializer) | ||
|
||
val sectionBEntry3 = sectionB.fields["key3"] | ||
assertIs<FieldDeclaration>(sectionBEntry3) | ||
assertLiteralValue("\"abc\"", sectionBEntry3.initializer) | ||
} | ||
} |
Oops, something went wrong.