[frameworks/karchive] src: kzip: zip64 write support

George Florea Bănuș <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 484401ffbbdfb095f7f9aaf2c9452c418bec9de3 by George Florea Bănuș.
Committed on 19/07/2026 at 15:20.
Pushed by meven into branch 'master'.

kzip: zip64 write support

BUG: 514117

M  +273  -76   src/kzip.cpp

https://invent.kde.org/frameworks/karchive/-/commit/484401ffbbdfb095f7f9aaf2c9452c418bec9de3

diff --git a/src/kzip.cpp b/src/kzip.cpp
index c05e72b..27e0a2c 100644
--- a/src/kzip.cpp
+++ b/src/kzip.cpp
@@ -877,7 +877,6 @@ bool KZip::closeArchive()
     // write all central dir file entries
 
     // to be written at the end of the file...
-    char buffer[22]; // first used for 12, then for 22 at the end
     uLong crc = crc32(0L, nullptr, 0);
 
     qint64 centraldiroffset = device()->pos();
@@ -893,23 +892,61 @@ bool KZip::closeArchive()
         //    << entry->path()
         //    << "encoding:" << entry->encoding();
 
+        QByteArray path = QFile::encodeName(entry->path());
         uLong mycrc = entry->crc32();
-        // crc checksum, at headerStart+14
+        qint64 compressedSize = entry->compressedSize();
+        qint64 size = entry->size();
+
+        bool compressedSizeNeeds64Bit = compressedSize >= 0xFFFFFFFF;
+        bool uncompressedSizeNeeds64Bit = size >= 0xFFFFFFFF;
+        bool isZip64File = (compressedSizeNeeds64Bit || uncompressedSizeNeeds64Bit);
+
+        char buffer[12];
+
+        // crc checksum
         qToLittleEndian<quint32>(mycrc, buffer);
 
-        qint64 mysize1 = entry->compressedSize();
-        // compressed file size, at headerStart+18
-        qToLittleEndian<quint32>(mysize1, buffer + 4);
+        if (!isZip64File) {
+            qToLittleEndian<quint32>(compressedSize, buffer + 4);
+            qToLittleEndian<quint32>(size, buffer + 8);
 
-        qint64 myusize = entry->size();
-        // uncompressed file size, at headerStart+22
-        qToLittleEndian<quint32>(myusize, buffer + 8);
+            if (device()->write(buffer, 12) != 12) {
+                setErrorString(tr("Could not write file header: %1").arg(device()->errorString()));
+                return false;
+            }
+        } else {
+            // compressed file size
+            qToLittleEndian<quint32>(0xFFFFFFFF, buffer + 4);
+            // uncompressed file size
+            qToLittleEndian<quint32>(0xFFFFFFFF, buffer + 8);
 
-        if (device()->write(buffer, 12) != 12) {
-            setErrorString(tr("Could not write file header: %1").arg(device()->errorString()));
-            return false;
+            if (device()->write(buffer, 12) != 12) {
+                setErrorString(tr("Could not write file header: %1").arg(device()->errorString()));
+                return false;
+            }
+
+            // jump to the Local File Header Zip64 extra field to fill the 64-bit sizes
+            qint64 extraFieldOffset = entry->headerStart() + 30 + path.length();
+            if (d->m_extraField == ModificationTime) {
+                extraFieldOffset += 17;
+            }
+            // skip Zip64 extra field header id and field size
+            extraFieldOffset += 4;
+            if (!device()->seek(extraFieldOffset)) {
+                setErrorString(tr("Could not seek to Zip64 extra field: %1").arg(device()->errorString()));
+                return false;
+            }
+
+            char z64Buffer[16];
+            qToLittleEndian<quint64>(size, z64Buffer);
+            qToLittleEndian<quint64>(compressedSize, z64Buffer + 8);
+            if (device()->write(z64Buffer, 16) != 16) {
+                setErrorString(tr("Could not write Zip64 compressed size: %1").arg(device()->errorString()));
+                return false;
+            }
         }
     }
