/* OSCAR ZIP archive creation * Provides a Qt-convenient wrapper around miniz, see https://github.com/richgel999/miniz * * Copyright (c) 2020-2024 The OSCAR Team * * This file is subject to the terms and conditions of the GNU General Public * License. See the file COPYING in the main directory of the source code * for more details. */ #include "zip.h" #include #include #include #include "SleepLib/progressdialog.h" static const quint64 PROGRESS_SCALE = 1024; // QProgressBar only holds an int, so report progress in KiB. // Static functions to abstract the details of miniz from the primary logic. static void* zip_init(); static bool zip_open(void* ctx, QFile & file); static bool zip_add(void* ctx, const QString & archive_name, const QByteArray & data, const QDateTime & modified); static void zip_close(void* ctx); static void zip_done(void* ctx); ZipFile::ZipFile() { m_ctx = zip_init(); } ZipFile::~ZipFile() { Close(); zip_done(m_ctx); } bool ZipFile::Open(const QString & filepath) { m_file.setFileName(filepath); bool ok = m_file.open(QIODevice::WriteOnly); if (!ok) { qWarning() << "Could not open" << m_file.fileName() << "for writing, error code" << m_file.error() << m_file.errorString(); // qWarning() << "unable to open" << m_file.fileName(); return false; } ok = zip_open(m_ctx, m_file); return ok; } void ZipFile::Close() { if (m_file.isOpen()) { zip_close(m_ctx); m_file.close(); } } bool ZipFile::AddDirectory(const QString & path, ProgressDialog* progress) { return AddDirectory(path, "", progress); } bool ZipFile::AddDirectory(const QString & path, const QString & prefix, ProgressDialog* progress) { bool ok; FileQueue queue; queue.AddDirectory(path, prefix); ok = AddFiles(queue, progress); return ok; } bool ZipFile::AddFiles(FileQueue & queue, ProgressDialog* progress) { bool ok; // Exclude the zip file that's being created (if it happens to be in the list). queue.Remove(QFileInfo(m_file).canonicalFilePath()); qDebug().noquote() << "Adding" << queue.toString(); m_abort = false; m_progress = 0; if (progress) { progress->addAbortButton(); progress->setWindowModality(Qt::ApplicationModal); progress->open(); connect(this, SIGNAL(setProgressMax(int)), progress, SLOT(setProgressMax(int))); connect(this, SIGNAL(setProgressValue(int)), progress, SLOT(setProgressValue(int))); connect(progress, SIGNAL(abortClicked()), this, SLOT(abort())); } // Always emit, since the caller may have configured and connected a progress dialog manually. emit setProgressValue(m_progress/PROGRESS_SCALE); emit setProgressMax((queue.byteCount() + queue.dirCount())/PROGRESS_SCALE); QCoreApplication::processEvents(); for (auto & entry : queue.files()) { ok = AddFile(entry.path, entry.name); if (!ok || m_abort) { break; } } if (progress) { disconnect(progress, SIGNAL(abortClicked()), this, SLOT(abort())); disconnect(this, SIGNAL(setProgressMax(int)), progress, SLOT(setProgressMax(int))); disconnect(this, SIGNAL(setProgressValue(int)), progress, SLOT(setProgressValue(int))); progress->close(); progress->deleteLater(); } if (!ok) { qWarning().noquote() << "Unable to create" << m_file.fileName(); Close(); m_file.remove(); } else if (aborted()) { qDebug().noquote() << "User canceled zip creation."; Close(); m_file.remove(); } else { qDebug().noquote() << "Created" << m_file.fileName() << m_file.size() << "bytes"; } return ok; } bool ZipFile::AddFile(const QString & path, const QString & name) { if (!m_file.isOpen()) { qWarning() << m_file.fileName() << "has not been opened for writing"; return false; } QFileInfo fi(path); QByteArray data; QString archive_name = name; if (archive_name.isEmpty()) archive_name = fi.fileName(); if (fi.isDir()) { archive_name += "/"; m_progress += 1; } else { // Open and read file into memory. QFile f(path); if (!f.open(QIODevice::ReadOnly)) { qWarning() << path << "can't open"; return false; } data = f.readAll(); m_progress += data.size(); } //qDebug() << "attempting to add" << archive_name << ":" << data.size() << "bytes"; bool ok = zip_add(m_ctx, archive_name, data, fi.lastModified()); emit setProgressValue(m_progress/PROGRESS_SCALE); QCoreApplication::processEvents(); return ok; } // ================================================================================================== bool FileQueue::AddDirectory(const QString & path, const QString & prefix) { QDir dir(path); if (!dir.exists() || !dir.isReadable()) { qWarning() << dir.canonicalPath() << "can't read directory"; #if defined(Q_OS_MACOS) // If this is a directory known to be protected by macOS "Full Disk Access" permissions, // skip it but don't consider it an error. static const QSet s_macProtectedDirs = { ".fseventsd", ".Spotlight-V100", ".Trashes" }; if (s_macProtectedDirs.contains(dir.dirName())) { return true; } #endif return false; } QString base = prefix; if (base.isEmpty()) base = dir.dirName(); // Add directory entry bool ok = AddFile(dir.canonicalPath(), base); if (!ok) { return false; } dir.setFilter(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files | QDir::Hidden); dir.setSorting(QDir::Name); QFileInfoList flist = dir.entryInfoList(); for (auto & fi : flist) { QString canonicalPath = fi.canonicalFilePath(); QString relative_path = base + "/" + fi.fileName(); if (fi.isSymLink()) { qWarning() << "skipping symlink" << canonicalPath << fi.symLinkTarget(); } else if (fi.isDir()) { // Descend and recurse ok &= AddDirectory(canonicalPath, relative_path); } else { // Add the file to the zip ok &= AddFile(canonicalPath, relative_path); } // Don't stop in our tracks when we hit an error. } return ok; } bool FileQueue::AddFile(const QString & path, const QString & prefix) { QFileInfo fi(path); QString canonicalPath = fi.canonicalFilePath(); QString archive_name = prefix; if (archive_name.isEmpty()) archive_name = fi.fileName(); if (fi.isDir()) { m_dir_count++; } else if (fi.exists()) { m_file_count++; m_byte_count += fi.size(); } else { qWarning() << "file doesn't exist" << canonicalPath; return false; } Entry entry = { canonicalPath, archive_name }; m_files.append(entry); QCoreApplication::processEvents(); return true; } int FileQueue::Remove(const QString & path, QString* outName) { QFileInfo fi(path); QString canonicalPath = fi.canonicalFilePath(); int removed = 0; QMutableListIterator i(m_files); while (i.hasNext()) { Entry & entry = i.next(); if (entry.path == canonicalPath) { if (outName) { // If the caller cares about the name, it will most likely be re-added later rather than skipped. *outName = entry.name; } else { qDebug().noquote() << "skipping file:" << path; } if (fi.isDir()) { m_dir_count--; } else { m_file_count--; m_byte_count -= fi.size(); } i.remove(); removed++; } } if (removed > 1) { qWarning().noquote() << removed << "copies found in zip queue:" << path; } return removed; } const QString FileQueue::toString() const { return QString("%1 directories, %2 files, %3 bytes").arg(m_dir_count).arg(m_file_count).arg(m_byte_count); } // ================================================================================================== // Static functions to abstract the details of miniz from the primary logic. #include "SleepLib/thirdparty/miniz.h" // Callback for miniz to write compressed data static size_t zip_write(void *pOpaque, mz_uint64 /*file_ofs*/, const void *pBuf, size_t n) { if (pOpaque == nullptr) { qCritical() << "null pointer passed to ZipFile::Write!"; return 0; } QFile* file = (QFile*) pOpaque; size_t written = file->write((const char*) pBuf, n); if (written < n) { qWarning() << "error writing to" << file->fileName(); } return written; } static void* zip_init() { mz_zip_archive* pZip = new mz_zip_archive(); // zero-initializes struct pZip->m_pWrite = zip_write; return pZip; } static void zip_done(void* ctx) { Q_ASSERT(ctx); mz_zip_archive* pZip = (mz_zip_archive*) ctx; delete pZip; } static bool zip_open(void* ctx, QFile & file) { Q_ASSERT(ctx); mz_zip_archive* pZip = (mz_zip_archive*) ctx; pZip->m_pIO_opaque = &file; bool ok = mz_zip_writer_init_v2(pZip, 0, MZ_ZIP_FLAG_CASE_SENSITIVE); if (!ok) { mz_zip_error mz_err = mz_zip_get_last_error(pZip); qWarning() << "unable to initialize miniz writer" << MZ_VERSION << mz_zip_get_error_string(mz_err); } return ok; } static bool zip_add(void* ctx, const QString & archive_name, const QByteArray & data, const QDateTime & modified) { Q_ASSERT(ctx); mz_zip_archive* pZip = (mz_zip_archive*) ctx; // Add to .zip time_t last_modified = modified.toTime_t(); // technically deprecated, but miniz expects a time_t bool ok = mz_zip_writer_add_mem_ex_v2(pZip, archive_name.toLocal8Bit(), data.constData(), data.size(), nullptr, 0, // no comment MZ_DEFAULT_COMPRESSION, 0, 0, // not used when compressing data &last_modified, nullptr, 0, // no user extra data nullptr, 0 // no user extra data central ); if (!ok) { mz_zip_error mz_err = mz_zip_get_last_error(pZip); qWarning() << "unable to add" << archive_name << ":" << data.size() << "bytes" << mz_zip_get_error_string(mz_err); } return ok; } static void zip_close(void* ctx) { Q_ASSERT(ctx); mz_zip_archive* pZip = (mz_zip_archive*) ctx; mz_zip_writer_finalize_archive(pZip); mz_zip_writer_end(pZip); }