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 | #include <QLoggingCategory> | 12 | #include <QLoggingCategory> |
| 13 | #include <QRegularExpression> | 13 | #include <QRegularExpression> |
| 14 | #include <QtEndian> | ||
| 15 | #include <QNetworkDatagram> | 14 | #include <QNetworkDatagram> |
| 16 | 15 | ||
| 17 | #include "ScriptContext.h" | 16 | #include "ScriptContext.h" |
| @@ -19,14 +18,144 @@ · 1 unmodified line | |||
| 19 | #include "ScriptValue.h" | 18 | #include "ScriptValue.h" |
| 20 | #include "OSCScriptingInterface.h" | 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 | enum OSCTag: char { | 151 | enum OSCTag: char { |
| 152 | // standard types | ||
| 26 | Int = 'i', | 153 | Int = 'i', |
| 27 | Float = 'f', | 154 | Float = 'f', |
| 28 | String = 's', | 155 | String = 's', |
| 29 | Blob = 'b', | 156 | Blob = 'b', |
| 157 | |||
| 158 | // extension types | ||
| 30 | False = 'F', | 159 | False = 'F', |
| 31 | True = 'T', | 160 | True = 'T', |
| 32 | Null = 'N', | 161 | Null = 'N', |
| @@ -78,77 +207,108 @@ void OSCScriptingInterface::readPacket() { · 45 unmodified lines | |||
| 78 | QByteArray argTypes; | 207 | QByteArray argTypes; |
| 79 | QVariantList args; | 208 | QVariantList args; |
| 80 | 209 | ||
| 81 | // address | 210 | if ( |
| 82 | for (int i = 0; (i + cursor) < data.length(); i++) { | 211 | auto s = DataHelpers::readUTF8NullTerminated<QString>(data, cursor); |
| 83 | if (data[i + cursor] == '\x00') { | 212 | s.has_value() && !s->isEmpty() |
| 84 | addr = QString::fromUtf8(data.constData(), i); | 213 | ) { |
| 85 | cursor = next4(cursor + i); | 214 | addr = *s; |
| 86 | break; | 215 | } else { |
| 87 | } | 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 | 226 | if ( |
| 93 | for (int i = 0; (i + cursor) < data.length(); i++) { | 227 | auto s = DataHelpers::readUTF8NullTerminated<QByteArray>(data, cursor); |
| 94 | if (data[i + cursor] == '\x00') { | 228 | s.has_value() && !s->isEmpty() |
| 95 | argTypes = QByteArray(data.constData() + cursor, i); | 229 | ) { |
| 96 | cursor = next4(cursor + i); | 230 | argTypes = *s; |
| 97 | break; | 231 | } else { |
| 98 | } | 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 | // +1 to exclude the comma | 242 | // +1 to exclude the comma |
| 102 | for (int arg = 1; arg < argTypes.length(); arg++) { | 243 | for (int arg = 1; arg < argTypes.length(); arg++) { |
| 103 | switch (argTypes[arg]) { | 244 | switch (argTypes[arg]) { |
| 104 | case OSCTag::Int: { | 245 | case OSCTag::Int: { |
| 105 | const char* ptr = data.constData() + cursor; | 246 | auto value = DataHelpers::read<qint32>(data, cursor); |
| 106 | qint32 tmp = qFromBigEndian<qint32>(ptr); | 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 | args.append(QVariantMap { | 253 | args.append(QVariantMap { |
| 108 | {"type", QString(OSCTag::Int)}, | 254 | {"type", QString(OSCTag::Int)}, |
| 109 | {"value", tmp}, | 255 | {"value", value.value()}, |
| 110 | }); | 256 | }); |
| 111 | cursor += 4; | ||
| 112 | } break; | 257 | } break; |
| 113 | 258 | ||
| 114 | case OSCTag::Float: { | 259 | case OSCTag::Float: { |
| 115 | const char* ptr = data.constData() + cursor; | 260 | auto value = DataHelpers::read<float>(data, cursor); |
| 116 | qint32 tmp = qFromBigEndian<qint32>(ptr); | 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 | args.append(QVariantMap { | 267 | args.append(QVariantMap { |
| 118 | {"type", QString(OSCTag::Float)}, | 268 | {"type", QString(OSCTag::Float)}, |
| 119 | {"value", *reinterpret_cast<float*>(&tmp)}, | 269 | {"value", value.value()}, |
| 120 | }); | 270 | }); |
| 121 | cursor += 4; | ||
| 122 | } break; | 271 | } break; |
| 123 | 272 | ||
| 124 | case OSCTag::String: { | 273 | case OSCTag::String: { |
| 125 | QString tmp; | 274 | int startCursor = cursor; |
| 126 | 275 | auto value = DataHelpers::readUTF8NullTerminated<QString>(data, cursor); | |
| 127 | for (int i = 0; i + cursor < data.length(); i++) { | 276 | |
| 128 | if (data[i + cursor] == '\x00') { | 277 | if (!value.has_value()) { |
| 129 | tmp = QString::fromUtf8(data.constData() + cursor, i); | 278 | qCritical(osc_cat, "Early EOF in packet (%s), string starting at offset %d", qUtf8Printable(addr), startCursor); |
| 130 | cursor = next4(cursor + i); | 279 | return; |
| 131 | break; | ||
| 132 | } | ||
| 133 | } | 280 | } |
| 134 | 281 | ||
| 282 | cursor = next4(cursor); | ||
| 283 | |||
| 135 | args.append(QVariantMap { | 284 | args.append(QVariantMap { |
| 136 | {"type", QString(OSCTag::String)}, | 285 | {"type", QString(OSCTag::String)}, |
| 137 | {"value", tmp}, | 286 | {"value", value.value()}, |
| 138 | }); | 287 | }); |
| 139 | } break; | 288 | } break; |
| 140 | 289 | ||
| 141 | case OSCTag::Blob: { | 290 | case OSCTag::Blob: { |
| 142 | const char* ptr = data.constData() + cursor; | 291 | auto len = DataHelpers::read<qint32>(data, cursor); |
| 143 | auto len = qFromBigEndian<qint32>(ptr); | 292 | |
| 144 | cursor += 4; | 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 | args.append(QVariantMap { | 306 | args.append(QVariantMap { |
| 147 | {"type", QString(OSCTag::Blob)}, | 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 | } break; | 312 | } break; |
| 153 | 313 | ||
| 154 | case OSCTag::False: | 314 | case OSCTag::False: |
| @@ -196,6 +356,12 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 41 unmodified lines | |||
| 196 | arguments.append(context->argument(i).toVariant()); | 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 | if (address.length() < 2 || !address.startsWith('/') || address.endsWith('/')) { | 365 | if (address.length() < 2 || !address.startsWith('/') || address.endsWith('/')) { |
| 200 | engine->raiseException("Address must be at least two characters and start with '/'"); | 366 | engine->raiseException("Address must be at least two characters and start with '/'"); |
| 201 | return engine->undefinedValue(); | 367 | return engine->undefinedValue(); |
| @@ -249,22 +415,12 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 47 unmodified lines | |||
| 249 | switch (expectedType) { | 415 | switch (expectedType) { |
| 250 | case QVariant::Int: { | 416 | case QVariant::Int: { |
| 251 | bytes.append(OSCTag::Int); | 417 | bytes.append(OSCTag::Int); |
| 252 | 418 | DataHelpers::write(bodyBytes, static_cast<qint32>(value.toDouble())); | |
| 253 | auto* ptr = bodyBytes.data() + bodyBytes.length(); | ||
| 254 | bodyBytes.append(4, 0); | ||
| 255 | |||
| 256 | qToBigEndian(static_cast<qint32>(value.toDouble()), ptr); | ||
| 257 | } break; | 419 | } break; |
| 258 | 420 | ||
| 259 | case QVariant::Double: { | 421 | case QVariant::Double: { |
| 260 | bytes.append(OSCTag::Float); | 422 | bytes.append(OSCTag::Float); |
| 261 | 423 | DataHelpers::write(bodyBytes, static_cast<float>(value.toDouble())); | |
| 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); | ||
| 268 | } break; | 424 | } break; |
| 269 | 425 | ||
| 270 | case QVariant::String: { | 426 | case QVariant::String: { |
| @@ -277,10 +433,7 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 6 unmodified lines | |||
| 277 | return engine->undefinedValue(); | 433 | return engine->undefinedValue(); |
| 278 | } | 434 | } |
| 279 | 435 | ||
| 280 | bodyBytes.append(stringArg.toUtf8()); | 436 | DataHelpers::writeUTF8NullTerminated(bodyBytes, stringArg); |
| 281 | |||
| 282 | // pad to 4-byte boundary with at least one zero for the string | ||
| 283 | bodyBytes.append('\x00'); | ||
| 284 | pad4(bodyBytes); | 437 | pad4(bodyBytes); |
| 285 | } break; | 438 | } break; |
| 286 | 439 | ||
| @@ -289,12 +442,7 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 2 unmodified lines | |||
| 289 | 442 | ||
| 290 | auto bytesArg = value.toByteArray(); | 443 | auto bytesArg = value.toByteArray(); |
| 291 | 444 | ||
| 292 | // write length prefix | 445 | DataHelpers::write(bodyBytes, bytesArg.length()); |
| 293 | auto* ptr = bodyBytes.data() + bodyBytes.length(); | ||
| 294 | bodyBytes.append(4, 0); | ||
| 295 | qToBigEndian(bytesArg.length(), ptr); | ||
| 296 | |||
| 297 | // write body | ||
| 298 | bodyBytes.append(bytesArg); | 446 | bodyBytes.append(bytesArg); |
| 299 | 447 | ||
| 300 | // pad to 4-byte boundary | 448 | // pad to 4-byte boundary |