Decompressor support for Zip file permissions and symlinks now optionally (used in Jdk Downloader)

GitOrigin-RevId: 4737cc8678ec898d5fe95b849214f9cefa8820e0
This commit is contained in:
Eugene Petrenko
2020-02-11 08:45:32 +00:00
committed by intellij-monorepo-bot
parent 7576030fb2
commit 5f6e96d4a7
3 changed files with 214 additions and 5 deletions
@@ -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
@@ -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 {
//</editor-fold>
}
//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<? extends ZipEntry> 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;
}
}
//</editor-fold>
}
private static class CommonsZip extends Decompressor {
CommonsZip(@NotNull File file) {
mySource = file;
}
//<editor-fold desc="Implementation">
private final File mySource;
private org.apache.commons.compress.archivers.zip.ZipFile myZip;
private Enumeration<? extends ZipArchiveEntry> 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();
@@ -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)