Skip to content

Commit

Permalink
INI file frontend (#1858)
Browse files Browse the repository at this point in the history
This PR implements support for INI files by providing a new `Language` and `LanguageFrontend`.
  • Loading branch information
maximiliankaul authored Nov 29, 2024
1 parent 5155fff commit 35e1704
Show file tree
Hide file tree
Showing 12 changed files with 385 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ cpg-neo4j @peckto

build.gradle.kts @oxisto
.github @oxisto

cpg-language-ini @maximiliankaul
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,9 @@ val enableJVMFrontend: Boolean by extra {
enableJVMFrontend.toBoolean()
}
project.logger.lifecycle("JVM frontend is ${if (enableJVMFrontend) "enabled" else "disabled"}")

val enableINIFrontend: Boolean by extra {
val enableINIFrontend: String? by project
enableINIFrontend.toBoolean()
}
project.logger.lifecycle("INI frontend is ${if (enableINIFrontend) "enabled" else "disabled"}")
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ val enableLLVMFrontend: Boolean by rootProject.extra
val enableTypeScriptFrontend: Boolean by rootProject.extra
val enableRubyFrontend: Boolean by rootProject.extra
val enableJVMFrontend: Boolean by rootProject.extra
val enableINIFrontend: Boolean by rootProject.extra

dependencies {
if (enableJavaFrontend) {
Expand Down Expand Up @@ -46,4 +47,8 @@ dependencies {
api(project(":cpg-language-ruby"))
kover(project(":cpg-language-ruby"))
}
if (enableINIFrontend) {
api(project(":cpg-language-ini"))
kover(project(":cpg-language-ini"))
}
}
2 changes: 2 additions & 0 deletions configure_frontends.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ answerRuby=$(ask "Do you want to enable the Ruby frontend? (currently $(getPrope
setProperty "enableRubyFrontend" $answerRuby
answerJVM=$(ask "Do you want to enable the JVM frontend? (currently $(getProperty "enableJVMFrontend"))")
setProperty "enableJVMFrontend" $answerJVM
answerINI=$(ask "Do you want to enable the INI frontend? (currently $(getProperty "enableINIFrontend"))")
setProperty "enableINIFrontend" $answerINI
48 changes: 48 additions & 0 deletions cpg-language-ini/build.gradle.kts
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"))
}
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]
}
}
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
}
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)
}
}
Loading

0 comments on commit 35e1704

Please sign in to comment.