wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
Serialization.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <array>
7#include <cstring>
8#include <limits>
9#include <utility>
10
11#include <QApplication>
12#include <QBitArray>
13#include <QDir>
14#include <QFile>
15#include <QFileInfo>
16#include <QIODevice>
17#include <QKeySequence>
18#include <QMetaType>
19#include <QScopeGuard>
20#include <QSysInfo>
21#include <QtEndian>
22#include <QVariant>
23#include <QVarLengthArray>
24
25#include "App/Core/Common.h"
28#include "App/IO/VersionInfo.h"
30
31namespace {
32
37QVersionNumber readVersionNumber(QDataStream &stream)
38{
39 quint32 count;
40 stream >> count;
41 if (count > 8) {
42 throw PANDACEPTION_WITH_CONTEXT("Serialization",
43 "Implausible version segment count %1 — stream may be corrupt",
44 QString::number(count));
45 }
46 QList<int> segs;
47 segs.reserve(static_cast<int>(count));
48 for (quint32 i = 0; i < count; ++i) {
49 int v; stream >> v;
50 segs.append(v);
51 }
52 return QVersionNumber(segs);
53}
54
59qint64 bytesAvailable(const QDataStream &stream)
60{
61 if (!stream.device()) return 0;
62 const qint64 n = stream.device()->bytesAvailable();
63 return n < 0 ? std::numeric_limits<qint64>::max() / 2 : n;
64}
65
68bool peekU32(QDataStream &stream, quint32 &out)
69{
70 char buf[4];
71 if (stream.device()->peek(buf, 4) != 4) return false;
72 out = qFromBigEndian<quint32>(buf);
73 return true;
74}
75
80QString readBoundedString(QDataStream &stream)
81{
82 quint32 byteLen = 0;
83 if (peekU32(stream, byteLen)) {
84 // 0xFFFFFFFF = null sentinel, 0 = empty — no allocation needed, skip check.
85 if (byteLen != 0xFFFFFFFFu && byteLen != 0) {
86 // byteLen bytes of data plus 4 bytes for the length field itself
87 if (static_cast<qint64>(byteLen) + 4 > bytesAvailable(stream))
88 throw PANDACEPTION_WITH_CONTEXT("Serialization",
89 "QString byte count %1 exceeds remaining stream bytes",
90 QString::number(byteLen));
91 }
92 }
93 QString result;
94 stream >> result;
95 return result;
96}
97
100QByteArray readBoundedByteArray(QDataStream &stream)
101{
102 quint32 len = 0;
103 if (peekU32(stream, len)) {
104 if (len != 0xFFFFFFFFu && len != 0) {
105 if (static_cast<qint64>(len) + 4 > bytesAvailable(stream))
106 throw PANDACEPTION_WITH_CONTEXT("Serialization",
107 "QByteArray length %1 exceeds remaining stream bytes",
108 QString::number(len));
109 }
110 }
111 QByteArray result;
112 stream >> result;
113 return result;
114}
115
118QStringList readBoundedStringList(QDataStream &stream)
119{
120 quint32 count = 0;
121 if (peekU32(stream, count) && count > 0) {
122 // Each serialised entry is at least 4 bytes (the empty-string quint32 header);
123 // subtract 4 for the count field itself which is still in the stream.
124 if (static_cast<qint64>(count) > (bytesAvailable(stream) - 4) / 4)
125 throw PANDACEPTION_WITH_CONTEXT("Serialization",
126 "QStringList count %1 implausible given remaining stream bytes",
127 QString::number(count));
128 }
129 QStringList result;
130 stream >> result;
131 return result;
132}
133
136QKeySequence readBoundedKeySequence(QDataStream &stream)
137{
138 quint32 count = 0;
139 if (peekU32(stream, count)) {
140 if (count > 4 || static_cast<qint64>(count) * 4 > bytesAvailable(stream))
141 throw PANDACEPTION_WITH_CONTEXT("Serialization",
142 "QKeySequence count %1 out of range — stream may be corrupt",
143 QString::number(count));
144 }
145 QKeySequence ks; stream >> ks; return ks;
146}
147
149QBitArray readBoundedBitArray(QDataStream &stream)
150{
151 quint32 numBits = 0;
152 if (peekU32(stream, numBits) && numBits > 0) {
153 const qint64 numBytes = (static_cast<qint64>(numBits) + 7) / 8;
154 if (numBytes > bytesAvailable(stream))
155 throw PANDACEPTION_WITH_CONTEXT("Serialization",
156 "QBitArray numBits %1 exceeds stream size — stream may be corrupt",
157 QString::number(numBits));
158 }
159 QBitArray ba; stream >> ba; return ba;
160}
161
174QVariant readBoundedVariant(QDataStream &stream)
175{
176 // Peek at typeId + isNull (5 bytes total) without consuming them.
177 char hdr[5] = {};
178 if (stream.device()->peek(hdr, 5) < 5) {
179 QVariant v; stream >> v; return v;
180 }
181
182 const quint32 typeId = qFromBigEndian<quint32>(hdr);
183 // hdr[4] holds the isNull byte; deliberately unread because Qt 6.9 also reads
184 // type-specific data when isNull != 0, so it is not a safe skip-guard (see
185 // function header). Still peek 5 bytes so the consumeHeader lambda below
186 // re-reads the exact same bytes Qt's QVariant operator>> expects.
187
188 // Consume the 5 header bytes we peeked before dispatching.
189 auto consumeHeader = [&]() { char s[5]; stream.readRawData(s, 5); };
190
191 // -----------------------------------------------------------------------
192 // Variable-size types: intercept unconditionally (Qt 6.9 reads their data
193 // even for null-marked variants, so isNull is not a safe guard here).
194 // -----------------------------------------------------------------------
195
196 if (typeId == quint32(QMetaType::QString)) {
197 consumeHeader();
198 return QVariant::fromValue(readBoundedString(stream));
199 }
200
201 // QKeySequence wire typeId is version-dependent:
202 // Qt_5_12 format writes 75 (Qt 5 backward-compat value)
203 // Qt 6 default format writes 4107 (QMetaType::QKeySequence in Qt 6)
204 // Files written before V_5_1 used Qt 6 default; afterwards Qt_5_12.
205 constexpr quint32 kWireKeySeqQt5 = 75;
206 constexpr quint32 kWireKeySeqQt6 = 4107; // quint32(QMetaType::QKeySequence)
207 if (typeId == kWireKeySeqQt5 || typeId == kWireKeySeqQt6) {
208 consumeHeader();
209 return QVariant::fromValue(readBoundedKeySequence(stream));
210 }
211
212 if (typeId == quint32(QMetaType::QBitArray)) {
213 consumeHeader();
214 return QVariant::fromValue(readBoundedBitArray(stream));
215 }
216
217 if (typeId == quint32(QMetaType::QStringList) ||
218 typeId == quint32(QMetaType::QVariantList) ||
219 typeId == quint32(QMetaType::QVariantMap)) {
220 consumeHeader();
221 if (typeId == quint32(QMetaType::QStringList))
222 return QVariant::fromValue(readBoundedStringList(stream));
223 // QVariantList and QVariantMap are not used in metadata; reject to
224 // avoid nested-container OOM (inner QVariants bypass this reader).
225 throw PANDACEPTION_WITH_CONTEXT("Serialization",
226 "Unsupported container type %1 in metadata — stream may be corrupt",
227 QString::number(typeId));
228 }
229
230 if (typeId == quint32(QMetaType::QByteArray)) {
231 consumeHeader();
232 return readBoundedByteArray(stream);
233 }
234
235 // -----------------------------------------------------------------------
236 // Fixed-size built-in types: safe to delegate to Qt unconditionally.
237 // -----------------------------------------------------------------------
238
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),
248 }};
249 const bool isFixedSize = std::find(kFixedSizeTypes.begin(),
250 kFixedSizeTypes.end(), typeId) != kFixedSizeTypes.end();
251
252 // Reject all unknown types regardless of isNull. Qt 6.9 reads type-specific
253 // data even for null-marked variants, so unknown types with isNull != 0 are
254 // still vulnerable to fuzz-controlled allocation via QArrayData::allocate.
255 if (!isFixedSize)
256 throw PANDACEPTION_WITH_CONTEXT("Serialization",
257 "Unsupported metadata value type %1 — stream may be corrupt",
258 QString::number(typeId));
259
260 QVariant result;
261 stream >> result;
262 return result;
263}
264
265} // namespace
266
267QString Serialization::readBoundedString(QDataStream &stream)
268{
269 return ::readBoundedString(stream); // delegate to the file-local free function
270}
271
272QKeySequence Serialization::readBoundedKeySequence(QDataStream &stream)
273{
274 return ::readBoundedKeySequence(stream); // delegate to the file-local free function
275}
276
277QMap<QString, QVariant> Serialization::readBoundedMetadata(QDataStream &stream)
278{
279 quint32 count;
280 stream >> count;
281 if (stream.status() != QDataStream::Ok)
282 throw PANDACEPTION("Stream error reading metadata map count");
283 // Minimum per entry: 4 bytes (key byteLen=0) + 4 (typeId) + 1 (isNull) = 9 bytes
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) {
289 QString key = readBoundedString(stream);
290 QVariant val = readBoundedVariant(stream);
291 if (stream.status() != QDataStream::Ok) break;
292 result.insert(std::move(key), std::move(val));
293 }
294 return result;
295}
296
297QMap<QString, QByteArray> Serialization::readBoundedBlobMap(QDataStream &stream)
298{
299 quint32 count;
300 stream >> count;
301 if (stream.status() != QDataStream::Ok)
302 throw PANDACEPTION("Stream error reading blob map count");
303 // Minimum per entry: 4 (key byteLen=0) + 4 (value byteLen=0) = 8 bytes
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) {
309 QString key = readBoundedString(stream);
310 QByteArray val = readBoundedByteArray(stream);
311 if (stream.status() != QDataStream::Ok) break;
312 result.insert(std::move(key), std::move(val));
313 }
314 return result;
315}
316
317void Serialization::writePandaHeader(QDataStream &stream)
318{
319 stream.setVersion(QDataStream::Qt_5_12);
320 stream << MAGIC_HEADER_CIRCUIT;
321 stream << FormatRev::current;
322}
323
324void Serialization::writePayload(QDataStream &stream, const QByteArray &payload)
325{
326 const QByteArray compressed = qCompress(payload);
327 stream.writeRawData(compressed.constData(), static_cast<int>(compressed.size()));
328}
329
330QByteArray Serialization::readPayload(QDataStream &stream, const QVersionNumber &version)
331{
332 const QByteArray raw = stream.device()->readAll();
333
334 if (!VersionInfo::hasCompressedPayload(version)) {
335 return raw;
336 }
337
338 // qCompress()'s wire format is a 4-byte big-endian "uncompressed size" header
339 // followed by the deflate stream; qUncompress() allocates that many bytes
340 // unconditionally, so a crafted file can claim an implausible size and OOM the
341 // process before any real decompression happens. Peek and bound it first,
342 // mirroring this file's peekU32()-then-validate idiom (see readBoundedString()
343 // and friends above) rather than trusting qUncompress() to fail cheaply.
344 if (raw.size() < 4) {
345 throw PANDACEPTION_WITH_CONTEXT("Serialization",
346 "Compressed payload too short (%1 bytes) to contain a size header",
347 QString::number(raw.size()));
348 }
349
350 const quint32 expectedSize = qFromBigEndian<quint32>(reinterpret_cast<const uchar *>(raw.constData()));
351 constexpr quint32 kMaxUncompressedPayload = 1u << 30; // 1 GiB -- far above any real circuit
352 constexpr quint32 kMaxExpansionRatio = 1032; // zlib's documented worst-case deflate expansion
353 const quint32 compressedBytes = static_cast<quint32>(raw.size() - 4);
354 if (expectedSize > kMaxUncompressedPayload ||
355 (compressedBytes > 0 && expectedSize / compressedBytes > kMaxExpansionRatio)) {
356 throw PANDACEPTION_WITH_CONTEXT("Serialization",
357 "Implausible decompressed payload size %1 -- stream may be corrupt",
358 QString::number(expectedSize));
359 }
360
361 const QByteArray decompressed = qUncompress(raw);
362 if (decompressed.isNull() && expectedSize != 0) {
363 throw PANDACEPTION_WITH_CONTEXT("Serialization",
364 "Failed to decompress circuit payload -- stream may be corrupt");
365 }
366 return decompressed;
367}
368
369QVersionNumber Serialization::readPandaHeader(QDataStream &stream)
370{
371 stream.setVersion(QDataStream::Qt_5_12);
372
373 QIODevice *device = stream.device();
374 if (!device) {
375 throw PANDACEPTION("Invalid file format.");
376 }
377
378 // Every speculative check below peeks (never consumes) so a truncated or
379 // legacy file can't latch QDataStream::status() to ReadPastEnd — that
380 // status is sticky, and device()->seek() rewinding the position does not
381 // reset it, so a later legitimate status check would see a stale error
382 // from a speculative read here instead of a real one. The live stream is
383 // only ever touched once the format is unambiguously confirmed.
384 quint32 magicHeader = 0;
385 const bool haveMagic = peekU32(stream, magicHeader);
386
387 if (haveMagic && magicHeader == MAGIC_HEADER_CIRCUIT) {
388 stream >> magicHeader; // consume the confirmed magic
389 return readVersionNumber(stream);
390 }
391
392 // Not the modern magic — the same 4 peeked bytes double as the length
393 // prefix of a legacy "wiRedPanda X.Y" text header.
394 constexpr quint32 NullStringMarker = 0xFFFFFFFFu;
395 constexpr qint64 MaxLegacyHeaderBytes = 128;
396
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);
403 QString appName;
404 strProbe >> appName;
405 if (strProbe.status() == QDataStream::Ok && appName.startsWith("wiRedPanda", Qt::CaseInsensitive)) {
406 // Confirmed — now safe to consume the header from the live stream.
407 QString consumed;
408 stream >> consumed;
409 const QStringList split = consumed.split(' ');
410 if (split.size() < 2) {
411 throw PANDACEPTION("Invalid legacy file header: %1", consumed);
412 }
413 return QVersionNumber::fromString(split.at(1));
414 }
415 }
416 }
417
418 // Headerless v4.1 clipboard format: the stream starts directly with the
419 // scene's center-point QPointF (2 doubles) — no header at all.
420 char pointProbeBuf[2 * sizeof(double)];
421 if (device->peek(pointProbeBuf, sizeof(pointProbeBuf)) != static_cast<qint64>(sizeof(pointProbeBuf))) {
422 throw PANDACEPTION("Invalid file format.");
423 }
424 QDataStream pointProbe(QByteArray(pointProbeBuf, sizeof(pointProbeBuf)));
425 pointProbe.setVersion(QDataStream::Qt_5_12);
426 QPointF center;
427 pointProbe >> center;
428 if (pointProbe.status() != QDataStream::Ok || center.isNull()) {
429 throw PANDACEPTION("Invalid file format.");
430 }
431
432 // Version 4.1 is the last release that used this headerless format; the
433 // live stream is left untouched so the caller reads the QPointF itself next.
434 return QVersionNumber(4, 1);
435}
436
437void Serialization::writeDolphinHeader(QDataStream &stream)
438{
439 stream.setVersion(QDataStream::Qt_5_12);
440 stream << MAGIC_HEADER_WAVEFORM;
441 stream << FormatRev::current;
442}
443
444void Serialization::readDolphinHeader(QDataStream &stream)
445{
446 stream.setVersion(QDataStream::Qt_5_12);
447
448 QIODevice *device = stream.device();
449 if (!device) {
450 throw PANDACEPTION("Invalid file format.");
451 }
452
453 // Same probe-first approach as readPandaHeader: peek (never consume) so a
454 // truncated/legacy file's speculative read can't latch a sticky
455 // ReadPastEnd status that a later seek() wouldn't reset.
456 quint32 magicHeader = 0;
457 const bool haveMagic = peekU32(stream, magicHeader);
458
459 if (haveMagic && magicHeader == MAGIC_HEADER_WAVEFORM) {
460 stream >> magicHeader; // consume the confirmed magic
461 readVersionNumber(stream); // advance stream past version, enforce segment count bound
462 return;
463 }
464
465 // Legacy beWavedDolphin format: "<byteLen><appName>" as raw UTF-16 code
466 // units (byteLen in bytes, so byteLen/2 UTF-16 units). The same 4 peeked
467 // bytes double as byteLen. "beWavedDolphin X.Y" is at most ~22 chars =
468 // 44 bytes; 256 is a generous cap.
469 if (!haveMagic || magicHeader == 0xFFFFFFFFu || magicHeader > 256 || magicHeader % 2 != 0) {
470 throw PANDACEPTION("Invalid file format.");
471 }
472 const qint64 probeLen = sizeof(quint32) + magicHeader;
473 QByteArray strProbeBuf(static_cast<int>(probeLen), '\0');
474 if (device->peek(strProbeBuf.data(), probeLen) != probeLen) {
475 throw PANDACEPTION("Invalid file format.");
476 }
477 // strProbeBuf's buffer has no 2-byte alignment guarantee, so the UTF-16 code
478 // units are copied into a properly-aligned scratch buffer instead of being
479 // reinterpret_cast in place — that cast is a strict-aliasing/alignment
480 // violation that -Wcast-align catches on armhf/riscv64 (issue #453).
481 // Plain byte copy, no endian conversion: unlike the QDataStream-framed
482 // "wiRedPanda X.Y" legacy header in readPandaHeader above (which is read
483 // back via stream >> QString), this beWavedDolphin field is a raw
484 // native-order UTF-16 dump — introducing qFromBigEndian/qFromLittleEndian
485 // here, or reusing readPandaHeader's QDataStream-based probe, would change
486 // behaviour, not just alignment.
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")) {
493 throw PANDACEPTION("Invalid file format.");
494 }
495
496 // Confirmed — now safe to consume the header from the live stream.
497 quint32 consumedLen = 0;
498 stream >> consumedLen;
499 QByteArray raw(static_cast<int>(consumedLen), '\0');
500 stream.readRawData(raw.data(), static_cast<int>(consumedLen));
501}
502
503void Serialization::serialize(const QList<QGraphicsItem *> &items, QDataStream &stream, SerializationOptions options)
504{
505 // Port serial IDs are computed from the element ID via Port::makeSerialId()
506 // (used by both GraphicElementSerializer::save() and ConnectionSerializer::save()).
507 // Elements with id=-1 (the unassigned sentinel) would all produce the same keys,
508 // causing portMap collisions on the next load and destroying connection topology.
509 //
510 // Only elements with id <= 0 need a temporary replacement ID. Elements that
511 // already have a valid positive scene ID must keep it: callers such as
512 // CommandUtils::saveItems() write "other" elements into the same stream
513 // before calling serialize(), and reassigning those already-used IDs to
514 // items in this list creates portMap key collisions on load.
515 //
516 // Temp IDs start above the highest positive ID already present in items so
517 // they cannot collide with any valid ID in the list.
518 QList<std::pair<GraphicElement *, int>> savedIds;
519 int localId = 0;
520 for (auto *item : items) {
521 if (auto *ge = qgraphicsitem_cast<GraphicElement *>(item)) {
522 if (ge->id() > localId) {
523 localId = ge->id();
524 }
525 }
526 }
527 for (auto *item : items) {
528 if (auto *ge = qgraphicsitem_cast<GraphicElement *>(item)) {
529 if (ge->id() <= 0) {
530 savedIds.append({ge, ge->id()});
531 ge->setId(++localId);
532 }
533 }
534 }
535
536 // Restore original IDs on scope exit, even if an exception escapes below.
537 // Without this guard, a thrown exception would leave elements with sequential
538 // IDs instead of their original values, causing ID collisions on scene insertion.
539 auto restoreIds = qScopeGuard([&savedIds] {
540 for (const auto &[ge, id] : savedIds) {
541 ge->setId(id);
542 }
543 });
544
545 // Elements must be written before connections because deserialization reads
546 // elements first to build the port map, then resolves connection endpoints
547 // using that map. Reversing the order would cause every connection load to fail.
548 // Serialize all graphic elements (gates, inputs, outputs, ICs)
549 // Type tag written here at serialization layer for symmetry with deserialize()
550 for (auto *item : items) {
551 if (auto *element = qgraphicsitem_cast<GraphicElement *>(item)) {
552 stream << static_cast<int>(GraphicElement::Type);
553 stream << element->elementType();
554 element->save(stream, options);
555 }
556 }
557
558 // Serialize all connections (wires)
559 // Type tag written here at serialization layer for symmetry with deserialize()
560 for (auto *item : items) {
561 if (auto *connection = qgraphicsitem_cast<Connection *>(item)) {
562 stream << static_cast<int>(Connection::Type);
563 connection->save(stream);
564 }
565 }
566}
567
568QList<QGraphicsItem *> Serialization::deserialize(QDataStream &stream, SerializationContext &context)
569{
570 // portMap maps the raw pointer value (quint64) that was stored at save time to
571 // the newly allocated Port object at load time. Raw pointer values are used
572 // as keys because they were the only unique, stable port identity available when
573 // the binary format was designed; they are meaningless as pointers after reload
574 // but work perfectly as opaque 64-bit tokens for this cross-reference purpose.
575 // When portMap is empty (top-level file load), Connection::load() falls back
576 // to directly casting the stored integer back to a pointer — only safe during
577 // the same process session (e.g., clipboard paste with in-process copy/paste).
578 QList<QGraphicsItem *> itemList;
579 // Exception safety: if any throw escapes the loop, the guard cleans up every
580 // item already constructed. Callers expect ownership transfer on success only —
581 // dismissing the guard at the end of the happy path hands the items to them.
582 //
583 // Order matters. Connections must be deleted before elements: an element's
584 // InputPort destructor calls Port::drainConnections(true), which
585 // delete()s every connection still attached to the port. If those connections
586 // are also in itemList, a naive qDeleteAll would walk onto a freed pointer
587 // and double-free. A connection's destructor only detaches itself from its
588 // endpoint ports — safe regardless of element state — so doing connections
589 // first is unconditionally the right order. Surfaced by the libFuzzer
590 // harness against malformed .panda input that triggers a mid-loop throw.
591 auto cleanupGuard = qScopeGuard([&itemList]() {
592 for (qsizetype i = 0; i < itemList.size(); ++i) {
593 if (itemList[i] && itemList[i]->type() == Connection::Type) {
594 delete itemList[i];
595 itemList[i] = nullptr;
596 }
597 }
598 qDeleteAll(itemList);
599 });
600
601 while (!stream.atEnd()) {
602 int type; stream >> type;
603
604 // Check stream integrity after reading type tag
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()));
608 }
609
610 qCDebug(three) << "Type: " << typeName(type);
611
612 switch (type) {
614 ElementType elmType; stream >> elmType;
615
616 // Check stream integrity after reading element type
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()));
620 }
621
622 // Append before load() so cleanupGuard owns the element if load throws
623 // (e.g. nested IC whose backing file is missing).
624 auto *elm = ElementFactory::buildElement(elmType);
625 itemList.append(elm);
626 elm->load(stream, context);
627
628 // Check stream integrity after element load
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()));
632 }
633
634 break;
635 }
636
637 case Connection::Type: {
638 qCDebug(three) << "Building connection.";
639 auto *conn = new Connection();
640 // Same as the element case: append first so a throw in conn->load() can't
641 // strand the freshly allocated connection.
642 itemList.append(conn);
643
644 qCDebug(three) << "Loading connection.";
645 conn->load(stream, context);
646
647 // Check stream integrity after connection load
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()));
651 }
652 break;
653 }
654
655 default:
656 throw PANDACEPTION("Invalid type. Data is possibly corrupted.");
657 }
658 }
659
660 qCDebug(zero) << "Finished deserializing.";
661 cleanupGuard.dismiss();
662 return itemList;
663}
664
665QString Serialization::loadDolphinFileName(QDataStream &stream, const QVersionNumber &version)
666{
667 QString filename;
668
669 if (VersionInfo::hasDolphinFilename(version)) {
670 filename = readBoundedString(stream);
671
672 // Versions 3.0–3.2 used the sentinel string "none" instead of an empty
673 // QString to indicate that no waveform file was associated; normalize it here
674 // so callers can simply check isEmpty()
675 if ((!VersionInfo::hasDolphinSentinelFix(version)) && (filename == "none")) {
676 filename.clear();
677 }
678 }
679
680 // Versions before 3.0 didn't store a dolphin filename at all; return empty string
681 return filename;
682}
683
684QRectF Serialization::loadRect(QDataStream &stream, const QVersionNumber &version)
685{
686 QRectF rect;
687
688 // The stored rect is always discarded by the caller (WorkSpace recomputes it from
689 // items after load), but it must still be read to advance the stream past this field
690 if (VersionInfo::hasSceneRect(version)) {
691 stream >> rect;
692 }
693
694 return rect;
695}
696
697void Serialization::createVersionedBackup(const QString &fileName, const QVersionNumber &version)
698{
699 QFileInfo info(fileName);
700 const QString backupName = info.absolutePath() + "/"
701 + info.completeBaseName()
702 + ".v" + version.toString()
703 + "." + info.suffix();
704
705 if (!QFile::exists(backupName)) {
706 if (QFile::copy(fileName, backupName)) {
707 qCDebug(three) << "Created versioned backup: " << backupName;
708 } else {
709 throw PANDACEPTION("Failed to create versioned backup: %1", backupName);
710 }
711 }
712}
713
715{
716 Preamble result;
717 result.version = readPandaHeader(stream);
718
719 // Everything after the header (dolphin/rect/metadata, and whatever the caller
720 // reads afterward) may be zlib-compressed (Rev100+); buffer and decompress it
721 // once here rather than reading further from the live device, which readPayload()
722 // has already fully consumed to do that decompression.
723 QByteArray payload = readPayload(stream, result.version);
724 QDataStream payloadStream(&payload, QIODevice::ReadOnly);
725 payloadStream.setVersion(QDataStream::Qt_5_12);
726
728 result.metadata = readBoundedMetadata(payloadStream);
729 } else {
730 loadDolphinFileName(payloadStream, result.version);
731 loadRect(payloadStream, result.version);
732 if (VersionInfo::hasMetadata(result.version)) {
733 result.metadata = readBoundedMetadata(payloadStream);
734 }
735 }
736
737 if (payloadStream.status() != QDataStream::Ok) {
738 throw PANDACEPTION("Stream error reading preamble: status %1", static_cast<int>(payloadStream.status()));
739 }
740
741 // Leftover bytes (elements + connections) for the caller to continue parsing
742 // from a fresh stream of its own -- the live device passed to this function
743 // has nothing left to offer past this point.
744 result.remainingPayload = payload.mid(static_cast<int>(payloadStream.device()->pos()));
745
746 return result;
747}
748
749QMap<QString, QByteArray> Serialization::deserializeBlobRegistry(const QMap<QString, QVariant> &metadata,
750 const QVersionNumber &fileVersion)
751{
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(&regBytes, QIODevice::ReadOnly);
759 regStream.setVersion(QDataStream::Qt_5_12);
760 // Pre-V_5_0 files: no setVersion — use Qt default matching the build that wrote them.
761 result = readBoundedBlobMap(regStream);
762 }
763 return result;
764}
765
766void Serialization::serializeBlobRegistry(const QMap<QString, QByteArray> &blobs, QMap<QString, QVariant> &metadata)
767{
768 if (!blobs.isEmpty()) {
769 QByteArray regBytes;
770 QDataStream regStream(&regBytes, QIODevice::WriteOnly);
771 regStream.setVersion(QDataStream::Qt_5_12);
772 regStream << blobs;
773 metadata["embeddedICs"] = regBytes;
774 }
775}
776
777QString Serialization::typeName(const int type) {
778 // These offsets must stay in sync with the ::Type enum constants defined in
779 // Port, Connection, and GraphicElement (all use QGraphicsItem::UserType + N)
780 static const QHash<int, QString> typeMap = {
781 { QGraphicsItem::UserType + 1, "Port" },
782 { QGraphicsItem::UserType + 2, "Connection" },
783 { QGraphicsItem::UserType + 3, "GraphicElement" },
784 };
785
786 return typeMap.value(type, "UnknownType");
787}
788
789void Serialization::copyPandaFile(const QFileInfo &srcPath, const QFileInfo &destPath,
790 QSet<QString> *visited, int depth)
791{
792 // Cycle detection (visited) alone doesn't bound a long, non-cyclic chain of distinct
793 // dependency files (A references B references C references ..., no repeats) — mirrors
794 // ICLoader's kMaxICNestingDepth for the identical failure mode on the IC-load path.
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());
799 }
800
801 QSet<QString> ownedVisited;
802 if (!visited) {
803 visited = &ownedVisited;
804 }
805 const QString canonical = srcPath.canonicalFilePath();
806 if (!canonical.isEmpty()) {
807 if (visited->contains(canonical)) {
808 return;
809 }
810 visited->insert(canonical);
811 }
812
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());
817 }
818 }
819
820 // Read the metadata section to find file-backed IC dependencies,
821 // then recursively copy each one. No full deserialization needed.
822 QFile file(srcPath.absoluteFilePath());
823 if (!file.open(QIODevice::ReadOnly)) {
824 return;
825 }
826
827 // Quick peek-based plausibility check before attempting a full parse, so a
828 // non-panda file encountered while walking IC dependency blobs is quietly
829 // skipped without paying for readPreamble()'s exception path.
830 // Peek only — never touch the stream readPreamble() constructs next.
831 {
832 char magicBuf[sizeof(quint32)];
833 if (file.peek(magicBuf, sizeof(magicBuf)) != static_cast<qint64>(sizeof(magicBuf))) {
834 return;
835 }
836 const quint32 magicHeader = qFromBigEndian<quint32>(magicBuf);
837 if (magicHeader != MAGIC_HEADER_CIRCUIT) {
838 constexpr quint32 NullStringMarker = 0xFFFFFFFFu;
839 constexpr qint64 MaxLegacyHeaderBytes = 128;
840 if (magicHeader == NullStringMarker || magicHeader > MaxLegacyHeaderBytes) {
841 return;
842 }
843 const qint64 probeLen = sizeof(quint32) + magicHeader;
844 QByteArray strProbeBuf(static_cast<int>(probeLen), '\0');
845 if (file.peek(strProbeBuf.data(), probeLen) != probeLen) {
846 return;
847 }
848 QDataStream strProbe(strProbeBuf);
849 strProbe.setVersion(QDataStream::Qt_5_12);
850 QString appName;
851 strProbe >> appName;
852 if (strProbe.status() != QDataStream::Ok || !appName.startsWith("wiRedPanda", Qt::CaseInsensitive)) {
853 return;
854 }
855 }
856 }
857
858 QDataStream stream(&file);
859 Preamble preamble;
860 try {
861 preamble = readPreamble(stream);
862 } catch (...) {
863 return; // Corrupt or non-panda file — nothing to recurse into
864 }
865 if (!VersionInfo::hasMetadata(preamble.version)) {
866 return;
867 }
868
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()) {
874 copyPandaFile(icSrc, icDest, visited, depth + 1);
875 }
876 }
877}
Common logging utilities, the Pandaception error type, and helper macros.
#define PANDACEPTION(msg,...)
Definition Common.h:98
#define PANDACEPTION_WITH_CONTEXT(context, msg,...)
Definition Common.h:102
#define qCDebug(category)
Definition Common.h:29
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
Definition Enums.h:107
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.
Definition Connection.h:38
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.
const QVersionNumber current
Definition Versions.h:63
bool hasCompressedPayload(const QVersionNumber &v)
Definition VersionInfo.h:87
bool hasVersionedBlobRegistry(const QVersionNumber &v)
Definition VersionInfo.h:83
bool hasDolphinFilename(const QVersionNumber &v)
V3.0: Dolphin waveform filename stored in file header.
Definition VersionInfo.h:48
bool hasMetadata(const QVersionNumber &v)
V4.5: File-level metadata map and embedded IC blob registry.
Definition VersionInfo.h:72
bool hasSceneRect(const QVersionNumber &v)
V1.4: Scene rectangle stored in file.
Definition VersionInfo.h:30
bool hasDolphinSentinelFix(const QVersionNumber &v)
V3.3: "none" sentinel normalised to empty string for dolphin filename.
Definition VersionInfo.h:54
bool hasUnifiedMetadata(const QVersionNumber &v)
V4.6: Dolphin filename moved into metadata map; scene rect no longer stored.
Definition VersionInfo.h:75
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