/* * Copyright (C) 2020 Emeric Poupon * * This file is part of LMS. * * LMS is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * LMS is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with LMS. If not, see . */ #include "ArchiveZipper.hpp" #include #include #include // strerror #include #include #include #include "core/ILogger.hpp" namespace lms::zip { std::unique_ptr createArchiveZipper(const EntryContainer& entries) { return std::make_unique(entries); } class FileException : public Exception { public: FileException(const std::filesystem::path& p, std::string_view message) : Exception{ "File '" + p.string() + "': " + std::string {message} } {} FileException(const std::filesystem::path& p, std::string_view message, int err) : Exception{ "File '" + p.string() + "': " + std::string {message} + ": " + ::strerror(err) } {} }; class ArchiveException : public Exception { public: ArchiveException(struct ::archive* arch) : Exception{ getError(arch) } {} static std::string_view getError(struct ::archive* arch) { const char* str{ archive_error_string(arch) }; if (!str) { static std::string unknownError{ "Unknown archive error" }; return unknownError; } return str; } }; void ArchiveZipper::ArchiveDeleter::operator()(struct ::archive* arch) { const int res{ ::archive_write_free(arch) }; if (res != ARCHIVE_OK) LMS_LOG(UTILS, ERROR, "Failure while freeing archive control struct: " << std::string{ ::strerror(res) }); } void ArchiveZipper::ArchiveEntryDeleter::operator()(struct ::archive_entry* archEntry) { ::archive_entry_free(archEntry); } ArchiveZipper::ArchiveZipper(const EntryContainer& entries) : _entries{ entries } , _readBuffer(_readBufferSize, {}) , _currentEntry{ std::cbegin(_entries) } { _archive = ArchivePtr{ ::archive_write_new() }; if (!_archive) throw Exception{ "Cannot create archive control struct" }; auto archiveOpen{ [](struct ::archive*, void*) { return ARCHIVE_OK; } }; auto archiveWrite{ [](struct ::archive*, void* clientData, const void* buff, ::size_t n) -> la_ssize_t { ArchiveZipper* zipper {static_cast(clientData)}; return zipper->onWriteCallback(static_cast(buff), n); } }; auto archiveClose{ [](struct ::archive*, void*) { return ARCHIVE_OK; } }; if (::archive_write_set_bytes_per_block(_archive.get(), _writeBlockSize) != ARCHIVE_OK) throw ArchiveException{ _archive.get() }; // 1 => no padding for last block if (::archive_write_set_bytes_in_last_block(_archive.get(), 1) != ARCHIVE_OK) throw ArchiveException{ _archive.get() }; if (::archive_write_set_format_zip(_archive.get()) != ARCHIVE_OK) throw ArchiveException{ _archive.get() }; if (::archive_write_set_option(_archive.get(), "zip", "compression", "deflate") != ARCHIVE_OK) throw ArchiveException{ _archive.get() }; int res{ ::archive_write_open(_archive.get(), this, archiveOpen, archiveWrite, archiveClose) }; if (res != ARCHIVE_OK) throw ArchiveException{ _archive.get() }; } std::uint64_t ArchiveZipper::writeSome(std::ostream& output) { assert(!_currentOutputStream); _currentOutputStream = &output; _bytesWrittenInCurrentOutputStream = 0; while (_bytesWrittenInCurrentOutputStream == 0) { if (!_currentArchiveEntry) { if (_currentEntry == std::cend(_entries)) { if (::archive_write_close(_archive.get()) != ARCHIVE_OK) throw ArchiveException{ _archive.get() }; _archive.reset(); break; } _currentArchiveEntry = createArchiveEntry(*_currentEntry); _currentEntryOffset = 0; if (::archive_write_header(_archive.get(), _currentArchiveEntry.get()) != ARCHIVE_OK) throw ArchiveException{ _archive.get() }; } if (writeSomeCurrentFileData()) { // entry complete if (::archive_write_finish_entry(_archive.get()) != ARCHIVE_OK) throw ArchiveException{ _archive.get() }; _currentArchiveEntry.reset(); _currentEntry++; } } _currentOutputStream = nullptr; return _bytesWrittenInCurrentOutputStream; } bool ArchiveZipper::isComplete() const { return !_archive; } void ArchiveZipper::abort() { LMS_LOG(UTILS, DEBUG, "Aborting zip creation"); if (_archive) { ::archive_write_fail(_archive.get()); _archive.reset(); } } static ::mode_t permsToMode(const std::filesystem::perms p) { using std::filesystem::perms; ::mode_t mode{}; auto testPerm{ [](perms p, perms permToTest) { return (p & permToTest) == permToTest; } }; if (testPerm(p, perms::owner_read)) mode |= S_IRUSR; if (testPerm(p, perms::owner_write)) mode |= S_IWUSR; if (testPerm(p, perms::owner_exec)) mode |= S_IXUSR; if (testPerm(p, perms::group_read)) mode |= S_IRGRP; if (testPerm(p, perms::group_write)) mode |= S_IWGRP; if (testPerm(p, perms::group_exec)) mode |= S_IXGRP; if (testPerm(p, perms::others_read)) mode |= S_IROTH; if (testPerm(p, perms::others_write)) mode |= S_IWOTH; if (testPerm(p, perms::others_exec)) mode |= S_IXOTH; return mode; } ArchiveZipper::ArchiveEntryPtr ArchiveZipper::createArchiveEntry(const Entry& entry) { try { if (!std::filesystem::is_regular_file(entry.filePath)) throw FileException{ entry.filePath, "not a regular file" }; ArchiveEntryPtr archiveEntry{ archive_entry_new() }; if (!archiveEntry) throw Exception{ "Cannot create archive entry control struct" }; archive_entry_set_pathname(archiveEntry.get(), entry.fileName.c_str()); archive_entry_set_size(archiveEntry.get(), std::filesystem::file_size(entry.filePath)); archive_entry_set_mode(archiveEntry.get(), permsToMode(std::filesystem::status(entry.filePath).permissions())); archive_entry_set_filetype(archiveEntry.get(), AE_IFREG); return archiveEntry; } catch (const std::filesystem::filesystem_error& error) { throw FileException{ entry.filePath, error.what() }; } } bool ArchiveZipper::writeSomeCurrentFileData() { assert(_currentEntry != std::cend(_entries)); std::ifstream ifs{ _currentEntry->filePath.c_str(), std::ios_base::binary }; if (!ifs) throw FileException{ _currentEntry->filePath, "cannot open file", errno }; ifs.seekg(0, std::ios::end); const std::uint64_t fileSize{ static_cast(ifs.tellg()) }; ifs.seekg(0, std::ios::beg); // TODO: store file size? if (fileSize < _currentEntryOffset) throw FileException{ _currentEntry->filePath, "size changed?" }; const std::uint64_t bytesToRead{ std::min(fileSize - _currentEntryOffset, static_cast(_readBufferSize)) }; // read from file if (!ifs.seekg(_currentEntryOffset, std::ios::beg)) throw FileException{ _currentEntry->filePath, "seek failed", errno }; if (!ifs.read(reinterpret_cast(&_readBuffer[0]), bytesToRead)) throw FileException{ _currentEntry->filePath, "read failed", errno }; const std::uint64_t actualBytesRead{ static_cast(ifs.gcount()) }; // write to archive { std::uint64_t remainingBytesToWrite{ actualBytesRead }; while (remainingBytesToWrite > 0) { const auto writtenBytes{ archive_write_data(_archive.get(), &_readBuffer[actualBytesRead - remainingBytesToWrite], remainingBytesToWrite) }; if (writtenBytes < 0) throw ArchiveException{ _archive.get() }; assert(static_cast(writtenBytes) <= remainingBytesToWrite); remainingBytesToWrite -= writtenBytes; } } _currentEntryOffset += actualBytesRead; return (_currentEntryOffset >= fileSize); } std::int64_t ArchiveZipper::onWriteCallback(const std::byte* buffer, std::size_t bufferSize) { if (!_currentOutputStream) { archive_set_error(_archive.get(), EIO, "IO error: operation cancelled"); return -1; } _currentOutputStream->write(reinterpret_cast(buffer), bufferSize); if (!*_currentOutputStream) throw Exception{ "Failed to write " + std::to_string(bufferSize) + " bytes in final archive output!" }; _bytesWrittenInCurrentOutputStream += bufferSize; return bufferSize; } }