Files
OpenVulkano/openVulkanoCpp/IO/Archive/ZipWriter.cpp
2025-02-12 13:21:30 +01:00

261 lines
9.2 KiB
C++

/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/* References:
* https://libzip.org/specifications/extrafld.txt
* https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html
* https://learn.microsoft.com/en-us/windows/win32/sysinfo/converting-a-time-t-value-to-a-file-time
*/
#include "ZipWriter.hpp"
#include "IO/MemMappedFile.hpp"
#include "Math/CRC32.hpp"
#include <ctime>
#include <cstdint>
namespace
{
#pragma clang diagnostic push
#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection"
#pragma pack(push, 1)
struct LocalFileHeader
{
uint32_t signature = 0x04034b50;
uint16_t versionToExtract = 10; // Version needed to extract (minimum)
uint16_t generalPurposeFlags = 0; // General purpose bit flag
uint16_t compressionMethod = 0; // Compression method
uint16_t fileLastModTime = 0; // File last modification time
uint16_t fileLastModDate = 0; // File last modification date
uint32_t crc32 = 0; // CRC-32
uint32_t compressedSize = 0; // Compressed size
uint32_t uncompressedSize = 0; // Uncompressed size
uint16_t fileNameLength = 0; // File name length (n)
uint16_t extraFieldLength = 0; // Extra field length (m)
// File Name[n]
// Extra Field[m]
};
struct CentralDirectoryFileHeader
{
uint32_t signature = 0x02014b50;
uint16_t versionMadeBy = 31; // Version made by
uint16_t versionToExtract = 10; // Version needed to extract (minimum)
uint16_t generalPurposeFlags = 0; // General purpose bit flag
uint16_t compressionMethod = 0; // Compression method
uint16_t fileLastModTime = 0; // File last modification time
uint16_t fileLastModDate = 0; // File last modification date
uint32_t crc32 = 0; // CRC-32
uint32_t compressedSize = 0; // Compressed size
uint32_t uncompressedSize = 0; // Uncompressed size
uint16_t fileNameLength = 0; // File name length (n)
uint16_t extraFieldLength = 0; // Extra field length (m)
uint16_t fileCommentLength = 0; // File comment length (k)
uint16_t diskNumber = 0; // Disk number where file starts
uint16_t internalFileAttribs = 0; // Internal file attributes
uint32_t externalFileAttribs = 0; // External file attributes
uint32_t relativeOffsetOfLocalFileHeader = 0; // Relative offset of local file header. This is the number of bytes between the start of the first disk on which the file occurs, and the start of the local file header
// File Name[n]
// Extra Field[m]
// File Comment[k]
};
struct NtfsExtraField
{
uint16_t tag = 10; // Tag for this "extra" block type
uint16_t tSize = 32; // Total Data Size for this block
uint32_t reserved = 0; // for future use
uint16_t tag1 = 1; // NTFS attribute tag value #1
uint16_t tSize1 = 24; // Size of attribute #1, in bytes
uint64_t modTime; // 64-bit NTFS file last modification time
uint64_t acTime; // 64-bit NTFS file last access time
uint64_t crTime; // 64-bit NTFS file creation time
static uint64_t TimetToWinFileTime(time_t t)
{
// See references for details
return (t * 10000000LL) + 116444736000000000LL;
}
NtfsExtraField(time_t modTime, time_t acTime, time_t crTime)
{
this->modTime = TimetToWinFileTime(modTime);
this->acTime = TimetToWinFileTime(acTime);
this->crTime = TimetToWinFileTime(crTime);
}
};
struct EndOfCentralDirectoryHeader
{
uint32_t signature = 0x06054b50;
uint16_t diskNumber = 0; // Number of this disk
uint16_t centralDirectoryDiskNumber = 0; // Disk where central directory starts
uint16_t centralDirectoryEntries = 0; // Number of central directory records on this disk
uint16_t totalCentralDirectoryEntries = 0; // Total number of central directory records
uint32_t centralDirectorySize = 0; // Size of central directory (bytes)
uint32_t centralDirectoryOffset = 0; // Offset of start of central directory, relative to start of archive
uint16_t commentLength = 0; // Comment length (n)
// Comment[n]
};
#pragma pack(pop)
#pragma clang diagnostic pop
static_assert(sizeof(LocalFileHeader) == 30, "Well packed struct");
static_assert(sizeof(CentralDirectoryFileHeader) == 46, "Well packed struct");
static_assert(sizeof(EndOfCentralDirectoryHeader) == 22, "Well packed struct");
static_assert(sizeof(NtfsExtraField) == 36, "Well packed struct");
uint32_t Cat(std::vector<uint8_t>& dest, size_t size, const uint8_t* thing)
{
uint32_t startOffset = dest.size();
dest.insert(dest.end(), thing, thing + size);
return startOffset;
}
template<typename T>
uint32_t Cat(std::vector<uint8_t>& dest, const T* thing)
{
return Cat(dest, sizeof(T), reinterpret_cast<const uint8_t*>(thing));
}
[[maybe_unused]] uint32_t Cat(std::vector<uint8_t>& dest, const std::vector<uint8_t>& thing)
{
return Cat(dest, thing.size(), thing.data());
}
std::pair<uint16_t, uint16_t> ConvertToDosTimeDate(const time_t time)
{
std::tm tm;
#if _MSC_VER >= 1400
if (gmtime_s(&tm, &time) != 0) [[unlikely]]
throw std::runtime_error("gmtime_s() failed");
#else
if (gmtime_r(&time, &tm) == nullptr) [[unlikely]]
throw std::runtime_error("gmtime_r() failed");
#endif
uint16_t dosTime = 0;
dosTime |= (tm.tm_sec / 2) & 0x1F; // Seconds divided by 2 (Bits 00-04)
dosTime |= (tm.tm_min & 0x3F) << 5; // Minutes (Bits 05-10)
dosTime |= (tm.tm_hour & 0x1F) << 11; // Hours (Bits 11-15)
uint16_t dosDate = 0;
dosDate |= (tm.tm_mday & 0x1F); // Day (Bits 00-04)
dosDate |= ((tm.tm_mon + 1) & 0x0F) << 5; // Month (Bits 05-08)
dosDate |= ((tm.tm_year - 80) & 0x7F) << 9; // Year from 1980 (Bits 09-15)
return { dosTime, dosDate };
}
template<int MAX_PADDING_SIZE = 64>
void ZFill(FILE *handle, size_t num)
{
uint8_t zeros[MAX_PADDING_SIZE] = {};
fwrite(&zeros, num, 1, handle);
}
}
namespace OpenVulkano
{
ZipWriter::ZipWriter(const std::filesystem::path& filePath, bool alignHeadersBy64)
{
m_file = fopen(filePath.string().c_str(), "wb");
if (!m_file)
{
throw std::runtime_error("Unable to open file for writing: " + filePath.string());
}
m_pad = alignHeadersBy64;
}
void ZipWriter::AddFile(const FileDescription& description, const void* buffer)
{
size_t fileSize = description.size;
size_t fileNameLength = description.path.size();
uint8_t *fileName = (uint8_t*)description.path.data();
CRC32 crc;
crc.Update(fileSize, (const uint8_t *)buffer);
uint32_t crc32 = crc.GetValue();
time_t createTime = description.createTime;
time_t modTime = description.modTime;
time_t accessTime = modTime; // FileDescription doesn't have this field
auto [dosTime, dosDate] = ConvertToDosTimeDate(modTime);
LocalFileHeader lfh;
lfh.fileLastModTime = dosTime;
lfh.fileLastModDate = dosDate;
lfh.crc32 = crc32;
lfh.compressedSize = lfh.uncompressedSize = fileSize;
lfh.fileNameLength = fileNameLength;
if (m_pad)
{
size_t headerSize = sizeof(LocalFileHeader) + fileNameLength;
size_t padding = 64 - ((ftell(m_file) + headerSize) & 63);
if (padding < 4) padding += 64;
lfh.extraFieldLength = padding;
}
size_t headerOffset = ftell(m_file);
fwrite(&lfh, sizeof(lfh), 1, m_file);
fwrite(fileName, fileNameLength, 1, m_file);
if (m_pad)
{
uint16_t marker = 6534;
uint16_t dataSize = lfh.extraFieldLength - 4;
fwrite(&marker, sizeof(uint16_t), 1, m_file);
fwrite(&dataSize, sizeof(uint16_t), 1, m_file);
ZFill<128>(m_file, dataSize);
}
fwrite(buffer, fileSize, 1, m_file);
CentralDirectoryFileHeader cdfh;
cdfh.fileLastModTime = dosTime;
cdfh.fileLastModDate = dosDate;
cdfh.crc32 = crc32;
cdfh.compressedSize = cdfh.uncompressedSize = fileSize;
cdfh.fileNameLength = fileNameLength;
cdfh.extraFieldLength = sizeof(NtfsExtraField);
cdfh.externalFileAttribs = 32; // NOTE(vb): I've no idea wtf is this value mean
cdfh.relativeOffsetOfLocalFileHeader = headerOffset;
NtfsExtraField ntfs(modTime, accessTime, createTime);
Cat(m_centralDirs, &cdfh);
Cat(m_centralDirs, fileNameLength, fileName);
Cat(m_centralDirs, &ntfs);
m_numFiles += 1;
}
void ZipWriter::AddFile(const std::filesystem::path& fileName, const char* inArchiveName)
{
MemMappedFile file = MemMappedFile(fileName.string());
auto desc = OpenVulkano::FileDescription::MkFile(inArchiveName, file.Size());
AddFile(desc, file.Data());
}
ZipWriter::~ZipWriter()
{
if (IsOpen())
{
int centralDirsOffset = 0;
size_t centralDirsSize = m_centralDirs.size();
if (m_numFiles)
{
centralDirsOffset = ftell(m_file);
fwrite(m_centralDirs.data(), centralDirsSize, 1, m_file);
m_centralDirs.clear();
}
EndOfCentralDirectoryHeader eocd;
eocd.centralDirectoryEntries = eocd.totalCentralDirectoryEntries = m_numFiles;
eocd.centralDirectoryOffset = centralDirsOffset;
eocd.centralDirectorySize = centralDirsSize;
fwrite(&eocd, sizeof(eocd), 1, m_file);
fclose(m_file);
}
}
}