Skip to content

Commit 8b4cf29

Browse files
authored
Merge pull request #10110 from igfoo/igfoo/compression
Kotlin: Add support for TRAP compression
2 parents acff279 + a6cee9e commit 8b4cf29

File tree

3 files changed

+195
-28
lines changed

3 files changed

+195
-28
lines changed

java/kotlin-extractor/src/main/kotlin/KotlinExtractorExtension.kt

Lines changed: 145 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@ import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
55
import org.jetbrains.kotlin.ir.declarations.*
66
import org.jetbrains.kotlin.ir.util.*
77
import org.jetbrains.kotlin.ir.IrElement
8+
import java.io.BufferedReader
9+
import java.io.BufferedWriter
10+
import java.io.BufferedInputStream
11+
import java.io.BufferedOutputStream
812
import java.io.File
13+
import java.io.FileInputStream
914
import java.io.FileOutputStream
15+
import java.io.InputStreamReader
16+
import java.io.OutputStreamWriter
1017
import java.lang.management.*
1118
import java.nio.file.Files
1219
import java.nio.file.Paths
20+
import java.util.zip.GZIPInputStream
21+
import java.util.zip.GZIPOutputStream
1322
import com.semmle.util.files.FileUtil
1423
import kotlin.system.exitProcess
1524

