Repository: GradleUp/shadow
Pull request #1245 (2025-02-08 10:22:02)

Improve readability


Patch:
diff --git a/api/shadow.api b/api/shadow.api
index ce7e384c..9df15508 100644
--- a/api/shadow.api
+++ b/api/shadow.api
@@ -244,15 +244,15 @@ public class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction$A
 	public fun getRelativePath ()Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction$RelativeArchivePath;
 	public synthetic fun getRelativePath ()Lorg/gradle/api/file/RelativePath;
 	public fun getSize ()J
-	public fun isClassFile ()Z
+	public fun isClass ()Z
 	public fun isDirectory ()Z
 	public fun open ()Ljava/io/InputStream;
 }
 
 public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction$BaseStreamAction : org/gradle/api/internal/file/CopyActionProcessingStreamAction {
 	public fun <init> ()V
-	protected final fun isArchive (Lorg/gradle/api/file/FileCopyDetails;)Z
-	protected final fun isClass (Lorg/gradle/api/file/FileCopyDetails;)Z
+	protected fun isClass (Lorg/gradle/api/file/FileCopyDetails;)Z
+	protected fun isJar (Lorg/gradle/api/file/FileCopyDetails;)Z
 	public fun processFile (Lorg/gradle/api/internal/file/copy/FileCopyDetailsInternal;)V
 	protected fun visitDir (Lorg/gradle/api/file/FileCopyDetails;)V
 	protected abstract fun visitFile (Lorg/gradle/api/file/FileCopyDetails;)V
@@ -263,14 +263,14 @@ public final class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAc
 }
 
 public class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction$RelativeArchivePath : org/gradle/api/file/RelativePath {
-	public fun <init> (Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction;Lorg/apache/tools/zip/ZipEntry;)V
+	public fun <init> (Lorg/apache/tools/zip/ZipEntry;Z)V
 	public final fun charAt (I)C
 	public fun get (I)C
 	public fun getEntry ()Lorg/apache/tools/zip/ZipEntry;
 	public fun getLength ()I
 	public fun getParent ()Lcom/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction$RelativeArchivePath;
 	public synthetic fun getParent ()Lorg/gradle/api/file/RelativePath;
-	public fun isClassFile ()Z
+	public fun isClass ()Z
 	public final fun length ()I
 }
 
@@ -583,7 +583,6 @@ public final class com/github/jengelman/gradle/plugins/shadow/transformers/Trans
 	public final fun copy (Ljava/lang/String;Ljava/io/InputStream;Ljava/util/Set;Lcom/github/jengelman/gradle/plugins/shadow/ShadowStats;)Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;
 	public static synthetic fun copy$default (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;Ljava/lang/String;Ljava/io/InputStream;Ljava/util/Set;Lcom/github/jengelman/gradle/plugins/shadow/ShadowStats;ILjava/lang/Object;)Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;
 	public fun equals (Ljava/lang/Object;)Z
-	public static final fun getEntryTimestamp (ZJ)J
 	public final fun getInputStream ()Ljava/io/InputStream;
 	public final fun getPath ()Ljava/lang/String;
 	public final fun getRelocators ()Ljava/util/Set;