+
     device()->seek(atbackup);
 
     for (KZipFileEntry *entry : d->m_fileList) {
@@ -917,24 +954,44 @@ bool KZip::closeArchive()
         //              << "encoding:" << entry->encoding();
 
         QByteArray path = QFile::encodeName(entry->path());
+        qint64 compressedSize = entry->compressedSize();
+        qint64 size = entry->size();
+        qint64 myhst = entry->headerStart();
+
+        bool compressedSizeNeeds64Bit = compressedSize >= 0xFFFFFFFF;
+        bool uncompressedSizeNeeds64Bit = size >= 0xFFFFFFFF;
+        bool offsetNeeds64Bit = myhst >= 0xFFFFFFFF;
+        bool entryNeedsZip64 = (compressedSizeNeeds64Bit || uncompressedSizeNeeds64Bit || offsetNeeds64Bit);
+
+        int extra_field_len = (d->m_extraField == ModificationTime) ? 9 : 0;
+        uint zip64FieldSize = 0;
+        if (entryNeedsZip64) {
+            // 2 header id + 2 field size +
+            // 8 uncompressed size + 8 compressed size + 8 offset
+            zip64FieldSize = 28;
+        }
+        extra_field_len += zip64FieldSize;
 
-        const int extra_field_len = (d->m_extraField == ModificationTime) ? 9 : 0;
         const int bufferSize = extra_field_len + path.length() + 46;
         char *buffer = new char[bufferSize];
 
         memset(buffer, 0, 46); // zero is a nice default for most header fields
 
-        /* clang-format off */
-        const char head[] = {
-            'P', 'K', 1, 2, // central file header signature
-            0x14, 3, // version made by (3 == UNIX)
-            0x14, 0 // version needed to extract
-        };
-        /* clang-format on */
+        // Central header signature
+        buffer[0] = 'P';
+        buffer[1] = 'K';
+        buffer[2] = 1;
+        buffer[3] = 2;
+
+        // Version made by (UNIX)
+        buffer[4] = 0x14;
+        buffer[5] = 3;
+        // Version needed to extract
+        buffer[6] = entryNeedsZip64 ? 45 : 0x14;
+        buffer[7] = 0;
 
-        // I do not know why memcpy is not working here
-        // memcpy(buffer, head, sizeof(head));
-        memmove(buffer, head, sizeof(head));
+        // General purpose bit flag
+        // qToLittleEndian<quint16>(0, buffer + 8);
 
         // compression method
         qToLittleEndian<quint16>(char(entry->encoding()), buffer + 10);
@@ -946,12 +1003,18 @@ bool KZip::closeArchive()
         qToLittleEndian<quint32>(mycrc, buffer + 16);
 
         // compressed file size
-        qint64 mysize1 = entry->compressedSize();
-        qToLittleEndian<quint32>(mysize1, buffer + 20);
+        if (entryNeedsZip64) {
+            qToLittleEndian<quint32>(0xFFFFFFFF, buffer + 20);
+        } else {
+            qToLittleEndian<quint32>(compressedSize, buffer + 20);
+        }
 
         // uncompressed file size
-        qint64 mysize = entry->size();
-        qToLittleEndian<quint32>(mysize, buffer + 24);
+        if (entryNeedsZip64) {
+            qToLittleEndian<quint32>(0xFFFFFFFF, buffer + 24);
+        } else {
+            qToLittleEndian<quint32>(size, buffer + 24);
+        }
 
         // fileName length
         qToLittleEndian<quint16>(path.length(), buffer + 28);
@@ -959,19 +1022,31 @@ bool KZip::closeArchive()
         // extra field length
         qToLittleEndian<quint16>(extra_field_len, buffer + 30);
 
-        qToLittleEndian<quint16>(entry->permissions(), buffer + 40);
+        // file comment length
+        // qToLittleEndian<quint16>(0, buffer + 32);
+
+        // disk number where file starts
+        // qToLittleEndian<quint16>(0, buffer + 34);
+
+        // internal file attributes
+        // none
+        // external file attributes
+        qToLittleEndian<quint32>(entry->permissions(), buffer + 40);
 
         // relative offset of local header
-        qint64 myhst = entry->headerStart();
-        qToLittleEndian<quint32>(myhst, buffer + 42);
+        if (entryNeedsZip64) {
+            qToLittleEndian<quint32>(0xFFFFFFFF, buffer + 42);
+        } else {
+            qToLittleEndian<quint32>(myhst, buffer + 42);
+        }
 
         // file name
         strncpy(buffer + 46, path.constData(), path.length());
         // qCDebug(KArchiveLog) << "closearchive length to write: " << bufferSize;
 
         // extra field
+        char *extfield = buffer + 46 + path.length();
         if (d->m_extraField == ModificationTime) {
-            char *extfield = buffer + 46 + path.length();
             // "Extended timestamp" header (0x5455)
             extfield[0] = 'U';
             extfield[1] = 'T';
@@ -982,6 +1057,28 @@ bool KZip::closeArchive()
             // provide only modification time
             unsigned long time = (unsigned long)entry->date().toSecsSinceEpoch();
             qToLittleEndian<quint32>(time, extfield + 5);
+            extfield += 9;
+        }
+
+        if (entryNeedsZip64) {
+            // zip64 header
+            extfield[0] = 0x01;
+            extfield[1] = 0x00;
+
+            // zip64 field size
+            qToLittleEndian<quint16>(24, extfield + 2);
+
+            // uncompressed file size
+            qToLittleEndian<quint64>(size, extfield + 4);
+
+            // compressed file size
+            qToLittleEndian<quint64>(compressedSize, extfield + 12);
+
+            // relative offset of local header
+            qToLittleEndian<quint64>(myhst, extfield + 20);
+
+            // number of the disk on which this file starts
+            // qToLittleEndian<quint32>(0, extfield + 28);
         }
 
         crc = crc32(crc, (Bytef *)buffer, bufferSize);
@@ -993,43 +1090,125 @@ bool KZip::closeArchive()
         }
     }
     qint64 centraldirendoffset = device()->pos();
