diff --git a/openVulkanoCpp/IO/Archive/ZipWriter.cpp b/openVulkanoCpp/IO/Archive/ZipWriter.cpp new file mode 100644 index 0000000..120e024 --- /dev/null +++ b/openVulkanoCpp/IO/Archive/ZipWriter.cpp @@ -0,0 +1,243 @@ +/* + * 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 "Math/CRC32.hpp" + +#include +#include + +namespace +{ +#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 CentalDirectoryFileHeader + { + 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 + }; + + 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) + + static_assert(sizeof(LocalFileHeader) == 30, "Well packed struct"); + static_assert(sizeof(CentalDirectoryFileHeader) == 46, "Well packed struct"); + static_assert(sizeof(EndOfCentralDirectoryHeader) == 22, "Well packed struct"); + static_assert(sizeof(NtfsExtraField) == 36, "Well packed struct"); + + std::vector ReadEntireFile(const std::string& fname) + { + FILE *file = fopen(fname.c_str(), "rb"); + std::vector buffer; + if(file) + { + fseek(file, 0, SEEK_END); + size_t size = ftell(file); + fseek(file, 0, SEEK_SET); + buffer.resize(size); + fread(buffer.data(), size, 1, file); + fclose(file); + } + return buffer; + } + + template + uint32_t Cat(std::vector& dest, T* thing) + { + uint32_t startOffset = dest.size(); + dest.insert(dest.end(), (uint8_t *)thing, (uint8_t *)(thing + 1)); + return startOffset; + } + + uint32_t Cat(std::vector& dest, size_t size, uint8_t* thing) + { + uint32_t startOffset = dest.size(); + dest.insert(dest.end(), (uint8_t*)thing, (uint8_t*)(thing + size)); + return startOffset; + } + + uint32_t Cat(std::vector& dest, const std::vector& thing) + { + uint32_t startOffset = Cat(dest, thing.size(), (uint8_t*)thing.data()); + return startOffset; + } + + void TimetToFileTime(time_t t, uint64_t* lpWinFileTime) + { + // See references for details + *lpWinFileTime = (t * 10000000LL) + 116444736000000000LL; + } + + std::pair ConvertToDosTimeDate(time_t time) + { + std::tm* tm = std::localtime(&time); + if (tm) + { + 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 }; + } + + return {}; + } +} + +namespace OpenVulkano +{ + 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; + + size_t headerOffset = Cat(m_headers, &lfh); + Cat(m_headers, fileNameLength, fileName); + Cat(m_headers, description.size, (uint8_t *)buffer); + + CentalDirectoryFileHeader 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; + TimetToFileTime(modTime, &ntfs.modTime); + TimetToFileTime(accessTime, &ntfs.acTime); + TimetToFileTime(createTime, &ntfs.crTime); + + Cat(m_centralDirs, &cdfh); + Cat(m_centralDirs, fileNameLength, fileName); + Cat(m_centralDirs, &ntfs); + + m_numFiles += 1; + } + + std::vector ZipWriter::GetMemory() + { + std::vector buffer = m_headers; + + if (m_numFiles) + { + int centralDirsOffset = Cat(buffer, m_centralDirs); + } + + EndOfCentralDirectoryHeader eocd; + eocd.centralDirectoryEntries = eocd.totalCentralDirectoryEntries = m_numFiles; + eocd.centralDirectoryOffset = m_headers.size(); + + Cat(buffer, &eocd); + + return buffer; + } + + void ZipWriter::AddFile(const std::filesystem::path& fileName, const char* inArchiveName) + { + auto data = ReadEntireFile(fileName.string()); + auto desc = OpenVulkano::FileDescription::MakeDescriptionForFile(inArchiveName, data.size()); + AddFile(desc, data.data()); + } + + bool ZipWriter::Write(const std::filesystem::path& archivePath) + { + FILE* file = fopen(archivePath.string().c_str(), "wb"); + if (file) + { + auto mem = GetMemory(); + fwrite(mem.data(), mem.size(), 1, file); + fclose(file); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/openVulkanoCpp/IO/Archive/ZipWriter.hpp b/openVulkanoCpp/IO/Archive/ZipWriter.hpp new file mode 100644 index 0000000..c0f4649 --- /dev/null +++ b/openVulkanoCpp/IO/Archive/ZipWriter.hpp @@ -0,0 +1,29 @@ +/* + * 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/. + */ +#pragma once + +#include +#include +#include + +#include "IO/FileDescription.hpp" + +namespace OpenVulkano +{ + class ZipWriter + { + std::vector m_headers; + std::vector m_centralDirs; + int m_numFiles = 0; + + public: + void AddFile(const FileDescription& description, const void* buffer); + void AddFile(const std::filesystem::path& fileName, const char* inArchiveName); + + bool Write(const std::filesystem::path& archivePath); + std::vector GetMemory(); + }; +} \ No newline at end of file diff --git a/tests/IO/Archive/ZipWriterTest.cpp b/tests/IO/Archive/ZipWriterTest.cpp new file mode 100644 index 0000000..5117d18 --- /dev/null +++ b/tests/IO/Archive/ZipWriterTest.cpp @@ -0,0 +1,76 @@ +/* + * 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/. + */ + +#include + +#include +#include +#include + +#include "IO/Archive/ZipWriter.hpp" + +using namespace OpenVulkano; + +TEST_CASE("Empty zip file", "[ZipWriter]") +{ + ZipWriter writer; + auto mem = writer.GetMemory(); + + const int expectSize = 22; + std::vector expect = {0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + + CHECK(mem.size() == expectSize); + CHECK(mem == expect); +} + +TEST_CASE("Zip with one file(AAA.txt that has 'AAA')", "[ZipWriter]") +{ + ZipWriter writer; + + FileDescription desc = FileDescription::MakeDescriptionForFile("AAA.txt", 3); + desc.modTime = {}; + desc.createTime = {}; + char buffer[] = {'A', 'A', 'A'}; + + writer.AddFile(desc, buffer); + + auto mem = writer.GetMemory(); + + const int expectSize = 151; + std::vector expect = {0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x21, 0xec, 0xa7, 0x31, 0xa0, 0x66, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x41, 0x41, 0x2e, 0x74, 0x78, 0x74, 0x41, 0x41, 0x41, 0x50, 0x4b, 0x01, 0x02, 0x1f, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x21, 0xec, 0xa7, 0x31, 0xa0, 0x66, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x41, 0x41, 0x2e, 0x74, 0x78, 0x74, 0x0a, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00}; + + CHECK(mem.size() == expectSize); + CHECK(mem == expect); +} + + +TEST_CASE("Zip with two files(AAA.txt that has 'AAA', BBB.bin that has 'BBB')", "[ZipWriter]") +{ + ZipWriter writer; + + FileDescription aaa = FileDescription::MakeDescriptionForFile("AAA.txt", 3); + aaa.modTime = {}; + aaa.createTime = {}; + char aaaBuffer[] = {'A', 'A', 'A'}; + + FileDescription bbb = FileDescription::MakeDescriptionForFile("BBB.bin", 3); + bbb.modTime = {}; + bbb.createTime = {}; + char bbbBuffer[] = {'B', 'B', 'B'}; + + writer.AddFile(aaa, aaaBuffer); + writer.AddFile(bbb, bbbBuffer); + + auto mem = writer.GetMemory(); + + const int expectSize = 280; + std::vector expect = {0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x21, 0xec, 0xa7, 0x31, 0xa0, 0x66, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x41, 0x41, 0x41, 0x2e, 0x74, 0x78, 0x74, 0x41, 0x41, 0x41, 0x50, 0x4b, 0x03, 0x04, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x21, 0xec, 0x87, 0x8d, 0xc2, 0xd6, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x2e, 0x62, 0x69, 0x6e, 0x42, 0x42, 0x42, 0x50, 0x4b, 0x01, 0x02, 0x1f, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x21, 0xec, 0xa7, 0x31, 0xa0, 0x66, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x41, 0x41, 0x2e, 0x74, 0x78, 0x74, 0x0a, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x50, 0x4b, 0x01, 0x02, 0x1f, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x21, 0xec, 0x87, 0x8d, 0xc2, 0xd6, 0x03, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x07, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x42, 0x42, 0x42, 0x2e, 0x62, 0x69, 0x6e, 0x0a, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x00, 0x80, 0x3e, 0xd5, 0xde, 0xb1, 0x9d, 0x01, 0x50, 0x4b, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00}; + + CHECK(mem.size() == expectSize); + CHECK(mem == expect); +} + +