-
Notifications
You must be signed in to change notification settings - Fork 250
Expand file tree
/
Copy pathKlsURI.kt
More file actions
165 lines (135 loc) · 5.58 KB
/
Copy pathKlsURI.kt
File metadata and controls
165 lines (135 loc) · 5.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
package org.javacs.kt.externalsources
import org.javacs.kt.util.partitionAroundLast
import org.javacs.kt.util.TemporaryDirectory
import org.javacs.kt.util.parseURI
import java.net.URI
import java.net.URL
import java.net.JarURLConnection
import java.io.BufferedReader
import java.io.File
import java.nio.file.Path
import java.nio.file.Files
import java.nio.file.Paths
import java.util.zip.ZipFile
fun URI.toKlsURI(): KlsURI? = when (scheme) {
"kls" -> KlsURI(URI("kls:${schemeSpecificPart}"))
"file" -> KlsURI(URI("kls:$this"))
else -> null
}
/**
* Identifies a class or source file inside an archive using a Uniform
* Resource Identifier (URI) with a "kls" (Kotlin language server) scheme.
* The URI should be structured as follows:
*
* <p><pre>
* kls:file:///path/to/jarFile.jar!/path/to/jvmClass.class
* </pre></p>
*
* Other file extensions for classes (such as .kt and .java) are supported too, in
* which case the file will directly be used without invoking the decompiler.
*/
data class KlsURI(val fileUri: URI, val query: Map<QueryParam, String>) {
/** Possible KLS URI query parameters. */
enum class QueryParam(val parameterName: String) {
SOURCE("source");
override fun toString(): String = parameterName
}
enum class ArchiveType(val delimiter: String) {
JAR("!"),
ZIP("!"),
JDK("!/modules")
}
private val queryString: String
get() = if (query.isEmpty()) "" else query.entries.fold("?") { accum, next -> "$accum${next.key}=${next.value}" }
val fileName: String
get() = fileUri.toString().substringAfterLast("/")
val fileExtension: String?
get() = fileName
.split(".")
.takeIf { it.size > 1 }
?.lastOrNull()
private val archiveType: ArchiveType
get() = when {
fileUri.schemeSpecificPart.contains("!/modules") -> {
ArchiveType.JDK
}
fileUri.schemeSpecificPart.contains(".zip!") -> {
ArchiveType.ZIP
}
else -> {
ArchiveType.JAR
}
}
val archivePath: Path
get() = Paths.get(parseURI(fileUri.schemeSpecificPart.split(archiveType.delimiter)[0]))
private val innerPath: String
get() = fileUri.schemeSpecificPart.split(archiveType.delimiter, limit = 2)[1]
val source: Boolean
get() = query[QueryParam.SOURCE]?.toBoolean() ?: false
constructor(uri: URI) : this(parseKlsURIFileURI(uri), parseKlsURIQuery(uri))
// If the newArchivePath doesn't have the kls scheme, it is added in the returned KlsURI.
fun withArchivePath(newArchivePath: Path): KlsURI? =
URI(newArchivePath.toUri().toString() + (innerPath.let { "!$it" } )).toKlsURI()?.let { KlsURI(it.fileUri, query) }
fun withFileExtension(newExtension: String): KlsURI {
val (parentUri, fileName) = fileUri.toString().partitionAroundLast("/")
val newUri = URI("$parentUri${fileName.split(".").first()}.$newExtension")
return KlsURI(newUri, query)
}
fun withSource(source: Boolean): KlsURI {
val newQuery: MutableMap<QueryParam, String> = mutableMapOf()
newQuery.putAll(query)
newQuery[QueryParam.SOURCE] = source.toString()
return KlsURI(fileUri, newQuery)
}
private fun toURI(): URI = URI(fileUri.toString() + queryString)
private fun toJarURL(): URL = URL("jar:${fileUri.schemeSpecificPart}")
private fun openJarURLConnection() = toJarURL().openConnection() as JarURLConnection
private inline fun <T> withJarURLConnection(action: (JarURLConnection) -> T): T {
val connection = openJarURLConnection()
val result = action(connection)
// https://bugs.openjdk.java.net/browse/JDK-8080094
if (connection.useCaches) {
connection.jarFile.close()
}
return result
}
fun readContents(): String = when (archiveType) {
ArchiveType.ZIP -> {
val zipFile = ZipFile(File("$archivePath"))
zipFile.getInputStream(zipFile.getEntry(innerPath.trimStart('/')))
.bufferedReader()
.use(BufferedReader::readText)
}
ArchiveType.JAR, ArchiveType.JDK -> {
withJarURLConnection {
it.jarFile
.getInputStream(it.jarEntry)
.bufferedReader()
.use(BufferedReader::readText)
}
}
}
fun extractToTemporaryFile(dir: TemporaryDirectory): Path = withJarURLConnection {
val name = it.jarEntry.name.substringAfterLast("/").split(".")
val tmpFile = dir.createTempFile(name[0], ".${name[1]}")
it.jarFile
.getInputStream(it.jarEntry)
.use { input -> Files.newOutputStream(tmpFile).use { input.copyTo(it) } }
tmpFile
}
override fun toString(): String = toURI().toString()
}
private fun parseKlsURIFileURI(uri: URI): URI = URI(uri.toString().split("?")[0])
private fun parseKlsURIQuery(uri: URI): Map<KlsURI.QueryParam, String> = parseQuery(uri.toString().split("?").getOrElse(1) { "" })
private fun parseQuery(query: String): Map<KlsURI.QueryParam, String> =
query.split("&").mapNotNull {
val parts = it.split("=")
if (parts.size == 2) getQueryParameter(parts[0], parts[1]) else null
}.toMap()
private fun getQueryParameter(property: String, value: String): Pair<KlsURI.QueryParam, String>? {
val queryParam: KlsURI.QueryParam? = KlsURI.QueryParam.values().find { it.parameterName == property }
if (queryParam != null) {
return Pair(queryParam, value)
}
return null
}