Revision control

Copy as Markdown

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qssglightmapio_p.h"
#include <private/qssgrenderloadedtexture_p.h>
#include <private/qssgassert_p.h>
#include <QDataStream>
#include <QDebug>
#include <QtEndian>
#include <QFile>
#include <algorithm>
#include <cstring>
QT_BEGIN_NAMESPACE
using IndexKey = std::tuple<QSSGLightmapIODataTag /* dataTag */, qint32 /* keySize */, QByteArray /* key */>;
struct IndexEntry
{
qint64 keyOffset; // 8 bytes
qint64 keySize; // 8 bytes
qint64 dataOffset; // 8 bytes
qint64 dataSize; // 8 bytes
QSSGLightmapIODataTag dataTag; // 4 bytes
quint32 padding;
};
struct QSSGLightmapIOPrivate
{
QByteArray readKey(const IndexKey &indexKey) const;
bool writeHeader() const;
bool writeData(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &data);
bool writeFooter();
bool decodeHeaders();
QList<std::pair<QString, QSSGLightmapIODataTag>> getKeys() const;
QSharedPointer<QIODevice> stream;
QMap<IndexKey, IndexEntry> entries; // Maps from name -> entry
qint64 entryCount = -1;
qint64 indexOffset = -1;
qint64 fileVersion = -1;
qint64 fileSize = -1;
};
static_assert(offsetof(IndexEntry, keyOffset) == 0, "Unexpected alignment");
static_assert(offsetof(IndexEntry, keySize) == 8, "Unexpected alignment");
static_assert(offsetof(IndexEntry, dataOffset) == 16, "Unexpected alignment");
static_assert(offsetof(IndexEntry, dataSize) == 24, "Unexpected alignment");
static_assert(offsetof(IndexEntry, dataTag) == 32, "Unexpected alignment");
static_assert(offsetof(IndexEntry, padding) == 36, "Unexpected alignment");
static_assert(sizeof(IndexEntry) == 40, "Unexpected size");
constexpr char fileSignature[] = "QTLTMP";
static IndexKey keyToIndexKey(const QString &key, QSSGLightmapIODataTag tag)
{
QByteArray keyBuffer = key.toUtf8();
return std::make_tuple(tag, keyBuffer.size(), keyBuffer);
}
static QByteArray mapToByteArray(const QVariantMap &map)
{
QByteArray byteArray;
QDataStream out(&byteArray, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_6_9);
out.setByteOrder(QDataStream::LittleEndian);
out << QVariant(map);
return byteArray;
}
static void convertEndian(QByteArray &buffer, int sizeOfDataType)
{
if (QSysInfo::ByteOrder == QSysInfo::LittleEndian) {
return; // No need to flip
} else if (QSysInfo::ByteOrder == QSysInfo::BigEndian) {
// Need to flip
} else {
qWarning() << "QSSGLightmapLoader::convertEndian: Unknown endianness";
return;
}
Q_ASSERT(buffer.size() % sizeOfDataType == 0);
if (buffer.size() % sizeOfDataType != 0) {
qWarning() << "QSSGLightmapLoader::convertEndian: Unexpected buffer size";
return;
}
if (sizeOfDataType == 1)
return;
const int buffSize = buffer.size();
for (int offset = 0; offset < buffSize; offset += sizeOfDataType) {
for (int i = 0; i < sizeOfDataType / 2; ++i) {
std::swap(buffer[offset + i], buffer[offset + sizeOfDataType - 1 - i]);
}
}
}
static QVariantMap byteArrayToMap(QByteArray input)
{
QVariant variant;
QDataStream in(input);
in.setVersion(QDataStream::Qt_6_9);
in.setByteOrder(QDataStream::LittleEndian);
in >> variant;
return variant.toMap();
}
bool QSSGLightmapIOPrivate::decodeHeaders()
{
Q_ASSERT(stream);
if (!stream->isOpen())
stream->open(QIODeviceBase::OpenModeFlag::ReadOnly);
if (!stream->isOpen()) {
qWarning() << "QSSGLightmapIO: Stream is not openable";
return false;
}
if (!stream->isReadable()) {
qWarning() << "QSSGLightmapIO: Stream is not readable";
return false;
}
// [Magic Bytes][Version]
constexpr int headerSize = 6 * sizeof(char) + sizeof(qint32);
// [Entry Count][Offset Index]
constexpr int footerSize = 2 * sizeof(qint64);
const qint64 fileSize = stream->size();
if (fileSize < headerSize + footerSize) {
qWarning() << "QSSGLightmapIO: File too small to contain header and footer";
return false;
}
stream->seek(0);
QByteArray headerData = stream->read(headerSize);
if (headerData.size() != qsizetype(headerSize)) {
qWarning() << "Failed to read header";
return false;
}
// Verify file signature
if (QByteArrayView(headerData.constData(), 6) != QByteArray::fromRawData(fileSignature, 6)) {
qWarning() << "QSSGLightmapIO: Invalid file signature";
return false;
}
qint32 fileVersion = -1;
// Read file version (4 bytes after 6 magic bytes)
const char *versionPtr = headerData.constData() + 6;
fileVersion = qFromLittleEndian<qint32>(versionPtr);
if (fileVersion != 1) {
qWarning() << "QSSGLightmapIO: Invalid file version";
return false;
}
// Seek to the last bytes (footer)
if (!stream->seek(fileSize - footerSize)) {
qWarning() << "Failed to seek to footer";
return false;
}
QByteArray footerData = stream->read(footerSize);
if (footerData.size() != qsizetype(footerSize)) {
qWarning() << "Failed to read footer";
return false;
}
const char *footerPtr = footerData.constData();
const qint64 indexOffset = qFromLittleEndian<qint64>(footerPtr);
const qint64 entryCount = qFromLittleEndian<qint64>(footerPtr + sizeof(qint64));
this->entryCount = entryCount;
this->indexOffset = indexOffset;
this->fileVersion = fileVersion;
this->fileSize = fileSize;
return true;
}
bool QSSGLightmapIOPrivate::writeHeader() const
{
Q_ASSERT(stream);
if (!stream->isOpen())
stream->open(QIODeviceBase::OpenModeFlag::WriteOnly);
if (!stream->isOpen()) {
qWarning() << "QSSGLightmapIO: File is not openable";
return false;
}
if (!stream->isWritable()) {
qWarning() << "QSSGLightmapIO: File is not writable";
return false;
}
stream->seek(0);
// Write file signature
if (stream->write(fileSignature, sizeof(fileSignature) - 1) != sizeof(fileSignature) - 1) {
qWarning() << "QSSGLightmapIO: Failed to write file signature";
stream->close();
return false;
}
// Write file version
const qint32 version = qToLittleEndian<qint32>(1);
if (stream->write(reinterpret_cast<const char *>(&version), sizeof(version)) != sizeof(version)) {
qWarning() << "QSSGLightmapIO: Failed to write file version";
stream->close();
return false;
}
return true;
}
bool QSSGLightmapIOPrivate::writeData(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &data)
{
Q_ASSERT(stream->isOpen() && stream->isWritable());
IndexKey keyBytes = keyToIndexKey(key, tag);
Q_ASSERT(!entries.contains(keyBytes));
IndexEntry &entry = entries[keyBytes];
entry.dataOffset = stream->pos();
entry.dataSize = data.size();
entry.dataTag = tag;
if (stream->write(data) != data.size()) {
qWarning() << "QSSGLightmapIO: Failed to write entry data";
return false;
}
return true;
}
template<typename T>
bool writeType(const QSharedPointer<QIODevice> &stream, T value)
{
return stream->write(reinterpret_cast<const char *>(&value), sizeof(value)) == sizeof(value);
}
bool QSSGLightmapIOPrivate::writeFooter()
{
Q_ASSERT(stream);
Q_ASSERT(stream->isOpen());
Q_ASSERT(stream->isWritable());
// Store the key strings
for (auto it = entries.begin(); it != entries.end(); ++it) {
auto [dataTag, keySize, key] = it.key();
IndexEntry &entry = it.value();
entry.keyOffset = stream->pos();
entry.keySize = keySize;
if (stream->write(key) != key.size()) {
qWarning() << "QSSGLightmapIO: Failed to write key";
return false;
}
}
// The file should be seeked to the end of the data segment so we can just
// add indices and footer now
const qint64 indexOffset = qToLittleEndian<qint64>(stream->pos());
const qint64 indexCount = qToLittleEndian<qint64>(entries.size());
for (const IndexEntry &entry : std::as_const(entries)) {
const qint64 keyOffset = qToLittleEndian<qint64>(entry.keyOffset);
const qint64 keySize = qToLittleEndian<qint64>(entry.keySize);
const qint64 dataOffset = qToLittleEndian<qint64>(entry.dataOffset);
const qint64 dataSize = qToLittleEndian<qint64>(entry.dataSize);
const quint32 dataTag = qToLittleEndian<quint32>(std::underlying_type_t<QSSGLightmapIODataTag>(entry.dataTag));
const quint32 padding = qToLittleEndian<quint32>(entry.padding);
if (!writeType(stream, keyOffset) || !writeType(stream, keySize) || !writeType(stream, dataOffset)
|| !writeType(stream, dataSize) || !writeType(stream, dataTag) || !writeType(stream, padding)) {
qWarning() << "QSSGLightmapIO: Failed to write entry";
return false;
}
}
// Write footer
if (!writeType(stream, indexOffset) || !writeType(stream, indexCount)) {
qWarning() << "QSSGLightmapIO: Failed to write footer";
return false;
}
stream->close();
return true;
}
QByteArray QSSGLightmapIOPrivate::readKey(const IndexKey &indexKey) const
{
Q_ASSERT(stream);
Q_ASSERT(entryCount >= 0);
Q_ASSERT(indexOffset >= 0);
Q_ASSERT(fileVersion >= 0);
Q_ASSERT(fileSize >= 0);
// Perform binary search by reading one IndexEntry at a time
qint64 low = 0;
qint64 high = entryCount;
IndexEntry entry;
auto [dataTag, keySize, key] = indexKey;
bool found = false;
qint64 matchOffset = 0;
qint64 matchSize = 0;
while (low < high) {
qint64 mid = (low + high) / 2;
qint64 offset = indexOffset + mid * sizeof(IndexEntry);
if (!stream->seek(offset)) {
qWarning() << "Failed to seek to index entry";
return {};
}
if (stream->read(reinterpret_cast<char *>(&entry), sizeof(IndexEntry)) != sizeof(IndexEntry)) {
qWarning() << "Failed to read index entry";
return {};
}
// Sort by dataTag, keySize and name (matching the order of IndexKey)
int cmp = qint64(entry.dataTag) - qint64(dataTag);
if (cmp == 0) {
cmp = entry.keySize - keySize;
}
if (cmp == 0) {
if (!stream->seek(entry.keyOffset)) {
qWarning() << "Failed to seek to key entry";
return {};
}
const QByteArray entryKey = stream->read(entry.keySize);
if (entryKey.size() != entry.keySize) {
qWarning() << "Failed to read to key entry";
return {};
}
for (int i = 0, n = entry.keySize; i < n; ++i) {
cmp = int(entryKey[i]) - int(key[i]);
if (cmp != 0)
break;
}
}
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid;
} else {
// Found
found = true;
matchOffset = qFromLittleEndian(entry.dataOffset);
matchSize = qFromLittleEndian(entry.dataSize);
break;
}
}
if (!found) {
qWarning() << "Key not found:" << key;
return {};
}
if (matchOffset + matchSize > fileSize) {
qWarning() << "Asset data out of bounds";
return {};
}
// Seek and read asset
if (!stream->seek(matchOffset)) {
qWarning() << "Failed to seek to asset data";
return {};
}
QByteArray assetData = stream->read(matchSize);
if (assetData.size() != static_cast<int>(matchSize)) {
qWarning() << "Failed to read full asset data";
return {};
}
return assetData;
}
QList<std::pair<QString, QSSGLightmapIODataTag>> QSSGLightmapIOPrivate::getKeys() const
{
Q_ASSERT(stream);
Q_ASSERT(entryCount >= 0);
Q_ASSERT(indexOffset >= 0);
Q_ASSERT(fileVersion >= 0);
Q_ASSERT(fileSize >= 0);
QList<std::pair<QString, QSSGLightmapIODataTag>> keys;
keys.resize(entryCount);
IndexEntry entry;
for (int i = 0; i < entryCount; ++i) {
const qint64 offset = indexOffset + i * sizeof(IndexEntry);
if (!stream->seek(offset)) {
qWarning() << "Failed to seek to index entry";
return {};
}
if (stream->read(reinterpret_cast<char *>(&entry), sizeof(IndexEntry)) != sizeof(IndexEntry)) {
qWarning() << "Failed to read index entry";
return {};
}
if (!stream->seek(entry.keyOffset)) {
qWarning() << "Failed to seek to key entry";
return {};
}
const QByteArray entryKey = stream->read(entry.keySize);
if (entryKey.size() != entry.keySize) {
qWarning() << "Failed to read to key entry";
return {};
}
keys[i] = std::make_pair(QString::fromUtf8(entryKey), static_cast<QSSGLightmapIODataTag>(entry.dataTag));
}
std::sort(keys.begin(), keys.end(), [](const auto &a, const auto &b) { return a.first < b.first; });
return keys;
}
/////////////////////////////// Loader /////////////////////////////////
QSSGLightmapLoader::QSSGLightmapLoader() : d(new QSSGLightmapIOPrivate) { };
QSSGLightmapLoader::~QSSGLightmapLoader()
{
if (d->stream)
d->stream->close();
delete d;
}
QSharedPointer<QSSGLightmapLoader> QSSGLightmapLoader::open(const QSharedPointer<QIODevice> &stream)
{
if (!stream) {
qWarning() << "Failed to open lightmap: invalid stream";
return nullptr;
}
auto loader = QSharedPointer<QSSGLightmapLoader>(new QSSGLightmapLoader);
loader->d->stream = stream;
if (!loader->d->decodeHeaders()) {
if (loader->d->stream->isOpen())
loader->d->stream->close();
return {};
}
return loader;
}
QSharedPointer<QSSGLightmapLoader> QSSGLightmapLoader::open(const QString &path)
{
if (QSharedPointer<QIODevice> source = QSSGInputUtil::getStreamForFile(path))
return open(source);
return nullptr;
}
QByteArray QSSGLightmapLoader::readF32Image(const QString &key, QSSGLightmapIODataTag tag) const
{
QByteArray buffer = d->readKey(keyToIndexKey(key, tag));
convertEndian(buffer, sizeof(float));
return buffer;
}
QByteArray QSSGLightmapLoader::readU32Image(const QString &key, QSSGLightmapIODataTag tag) const
{
QByteArray buffer = d->readKey(keyToIndexKey(key, tag));
convertEndian(buffer, sizeof(quint32));
return buffer;
}
QByteArray QSSGLightmapLoader::readData(const QString &key, QSSGLightmapIODataTag tag) const
{
return d->readKey(keyToIndexKey(key, tag));
}
QVariantMap QSSGLightmapLoader::readMap(const QString &key, QSSGLightmapIODataTag tag) const
{
QByteArray metadataBuffer = d->readKey(keyToIndexKey(key, tag));
QVariantMap metadata = byteArrayToMap(metadataBuffer);
return metadata;
}
QList<std::pair<QString, QSSGLightmapIODataTag>> QSSGLightmapLoader::getKeys() const
{
return d->getKeys();
}
inline int calculateLine(int width, int bitdepth)
{
return ((width * bitdepth) + 7) / 8;
}
inline int calculatePitch(int line)
{
return (line + 3) & ~3;
}
QSSGLoadedTexture *QSSGLightmapLoader::createTexture(QSharedPointer<QIODevice> stream,
const QSSGRenderTextureFormat &format,
const QString &key)
{
QSharedPointer<QSSGLightmapLoader> loader = QSSGLightmapLoader::open(stream);
if (!loader)
return nullptr;
QVariantMap metadata = loader->readMap(key, QSSGLightmapIODataTag::Metadata);
if (metadata.isEmpty())
return nullptr;
bool ok = true;
const int w = metadata[QStringLiteral("width")].toInt(&ok);
Q_ASSERT(ok);
const int h = metadata[QStringLiteral("height")].toInt(&ok);
Q_ASSERT(ok);
QSize pixelSize = QSize(w, h);
QByteArray imageFP32 = loader->readF32Image(key, QSSGLightmapIODataTag::Texture_Final);
if (imageFP32.isEmpty())
return nullptr;
// Setup Output container
const int bytesPerPixel = format.getSizeofFormat();
const int bitCount = bytesPerPixel * 8;
const int pitch = calculatePitch(calculateLine(pixelSize.width(), bitCount));
const size_t dataSize = pixelSize.height() * pitch;
QSSG_CHECK_X(dataSize <= std::numeric_limits<quint32>::max(), "Requested data size exceeds 4GB limit!");
QSSGLoadedTexture *imageData = new QSSGLoadedTexture;
imageData->dataSizeInBytes = quint32(dataSize);
imageData->data = ::malloc(imageData->dataSizeInBytes);
imageData->width = pixelSize.width();
imageData->height = pixelSize.height();
imageData->format = format;
imageData->components = format.getNumberOfComponent();
imageData->isSRGB = false;
std::array<float, 4> *source = reinterpret_cast<std::array<float, 4> *>(imageFP32.data());
quint8 *target = reinterpret_cast<quint8 *>(imageData->data);
int idx = 0;
float rgbaF32[4];
for (int y = imageData->height - 1; y >= 0; --y) {
for (int x = 0; x < imageData->width; x++) {
const int i = y * imageData->width + x;
const int lh = i * 4 * sizeof(float);
const int rh = imageFP32.size();
Q_ASSERT(lh < rh);
std::array<float, 4> v = source[i];
rgbaF32[0] = v[0];
rgbaF32[1] = v[1];
rgbaF32[2] = v[2];
rgbaF32[3] = v[3];
format.encodeToPixel(rgbaF32, target, idx * bytesPerPixel);
++idx;
}
}
return imageData;
}
///////////////////////////// Writer ///////////////////////////////////
QSSGLightmapWriter::QSSGLightmapWriter() : d(new QSSGLightmapIOPrivate) { }
QSSGLightmapWriter::~QSSGLightmapWriter()
{
if (d->stream->isOpen()) {
qWarning() << "Lightmap file open on destruction, closing";
close();
}
delete d;
}
QSharedPointer<QSSGLightmapWriter> QSSGLightmapWriter::open(const QString &path)
{
return open(QSharedPointer<QIODevice>(new QFile(path)));
}
QSharedPointer<QSSGLightmapWriter> QSSGLightmapWriter::open(const QSharedPointer<QIODevice> &stream)
{
if (!stream) {
qWarning() << "Failed to open lightmap file";
return nullptr;
}
QSharedPointer<QSSGLightmapWriter> writer = QSharedPointer<QSSGLightmapWriter>(new QSSGLightmapWriter);
if (!stream->isOpen() && !stream->open(QIODeviceBase::WriteOnly)) {
qWarning() << "Failed to open lightmap file";
return nullptr;
}
writer->d->stream = stream;
if (!writer->d->writeHeader())
return nullptr;
return writer;
}
bool QSSGLightmapWriter::writeF32Image(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &imageFP32)
{
QByteArray buffer = QByteArray(imageFP32.constData(), imageFP32.size());
convertEndian(buffer, sizeof(float));
return d->writeData(key, tag, buffer);
}
bool QSSGLightmapWriter::writeU32Image(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &imageU32)
{
QByteArray buffer = QByteArray(imageU32.constData(), imageU32.size());
convertEndian(buffer, sizeof(quint32));
return d->writeData(key, tag, buffer);
}
bool QSSGLightmapWriter::writeData(const QString &key, QSSGLightmapIODataTag tag, const QByteArray &buffer)
{
return d->writeData(key, tag, buffer);
}
bool QSSGLightmapWriter::writeMap(const QString &key, QSSGLightmapIODataTag tag, const QVariantMap &data)
{
return d->writeData(key, tag, mapToByteArray(data));
}
bool QSSGLightmapWriter::close() const
{
return d->writeFooter();
}
QT_END_NAMESPACE