+    qint64 cdsize = centraldirendoffset - centraldiroffset;
+    // total number of entries in central dir
+    qint64 count = d->m_fileList.count();
+
+    bool archiveNeedsZip64 = (count >= 0xFFFF || cdsize >= 0xFFFFFFFF || centraldiroffset >= 0xFFFFFFFF);
+
+    if (archiveNeedsZip64) {
+        // Write Zip64 End of Central Directory Record (PK\6\6)
+        char zip64Eocd[56];
+        zip64Eocd[0] = 'P';
+        zip64Eocd[1] = 'K';
+        zip64Eocd[2] = 6;
+        zip64Eocd[3] = 6;
+
+        // size of the EOCD64 minus 12
+        quint64 eocdSize = 56 - 12;
+        qToLittleEndian<quint64>(eocdSize, zip64Eocd + 4);
+
+        // Version made by
+        qToLittleEndian<quint16>(45, zip64Eocd + 12);
+        // Version needed to extract
+        qToLittleEndian<quint16>(45, zip64Eocd + 14);
+        // Number of this disk
+        qToLittleEndian<quint32>(0, zip64Eocd + 16);
+        // Disk where central directory starts
+        qToLittleEndian<quint32>(0, zip64Eocd + 20);
+        // Number of central directory records on this disk
+        qToLittleEndian<quint64>(count, zip64Eocd + 24);
+        // Total number of central directory records
+        qToLittleEndian<quint64>(count, zip64Eocd + 32);
+        // Size of central directory in bytes
+        qToLittleEndian<quint64>(cdsize, zip64Eocd + 40);
+        // Offset of start of central directory, relative to start of archive
+        qToLittleEndian<quint64>(centraldiroffset, zip64Eocd + 48);
+        // Comment (up to the size of EOCD64)
+        // no comment
+
+        if (device()->write(zip64Eocd, 56) != 56) {
+            setErrorString(tr("Could not write Zip64 EOCD record: %1").arg(device()->errorString()));
+            return false;
+        }
+
+        // Write Zip64 End of Central Directory Locator (PK\6\7)
+        char zip64EocdLocator[20];
+        zip64EocdLocator[0] = 'P';
+        zip64EocdLocator[1] = 'K';
+        zip64EocdLocator[2] = 6;
+        zip64EocdLocator[3] = 7;
+
+        // Disk where EOCD64 starts
+        qToLittleEndian<quint32>(0, zip64EocdLocator + 4);
+        // Offset to start of EOCD64, relative to start of archive
+        qToLittleEndian<quint64>(centraldirendoffset, zip64EocdLocator + 8);
+        // Total number of disks
+        qToLittleEndian<quint32>(1, zip64EocdLocator + 16);
+
+        if (device()->write(zip64EocdLocator, 20) != 20) {
+            setErrorString(tr("Could not write Zip64 EOCD locator: %1").arg(device()->errorString()));
+            return false;
+        }
+    }
+
     // qCDebug(KArchiveLog) << "closearchive: centraldirendoffset: " << centraldirendoffset;
     // qCDebug(KArchiveLog) << "closearchive: device()->pos(): " << device()->pos();
 
     // write end of central dir record.
