#include "ZipWriter.hpp" #include #include #include #include #include namespace margelo::nitro::nitroepub { namespace { constexpr std::uint16_t UTF8_FLAG = 0x0800; constexpr std::uint16_t STORED = 0; constexpr std::uint16_t DEFLATED = 8; constexpr std::uint16_t DOS_DATE_1980_01_01 = 0x0021; std::uint32_t checked32(std::uint64_t value, const char* description) { if (value > std::numeric_limits::max()) { throw std::runtime_error(std::string("EPUB exceeds ZIP32 ") + description); } return static_cast(value); } std::uint16_t checked16(std::size_t value, const char* description) { if (value > std::numeric_limits::max()) { throw std::runtime_error(std::string("EPUB exceeds ZIP16 ") + description); } return static_cast(value); } std::vector deflateRaw(const std::vector& input) { z_stream stream{}; if (deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY) != Z_OK) { throw std::runtime_error("Failed to initialize EPUB compression"); } std::vector output( std::max(128, compressBound(input.size()))); stream.next_in = const_cast( reinterpret_cast(input.data())); stream.avail_in = checked32(input.size(), "entry size"); stream.next_out = reinterpret_cast(output.data()); stream.avail_out = checked32(output.size(), "compressed entry size"); const int result = deflate(&stream, Z_FINISH); if (result != Z_STREAM_END) { deflateEnd(&stream); throw std::runtime_error("Failed to compress EPUB entry"); } output.resize(stream.total_out); deflateEnd(&stream); return output; } } // namespace ZipWriter::ZipWriter(const std::filesystem::path& outputPath) : output_(outputPath, std::ios::binary | std::ios::trunc) { if (!output_) { throw std::runtime_error("Unable to create EPUB at " + outputPath.string()); } } ZipWriter::~ZipWriter() { if (!finished_) { output_.close(); } } void ZipWriter::addStored(const std::string& archivePath, const std::string& content) { add(archivePath, std::vector(content.begin(), content.end()), false); } void ZipWriter::addDeflated(const std::string& archivePath, const std::string& content) { add(archivePath, std::vector(content.begin(), content.end()), true); } void ZipWriter::addFile(const std::string& archivePath, const std::filesystem::path& sourcePath, bool compress) { std::ifstream input(sourcePath, std::ios::binary); if (!input) { throw std::runtime_error("Unable to read EPUB asset " + sourcePath.string()); } std::vector content( (std::istreambuf_iterator(input)), std::istreambuf_iterator()); add(archivePath, content, compress); } void ZipWriter::add(const std::string& archivePath, const std::vector& content, bool compress) { if (finished_) { throw std::runtime_error("Cannot add an entry to a finished EPUB"); } if (archivePath.empty() || archivePath.front() == '/' || archivePath.find("..") != std::string::npos) { throw std::runtime_error("Unsafe EPUB archive path: " + archivePath); } const auto compressed = compress ? deflateRaw(content) : content; const auto crc = static_cast( crc32(0, reinterpret_cast(content.data()), checked32(content.size(), "entry size"))); const auto offset = checked32(static_cast(output_.tellp()), "local header offset"); const auto pathLength = checked16(archivePath.size(), "entry name length"); const auto method = static_cast(compress ? DEFLATED : STORED); write32(0x04034b50); write16(20); write16(UTF8_FLAG); write16(method); write16(0); write16(DOS_DATE_1980_01_01); write32(crc); write32(checked32(compressed.size(), "compressed entry size")); write32(checked32(content.size(), "entry size")); write16(pathLength); write16(0); output_.write(archivePath.data(), pathLength); output_.write(reinterpret_cast(compressed.data()), static_cast(compressed.size())); if (!output_) { throw std::runtime_error("Failed while writing EPUB entry " + archivePath); } entries_.push_back(CentralEntry{ archivePath, method, crc, checked32(compressed.size(), "compressed entry size"), checked32(content.size(), "entry size"), offset, }); } void ZipWriter::finish() { if (finished_) { return; } const auto directoryOffset = checked32(static_cast(output_.tellp()), "central directory offset"); for (const auto& entry : entries_) { const auto pathLength = checked16(entry.archivePath.size(), "entry name length"); write32(0x02014b50); write16(20); write16(20); write16(UTF8_FLAG); write16(entry.method); write16(0); write16(DOS_DATE_1980_01_01); write32(entry.crc); write32(entry.compressedSize); write32(entry.uncompressedSize); write16(pathLength); write16(0); write16(0); write16(0); write16(0); write32(0); write32(entry.localHeaderOffset); output_.write(entry.archivePath.data(), pathLength); } const auto directoryEnd = checked32(static_cast(output_.tellp()), "central directory size"); const auto entryCount = checked16(entries_.size(), "entry count"); write32(0x06054b50); write16(0); write16(0); write16(entryCount); write16(entryCount); write32(directoryEnd - directoryOffset); write32(directoryOffset); write16(0); output_.flush(); if (!output_) { throw std::runtime_error("Failed to finalize EPUB archive"); } output_.close(); finished_ = true; } void ZipWriter::write16(std::uint16_t value) { const char bytes[] = { static_cast(value & 0xff), static_cast((value >> 8) & 0xff), }; output_.write(bytes, sizeof(bytes)); } void ZipWriter::write32(std::uint32_t value) { const char bytes[] = { static_cast(value & 0xff), static_cast((value >> 8) & 0xff), static_cast((value >> 16) & 0xff), static_cast((value >> 24) & 0xff), }; output_.write(bytes, sizeof(bytes)); } } // namespace margelo::nitro::nitroepub