diff --git a/src/io/devices/Device.hpp b/src/io/devices/Device.hpp index a3addaf5..cfbe4b1e 100644 --- a/src/io/devices/Device.hpp +++ b/src/io/devices/Device.hpp @@ -8,7 +8,6 @@ #include "../path.hpp" namespace io { - /// @brief Device interface for file system operations class Device { public: @@ -28,6 +27,9 @@ namespace io { /// @brief Get file size in bytes virtual size_t size(std::string_view path) = 0; + /// @brief Get file last write timestamp + virtual file_time_type lastWriteTime(std::string_view path) = 0; + /// @brief Check if file or directory exists virtual bool exists(std::string_view path) = 0; @@ -82,6 +84,10 @@ namespace io { return parent->size((root / path).pathPart()); } + file_time_type lastWriteTime(std::string_view path) override { + return parent->lastWriteTime((root / path).pathPart()); + } + bool exists(std::string_view path) override { return parent->exists((root / path).pathPart()); } diff --git a/src/io/devices/StdfsDevice.cpp b/src/io/devices/StdfsDevice.cpp index cb47c2e6..fc0602b5 100644 --- a/src/io/devices/StdfsDevice.cpp +++ b/src/io/devices/StdfsDevice.cpp @@ -45,23 +45,23 @@ std::unique_ptr StdfsDevice::read(std::string_view path) { } size_t StdfsDevice::size(std::string_view path) { - auto resolved = resolve(path); - return fs::file_size(resolved); + return fs::file_size(resolve(path)); +} + +file_time_type StdfsDevice::lastWriteTime(std::string_view path) { + return fs::last_write_time(resolve(path)); } bool StdfsDevice::exists(std::string_view path) { - auto resolved = resolve(path); - return fs::exists(resolved); + return fs::exists(resolve(path)); } bool StdfsDevice::isdir(std::string_view path) { - auto resolved = resolve(path); - return fs::is_directory(resolved); + return fs::is_directory(resolve(path)); } bool StdfsDevice::isfile(std::string_view path) { - auto resolved = resolve(path); - return fs::is_regular_file(resolved); + return fs::is_regular_file(resolve(path)); } bool StdfsDevice::mkdir(std::string_view path) { diff --git a/src/io/devices/StdfsDevice.hpp b/src/io/devices/StdfsDevice.hpp index ec1a9526..69b12234 100644 --- a/src/io/devices/StdfsDevice.hpp +++ b/src/io/devices/StdfsDevice.hpp @@ -10,6 +10,7 @@ namespace io { std::unique_ptr write(std::string_view path) override; std::unique_ptr read(std::string_view path) override; size_t size(std::string_view path) override; + file_time_type lastWriteTime(std::string_view path) override; bool exists(std::string_view path) override; bool isdir(std::string_view path) override; bool isfile(std::string_view path) override; diff --git a/src/io/devices/ZipFileDevice.cpp b/src/io/devices/ZipFileDevice.cpp index 0496fc57..5171ee83 100644 --- a/src/io/devices/ZipFileDevice.cpp +++ b/src/io/devices/ZipFileDevice.cpp @@ -11,6 +11,7 @@ static debug::Logger logger("zip-file"); using namespace io; +using namespace std::chrono; static constexpr uint32_t EOCD_SIGNATURE = 0x06054b50; static constexpr uint32_t CENTRAL_DIR_SIGNATURE = 0x02014b50; @@ -18,17 +19,52 @@ static constexpr uint32_t LOCAL_FILE_SIGNATURE = 0x04034b50; static constexpr uint32_t COMPRESSION_NONE = 0; static constexpr uint32_t COMPRESSION_DEFLATE = 8; -template -static T read_int(std::unique_ptr& file) { - T value = 0; - file->read(reinterpret_cast(&value), sizeof(value)); - return dataio::le2h(value); -} +namespace { + template + T read_int(std::unique_ptr& file) { + T value = 0; + file->read(reinterpret_cast(&value), sizeof(value)); + return dataio::le2h(value); + } -template -static void read_int(std::unique_ptr& file, T& value) { - file->read(reinterpret_cast(&value), sizeof(value)); - value = dataio::le2h(value); + template + void read_int(std::unique_ptr& file, T& value) { + file->read(reinterpret_cast(&value), sizeof(value)); + value = dataio::le2h(value); + } + file_time_type msdos_to_file_time(uint16_t date, uint16_t time) { + uint16_t year = ((date >> 9) & 0x7F) + 1980; + uint16_t month = (date >> 5) & 0x0F; + uint16_t day = date & 0x1F; + + uint16_t hours = (time >> 11) & 0x1F; + uint16_t minutes = (time >> 5) & 0x3F; + uint16_t seconds = (time & 0x1F) * 2; + + std::tm time_struct = {}; + time_struct.tm_year = year - 1900; + time_struct.tm_mon = month - 1; + time_struct.tm_mday = day; + time_struct.tm_hour = hours; + time_struct.tm_min = minutes; + time_struct.tm_sec = seconds; + time_struct.tm_isdst = -1; + + std::time_t time_t_value = std::mktime(&time_struct); + auto time_point = system_clock::from_time_t(time_t_value); + return file_time_type::clock::now() + (time_point - system_clock::now()); + } + + uint32_t to_ms_dos_timestamp(const file_time_type& fileTime) { + auto timePoint = time_point_cast( + fileTime - file_time_type::clock::now() + system_clock::now() + ); + std::time_t timeT = system_clock::to_time_t(timePoint); + std::tm tm = *std::localtime(&timeT); + uint16_t date = (tm.tm_year - 80) << 9 | (tm.tm_mon + 1) << 5 | tm.tm_mday; + uint16_t time = (tm.tm_hour << 11) | (tm.tm_min << 5) | (tm.tm_sec / 2); + return (date << 16) | time; + } } ZipFileDevice::Entry ZipFileDevice::readEntry() { @@ -192,6 +228,14 @@ size_t ZipFileDevice::size(std::string_view path) { return found->second.uncompressedSize; } +file_time_type ZipFileDevice::lastWriteTime(std::string_view path) { + const auto& found = entries.find(std::string(path)); + if (found == entries.end()) { + return file_time_type::min(); + } + return msdos_to_file_time(found->second.modDate, found->second.modTime); +} + bool ZipFileDevice::exists(std::string_view path) { return entries.find(std::string(path)) != entries.end(); } @@ -262,3 +306,105 @@ std::unique_ptr ZipFileDevice::list(std::string_view path) { } return std::make_unique(std::move(names)); } + +#include "io/io.hpp" +#include "coders/byte_utils.hpp" + +static void write_headers( + std::ostream& file, + const std::string& name, + size_t srcSize, + size_t compressedSize, + uint32_t crc, + const file_time_type& modificationTime, + ByteBuilder& centralDir +) { + auto timestamp = to_ms_dos_timestamp(modificationTime); + ByteBuilder header; + header.putInt32(LOCAL_FILE_SIGNATURE); + header.putInt16(10); // version + header.putInt16(0); // flags + header.putInt16(0); // compression method + header.putInt32(timestamp); // last modification datetime + header.putInt32(crc); // crc32 + header.putInt32(compressedSize); + header.putInt32(srcSize); + header.putInt16(name.length()); + header.putInt16(0); // extra field length + header.put(reinterpret_cast(name.data()), name.length()); + + size_t localHeaderOffset = file.tellp(); + file.write(reinterpret_cast(header.data()), header.size()); + + centralDir.putInt32(CENTRAL_DIR_SIGNATURE); + centralDir.putInt16(10); // version + centralDir.putInt16(0); // version + centralDir.putInt16(0); // flags + centralDir.putInt16(0); // compression method + centralDir.putInt32(timestamp); // last modification datetime + centralDir.putInt32(crc); // crc32 + centralDir.putInt32(compressedSize); + centralDir.putInt32(srcSize); + centralDir.putInt16(name.length()); + centralDir.putInt16(0); // extra field length + centralDir.putInt16(0); // file comment length + centralDir.putInt16(0); // disk number start + centralDir.putInt16(0); // internal attributes + centralDir.putInt32(0); // external attributes + centralDir.putInt32(localHeaderOffset); // local header offset + centralDir.put(reinterpret_cast(name.data()), name.length()); +} + +static size_t write_zip( + const std::string& root, + const path& folder, + std::ostream& file, + ByteBuilder& centralDir +) { + size_t entries = 0; + ByteBuilder localHeader; + for (const auto& entry : io::directory_iterator(folder)) { + auto name = entry.pathPart().substr(root.length() + 1); + auto modificationTime = io::last_write_time(entry); + if (io::is_directory(entry)) { + name = name + "/"; + write_headers(file, name, 0, 0, 0, modificationTime, centralDir); + entries += write_zip(root, entry, file, centralDir) + 1; + } else { + auto data = io::read_bytes_buffer(entry); + uint32_t crc = crc32(0, data.data(), data.size()); + write_headers( + file, + name, + data.size(), + data.size(), + crc, + modificationTime, + centralDir + ); + file.write(reinterpret_cast(data.data()), data.size()); + entries++; + } + } + return entries; +} + +void io::write_zip(const path& folder, const path& file) { + ByteBuilder centralDir; + auto out = io::write(file); + size_t entries = write_zip(folder.pathPart(), folder, *out, centralDir); + + size_t centralDirOffset = out->tellp(); + out->write(reinterpret_cast(centralDir.data()), centralDir.size()); + + ByteBuilder eocd; + eocd.putInt32(EOCD_SIGNATURE); + eocd.putInt16(0); // disk number + eocd.putInt16(0); // central dir disk + eocd.putInt16(entries); // num entries + eocd.putInt16(entries); // total entries + eocd.putInt32(centralDir.size()); // central dir size + eocd.putInt32(centralDirOffset); // central dir offset + eocd.putInt16(0); // comment length + out->write(reinterpret_cast(eocd.data()), eocd.size()); +} diff --git a/src/io/devices/ZipFileDevice.hpp b/src/io/devices/ZipFileDevice.hpp index 2598c750..69042821 100644 --- a/src/io/devices/ZipFileDevice.hpp +++ b/src/io/devices/ZipFileDevice.hpp @@ -40,6 +40,7 @@ namespace io { std::unique_ptr write(std::string_view path) override; std::unique_ptr read(std::string_view path) override; size_t size(std::string_view path) override; + io::file_time_type lastWriteTime(std::string_view path) override; bool exists(std::string_view path) override; bool isdir(std::string_view path) override; bool isfile(std::string_view path) override; @@ -56,4 +57,6 @@ namespace io { Entry readEntry(); void findBlob(Entry& entry); }; + + void write_zip(const path& folder, const path& file); } diff --git a/src/io/io.cpp b/src/io/io.cpp index 8ee9f8ce..2f4eab11 100644 --- a/src/io/io.cpp +++ b/src/io/io.cpp @@ -107,6 +107,14 @@ bool io::read(const io::path& filename, char* data, size_t size) { return stream->good(); } +std::unique_ptr io::write(const io::path& file) { + auto device = io::get_device(file.entryPoint()); + if (device == nullptr) { + throw std::runtime_error("io-device not found: " + file.entryPoint()); + } + return device->write(file.pathPart()); +} + std::unique_ptr io::read(const io::path& filename) { auto device = io::get_device(filename.entryPoint()); if (device == nullptr) { @@ -307,6 +315,11 @@ size_t io::file_size(const io::path& file) { return device.size(file.pathPart()); } +io::file_time_type io::last_write_time(const io::path& file) { + auto& device = io::require_device(file.entryPoint()); + return device.lastWriteTime(file.pathPart()); +} + std::filesystem::path io::resolve(const io::path& file) { auto device = io::get_device(file.entryPoint()); if (device == nullptr) { diff --git a/src/io/io.hpp b/src/io/io.hpp index 0b2f950d..5cc224be 100644 --- a/src/io/io.hpp +++ b/src/io/io.hpp @@ -142,6 +142,10 @@ namespace io { bool compressed = false ); + /// @brief Open file for writing + /// @throw std::runtime_error if file cannot be opened + std::unique_ptr write(const io::path& file); + /// @brief Open file for reading /// @throw std::runtime_error if file cannot be opened std::unique_ptr read(const io::path& file); @@ -202,6 +206,9 @@ namespace io { /// @brief Get file size in bytes size_t file_size(const io::path& file); + /// @brief Get file last write time timestamp + file_time_type last_write_time(const io::path& file); + std::filesystem::path resolve(const io::path& file); /// @brief Check if file is one of the supported data interchange formats diff --git a/src/io/path.hpp b/src/io/path.hpp index 5abc110e..538431b7 100644 --- a/src/io/path.hpp +++ b/src/io/path.hpp @@ -5,6 +5,8 @@ #include namespace io { + using file_time_type = std::filesystem::file_time_type; + /// @brief Access violation error class access_error : public std::runtime_error { public: