11#include <QApplication>
17#include <QKeySequence>
23#include <QVarLengthArray>
37QVersionNumber readVersionNumber(QDataStream &stream)
43 "Implausible version segment count %1 — stream may be corrupt",
44 QString::number(count));
47 segs.reserve(
static_cast<int>(count));
48 for (quint32 i = 0; i < count; ++i) {
52 return QVersionNumber(segs);
59qint64 bytesAvailable(
const QDataStream &stream)
61 if (!stream.device())
return 0;
62 const qint64 n = stream.device()->bytesAvailable();
63 return n < 0 ? std::numeric_limits<qint64>::max() / 2 : n;
68bool peekU32(QDataStream &stream, quint32 &out)
71 if (stream.device()->peek(buf, 4) != 4)
return false;
72 out = qFromBigEndian<quint32>(buf);
80QString readBoundedString(QDataStream &stream)
83 if (peekU32(stream, byteLen)) {
85 if (byteLen != 0xFFFFFFFFu && byteLen != 0) {
87 if (
static_cast<qint64
>(byteLen) + 4 > bytesAvailable(stream))
89 "QString byte count %1 exceeds remaining stream bytes",
90 QString::number(byteLen));
100QByteArray readBoundedByteArray(QDataStream &stream)
103 if (peekU32(stream, len)) {
104 if (len != 0xFFFFFFFFu && len != 0) {
105 if (
static_cast<qint64
>(len) + 4 > bytesAvailable(stream))
107 "QByteArray length %1 exceeds remaining stream bytes",
108 QString::number(len));
118QStringList readBoundedStringList(QDataStream &stream)
121 if (peekU32(stream, count) && count > 0) {
124 if (
static_cast<qint64
>(count) > (bytesAvailable(stream) - 4) / 4)
126 "QStringList count %1 implausible given remaining stream bytes",
127 QString::number(count));
136QKeySequence readBoundedKeySequence(QDataStream &stream)
139 if (peekU32(stream, count)) {
140 if (count > 4 ||
static_cast<qint64
>(count) * 4 > bytesAvailable(stream))
142 "QKeySequence count %1 out of range — stream may be corrupt",
143 QString::number(count));
145 QKeySequence ks; stream >> ks;
return ks;
149QBitArray readBoundedBitArray(QDataStream &stream)
152 if (peekU32(stream, numBits) && numBits > 0) {
153 const qint64 numBytes = (
static_cast<qint64
>(numBits) + 7) / 8;
154 if (numBytes > bytesAvailable(stream))
156 "QBitArray numBits %1 exceeds stream size — stream may be corrupt",
157 QString::number(numBits));
159 QBitArray ba; stream >> ba;
return ba;
174QVariant readBoundedVariant(QDataStream &stream)
178 if (stream.device()->peek(hdr, 5) < 5) {
179 QVariant v; stream >> v;
return v;
182 const quint32 typeId = qFromBigEndian<quint32>(hdr);
189 auto consumeHeader = [&]() {
char s[5]; stream.readRawData(s, 5); };
196 if (typeId == quint32(QMetaType::QString)) {
198 return QVariant::fromValue(readBoundedString(stream));
205 constexpr quint32 kWireKeySeqQt5 = 75;
206 constexpr quint32 kWireKeySeqQt6 = 4107;
207 if (typeId == kWireKeySeqQt5 || typeId == kWireKeySeqQt6) {
209 return QVariant::fromValue(readBoundedKeySequence(stream));
212 if (typeId == quint32(QMetaType::QBitArray)) {
214 return QVariant::fromValue(readBoundedBitArray(stream));
217 if (typeId == quint32(QMetaType::QStringList) ||
218 typeId == quint32(QMetaType::QVariantList) ||
219 typeId == quint32(QMetaType::QVariantMap)) {
221 if (typeId == quint32(QMetaType::QStringList))
222 return QVariant::fromValue(readBoundedStringList(stream));
226 "Unsupported container type %1 in metadata — stream may be corrupt",
227 QString::number(typeId));
230 if (typeId == quint32(QMetaType::QByteArray)) {
232 return readBoundedByteArray(stream);
239 static constexpr std::array<quint32, 16> kFixedSizeTypes{{
240 quint32(QMetaType::Bool), quint32(QMetaType::Int),
241 quint32(QMetaType::UInt), quint32(QMetaType::LongLong),
242 quint32(QMetaType::ULongLong), quint32(QMetaType::Double),
243 quint32(QMetaType::Float), quint32(QMetaType::QChar),
244 quint32(QMetaType::QDate), quint32(QMetaType::QTime),
245 quint32(QMetaType::QRect), quint32(QMetaType::QRectF),
246 quint32(QMetaType::QSize), quint32(QMetaType::QSizeF),
247 quint32(QMetaType::QPoint), quint32(QMetaType::QPointF),
249 const bool isFixedSize = std::find(kFixedSizeTypes.begin(),
250 kFixedSizeTypes.end(), typeId) != kFixedSizeTypes.end();
257 "Unsupported metadata value type %1 — stream may be corrupt",
258 QString::number(typeId));
269 return ::readBoundedString(stream);
274 return ::readBoundedKeySequence(stream);
281 if (stream.status() != QDataStream::Ok)
282 throw PANDACEPTION(
"Stream error reading metadata map count");
284 if (count > 0 &&
static_cast<qint64
>(count) > bytesAvailable(stream) / 9)
285 throw PANDACEPTION(
"Metadata map count %1 implausible given remaining stream bytes",
286 QString::number(count));
287 QMap<QString, QVariant> result;
288 for (quint32 i = 0; i < count; ++i) {
290 QVariant val = readBoundedVariant(stream);
291 if (stream.status() != QDataStream::Ok)
break;
292 result.insert(std::move(key), std::move(val));
301 if (stream.status() != QDataStream::Ok)
302 throw PANDACEPTION(
"Stream error reading blob map count");
304 if (count > 0 &&
static_cast<qint64
>(count) > bytesAvailable(stream) / 8)
305 throw PANDACEPTION(
"Blob map count %1 implausible given remaining stream bytes",
306 QString::number(count));
307 QMap<QString, QByteArray> result;
308 for (quint32 i = 0; i < count; ++i) {
310 QByteArray val = readBoundedByteArray(stream);
311 if (stream.status() != QDataStream::Ok)
break;
312 result.insert(std::move(key), std::move(val));
319 stream.setVersion(QDataStream::Qt_5_12);
326 const QByteArray compressed = qCompress(payload);
327 stream.writeRawData(compressed.constData(),
static_cast<int>(compressed.size()));
332 const QByteArray raw = stream.device()->readAll();
344 if (raw.size() < 4) {
346 "Compressed payload too short (%1 bytes) to contain a size header",
347 QString::number(raw.size()));
350 const quint32 expectedSize = qFromBigEndian<quint32>(
reinterpret_cast<const uchar *
>(raw.constData()));
351 constexpr quint32 kMaxUncompressedPayload = 1u << 30;
352 constexpr quint32 kMaxExpansionRatio = 1032;
353 const quint32 compressedBytes =
static_cast<quint32
>(raw.size() - 4);
354 if (expectedSize > kMaxUncompressedPayload ||
355 (compressedBytes > 0 && expectedSize / compressedBytes > kMaxExpansionRatio)) {
357 "Implausible decompressed payload size %1 -- stream may be corrupt",
358 QString::number(expectedSize));
361 const QByteArray decompressed = qUncompress(raw);
362 if (decompressed.isNull() && expectedSize != 0) {
364 "Failed to decompress circuit payload -- stream may be corrupt");
371 stream.setVersion(QDataStream::Qt_5_12);
373 QIODevice *device = stream.device();
384 quint32 magicHeader = 0;
385 const bool haveMagic = peekU32(stream, magicHeader);
388 stream >> magicHeader;
389 return readVersionNumber(stream);
394 constexpr quint32 NullStringMarker = 0xFFFFFFFFu;
395 constexpr qint64 MaxLegacyHeaderBytes = 128;
397 if (haveMagic && magicHeader != NullStringMarker && magicHeader <= MaxLegacyHeaderBytes) {
398 const qint64 probeLen =
sizeof(quint32) + magicHeader;
399 QByteArray strProbeBuf(
static_cast<int>(probeLen),
'\0');
400 if (device->peek(strProbeBuf.data(), probeLen) == probeLen) {
401 QDataStream strProbe(strProbeBuf);
402 strProbe.setVersion(QDataStream::Qt_5_12);
405 if (strProbe.status() == QDataStream::Ok && appName.startsWith(
"wiRedPanda", Qt::CaseInsensitive)) {
409 const QStringList split = consumed.split(
' ');
410 if (split.size() < 2) {
411 throw PANDACEPTION(
"Invalid legacy file header: %1", consumed);
413 return QVersionNumber::fromString(split.at(1));
420 char pointProbeBuf[2 *
sizeof(double)];
421 if (device->peek(pointProbeBuf,
sizeof(pointProbeBuf)) !=
static_cast<qint64
>(
sizeof(pointProbeBuf))) {
424 QDataStream pointProbe(QByteArray(pointProbeBuf,
sizeof(pointProbeBuf)));
425 pointProbe.setVersion(QDataStream::Qt_5_12);
427 pointProbe >> center;
428 if (pointProbe.status() != QDataStream::Ok || center.isNull()) {
434 return QVersionNumber(4, 1);
439 stream.setVersion(QDataStream::Qt_5_12);
446 stream.setVersion(QDataStream::Qt_5_12);
448 QIODevice *device = stream.device();
456 quint32 magicHeader = 0;
457 const bool haveMagic = peekU32(stream, magicHeader);
460 stream >> magicHeader;
461 readVersionNumber(stream);
469 if (!haveMagic || magicHeader == 0xFFFFFFFFu || magicHeader > 256 || magicHeader % 2 != 0) {
472 const qint64 probeLen =
sizeof(quint32) + magicHeader;
473 QByteArray strProbeBuf(
static_cast<int>(probeLen),
'\0');
474 if (device->peek(strProbeBuf.data(), probeLen) != probeLen) {
487 const int codeUnitCount =
static_cast<int>(magicHeader / 2);
488 QVarLengthArray<char16_t, 128> appNameBuf(codeUnitCount);
489 std::memcpy(appNameBuf.data(), strProbeBuf.constData() +
sizeof(quint32),
490 static_cast<size_t>(magicHeader));
491 const QString appName = QString::fromUtf16(appNameBuf.constData(), codeUnitCount);
492 if (!appName.startsWith(
"beWavedDolphin")) {
497 quint32 consumedLen = 0;
498 stream >> consumedLen;
499 QByteArray raw(
static_cast<int>(consumedLen),
'\0');
500 stream.readRawData(raw.data(),
static_cast<int>(consumedLen));
518 QList<std::pair<GraphicElement *, int>> savedIds;
520 for (
auto *item : items) {
521 if (
auto *ge = qgraphicsitem_cast<GraphicElement *>(item)) {
522 if (ge->id() > localId) {
527 for (
auto *item : items) {
528 if (
auto *ge = qgraphicsitem_cast<GraphicElement *>(item)) {
530 savedIds.append({ge, ge->id()});
531 ge->setId(++localId);
539 auto restoreIds = qScopeGuard([&savedIds] {
540 for (
const auto &[ge,
id] : savedIds) {
550 for (
auto *item : items) {
551 if (
auto *element = qgraphicsitem_cast<GraphicElement *>(item)) {
553 stream << element->elementType();
554 element->save(stream, options);
560 for (
auto *item : items) {
561 if (
auto *connection = qgraphicsitem_cast<Connection *>(item)) {
563 connection->save(stream);
578 QList<QGraphicsItem *> itemList;
591 auto cleanupGuard = qScopeGuard([&itemList]() {
592 for (qsizetype i = 0; i < itemList.size(); ++i) {
595 itemList[i] =
nullptr;
598 qDeleteAll(itemList);
601 while (!stream.atEnd()) {
602 int type; stream >> type;
605 if (stream.status() != QDataStream::Ok) {
606 throw PANDACEPTION(
"Stream error reading type tag at position %1: status %2",
607 stream.device()->pos(),
static_cast<int>(stream.status()));
617 if (stream.status() != QDataStream::Ok) {
618 throw PANDACEPTION(
"Stream error reading element type at position %1: status %2",
619 stream.device()->pos(),
static_cast<int>(stream.status()));
625 itemList.append(elm);
626 elm->load(stream, context);
629 if (stream.status() != QDataStream::Ok) {
630 throw PANDACEPTION(
"Stream error loading element at position %1: status %2",
631 stream.device()->pos(),
static_cast<int>(stream.status()));
638 qCDebug(three) <<
"Building connection.";
642 itemList.append(conn);
644 qCDebug(three) <<
"Loading connection.";
645 conn->load(stream, context);
648 if (stream.status() != QDataStream::Ok) {
649 throw PANDACEPTION(
"Stream error loading connection at position %1: status %2",
650 stream.device()->pos(),
static_cast<int>(stream.status()));
656 throw PANDACEPTION(
"Invalid type. Data is possibly corrupted.");
660 qCDebug(zero) <<
"Finished deserializing.";
661 cleanupGuard.dismiss();
699 QFileInfo info(fileName);
700 const QString backupName = info.absolutePath() +
"/"
701 + info.completeBaseName()
702 +
".v" + version.toString()
703 +
"." + info.suffix();
705 if (!QFile::exists(backupName)) {
706 if (QFile::copy(fileName, backupName)) {
707 qCDebug(three) <<
"Created versioned backup: " << backupName;
709 throw PANDACEPTION(
"Failed to create versioned backup: %1", backupName);
724 QDataStream payloadStream(&payload, QIODevice::ReadOnly);
725 payloadStream.setVersion(QDataStream::Qt_5_12);
737 if (payloadStream.status() != QDataStream::Ok) {
738 throw PANDACEPTION(
"Stream error reading preamble: status %1",
static_cast<int>(payloadStream.status()));
744 result.
remainingPayload = payload.mid(
static_cast<int>(payloadStream.device()->pos()));
750 const QVersionNumber &fileVersion)
752 QMap<QString, QByteArray> result;
753 const QString key = metadata.contains(
"embeddedICs") ? QStringLiteral(
"embeddedICs")
754 : QStringLiteral(
"blobRegistry");
755 if (metadata.contains(key)) {
756 QByteArray regBytes = metadata.value(key).toByteArray();
757 QDataStream regStream(®Bytes, QIODevice::ReadOnly);
759 regStream.setVersion(QDataStream::Qt_5_12);
768 if (!blobs.isEmpty()) {
770 QDataStream regStream(®Bytes, QIODevice::WriteOnly);
771 regStream.setVersion(QDataStream::Qt_5_12);
773 metadata[
"embeddedICs"] = regBytes;
780 static const QHash<int, QString> typeMap = {
781 { QGraphicsItem::UserType + 1,
"Port" },
782 { QGraphicsItem::UserType + 2,
"Connection" },
783 { QGraphicsItem::UserType + 3,
"GraphicElement" },
786 return typeMap.value(type,
"UnknownType");
790 QSet<QString> *visited,
int depth)
795 constexpr int kMaxPandaFileCopyDepth = 16;
796 if (depth >= kMaxPandaFileCopyDepth) {
797 throw PANDACEPTION(
"Panda file dependency chain exceeds the maximum nesting depth (%1) while copying '%2'",
798 QString::number(kMaxPandaFileCopyDepth), srcPath.filePath());
801 QSet<QString> ownedVisited;
803 visited = &ownedVisited;
805 const QString canonical = srcPath.canonicalFilePath();
806 if (!canonical.isEmpty()) {
807 if (visited->contains(canonical)) {
810 visited->insert(canonical);
813 if (!QFile::exists(destPath.absoluteFilePath())) {
814 QFile srcFile(srcPath.absoluteFilePath());
815 if (!srcFile.copy(destPath.absoluteFilePath())) {
816 throw PANDACEPTION(
"Error copying file: %1", srcFile.errorString());
822 QFile file(srcPath.absoluteFilePath());
823 if (!file.open(QIODevice::ReadOnly)) {
832 char magicBuf[
sizeof(quint32)];
833 if (file.peek(magicBuf,
sizeof(magicBuf)) !=
static_cast<qint64
>(
sizeof(magicBuf))) {
836 const quint32 magicHeader = qFromBigEndian<quint32>(magicBuf);
838 constexpr quint32 NullStringMarker = 0xFFFFFFFFu;
839 constexpr qint64 MaxLegacyHeaderBytes = 128;
840 if (magicHeader == NullStringMarker || magicHeader > MaxLegacyHeaderBytes) {
843 const qint64 probeLen =
sizeof(quint32) + magicHeader;
844 QByteArray strProbeBuf(
static_cast<int>(probeLen),
'\0');
845 if (file.peek(strProbeBuf.data(), probeLen) != probeLen) {
848 QDataStream strProbe(strProbeBuf);
849 strProbe.setVersion(QDataStream::Qt_5_12);
852 if (strProbe.status() != QDataStream::Ok || !appName.startsWith(
"wiRedPanda", Qt::CaseInsensitive)) {
858 QDataStream stream(&file);
869 const QStringList icFiles = preamble.
metadata.value(
"fileBackedICs").toStringList();
870 for (
const QString &icFile : icFiles) {
871 const QFileInfo icSrc(QDir(srcPath.absolutePath()), icFile);
872 const QFileInfo icDest(QDir(destPath.absolutePath()), icFile);
873 if (icSrc.exists()) {
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
#define PANDACEPTION_WITH_CONTEXT(context, msg,...)
#define qCDebug(category)
Connection: a wire that connects an output port to an input port in the circuit scene.
Singleton factory for all circuit element types.
Enums::ElementType ElementType
Abstract base class for all graphical circuit elements.
Circuit and waveform file serialization/deserialization utilities.
Named version predicates for file-format compatibility checks.
A bezier-curve wire connecting an output port to an input port in the scene.
static GraphicElement * buildElement(const ElementType type)
Constructs and returns a new graphic element of the given type.
static void readDolphinHeader(QDataStream &stream)
Reads and validates the BeWavedDolphin waveform file header.
static QList< QGraphicsItem * > deserialize(QDataStream &stream, SerializationContext &context)
Deserializes items from stream until the stream is exhausted.
static void serialize(const QList< QGraphicsItem * > &items, QDataStream &stream, SerializationOptions options)
Serializes items to stream in the current .panda binary format.
static QMap< QString, QByteArray > readBoundedBlobMap(QDataStream &stream)
Reads a QMap<QString,QByteArray> from stream with bounds checking.
static void copyPandaFile(const QFileInfo &srcPath, const QFileInfo &destPath, QSet< QString > *visited=nullptr, int depth=0)
Copies a .panda file and its file-backed IC dependencies to destPath.
static QString readBoundedString(QDataStream &stream)
Reads a QString from stream, refusing to allocate more bytes than remain in the stream (prevents OOM ...
static void writePandaHeader(QDataStream &stream)
Writes the .panda circuit file header to stream.
static constexpr quint32 MAGIC_HEADER_WAVEFORM
static QString typeName(const int type)
Returns the human-readable element type name for the given type integer.
static void writeDolphinHeader(QDataStream &stream)
Writes the BeWavedDolphin waveform file header to stream.
static void serializeBlobRegistry(const QMap< QString, QByteArray > &blobs, QMap< QString, QVariant > &metadata)
Serializes embedded ICs into a metadata map (sets the "embeddedICs" key).
static QKeySequence readBoundedKeySequence(QDataStream &stream)
Reads a QKeySequence from stream, rejecting an implausible/oversized internal key-combination count b...
static QVersionNumber readPandaHeader(QDataStream &stream)
Reads and validates the .panda circuit file header; returns the stored version number.
static QString loadDolphinFileName(QDataStream &stream, const QVersionNumber &version)
Returns the BeWavedDolphin waveform file name stored in stream at version.
static void writePayload(QDataStream &stream, const QByteArray &payload)
Compresses payload (qCompress) and writes it to stream.
static Preamble readPreamble(QDataStream &stream)
Reads the full .panda preamble: header, dolphin filename, rect, and metadata (V_4_5+).
static QByteArray readPayload(QDataStream &stream, const QVersionNumber &version)
Reads the remainder of stream's device, decompressing it first if version indicates the compressed-pa...
static QMap< QString, QVariant > readBoundedMetadata(QDataStream &stream)
Reads the file-level metadata QMap<QString,QVariant> from stream without calling QList::reserve() wit...
static QMap< QString, QByteArray > deserializeBlobRegistry(const QMap< QString, QVariant > &metadata, const QVersionNumber &fileVersion)
Extracts the embedded IC registry from a metadata map.
static void createVersionedBackup(const QString &fileName, const QVersionNumber &version)
Copies fileName to a versioned sidecar before overwriting it during migration.
static constexpr quint32 MAGIC_HEADER_CIRCUIT
static QRectF loadRect(QDataStream &stream, const QVersionNumber &version)
Returns the canvas viewport rectangle from the last saved session.
bool hasCompressedPayload(const QVersionNumber &v)
bool hasVersionedBlobRegistry(const QVersionNumber &v)
bool hasDolphinFilename(const QVersionNumber &v)
V3.0: Dolphin waveform filename stored in file header.
bool hasMetadata(const QVersionNumber &v)
V4.5: File-level metadata map and embedded IC blob registry.
bool hasSceneRect(const QVersionNumber &v)
V1.4: Scene rectangle stored in file.
bool hasDolphinSentinelFix(const QVersionNumber &v)
V3.3: "none" sentinel normalised to empty string for dolphin filename.
bool hasUnifiedMetadata(const QVersionNumber &v)
V4.6: Dolphin filename moved into metadata map; scene rect no longer stored.
Bundles all per-deserialization state so that load() overrides receive it through one explicit parame...
Options passed to GraphicElement::save() (and friends); the save-side counterpart of SerializationCon...
Result of reading a .panda file preamble (header + dolphin + rect + metadata).
QMap< QString, QVariant > metadata
QByteArray remainingPayload