@@ -11,14 +11,21 @@ import com.intellij.openapi.progress.ProgressManager
1111import com.intellij.openapi.progress.Task
1212import com.intellij.openapi.application.PathManager
1313import com.intellij.openapi.util.SystemInfo
14- import com.intellij.util.io.Decompressor
1514import okhttp3.OkHttpClient
1615import okhttp3.Request
16+ import org.apache.commons.compress.archivers.tar.TarArchiveEntry
17+ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream
1718import java.io.File
1819import 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
1924import java.security.MessageDigest
2025import java.util.concurrent.TimeUnit
2126import 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