Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qssggltfresourceresolver_p.h"
#include <QtCore/qdir.h>
#include <QtCore/qfile.h>
#include <QtCore/qurl.h>
QT_BEGIN_NAMESPACE
/*!
\namespace QSSGGltfResourceResolver
\internal
Resolves glTF URIs: data: URIs (base64 or percent-encoded), and relative
or absolute file paths, including Qt resource (":/...") paths.
*/
namespace QSSGGltfResourceResolver {
/*!
\internal
Returns whether \a uri is a data: URI.
*/
bool isDataUri(QStringView uri)
{
return uri.startsWith(QLatin1String("data:"), Qt::CaseInsensitive);
}
/*!
\internal
Decodes the payload of the data: URI \a uri. Returns an empty array and
sets \a errorMessage on failure.
*/
QByteArray decodeDataUri(QStringView uri, QString *errorMessage)
{
// data:[<mediatype>][;base64],<payload>
const qsizetype comma = uri.indexOf(u',');
if (!isDataUri(uri) || comma < 0) {
if (errorMessage)
*errorMessage = QStringLiteral("Malformed data URI");
return {};
}
const QStringView header = uri.mid(0, comma);
const QStringView payload = uri.mid(comma + 1);
if (header.endsWith(QLatin1String(";base64"), Qt::CaseInsensitive)) {
auto result = QByteArray::fromBase64Encoding(payload.toLatin1());
if (!result) {
if (errorMessage)
*errorMessage = QStringLiteral("Invalid base64 payload in data URI");
return {};
}
return result.decoded;
}
// Non-base64 data URIs carry percent-encoded octets. Decoding has to stay
// in QByteArray: going through QString would replace every octet sequence
// that is not valid UTF-8 with U+FFFD, changing both the contents and the
// length of what is meant to be arbitrary binary data.
return QByteArray::fromPercentEncoding(payload.toLatin1());
}
/*!
\internal
Resolves the relative file URI \a uri against \a baseDir to a local or qrc
path. The URI is percent-decoded as required by the glTF specification.
A URI that would reach outside \a baseDir is rejected, which returns an
empty string. The specification only allows relative references here, so
an absolute path or one climbing out with ".." is malformed rather than
merely unusual, and refusing it keeps a hostile asset from naming an
arbitrary file for the application to load and read back.
*/
QString resolveFilePath(QStringView uri, const QString &baseDir)
{
// glTF URIs are percent-encoded relative URI references
const QString decoded = QUrl::fromPercentEncoding(uri.toUtf8());
if (decoded.isEmpty())
return {};
if (QDir::isAbsolutePath(decoded) || decoded.startsWith(QLatin1String(":/")))
return {};
if (baseDir.isEmpty())
return QDir::cleanPath(decoded).startsWith(QLatin1String("..")) ? QString() : decoded;
const QString base = QDir::cleanPath(baseDir);
const QString resolved = QDir::cleanPath(base + QLatin1Char('/') + decoded);
if (resolved != base && !resolved.startsWith(base + QLatin1Char('/')))
return {};
return resolved;
}
/*!
\internal
Loads the contents referenced by the glTF URI \a uri: data: URIs are
decoded, anything else is treated as a file path relative to \a baseDir.
Sets \a errorMessage on failure.
*/
QByteArray loadUri(QStringView uri, const QString &baseDir, QString *errorMessage)
{
if (isDataUri(uri))
return decodeDataUri(uri, errorMessage);
const QString path = resolveFilePath(uri, baseDir);
if (path.isEmpty()) {
if (errorMessage)
*errorMessage = QStringLiteral("Refusing to resolve '%1' outside the asset directory").arg(uri);
return {};
}
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
if (errorMessage)
*errorMessage = QStringLiteral("Failed to open '%1': %2").arg(path, file.errorString());
return {};
}
return file.readAll();
}
} // namespace QSSGGltfResourceResolver
QT_END_NAMESPACE