-    buffer[0] = 'P'; // end of central dir signature
-    buffer[1] = 'K';
-    buffer[2] = 5;
-    buffer[3] = 6;
-
-    qToLittleEndian<quint16>(0, buffer + 4);
-
-    // number of disk with start of central dir
-    qToLittleEndian<quint16>(0, buffer + 6);
-
-    // total number of entries in central dir
-    int count = d->m_fileList.count();
+    char eocd[22];
+    eocd[0] = 'P'; // end of central dir signature
+    eocd[1] = 'K';
+    eocd[2] = 5;
+    eocd[3] = 6;
+
+    // Number of this disk (or FF FF for ZIP64)
+    if (archiveNeedsZip64) {
+        qToLittleEndian<quint16>(0xFFFF, eocd + 4);
+    } else {
+        qToLittleEndian<quint16>(0, eocd + 4);
+    }
+    // Disk where central directory starts (or FF FF for ZIP64)
+    if (archiveNeedsZip64) {
+        qToLittleEndian<quint16>(0xFFFF, eocd + 6);
+    } else {
+        qToLittleEndian<quint16>(0, eocd + 6);
+    }
+    // Number of central directory records on this disk (or FF FF for ZIP64)
+    if (archiveNeedsZip64) {
+        qToLittleEndian<quint16>(0xFFFF, eocd + 8);
+    } else {
+        qToLittleEndian<quint16>(count, eocd + 8);
+    }
+    // Total number of central directory records (or FF FF for ZIP64)
+    if (archiveNeedsZip64) {
+        qToLittleEndian<quint16>(0xFFFF, eocd + 10);
+    } else {
+        qToLittleEndian<quint16>(count, eocd + 10);
+    }
     // qCDebug(KArchiveLog) << "number of files (count): " << count;
 
-    // count for this disk
-    qToLittleEndian<quint16>(count, buffer + 8);
-    // count for total number of entries in the central dir
-    qToLittleEndian<quint16>(count, buffer + 10);
-
-    // size of the central dir
-    int cdsize = centraldirendoffset - centraldiroffset;
-    qToLittleEndian<quint32>(cdsize, buffer + 12);
-
-    // qCDebug(KArchiveLog) << "end : centraldiroffset: " << centraldiroffset;
+    // Size of central directory in bytes (or FF FF FF FF for ZIP64)
+    if (archiveNeedsZip64) {
+        qToLittleEndian<quint32>(0xFFFFFFFF, eocd + 12);
+    } else {
+        qToLittleEndian<quint32>(cdsize, eocd + 12);
+    }
     // qCDebug(KArchiveLog) << "end : centraldirsize: " << cdsize;
 
-    // central dir offset
-    qToLittleEndian<quint32>(centraldiroffset, buffer + 16);
+    // Offset of start of central directory, relative to start of archive (or FF FF FF FF for ZIP64)
+    if (archiveNeedsZip64) {
+        qToLittleEndian<quint32>(0xFFFFFFFF, eocd + 16);
+    } else {
+        qToLittleEndian<quint32>(centraldiroffset, eocd + 16);
+    }
+    // qCDebug(KArchiveLog) << "end : centraldiroffset: " << centraldiroffset;
 
-    // zipfile comment length
-    qToLittleEndian<quint16>(0, buffer + 20);
+    // Comment length
+    qToLittleEndian<quint16>(0, eocd + 20);
+    // Comment (none)
 