@@ -603,7 +602,6 @@ public final class com/github/jengelman/gradle/plugins/shadow/transformers/Trans
 
 public final class com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext$Companion {
 	public final fun builder ()Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext$Builder;
-	public final fun getEntryTimestamp (ZJ)J
 }
 
 public class com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/Transformer {
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RealStreamAction.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RealStreamAction.kt
new file mode 100644
index 00000000..fe095fcf
--- /dev/null
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RealStreamAction.kt
@@ -0,0 +1,250 @@
+package com.github.jengelman.gradle.plugins.shadow.internal
+
+import com.github.jengelman.gradle.plugins.shadow.ShadowStats
+import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction.ArchiveFileTreeElement
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction.BaseStreamAction
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction.RelativeArchivePath
+import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
+import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
+import java.io.File
+import java.io.InputStream
+import java.util.zip.ZipException
+import org.apache.tools.zip.UnixStat
+import org.apache.tools.zip.ZipFile
+import org.apache.tools.zip.ZipOutputStream
+import org.gradle.api.GradleException
+import org.gradle.api.file.FileCopyDetails
+import org.gradle.api.file.FileTreeElement
+import org.gradle.api.file.RelativePath
+import org.gradle.api.tasks.util.PatternSet
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.commons.ClassRemapper
+import org.slf4j.Logger
+
+internal class RealStreamAction(
+  private val zipOutStr: ZipOutputStream,
+  encoding: String?,
+  private val transformers: Set<Transformer>,
+  private val relocators: Set<Relocator>,
+  private val patternSet: PatternSet,
+  private val unused: Set<String>,
+  private val stats: ShadowStats,
+  private val zipFile: File,
+  private val preserveFileTimestamps: Boolean,
+  private val logger: Logger,
+) : BaseStreamAction() {
+  private val remapper = RelocatorRemapper(relocators, stats)
+  private val visitedFiles = mutableSetOf<String>()
+
+  init {
+    if (encoding != null) {
+      this.zipOutStr.setEncoding(encoding)
+    }
+  }
+
+  override fun visitFile(fileDetails: FileCopyDetails) {
+    if (fileDetails.isJar) {
+      processArchive(fileDetails)
+    } else {
+      try {
+        val isClass = fileDetails.isClass
+        if (!remapper.hasRelocators() || !isClass) {
+          if (isTransformable(fileDetails)) {
+            transform(fileDetails)
+          } else {
+            val mappedPath = remapper.map(fileDetails.relativePath.pathString)
+            val entry = zipEntry(mappedPath, preserveFileTimestamps, fileDetails.lastModified) {
+              unixMode = UnixStat.FILE_FLAG or fileDetails.permissions.toUnixNumeric()
+            }
+            zipOutStr.putNextEntry(entry)
+            fileDetails.copyTo(zipOutStr)
+            zipOutStr.closeEntry()
+          }
+        } else if (isClass && !isUnused(fileDetails.path)) {
+          remapClass(fileDetails)
+        }
+        recordVisit(fileDetails.relativePath)
+      } catch (e: Exception) {
+        throw GradleException("Could not add $fileDetails to ZIP '$zipFile'.", e)
+      }
+    }
+  }
+
+  override fun visitDir(dirDetails: FileCopyDetails) {
+    try {
+      // Trailing slash in name indicates that entry is a directory.
+      val path = dirDetails.relativePath.pathString + "/"
+      val entry = zipEntry(path, preserveFileTimestamps, dirDetails.lastModified) {
+        unixMode = UnixStat.DIR_FLAG or dirDetails.permissions.toUnixNumeric()
+      }
+      zipOutStr.putNextEntry(entry)
+      zipOutStr.closeEntry()
+      recordVisit(dirDetails.relativePath)
+    } catch (e: Exception) {
+      throw GradleException("Could not add $dirDetails to ZIP '$zipFile'.", e)
+    }
+  }
+
+  private fun recordVisit(path: RelativePath): Boolean {
+    return visitedFiles.add(path.pathString)
+  }
+
+  private fun processArchive(fileDetails: FileCopyDetails) {
+    stats.startJar()
+    ZipFile(fileDetails.file).use { archive ->
+      archive.entries.asSequence()
+        .map {
+          ArchiveFileTreeElement(RelativeArchivePath(it, preserveFileTimestamps))
+        }
+        .filter {
+          patternSet.asSpec.isSatisfiedBy(it.asFileTreeElement())
+        }.forEach { archiveElement ->
+          if (archiveElement.relativePath.isFile) {
+            visitArchiveFile(archiveElement, archive)
+          }
+        }
+    }
+    stats.finishJar()
+  }
+
+  private fun visitArchiveDirectory(archiveDir: RelativeArchivePath) {
+    if (recordVisit(archiveDir)) {
+      zipOutStr.putNextEntry(archiveDir.entry)
+      zipOutStr.closeEntry()
+    }
+  }
+
+  private fun visitArchiveFile(archiveFile: ArchiveFileTreeElement, archive: ZipFile) {
+    val archiveFilePath = archiveFile.relativePath
+    if (archiveFile.isClass || !isTransformable(archiveFile)) {
+      if (recordVisit(archiveFilePath) && !isUnused(archiveFilePath.entry.name)) {
+        if (!remapper.hasRelocators() || !archiveFile.isClass) {
+          copyArchiveEntry(archiveFilePath, archive)
+        } else {
+          remapClass(archiveFilePath, archive)
+        }
+      }
+    } else {
+      transform(archiveFile, archive)
+    }
+  }
+
+  private fun addParentDirectories(file: RelativeArchivePath?) {
+    file?.let {
+      addParentDirectories(it.parent)
+      if (!it.isFile) {
+        visitArchiveDirectory(it)
+      }
+    }
+  }
+
+  private fun isUnused(classPath: String): Boolean {
+    val classPathWithoutExtension = classPath.substringBeforeLast(".")
+    val className = classPathWithoutExtension.replace('/', '.')
+    val result = unused.contains(className)
+    if (result) {
+      logger.debug("Dropping unused class: $className")
+    }
+    return result
+  }
+
+  private fun remapClass(file: RelativeArchivePath, archive: ZipFile) {
+    if (file.isClass) {
+      val entry = zipEntry(remapper.mapPath(file) + CLASS_SUFFIX, preserveFileTimestamps)
+      addParentDirectories(RelativeArchivePath(entry, preserveFileTimestamps))
+      remapClass(archive.getInputStream(file.entry), file.pathString, file.entry.time)
+    }
+  }
+
+  private fun remapClass(fileCopyDetails: FileCopyDetails) {
+    if (fileCopyDetails.isClass) {
+      fileCopyDetails.file.inputStream().use {
+        remapClass(it, fileCopyDetails.path, fileCopyDetails.lastModified)
+      }
+    }
+  }
+
+  /**
+   * Applies remapping to the given class with the specified relocation path. The remapped class is then written
+   * to the zip file. [classInputStream] is closed automatically to prevent future file leaks.
+   * See #364 and #408.
+   */
+  private fun remapClass(classInputStream: InputStream, path: String, lastModified: Long) {
+    // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool.
+    // Copying the original constant pool should be avoided because it would keep references
+    // to the original class names. This is not a problem at runtime (because these entries in the
+    // constant pool are never used), but confuses some tools such as Felix's maven-bundle-plugin
+    // that use the constant pool to determine the dependencies of a class.
+    val cw = ClassWriter(0)
+    val cr = ClassReader(classInputStream)
+    val cv = ClassRemapper(cw, remapper)
+
+    try {
+      cr.accept(cv, ClassReader.EXPAND_FRAMES)
+    } catch (t: Throwable) {
+      throw GradleException("Error in ASM processing class $path", t)
+    }
+
+    val renamedClass = cw.toByteArray()
+    // Temporarily remove the multi-release prefix.
+    val multiReleasePrefix = "^META-INF/versions/\\d+/".toRegex().find(path)?.value.orEmpty()
+    val newPath = path.replace(multiReleasePrefix, "")
+    val mappedName = multiReleasePrefix + remapper.mapPath(newPath)
+    try {
+      // Now we put it back on so the class file is written out with the right extension.
+      zipOutStr.putNextEntry(zipEntry("$mappedName.class", preserveFileTimestamps, lastModified))
+      renamedClass.inputStream().use {
+        it.copyTo(zipOutStr)
+      }
+      zipOutStr.closeEntry()
+    } catch (_: ZipException) {
+      logger.warn("We have a duplicate $mappedName in source project")
+    }
+  }
+
+  private fun copyArchiveEntry(archiveFile: RelativeArchivePath, archive: ZipFile) {
+    val mappedPath = remapper.map(archiveFile.entry.name)
+    val entry = zipEntry(mappedPath, preserveFileTimestamps, archiveFile.entry.time)
+    val mappedFile = RelativeArchivePath(entry, preserveFileTimestamps)
+    addParentDirectories(mappedFile)
+    zipOutStr.putNextEntry(mappedFile.entry)
+    archive.getInputStream(archiveFile.entry).use {
+      it.copyTo(zipOutStr)
+    }
+    zipOutStr.closeEntry()
+  }
+
+  private fun transform(element: ArchiveFileTreeElement, archive: ZipFile) {
+    transformAndClose(element, archive.getInputStream(element.relativePath.entry))
+  }
+
+  private fun transform(details: FileCopyDetails) {
+    transformAndClose(details, details.file.inputStream())
+  }
+
+  private fun transformAndClose(element: FileTreeElement, inputStream: InputStream) {
+    inputStream.use { steam ->
+      val mappedPath = remapper.map(element.relativePath.pathString)
+      transformers.find {
+        it.canTransformResource(element)
+      }?.transform(
+        TransformerContext(
+          path = mappedPath,
+          inputStream = steam,
+          relocators = relocators,
+          stats = stats,
+        ),
+      )
+    }
+  }
+
+  private fun isTransformable(element: FileTreeElement): Boolean {
+    return transformers.any { it.canTransformResource(element) }
+  }
+
+  companion object {
+    const val CLASS_SUFFIX = ".class"
+  }
+}
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Utils.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Utils.kt
index f8265f3f..8c90053a 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Utils.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Utils.kt
@@ -1,5 +1,6 @@
 package com.github.jengelman.gradle.plugins.shadow.internal
 
+import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
 import java.io.File
@@ -9,8 +10,8 @@ import java.nio.file.NoSuchFileException
 import java.nio.file.Path
 import java.util.Properties
 import java.util.jar.Attributes.Name as JarAttributeName
-import kotlin.io.path.inputStream
 import kotlin.io.path.toPath
+import org.apache.tools.zip.ZipEntry
 import org.gradle.api.file.RelativePath
 import org.gradle.api.internal.file.DefaultFileTreeElement
 import org.gradle.internal.file.Chmod
@@ -32,6 +33,22 @@ internal val classPathAttributeKey = JarAttributeName.CLASS_PATH.toString()
  */
 internal val multiReleaseAttributeKey = JarAttributeName.MULTI_RELEASE.toString()
 
+internal inline fun zipEntry(
+  name: String,
+  preserveLastModified: Boolean = true,
+  lastModified: Long = -1,
+  block: ZipEntry.() -> Unit = {},
+): ZipEntry = ZipEntry(name).apply {
+  if (preserveLastModified) {
+    if (lastModified >= 0) {
+      time = lastModified
+    }
+  } else {
+    time = ShadowCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES
+  }
+  block()
+}
+
 /**
  * This is used for creating a [DefaultFileTreeElement] with default values.
  * [file], [chmod], and [stat] should be non-null, so they are set to dummy values here.
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt
index 1aa537bc..404f5710 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt
@@ -1,23 +1,21 @@
 package com.github.jengelman.gradle.plugins.shadow.tasks
 
 import com.github.jengelman.gradle.plugins.shadow.ShadowStats
-import com.github.jengelman.gradle.plugins.shadow.internal.RelocatorRemapper
+import com.github.jengelman.gradle.plugins.shadow.internal.RealStreamAction
+import com.github.jengelman.gradle.plugins.shadow.internal.RealStreamAction.Companion.CLASS_SUFFIX
 import com.github.jengelman.gradle.plugins.shadow.internal.UnusedTracker
 import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor
 import com.github.jengelman.gradle.plugins.shadow.internal.createDefaultFileTreeElement
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
 import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer
-import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
 import java.io.File
 import java.io.IOException
 import java.io.InputStream
 import java.io.OutputStream
 import java.util.GregorianCalendar
-import java.util.zip.ZipException
-import org.apache.tools.zip.UnixStat
 import org.apache.tools.zip.Zip64RequiredException
 import org.apache.tools.zip.ZipEntry
-import org.apache.tools.zip.ZipFile
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.GradleException
 import org.gradle.api.file.FileCopyDetails
@@ -34,9 +32,6 @@ import org.gradle.api.tasks.WorkResult
 import org.gradle.api.tasks.WorkResults
 import org.gradle.api.tasks.bundling.Zip
 import org.gradle.api.tasks.util.PatternSet
-import org.objectweb.asm.ClassReader
-import org.objectweb.asm.ClassWriter
-import org.objectweb.asm.commons.ClassRemapper
 import org.slf4j.LoggerFactory
 
 public open class ShadowCopyAction internal constructor(
@@ -84,7 +79,7 @@ public open class ShadowCopyAction internal constructor(
         object : BaseStreamAction() {
           override fun visitFile(fileDetails: FileCopyDetails) {
             // All project sources are already present, we just need to deal with JAR dependencies.
-            if (isArchive(fileDetails)) {
+            if (fileDetails.isJar) {
               unusedTracker.addDependency(fileDetails.file)
             }
           }
@@ -104,7 +99,7 @@ public open class ShadowCopyAction internal constructor(
     try {
       zipOutStream.use { outputStream ->
         stream.process(
-          StreamAction(
+          RealStreamAction(
             outputStream,
             encoding,
             transformers,
@@ -112,6 +107,9 @@ public open class ShadowCopyAction internal constructor(
             patternSet,
             unusedClasses,
             stats,
+            zipFile,
+            preserveFileTimestamps,
+            logger,
           ),
         )
         processTransformers(outputStream)
@@ -137,265 +135,27 @@ public open class ShadowCopyAction internal constructor(
     }
   }
 
-  private fun getArchiveTimeFor(timestamp: Long): Long {
-    return if (preserveFileTimestamps) timestamp else CONSTANT_TIME_FOR_ZIP_ENTRIES
-  }
-
-  private fun setArchiveTimes(zipEntry: ZipEntry): ZipEntry {
-    if (!preserveFileTimestamps) {
-      zipEntry.time = CONSTANT_TIME_FOR_ZIP_ENTRIES
-    }
-    return zipEntry
-  }
-
   public abstract class BaseStreamAction : CopyActionProcessingStreamAction {
-    protected fun isArchive(fileDetails: FileCopyDetails): Boolean {
-      return fileDetails.relativePath.pathString.endsWith(".jar")
-    }
-
-    protected fun isClass(fileDetails: FileCopyDetails): Boolean {
-      return fileDetails.path.endsWith(".class")
-    }
-
     override fun processFile(details: FileCopyDetailsInternal) {
       if (details.isDirectory) visitDir(details) else visitFile(details)
     }
 
     protected open fun visitDir(dirDetails: FileCopyDetails) {}
-
     protected abstract fun visitFile(fileDetails: FileCopyDetails)
-  }
-
-  private inner class StreamAction(
-    private val zipOutStr: ZipOutputStream,
-    encoding: String?,
-    private val transformers: Set<Transformer>,
-    private val relocators: Set<Relocator>,
-    private val patternSet: PatternSet,
-    private val unused: Set<String>,
-    private val stats: ShadowStats,
-  ) : BaseStreamAction() {
-    private val remapper = RelocatorRemapper(relocators, stats)
-    private val visitedFiles = mutableSetOf<String>()
-
-    init {
-      if (encoding != null) {
-        this.zipOutStr.setEncoding(encoding)
-      }
-    }
-
-    override fun visitFile(fileDetails: FileCopyDetails) {
-      if (!isArchive(fileDetails)) {
-        try {
-          val isClass = isClass(fileDetails)
-          if (!remapper.hasRelocators() || !isClass) {
-            if (!isTransformable(fileDetails)) {
-              val mappedPath = remapper.map(fileDetails.relativePath.pathString)
-              val archiveEntry = ZipEntry(mappedPath)
-              archiveEntry.time = getArchiveTimeFor(fileDetails.lastModified)
-              archiveEntry.unixMode = UnixStat.FILE_FLAG or fileDetails.permissions.toUnixNumeric()
-              zipOutStr.putNextEntry(archiveEntry)
-              fileDetails.copyTo(zipOutStr)
-              zipOutStr.closeEntry()
-            } else {
-              transform(fileDetails)
-            }
-          } else if (isClass && !isUnused(fileDetails.path)) {
-            remapClass(fileDetails)
-          }
-          recordVisit(fileDetails.relativePath)
-        } catch (e: Exception) {
-          throw GradleException("Could not add $fileDetails to ZIP '$zipFile'.", e)
-        }
-      } else {
-        processArchive(fileDetails)
-      }
-    }
-
-    private fun recordVisit(path: RelativePath): Boolean {
-      return visitedFiles.add(path.pathString)
-    }
-
-    private fun processArchive(fileDetails: FileCopyDetails) {
-      stats.startJar()
-      ZipFile(fileDetails.file).use { archive ->
-        archive.entries.asSequence()
-          .map {
-            ArchiveFileTreeElement(RelativeArchivePath(it))
-          }
-          .filter {
-            patternSet.asSpec.isSatisfiedBy(it.asFileTreeElement())
-          }.forEach { archiveElement ->
-            if (archiveElement.relativePath.isFile) {
-              visitArchiveFile(archiveElement, archive)
-            }
-          }
-      }
-      stats.finishJar()
-    }
-
-    private fun visitArchiveDirectory(archiveDir: RelativeArchivePath) {
-      if (recordVisit(archiveDir)) {
-        zipOutStr.putNextEntry(archiveDir.entry)
-        zipOutStr.closeEntry()
-      }
-    }
-
-    private fun visitArchiveFile(archiveFile: ArchiveFileTreeElement, archive: ZipFile) {
-      val archiveFilePath = archiveFile.relativePath
-      if (archiveFile.isClassFile || !isTransformable(archiveFile)) {
-        if (recordVisit(archiveFilePath) && !isUnused(archiveFilePath.entry.name)) {
-          if (!remapper.hasRelocators() || !archiveFile.isClassFile) {
-            copyArchiveEntry(archiveFilePath, archive)
-          } else {
-            remapClass(archiveFilePath, archive)
-          }
-        }
-      } else {
-        transform(archiveFile, archive)
-      }
-    }
 
-    private fun addParentDirectories(file: RelativeArchivePath?) {
-      file?.let {
-        addParentDirectories(it.parent)
-        if (!it.isFile) {
-          visitArchiveDirectory(it)
-        }
-      }
-    }
-
-    private fun isUnused(classPath: String): Boolean {
-      val classPathWithoutExtension = classPath.substringBeforeLast(".")
-      val className = classPathWithoutExtension.replace('/', '.')
-      val result = unused.contains(className)
-      if (result) {
-        logger.debug("Dropping unused class: $className")
-      }
-      return result
-    }
-
-    private fun remapClass(file: RelativeArchivePath, archive: ZipFile) {
-      if (file.isClassFile) {
-        val zipEntry = setArchiveTimes(ZipEntry(remapper.mapPath(file) + ".class"))
-        addParentDirectories(RelativeArchivePath(zipEntry))
-        remapClass(archive.getInputStream(file.entry), file.pathString, file.entry.time)
-      }
-    }
-
-    private fun remapClass(fileCopyDetails: FileCopyDetails) {
-      if (fileCopyDetails.name.endsWith(".class")) {
-        fileCopyDetails.file.inputStream().use {
-          remapClass(it, fileCopyDetails.path, fileCopyDetails.lastModified)
-        }
-      }
-    }
-
-    /**
-     * Applies remapping to the given class with the specified relocation path. The remapped class is then written
-     * to the zip file. [classInputStream] is closed automatically to prevent future file leaks.
-     * See #364 and #408.
-     */
-    private fun remapClass(classInputStream: InputStream, path: String, lastModified: Long) {
-      // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool.
-      // Copying the original constant pool should be avoided because it would keep references
-      // to the original class names. This is not a problem at runtime (because these entries in the
-      // constant pool are never used), but confuses some tools such as Felix's maven-bundle-plugin
-      // that use the constant pool to determine the dependencies of a class.
-      val cw = ClassWriter(0)
-      val cr = ClassReader(classInputStream)
-      val cv = ClassRemapper(cw, remapper)
-
-      try {
-        cr.accept(cv, ClassReader.EXPAND_FRAMES)
-      } catch (t: Throwable) {
-        throw GradleException("Error in ASM processing class $path", t)
-      }
-
-      val renamedClass = cw.toByteArray()
-      // Temporarily remove the multi-release prefix.
-      val multiReleasePrefix = "^META-INF/versions/\\d+/".toRegex().find(path)?.value.orEmpty()
-      val newPath = path.replace(multiReleasePrefix, "")
-      val mappedName = multiReleasePrefix + remapper.mapPath(newPath)
-      try {
-        // Now we put it back on so the class file is written out with the right extension.
-        val archiveEntry = ZipEntry("$mappedName.class")
-        archiveEntry.time = getArchiveTimeFor(lastModified)
-        zipOutStr.putNextEntry(archiveEntry)
-        renamedClass.inputStream().use {
-          it.copyTo(zipOutStr)
-        }
-        zipOutStr.closeEntry()
-      } catch (ignored: ZipException) {
-        logger.warn("We have a duplicate $mappedName in source project")
-      }
-    }
-
-    private fun copyArchiveEntry(archiveFile: RelativeArchivePath, archive: ZipFile) {
-      val mappedPath = remapper.map(archiveFile.entry.name)
-      val entry = ZipEntry(mappedPath)
-      entry.time = getArchiveTimeFor(archiveFile.entry.time)
-      val mappedFile = RelativeArchivePath(entry)
-      addParentDirectories(mappedFile)
-      zipOutStr.putNextEntry(mappedFile.entry)
-      archive.getInputStream(archiveFile.entry).use {
-        it.copyTo(zipOutStr)
-      }
-      zipOutStr.closeEntry()
-    }
-
-    override fun visitDir(dirDetails: FileCopyDetails) {
-      try {
-        // Trailing slash in name indicates that entry is a directory.
-        val path = dirDetails.relativePath.pathString + "/"
-        val archiveEntry = ZipEntry(path)
-        archiveEntry.time = getArchiveTimeFor(dirDetails.lastModified)
-        archiveEntry.unixMode = UnixStat.DIR_FLAG or dirDetails.permissions.toUnixNumeric()
-        zipOutStr.putNextEntry(archiveEntry)
-        zipOutStr.closeEntry()
-        recordVisit(dirDetails.relativePath)
-      } catch (e: Exception) {
-        throw GradleException("Could not add $dirDetails to ZIP '$zipFile'.", e)
-      }
-    }
-
-    private fun transform(element: ArchiveFileTreeElement, archive: ZipFile) {
-      transformAndClose(element, archive.getInputStream(element.relativePath.entry))
-    }
-
-    private fun transform(details: FileCopyDetails) {
-      transformAndClose(details, details.file.inputStream())
-    }
-
-    private fun transformAndClose(element: FileTreeElement, inputStream: InputStream) {
-      inputStream.use { steam ->
-        val mappedPath = remapper.map(element.relativePath.pathString)
-        transformers.find {
-          it.canTransformResource(element)
-        }?.transform(
-          TransformerContext(
-            path = mappedPath,
-            inputStream = steam,
-            relocators = relocators,
-            stats = stats,
-          ),
-        )
-      }
-    }
-
-    private fun isTransformable(element: FileTreeElement): Boolean {
-      return transformers.any { it.canTransformResource(element) }
-    }
+    protected open val FileCopyDetails.isClass: Boolean get() = relativePath.pathString.endsWith(CLASS_SUFFIX)
+    protected open val FileCopyDetails.isJar: Boolean get() = relativePath.pathString.endsWith(".jar")
   }
 
