whalibmob 5.10.3 → 5.10.5
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/README.md +22 -20
- package/cli.js +7 -7
- package/index.js +4 -0
- package/lib/Client.js +68 -11
- package/lib/argo/ArgoDecoder.js +240 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1449,24 +1449,26 @@ client.on('account_restriction', (r) => {
|
|
|
1449
1449
|
> client-side shortens it; the only thing that helps is not sending more
|
|
1450
1450
|
> unanswered first messages while it is on.
|
|
1451
1451
|
|
|
1452
|
-
> [!
|
|
1453
|
-
> **
|
|
1454
|
-
>
|
|
1455
|
-
>
|
|
1456
|
-
>
|
|
1452
|
+
> [!NOTE]
|
|
1453
|
+
> **This transport answers in one of two encodings**, and the reply says which:
|
|
1454
|
+
> JSON, or Argo — Meta's compact binary encoding for GraphQL. Both are read.
|
|
1455
|
+
> WhatsApp sends Argo with its self-describing flag set, so it carries its own
|
|
1456
|
+
> field names and decodes without the GraphQL schema the query was written
|
|
1457
|
+
> against.
|
|
1457
1458
|
>
|
|
1458
|
-
> Do not add a `format` attribute to the query in the hope of
|
|
1459
|
+
> Do not add a `format` attribute to the query in the hope of steering this — it
|
|
1459
1460
|
> was tried against a live server, which responded by dropping the stanza
|
|
1460
|
-
> entirely
|
|
1461
|
+
> entirely and answering nothing.
|
|
1461
1462
|
>
|
|
1462
|
-
>
|
|
1463
|
-
>
|
|
1464
|
-
>
|
|
1463
|
+
> Some accounts get an answer with no restriction data in it at all — a bare
|
|
1464
|
+
> `false` rather than a result. That throws, with the decoded value on
|
|
1465
|
+
> `err.mexDecoded`, rather than being reported as "not restricted": an
|
|
1466
|
+
> all-clear invented from an answer that never said so would have you send more
|
|
1467
|
+
> messages and lengthen the very restriction you were asking about.
|
|
1465
1468
|
>
|
|
1466
|
-
> The `account_restriction` event
|
|
1467
|
-
> restriction starting and lifting, and those announcements
|
|
1468
|
-
>
|
|
1469
|
-
> told, with the countdown, without ever calling the query.
|
|
1469
|
+
> The `account_restriction` event does not depend on any of this. The server
|
|
1470
|
+
> *announces* a restriction starting and lifting, and those announcements carry
|
|
1471
|
+
> the countdown. Keep a listener attached and you are told either way.
|
|
1470
1472
|
|
|
1471
1473
|
### What Is Automatic vs What You Need to Do
|
|
1472
1474
|
|
|
@@ -3284,19 +3286,19 @@ wa> /restriction --demo
|
|
|
3284
3286
|
`--demo 90` uses ninety seconds instead of the default five hours, which is
|
|
3285
3287
|
short enough to watch it reach zero and stop on its own.
|
|
3286
3288
|
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
+
Some accounts get a reply that decodes cleanly but holds no restriction data.
|
|
3290
|
+
The command says that, rather than reading it as an all-clear:
|
|
3289
3291
|
|
|
3290
3292
|
```sh
|
|
3291
3293
|
wa> /restriction
|
|
3292
3294
|
checking account restriction...
|
|
3293
|
-
error: mex query 23983697327930364: the server answered
|
|
3295
|
+
error: mex query 23983697327930364: the server answered false rather than a result — this query returns no data for this account
|
|
3294
3296
|
|
|
3295
|
-
The
|
|
3296
|
-
|
|
3297
|
+
The reply was read successfully — it simply contains no restriction
|
|
3298
|
+
data. This query returns nothing usable for your account.
|
|
3297
3299
|
|
|
3298
3300
|
You will still be told about a restriction: the server announces one when
|
|
3299
|
-
it starts and when it lifts, and those announcements
|
|
3301
|
+
it starts and when it lifts, and those announcements carry the countdown.
|
|
3300
3302
|
```
|
|
3301
3303
|
|
|
3302
3304
|
An account that is fine says so and returns immediately:
|
package/cli.js
CHANGED
|
@@ -1516,19 +1516,19 @@ async function handleLine(line) {
|
|
|
1516
1516
|
st = await _client.fetchReachoutTimelock();
|
|
1517
1517
|
} catch (e) {
|
|
1518
1518
|
fail(e.message);
|
|
1519
|
-
if (e.
|
|
1519
|
+
if (e.mexDecoded !== undefined) {
|
|
1520
1520
|
out('');
|
|
1521
|
-
out(' The
|
|
1522
|
-
out('
|
|
1521
|
+
out(' The reply was read successfully — it simply contains no restriction');
|
|
1522
|
+
out(' data. This query returns nothing usable for your account.');
|
|
1523
1523
|
out('');
|
|
1524
1524
|
out(' You will still be told about a restriction: the server announces one when');
|
|
1525
|
-
out(' it starts and when it lifts, and those announcements
|
|
1525
|
+
out(' it starts and when it lifts, and those announcements carry the countdown.');
|
|
1526
1526
|
out(' Leave the session connected and watch for:');
|
|
1527
1527
|
out(' ACCOUNT RESTRICTED — <reason>, HH:MM:SS remaining');
|
|
1528
|
+
} else if (e.mexFormat) {
|
|
1528
1529
|
out('');
|
|
1529
|
-
out(' The
|
|
1530
|
-
out('
|
|
1531
|
-
out(' is what makes this format readable without its schema.');
|
|
1530
|
+
out(' The query worked, but the reply is in an encoding that could not be');
|
|
1531
|
+
out(' decoded. Paste the bytes above when reporting this.');
|
|
1532
1532
|
} else {
|
|
1533
1533
|
out('');
|
|
1534
1534
|
out(' If the reply is sketched above, paste that line when reporting this —');
|
package/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const { MessageSender, makeJid, generateMessageId } = require('./lib/messages/Me
|
|
|
14
14
|
const { GroupParticipantResult, GroupJoinRequest } = require('./lib/GroupParticipant');
|
|
15
15
|
const { AppStateStore, COLLECTIONS: APP_STATE_COLLECTIONS } = require('./lib/appstate/AppStateStore');
|
|
16
16
|
const ReachoutTimelock = require('./lib/ReachoutTimelock');
|
|
17
|
+
const { decodeArgo, tryDecodeArgo } = require('./lib/argo/ArgoDecoder');
|
|
17
18
|
const AppStateSync = require('./lib/appstate/AppStateSync');
|
|
18
19
|
const { PATCH_INTEGRITY: LT_HASH } = require('./lib/appstate/LTHash');
|
|
19
20
|
const {
|
|
@@ -113,6 +114,9 @@ module.exports = {
|
|
|
113
114
|
LT_HASH,
|
|
114
115
|
// Account restriction
|
|
115
116
|
ReachoutTimelock,
|
|
117
|
+
// Argo (the binary encoding the GraphQL transport may answer in)
|
|
118
|
+
decodeArgo,
|
|
119
|
+
tryDecodeArgo,
|
|
116
120
|
// Auth utilities
|
|
117
121
|
makeCacheableSignalKeyStore,
|
|
118
122
|
addTransactionCapability,
|
package/lib/Client.js
CHANGED
|
@@ -14,6 +14,7 @@ const { createNewStore, saveStore, loadStore, toSixParts, fromSixParts } = requi
|
|
|
14
14
|
const { BinaryNode } = require('./BinaryNode');
|
|
15
15
|
const { AppStateStore, COLLECTIONS } = require('./appstate/AppStateStore');
|
|
16
16
|
const ReachoutTimelock = require('./ReachoutTimelock');
|
|
17
|
+
const { decodeArgo } = require('./argo/ArgoDecoder');
|
|
17
18
|
const AppStateSync = require('./appstate/AppStateSync');
|
|
18
19
|
const SyncdProto = require('./appstate/SyncdProto');
|
|
19
20
|
const AppMutations = require('./appstate/Mutations');
|
|
@@ -4999,15 +5000,56 @@ class WhalibmobClient extends EventEmitter {
|
|
|
4999
5000
|
return dataPath ? (json.data && json.data[dataPath]) : json.data;
|
|
5000
5001
|
}
|
|
5001
5002
|
|
|
5002
|
-
//
|
|
5003
|
-
//
|
|
5004
|
-
//
|
|
5003
|
+
// Not JSON, so the other encoding this transport uses: Argo, which is what
|
|
5004
|
+
// newer servers answer with. It carries its own field names, so it decodes
|
|
5005
|
+
// without the GraphQL schema the query was written against.
|
|
5005
5006
|
const resultNode = findChildDeep(resp, 'result');
|
|
5006
5007
|
const format = resultNode && resultNode.attrs && resultNode.attrs.format;
|
|
5008
|
+
const raw = resultNode ? getNodeContent(resultNode) : null;
|
|
5009
|
+
|
|
5010
|
+
// Only when the server said so. Guessing that unlabelled bytes are Argo
|
|
5011
|
+
// turns "this reply is not what I expected" into a misleading complaint
|
|
5012
|
+
// about a format that was never claimed.
|
|
5013
|
+
if (raw && raw.length && format && String(format).toLowerCase() === 'argo') {
|
|
5014
|
+
let decoded;
|
|
5015
|
+
try {
|
|
5016
|
+
decoded = decodeArgo(raw);
|
|
5017
|
+
} catch (e) {
|
|
5018
|
+
const err = new Error('mex query ' + queryId + ': the reply is ' +
|
|
5019
|
+
(format ? '"' + format + '"' : 'binary') + ' and could not be decoded — ' +
|
|
5020
|
+
e.message + ' (' + raw.length + ' bytes: ' + raw.toString('hex') + ')');
|
|
5021
|
+
err.mexFormat = format ? String(format) : null;
|
|
5022
|
+
err.mexPayload = raw;
|
|
5023
|
+
throw err;
|
|
5024
|
+
}
|
|
5025
|
+
_whaDbg('[DBG] MEX_ARGO decoded ' + JSON.stringify(decoded));
|
|
5026
|
+
|
|
5027
|
+
if (decoded && typeof decoded === 'object') {
|
|
5028
|
+
if (decoded.errors && decoded.errors.length) {
|
|
5029
|
+
throw new Error('mex query ' + queryId + ': ' +
|
|
5030
|
+
decoded.errors.map(e => (e && e.message) || 'unknown error').join(', '));
|
|
5031
|
+
}
|
|
5032
|
+
if (decoded.data !== undefined) {
|
|
5033
|
+
return dataPath ? (decoded.data && decoded.data[dataPath]) : decoded.data;
|
|
5034
|
+
}
|
|
5035
|
+
return dataPath ? decoded[dataPath] : decoded;
|
|
5036
|
+
}
|
|
5037
|
+
|
|
5038
|
+
// The server answered something that is not a GraphQL result at all —
|
|
5039
|
+
// a bare boolean, most often. There is no data in it to return, and
|
|
5040
|
+
// saying so beats handing back a shape the caller will misread.
|
|
5041
|
+
const err = new Error('mex query ' + queryId + ': the server answered ' +
|
|
5042
|
+
JSON.stringify(decoded) + ' rather than a result — this query returns ' +
|
|
5043
|
+
'no data for this account');
|
|
5044
|
+
err.mexFormat = format ? String(format) : null;
|
|
5045
|
+
err.mexPayload = raw;
|
|
5046
|
+
err.mexDecoded = decoded;
|
|
5047
|
+
throw err;
|
|
5048
|
+
}
|
|
5049
|
+
|
|
5007
5050
|
if (format && String(format).toLowerCase() !== 'json') {
|
|
5008
|
-
const raw = getNodeContent(resultNode);
|
|
5009
5051
|
const err = new Error('mex query ' + queryId + ': the server answered in "' +
|
|
5010
|
-
format + '"
|
|
5052
|
+
format + '", which this library cannot read' +
|
|
5011
5053
|
(raw ? ' (' + raw.length + ' bytes: ' + raw.toString('hex') + ')' : ''));
|
|
5012
5054
|
err.mexFormat = String(format);
|
|
5013
5055
|
err.mexPayload = raw || null;
|
|
@@ -5080,13 +5122,27 @@ class WhalibmobClient extends EventEmitter {
|
|
|
5080
5122
|
}
|
|
5081
5123
|
|
|
5082
5124
|
// Internal: parse newsletter response from w:mex IQ result
|
|
5125
|
+
//
|
|
5126
|
+
// Every newsletter call travels the same transport as the restriction query,
|
|
5127
|
+
// so a server that answers that one in Argo answers these in Argo too. This
|
|
5128
|
+
// used to swallow that and return null, which reads as "no such newsletter"
|
|
5129
|
+
// rather than "this reply is in an encoding nothing here can read" — the
|
|
5130
|
+
// difference between a shrug and something a caller can act on.
|
|
5083
5131
|
_parseNewsletterResponse(resp) {
|
|
5084
5132
|
try {
|
|
5085
|
-
const
|
|
5086
|
-
if (!
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5133
|
+
const json = this._mexJsonFrom(resp);
|
|
5134
|
+
if (!json) {
|
|
5135
|
+
const resultNode = findChildDeep(resp, 'result');
|
|
5136
|
+
const format = resultNode && resultNode.attrs && resultNode.attrs.format;
|
|
5137
|
+
if (format && String(format).toLowerCase() !== 'json') {
|
|
5138
|
+
_whaDbg('[DBG] NEWSLETTER reply format=' + format + ' — cannot decode');
|
|
5139
|
+
const err = new Error('the server answered in "' + format +
|
|
5140
|
+
'" rather than JSON, which this library cannot read');
|
|
5141
|
+
err.mexFormat = String(format);
|
|
5142
|
+
throw err;
|
|
5143
|
+
}
|
|
5144
|
+
return null;
|
|
5145
|
+
}
|
|
5090
5146
|
const nl = json && (json.newsletter || (json.data && json.data.xwa2_newsletter));
|
|
5091
5147
|
if (!nl) return null;
|
|
5092
5148
|
const meta = nl.metadata || nl;
|
|
@@ -5097,7 +5153,8 @@ class WhalibmobClient extends EventEmitter {
|
|
|
5097
5153
|
subscriberCount: nl.subscribers_count || meta.subscriber_count || 0,
|
|
5098
5154
|
creationTime: nl.creation_time || meta.creation_time || 0
|
|
5099
5155
|
};
|
|
5100
|
-
} catch (
|
|
5156
|
+
} catch (err) {
|
|
5157
|
+
if (err && err.mexFormat) throw err;
|
|
5101
5158
|
return null;
|
|
5102
5159
|
}
|
|
5103
5160
|
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Argo — the binary encoding WhatsApp's GraphQL transport answers in.
|
|
4
|
+
//
|
|
5
|
+
// A w:mex reply can come back as JSON or as Argo; the <result> node says which.
|
|
6
|
+
// Argo is normally decoded against the schema of the query that asked, which a
|
|
7
|
+
// client outside Meta does not have. But a message can set a SelfDescribing
|
|
8
|
+
// flag, and then it carries its own field names and types — which is what
|
|
9
|
+
// WhatsApp sends, and what makes this readable at all.
|
|
10
|
+
//
|
|
11
|
+
// Only the self-describing form is implemented. A schema-typed message is
|
|
12
|
+
// refused rather than guessed at.
|
|
13
|
+
//
|
|
14
|
+
// Layout:
|
|
15
|
+
// [header bitset][block][block]…[core]
|
|
16
|
+
// where each block is a length label followed by that many bytes, and the core
|
|
17
|
+
// is the last one. Values in the core are Labels — zigzag ULEB128 integers —
|
|
18
|
+
// that either carry a small value outright or point into a block.
|
|
19
|
+
|
|
20
|
+
// ─── labels ──────────────────────────────────────────────────────────────────
|
|
21
|
+
//
|
|
22
|
+
// A label is one signed integer doing several jobs at once. Non-negative is a
|
|
23
|
+
// length or a boolean; the three values below are markers; anything lower is a
|
|
24
|
+
// backreference to a value already seen.
|
|
25
|
+
const LABEL_NULL = -1;
|
|
26
|
+
const LABEL_ABSENT = -2;
|
|
27
|
+
const LABEL_ERROR = -3;
|
|
28
|
+
// -4 is the first backreference and means "the first value in this block".
|
|
29
|
+
const BACKREF_BASE = -4;
|
|
30
|
+
|
|
31
|
+
// Self-describing type markers, as label values.
|
|
32
|
+
const MARKER = {
|
|
33
|
+
NULL: -1,
|
|
34
|
+
FALSE: 0,
|
|
35
|
+
TRUE: 1,
|
|
36
|
+
OBJECT: 2,
|
|
37
|
+
LIST: 3,
|
|
38
|
+
STRING: 4,
|
|
39
|
+
BYTES: 5,
|
|
40
|
+
INT: 6,
|
|
41
|
+
FLOAT: 7
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Header flag bit positions.
|
|
45
|
+
const FLAG_INLINE_EVERYTHING = 0;
|
|
46
|
+
const FLAG_SELF_DESCRIBING = 1;
|
|
47
|
+
|
|
48
|
+
class Reader {
|
|
49
|
+
constructor(buf) {
|
|
50
|
+
this.buf = buf;
|
|
51
|
+
this.pos = 0;
|
|
52
|
+
}
|
|
53
|
+
get done() { return this.pos >= this.buf.length; }
|
|
54
|
+
byte() {
|
|
55
|
+
if (this.pos >= this.buf.length) throw new Error('argo: ran off the end of the message');
|
|
56
|
+
return this.buf[this.pos++];
|
|
57
|
+
}
|
|
58
|
+
peek() { return this.buf[this.pos]; }
|
|
59
|
+
take(n) {
|
|
60
|
+
if (n < 0 || this.pos + n > this.buf.length) {
|
|
61
|
+
throw new Error('argo: asked for ' + n + ' bytes with ' +
|
|
62
|
+
(this.buf.length - this.pos) + ' left');
|
|
63
|
+
}
|
|
64
|
+
const out = this.buf.slice(this.pos, this.pos + n);
|
|
65
|
+
this.pos += n;
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
// ULEB128, then zigzag back to signed.
|
|
69
|
+
label() {
|
|
70
|
+
let value = 0n, shift = 0n;
|
|
71
|
+
for (;;) {
|
|
72
|
+
const b = this.byte();
|
|
73
|
+
value |= BigInt(b & 0x7f) << shift;
|
|
74
|
+
if (!(b & 0x80)) break;
|
|
75
|
+
shift += 7n;
|
|
76
|
+
if (shift > 63n) throw new Error('argo: label is too long to be a number');
|
|
77
|
+
}
|
|
78
|
+
const signed = (value >> 1n) ^ -(value & 1n);
|
|
79
|
+
return Number(signed);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function readHeader(reader) {
|
|
84
|
+
let bits = 0, shift = 0, more = true;
|
|
85
|
+
while (more) {
|
|
86
|
+
const b = reader.byte();
|
|
87
|
+
bits |= (b >> 1) << shift;
|
|
88
|
+
shift += 7;
|
|
89
|
+
more = (b & 1) === 1;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
bits,
|
|
93
|
+
selfDescribing: !!(bits >> FLAG_SELF_DESCRIBING & 1),
|
|
94
|
+
inlineEverything: !!(bits >> FLAG_INLINE_EVERYTHING & 1)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A block holds the bulky values — strings, byte arrays, numbers — with the
|
|
99
|
+
// core referring to them in order. Blocks are handed out in the order their
|
|
100
|
+
// types are first needed, which is why they are taken from a queue rather than
|
|
101
|
+
// looked up by name.
|
|
102
|
+
class BlockQueue {
|
|
103
|
+
constructor(blocks, core, inlineEverything) {
|
|
104
|
+
this.blocks = blocks;
|
|
105
|
+
this.core = core;
|
|
106
|
+
this.inline = inlineEverything;
|
|
107
|
+
this.next = 0;
|
|
108
|
+
this.byKey = new Map();
|
|
109
|
+
}
|
|
110
|
+
for(key) {
|
|
111
|
+
if (this.inline) return { reader: this.core, seen: [] };
|
|
112
|
+
if (!this.byKey.has(key)) {
|
|
113
|
+
const buf = this.blocks[this.next++];
|
|
114
|
+
if (buf === undefined) throw new Error('argo: message is missing a block for ' + key);
|
|
115
|
+
this.byKey.set(key, { reader: new Reader(buf), seen: [] });
|
|
116
|
+
}
|
|
117
|
+
return this.byKey.get(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function decodeArgo(bytes) {
|
|
122
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
123
|
+
const reader = new Reader(buf);
|
|
124
|
+
const header = readHeader(reader);
|
|
125
|
+
|
|
126
|
+
if (!header.selfDescribing) {
|
|
127
|
+
const err = new Error('argo: this message is encoded against a GraphQL schema ' +
|
|
128
|
+
'rather than describing itself, so it cannot be read without that schema');
|
|
129
|
+
err.needsSchema = true;
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const blocks = [];
|
|
134
|
+
if (header.inlineEverything) {
|
|
135
|
+
blocks.push(reader.take(buf.length - reader.pos));
|
|
136
|
+
} else {
|
|
137
|
+
while (!reader.done) {
|
|
138
|
+
const len = reader.label();
|
|
139
|
+
if (len < 0) throw new Error('argo: block length cannot be ' + len);
|
|
140
|
+
blocks.push(reader.take(len));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!blocks.length) throw new Error('argo: message has no core');
|
|
144
|
+
|
|
145
|
+
// The core is the last block, and every other block feeds it.
|
|
146
|
+
const core = new Reader(blocks[blocks.length - 1]);
|
|
147
|
+
const queue = new BlockQueue(blocks.slice(0, -1), core, header.inlineEverything);
|
|
148
|
+
|
|
149
|
+
return readValue(core, queue);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// One length-prefixed value out of a block, with backreferences resolved.
|
|
153
|
+
//
|
|
154
|
+
// The same string written twice is written once and pointed at the second time,
|
|
155
|
+
// which is most of why this format is small. A backreference that points at
|
|
156
|
+
// nothing means the message and our reading of it have diverged.
|
|
157
|
+
function readBlockValue(core, queue, key, convert) {
|
|
158
|
+
const block = queue.for(key);
|
|
159
|
+
const label = core.label();
|
|
160
|
+
if (label <= BACKREF_BASE) {
|
|
161
|
+
const idx = -label + BACKREF_BASE;
|
|
162
|
+
if (idx < 0 || idx >= block.seen.length) {
|
|
163
|
+
throw new Error('argo: backreference ' + label + ' points outside ' + key);
|
|
164
|
+
}
|
|
165
|
+
return block.seen[idx];
|
|
166
|
+
}
|
|
167
|
+
if (label < 0) throw new Error('argo: ' + key + ' cannot have label ' + label);
|
|
168
|
+
const value = convert(block.reader.take(label));
|
|
169
|
+
block.seen.push(value);
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readValue(core, queue) {
|
|
174
|
+
const marker = core.label();
|
|
175
|
+
|
|
176
|
+
switch (marker) {
|
|
177
|
+
case MARKER.NULL: return null;
|
|
178
|
+
case MARKER.FALSE: return false;
|
|
179
|
+
case MARKER.TRUE: return true;
|
|
180
|
+
|
|
181
|
+
case MARKER.OBJECT: {
|
|
182
|
+
const count = core.label();
|
|
183
|
+
if (count < 0) throw new Error('argo: object cannot have ' + count + ' fields');
|
|
184
|
+
const out = {};
|
|
185
|
+
for (let i = 0; i < count; i++) {
|
|
186
|
+
const name = readBlockValue(core, queue, 'String', b => b.toString('utf8'));
|
|
187
|
+
out[name] = readValue(core, queue);
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
case MARKER.LIST: {
|
|
193
|
+
const count = core.label();
|
|
194
|
+
if (count < 0) throw new Error('argo: list cannot have ' + count + ' items');
|
|
195
|
+
const out = [];
|
|
196
|
+
for (let i = 0; i < count; i++) out.push(readValue(core, queue));
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case MARKER.STRING: return readBlockValue(core, queue, 'String', b => b.toString('utf8'));
|
|
201
|
+
case MARKER.BYTES: return readBlockValue(core, queue, 'Bytes', b => Buffer.from(b));
|
|
202
|
+
|
|
203
|
+
// An integer lives entirely in its block — the core spends nothing on it.
|
|
204
|
+
case MARKER.INT: {
|
|
205
|
+
const block = queue.for('Varint');
|
|
206
|
+
return block.reader.label();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
case MARKER.FLOAT: {
|
|
210
|
+
const block = queue.for('Float');
|
|
211
|
+
return block.reader.take(8).readDoubleLE(0);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
default:
|
|
215
|
+
if (marker === LABEL_ABSENT) return undefined;
|
|
216
|
+
if (marker === LABEL_ERROR) return readValue(core, queue);
|
|
217
|
+
throw new Error('argo: unknown type marker ' + marker);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Decode an Argo message, or return null when it is not one this can read.
|
|
223
|
+
*
|
|
224
|
+
* Never throws: a caller reaching for this already has a reply it could not
|
|
225
|
+
* parse, and a second failure on top of that is not news. `decodeArgo` is there
|
|
226
|
+
* for when the reason matters.
|
|
227
|
+
*/
|
|
228
|
+
function tryDecodeArgo(bytes) {
|
|
229
|
+
try { return decodeArgo(bytes); }
|
|
230
|
+
catch (_) { return null; }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
decodeArgo,
|
|
235
|
+
tryDecodeArgo,
|
|
236
|
+
MARKER,
|
|
237
|
+
LABEL_NULL,
|
|
238
|
+
LABEL_ABSENT,
|
|
239
|
+
LABEL_ERROR
|
|
240
|
+
};
|