1+ import org.apache.commons.compress.archivers.zip.Zip64Mode
2+ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
3+ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
14import org.eclipse.jgit.api.Git
25import org.slf4j.LoggerFactory
36import java.io.IOException
@@ -9,11 +12,13 @@ import java.nio.file.Files
912import java.nio.file.Path
1013import java.nio.file.Paths
1114import java.nio.file.StandardCopyOption
15+ import java.nio.file.attribute.FileTime
1216import java.nio.file.attribute.PosixFilePermission
1317import java.security.MessageDigest
1418import java.sql.Connection
1519import java.sql.DriverManager
1620import java.time.Duration
21+ import java.util.zip.CRC32
1722import java.util.zip.ZipEntry
1823import java.util.zip.ZipOutputStream
1924import kotlin.io.path.inputStream
@@ -227,6 +232,9 @@ data class FafDatabase(
227232 }
228233}
229234
235+ private const val MINIMUM_ZIP_DATE = 315532800000L // 1980-01-01
236+ private val MINIMUM_ZIP_FILE_TIME = FileTime .fromMillis(MINIMUM_ZIP_DATE )
237+
230238class Patcher (
231239 val patchVersion : Int ,
232240 val targetDir : Path ,
@@ -355,7 +363,11 @@ class Patcher(
355363
356364 private fun zipPreserveStructure (sources : List <Path >, outputFile : Path , base : Path ) {
357365 Files .createDirectories(outputFile.parent)
358- ZipOutputStream (Files .newOutputStream(outputFile)).use { zos ->
366+
367+ // Never pass a stream here; this will cause extended local headers to be used, making it incompatible to FA!
368+ ZipArchiveOutputStream (outputFile.toFile()).use { zos ->
369+ zos.setMethod(ZipArchiveEntry .DEFLATED )
370+
359371 for (src in sources) {
360372 if (! Files .exists(src)) {
361373 // skip
@@ -375,28 +387,23 @@ class Patcher(
375387 }
376388 }
377389
378- private fun ZipOutputStream .pushNormalizedFile (base : Path , path : Path ) {
390+ private fun ZipArchiveOutputStream .pushNormalizedFile (base : Path , path : Path ) {
379391 require(Files .isRegularFile(path)) { " Path $path is not a regular file" }
380392
381393 val archiveName = base.relativize(path).toString().replace(" \\ " , " /" )
382394
383-
384- // Read file fully (FA requires sizes & CRC up front otherwise can't read the zip file)
385- val bytes = Files .readAllBytes(path)
386- val crc = java.util.zip.CRC32 ().apply { update(bytes) }
387-
388- val entry = ZipEntry (archiveName).apply {
389- method = ZipEntry .DEFLATED
390- size = bytes.size.toLong()
391- compressedSize = - 1 // let deflater handle, still no descriptor
392- this .crc = crc.value
393- // fix timestamp for determinism (not strictly necessary)
394- time = 315532800000L // 1980-01-01
395+ // Use the same constructor as the FAF API:
396+ val entry = ZipArchiveEntry (path.toFile(), archiveName).apply {
397+ // Ensure deterministic times
398+ setTime(MINIMUM_ZIP_FILE_TIME )
399+ setCreationTime(MINIMUM_ZIP_FILE_TIME )
400+ setLastModifiedTime(MINIMUM_ZIP_FILE_TIME )
401+ setLastAccessTime(MINIMUM_ZIP_FILE_TIME )
395402 }
396403
397- this .putNextEntry (entry)
398- this .write(bytes)
399- this .closeEntry ()
404+ this .putArchiveEntry (entry)
405+ Files .newInputStream(path).use { inp -> inp.copyTo( this ) }
406+ this .closeArchiveEntry ()
400407 }
401408
402409}
0 commit comments