diff options
-rw-r--r-- | src/libs/installer/libarchivearchive.cpp | 268 | ||||
-rw-r--r-- | tests/auto/installer/libarchivearchive/tst_libarchivearchive.cpp | 66 |
2 files changed, 297 insertions, 37 deletions
diff --git a/src/libs/installer/libarchivearchive.cpp b/src/libs/installer/libarchivearchive.cpp index 09bcb7544..b912f19df 100644 --- a/src/libs/installer/libarchivearchive.cpp +++ b/src/libs/installer/libarchivearchive.cpp @@ -64,6 +64,184 @@ namespace QInstaller { \internal */ +namespace ArchiveEntryPaths { + +// TODO: it is expected that the filename handling will change in the major +// version jump to libarchive 4.x, and the *_w methods will disappear. + +/*! + \internal + + Returns the path name from the archive \a entry as a \c QString. The path is + stored internally in a multistring that can contain a combination of a wide + character or multibyte string in current locale or unicode string encoded as UTF-8. + + \note The MBS version is expected to be convertable from latin-1 which might + not actually be the case, as the encoding depends on the current locale. +*/ +static QString pathname(archive_entry *const entry) +{ + if (!entry) + return QString(); +#ifdef Q_OS_WIN + if (const wchar_t *path = archive_entry_pathname_w(entry)) + return QString::fromWCharArray(path); +#endif + if (const char *path = archive_entry_pathname_utf8(entry)) + return QString::fromUtf8(path); + + return QString::fromLatin1(archive_entry_pathname(entry)); +} + +/*! + \internal + + Sets the path name for the archive \a entry to \a path. This function + tries to update all variants of the string in the internal multistring + struct. +*/ +static void setPathname(archive_entry *const entry, const QString &path) +{ + if (!entry) + return; + + // Try updating all variants at once, stops on failure + if (archive_entry_update_pathname_utf8(entry, path.toUtf8())) + return; + + // If that does not work, then set them individually + archive_entry_set_pathname(entry, path.toLatin1()); + archive_entry_set_pathname_utf8(entry, path.toUtf8()); +#ifdef Q_OS_WIN + wchar_t *wpath = new wchar_t[path.length() + 1]; + path.toWCharArray(wpath); + wpath[path.length()] = '\0'; + + archive_entry_copy_pathname_w(entry, wpath); + delete[] wpath; +#endif +} + +/*! + \internal + + Returns the source path on disk from the current archive \a entry as a \c QString. + The path is stored internally in a multistring that can contain a combination + of a wide character or multibyte string in current locale. + + \note The MBS version is expected to be convertable from UTF-8. +*/ +static QString sourcepath(archive_entry * const entry) +{ + if (!entry) + return QString(); +#ifdef Q_OS_WIN + if (const wchar_t *path = archive_entry_sourcepath_w(entry)) + return QString::fromWCharArray(path); +#endif + return QString::fromUtf8(archive_entry_sourcepath(entry)); +} + +/*! + \internal + + Returns the hardlink path from the current archive \a entry as a \c QString. + The path is stored internally in a multistring that can contain a combination of a wide + character or multibyte string in current locale or unicode string encoded as UTF-8. + + \note The MBS version is expected to be convertable from latin-1 which might + not actually be the case, as the encoding depends on the current locale. +*/ +static QString hardlink(archive_entry * const entry) +{ + if (!entry) + return QString(); +#ifdef Q_OS_WIN + if (const wchar_t *path = archive_entry_hardlink_w(entry)) + return QString::fromWCharArray(path); +#endif + if (const char *path = archive_entry_hardlink_utf8(entry)) + return QString::fromUtf8(path); + + return QString::fromLatin1(archive_entry_hardlink(entry)); +} + +/*! + \internal + + Sets the hard link path for the archive \a entry to \a path. This function + tries to update all variants of the string in the internal multistring + struct. +*/ +static void setHardlink(archive_entry *const entry, const QString &path) +{ + if (!entry) + return; + + // Try updating all variants at once, stops on failure + if (archive_entry_update_hardlink_utf8(entry, path.toUtf8())) + return; + + // If that does not work, then set them individually + archive_entry_set_hardlink(entry, path.toLatin1()); + archive_entry_set_hardlink_utf8(entry, path.toUtf8()); +#ifdef Q_OS_WIN + wchar_t *wpath = new wchar_t[path.length() + 1]; + path.toWCharArray(wpath); + wpath[path.length()] = '\0'; + + archive_entry_copy_hardlink_w(entry, wpath); + delete[] wpath; +#endif +} + +/*! + \internal + + Calls a function object or pointer \a func with any number of extra + arguments \a args. Returns the return value of the function of type T. + + On Windows, this changes the locale category LC_CTYPE from C to system + locale. The original LC_CTYPE is restored after the function call. + Currently the locale is unchanged on other platforms. +*/ +template <typename T, typename F, typename... Args> +static T callWithSystemLocale(F func, Args... args) +{ +#ifdef Q_OS_WIN + const QByteArray oldLocale = setlocale(LC_CTYPE, ""); +#endif + T returnValue = func(std::forward<Args>(args)...); +#ifdef Q_OS_WIN + setlocale(LC_CTYPE, oldLocale.constData()); +#endif + return returnValue; +} + +/*! + \internal + + Calls a function object or pointer \a func with any number of extra + arguments \a args. + + On Windows, this changes the locale category LC_CTYPE from C to system + locale. The original LC_CTYPE is restored after the function call. + Currently the locale is unchanged on other platforms. +*/ +template <typename F, typename... Args> +static void callWithSystemLocale(F func, Args... args) +{ +#ifdef Q_OS_WIN + const QByteArray oldLocale = setlocale(LC_CTYPE, ""); +#endif + func(std::forward<Args>(args)...); +#ifdef Q_OS_WIN + setlocale(LC_CTYPE, oldLocale.constData()); +#endif +} + +} // namespace ArchiveEntryPaths + /*! \inmodule QtInstallerFramework \class QInstaller::ExtractWorker @@ -116,7 +294,7 @@ void ExtractWorker::extract(const QString &dirPath, const quint64 totalFiles) emit finished(QLatin1String("Extract canceled.")); return; } - status = archive_read_next_header(reader.get(), &entry); + status = ArchiveEntryPaths::callWithSystemLocale<int>(archive_read_next_header, reader.get(), &entry); if (status == ARCHIVE_EOF) break; if (status != ARCHIVE_OK) { @@ -124,14 +302,14 @@ void ExtractWorker::extract(const QString &dirPath, const quint64 totalFiles) emit finished(LibArchiveArchive::errorStringWithCode(reader.get())); return; } - const char *current = archive_entry_pathname(entry); - const QString outputPath = dirPath + QDir::separator() + QString::fromLocal8Bit(current); - archive_entry_set_pathname(entry, outputPath.toLocal8Bit()); - - const char *hardlink = archive_entry_hardlink(entry); - if (hardlink) { - const QString hardLinkPath = dirPath + QDir::separator() + QString::fromLocal8Bit(hardlink); - archive_entry_set_hardlink(entry, hardLinkPath.toLocal8Bit()); + const QString current = ArchiveEntryPaths::callWithSystemLocale<QString>(ArchiveEntryPaths::pathname, entry); + const QString outputPath = dirPath + QDir::separator() + current; + ArchiveEntryPaths::callWithSystemLocale(&ArchiveEntryPaths::setPathname, entry, outputPath); + + const QString hardlink = ArchiveEntryPaths::callWithSystemLocale<QString>(ArchiveEntryPaths::hardlink, entry); + if (!hardlink.isEmpty()) { + const QString hardLinkPath = dirPath + QDir::separator() + hardlink; + ArchiveEntryPaths::callWithSystemLocale(ArchiveEntryPaths::setHardlink, entry, hardLinkPath); } emit currentEntryChanged(outputPath); @@ -441,20 +619,20 @@ bool LibArchiveArchive::extract(const QString &dirPath, const quint64 totalFiles if (m_cancelScheduled) throw Error(QLatin1String("Extract canceled.")); - status = archive_read_next_header(reader.get(), &entry); + status = ArchiveEntryPaths::callWithSystemLocale<int>(archive_read_next_header, reader.get(), &entry); if (status == ARCHIVE_EOF) break; if (status != ARCHIVE_OK) throw Error(errorStringWithCode(reader.get())); - const char *current = archive_entry_pathname(entry); - const QString outputPath = dirPath + QDir::separator() + QString::fromLocal8Bit(current); - archive_entry_set_pathname(entry, outputPath.toLocal8Bit()); + const QString current = ArchiveEntryPaths::callWithSystemLocale<QString>(ArchiveEntryPaths::pathname, entry); + const QString outputPath = dirPath + QDir::separator() + current; + ArchiveEntryPaths::callWithSystemLocale(ArchiveEntryPaths::setPathname, entry, outputPath); - const char *hardlink = archive_entry_hardlink(entry); - if (hardlink) { - const QString hardLinkPath = dirPath + QDir::separator() + QString::fromLocal8Bit(hardlink); - archive_entry_set_hardlink(entry, hardLinkPath.toLocal8Bit()); + const QString hardlink = ArchiveEntryPaths::callWithSystemLocale<QString>(ArchiveEntryPaths::hardlink, entry); + if (!hardlink.isEmpty()) { + const QString hardLinkPath = dirPath + QDir::separator() + hardlink; + ArchiveEntryPaths::callWithSystemLocale(ArchiveEntryPaths::setHardlink, entry, hardLinkPath); } emit currentEntryChanged(outputPath); @@ -508,29 +686,50 @@ bool LibArchiveArchive::create(const QStringList &data) try { int status; - if ((status = archive_write_open_filename(writer.get(), m_data->file.fileName().toLocal8Bit()))) - throw Error(errorStringWithCode(writer.get())); +#ifdef Q_OS_WIN + QScopedPointer<wchar_t, QScopedPointerArrayDeleter<wchar_t>> fileName_w( + new wchar_t[m_data->file.fileName().length() + 1]); + + m_data->file.fileName().toWCharArray(fileName_w.get()); + fileName_w.get()[m_data->file.fileName().length()] = '\0'; + if ((status = archive_write_open_filename_w(writer.get(), fileName_w.get()))) + throw Error(errorStringWithCode(writer.get())); +#else + if ((status = archive_write_open_filename(writer.get(), m_data->file.fileName().toUtf8()))) + throw Error(errorStringWithCode(writer.get())); +#endif for (auto &dataEntry : globbedData) { QScopedPointer<archive, ScopedPointerReaderDeleter> reader(archive_read_disk_new()); configureDiskReader(reader.get()); - if ((status = archive_read_disk_open(reader.get(), dataEntry.toLocal8Bit()))) - throw Error(errorStringWithCode(reader.get())); +#ifdef Q_OS_WIN + QScopedPointer<wchar_t, QScopedPointerArrayDeleter<wchar_t>> dataEntry_w( + new wchar_t[dataEntry.length() + 1]); + + dataEntry.toWCharArray(dataEntry_w.get()); + dataEntry_w.get()[dataEntry.length()] = '\0'; + if ((status = archive_read_disk_open_w(reader.get(), dataEntry_w.get()))) + throw Error(errorStringWithCode(reader.get())); +#else + if ((status = archive_read_disk_open(reader.get(), dataEntry.toUtf8()))) + throw Error(errorStringWithCode(reader.get())); +#endif QDir basePath = QFileInfo(dataEntry).dir(); forever { QScopedPointer<archive_entry, ScopedPointerEntryDeleter> entry(archive_entry_new()); - status = archive_read_next_header2(reader.get(), entry.get()); + status = ArchiveEntryPaths::callWithSystemLocale<int>(archive_read_next_header2, reader.get(), entry.get()); if (status == ARCHIVE_EOF) break; if (status != ARCHIVE_OK) throw Error(errorStringWithCode(reader.get())); - const QFileInfo fileOrDir(pathWithoutNamespace(QLatin1String(archive_entry_sourcepath(entry.get())))); + const QFileInfo fileOrDir(pathWithoutNamespace( + ArchiveEntryPaths::callWithSystemLocale<QString>(ArchiveEntryPaths::sourcepath, entry.get()))); // Set new path name in archive, otherwise we add all directories from absolute path const QString newPath = basePath.relativeFilePath(fileOrDir.filePath()); - archive_entry_set_pathname(entry.get(), newPath.toLocal8Bit()); + ArchiveEntryPaths::callWithSystemLocale(ArchiveEntryPaths::setPathname, entry.get(), newPath); archive_read_disk_descend(reader.get()); status = archive_write_header(writer.get(), entry.get()); @@ -540,7 +739,8 @@ bool LibArchiveArchive::create(const QStringList &data) if (fileOrDir.isDir() || archive_entry_size(entry.get()) == 0) continue; // nothing to copy - QFile file(pathWithoutNamespace(QLatin1String(archive_entry_sourcepath(entry.get())))); + QFile file(pathWithoutNamespace(ArchiveEntryPaths::callWithSystemLocale<QString>( + ArchiveEntryPaths::sourcepath, entry.get()))); if (!file.open(QIODevice::ReadOnly)) throw Error(file.errorString()); @@ -583,14 +783,14 @@ QVector<ArchiveEntry> LibArchiveArchive::list() throw Error(errorStringWithCode(reader.get())); forever { - status = archive_read_next_header(reader.get(), &entry); + status = ArchiveEntryPaths::callWithSystemLocale<int>(archive_read_next_header, reader.get(), &entry); if (status == ARCHIVE_EOF) break; if (status != ARCHIVE_OK) throw Error(errorStringWithCode(reader.get())); ArchiveEntry archiveEntry; - archiveEntry.path = QLatin1String(archive_entry_pathname(entry)); + archiveEntry.path = ArchiveEntryPaths::callWithSystemLocale<QString>(ArchiveEntryPaths::pathname, entry); archiveEntry.utcTime = QDateTime::fromTime_t(archive_entry_mtime(entry)); archiveEntry.isDirectory = (archive_entry_filetype(entry) == AE_IFDIR); archiveEntry.isSymbolicLink = (archive_entry_filetype(entry) == AE_IFLNK); @@ -724,15 +924,19 @@ void LibArchiveArchive::configureWriter(archive *archive) // The Qt board support package file extension is really a 7z. archive_write_set_format_7zip(archive); } else { - archive_write_set_format_filter_by_ext(archive, fileName.toLatin1()); + archive_write_set_format_filter_by_ext(archive, fileName.toUtf8()); } + const QByteArray charset = "hdrcharset=UTF-8"; + // not checked as this is ignored on some archive formats like 7z + archive_write_set_options(archive, charset); + if (compressionLevel() == CompressionLevel::Normal) return; - const QByteArray options = "compression-level=" + QString::number(compressionLevel()).toLatin1(); - if (archive_write_set_options(archive, options.constData())) { // not fatal - qCWarning(QInstaller::lcInstallerInstallLog) << "Could not set options" << options + const QByteArray compression = "compression-level=" + QString::number(compressionLevel()).toLatin1(); + if (archive_write_set_options(archive, compression.constData())) { // not fatal + qCWarning(QInstaller::lcInstallerInstallLog) << "Could not set option" << compression << "for archive" << m_data->file.fileName() << ":" << errorStringWithCode(archive); } } @@ -964,7 +1168,7 @@ quint64 LibArchiveArchive::totalFiles() throw Error(errorStringWithCode(reader.get())); forever { - status = archive_read_next_header(reader.get(), &entry); + status = ArchiveEntryPaths::callWithSystemLocale<int>(archive_read_next_header, reader.get(), &entry); if (status == ARCHIVE_EOF) break; if (status != ARCHIVE_OK) diff --git a/tests/auto/installer/libarchivearchive/tst_libarchivearchive.cpp b/tests/auto/installer/libarchivearchive/tst_libarchivearchive.cpp index 57f16b762..ab7030e2f 100644 --- a/tests/auto/installer/libarchivearchive/tst_libarchivearchive.cpp +++ b/tests/auto/installer/libarchivearchive/tst_libarchivearchive.cpp @@ -99,8 +99,10 @@ private slots: const QString filename = generateTemporaryFileName() + suffix; LibArchiveArchive target(filename); - QVERIFY(target.open(QIODevice::ReadWrite)); + QVERIFY(target.open(QIODevice::WriteOnly)); QVERIFY(target.create(QStringList() << path1 << path2)); + target.close(); + QVERIFY(target.open(QIODevice::ReadOnly)); QCOMPARE(target.list().count(), 2); target.close(); QVERIFY(QFile(filename).remove()); @@ -115,7 +117,7 @@ private slots: { QFETCH(QString, suffix); - const QString baseDir(QDir::tempPath() + "/tst_libarchivearchive"); + const QString baseDir(generateTemporaryFileName(QDir::tempPath() + "/tst_libarchivearchive.XXXXXX")); QVERIFY(QDir().mkpath(baseDir)); const QString path1 = tempSourceFile( @@ -129,8 +131,10 @@ private slots: const QString filename = generateTemporaryFileName() + suffix; LibArchiveArchive target(filename); - QVERIFY(target.open(QIODevice::ReadWrite)); + QVERIFY(target.open(QIODevice::WriteOnly)); QVERIFY(target.create(QStringList() << baseDir + "/*")); + target.close(); + QVERIFY(target.open(QIODevice::ReadOnly)); QCOMPARE(target.list().count(), 2); target.close(); @@ -159,8 +163,10 @@ private slots: const QString filename = QDir::tempPath() + "/target file with spaces" + suffix; LibArchiveArchive target(filename); target.setFilename(filename); - QVERIFY(target.open(QIODevice::ReadWrite)); + QVERIFY(target.open(QIODevice::WriteOnly)); QVERIFY(target.create(QStringList() << path1 << path2)); + target.close(); + QVERIFY(target.open(QIODevice::ReadOnly)); QCOMPARE(target.list().count(), 2); target.close(); QVERIFY(QFile(filename).remove()); @@ -211,10 +217,12 @@ private slots: QVERIFY(QFile::link(source.fileName(), linkName)); LibArchiveArchive archive(archiveName); - QVERIFY(archive.open(QIODevice::ReadWrite)); + QVERIFY(archive.open(QIODevice::WriteOnly)); QVERIFY(archive.create(QStringList() << source.fileName() << linkName)); QVERIFY(QFileInfo::exists(archiveName)); + archive.close(); + QVERIFY(archive.open(QIODevice::ReadOnly)); QVERIFY(archive.extract(targetName)); const QString sourceFilename = QFileInfo(source.fileName()).fileName(); const QString linkFilename = QFileInfo(linkName).fileName(); @@ -237,6 +245,54 @@ private slots: removeDirectory(workingDir, true); } + void testCreateExtractWithUnicodePaths_data() + { + archiveSuffixesTestData(); + } + + void testCreateExtractWithUnicodePaths() + { + QFETCH(QString, suffix); + + const QString targetName = generateTemporaryFileName() + QDir::separator(); + const QString archiveName = QDir::tempPath() + "/test_archive" + suffix; + + const QString path1 = tempSourceFile( + "Source File 1.", + QDir::tempPath() + QString::fromUtf8("/测试文件.XXXXXX") + ); + const QString path2 = tempSourceFile( + "Source File 2.", + QDir::tempPath() + QString::fromUtf8("/тестовый файл.XXXXXX") + ); + const QString path3 = tempSourceFile( + "Source File 3.", + QDir::tempPath() + QString::fromUtf8("/ملف الاختبار.XXXXXX") + ); + + LibArchiveArchive archive(archiveName); + archive.setFilename(archiveName); + QVERIFY(archive.open(QIODevice::WriteOnly)); + QVERIFY(archive.create(QStringList() << path1 << path2 << path3)); + archive.close(); + QVERIFY(archive.open(QIODevice::ReadOnly)); + QCOMPARE(archive.list().count(), 3); + + QVERIFY(archive.extract(targetName)); + + const QString targetPath1 = targetName + QFileInfo(path1).fileName(); + const QString targetPath2 = targetName + QFileInfo(path2).fileName(); + const QString targetPath3 = targetName + QFileInfo(path3).fileName(); + + QVERIFY(QFileInfo::exists(targetPath1)); + QVERIFY(QFileInfo::exists(targetPath2)); + QVERIFY(QFileInfo::exists(targetPath3)); + + archive.close(); + QVERIFY(QFile::remove(archiveName)); + QVERIFY(QDir(targetName).removeRecursively()); + } + private: void archiveFilenamesTestData() { |