Skip to content

Commit 58e298f

Browse files
committed
use special zip extraction
1 parent 6397b55 commit 58e298f

2 files changed

Lines changed: 110 additions & 4 deletions

File tree

jetbrains_plugin/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ repositories {
229229
dependencies {
230230
implementation("com.squareup.okhttp3:okhttp:4.10.0")
231231
implementation("com.google.code.gson:gson:2.10.1")
232+
implementation("org.apache.commons:commons-compress:1.27.1")
232233
testImplementation("junit:junit:4.13.2")
233234
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.23.4")
234235
}
@@ -333,4 +334,4 @@ detekt {
333334
sarif.required.set(true)
334335
md.required.set(true)
335336
}
336-
}
337+
}

jetbrains_plugin/src/main/kotlin/org/zoocode/jetbrains/core/NodeRuntimeManager.kt

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,21 @@ import com.intellij.openapi.progress.ProgressManager
1111
import com.intellij.openapi.progress.Task
1212
import com.intellij.openapi.application.PathManager
1313
import com.intellij.openapi.util.SystemInfo
14-
import com.intellij.util.io.Decompressor
1514
import okhttp3.OkHttpClient
1615
import okhttp3.Request
16+
import org.apache.commons.compress.archivers.tar.TarArchiveEntry
17+
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
1718
import java.io.File
1819
import java.io.IOException
20+
import java.nio.file.Files
21+
import java.nio.file.LinkOption
22+
import java.nio.file.Path
23+
import java.nio.file.attribute.PosixFilePermission
1924
import java.security.MessageDigest
2025
import java.util.concurrent.TimeUnit
2126
import java.util.concurrent.locks.ReentrantLock
27+
import java.util.zip.GZIPInputStream
28+
import java.util.zip.ZipInputStream
2229

2330
/**
2431
* Managed Node.js runtime manager
@@ -318,11 +325,109 @@ object NodeRuntimeManager {
318325
* Extract a Node.js distribution archive into the destination directory
319326
*/
320327
private fun extractArchive(archive: File, archiveExtension: String, destDir: File) {
328+
if (!destDir.exists() && !destDir.mkdirs()) {
329+
throw IOException("Cannot create archive destination: ${destDir.absolutePath}")
330+
}
321331
if (archiveExtension == "zip") {
322-
Decompressor.Zip(archive).extract(destDir)
332+
extractZip(archive, destDir)
323333
} else {
324-
Decompressor.Tar(archive).extract(destDir)
334+
extractTarGz(archive, destDir)
335+
}
336+
}
337+
338+
private fun extractZip(archive: File, destDir: File) {
339+
ZipInputStream(archive.inputStream().buffered()).use { input ->
340+
while (true) {
341+
val entry = input.nextEntry ?: break
342+
val target = safeArchiveTarget(destDir, entry.name)
343+
if (entry.isDirectory) {
344+
Files.createDirectories(target)
345+
} else {
346+
Files.createDirectories(target.parent)
347+
Files.newOutputStream(target).use { output -> input.copyTo(output) }
348+
}
349+
input.closeEntry()
350+
}
351+
}
352+
}
353+
354+
private fun extractTarGz(archive: File, destDir: File) {
355+
val pendingLinks = mutableListOf<Pair<Path, TarArchiveEntry>>()
356+
TarArchiveInputStream(GZIPInputStream(archive.inputStream().buffered())).use { input ->
357+
while (true) {
358+
val entry = input.nextEntry ?: break
359+
val target = safeArchiveTarget(destDir, entry.name)
360+
when {
361+
entry.isDirectory -> Files.createDirectories(target)
362+
entry.isSymbolicLink || entry.isLink -> pendingLinks.add(target to entry)
363+
entry.isFile -> {
364+
Files.createDirectories(target.parent)
365+
Files.newOutputStream(target).use { output -> input.copyTo(output) }
366+
applyPosixPermissions(target, entry.mode)
367+
}
368+
}
369+
}
370+
}
371+
372+
pendingLinks.forEach { (target, entry) ->
373+
Files.createDirectories(target.parent)
374+
if (entry.isSymbolicLink) {
375+
val linkTarget = Path.of(entry.linkName)
376+
val resolvedLinkTarget = if (linkTarget.isAbsolute) {
377+
linkTarget.normalize()
378+
} else {
379+
target.parent.resolve(linkTarget).normalize()
380+
}
381+
val root = destDir.toPath().toAbsolutePath().normalize()
382+
if (!resolvedLinkTarget.startsWith(root)) {
383+
throw IOException("Archive link escapes destination: ${entry.name} -> ${entry.linkName}")
384+
}
385+
Files.createSymbolicLink(target, linkTarget)
386+
} else {
387+
val linkTarget = safeArchiveTarget(destDir, entry.linkName)
388+
Files.createLink(target, linkTarget)
389+
}
390+
}
391+
}
392+
393+
private fun applyPosixPermissions(path: Path, mode: Int) {
394+
val permissions = mutableSetOf<PosixFilePermission>()
395+
if (mode and 0b100_000_000 != 0) permissions.add(PosixFilePermission.OWNER_READ)
396+
if (mode and 0b010_000_000 != 0) permissions.add(PosixFilePermission.OWNER_WRITE)
397+
if (mode and 0b001_000_000 != 0) permissions.add(PosixFilePermission.OWNER_EXECUTE)
398+
if (mode and 0b000_100_000 != 0) permissions.add(PosixFilePermission.GROUP_READ)
399+
if (mode and 0b000_010_000 != 0) permissions.add(PosixFilePermission.GROUP_WRITE)
400+
if (mode and 0b000_001_000 != 0) permissions.add(PosixFilePermission.GROUP_EXECUTE)
401+
if (mode and 0b000_000_100 != 0) permissions.add(PosixFilePermission.OTHERS_READ)
402+
if (mode and 0b000_000_010 != 0) permissions.add(PosixFilePermission.OTHERS_WRITE)
403+
if (mode and 0b000_000_001 != 0) permissions.add(PosixFilePermission.OTHERS_EXECUTE)
404+
try {
405+
Files.setPosixFilePermissions(path, permissions)
406+
} catch (_: UnsupportedOperationException) {
407+
// Windows and other non-POSIX file systems do not expose Unix mode bits.
408+
}
409+
}
410+
411+
/**
412+
* Resolve an archive entry without allowing it to escape the destination directory.
413+
*/
414+
private fun safeArchiveTarget(destDir: File, entryName: String): Path {
415+
val root = destDir.toPath().toAbsolutePath().normalize()
416+
val target = root.resolve(entryName).normalize()
417+
if (!target.startsWith(root)) {
418+
throw IOException("Archive entry escapes destination: $entryName")
419+
}
420+
421+
// Refuse to write through a symlink left by a partially extracted archive.
422+
var current = target.parent
423+
while (current != null && current.startsWith(root)) {
424+
if (Files.isSymbolicLink(current) || Files.exists(current, LinkOption.NOFOLLOW_LINKS) && !Files.isDirectory(current)) {
425+
throw IOException("Archive entry has an unsafe parent: $entryName")
426+
}
427+
if (current == root) break
428+
current = current.parent
325429
}
430+
return target
326431
}
327432

328433
private fun failureMarkerFile(version: String): File {

0 commit comments

Comments
 (0)