-  public open inner class RelativeArchivePath(
+  public open class RelativeArchivePath(
     public open val entry: ZipEntry,
+    private val preserveFileTimestamps: Boolean,
   ) : RelativePath(
     !entry.isDirectory,
     // `dir/` will be split into ["dir", ""], we have to trim empty segments here.
     *entry.name.split('/').filter(CharSequence::isNotEmpty).toTypedArray(),
   ) {
-    public open val isClassFile: Boolean get() = lastName.endsWith(".class")
+    public open val isClass: Boolean get() = lastName.endsWith(CLASS_SUFFIX)
 
     @Suppress("WRONG_NULLABILITY_FOR_JAVA_OVERRIDE") // It could return null in super.getParent().
     override fun getParent(): RelativeArchivePath? {
@@ -404,8 +164,10 @@ public open class ShadowCopyAction internal constructor(
       } else {
         // Parent is always a directory so add / to the end of the path.
         val parentPath = segments.dropLast(1).joinToString("/") + "/"
-        val entry = setArchiveTimes(ZipEntry(parentPath))
-        RelativeArchivePath(entry)
+        RelativeArchivePath(
+          zipEntry(parentPath, preserveFileTimestamps),
+          preserveFileTimestamps,
+        )
       }
     }
   }
@@ -413,7 +175,7 @@ public open class ShadowCopyAction internal constructor(
   public open class ArchiveFileTreeElement(
     private val archivePath: RelativeArchivePath,
   ) : FileTreeElement {
-    public open val isClassFile: Boolean get() = archivePath.isClassFile
+    public open val isClass: Boolean get() = archivePath.isClass
 
     override fun isDirectory(): Boolean = archivePath.entry.isDirectory
 
@@ -437,11 +199,8 @@ public open class ShadowCopyAction internal constructor(
     }
 
     override fun getFile(): File = throw UnsupportedOperationException()
-
     override fun open(): InputStream = throw UnsupportedOperationException()
-
     override fun copyTo(outputStream: OutputStream): Unit = throw UnsupportedOperationException()
-
     override fun copyTo(file: File): Boolean = throw UnsupportedOperationException()
   }
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.kt
index 651456fa..e9fafdc5 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.kt
@@ -1,6 +1,7 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.internal.property
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import java.io.PrintWriter
 import java.nio.charset.Charset
 import java.text.SimpleDateFormat
@@ -8,7 +9,6 @@ import java.util.Date
 import java.util.Locale
 import java.util.TreeSet
 import javax.inject.Inject
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 import org.gradle.api.model.ObjectFactory
@@ -158,9 +158,7 @@ public open class ApacheNoticeResourceTransformer @Inject constructor(
   override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
     val copyright = copyright.orNull
 
-    val zipEntry = ZipEntry(NOTICE_PATH)
-    zipEntry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, zipEntry.time)
-    os.putNextEntry(zipEntry)
+    os.putNextEntry(zipEntry(NOTICE_PATH, preserveFileTimestamps))
 
     val writer = PrintWriter(os.writer(charset))
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.kt
index 7b3dd768..c0fdcfa8 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/AppendingTransformer.kt
@@ -1,9 +1,9 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.internal.property
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import java.io.ByteArrayOutputStream
 import javax.inject.Inject
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 import org.gradle.api.model.ObjectFactory
@@ -55,9 +55,7 @@ public open class AppendingTransformer @Inject constructor(
   }
 
   override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
-    val entry = ZipEntry(resource.get())
-    entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time)
-    os.putNextEntry(entry)
+    os.putNextEntry(zipEntry(resource.get(), preserveFileTimestamps))
 
     // Closing a ByteArrayOutputStream has no effect, so we don't use a use block here.
     data.toByteArray().inputStream().copyTo(os)
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformer.kt
index a2e391b7..d21bc44e 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ComponentsXmlResourceTransformer.kt
@@ -1,11 +1,10 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext
-import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext.Companion.getEntryTimestamp
 import java.io.BufferedInputStream
 import java.io.ByteArrayOutputStream
 import java.io.IOException
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.codehaus.plexus.util.xml.XmlStreamReader
 import org.codehaus.plexus.util.xml.XmlStreamWriter
@@ -79,9 +78,7 @@ public open class ComponentsXmlResourceTransformer : Transformer {
   }
 
   override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
-    val entry = ZipEntry(COMPONENTS_XML_PATH)
-    entry.time = getEntryTimestamp(preserveFileTimestamps, entry.time)
-    os.putNextEntry(entry)
+    os.putNextEntry(zipEntry(COMPONENTS_XML_PATH, preserveFileTimestamps))
     os.write(transformedResource)
     components.clear()
   }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.kt
