[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();