diff options
Diffstat (limited to 'src/gui/util/qktxhandler.cpp')
-rw-r--r-- | src/gui/util/qktxhandler.cpp | 303 |
1 files changed, 229 insertions, 74 deletions
diff --git a/src/gui/util/qktxhandler.cpp b/src/gui/util/qktxhandler.cpp index 31c572db54..35f1df1185 100644 --- a/src/gui/util/qktxhandler.cpp +++ b/src/gui/util/qktxhandler.cpp @@ -1,46 +1,11 @@ -/**************************************************************************** -** -** Copyright (C) 2017 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtGui module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2017 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qktxhandler_p.h" #include "qtexturefiledata_p.h" #include <QtEndian> #include <QSize> +#include <QMap> #include <QtCore/qiodevice.h> //#define KTX_DEBUG @@ -52,6 +17,8 @@ QT_BEGIN_NAMESPACE +using namespace Qt::StringLiterals; + #define KTX_IDENTIFIER_LENGTH 12 static const char ktxIdentifier[KTX_IDENTIFIER_LENGTH] = { '\xAB', 'K', 'T', 'X', ' ', '1', '1', '\xBB', '\r', '\n', '\x1A', '\n' }; static const quint32 platformEndianIdentifier = 0x04030201; @@ -74,7 +41,7 @@ struct KTXHeader { quint32 bytesOfKeyValueData; }; -static const quint32 headerSize = sizeof(KTXHeader); +static constexpr quint32 qktxh_headerSize = sizeof(KTXHeader); // Currently unused, declared for future reference struct KTXKeyValuePairItem { @@ -104,11 +71,32 @@ struct KTXMipmapLevel { */ }; +// Returns the nearest multiple of 4 greater than or equal to 'value' +static const std::optional<quint32> nearestMultipleOf4(quint32 value) +{ + constexpr quint32 rounding = 4; + quint32 result = 0; + if (qAddOverflow(value, rounding - 1, &result)) + return std::nullopt; + result &= ~(rounding - 1); + return result; +} + +// Returns a view with prechecked bounds +static QByteArrayView safeView(QByteArrayView view, quint32 start, quint32 length) +{ + quint32 end = 0; + if (qAddOverflow(start, length, &end) || end > quint32(view.length())) + return {}; + return view.sliced(start, length); +} + +QKtxHandler::~QKtxHandler() = default; + bool QKtxHandler::canRead(const QByteArray &suffix, const QByteArray &block) { Q_UNUSED(suffix); - - return (qstrncmp(block.constData(), ktxIdentifier, KTX_IDENTIFIER_LENGTH) == 0); + return block.startsWith(ktxIdentifier); } QTextureFileData QKtxHandler::read() @@ -116,43 +104,123 @@ QTextureFileData QKtxHandler::read() if (!device()) return QTextureFileData(); - QByteArray buf = device()->readAll(); - const quint32 dataSize = quint32(buf.size()); - if (dataSize < headerSize || !canRead(QByteArray(), buf)) { - qCDebug(lcQtGuiTextureIO, "Invalid KTX file %s", logName().constData()); + const QByteArray buf = device()->readAll(); + if (static_cast<size_t>(buf.size()) > std::numeric_limits<quint32>::max()) { + qWarning(lcQtGuiTextureIO, "Too big KTX file %s", logName().constData()); + return QTextureFileData(); + } + + if (!canRead(QByteArray(), buf)) { + qWarning(lcQtGuiTextureIO, "Invalid KTX file %s", logName().constData()); + return QTextureFileData(); + } + + if (buf.size() < qsizetype(qktxh_headerSize)) { + qWarning(lcQtGuiTextureIO, "Invalid KTX header size in %s", logName().constData()); return QTextureFileData(); } - const KTXHeader *header = reinterpret_cast<const KTXHeader *>(buf.constData()); - if (!checkHeader(*header)) { - qCDebug(lcQtGuiTextureIO, "Unsupported KTX file format in %s", logName().constData()); + KTXHeader header; + memcpy(&header, buf.data(), qktxh_headerSize); + if (!checkHeader(header)) { + qWarning(lcQtGuiTextureIO, "Unsupported KTX file format in %s", logName().constData()); return QTextureFileData(); } QTextureFileData texData; texData.setData(buf); - texData.setSize(QSize(decode(header->pixelWidth), decode(header->pixelHeight))); - texData.setGLFormat(decode(header->glFormat)); - texData.setGLInternalFormat(decode(header->glInternalFormat)); - texData.setGLBaseInternalFormat(decode(header->glBaseInternalFormat)); - - texData.setNumLevels(decode(header->numberOfMipmapLevels)); - texData.setNumFaces(1); - quint32 offset = headerSize + decode(header->bytesOfKeyValueData); - const int maxLevels = qMin(texData.numLevels(), 32); // Cap iterations in case of corrupt file. - for (int i = 0; i < maxLevels; i++) { - if (offset + sizeof(KTXMipmapLevel) > dataSize) // Corrupt file; avoid oob read - break; - const KTXMipmapLevel *level = reinterpret_cast<const KTXMipmapLevel *>(buf.constData() + offset); - quint32 levelLen = decode(level->imageSize); - texData.setDataOffset(offset + sizeof(KTXMipmapLevel::imageSize), i); - texData.setDataLength(levelLen, i); - offset += sizeof(KTXMipmapLevel::imageSize) + levelLen + (3 - ((levelLen + 3) % 4)); + texData.setSize(QSize(decode(header.pixelWidth), decode(header.pixelHeight))); + texData.setGLFormat(decode(header.glFormat)); + texData.setGLInternalFormat(decode(header.glInternalFormat)); + texData.setGLBaseInternalFormat(decode(header.glBaseInternalFormat)); + + texData.setNumLevels(decode(header.numberOfMipmapLevels)); + texData.setNumFaces(decode(header.numberOfFaces)); + + const quint32 bytesOfKeyValueData = decode(header.bytesOfKeyValueData); + quint32 headerKeyValueSize; + if (qAddOverflow(qktxh_headerSize, bytesOfKeyValueData, &headerKeyValueSize)) { + qWarning(lcQtGuiTextureIO, "Overflow in size of key value data in header of KTX file %s", + logName().constData()); + return QTextureFileData(); + } + + if (headerKeyValueSize >= quint32(buf.size())) { + qWarning(lcQtGuiTextureIO, "OOB request in KTX file %s", logName().constData()); + return QTextureFileData(); + } + + // File contains key/values + if (bytesOfKeyValueData > 0) { + auto keyValueDataView = safeView(buf, qktxh_headerSize, bytesOfKeyValueData); + if (keyValueDataView.isEmpty()) { + qWarning(lcQtGuiTextureIO, "Invalid view in KTX file %s", logName().constData()); + return QTextureFileData(); + } + + auto keyValues = decodeKeyValues(keyValueDataView); + if (!keyValues) { + qWarning(lcQtGuiTextureIO, "Could not parse key values in KTX file %s", + logName().constData()); + return QTextureFileData(); + } + + texData.setKeyValueMetadata(*keyValues); + } + + // Technically, any number of levels is allowed but if the value is bigger than + // what is possible in KTX V2 (and what makes sense) we return an error. + // maxLevels = log2(max(width, height, depth)) + const int maxLevels = (sizeof(quint32) * 8) + - qCountLeadingZeroBits(std::max( + { header.pixelWidth, header.pixelHeight, header.pixelDepth })); + + if (texData.numLevels() > maxLevels) { + qWarning(lcQtGuiTextureIO, "Too many levels in KTX file %s", logName().constData()); + return QTextureFileData(); + } + + if (texData.numFaces() != 1 && texData.numFaces() != 6) { + qWarning(lcQtGuiTextureIO, "Invalid number of faces in KTX file %s", logName().constData()); + return QTextureFileData(); + } + + quint32 offset = headerKeyValueSize; + for (int level = 0; level < texData.numLevels(); level++) { + const auto imageSizeView = safeView(buf, offset, sizeof(quint32)); + if (imageSizeView.isEmpty()) { + qWarning(lcQtGuiTextureIO, "OOB request in KTX file %s", logName().constData()); + return QTextureFileData(); + } + + const quint32 imageSize = decode(qFromUnaligned<quint32>(imageSizeView.data())); + offset += sizeof(quint32); // overflow checked indirectly above + + for (int face = 0; face < texData.numFaces(); face++) { + texData.setDataOffset(offset, level, face); + texData.setDataLength(imageSize, level, face); + + // Add image data and padding to offset + const auto padded = nearestMultipleOf4(imageSize); + if (!padded) { + qWarning(lcQtGuiTextureIO, "Overflow in KTX file %s", logName().constData()); + return QTextureFileData(); + } + + quint32 offsetNext; + if (qAddOverflow(offset, *padded, &offsetNext)) { + qWarning(lcQtGuiTextureIO, "OOB request in KTX file %s", logName().constData()); + return QTextureFileData(); + } + + offset = offsetNext; + } } if (!texData.isValid()) { - qCDebug(lcQtGuiTextureIO, "Invalid values in header of KTX file %s", logName().constData()); + qWarning(lcQtGuiTextureIO, "Invalid values in header of KTX file %s", + logName().constData()); return QTextureFileData(); } @@ -176,9 +244,12 @@ bool QKtxHandler::checkHeader(const KTXHeader &header) qDebug("Header of %s:", logName().constData()); qDebug(" glType: 0x%x (%s)", decode(header.glType), ptme.valueToKey(decode(header.glType))); qDebug(" glTypeSize: %u", decode(header.glTypeSize)); - qDebug(" glFormat: 0x%x (%s)", decode(header.glFormat), tfme.valueToKey(decode(header.glFormat))); - qDebug(" glInternalFormat: 0x%x (%s)", decode(header.glInternalFormat), tfme.valueToKey(decode(header.glInternalFormat))); - qDebug(" glBaseInternalFormat: 0x%x (%s)", decode(header.glBaseInternalFormat), tfme.valueToKey(decode(header.glBaseInternalFormat))); + qDebug(" glFormat: 0x%x (%s)", decode(header.glFormat), + tfme.valueToKey(decode(header.glFormat))); + qDebug(" glInternalFormat: 0x%x (%s)", decode(header.glInternalFormat), + tfme.valueToKey(decode(header.glInternalFormat))); + qDebug(" glBaseInternalFormat: 0x%x (%s)", decode(header.glBaseInternalFormat), + tfme.valueToKey(decode(header.glBaseInternalFormat))); qDebug(" pixelWidth: %u", decode(header.pixelWidth)); qDebug(" pixelHeight: %u", decode(header.pixelHeight)); qDebug(" pixelDepth: %u", decode(header.pixelDepth)); @@ -187,13 +258,97 @@ bool QKtxHandler::checkHeader(const KTXHeader &header) qDebug(" numberOfMipmapLevels: %u", decode(header.numberOfMipmapLevels)); qDebug(" bytesOfKeyValueData: %u", decode(header.bytesOfKeyValueData)); #endif - return ((decode(header.glType) == 0) && - (decode(header.glFormat) == 0) && - (decode(header.pixelDepth) == 0) && - (decode(header.numberOfFaces) == 1)); + const bool isCompressedImage = decode(header.glType) == 0 && decode(header.glFormat) == 0 + && decode(header.pixelDepth) == 0; + const bool isCubeMap = decode(header.numberOfFaces) == 6; + const bool is2D = decode(header.pixelDepth) == 0 && decode(header.numberOfArrayElements) == 0; + + return is2D && (isCubeMap || isCompressedImage); +} + +std::optional<QMap<QByteArray, QByteArray>> QKtxHandler::decodeKeyValues(QByteArrayView view) const +{ + QMap<QByteArray, QByteArray> output; + quint32 offset = 0; + while (offset < quint32(view.size())) { + const auto keyAndValueByteSizeView = safeView(view, offset, sizeof(quint32)); + if (keyAndValueByteSizeView.isEmpty()) { + qWarning(lcQtGuiTextureIO, "Invalid view in KTX key-value"); + return std::nullopt; + } + + const quint32 keyAndValueByteSize = + decode(qFromUnaligned<quint32>(keyAndValueByteSizeView.data())); + + quint32 offsetKeyAndValueStart; + if (qAddOverflow(offset, quint32(sizeof(quint32)), &offsetKeyAndValueStart)) { + qWarning(lcQtGuiTextureIO, "Overflow in KTX key-value"); + return std::nullopt; + } + + quint32 offsetKeyAndValueEnd; + if (qAddOverflow(offsetKeyAndValueStart, keyAndValueByteSize, &offsetKeyAndValueEnd)) { + qWarning(lcQtGuiTextureIO, "Overflow in KTX key-value"); + return std::nullopt; + } + + const auto keyValueView = safeView(view, offsetKeyAndValueStart, keyAndValueByteSize); + if (keyValueView.isEmpty()) { + qWarning(lcQtGuiTextureIO, "Invalid view in KTX key-value"); + return std::nullopt; + } + + // 'key' is a UTF-8 string ending with a null terminator, 'value' is the rest. + // To separate the key and value we convert the complete data to utf-8 and find the first + // null terminator from the left, here we split the data into two. + + const int idx = keyValueView.indexOf('\0'); + if (idx == -1) { + qWarning(lcQtGuiTextureIO, "Invalid key in KTX key-value"); + return std::nullopt; + } + + const QByteArrayView keyView = safeView(view, offsetKeyAndValueStart, idx); + if (keyView.isEmpty()) { + qWarning(lcQtGuiTextureIO, "Overflow in KTX key-value"); + return std::nullopt; + } + + const quint32 keySize = idx + 1; // Actual data size + + quint32 offsetValueStart; + if (qAddOverflow(offsetKeyAndValueStart, keySize, &offsetValueStart)) { + qWarning(lcQtGuiTextureIO, "Overflow in KTX key-value"); + return std::nullopt; + } + + quint32 valueSize; + if (qSubOverflow(keyAndValueByteSize, keySize, &valueSize)) { + qWarning(lcQtGuiTextureIO, "Underflow in KTX key-value"); + return std::nullopt; + } + + const QByteArrayView valueView = safeView(view, offsetValueStart, valueSize); + if (valueView.isEmpty()) { + qWarning(lcQtGuiTextureIO, "Invalid view in KTX key-value"); + return std::nullopt; + } + + output.insert(keyView.toByteArray(), valueView.toByteArray()); + + const auto offsetNext = nearestMultipleOf4(offsetKeyAndValueEnd); + if (!offsetNext) { + qWarning(lcQtGuiTextureIO, "Overflow in KTX key-value"); + return std::nullopt; + } + + offset = *offsetNext; + } + + return output; } -quint32 QKtxHandler::decode(quint32 val) +quint32 QKtxHandler::decode(quint32 val) const { return inverseEndian ? qbswap<quint32>(val) : val; } |