diff options
author | Arttu Tarkiainen <arttu.tarkiainen@qt.io> | 2022-02-03 17:26:57 +0200 |
---|---|---|
committer | Arttu Tarkiainen <arttu.tarkiainen@qt.io> | 2022-02-17 14:50:12 +0200 |
commit | 1941fe9a1ade23753bdb0aa3da2fac0daa838d1c (patch) | |
tree | d12518630919b4b5fe38ebc667909ed4f408c898 /src/libs | |
parent | b6cccb7b317313a69ff8b0688eed8430e76d66a6 (diff) |
Fix extracting entries with file paths that contain unicode characters
Libarchive stores most paths internally in a multistring that can
contain a combination of a wide character string, multibyte string in
current locale or unicode string encoded as UTF-8. Try to retrieve and
set a variant of the string with the correct encoding when converting
libarchive strings to/from Qt string classes.
Task-number: QTIFW-2506
Change-Id: I7e7c1e3c5709b669df68b2f3918789ad4ebe4f13
Reviewed-by: Katja Marttila <katja.marttila@qt.io>
Diffstat (limited to 'src/libs')
-rw-r--r-- | src/libs/installer/libarchivearchive.cpp | 268 |
1 files changed, 236 insertions, 32 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) |