Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import java.math.BigInteger
*
*/
interface JIRByteCodeLocation {
val jarOrFolder: File

/**
* Is being calculated each time when it is invoked.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,22 @@ import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.seqra.ir.api.jvm.JavaVersion
import org.seqra.ir.api.jvm.JIRByteCodeLocation
import org.seqra.ir.api.jvm.JIRClasspath
import org.seqra.ir.api.jvm.JIRClasspathFeature
import org.seqra.ir.api.jvm.JIRDatabase
import org.seqra.ir.api.jvm.JIRDatabasePersistence
import org.seqra.ir.api.jvm.JIRFeature
import org.seqra.ir.api.jvm.JIRSettings
import org.seqra.ir.api.jvm.JavaVersion
import org.seqra.ir.api.jvm.RegisteredLocation
import org.seqra.ir.impl.features.classpaths.ClasspathCache
import org.seqra.ir.impl.features.classpaths.KotlinMetadata
import org.seqra.ir.impl.features.classpaths.MethodInstructionsFeature
import org.seqra.ir.impl.features.classpaths.UnknownClassMethodsAndFields
import org.seqra.ir.impl.features.classpaths.UnknownClasses
import org.seqra.ir.impl.fs.JavaRuntime
import org.seqra.ir.impl.fs.asByteCodeLocation
import org.seqra.ir.impl.fs.filterExisting
import org.seqra.ir.impl.fs.createNonRuntimeByteCodeLocations
import org.seqra.ir.impl.fs.lazySources
import org.seqra.ir.impl.fs.sources
import org.seqra.ir.impl.storage.ers.ERS_DATABASE_PERSISTENCE_SPI
Expand Down Expand Up @@ -89,8 +88,7 @@ class JIRDatabaseImpl(
val runtime = JavaRuntime(settings.jre).allLocations.takeIf { settings.buildModelForJRE }
val runtimeNew = runtime?.let { locationsRegistry.setup(it).new }
val registeredNew = locationsRegistry.registerIfNeeded(
settings.predefinedDirOrJars.filter { it.exists() }
.flatMap { it.asByteCodeLocation(javaRuntime.version, isRuntime = false) }.distinct()
settings.predefinedDirOrJars.createNonRuntimeByteCodeLocations(javaRuntime.version)
).new
if (canBeDumped() && persistence.tryLoad(id)) {
isImmutable = true
Expand Down Expand Up @@ -118,8 +116,7 @@ class JIRDatabaseImpl(

override suspend fun classpath(dirOrJars: List<File>, features: List<JIRClasspathFeature>?): JIRClasspath {
assertNotClosed()
val existingLocations =
dirOrJars.filterExisting().flatMap { it.asByteCodeLocation(javaRuntime.version) }.distinct()
val existingLocations = dirOrJars.createNonRuntimeByteCodeLocations(javaRuntime.version)
val processed = locationsRegistry.registerIfNeeded(existingLocations)
.also { it.new.process(true) }.registered + locationsRegistry.runtimeLocations
return JIRClasspathImpl(
Expand Down Expand Up @@ -150,7 +147,7 @@ class JIRDatabaseImpl(

override suspend fun load(dirOrJars: List<File>) = apply {
assertNotClosed()
loadLocations(dirOrJars.filterExisting().flatMap { it.asByteCodeLocation(javaRuntime.version) }.distinct())
loadLocations(dirOrJars.createNonRuntimeByteCodeLocations(javaRuntime.version))
}

override suspend fun loadLocations(locations: List<JIRByteCodeLocation>) = apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package org.seqra.ir.impl.features.classpaths

import org.seqra.ir.api.jvm.*
import org.seqra.ir.api.jvm.ext.objectType
import org.seqra.ir.impl.cfg.util.OBJECT_CLASS
import org.seqra.ir.impl.cfg.util.typeNameFromJvmName
import org.objectweb.asm.Opcodes
import org.seqra.ir.api.jvm.ext.toType


class JIRUnknownType(
Expand All @@ -27,10 +27,11 @@ class JIRUnknownType(
override val fields: List<JIRTypedField> = emptyList()
override val typeParameters: List<JIRTypeVariableDeclaration> = emptyList()
override val typeArguments: List<JIRRefType> = emptyList()
override val superType: JIRClassType get() = classpath.objectType
override val interfaces: List<JIRClassType> = emptyList()
override val innerTypes: List<JIRClassType> = emptyList()

override val superType: JIRClassType? get() = jIRClass.superClass?.toType()

override val typeName: String
get() = name

Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,10 @@
package org.seqra.ir.impl.fs

import com.google.common.hash.Hashing
import org.seqra.ir.api.jvm.JIRByteCodeLocation
import java.io.File
import java.math.BigInteger
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets


abstract class AbstractByteCodeLocation(override val jarOrFolder: File) : JIRByteCodeLocation {

override val path: String
get() = jarOrFolder.absolutePath

abstract class AbstractByteCodeLocation : JIRByteCodeLocation {
override val fileSystemIdHash: BigInteger by lazy { currentHash }

override fun isChanged() = fileSystemIdHash != currentHash

protected val String.shaHash: ByteArray
get() {
return Hashing.sha256()
.hashString(this, StandardCharsets.UTF_8)
.asBytes()
}

protected val ByteBuffer.shaHash: ByteArray
get() {
return Hashing.sha256()
.hashBytes(this)
.asBytes()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ import java.nio.file.Paths
import kotlin.streams.asSequence
import kotlin.text.Charsets.UTF_8

class BuildFolderLocation(folder: File) : AbstractByteCodeLocation(folder) {
class BuildFolderLocation(val jarOrFolder: File) : AbstractByteCodeLocation() {

companion object : KLogging()

override val path: String
get() = jarOrFolder.absolutePath

@Suppress("UnstableApiUsage")
override val currentHash: BigInteger
get() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.seqra.ir.impl.fs

import mu.KLogging
import org.seqra.ir.api.jvm.JavaVersion
import org.seqra.ir.api.jvm.JIRByteCodeLocation
import org.seqra.ir.api.jvm.JavaVersion
import java.io.File
import java.nio.file.Paths
import java.util.jar.JarFile
Expand All @@ -15,19 +15,23 @@ val logger = object : KLogging() {}.logger
* The method called of different files can have same locations in the result, so use `distinct()` to
* filter duplicates out.
*/
fun File.asByteCodeLocation(runtimeVersion: JavaVersion, isRuntime: Boolean = false): Collection<JIRByteCodeLocation> {
if (!exists()) {
throw IllegalArgumentException("file $absolutePath doesn't exist")
fun File.dirOrJarAsBytecodeLocation(runtimeVersion: JavaVersion, isRuntime: Boolean): Collection<JIRByteCodeLocation> {
if (isJar()) {
return mutableSetOf<File>().also { classPath(it) }.map { JarLocation(it, isRuntime, runtimeVersion) }
}
return if (isJar()) {
mutableSetOf<File>().also { classPath(it) }.map { JarLocation(it, isRuntime, runtimeVersion) }
} else if (isDirectory) {
listOf(BuildFolderLocation(this))
} else {
error("$absolutePath is nether a jar file nor a build directory")

if (isDirectory) {
return listOf(BuildFolderLocation(this))
}

error("$absolutePath is invalid bytecode location")
}

fun Collection<File>.createNonRuntimeByteCodeLocations(runtimeVersion: JavaVersion): List<JIRByteCodeLocation> =
filterExisting()
.flatMap { it.dirOrJarAsBytecodeLocation(runtimeVersion, isRuntime = false) }
.distinct()

fun Collection<File>.filterExisting(): List<File> = filter { file ->
file.exists().also {
if (!it) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,22 @@ import java.util.jar.JarFile
import kotlin.text.Charsets.UTF_8

open class JarLocation(
file: File,
val jarOrFolder: File,
private val isRuntime: Boolean,
private val runtimeVersion: JavaVersion
) : AbstractByteCodeLocation(file) {
) : AbstractByteCodeLocation() {

companion object : KLogging()

override val path: String get() = jarOrFolder.absolutePath

@Suppress("UnstableApiUsage")
override val currentHash: BigInteger
get() {
val jarFile = jarFile() ?: return BigInteger.ZERO
return Hashing.sha256().newHasher().let { h ->
jarFile.use {
it.entries().asSequence().filter { !it.isDirectory }.sortedBy { it.name }.forEach { entry ->
jarFile.use { jf ->
jf.entries().asSequence().filter { !it.isDirectory }.sortedBy { it.name }.forEach { entry ->
h.putString(entry.name, UTF_8)
h.putLong(entry.crc)
h.putLong(entry.size)
Expand Down
23 changes: 20 additions & 3 deletions seqra-ir-core/src/main/kotlin/org/seqra/ir/impl/fs/JavaRuntime.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.seqra.ir.impl.fs

import org.seqra.ir.api.jvm.JavaVersion
import org.seqra.ir.api.jvm.JIRByteCodeLocation
import org.seqra.ir.api.jvm.JavaVersion
import java.io.File
import java.nio.file.Paths

Expand All @@ -21,7 +21,24 @@ class JavaRuntime(private val javaHome: File) {
parseRuntimeVersion("1.8.0")
}

val allLocations: List<JIRByteCodeLocation> = modules.takeIf { it.isNotEmpty() } ?: (bootstrapJars + extJars)
val allLocations: List<JIRByteCodeLocation> = findRuntimeLocations()

private fun findRuntimeLocations(): List<JIRByteCodeLocation> {
val modulesPath = javaHome.resolve("lib/modules")
if (modulesPath.exists()) {
val modulesLocation = JavaRuntimeModuleLocation.loadModules(javaHome)
if (modulesLocation.isNotEmpty()) return modulesLocation

val modules = locations("jmods")
if (modules.isEmpty()) {
logger.warn("Can't load JDK modules")
}
}

modules.takeIf { it.isNotEmpty() }?.let { return it }

return bootstrapJars + extJars
}

private val modules: List<JIRByteCodeLocation> get() = locations("jmods")

Expand All @@ -48,7 +65,7 @@ class JavaRuntime(private val javaHome: File) {
.listFiles { file -> file.name.endsWith(".jar") || file.name.endsWith(".jmod") }
.orEmpty()
.toList()
.flatMap { it.asByteCodeLocation(version, true) }
.flatMap { it.dirOrJarAsBytecodeLocation(version, true) }
.distinct()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package org.seqra.ir.impl.fs

import com.google.common.hash.Hashing
import org.seqra.ir.api.jvm.JIRByteCodeLocation
import org.seqra.ir.api.jvm.LocationType
import java.io.File
import java.math.BigInteger
import java.net.URI
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Path
import kotlin.io.path.exists
import kotlin.io.path.fileSize
import kotlin.io.path.listDirectoryEntries
import kotlin.io.path.name
import kotlin.io.path.readBytes
import kotlin.io.path.walk
import kotlin.text.Charsets.UTF_8

open class JavaRuntimeModuleLocation(
val javaHome: File,
val module: String,
) : AbstractByteCodeLocation() {

override val path: String get() = createPath(javaHome.absolutePath, module)

override val type: LocationType get() = LocationType.RUNTIME

@Suppress("UnstableApiUsage")
override val currentHash: BigInteger
get() {
val hasher = Hashing.sha256().newHasher()
useModule { moduleBase, moduleBasePath ->
moduleBase.walk()
.filter { it.isValidClassFile() }
.sortedBy { it.toString() }
.forEach { classFile ->
val classFileName = classFile.classFileName(moduleBasePath)
hasher.putString(classFileName, UTF_8)
hasher.putLong(classFile.fileSize())
}
}
return BigInteger(hasher.hash().asBytes())
}

override val classNames: Set<String>?
get() = useModule { moduleBase, moduleBasePath ->
moduleBase.walk()
.filter { it.isValidClassFile() }
.mapTo(hashSetOf()) { it.className(moduleBasePath) }
}

override val classes: Map<String, ByteArray>
get() = useModule { moduleBase, moduleBasePath ->
moduleBase.walk()
.filter { it.isValidClassFile() }
.associateTo(hashMapOf()) { classFile ->
val className = classFile.className(moduleBasePath)
val classBytes = classFile.readBytes()
className to classBytes
}
}

override fun resolve(classFullName: String): ByteArray? {
val classFilePath = classFullName.replace('.', '/') + ".class"
useModule { moduleBase, _ ->
val classFile = moduleBase.resolve(classFilePath)
if (!classFile.exists()) return null
return classFile.readBytes()
}
}

override fun createRefreshed(): JIRByteCodeLocation? = JavaRuntimeModuleLocation(javaHome, module)

override fun equals(other: Any?): Boolean {
if (other == null || other !is JavaRuntimeModuleLocation) {
return false
}
return other.module == module && other.javaHome == javaHome
}

override fun hashCode(): Int {
var result = javaHome.hashCode()
result = 31 * result + module.hashCode()
return result
}

private inline fun <T> useModule(body: (Path, String) -> T): T {
val fs = newFs(javaHome)
?: error("JRT file system not available")

return fs.use {
val module = fs.getPath(MODULES, module)
body(module, "$module/")
}
}

companion object {
private const val MODULES = "/modules"

fun loadModules(javaHome: File): List<JavaRuntimeModuleLocation> =
newFs(javaHome)?.use { fs ->
val modulesDir = fs.getPath(MODULES)
val modules = modulesDir.listDirectoryEntries()
modules.map { JavaRuntimeModuleLocation(javaHome, it.name) }
}.orEmpty()

private fun newFs(javaHome: File): FileSystem? {
val env = hashMapOf<String, Any>("java.home" to javaHome.absolutePath)

return runCatching {
FileSystems.newFileSystem(URI.create(JRT), env)
}.getOrNull()
}

private const val JRT = "jrt:/"

// note: use null symbol because it can't appear in any path
private const val SEPARATOR = '\u0000'

private const val PATH_PREFIX = "$JRT$SEPARATOR"

fun isModuleLocation(path: String): Boolean = path.startsWith(PATH_PREFIX)

fun fromPath(path: String): JavaRuntimeModuleLocation {
val moduleHomeSeparatorPos = path.indexOf(SEPARATOR, startIndex = PATH_PREFIX.length)
val module = path.substring(PATH_PREFIX.length + 1, moduleHomeSeparatorPos)
val home = path.substring(moduleHomeSeparatorPos + 1)
return JavaRuntimeModuleLocation(File(home), module)
}

private fun createPath(javaHome: String, module: String): String =
"$PATH_PREFIX$module$SEPARATOR$javaHome"

private fun Path.isValidClassFile(): Boolean =
name.endsWith(".class") && !name.contains("module-info")

private fun Path.classFileName(moduleBasePath: String): String =
toString().removePrefix(moduleBasePath)

private fun Path.className(moduleBasePath: String): String =
classFileName(moduleBasePath).removeSuffix(".class").replace('/', '.')
}
}
Loading
Loading