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
1111
1212 #include <QLoggingCategory>
1313 #include <QRegularExpression>
14−#include <QtEndian>
1514 #include <QNetworkDatagram>
1615
1716 #include "ScriptContext.h"
@@ -19,14 +18,144 @@ · 1 unmodified line
1918 #include "ScriptValue.h"
2019 #include "OSCScriptingInterface.h"
2120
21+#include <array>
22+#include <bit>
23+#include <optional>
24+#include <QByteArray>
2225
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")
24150
25151 enum OSCTag: char {
152+ // standard types
26153 Int = 'i',
27154 Float = 'f',
28155 String = 's',
29156 Blob = 'b',
157+
158+ // extension types
30159 False = 'F',
31160 True = 'T',
32161 Null = 'N',
@@ -78,77 +207,108 @@ void OSCScriptingInterface::readPacket() { · 45 unmodified lines
78207 QByteArray argTypes;
79208 QVariantList args;
80209
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;
88218 }
219+ cursor = next4(cursor);
89220
90− if (addr.isEmpty()) { return; }
221+ if (addr == "#bundle") {
222+ qCritical(osc_cat, "Received OSC bundle, which isn't supported");
223+ return;
224+ }
91225
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;
99240 }
100241
101242 // +1 to exclude the comma
102243 for (int arg = 1; arg < argTypes.length(); arg++) {
103244 switch (argTypes[arg]) {
104245 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+
107253 args.append(QVariantMap {
108254 {"type", QString(OSCTag::Int)},
109− {"value", tmp},
255+ {"value", value.value()},
110256 });
111− cursor += 4;
112257 } break;
113258
114259 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+
117267 args.append(QVariantMap {
118268 {"type", QString(OSCTag::Float)},
119− {"value", *reinterpret_cast<float*>(&tmp)},
269+ {"value", value.value()},
120270 });
121− cursor += 4;
122271 } break;
123272
124273 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;
133280 }
134281
282+ cursor = next4(cursor);
283+
135284 args.append(QVariantMap {
136285 {"type", QString(OSCTag::String)},
137− {"value", tmp},
286+ {"value", value.value()},
138287 });
139288 } break;
140289
141290 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+ }
145305
146306 args.append(QVariantMap {
147307 {"type", QString(OSCTag::Blob)},
148− {"value", QByteArray(data.constData() + cursor, len)},
308+ {"value", bytes.value()},
149309 });
150310
151− cursor = next4(cursor + len);
311+ cursor = next4(cursor + len.value());
152312 } break;
153313
154314 case OSCTag::False:
@@ -196,6 +356,12 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 41 unmodified lines
196356 arguments.append(context->argument(i).toVariant());
197357 }
198358
359+
360+ if (address == "#bundle") {
361+ engine->raiseException("OSC bundles are not supported");
362+ return engine->undefinedValue();
363+ }
364+
199365 if (address.length() < 2 || !address.startsWith('/') || address.endsWith('/')) {
200366 engine->raiseException("Address must be at least two characters and start with '/'");
201367 return engine->undefinedValue();
@@ -249,22 +415,12 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 47 unmodified lines
249415 switch (expectedType) {
250416 case QVariant::Int: {
251417 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()));
257419 } break;
258420
259421 case QVariant::Double: {
260422 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()));
268424 } break;
269425
270426 case QVariant::String: {
@@ -277,10 +433,7 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 6 unmodified lines
277433 return engine->undefinedValue();
278434 }
279435
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);
284437 pad4(bodyBytes);
285438 } break;
286439
@@ -289,12 +442,7 @@ ScriptValue OSCScriptingInterface::sendPacket(ScriptContext* context, ScriptEngi · 2 unmodified lines
289442
290443 auto bytesArg = value.toByteArray();
291444
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());
298446 bodyBytes.append(bytesArg);
299447
300448 // pad to 4-byte boundary