Showing the changes to interface/src/scripting/OSCScriptingInterface.cpp (1 of 1 file). Show all
Unified · Split
| @@ -11,7 +11,6 @@ · 10 unmodified lines | ||
| 11 | 11 | |
| 12 | 12 | #include <QLoggingCategory> |
| 13 | 13 | #include <QRegularExpression> |
| 14 | −#include <QtEndian> | |
| 15 | 14 | #include <QNetworkDatagram> |
| 16 | 15 | |
| 17 | 16 | #include "ScriptContext.h" |
| @@ -19,14 +18,144 @@ · 1 unmodified line | ||
| 19 | 18 | #include "ScriptValue.h" |
| 20 | 19 | #include "OSCScriptingInterface.h" |
| 21 | 20 | |
| 21 | +#include <array> | |
| 22 | +#include <bit> | |
| 23 | +#include <optional> | |
| 24 | +#include <QByteArray> | |
| 22 | 25 | |
| 23 | −Q_LOGGING_CATEGORY(osc_cat, "osc") | |
| 26 | +// TODO: Split this out into libshared later | |
| 27 | +// This might be handy to reuse in other modules too | |
| 28 | +namespace DataHelpers { | |
| 29 | + | |
| 30 | +template<typename T, std::endian endianness = std::endian::big> | |
| 31 | + // don't try to write pointers, references, structs, etc | |
| 32 | + requires (std::is_arithmetic_v<T>) | |
| 33 | +inline auto write(QByteArray& ba, T v) { | |
| 34 | + std::array<char, sizeof(v)> buffer; | |
| 35 | + | |
| 36 | + std::copy_n(reinterpret_cast<char*>(&v), sizeof(v), buffer.begin()); | |
| 37 | + | |
| 38 | + if constexpr (std::endian::native != endianness) { | |
| 39 | + std::reverse(buffer.begin(), buffer.end()); | |
| 40 | + } | |
| 41 | + | |
| 42 | + return ba.append(buffer.data(), buffer.size()); | |
| 43 | +} | |
| 44 | + | |
| 45 | +template<typename T> | |
| 46 | +inline auto writeUTF8NullTerminated(QByteArray& ba, const T& v); | |
| 47 | + | |
| 48 | +template<> | |
| 49 | +inline auto writeUTF8NullTerminated(QByteArray& ba, const QString& v) { | |
| 50 | + ba.append(v.toUtf8()); | |
| 51 | + return ba.append('\x00'); | |
| 52 | +} | |
| 53 | + | |
| 54 | +template<typename T, std::endian endianness = std::endian::big> | |
| 55 | + // don't try to read pointers, references, structs, etc | |
| 56 | + requires (std::is_arithmetic_v<T>) | |
| 57 | +inline std::optional<T> read(QByteArray& ba, int& offset) { | |
| 58 | + if (offset < 0) { return std::nullopt; } | |
| 59 | + | |
| 60 | + std::array<char, sizeof(T)> buffer; | |
| 61 | + | |
| 62 | + // the byte array doesn't have enough | |
| 63 | + // data left to fulfill the read | |
| 64 | + if (offset + static_cast<int>(sizeof(T)) > ba.size()) { | |
| 65 | + return std::nullopt; | |
| 66 | + } | |
| 67 | + | |
| 68 | + std::copy_n(ba.constData() + offset, sizeof(T), buffer.begin()); | |
| 69 | + | |
| 70 | + if constexpr (std::endian::native != endianness) { | |
| 71 | + std::reverse(buffer.begin(), buffer.end()); | |
| 72 | + } | |
| 73 | + | |
| 74 | + T value; | |
| 75 | + std::copy_n(buffer.data(), buffer.size(), reinterpret_cast<char*>(&value)); | |
| 76 | + | |
| 77 | + offset += sizeof(T); | |
| 78 | + | |
| 79 | + return value; | |
| 80 | +} | |
| 81 | + | |
| 82 | +template<typename T, int N> | |
| 83 | + requires (sizeof(T) == 1 && N > 0) | |
| 84 | +inline std::optional<std::array<T, N>> read(QByteArray& ba, int& offset) { | |
| 85 | + if (offset < 0) { return std::nullopt; } | |
| 86 | + | |
| 87 | + std::array<T, N> buffer; | |
| 88 | + | |
| 89 | + // the byte array doesn't have enough | |
| 90 | + // data left to fulfill the read | |
| 91 | + if (offset + N > ba.size()) { | |
| 92 | + return std::nullopt; | |
| 93 | + } | |
| 94 | + | |
| 95 | + std::copy_n(ba.constData() + offset, N, buffer.begin()); | |
| 96 | + | |
| 97 | + return buffer; | |
| 98 | +} | |
| 99 | + | |
| 100 | +inline std::optional<QByteArray> read(QByteArray& ba, int offset, int length) { | |
| 101 | + if (offset < 0 || length < 1) { return std::nullopt; } | |
| 102 | + | |
| 103 | + // the byte array doesn't have enough | |
| 104 | + // data left to fulfill the read | |
| 105 | + if (offset + length > ba.size()) { | |
| 106 | + return std::nullopt; | |
| 107 | + } | |
| 108 | + | |
| 109 | + return QByteArray(ba.constData() + offset, length); | |
| 110 | +} | |
| 111 | + | |
| 112 | +template<typename T> | |
| 113 | +inline std::optional<T> | |
| 114 | +readUTF8NullTerminated(QByteArray& ba, int& offset); | |
| 115 | + | |
| 116 | +template<> | |
| 117 | +inline std::optional<QString> | |
| 118 | +readUTF8NullTerminated(QByteArray& ba, int& offset) { | |
| 119 | + auto cursor = offset; | |
| 120 | + | |
| 121 | + for (int i = 0; (i + cursor) < ba.size(); i++) { | |
| 122 | + if (ba[i + cursor] == '\x00') { | |
| 123 | + offset = offset + i; | |
| 124 | + return QString::fromUtf8(ba.constData() + cursor, i); | |
| 125 | + } | |
| 126 | + } | |
| 127 | + | |
| 128 | + return std::nullopt; | |
| 129 | +} | |
| 130 | + | |
| 131 | +template<> | |
| 132 | +inline std::optional<QByteArray> | |
| 133 | +readUTF8NullTerminated(QByteArray& ba, int& offset) { | |
| 134 | + auto cursor = offset; | |
| 135 | + | |
| 136 | + for (int i = 0; (i + cursor) < ba.size(); i++) { | |
| 137 | + if (ba[i + cursor] == '\x00') { | |
| 138 | + offset = offset + i; | |
| 139 | + return QByteArray(ba.constData() + cursor, i); | |
| 140 | + } | |
| 141 | + } | |
| 142 | + | |
| 143 | + return std::nullopt; | |
| 144 | +} | |
| 145 | + | |
| 146 | +} | |
| 147 | + | |
| 148 | + | |
| 149 | +Q_LOGGING_CATEGORY(osc_cat, "overte.osc") | |
| 24 | 150 | |
| 25 | 151 | enum OSCTag: char { |
| 152 | + // standard types | |
| 26 | 153 | Int = 'i', |
| 27 | 154 | Float = 'f', |
| 28 | 155 | String = 's', |
| 29 | 156 | Blob = 'b', |
| 157 | + | |
| 158 | + // extension types | |
| 30 | 159 | False = 'F', |
| 31 | 160 | True = 'T', |
| 32 | 161 | Null = 'N', |
| @@ -78,77 +207,108 @@ void OSCScriptingInterface::readPacket() { · 45 unmodified lines | ||
| 78 | 207 | QByteArray argTypes; |
| 79 | 208 | QVariantList args; |
| 80 | 209 | |
| 81 | − // address | |
| 82 | − for (int i = 0; (i + cursor) < data.length(); i++) { | |
| 83 | − if (data[i + cursor] == '\x00') { | |
| 84 | − addr = QString::fromUtf8(data.constData(), i); | |
| 85 | − cursor = next4(cursor + i); | |
| 86 | − break; | |
| 87 | − } | |
| 210 | + if ( | |
| 211 | + auto s = DataHelpers::readUTF8NullTerminated<QString>(data, cursor); | |
| 212 | + s.has_value() && !s->isEmpty() | |
| 213 | + ) { | |
| 214 | + addr = *s; | |
| 215 | + } else { | |
| 216 | + qCritical(osc_cat, "Early EOF in packet, OSC address missing"); | |
| 217 | + return; | |
| 88 | 218 | } |
| 219 | + cursor = next4(cursor); | |
| 89 | 220 | |
| 90 | − if (addr.isEmpty()) { return; } | |
| 221 | + if (addr == "#bundle") { | |
| 222 | + qCritical(osc_cat, "Received OSC bundle, which isn't supported"); | |
| 223 | + return; | |
| 224 | + } | |
| 91 | 225 | |
| 92 | − // argument types | |
| 93 | − for (int i = 0; (i + cursor) < data.length(); i++) { | |
| 94 | − if (data[i + cursor] == '\x00') { | |
| 95 | − argTypes = QByteArray(data.constData() + cursor, i); | |
| 96 | − cursor = next4(cursor + i); | |
| 97 | − break; | |
| 98 | − } | |
| 226 | + if ( | |
| 227 | + auto s = DataHelpers::readUTF8NullTerminated<QByteArray>(data, cursor); | |
| 228 | + s.has_value() && !s->isEmpty() | |
| 229 | + ) { | |
| 230 | + argTypes = *s; | |
| 231 | + } else { | |
| 232 | + qCritical(osc_cat, "Early EOF in packet, argument types string missing"); | |
| 233 | + return; | |
| 234 | + } | |
| 235 | + cursor = next4(cursor); | |
| 236 | + | |
| 237 | + if (argTypes[0] != ',') { | |
| 238 | + qCritical(osc_cat, "Argument type string does not start with ','"); | |
| 239 | + return; | |
| 99 | 240 | } |
| 100 | 241 | |
| 101 | 242 | // +1 to exclude the comma |
| 102 | 243 | for (int arg = 1; arg < argTypes.length(); arg++) { |
| 103 | 244 | switch (argTypes[arg]) { |
| 104 | 245 | case OSCTag::Int: { |
| 105 | − const char* ptr = data.constData() + cursor; | |
| 106 | − qint32 tmp = qFromBigEndian<qint32>(ptr); | |
| 246 | + auto value = DataHelpers::read<qint32>(data, cursor); | |
| 247 | + | |
| 248 | + if (!value.has_value()) { | |
| 249 | + qCritical(osc_cat, "Early EOF in packet (%s), reading int at offset %d", qUtf8Printable(addr), cursor); | |
| 250 | + return; | |
| 251 | + } | |
| 252 | + | |
| 107 | 253 | args.append(QVariantMap { |
| 108 | 254 | {"type", QString(OSCTag::Int)}, |
| 109 | − {"value", tmp}, | |
| 255 | + {"value", value.value()}, | |
| 110 | 256 | }); |
| 111 | − cursor += 4; | |
| 112 | 257 | } break; |
| 113 | 258 | |
| 114 | 259 | case OSCTag::Float: { |
| 115 | − const char* ptr = data.constData() + cursor; | |
| 116 | − qint32 tmp = qFromBigEndian<qint32>(ptr); | |
| 260 | + auto value = DataHelpers::read<float>(data, cursor); | |
| 261 | + | |
| 262 | + if (!value.has_value()) { | |
| 263 | + qCritical(osc_cat, "Early EOF in packet (%s), reading float at offset %d", qUtf8Printable(addr), cursor); | |
| 264 | + return; | |
| 265 | + } | |
| 266 | + | |
| 117 | 267 | args.append(QVariantMap { |
| 118 | 268 | {"type", QString(OSCTag::Float)}, |
| 119 | − {"value", *reinterpret_cast<float*>(&tmp)}, | |
| 269 | + {"value", value.value()}, | |
| 120 | 270 | }); |
| 121 | − cursor += 4; | |
| 122 | 271 | } break; |
| 123 | 272 | |
| 124 | 273 | case OSCTag::String: { |
| 125 | − QString tmp; | |
| 126 | − | |
| 127 | − for (int i = 0; i + cursor < data.length(); i++) { | |
| 128 | − if (data[i + cursor] == '\x00') { | |
| 129 | − tmp = QString::fromUtf8(data.constData() + cursor, i); | |
| 130 | − cursor = next4(cursor + i); | |
| 131 | − break; | |
| 132 | − } | |
| 274 | + int startCursor = cursor; | |
| 275 | + auto value = DataHelpers::readUTF8NullTerminated<QString>(data, cursor); | |
| 276 | + | |
| 277 | + if (!value.has_value()) { | |
| 278 | + qCritical(osc_cat, "Early EOF in packet (%s), string starting at offset %d", qUtf8Printable(addr), startCursor); | |
| 279 | + return; | |
| 133 | 280 | } |
| 134 | 281 | |
| 282 | + cursor = next4(cursor); | |
| 283 | + | |
| 135 | 284 | args.append(QVariantMap { |
| 136 | 285 | {"type", QString(OSCTag::String)}, |
| 137 | − {"value", tmp}, | |
| 286 | + {"value", value.value()}, | |
| 138 | 287 | }); |
| 139 | 288 | } break; |
| 140 | 289 | |
| 141 | 290 | case OSCTag::Blob: { |
| 142 | − const char* ptr = data.constData() + cursor; | |
| 143 | − auto len = qFromBigEndian<qint32>(ptr); | |
| 144 | − cursor += 4; | |
| 291 | + auto len = DataHelpers::read<qint32>(data, cursor); | |
| 292 | + | |
| 293 | + if (!len.has_value()) { | |
| 294 | + qCritical(osc_cat, "Early EOF in packet (%s), reading blob at offset %d (length prefix)", qUtf8Printable(addr), cursor); | |
| 295 | + return; | |
| 296 | + } | |
| 297 | + | |
| 298 | + auto startCursor = cursor; | |
| 299 | + auto bytes = DataHelpers::read(data, cursor, len.value()); | |
| 300 | + | |
| 301 | + if (!bytes.has_value()) { | |
| 302 | + qCritical(osc_cat, "Early EOF in packet (%s), reading blob at offset %d (data)", qUtf8Printable(addr), startCursor); | |
| 303 | + return; | |
| 304 | + } | |
| 145 | 305 | |
| 146 | 306 | args.append(QVariantMap { |
| 147 | 307 | {"type", QString(OSCTag::Blob)}, |
| 148 | − {"value", QByteArray(data.constData() + cursor, len)}, | |
| 308 | + {"value", bytes.value()}, | |
| 149 | 309 | }); |
| 150 | 310 | |
| 151 | − cursor = next4(cursor + len); | |
| 311 | + cursor = next4(cursor + len.value()); | |
| 152 | 312 | } break; |
| 153 | 313 | |
| 154 | 314 | case OSCTag::False: |
| @@ -196,6 +356,12 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 41 unmodified lines | ||
| 196 | 356 | arguments.append(context->argument(i).toVariant()); |
| 197 | 357 | } |
| 198 | 358 | |
| 359 | + | |
| 360 | + if (address == "#bundle") { | |
| 361 | + engine->raiseException("OSC bundles are not supported"); | |
| 362 | + return engine->undefinedValue(); | |
| 363 | + } | |
| 364 | + | |
| 199 | 365 | if (address.length() < 2 || !address.startsWith('/') || address.endsWith('/')) { |
| 200 | 366 | engine->raiseException("Address must be at least two characters and start with '/'"); |
| 201 | 367 | return engine->undefinedValue(); |
| @@ -249,22 +415,12 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 47 unmodified lines | ||
| 249 | 415 | switch (expectedType) { |
| 250 | 416 | case QVariant::Int: { |
| 251 | 417 | bytes.append(OSCTag::Int); |
| 252 | − | |
| 253 | − auto* ptr = bodyBytes.data() + bodyBytes.length(); | |
| 254 | − bodyBytes.append(4, 0); | |
| 255 | − | |
| 256 | − qToBigEndian(static_cast<qint32>(value.toDouble()), ptr); | |
| 418 | + DataHelpers::write(bodyBytes, static_cast<qint32>(value.toDouble())); | |
| 257 | 419 | } break; |
| 258 | 420 | |
| 259 | 421 | case QVariant::Double: { |
| 260 | 422 | bytes.append(OSCTag::Float); |
| 261 | − | |
| 262 | − auto* ptr = bodyBytes.data() + bodyBytes.length(); | |
| 263 | − bodyBytes.append(4, 0); | |
| 264 | − | |
| 265 | − // FIXME: use std::bit_cast instead of this hack once c++20 is in | |
| 266 | − auto tmp = static_cast<float>(value.toDouble()); | |
| 267 | − qToBigEndian(*reinterpret_cast<quint32*>(&tmp), ptr); | |
| 423 | + DataHelpers::write(bodyBytes, static_cast<float>(value.toDouble())); | |
| 268 | 424 | } break; |
| 269 | 425 | |
| 270 | 426 | case QVariant::String: { |
| @@ -277,10 +433,7 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 6 unmodified lines | ||
| 277 | 433 | return engine->undefinedValue(); |
| 278 | 434 | } |
| 279 | 435 | |
| 280 | − bodyBytes.append(stringArg.toUtf8()); | |
| 281 | − | |
| 282 | − // pad to 4-byte boundary with at least one zero for the string | |
| 283 | − bodyBytes.append('\x00'); | |
| 436 | + DataHelpers::writeUTF8NullTerminated(bodyBytes, stringArg); | |
| 284 | 437 | pad4(bodyBytes); |
| 285 | 438 | } break; |
| 286 | 439 | |
| @@ -289,12 +442,7 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 2 unmodified lines | ||
| 289 | 442 | |
| 290 | 443 | auto bytesArg = value.toByteArray(); |
| 291 | 444 | |
| 292 | − // write length prefix | |
| 293 | − auto* ptr = bodyBytes.data() + bodyBytes.length(); | |
| 294 | − bodyBytes.append(4, 0); | |
| 295 | − qToBigEndian(bytesArg.length(), ptr); | |
| 296 | − | |
| 297 | − // write body | |
| 445 | + DataHelpers::write(bodyBytes, bytesArg.length()); | |
| 298 | 446 | bodyBytes.append(bytesArg); |
| 299 | 447 | |
| 300 | 448 | // pad to 4-byte boundary |