| 965 | // ~96-106, signal peaks ~110-115. Colour-mapped in SpectrumWidget. |
| 966 | |
| 967 | void PanadapterStream::decodeWaterfallTile(const uchar* raw, int totalBytes, bool hasTrailer, quint32 streamId) |
| 968 | { |
| 969 | static constexpr int TILE_SUBHEADER_BYTES = 36; |
| 970 | if (totalBytes < VITA49_HEADER_BYTES + TILE_SUBHEADER_BYTES) return; |
| 971 | |
| 972 | const uchar* sub = raw + VITA49_HEADER_BYTES; |
| 973 | |
| 974 | // Extract frequency range from tile sub-header. |
| 975 | // FrameLowFreq and BinBandwidth are int64 "VitaFrequency" (Hz × 2^20). |
| 976 | const qint64 frameLowRaw = qFromBigEndian<qint64>(sub + 0); |
| 977 | const qint64 binBwRaw = qFromBigEndian<qint64>(sub + 8); |
| 978 | const quint16 tileWidth = qFromBigEndian<quint16>(sub + 20); |
| 979 | const quint16 tileHeight = qFromBigEndian<quint16>(sub + 22); |
| 980 | const quint32 timecode = qFromBigEndian<quint32>(sub + 24); |
| 981 | const quint32 autoBlack = qFromBigEndian<quint32>(sub + 28); |
| 982 | const quint16 totalBinsInFrame = qFromBigEndian<quint16>(sub + 32); |
| 983 | const quint16 firstBinIndex = qFromBigEndian<quint16>(sub + 34); |
| 984 | |
| 985 | if (tileWidth == 0 || tileHeight == 0) return; |
| 986 | |
| 987 | // FrameLowFreq and BinBandwidth arrive as either VitaFrequency (Hz × 2^20) |
| 988 | // or plain Hz; disambiguate on the raw integer magnitude so there is no |
| 989 | // upper frequency ceiling. The previous "divide then reject results above |
| 990 | // 1000 MHz" heuristic blacked out the waterfall for every transverter above |
| 991 | // 1 GHz (#3449, #1843, #1928, #2835). See VitaTileFrequency.h. |
| 992 | const auto tileFreq = AetherSDR::Vita::decodeTileFrequencyMhz(frameLowRaw, binBwRaw); |
| 993 | const double lowFreqMhz = tileFreq.lowMhz; |
| 994 | const double binBwMhz = tileFreq.binBwMhz; |
| 995 | const double highFreqMhz = lowFreqMhz + binBwMhz * tileWidth; |
| 996 | |
| 997 | const int payloadOffset = VITA49_HEADER_BYTES + TILE_SUBHEADER_BYTES; |
| 998 | const int payloadBytes = totalBytes - payloadOffset - (hasTrailer ? 4 : 0); |
| 999 | if (payloadBytes < tileWidth * 2) return; // need at least one row of bins |
| 1000 | |
| 1001 | static bool loggedOnce = false; |
| 1002 | if (!loggedOnce) { |
| 1003 | qCDebug(lcVita49) << "WaterfallTile: width=" << tileWidth << "height=" << tileHeight |
| 1004 | << "totalBinsInFrame=" << totalBinsInFrame |
| 1005 | << "firstBinIndex=" << firstBinIndex |
| 1006 | << "timecode=" << timecode |
| 1007 | << "lowFreqMhz=" << lowFreqMhz |
| 1008 | << "binBwMhz=" << binBwMhz |
| 1009 | << "highFreqMhz=" << highFreqMhz |
| 1010 | << "fullFrameMhz=" << (lowFreqMhz + binBwMhz * totalBinsInFrame) |
| 1011 | << "autoBlack=" << autoBlack; |
| 1012 | loggedOnce = true; |
| 1013 | } |
| 1014 | |
| 1015 | // ── Waterfall frame assembly ───────────────────────────────────────── |
| 1016 | // Start a new frame if timecode changed OR if totalBinsInFrame changed. |
| 1017 | // Without the totalBins check, a spoofed packet that reuses a timecode |
| 1018 | // with an inflated totalBinsInFrame leaves wfFrame.buf undersized |
| 1019 | // relative to the bounds calculation below, and the inner write loop |
| 1020 | // then writes past the buffer's end with attacker-chosen bytes. |
| 1021 | // See GHSA-7gvg-x594-pprq. |
| 1022 | auto& wfFrame = m_wfFrames[streamId]; |
| 1023 | if (timecode != wfFrame.timecode |
| 1024 | || totalBinsInFrame != wfFrame.totalBins) { |
nothing calls this directly
no test coverage detected