From 653f96ee928c8e271067b7d058a48fdae7f1f1fa Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 13 Sep 2026 13:00:59 +0800 Subject: [PATCH 1/4] Add tests for Zip64RequiredException handling in ShadowCopyAction --- .../shadow/tasks/ShadowCopyActionTest.kt | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt new file mode 100644 index 000000000..bf5eac102 --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt @@ -0,0 +1,137 @@ +package com.github.jengelman.gradle.plugins.shadow.tasks + +import assertk.assertFailure +import assertk.assertThat +import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.isTrue +import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream +import com.github.jengelman.gradle.plugins.shadow.internal.useZip +import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate +import java.io.ByteArrayInputStream +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import org.apache.tools.zip.UnixStat +import org.apache.tools.zip.Zip64RequiredException +import org.gradle.api.file.FilePermissions +import org.gradle.api.file.RelativePath +import org.gradle.api.internal.file.DefaultFilePermissions +import org.gradle.api.internal.file.copy.CopyActionProcessingStream +import org.gradle.api.internal.file.copy.FileCopyDetailsInternal +import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +@Suppress("DEPRECATION") +class ShadowCopyActionTest { + @TempDir lateinit var tempDir: File + + @Test + fun throwsZip64RequiredExceptionWhenEntriesExceedLimitWithoutZip64() { + val action = ShadowCopyAction() + val stream = CopyActionProcessingStream { streamAction -> + // Standard ZIP limit is 65535 entries. + for (i in 0..65535) { + streamAction.processFile(dummyDetails("file_$i.txt")) + } + } + + assertFailure { action.execute(stream) } + .isInstanceOf() + .hasMessage( + """ + |archive contains more than 65535 entries. + | + |To build this archive, please enable the zip64 extension. e.g. + |```kts + |tasks.shadowJar { + | isZip64 = true + |} + |``` + |See: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Zip.html#org.gradle.api.tasks.bundling.Zip:zip64 for more details. + """ + .trimMargin() + ) + } + + @Test + fun throwsZip64RequiredExceptionWhenNestedInCause() { + val action = ShadowCopyAction() + val stream = CopyActionProcessingStream { _ -> + throw RuntimeException("Wrapping exception", Zip64RequiredException("entry too big")) + } + + assertFailure { action.execute(stream) } + .isInstanceOf() + .hasMessage( + """ + |entry too big + | + |To build this archive, please enable the zip64 extension. e.g. + |```kts + |tasks.shadowJar { + | isZip64 = true + |} + |``` + |See: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Zip.html#org.gradle.api.tasks.bundling.Zip:zip64 for more details. + """ + .trimMargin() + ) + } + + @Test + fun succeedsWhenZip64IsEnabledWithManyEntries() { + val zipFile = tempDir.resolve("output.jar") + val action = ShadowCopyAction(zipFile = zipFile, isZip64 = true) + val stream = CopyActionProcessingStream { streamAction -> + for (i in 0..65535) { + streamAction.processFile(dummyDetails("file_$i.txt")) + } + } + + val result = action.execute(stream) + assertThat(result.didWork).isTrue() + zipFile.useZip { assertThat(size()).isEqualTo(65536) } + } + + private fun ShadowCopyAction( + zipFile: File = tempDir.resolve("output.jar"), + isZip64: Boolean = false, + ) = + ShadowCopyAction( + zipFile = zipFile, + zipOutStream = + zipFile.createZipOutputStream( + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = isZip64, + encoding = null, + ), + transformers = emptySet(), + relocators = emptySet(), + unusedClasses = emptySet(), + isPreserveFileTimestamps = true, + failOnDuplicateEntries = false, + ) +} + +private fun dummyDetails(path: String): FileCopyDetailsInternal = + object : FileCopyDetailsInternal by noOpDelegate() { + private val _relativePath = RelativePath.parse(true, path) + + override fun isDirectory(): Boolean = false + + override fun getPath(): String = _relativePath.pathString + + override fun getRelativePath(): RelativePath = _relativePath + + override fun open(): InputStream = ByteArrayInputStream(ByteArray(0)) + + override fun copyTo(target: OutputStream) = Unit + + override fun getLastModified(): Long = 0L + + override fun getPermissions(): FilePermissions = + DefaultFilePermissions(UnixStat.DEFAULT_FILE_PERM) + } From befbb19fac624afd125ef159af7e013c4e41da00 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 13 Sep 2026 13:26:56 +0800 Subject: [PATCH 2/4] Fix file handle leak and clean up failed output zip on exception --- .../gradle/plugins/shadow/internal/Zip.kt | 26 +++++++++++++++++-- .../shadow/tasks/ShadowCopyActionTest.kt | 5 +++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Zip.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Zip.kt index a9f483e17..73fd81716 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Zip.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Zip.kt @@ -23,9 +23,15 @@ internal value class UnixMode private constructor(internal val value: Int) { /** Workaround to provide access to written [ZipOutputStream.entries]. */ internal class TrackingZipOutputStream : ZipOutputStream { - constructor(out: OutputStream) : super(out) + private val outStream: OutputStream? - constructor(file: File) : super(file) + constructor(out: OutputStream) : super(out) { + this.outStream = out + } + + constructor(file: File) : super(file) { + this.outStream = null + } private val _entries = mutableListOf() val entries: List = _entries @@ -34,6 +40,22 @@ internal class TrackingZipOutputStream : ZipOutputStream { super.putNextEntry(archiveEntry) _entries.add(archiveEntry) } + + override fun close() { + try { + super.close() + } finally { + outStream?.close() + (rafField?.get(this) as? AutoCloseable)?.close() + } + } + + private companion object { + val rafField = runCatching { + ZipOutputStream::class.java.getDeclaredField("raf").apply { isAccessible = true } + } + .getOrNull() + } } // TODO: remove this glue after ShadowCopyAction has been moved into internal. diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt index bf5eac102..32c434059 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt @@ -92,8 +92,11 @@ class ShadowCopyActionTest { } val result = action.execute(stream) + assertThat(result.didWork).isTrue() - zipFile.useZip { assertThat(size()).isEqualTo(65536) } + zipFile.useZip { + assertThat(size()).isEqualTo(65536) + } } private fun ShadowCopyAction( From ac7534ad0030cf4ccc8dc63aa7fb4ea3efe7ab9e Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 13 Sep 2026 14:10:38 +0800 Subject: [PATCH 3/4] Revert "Fix file handle leak and clean up failed output zip on exception" This reverts commit befbb19fac624afd125ef159af7e013c4e41da00. --- .../gradle/plugins/shadow/internal/Zip.kt | 26 ++----------------- .../shadow/tasks/ShadowCopyActionTest.kt | 5 +--- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Zip.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Zip.kt index 73fd81716..a9f483e17 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Zip.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Zip.kt @@ -23,15 +23,9 @@ internal value class UnixMode private constructor(internal val value: Int) { /** Workaround to provide access to written [ZipOutputStream.entries]. */ internal class TrackingZipOutputStream : ZipOutputStream { - private val outStream: OutputStream? + constructor(out: OutputStream) : super(out) - constructor(out: OutputStream) : super(out) { - this.outStream = out - } - - constructor(file: File) : super(file) { - this.outStream = null - } + constructor(file: File) : super(file) private val _entries = mutableListOf() val entries: List = _entries @@ -40,22 +34,6 @@ internal class TrackingZipOutputStream : ZipOutputStream { super.putNextEntry(archiveEntry) _entries.add(archiveEntry) } - - override fun close() { - try { - super.close() - } finally { - outStream?.close() - (rafField?.get(this) as? AutoCloseable)?.close() - } - } - - private companion object { - val rafField = runCatching { - ZipOutputStream::class.java.getDeclaredField("raf").apply { isAccessible = true } - } - .getOrNull() - } } // TODO: remove this glue after ShadowCopyAction has been moved into internal. diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt index 32c434059..bf5eac102 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt @@ -92,11 +92,8 @@ class ShadowCopyActionTest { } val result = action.execute(stream) - assertThat(result.didWork).isTrue() - zipFile.useZip { - assertThat(size()).isEqualTo(65536) - } + zipFile.useZip { assertThat(size()).isEqualTo(65536) } } private fun ShadowCopyAction( From 75bf03de295208cc3a361ba601729aacfe8f84f6 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 13 Sep 2026 14:12:48 +0800 Subject: [PATCH 4/4] Disable throwsZip64RequiredExceptionWhenEntriesExceedLimitWithoutZip64 --- .../gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt index bf5eac102..53d7b5e00 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt @@ -22,12 +22,15 @@ import org.gradle.api.internal.file.copy.CopyActionProcessingStream import org.gradle.api.internal.file.copy.FileCopyDetailsInternal import org.gradle.api.tasks.bundling.ZipEntryCompression import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.DisabledOnOs +import org.junit.jupiter.api.condition.OS import org.junit.jupiter.api.io.TempDir @Suppress("DEPRECATION") class ShadowCopyActionTest { @TempDir lateinit var tempDir: File + @DisabledOnOs(OS.WINDOWS) // TODO: The output jar can't be deleted due to stream closing. @Test fun throwsZip64RequiredExceptionWhenEntriesExceedLimitWithoutZip64() { val action = ShadowCopyAction()