From b79cdd247c9db955c57d6a8d362b387905aadf7d Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Sat, 17 Jun 2023 00:50:29 +0100 Subject: [PATCH] Fix writing zip64's offset ZipFileSystem's `sync` writes all the zip file entries, followed by the central directory ("cen") entries, counting the all `written` bytes, which it passes to write the end ("end") entry. ZipCentralDir's `dump` skips writing the file entries and only writes the cen entries, which means that its `written` is missing all the bytes of the file entries. Fortunately that missing value is recorded in the `end` as the offset where the cen entries start, so we fix `written` with `cenoff`. This bug only impacted writing zip64 jars, through ZipCentralDir. --- .../sbt/internal/inc/zip/ZipCentralDir.java | 8 +- .../classfile/IndexBasedZipFsOpsSpec.scala | 75 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 internal/zinc-classfile/src/test/scala/sbt/internal/inc/classfile/IndexBasedZipFsOpsSpec.scala diff --git a/internal/zinc-classfile/src/main/java/sbt/internal/inc/zip/ZipCentralDir.java b/internal/zinc-classfile/src/main/java/sbt/internal/inc/zip/ZipCentralDir.java index 7d9450983f..20fe9e1c98 100644 --- a/internal/zinc-classfile/src/main/java/sbt/internal/inc/zip/ZipCentralDir.java +++ b/internal/zinc-classfile/src/main/java/sbt/internal/inc/zip/ZipCentralDir.java @@ -111,7 +111,7 @@ public void dump(OutputStream os) throws IOException { } end.centot = elist.size(); end.cenlen = written; - end.write(os, written); + end.write(os, written + end.cenoff); } private List readEntries() throws IOException { @@ -242,6 +242,7 @@ private END findEND() throws IOException end.centot = (int)ZIP64_ENDTOT(end64buf); // assume total < 2g end.endpos = end64pos; } + log(end); return end; } } @@ -431,6 +432,8 @@ void write(OutputStream os, long offset) throws IOException { writeShort(os, 0); } } + + public String toString() { return String.format("END[@%07d #%05d %07dB]", cenoff, centot, cenlen); } } public static class Entry extends IndexNode { @@ -739,4 +742,7 @@ void readExtra(ZipCentralDir zipfs) throws IOException { } } + private static void log(Object x) { + //System.out.println(x); + } } diff --git a/internal/zinc-classfile/src/test/scala/sbt/internal/inc/classfile/IndexBasedZipFsOpsSpec.scala b/internal/zinc-classfile/src/test/scala/sbt/internal/inc/classfile/IndexBasedZipFsOpsSpec.scala new file mode 100644 index 0000000000..3402d18715 --- /dev/null +++ b/internal/zinc-classfile/src/test/scala/sbt/internal/inc/classfile/IndexBasedZipFsOpsSpec.scala @@ -0,0 +1,75 @@ +package sbt.internal.inc +package classfile + +import java.nio.file._ +import java.nio.file.spi.FileSystemProvider +import scala.collection.JavaConverters._ + +class IndexBasedZipFsOpsSpec extends UnitSpec { + private val XL = 0xffff // minimum size to be zip64, which I'm calling "XL" + private val L = XL - 1 // last size to be standard zip, which I'm calling "L" + private val tmpDir = Files.createTempDirectory("zinc-zipmergetest") + private lazy val zipFsProvider = + FileSystemProvider.installedProviders().stream().filter(_.getScheme == "jar").findAny().get() + + it should "create XS jars" in assertSize(createJar(2), 2) + it should "merge XS jars" in assertMerge(2, 1) + it should "shrink XS jars" in assertShrink(3, 2) + + it should "create L jars" in assertSize(createJar(L), L) + it should "merge L jars" in assertMerge(L, 1) // breach threshold + it should "shrink L jars" in assertShrink(L + 1, L) // breach threshold back + + it should "create XL jars" in assertSize(createJar(XL), XL) + it should "merge XL jars" in assertMerge(XL, 1) + it should "shrink XL jars" in assertShrink(XL + 1, XL) + + private def assertMerge(size1: Int, size2: Int) = { + val a = createJar(size1) + val b = createJar(size2, "b", size1) + safely(IndexBasedZipFsOps.mergeArchives(a, b)) + assertSize(a, size1 + size2) + } + + private def assertShrink(size1: Int, size2: Int) = { + val a = createJar(size1) + val files = for (i <- size2 until size1) yield classFileName(i) + safely(IndexBasedZipFsOps.removeEntries(a.toFile, files)) + assertSize(a, size2) + } + + private def assertSize(p: Path, size: Int) = { + val cen = safely(IndexBasedZipFsOps.readCentralDir(p.toFile)) + assert(cen.getHeaders.size() == size) + Files.delete(p) + } + + private def createJar(n: Int, name: String = "a", from: Int = 0) = { + val out = tmpDir.resolve(s"$name.jar") + val zipfs = zipFsProvider.newFileSystem(out, Map("create" -> "true").asJava) + val root = zipfs.getRootDirectories.iterator().next() + for (i <- from until (from + n)) { + val empty = root.resolve(classFileName(i)) + Files.write(empty, Array.emptyByteArray) + } + zipfs.close() + out + } + + private def classFileName(i: Int) = f"C$i%032d.class" + + private def safely[A](op: => A) = + try op + catch { case ex: java.util.zip.ZipError => throw new ZipException(ex) } +} + +// Avoid java.util.zip.ZipError, which is a VirtualMachineError!!?! +final class ZipException(val cause: Throwable) extends Exception + with scala.util.control.NoStackTrace { + override def toString: String = cause.toString + override def getCause: Throwable = cause.getCause + override def getMessage: String = cause.getMessage + override def getStackTrace: Array[StackTraceElement] = cause.getStackTrace + override def printStackTrace(s: java.io.PrintStream): Unit = cause.printStackTrace(s) + override def printStackTrace(s: java.io.PrintWriter): Unit = cause.printStackTrace(s) +}