diff --git a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkList.kt b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkList.kt index 29dcfef769a3..e52ae467a939 100644 --- a/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkList.kt +++ b/platform/lang-impl/src/com/intellij/openapi/projectRoots/impl/jdkDownloader/JdkList.kt @@ -139,12 +139,12 @@ data class JdkItem( enum class JdkPackageType(@NonNls val type: String) { @Suppress("unused") ZIP("zip") { - override fun openDecompressor(archiveFile: File) = Decompressor.Zip(archiveFile) + override fun openDecompressor(archiveFile: File) = Decompressor.Zip(archiveFile).withUnixPermissionsAndSymlinks() }, @Suppress("SpellCheckingInspection", "unused") TAR_GZ("targz") { - override fun openDecompressor(archiveFile: File) = Decompressor.Tar(archiveFile) + override fun openDecompressor(archiveFile: File) = Decompressor.Tar(archiveFile).withSymlinks() }; abstract fun openDecompressor(archiveFile: File): Decompressor diff --git a/platform/util/src/com/intellij/util/io/Decompressor.java b/platform/util/src/com/intellij/util/io/Decompressor.java index 8ba7053ed456..c208d40ec296 100644 --- a/platform/util/src/com/intellij/util/io/Decompressor.java +++ b/platform/util/src/com/intellij/util/io/Decompressor.java @@ -9,6 +9,7 @@ import com.intellij.util.ArrayUtil; import com.intellij.util.Consumer; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.compressors.CompressorException; import org.apache.commons.compress.compressors.CompressorStreamFactory; import org.jetbrains.annotations.NotNull; @@ -90,6 +91,7 @@ public abstract class Decompressor { // } + //NOTE. This class should work without CommonsCompress! public static class Zip extends Decompressor { public Zip(@NotNull File file) { mySource = file; @@ -101,6 +103,15 @@ public abstract class Decompressor { private Enumeration myEntries; private ZipEntry myEntry; + /** + * Enables Zip Extensions to consider symlinks and unix file permissions. + * NOTE. It will require CommonsCompress in the classpath + */ + @NotNull + public Decompressor withUnixPermissionsAndSymlinks() { + return new CommonsZip(mySource); + } + @Override protected void openStream() throws IOException { myZip = new ZipFile(mySource); @@ -123,6 +134,68 @@ public abstract class Decompressor { stream.close(); } + @Override + protected void closeStream() throws IOException { + if (myZip != null) { + myZip.close(); + myZip = null; + } + } + // + } + + private static class CommonsZip extends Decompressor { + CommonsZip(@NotNull File file) { + mySource = file; + } + + // + private final File mySource; + private org.apache.commons.compress.archivers.zip.ZipFile myZip; + private Enumeration myEntries; + private ZipArchiveEntry myEntry; + + @Override + protected void openStream() throws IOException { + myZip = new org.apache.commons.compress.archivers.zip.ZipFile(mySource); + myEntries = myZip.getEntries(); + } + + @Override + protected Entry nextEntry() throws IOException { + if (!myEntries.hasMoreElements()) { + myEntry = null; + return null; + } + + myEntry = myEntries.nextElement(); + if (myEntry == null) { + return null; + } + + String linkTarget = myEntry.isUnixSymlink() ? myZip.getUnixSymlink(myEntry) : null; + //noinspection OctalInteger + return new Entry(myEntry.getName(), + type(myEntry), + isSet(myEntry.getUnixMode(), 0200), + isSet(myEntry.getUnixMode(), 0100), + linkTarget); + } + + private static Type type(ZipArchiveEntry te) { + return te.isUnixSymlink() ? Type.SYMLINK : te.isDirectory() ? Type.DIR : Type.FILE; + } + + @Override + protected InputStream openEntryStream(Entry entry) throws IOException { + return myZip.getInputStream(myEntry); + } + + @Override + protected void closeEntryStream(InputStream stream) throws IOException { + stream.close(); + } + @Override protected void closeStream() throws IOException { myZip.close(); diff --git a/platform/util/testSrc/com/intellij/util/io/DecompressorTest.kt b/platform/util/testSrc/com/intellij/util/io/DecompressorTest.kt index 6c275f478a5d..07b159261ed0 100644 --- a/platform/util/testSrc/com/intellij/util/io/DecompressorTest.kt +++ b/platform/util/testSrc/com/intellij/util/io/DecompressorTest.kt @@ -6,6 +6,9 @@ import com.intellij.openapi.util.io.IoTestUtil.assumeSymLinkCreationIsSupported import com.intellij.testFramework.rules.TempDirectory import org.apache.commons.compress.archivers.tar.TarArchiveEntry import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream +import org.apache.commons.compress.archivers.zip.UnixStat +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Condition @@ -15,6 +18,8 @@ import org.junit.Test import java.io.File import java.io.FileOutputStream import java.io.IOException +import java.nio.file.attribute.FileTime +import java.time.Instant import java.util.* import java.util.function.Predicate import java.util.zip.ZipEntry @@ -31,6 +36,13 @@ class DecompressorTest { testNoTraversal(Decompressor.Zip(zip), dir, File(dir, "bad.txt")) } + @Test fun noInternalTraversalInCommonsZip() { + val zip = tempDir.newFile("test.zip") + ZipOutputStream(FileOutputStream(zip)).use { writeEntry(it, "a/../bad.txt") } + val dir = tempDir.newFolder("unpacked") + testNoTraversal(Decompressor.Zip(zip).withUnixPermissionsAndSymlinks(), dir, File(dir, "bad.txt")) + } + @Test fun noExternalTraversalInZip() { val zip = tempDir.newFile("test.zip") ZipOutputStream(FileOutputStream(zip)).use { writeEntry(it, "../evil.txt") } @@ -38,6 +50,13 @@ class DecompressorTest { testNoTraversal(Decompressor.Zip(zip), dir, File(dir.parent, "evil.txt")) } + @Test fun noExternalTraversalInCommons() { + val zip = tempDir.newFile("test.zip") + ZipOutputStream(FileOutputStream(zip)).use { writeEntry(it, "../evil.txt") } + val dir = tempDir.newFolder("unpacked") + testNoTraversal(Decompressor.Zip(zip).withUnixPermissionsAndSymlinks(), dir, File(dir.parent, "evil.txt")) + } + @Test fun noAbsolutePathsInZip() { val zip = tempDir.newFile("test.zip") ZipOutputStream(FileOutputStream(zip)).use { writeEntry(it, "/root.txt") } @@ -46,6 +65,14 @@ class DecompressorTest { assertThat(File(dir, "root.txt")).exists() } + @Test fun noAbsolutePathsInCommonsZip() { + val zip = tempDir.newFile("test.zip") + ZipOutputStream(FileOutputStream(zip)).use { writeEntry(it, "/root.txt") } + val dir = tempDir.newFolder("unpacked") + Decompressor.Zip(zip).withUnixPermissionsAndSymlinks().extract(dir) + assertThat(File(dir, "root.txt")).exists() + } + @Test fun tarDetectionPlain() { val tar = tempDir.newFile("test.tar") TarArchiveOutputStream(FileOutputStream(tar)).use { writeEntry(it, "dir/file.txt") } @@ -92,6 +119,14 @@ class DecompressorTest { Decompressor.Zip(zip).extract(dir) } + @Test(expected = ZipException::class) + fun failsOnCorruptedCommonsZip() { + val zip = tempDir.newFile("test.zip") + zip.writeText("whatever") + val dir = tempDir.newFolder("unpacked") + Decompressor.Zip(zip).withUnixPermissionsAndSymlinks().extract(dir) + } + @Test fun tarFileModes() { val tar = tempDir.newFile("test.tar") TarArchiveOutputStream(FileOutputStream(tar)).use { @@ -108,6 +143,22 @@ class DecompressorTest { assertThat(File(dir, "dir/rwx")).exists().`is`(Writable).`is`(Executable) } + @Test fun zipFileModes() { + val zip = tempDir.newFile("test.zip") + ZipArchiveOutputStream(FileOutputStream(zip)).use { + writeEntry(it, "dir/r", mode = 0b100_000_000) + writeEntry(it, "dir/rw", mode = 0b110_000_000) + writeEntry(it, "dir/rx", mode = 0b101_000_000) + writeEntry(it, "dir/rwx", mode = 0b111_000_000) + } + val dir = tempDir.newFolder("unpacked") + Decompressor.Zip(zip).withUnixPermissionsAndSymlinks().extract(dir) + assertThat(File(dir, "dir/r")).exists().isNot(Writable).let { if (SystemInfo.isUnix) it.isNot(Executable) } + assertThat(File(dir, "dir/rw")).exists().`is`(Writable).let { if (SystemInfo.isUnix) it.isNot(Executable) } + assertThat(File(dir, "dir/rx")).exists().isNot(Writable).`is`(Executable) + assertThat(File(dir, "dir/rwx")).exists().`is`(Writable).`is`(Executable) + } + @Test fun filtering() { val zip = tempDir.newFile("test.zip") ZipOutputStream(FileOutputStream(zip)).use { @@ -120,7 +171,7 @@ class DecompressorTest { assertThat(File(dir, "d2")).doesNotExist() } - @Test fun symlinks() { + @Test fun tarSymlinks() { assumeSymLinkCreationIsSupported() val tar = tempDir.newFile("test.tar") @@ -133,7 +184,20 @@ class DecompressorTest { assertThat(File(dir, "links/ok").toPath()).isSymbolicLink().hasSameContentAs(File(dir, "f").toPath()) } - @Test fun rogueSymlinks() { + @Test fun zipSymlinks() { + assumeSymLinkCreationIsSupported() + + val zip = tempDir.newFile("test.zip") + ZipArchiveOutputStream(FileOutputStream(zip)).use { + writeEntry(it, "f") + writeEntry(it, "links/ok", link = "../f") + } + val dir = tempDir.newFolder("unpacked") + Decompressor.Zip(zip).withUnixPermissionsAndSymlinks().extract(dir) + assertThat(File(dir, "links/ok").toPath()).isSymbolicLink().hasSameContentAs(File(dir, "f").toPath()) + } + + @Test fun tarRogueSymlinks() { assumeSymLinkCreationIsSupported() val tar = tempDir.newFile("test.tar") @@ -142,6 +206,15 @@ class DecompressorTest { testNoTraversal(Decompressor.Tar(tar).withSymlinks(), dir, File(dir, "rogue")) } + @Test fun zipRogueSymlinks() { + assumeSymLinkCreationIsSupported() + + val zip = tempDir.newFile("test.zip") + ZipArchiveOutputStream(FileOutputStream(zip)).use { writeEntry(it, "rogue", link = "../f") } + val dir = tempDir.newFolder("unpacked") + testNoTraversal(Decompressor.Zip(zip).withUnixPermissionsAndSymlinks(), dir, File(dir, "rogue")) + } + @Test fun prefixPathsFilesInZip() { val zip = tempDir.newFile("test.zip") ZipOutputStream(FileOutputStream(zip)).use { @@ -156,7 +229,21 @@ class DecompressorTest { assertThat(File(dir, "b")).doesNotExist() } - @Test fun prefixPathFilesInZipWitFilter() { + @Test fun prefixPathsFilesInCommonsZip() { + val zip = tempDir.newFile("test.zip") + ZipOutputStream(FileOutputStream(zip)).use { + writeEntry(it, "a/b/c.txt") + } + val dir = tempDir.newFolder("unpacked") + Decompressor.Zip(zip).withUnixPermissionsAndSymlinks().removePrefixPath("a/b").extract(dir) + + assertThat(File(dir, "c.txt")).isFile() + assertThat(File(dir, "a")).doesNotExist() + assertThat(File(dir, "a/b")).doesNotExist() + assertThat(File(dir, "b")).doesNotExist() + } + + @Test fun prefixPathFilesInZipWithFilter() { val zip = tempDir.newFile("test.zip") ZipOutputStream(FileOutputStream(zip)).use { writeEntry(it, "a/b/c.txt") @@ -231,6 +318,21 @@ class DecompressorTest { assertThat(File(dir, "h")).isFile() } + @Test fun prefixPathWithCommonsZip() { + val zip = tempDir.newFile("test.zip") + ZipArchiveOutputStream(FileOutputStream(zip)).use { + writeEntry(it, "./a/b/f") + writeEntry(it, "/a/b/g") + writeEntry(it, "././././././//a/b/h") + } + val dir = tempDir.newFolder("unpacked") + Decompressor.Zip(zip).withUnixPermissionsAndSymlinks().removePrefixPath("./a/b").extract(dir) + + assertThat(File(dir, "f")).isFile() + assertThat(File(dir, "g")).isFile() + assertThat(File(dir, "h")).isFile() + } + @Test fun prefixPathTarSymlink() { assumeSymLinkCreationIsSupported() @@ -246,6 +348,21 @@ class DecompressorTest { assertThat(File(dir, "links/ok").toPath()).isSymbolicLink().hasSameContentAs(File(dir, "f").toPath()) } + @Test fun prefixPathZipSymlink() { + assumeSymLinkCreationIsSupported() + + val zip = tempDir.newFile("test.zip") + ZipArchiveOutputStream(FileOutputStream(zip)).use { + writeEntry(it, "./a/b/f") + writeEntry(it, "a/b/links/ok", link = "../f") + } + val dir = tempDir.newFolder("unpacked") + Decompressor.Zip(zip).withUnixPermissionsAndSymlinks().removePrefixPath("a/b").extract(dir) + + assertThat(File(dir, "f")).isFile() + assertThat(File(dir, "links/ok").toPath()).isSymbolicLink().hasSameContentAs(File(dir, "f").toPath()) + } + @Test fun prefixPathRogueSymlinks() { assumeSymLinkCreationIsSupported() @@ -296,6 +413,25 @@ class DecompressorTest { tar.closeArchiveEntry() } + private fun writeEntry(zip: ZipArchiveOutputStream, name: String, mode: Int = 0, link: String? = null) { + val entry = ZipArchiveEntry(name) + entry.lastModifiedTime = FileTime.from(Instant.now()) + + if (link != null) { + entry.lastModifiedTime = FileTime.from(Instant.now()) + entry.unixMode = UnixStat.LINK_FLAG + zip.putArchiveEntry(entry) + zip.write(link.toByteArray(Charsets.UTF_8)) + } + else { + entry.size = 1 + if (mode != 0) entry.unixMode = mode + zip.putArchiveEntry(entry) + zip.write('-'.toInt()) + } + zip.closeArchiveEntry() + } + private fun testNoTraversal(decompressor: Decompressor, dir: File, unexpected: File) { val error = try { decompressor.extract(dir)