mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Decompressor support for Zip file permissions and symlinks now optionally (used in Jdk Downloader)
GitOrigin-RevId: 4737cc8678ec898d5fe95b849214f9cefa8820e0
This commit is contained in:
committed by
intellij-monorepo-bot
parent
7576030fb2
commit
5f6e96d4a7
+2
-2
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user