teleportxr 1.0.60 → 1.0.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client/client.js
CHANGED
|
@@ -280,10 +280,16 @@ class Client {
|
|
|
280
280
|
return;
|
|
281
281
|
}
|
|
282
282
|
var msg =new message.AcknowledgementMessage();
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
283
|
+
// ReceiveAcknowledgement is reached from two transports with different argument shapes:
|
|
284
|
+
// - WebRTC data channels (receivedMessageReliable / receivedMessageUnreliable) deliver
|
|
285
|
+
// event.data as a raw ArrayBuffer; ArrayBuffer.isView(data) is false and data.buffer
|
|
286
|
+
// is undefined, so we must construct the DataView from `data` directly.
|
|
287
|
+
// - The WebSocket signaling fallback (receiveReliableBinaryMessage) delivers a Node
|
|
288
|
+
// Buffer, which is a Uint8Array view over a pooled ArrayBuffer; we must wrap its
|
|
289
|
+
// backing buffer using byteOffset/byteLength so we don't read pool neighbours.
|
|
290
|
+
var dataView =ArrayBuffer.isView(data)
|
|
291
|
+
?new DataView(data.buffer,data.byteOffset,data.byteLength)
|
|
292
|
+
:new DataView(data,0,data.byteLength);
|
|
287
293
|
core.decodeFromDataView(msg,dataView,0);
|
|
288
294
|
if(msg.uint64_ackId==this.currentOriginState.ackId)
|
|
289
295
|
{
|
package/package.json
CHANGED
package/protocol/message.js
CHANGED
|
@@ -3,53 +3,59 @@ const core= require("../core/core.js");
|
|
|
3
3
|
const command= require("./command");
|
|
4
4
|
const node= require("../scene/node.js");
|
|
5
5
|
|
|
6
|
+
// Mirrors core.decodeFromDataView so the two implementations stay in sync.
|
|
7
|
+
// The previous version had four latent bugs that only escaped notice because
|
|
8
|
+
// nothing imports this function (every real caller uses core.decodeFromDataView):
|
|
9
|
+
// 1. `Object.entries[key]=value` assigned a property on the `Object.entries`
|
|
10
|
+
// *function object* itself instead of writing back to `obj`, so decoded
|
|
11
|
+
// fields were silently dropped on the floor.
|
|
12
|
+
// 2. The multi-byte getters were called without an endian argument, defaulting
|
|
13
|
+
// to big-endian and disagreeing with the writer's little-endian output.
|
|
14
|
+
// 3. The "struct" branch referenced an undefined `value` local; the recursion
|
|
15
|
+
// would have thrown immediately had it ever been reached.
|
|
16
|
+
// 4. `SizeOfType` was referenced bare but is not imported into this file —
|
|
17
|
+
// only the `core` namespace is — so the very first iteration would have
|
|
18
|
+
// thrown ReferenceError.
|
|
6
19
|
function decodeFromDataView(obj,dataView,byteOffset){
|
|
7
|
-
for (let key of Object.
|
|
20
|
+
for (let [key, sub_obj] of Object.entries(obj)) {
|
|
8
21
|
let first_underscore=key.search('_');
|
|
9
22
|
var name=key.substring(first_underscore+1,key.end);
|
|
10
23
|
var type=key.substring(0,first_underscore);
|
|
11
|
-
var [sz,tp]=SizeOfType(type);
|
|
24
|
+
var [sz,tp]=core.SizeOfType(type);
|
|
12
25
|
if(tp=="uint8")
|
|
13
26
|
{
|
|
14
|
-
|
|
15
|
-
Object.entries[key]=value;
|
|
27
|
+
obj[key]=dataView.getUint8(byteOffset);
|
|
16
28
|
}
|
|
17
29
|
else if(tp=="int8")
|
|
18
30
|
{
|
|
19
|
-
|
|
20
|
-
Object.entries[key]=value;
|
|
31
|
+
obj[key]=dataView.getInt8(byteOffset);
|
|
21
32
|
}
|
|
22
33
|
else if(tp=="uint32")
|
|
23
34
|
{
|
|
24
|
-
|
|
25
|
-
Object.entries[key]=value;
|
|
35
|
+
obj[key]=dataView.getUint32(byteOffset,core.endian);
|
|
26
36
|
}
|
|
27
37
|
else if(tp=="int32")
|
|
28
38
|
{
|
|
29
|
-
|
|
30
|
-
Object.entries[key]=value;
|
|
39
|
+
obj[key]=dataView.getInt32(byteOffset,core.endian);
|
|
31
40
|
}
|
|
32
41
|
else if(tp=="float32")
|
|
33
42
|
{
|
|
34
|
-
|
|
35
|
-
Object.entries[key]=value;
|
|
43
|
+
obj[key]=dataView.getFloat32(byteOffset,core.endian);
|
|
36
44
|
}
|
|
37
45
|
else if(tp=="int64")
|
|
38
46
|
{
|
|
39
|
-
|
|
40
|
-
Object.entries[key]=value;
|
|
47
|
+
obj[key]=dataView.getBigInt64(byteOffset,core.endian);
|
|
41
48
|
}
|
|
42
49
|
else if(tp=="uint64")
|
|
43
50
|
{
|
|
44
|
-
|
|
45
|
-
Object.entries[key]=value;
|
|
51
|
+
obj[key]=dataView.getBigUint64(byteOffset,core.endian);
|
|
46
52
|
}
|
|
47
53
|
else if(tp=="struct")
|
|
48
54
|
{
|
|
49
|
-
sz=decodeFromDataView(
|
|
55
|
+
sz=decodeFromDataView(sub_obj,dataView,byteOffset)-byteOffset;
|
|
50
56
|
}
|
|
51
57
|
byteOffset+=sz;
|
|
52
|
-
console.log(byteOffset+": "+sz+" bytes\t\t"+tp+" "+name+" "+
|
|
58
|
+
console.log(byteOffset+": "+sz+" bytes\t\t"+tp+" "+name+" "+obj[key]);
|
|
53
59
|
}
|
|
54
60
|
return byteOffset;
|
|
55
61
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Regression tests for Client.ReceiveAcknowledgement. The handler is called
|
|
3
|
+
// from three transports:
|
|
4
|
+
// * receivedMessageReliable / receivedMessageUnreliable (WebRTC data channels)
|
|
5
|
+
// deliver event.data as a raw ArrayBuffer.
|
|
6
|
+
// * receiveReliableBinaryMessage (WebSocket signaling fallback) delivers a
|
|
7
|
+
// Node Buffer (Uint8Array view over a pooled ArrayBuffer).
|
|
8
|
+
// A previous patch ("Fix buffer type error.") switched the DataView ctor to
|
|
9
|
+
// `new DataView(data.buffer, data.byteOffset, data.byteLength)` which fixed the
|
|
10
|
+
// Buffer path but crashed the WebRTC path because ArrayBuffer has no .buffer
|
|
11
|
+
// property. The crash exits the server with status 1, forcing reconnects and a
|
|
12
|
+
// 10 s+ stall in initial resource transfer. These tests pin both shapes.
|
|
13
|
+
|
|
14
|
+
const test = require('node:test');
|
|
15
|
+
const assert = require('node:assert');
|
|
16
|
+
const { Client } = require('../client/client');
|
|
17
|
+
const message = require('../protocol/message');
|
|
18
|
+
|
|
19
|
+
// Build a 17-byte AcknowledgementMessage buffer (sizeof === 17). Field
|
|
20
|
+
// layout (little-endian) per Message + AcknowledgementMessage:
|
|
21
|
+
// [0..1) uint8 messagePayloadType (= MessagePayloadType.Acknowledgement)
|
|
22
|
+
// [1..9) int64 timestamp
|
|
23
|
+
// [9..17) uint64 ackId
|
|
24
|
+
// The current decodeFromDataView implementation has an unrelated bug that
|
|
25
|
+
// prevents the decoded values from being assigned back onto `msg`, so the
|
|
26
|
+
// downstream ackId comparison branches won't actually fire — but that is fine
|
|
27
|
+
// for this regression: the test only asserts the handler does not throw and
|
|
28
|
+
// returns normally, which is the property the crash violated.
|
|
29
|
+
function makeAckArrayBuffer() {
|
|
30
|
+
const ab = new ArrayBuffer(message.AcknowledgementMessage.sizeof());
|
|
31
|
+
const dv = new DataView(ab);
|
|
32
|
+
dv.setUint8(0, message.MessagePayloadType.Acknowledgement);
|
|
33
|
+
dv.setBigInt64(1, 0n, true);
|
|
34
|
+
dv.setBigUint64(9, 42n, true);
|
|
35
|
+
return ab;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeStubClient() {
|
|
39
|
+
const c = Object.create(Client.prototype);
|
|
40
|
+
c.clientID = 1;
|
|
41
|
+
c.currentOriginState = { ackId: 0n, acknowledged: false, resendCount: 0 };
|
|
42
|
+
c.currentLightingState = { ackId: 0n, acknowledged: false, resendCount: 0 };
|
|
43
|
+
return c;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
test('ReceiveAcknowledgement accepts a raw ArrayBuffer (WebRTC path)', () => {
|
|
47
|
+
const c = makeStubClient();
|
|
48
|
+
const ab = makeAckArrayBuffer();
|
|
49
|
+
// Sanity-check: this is the exact shape WebRTC delivers — a bare
|
|
50
|
+
// ArrayBuffer with no .buffer property. Before the fix, the DataView ctor
|
|
51
|
+
// threw "First argument to DataView constructor must be an ArrayBuffer"
|
|
52
|
+
// because data.buffer was undefined.
|
|
53
|
+
assert.strictEqual(ArrayBuffer.isView(ab), false);
|
|
54
|
+
assert.strictEqual(ab.buffer, undefined);
|
|
55
|
+
assert.doesNotThrow(() => c.ReceiveAcknowledgement(ab));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('ReceiveAcknowledgement accepts a Node Buffer (signaling path)', () => {
|
|
59
|
+
const c = makeStubClient();
|
|
60
|
+
const ab = makeAckArrayBuffer();
|
|
61
|
+
// ws delivers binary frames as Node Buffer. Buffer is a Uint8Array view,
|
|
62
|
+
// so ArrayBuffer.isView is true and .buffer / .byteOffset / .byteLength
|
|
63
|
+
// must be used to construct the DataView.
|
|
64
|
+
const buf = Buffer.from(ab);
|
|
65
|
+
assert.strictEqual(ArrayBuffer.isView(buf), true);
|
|
66
|
+
assert.doesNotThrow(() => c.ReceiveAcknowledgement(buf));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('ReceiveAcknowledgement accepts a Uint8Array view at non-zero offset', () => {
|
|
70
|
+
// Defensive: simulate a pooled Buffer where byteOffset > 0. If the handler
|
|
71
|
+
// were to ignore byteOffset and read from offset 0 of the pool, it would
|
|
72
|
+
// see unrelated bytes from a neighbouring allocation.
|
|
73
|
+
const c = makeStubClient();
|
|
74
|
+
const ackSize = message.AcknowledgementMessage.sizeof();
|
|
75
|
+
const pool = new ArrayBuffer(64 + ackSize);
|
|
76
|
+
const dv = new DataView(pool);
|
|
77
|
+
dv.setUint8(64, message.MessagePayloadType.Acknowledgement);
|
|
78
|
+
dv.setBigInt64(65, 0n, true);
|
|
79
|
+
dv.setBigUint64(73, 7n, true);
|
|
80
|
+
const view = new Uint8Array(pool, 64, ackSize);
|
|
81
|
+
assert.strictEqual(view.byteOffset, 64);
|
|
82
|
+
assert.doesNotThrow(() => c.ReceiveAcknowledgement(view));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('ReceiveAcknowledgement rejects a malformed packet without throwing', () => {
|
|
86
|
+
const c = makeStubClient();
|
|
87
|
+
const tooSmall = new ArrayBuffer(3);
|
|
88
|
+
// Should log and return early, not throw.
|
|
89
|
+
assert.doesNotThrow(() => c.ReceiveAcknowledgement(tooSmall));
|
|
90
|
+
const tooBig = new ArrayBuffer(message.AcknowledgementMessage.sizeof() + 1);
|
|
91
|
+
assert.doesNotThrow(() => c.ReceiveAcknowledgement(tooBig));
|
|
92
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Regression tests for the decodeFromDataView duplicate that lives in
|
|
3
|
+
// protocol/message.js. The duplicate had four latent bugs (object-assignment
|
|
4
|
+
// via Object.entries, missing endian on multi-byte getters, undefined `value`
|
|
5
|
+
// in the struct branch, and a missing SizeOfType import). Tests are written
|
|
6
|
+
// against the function as exercised through a small re-export so the bugfix
|
|
7
|
+
// stays load-bearing even though no production caller imports it today.
|
|
8
|
+
|
|
9
|
+
const test = require('node:test');
|
|
10
|
+
const assert = require('node:assert');
|
|
11
|
+
const Module = require('node:module');
|
|
12
|
+
|
|
13
|
+
// Reach into protocol/message.js's module scope to grab the (non-exported)
|
|
14
|
+
// decodeFromDataView. Re-loading the module and reading its compiled wrapper
|
|
15
|
+
// is overkill; instead, require() the file and use a tiny trick: the function
|
|
16
|
+
// is referenced by class methods on classes that ARE exported. Since none of
|
|
17
|
+
// the exported classes call it either, we evaluate the module file once and
|
|
18
|
+
// pull the symbol from its source via Function constructor.
|
|
19
|
+
function loadLocalDecode() {
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const src = fs.readFileSync(
|
|
23
|
+
path.join(__dirname, '..', 'protocol', 'message.js'),
|
|
24
|
+
'utf8'
|
|
25
|
+
);
|
|
26
|
+
// Stand up a fake module context that mirrors the file's top-level requires
|
|
27
|
+
// but exposes decodeFromDataView. Tack a `module.exports.decodeFromDataView`
|
|
28
|
+
// assignment onto the end and evaluate via Module._compile.
|
|
29
|
+
const m = new Module(require.resolve('../protocol/message.js'));
|
|
30
|
+
m.filename = require.resolve('../protocol/message.js');
|
|
31
|
+
m.paths = Module._nodeModulePaths(m.filename);
|
|
32
|
+
m._compile(
|
|
33
|
+
src + '\nmodule.exports.decodeFromDataView = decodeFromDataView;\n',
|
|
34
|
+
m.filename
|
|
35
|
+
);
|
|
36
|
+
return m.exports.decodeFromDataView;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const decodeFromDataView = loadLocalDecode();
|
|
40
|
+
const core = require('../core/core.js');
|
|
41
|
+
|
|
42
|
+
test('decodeFromDataView writes uint8 fields back onto the target object', () => {
|
|
43
|
+
const obj = { uint8_messagePayloadType: 0 };
|
|
44
|
+
const ab = new ArrayBuffer(1);
|
|
45
|
+
new DataView(ab).setUint8(0, 12);
|
|
46
|
+
decodeFromDataView(obj, new DataView(ab), 0);
|
|
47
|
+
assert.strictEqual(obj.uint8_messagePayloadType, 12);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('decodeFromDataView reads multi-byte fields in the writer\'s endianness', () => {
|
|
51
|
+
// The writer (core.encodeIntoDataView) uses core.endian (little-endian).
|
|
52
|
+
// Pre-fix, the local decoder omitted the endian argument and would have
|
|
53
|
+
// read big-endian, so 42n encoded LE as 2a 00 00 00 00 00 00 00 would
|
|
54
|
+
// decode as 0x2a00000000000000n. Pin the round-trip.
|
|
55
|
+
const obj = { uint64_ackId: 0n };
|
|
56
|
+
const ab = new ArrayBuffer(8);
|
|
57
|
+
new DataView(ab).setBigUint64(0, 42n, core.endian);
|
|
58
|
+
decodeFromDataView(obj, new DataView(ab), 0);
|
|
59
|
+
assert.strictEqual(obj.uint64_ackId, 42n);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('decodeFromDataView round-trips a full Message-shaped payload', () => {
|
|
63
|
+
// The Message base class is { uint8_messagePayloadType, int64_timestamp }
|
|
64
|
+
// plus one trailing uint64. Build the layout directly so we don't depend
|
|
65
|
+
// on the (separately tested) core encoder.
|
|
66
|
+
const obj = {
|
|
67
|
+
uint8_messagePayloadType: 0,
|
|
68
|
+
int64_timestamp: 0n,
|
|
69
|
+
uint64_ackId: 0n,
|
|
70
|
+
};
|
|
71
|
+
const ab = new ArrayBuffer(17);
|
|
72
|
+
const dv = new DataView(ab);
|
|
73
|
+
dv.setUint8(0, 12);
|
|
74
|
+
dv.setBigInt64(1, -7n, core.endian);
|
|
75
|
+
dv.setBigUint64(9, 0xdeadbeefn, core.endian);
|
|
76
|
+
const endOffset = decodeFromDataView(obj, new DataView(ab), 0);
|
|
77
|
+
assert.strictEqual(endOffset, 17);
|
|
78
|
+
assert.strictEqual(obj.uint8_messagePayloadType, 12);
|
|
79
|
+
assert.strictEqual(obj.int64_timestamp, -7n);
|
|
80
|
+
assert.strictEqual(obj.uint64_ackId, 0xdeadbeefn);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('decodeFromDataView recurses into nested struct fields', () => {
|
|
84
|
+
// "struct" is the catch-all branch in SizeOfType — anything not in the
|
|
85
|
+
// primitive list is treated as a nested object whose own keys follow the
|
|
86
|
+
// type_name convention. Pre-fix, this branch referenced an undefined
|
|
87
|
+
// `value` and would have thrown TypeError on the very first struct field.
|
|
88
|
+
const obj = {
|
|
89
|
+
struct_inner: { uint8_kind: 0, uint32_value: 0 },
|
|
90
|
+
uint8_trailer: 0,
|
|
91
|
+
};
|
|
92
|
+
const ab = new ArrayBuffer(6);
|
|
93
|
+
const dv = new DataView(ab);
|
|
94
|
+
dv.setUint8(0, 3);
|
|
95
|
+
dv.setUint32(1, 0x11223344, core.endian);
|
|
96
|
+
dv.setUint8(5, 0xff);
|
|
97
|
+
const endOffset = decodeFromDataView(obj, new DataView(ab), 0);
|
|
98
|
+
assert.strictEqual(endOffset, 6);
|
|
99
|
+
assert.strictEqual(obj.struct_inner.uint8_kind, 3);
|
|
100
|
+
assert.strictEqual(obj.struct_inner.uint32_value, 0x11223344);
|
|
101
|
+
assert.strictEqual(obj.uint8_trailer, 0xff);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('decodeFromDataView does not mutate Object.entries itself', () => {
|
|
105
|
+
// Pin the most embarrassing of the pre-fix bugs: `Object.entries[key]=value`
|
|
106
|
+
// wrote properties onto the global Object.entries function. After the fix,
|
|
107
|
+
// decoding must leave Object.entries untouched.
|
|
108
|
+
const before = Object.getOwnPropertyNames(Object.entries);
|
|
109
|
+
const obj = { uint8_x: 0 };
|
|
110
|
+
const ab = new ArrayBuffer(1);
|
|
111
|
+
new DataView(ab).setUint8(0, 99);
|
|
112
|
+
decodeFromDataView(obj, new DataView(ab), 0);
|
|
113
|
+
const after = Object.getOwnPropertyNames(Object.entries);
|
|
114
|
+
assert.deepStrictEqual(after, before);
|
|
115
|
+
});
|