Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial version of INI file parser #1858

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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-configfiles @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 enableConfigfilesFrontend: Boolean by extra {
val enableConfigfilesFrontend: String? by project
enableConfigfilesFrontend.toBoolean()
}
project.logger.lifecycle("Configfiles frontend is ${if (enableConfigfilesFrontend) "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 enableConfigfilesFrontend: 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 (enableConfigfilesFrontend) {
api(project(":cpg-language-configfiles"))
kover(project(":cpg-language-configfiles"))
}
}
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
answerConfigfiles=$(ask "Do you want to enable the Configfiles frontend? (currently $(getProperty "enableConfigfilesFrontend"))")
setProperty "enableConfigfilesFrontend" $answerConfigfiles
48 changes: 48 additions & 0 deletions cpg-language-configfiles/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-configfiles") {
pom {
artifactId = "cpg-language-configfiles"
name.set("Code Property Graph - Configfiles Frontend")
description.set("A configuration file (ini/yaml/toml/...) 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,128 @@
/*
* 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 java.io.File
import java.io.FileInputStream
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 [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] TODO what's this?
*/
class IniFilesFrontend(language: Language<IniFilesFrontend>, ctx: TranslationContext) :
LanguageFrontend<Any, Any?>(language, ctx) {

override fun parse(file: File): TranslationUnitDeclaration {
val ini = Ini()
try {
ini.load(FileInputStream(file))
} catch (ex: Exception) {
throw TranslationException("Parsing failed with exception: $ex")
}
val tud = newTranslationUnitDeclaration(name = file.name, rawNode = ini)
scopeManager.resetToGlobal(tud)
val nsd = newNamespaceDeclaration(name = file.name, 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()
}

override fun locationOf(astNode: Any): PhysicalLocation? {
return null // currently, the line number / column cannot be accessed given an Ini object
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return entire file first line number to last line number

}

override fun setComment(node: Node, astNode: Any) {
TODO("Not yet implemented")
}
}
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`
* - 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 IniFilesLanguage : Language<IniFilesFrontend>() {
override val fileExtensions = listOf("ini")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

".conf" extension, too

override val namespaceDelimiter: String = "" // no such thing
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

path separator .


@Transient override val frontend: KClass<out IniFilesFrontend> = IniFilesFrontend::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,82 @@
/*
* 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.assertLiteralValue
import java.nio.file.Path
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertIs

class IniFilesTest : BaseTest() {

@Test
fun gettingStartedWithINIConfigfiles() {
val topLevel = Path.of("src", "test", "resources")
val tu =
analyzeAndGetFirstTU(listOf(topLevel.resolve("config.ini").toFile()), topLevel, true) {
it.registerLanguage<IniFilesLanguage>()
}
assertIs<TranslationUnitDeclaration>(tu)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add namespace test

assertEquals(2, tu.records.size, "Expected two records")

val ownerRecord = tu.records["owner"]
assertIs<RecordDeclaration>(ownerRecord)
assertEquals(2, ownerRecord.fields.size, "Expected two fields")

val nameField = ownerRecord.fields["name"]
assertIs<FieldDeclaration>(nameField)
assertLiteralValue("John Doe", nameField.initializer)

val organizationField = ownerRecord.fields["organization"]
assertIs<FieldDeclaration>(organizationField)
assertLiteralValue("Acme Widgets Inc.", organizationField.initializer)

val databaseRecord = tu.records["database"]
assertIs<RecordDeclaration>(databaseRecord)
assertEquals(3, databaseRecord.fields.size, "Expected three fields")

val serverField = databaseRecord.fields["server"]
assertIs<FieldDeclaration>(serverField)
assertLiteralValue("192.0.2.62", serverField.initializer)

val portField = databaseRecord.fields["port"]
assertIs<FieldDeclaration>(portField)
assertLiteralValue("143", portField.initializer)

val fileField = databaseRecord.fields["file"]
assertIs<FieldDeclaration>(fileField)
assertLiteralValue("\"payroll.dat\"", fileField.initializer)
}
}
12 changes: 12 additions & 0 deletions cpg-language-configfiles/src/test/resources/config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
; Wikipedia example https://en.wikipedia.org/wiki/INI_file

; last modified 1 April 2001 by John Doe
[owner]
name = John Doe
organization = Acme Widgets Inc.

[database]
; use IP address in case network name resolution is not working
server = 192.0.2.62
port = 143
file = "payroll.dat"
1 change: 1 addition & 0 deletions gradle.properties.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ enableLLVMFrontend=true
enableTypeScriptFrontend=true
enableRubyFrontend=true
enableJVMFrontend=true
enableConfigfilesFrontend=true
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ llvm = { module = "org.bytedeco:llvm-platform", version = "16.0.4-1.5.9"}
jruby = { module = "org.jruby:jruby-core", version = "9.4.3.0" }
jline = { module = "org.jline:jline", version = "3.27.0" }
antlr-runtime = { module = "org.antlr:antlr4-runtime", version = "4.8-1" } # we cannot upgrade until ki-shell upgrades this!
ini4j = { module = "org.ini4j:ini4j", version = "0.5.4" }

# test
junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version = "5.11.0"}
Expand Down
5 changes: 5 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ val enableJVMFrontend: Boolean by extra {
val enableJVMFrontend: String? by settings
enableJVMFrontend.toBoolean()
}
val enableConfigfilesFrontend: Boolean by extra {
val enableConfigfilesFrontend: String? by settings
enableConfigfilesFrontend.toBoolean()
}

if (enableJavaFrontend) include(":cpg-language-java")
if (enableCXXFrontend) include(":cpg-language-cxx")
Expand All @@ -50,3 +54,4 @@ if (enablePythonFrontend) include(":cpg-language-python")
if (enableTypeScriptFrontend) include(":cpg-language-typescript")
if (enableRubyFrontend) include(":cpg-language-ruby")
if (enableJVMFrontend) include(":cpg-language-jvm")
if (enableConfigfilesFrontend) include(":cpg-language-configfiles")
Loading