diff --git a/antlr-bridge/build.gradle.kts b/antlr-bridge/build.gradle.kts new file mode 100644 index 0000000..bc36199 --- /dev/null +++ b/antlr-bridge/build.gradle.kts @@ -0,0 +1,9 @@ +dependencies { + implementation(project(":core")) + implementation(libs.antlr) + implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.core.jvm) + testImplementation(libs.kotlin.test) + testImplementation(libs.kotlin.test.junit5) +} \ No newline at end of file diff --git a/antlr-bridge/src/main/kotlin/tw/xcc/gumtree/antlrBridge/FileHelper.kt b/antlr-bridge/src/main/kotlin/tw/xcc/gumtree/antlrBridge/FileHelper.kt new file mode 100644 index 0000000..4f696c6 --- /dev/null +++ b/antlr-bridge/src/main/kotlin/tw/xcc/gumtree/antlrBridge/FileHelper.kt @@ -0,0 +1,31 @@ +package tw.xcc.gumtree.antlrBridge + +import java.io.File + +private fun File.isFileSafeToRead(): Boolean = this.exists() && this.isFile && this.canRead() + +private fun File.isNotSymbolicLink(): Boolean = !this.toPath().toRealPath().equals(this.canonicalFile.toPath()) + +private fun File.isPathSecure(allowedDirectory: String): Boolean { + val canonicalFilePath = this.canonicalPath + val canonicalAllowedDirectory = File(allowedDirectory).canonicalPath + return canonicalFilePath.startsWith(canonicalAllowedDirectory) +} + +private fun File.isFileSizeAcceptable(maxSizeInBytes: Long): Boolean = this.length() <= maxSizeInBytes + +private fun File.isFileTypeAllowed(allowedExtensions: Set): Boolean { + val fileExtension = this.extension + return fileExtension in allowedExtensions +} + +internal fun File.isValidToRead( + maxSizeInBytes: Long, + allowedDirectory: String, + allowedExtensions: Set +): Boolean = + isFileSafeToRead() && + isPathSecure(allowedDirectory) && + isNotSymbolicLink() && + isFileSizeAcceptable(maxSizeInBytes) && + isFileTypeAllowed(allowedExtensions) \ No newline at end of file diff --git a/antlr-bridge/src/main/kotlin/tw/xcc/gumtree/antlrBridge/GumTreeConverter.kt b/antlr-bridge/src/main/kotlin/tw/xcc/gumtree/antlrBridge/GumTreeConverter.kt new file mode 100644 index 0000000..2959c90 --- /dev/null +++ b/antlr-bridge/src/main/kotlin/tw/xcc/gumtree/antlrBridge/GumTreeConverter.kt @@ -0,0 +1,97 @@ +package tw.xcc.gumtree.antlrBridge + +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.withContext +import org.antlr.v4.runtime.CharStream +import org.antlr.v4.runtime.CharStreams +import org.antlr.v4.runtime.ParserRuleContext +import org.antlr.v4.runtime.Token +import org.antlr.v4.runtime.Vocabulary +import org.antlr.v4.runtime.tree.Tree +import tw.xcc.gumtree.model.GumTree +import tw.xcc.gumtree.model.TreeType +import java.io.File +import java.io.InputStream +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +class GumTreeConverter(private val vocabulary: Vocabulary) { + private fun fileStreamOf(path: String): InputStream { + val file = File(path) + val isValidToRead = + file.isValidToRead( + maxSizeInBytes = DEFAULT_ALLOWED_MAX_FILE_SIZE_IN_BYTE, + allowedDirectory = file.parent, + allowedExtensions = setOf(file.extension) + ) + + if (!isValidToRead) { + throw IllegalArgumentException("File is not valid to read") + } + + return file.inputStream() + } + + private fun createSingleGumTreeNodeFrom(token: Token): GumTree = + GumTree( + GumTree.Info( + type = TreeType(vocabulary.getSymbolicName(token.type) ?: ""), + text = token.toString(), + line = token.line, + posOfLine = token.charPositionInLine + ) + ) + + private suspend fun buildWholeGumTreeFrom(antlrTree: Tree): GumTree? = + coroutineScope { + val self = + when { + antlrTree is Token -> createSingleGumTreeNodeFrom(antlrTree) + else -> null + } ?: return@coroutineScope null + + with(antlrTree) { + val buildChildJobs = mutableListOf>() + for (childIdx in 0 until childCount) { + buildChildJobs.add( + async { buildWholeGumTreeFrom(getChild(childIdx)) } + ) + } + self.setChildrenTo( + buildChildJobs.awaitAll().filterNotNull() + ) + } + + return@coroutineScope self + } + + @OptIn(ExperimentalContracts::class) + suspend fun convertFrom( + filePath: String, + parseTreeCreation: (CharStream) -> ParserRuleContext + ): GumTree? { + contract { + callsInPlace(parseTreeCreation, InvocationKind.AT_MOST_ONCE) + } + + return coroutineScope { + val charStream = + withContext(Dispatchers.IO) { + val inputStream = fileStreamOf(filePath) + CharStreams.fromStream(inputStream) + } + + val firstGrammarEntry = parseTreeCreation(charStream) + buildWholeGumTreeFrom(firstGrammarEntry) + } + } + + companion object { + private const val DEFAULT_ALLOWED_MAX_FILE_SIZE_IN_BYTE = 1L * 1024 * 1024 * 1024 // 1GB + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 3e34093..be33f6c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,4 +16,5 @@ dependencyResolutionManagement { } } -include(":core") \ No newline at end of file +include(":core") +include(":antlr-bridge") \ No newline at end of file