History

DataHelpers namespace for easier reading/writing

Ada committed 10 months ago

Commit a29c9611 parent 08ce6271 file · +209 −61Browse files

Showing the changes to interface/src/scripting/OSCScriptingInterface.cpp (1 of 1 file). Show all

Unified · Split

interface/src/scripting/OSCScriptingInterface.cpp+209−61
@@ -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
23Q_LOGGING_CATEGORY(osc_cat, "osc")26// TODO: Split this out into libshared later
27// This might be handy to reuse in other modules too
28namespace DataHelpers {
29
30template<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>)
33inline 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
45template<typename T>
46inline auto writeUTF8NullTerminated(QByteArray& ba, const T& v);
47
48template<>
49inline auto writeUTF8NullTerminated(QByteArray& ba, const QString& v) {
50 ba.append(v.toUtf8());
51 return ba.append('\x00');
52}
53
54template<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>)
57inline 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
82template<typename T, int N>
83 requires (sizeof(T) == 1 && N > 0)
84inline 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
100inline 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
112template<typename T>
113inline std::optional<T>
114readUTF8NullTerminated(QByteArray& ba, int& offset);
115
116template<>
117inline std::optional<QString>
118readUTF8NullTerminated(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
131template<>
132inline std::optional<QByteArray>
133readUTF8NullTerminated(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
149Q_LOGGING_CATEGORY(osc_cat, "overte.osc")
24 150
25enum OSCTag: char {151enum 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 // address210 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 types226 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 comma242 // +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 prefix445 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 boundary448 // pad to 4-byte boundary