index e001dc5b..2a1dec80 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.kt
@@ -1,9 +1,8 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.internal.inputStream
-import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext.Companion.getEntryTimestamp
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import java.util.Properties
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 
@@ -77,9 +76,7 @@ public open class GroovyExtensionModuleTransformer : Transformer {
 
   override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
     val name = if (legacy) PATH_LEGACY_GROOVY_EXTENSION_MODULE_DESCRIPTOR else PATH_GROOVY_EXTENSION_MODULE_DESCRIPTOR
-    val entry = ZipEntry(name)
-    entry.time = getEntryTimestamp(preserveFileTimestamps, entry.time)
-    os.putNextEntry(entry)
+    os.putNextEntry(zipEntry(name, preserveFileTimestamps))
     module.inputStream().use {
       it.copyTo(os)
     }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/IncludeResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/IncludeResourceTransformer.kt
index bd064616..c3f903b1 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/IncludeResourceTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/IncludeResourceTransformer.kt
@@ -1,9 +1,8 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.internal.property
-import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext.Companion.getEntryTimestamp
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import javax.inject.Inject
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.RegularFileProperty
 import org.gradle.api.model.ObjectFactory
@@ -34,9 +33,7 @@ public open class IncludeResourceTransformer @Inject constructor(
   override fun hasTransformedResource(): Boolean = file.get().asFile.exists()
 
   override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
-    val entry = ZipEntry(resource.get())
-    entry.time = getEntryTimestamp(preserveFileTimestamps, entry.time)
-    os.putNextEntry(entry)
+    os.putNextEntry(zipEntry(resource.get(), preserveFileTimestamps))
 
     file.get().asFile.inputStream().use { inputStream ->
       inputStream.copyTo(os)
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/Log4j2PluginsCacheFileTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/Log4j2PluginsCacheFileTransformer.kt
index c4784fcc..280b7a34 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/Log4j2PluginsCacheFileTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/Log4j2PluginsCacheFileTransformer.kt
@@ -1,9 +1,9 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.ShadowStats
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext
 import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
-import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext.Companion.getEntryTimestamp
 import java.net.URL
 import java.nio.file.Path
 import java.util.Collections
@@ -14,7 +14,6 @@ import kotlin.io.path.outputStream
 import org.apache.commons.io.output.CloseShieldOutputStream
 import org.apache.logging.log4j.core.config.plugins.processor.PluginCache
 import org.apache.logging.log4j.core.config.plugins.processor.PluginProcessor.PLUGIN_CACHE_FILE
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 
@@ -68,9 +67,7 @@ public open class Log4j2PluginsCacheFileTransformer : Transformer {
       val aggregator = PluginCache()
       aggregator.loadCacheFiles(urlEnumeration)
       relocatePlugins(aggregator)
-      val entry = ZipEntry(PLUGIN_CACHE_FILE)
-      entry.time = getEntryTimestamp(preserveFileTimestamps, entry.time)
-      os.putNextEntry(entry)
+      os.putNextEntry(zipEntry(PLUGIN_CACHE_FILE, preserveFileTimestamps))
       // Prevent the aggregator to close the jar output.
       aggregator.writeCache(CloseShieldOutputStream.wrap(os))
     } finally {
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestAppenderTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestAppenderTransformer.kt
index 8617b684..ee7a7e60 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestAppenderTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestAppenderTransformer.kt
@@ -1,11 +1,11 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.internal.setProperty
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import java.io.ByteArrayOutputStream
 import java.io.IOException
 import java.util.jar.JarFile.MANIFEST_NAME
 import javax.inject.Inject
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 import org.gradle.api.model.ObjectFactory
@@ -51,9 +51,7 @@ public open class ManifestAppenderTransformer @Inject constructor(
   override fun hasTransformedResource(): Boolean = attributes.get().isNotEmpty()
 
   override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
-    val entry = ZipEntry(MANIFEST_NAME)
-    entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time)
-    os.putNextEntry(entry)
+    os.putNextEntry(zipEntry(MANIFEST_NAME, preserveFileTimestamps))
     os.write(manifestContents)
 
     if (attributes.get().isNotEmpty()) {
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.kt
index 5c9c8dd7..2ca483e2 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.kt
@@ -2,13 +2,12 @@ package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.internal.mapProperty
 import com.github.jengelman.gradle.plugins.shadow.internal.property
-import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext.Companion.getEntryTimestamp
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import java.io.IOException
 import java.util.jar.Attributes
 import java.util.jar.JarFile
 import java.util.jar.Manifest
 import javax.inject.Inject
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 import org.gradle.api.model.ObjectFactory
@@ -78,9 +77,7 @@ public open class ManifestResourceTransformer @Inject constructor(
       attributes[Attributes.Name(key)] = value
     }
 
-    val entry = ZipEntry(JarFile.MANIFEST_NAME)
-    entry.time = getEntryTimestamp(preserveFileTimestamps, entry.time)
-    os.putNextEntry(entry)
+    os.putNextEntry(zipEntry(JarFile.MANIFEST_NAME, preserveFileTimestamps))
     manifest!!.write(os)
   }
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.kt
index 89cf3eab..1d5a076b 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.kt
@@ -5,12 +5,12 @@ import com.github.jengelman.gradle.plugins.shadow.internal.inputStream
 import com.github.jengelman.gradle.plugins.shadow.internal.mapProperty
 import com.github.jengelman.gradle.plugins.shadow.internal.property
 import com.github.jengelman.gradle.plugins.shadow.internal.setProperty
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import com.github.jengelman.gradle.plugins.shadow.transformers.PropertiesFileTransformer.MergeStrategy
 import java.io.InputStream
 import java.nio.charset.Charset
 import java.util.Properties
 import javax.inject.Inject
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 import org.gradle.api.model.ObjectFactory
@@ -221,9 +221,7 @@ public open class PropertiesFileTransformer @Inject constructor(
     // Cannot close the writer as the OutputStream needs to remain open.
     val zipWriter = os.writer(charset)
     propertiesEntries.forEach { (path, props) ->
-      val entry = ZipEntry(path)
-      entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time)
-      os.putNextEntry(entry)
+      os.putNextEntry(zipEntry(path, preserveFileTimestamps))
       props.inputStream(charset).bufferedReader(charset).use {
         it.copyTo(zipWriter)
       }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.kt
index 142e64f9..d633d933 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.kt
@@ -1,9 +1,9 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext
 import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction
 import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer.Companion.PATH_LEGACY_GROOVY_EXTENSION_MODULE_DESCRIPTOR
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 import org.gradle.api.tasks.Input
@@ -74,9 +74,7 @@ public open class ServiceFileTransformer(
 
   override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
     serviceEntries.forEach { (path, data) ->
-      val entry = ZipEntry(path)
-      entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time)
-      os.putNextEntry(entry)
+      os.putNextEntry(zipEntry(path, preserveFileTimestamps))
       os.write(data.joinToString("\n").toByteArray())
       os.closeEntry()
     }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.kt
index cefd226a..1ee33cd0 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.kt
@@ -2,7 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.ShadowStats
 import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
-import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction
 import java.io.InputStream
 
 public data class TransformerContext @JvmOverloads constructor(
@@ -32,10 +31,5 @@ public data class TransformerContext @JvmOverloads constructor(
   public companion object {
     @JvmStatic
     public fun builder(): Builder = Builder()
-
-    @JvmStatic
-    public fun getEntryTimestamp(preserveFileTimestamps: Boolean, entryTime: Long): Long {
-      return if (preserveFileTimestamps) entryTime else ShadowCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES
-    }
   }
 }
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.kt
index f60d034d..f201acbd 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/XmlAppendingTransformer.kt
@@ -1,9 +1,9 @@
 package com.github.jengelman.gradle.plugins.shadow.transformers
 
 import com.github.jengelman.gradle.plugins.shadow.internal.property
+import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
 import java.io.StringReader
 import javax.inject.Inject
-import org.apache.tools.zip.ZipEntry
 import org.apache.tools.zip.ZipOutputStream
 import org.gradle.api.file.FileTreeElement
 import org.gradle.api.model.ObjectFactory
@@ -75,9 +75,7 @@ public open class XmlAppendingTransformer @Inject constructor(
   override fun hasTransformedResource(): Boolean = doc != null
 
   override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
-    val entry = ZipEntry(resource.get())
-    entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time)
-    os.putNextEntry(entry)
+    os.putNextEntry(zipEntry(resource.get(), preserveFileTimestamps))
     XMLOutputter(Format.getPrettyFormat()).output(doc, os)
     doc = null
   }


Test patch:
diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerTest.kt
index 40ea4493..566dc5c7 100644
--- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerTest.kt
+++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformerTest.kt
@@ -68,7 +68,7 @@ class ApacheNoticeResourceTransformerTest : BaseTransformerTest<ApacheNoticeReso
           inputStream = noticeText.byteInputStream(),
         ),
       )
-    } catch (ignored: NullPointerException) {
+    } catch (_: NullPointerException) {
       fail("Null pointer should not be thrown when no parameters are set.")
     }
   }