-    if (device()->write(buffer, 22) != 22) {
+    if (device()->write(eocd, 22) != 22) {
         setErrorString(tr("Could not write central dir record: %1").arg(device()->errorString()));
         return false;
     }
@@ -1058,7 +1237,7 @@ bool KZip::doWriteDir(const QString &name,
 bool KZip::doPrepareWriting(const QString &name,
                             const QString &user,
                             const QString &group,
-                            qint64 /*size*/,
+                            qint64 size,
                             mode_t perm,
                             const QDateTime &accessTime,
                             const QDateTime &modificationTime,
@@ -1123,6 +1302,21 @@ bool KZip::doPrepareWriting(const QString &name,
         }
     }
 
+    bool isZip64Archive = size >= 0xFFFFFFFF;
+
+    int extra_field_len = 0;
+    if (d->m_extraField == ModificationTime) {
+        extra_field_len = 17; // value also used in finishWriting()
+    }
+    // if isZip64Archive is true we write both sizes to the zip64 field
+    if (isZip64Archive) {
+        // 2 header id + 2 field size +
+        // 8 uncompressed size + 8 compressed size
+        extra_field_len += 20;
+    }
+
+    QByteArray encodedName = QFile::encodeName(name);
+
     // construct a KZipFileEntry and add it to list
     KZipFileEntry *e = new KZipFileEntry(this,
                                          fileName,
@@ -1132,7 +1326,7 @@ bool KZip::doPrepareWriting(const QString &name,
                                          group,
                                          QString(),
                                          name,
-                                         device()->pos() + 30 + name.length(), // start
+                                         device()->pos() + 30 + encodedName.length() + extra_field_len, // start
                                          0 /*size unknown yet*/,
                                          d->m_compression,
                                          0 /*csize unknown yet*/);
@@ -1145,13 +1339,7 @@ bool KZip::doPrepareWriting(const QString &name,
     d->m_currentFile = e;
     d->m_fileList.append(e);
 
-    int extra_field_len = 0;
-    if (d->m_extraField == ModificationTime) {
-        extra_field_len = 17; // value also used in finishWriting()
-    }
-
     // write out zip header
-    QByteArray encodedName = QFile::encodeName(name);
     int bufferSize = extra_field_len + encodedName.length() + 30;
     // qCDebug(KArchiveLog) << "bufferSize=" << bufferSize;
     char *buffer = new char[bufferSize];
@@ -1162,7 +1350,7 @@ bool KZip::doPrepareWriting(const QString &name,
     buffer[3] = 4;
 
     // version needed to extract
-    qToLittleEndian<quint16>(0x14, buffer + 4);
+    qToLittleEndian<quint16>(isZip64Archive ? 45 : 0x14, buffer + 4);
 
     // general purpose bit flag
     qToLittleEndian<quint16>(0, buffer + 6);
@@ -1191,14 +1379,14 @@ bool KZip::doPrepareWriting(const QString &name,
     qToLittleEndian<quint16>(encodedName.length(), buffer + 26);
 
     // extra field length
-    qToLittleEndian<quint16>((uchar)(extra_field_len), buffer + 28);
+    qToLittleEndian<quint16>(extra_field_len, buffer + 28);
 
     // file name
     strncpy(buffer + 30, encodedName.constData(), encodedName.length());
 
     // extra field
+    char *extfield = buffer + 30 + encodedName.length();
     if (d->m_extraField == ModificationTime) {
-        char *extfield = buffer + 30 + encodedName.length();
         // "Extended timestamp" header (0x5455)
         extfield[0] = 'U';
         extfield[1] = 'T';
@@ -1206,9 +1394,22 @@ bool KZip::doPrepareWriting(const QString &name,
         extfield[3] = 0;
         extfield[4] = 1 | 2 | 4; // contains mtime, atime, ctime
 
-        qToLittleEndian<quint16>(mtime, extfield + 5);
-        qToLittleEndian<quint16>(atime, extfield + 9);
-        qToLittleEndian<quint16>(ctime, extfield + 13);
+        qToLittleEndian<quint32>(mtime, extfield + 5);
+        qToLittleEndian<quint32>(atime, extfield + 9);
+        qToLittleEndian<quint32>(ctime, extfield + 13);
+        extfield += 17;
+    }
+
+    if (isZip64Archive) {
+        // zip64 header
+        extfield[0] = 0x01;
+        extfield[1] = 0x00;
+        // size of uncompressed and compressed fields
+        qToLittleEndian<quint16>(16, extfield + 2);
+        // uncompressed size
+        qToLittleEndian<quint64>(size, extfield + 4);
+        // compressed size
+        qToLittleEndian<quint64>(0, extfield + 12);
     }
 
     // Write header
@@ -1256,13 +1457,9 @@ bool KZip::doFinishWriting(qint64 size)
     // qCDebug(KArchiveLog) << "fileName: " << d->m_currentFile->path();
     // qCDebug(KArchiveLog) << "getpos (at): " << device()->pos();
     d->m_currentFile->setSize(size);
-    int extra_field_len = 0;
-    if (d->m_extraField == ModificationTime) {
-        extra_field_len = 17; // value also used in finishWriting()
-    }
 
     const QByteArray encodedName = QFile::encodeName(d->m_currentFile->path());
-    qint64 csize = device()->pos() - d->m_currentFile->headerStart() - 30 - encodedName.length() - extra_field_len;
+    qint64 csize = device()->pos() - d->m_currentFile->position();
     d->m_currentFile->setCompressedSize(csize);
     // qCDebug(KArchiveLog) << "usize: " << d->m_currentFile->size();
     // qCDebug(KArchiveLog) << "csize: " << d->m_currentFile->compressedSize();
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.