@@ -89,8 +98,29 @@ class KotlinExtractorExtension(
8998
val startTimeMs = System.currentTimeMillis()
9099
// This default should be kept in sync with com.semmle.extractor.java.interceptors.KotlinInterceptor.initializeExtractionContext
91100
val trapDir = File(System.getenv("CODEQL_EXTRACTOR_JAVA_TRAP_DIR").takeUnless { it.isNullOrEmpty() } ?: "kotlin-extractor/trap")
101+
val compression_env_var = "CODEQL_EXTRACTOR_JAVA_OPTION_TRAP_COMPRESSION"
102+
val compression_option = System.getenv(compression_env_var)
103+
val defaultCompression = Compression.GZIP
104+
val (compression, compressionWarning) =
105+
if (compression_option == null) {
106+
Pair(defaultCompression, null)
107+
} else {
108+
try {
109+
@OptIn(kotlin.ExperimentalStdlibApi::class) // Annotation required by kotlin versions < 1.5
110+
val requested_compression = Compression.valueOf(compression_option.uppercase())
111+
if (requested_compression == Compression.BROTLI) {
112+
Pair(Compression.GZIP, "Kotlin extractor doesn't support Brotli compression. Using GZip instead.")
113+
} else {
114+
Pair(requested_compression, null)
115+
}
116+
} catch (e: IllegalArgumentException) {
117+
Pair(defaultCompression,
118+
"Unsupported compression type (\$$compression_env_var) \"$compression_option\". Supported values are ${Compression.values().joinToString()}")
119+
}
120+
}
92121
// The invocation TRAP file will already have been started
93-
// before the plugin is run, so we open it in append mode.
122+
// before the plugin is run, so we always use no compression
123+
// and we open it in append mode.
94124
FileOutputStream(File(invocationTrapFile), true).bufferedWriter().use { invocationTrapFileBW ->
95125
val invocationExtractionProblems = ExtractionProblems()
96126
val lm = TrapLabelManager()
@@ -113,6 +143,10 @@ class KotlinExtractorExtension(
113143
if (System.getenv("CODEQL_EXTRACTOR_JAVA_KOTLIN_DUMP") == "true") {
114144
logger.info("moduleFragment:\n" + moduleFragment.dump())
115145
}
146+
if (compressionWarning != null) {
147+
logger.warn(compressionWarning)
148+
}
149+
116150
val primitiveTypeMapping = PrimitiveTypeMapping(logger, pluginContext)
117151
// FIXME: FileUtil expects a static global logger
118152
// which should be provided by SLF4J's factory facility. For now we set it here.
@@ -125,7 +159,7 @@ class KotlinExtractorExtension(
125159
val fileTrapWriter = tw.makeSourceFileTrapWriter(file, true)
126160
loggerBase.setFileNumber(index)
127161
fileTrapWriter.writeCompilation_compiling_files(compilation, index, fileTrapWriter.fileId)
128-
doFile(fileExtractionProblems, invocationTrapFile, fileTrapWriter, checkTrapIdentical, loggerBase, trapDir, srcDir, file, primitiveTypeMapping, pluginContext, globalExtensionState)
162+
doFile(compression, fileExtractionProblems, invocationTrapFile, fileTrapWriter, checkTrapIdentical, loggerBase, trapDir, srcDir, file, primitiveTypeMapping, pluginContext, globalExtensionState)
129163
fileTrapWriter.writeCompilation_compiling_files_completed(compilation, index, fileExtractionProblems.extractionResult())
130164
}
131165
loggerBase.printLimitedDiagnosticCounts(tw)
@@ -218,12 +252,12 @@ This function determines whether 2 TRAP files should be considered to be
218252
equivalent. It returns `true` iff all of their non-comment lines are
219253
identical.
220254
*/
221-
private fun equivalentTrap(f1: File, f2: File): Boolean {
222-
f1.bufferedReader().use { bw1 ->
223-
f2.bufferedReader().use { bw2 ->
255+
private fun equivalentTrap(r1: BufferedReader, r2: BufferedReader): Boolean {
256+
r1.use { br1 ->
257+
r2.use { br2 ->
224258
while(true) {
225-
val l1 = bw1.readLine()
226-
val l2 = bw2.readLine()
259+
val l1 = br1.readLine()
260+
val l2 = br2.readLine()
227261
if (l1 == null && l2 == null) {
228262
return true
229263
} else if (l1 == null || l2 == null) {
@@ -239,6 +273,7 @@ private fun equivalentTrap(f1: File, f2: File): Boolean {
239273
}
240274

241275
private fun doFile(
276+
compression: Compression,
242277
fileExtractionProblems: FileExtractionProblems,
243278
invocationTrapFile: String,
244279
fileTrapWriter: FileTrapWriter,
@@ -270,15 +305,14 @@ private fun doFile(
270305
}
271306
srcTmpFile.renameTo(dbSrcFilePath.toFile())
272307

273-
val trapFile = File("$dbTrapDir/$srcFilePath.trap")
274-
val trapFileDir = trapFile.parentFile
275-
trapFileDir.mkdirs()
308+
val trapFileName = "$dbTrapDir/$srcFilePath.trap"
309+
val trapFileWriter = getTrapFileWriter(compression, logger, trapFileName)
276310

277-
if (checkTrapIdentical || !trapFile.exists()) {
278-
val trapTmpFile = File.createTempFile("$srcFilePath.", ".trap.tmp", trapFileDir)
311+
if (checkTrapIdentical || !trapFileWriter.exists()) {
312+
trapFileWriter.makeParentDirectory()
279313

280314
try {
281-
trapTmpFile.bufferedWriter().use { trapFileBW ->
315+
trapFileWriter.getTempWriter().use { trapFileBW ->
282316
// We want our comments to be the first thing in the file,
283317
// so start off with a mere TrapWriter
284318
val tw = TrapWriter(loggerBase, TrapLabelManager(), trapFileBW, fileTrapWriter)
@@ -294,31 +328,114 @@ private fun doFile(
294328
externalDeclExtractor.extractExternalClasses()
295329
}
296330

297-
if (checkTrapIdentical && trapFile.exists()) {
298-
if (equivalentTrap(trapTmpFile, trapFile)) {
299-
if (!trapTmpFile.delete()) {
300-
logger.warn("Failed to delete $trapTmpFile")
301-
}
331+
if (checkTrapIdentical && trapFileWriter.exists()) {
332+
if (equivalentTrap(trapFileWriter.getTempReader(), trapFileWriter.getRealReader())) {
333+
trapFileWriter.deleteTemp()
302334
} else {
303-
val trapDifferentFile = File.createTempFile("$srcFilePath.", ".trap.different", dbTrapDir)
304-
if (trapTmpFile.renameTo(trapDifferentFile)) {
305-
logger.warn("TRAP difference: $trapFile vs $trapDifferentFile")
306-
} else {
307-
logger.warn("Failed to rename $trapTmpFile to $trapFile")
308-
}
335+
trapFileWriter.renameTempToDifferent()
309336
}
310337
} else {
311-
if (!trapTmpFile.renameTo(trapFile)) {
312-
logger.warn("Failed to rename $trapTmpFile to $trapFile")
313-
}
338+
trapFileWriter.renameTempToReal()
314339
}
315340
// We catch Throwable rather than Exception, as we want to
316341
// continue trying to extract everything else even if we get a
317342
// stack overflow or an assertion failure in one file.
318343
} catch (e: Throwable) {
319-
logger.error("Failed to extract '$srcFilePath'. Partial TRAP file location is $trapTmpFile", e)
344+
logger.error("Failed to extract '$srcFilePath'. " + trapFileWriter.debugInfo(), e)
320345
context.clear()
321346
fileExtractionProblems.setNonRecoverableProblem()
322347
}
323348
}
324349
}
350+
351+
enum class Compression { NONE, GZIP, BROTLI }
352+
353+
private fun getTrapFileWriter(compression: Compression, logger: FileLogger, trapFileName: String): TrapFileWriter {
354+
return when (compression) {
355+
Compression.NONE -> NonCompressedTrapFileWriter(logger, trapFileName)
356+
Compression.GZIP -> GZipCompressedTrapFileWriter(logger, trapFileName)
357+
Compression.BROTLI -> throw Exception("Brotli compression is not supported by the Kotlin extractor")
358+
}
359+
}
360+
361+
private abstract class TrapFileWriter(val logger: FileLogger, trapName: String, val extension: String) {
362+
private val realFile = File(trapName + extension)
363+
private val parentDir = realFile.parentFile
364+
lateinit private var tempFile: File
365+
366+
fun debugInfo(): String {
367+
if (this::tempFile.isInitialized) {
368+
return "Partial TRAP file location is $tempFile"
369+
} else {
370+
return "Temporary file not yet created."
371+
}
372+
}
373+
374+
fun makeParentDirectory() {
375+
parentDir.mkdirs()
376+
}
377+
378+
fun exists(): Boolean {
379+
return realFile.exists()
380+
}
381+
382+
abstract protected fun getReader(file: File): BufferedReader
383+
abstract protected fun getWriter(file: File): BufferedWriter
384+
385+
fun getRealReader(): BufferedReader {
386+
return getReader(realFile)
387+
}
388+
389+
fun getTempReader(): BufferedReader {
390+
return getReader(tempFile)
391+
}
392+
393+
fun getTempWriter(): BufferedWriter {
394+
if (this::tempFile.isInitialized) {
395+
logger.error("Temp writer reinitiailised for $realFile")
396+
}
397+
tempFile = File.createTempFile(realFile.getName() + ".", ".trap.tmp" + extension, parentDir)
398+
return getWriter(tempFile)
399+
}
400+
401+
fun deleteTemp() {
402+
if (!tempFile.delete()) {
403+
logger.warn("Failed to delete $tempFile")
404+
}
405+
}
406+
407+
fun renameTempToDifferent() {
408+
val trapDifferentFile = File.createTempFile(realFile.getName() + ".", ".trap.different" + extension, parentDir)
409+
if (tempFile.renameTo(trapDifferentFile)) {
410+
logger.warn("TRAP difference: $realFile vs $trapDifferentFile")
411+
} else {
412+
logger.warn("Failed to rename $tempFile to $realFile")
413+
}
414+
}
415+
416+
fun renameTempToReal() {
417+
if (!tempFile.renameTo(realFile)) {
418+
logger.warn("Failed to rename $tempFile to $realFile")
419+
}
420+
}
421+
}
422+
423+
private class NonCompressedTrapFileWriter(logger: FileLogger, trapName: String): TrapFileWriter(logger, trapName, "") {
424+
override protected fun getReader(file: File): BufferedReader {
425+
return file.bufferedReader()
426+
}
427+
428+
override protected fun getWriter(file: File): BufferedWriter {
429+
return file.bufferedWriter()
430+
}
431+
}
432+
433+
private class GZipCompressedTrapFileWriter(logger: FileLogger, trapName: String): TrapFileWriter(logger, trapName, ".gz") {
434+
override protected fun getReader(file: File): BufferedReader {
435+
return BufferedReader(InputStreamReader(GZIPInputStream(BufferedInputStream(FileInputStream(file)))))
436+
}
437+
438+
override protected fun getWriter(file: File): BufferedWriter {
439+
return BufferedWriter(OutputStreamWriter(GZIPOutputStream(BufferedOutputStream(FileOutputStream(file)))))
440+
}
441+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class Test {
2+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from create_database_utils import *
2+
3+
def check_extension(directory, expected_extension):
4+
if expected_extension == '.trap':
5+
# We start TRAP files with a comment
6+
expected_start = b'//'
7+
elif expected_extension == '.trap.gz':
8+
# The GZip magic numbers
9+
expected_start = b'\x1f\x8b'
10+
else:
11+
raise Exception('Unknown expected extension ' + expected_extension)
12+
count = check_extension_worker(directory, expected_extension, expected_start)
13+
if count != 1:
14+
raise Exception('Expected 1 relevant file, but found ' + str(count) + ' in ' + directory)
15+
16+
def check_extension_worker(directory, expected_extension, expected_start):
17+
count = 0
18+
for f in os.listdir(directory):
19+
x = os.path.join(directory, f)
20+
if os.path.isdir(x):
21+
count += check_extension_worker(x, expected_extension, expected_start)
22+
else:
23+
if f.startswith('test.kt') and not f.endswith('.set'):
24+
if f.endswith(expected_extension):
25+
with open(x, 'rb') as f_in:
26+
content = f_in.read()
27+
if content.startswith(expected_start):
28+
count += 1
29+
else:
30+
raise Exception('Unexpected start to content of ' + x)
31+
else:
32+
raise Exception('Expected test.kt TRAP file to have extension ' + expected_extension + ', but found ' + x)
33+
return count
34+
35+
run_codeql_database_create(['kotlinc test.kt'], test_db="default-db", db=None, lang="java")
36+
check_extension('default-db/trap', '.trap.gz')
37+
os.environ["CODEQL_EXTRACTOR_JAVA_OPTION_TRAP_COMPRESSION"] = "nOnE"
38+
run_codeql_database_create(['kotlinc test.kt'], test_db="none-db", db=None, lang="java")
39+
check_extension('none-db/trap', '.trap')
40+
os.environ["CODEQL_EXTRACTOR_JAVA_OPTION_TRAP_COMPRESSION"] = "gzip"
41+
run_codeql_database_create(['kotlinc test.kt'], test_db="gzip-db", db=None, lang="java")
42+
check_extension('gzip-db/trap', '.trap.gz')
43+
os.environ["CODEQL_EXTRACTOR_JAVA_OPTION_TRAP_COMPRESSION"] = "brotli"
44+
run_codeql_database_create(['kotlinc test.kt'], test_db="brotli-db", db=None, lang="java")
45+
check_extension('brotli-db/trap', '.trap.gz')
46+
os.environ["CODEQL_EXTRACTOR_JAVA_OPTION_TRAP_COMPRESSION"] = "invalidValue"
47+
run_codeql_database_create(['kotlinc test.kt'], test_db="invalid-db", db=None, lang="java")
48+
check_extension('invalid-db/trap', '.trap.gz')

0 commit comments

Comments
 (0)