1 | //
|
2 | //
|
3 | //
|
4 |
|
5 | /*
|
6 |
|
7 | The AMQP 0-9-1 is a mess when it comes to the types that can be
|
8 | encoded on the wire.
|
9 |
|
10 | There are four encoding schemes, and three overlapping sets of types:
|
11 | frames, methods, (field-)tables, and properties.
|
12 |
|
13 | Each *frame type* has a set layout in which values of given types are
|
14 | concatenated along with sections of "raw binary" data.
|
15 |
|
16 | In frames there are `shortstr`s, that is length-prefixed strings of
|
17 | UTF8 chars, 8 bit unsigned integers (called `octet`), unsigned 16 bit
|
18 | integers (called `short` or `short-uint`), unsigned 32 bit integers
|
19 | (called `long` or `long-uint`), unsigned 64 bit integers (called
|
20 | `longlong` or `longlong-uint`), and flags (called `bit`).
|
21 |
|
22 | Methods are encoded as a frame giving a method ID and a sequence of
|
23 | arguments of known types. The encoded method argument values are
|
24 | concatenated (with some fun complications around "packing" consecutive
|
25 | bit values into bytes).
|
26 |
|
27 | Along with the types given in frames, method arguments may be long
|
28 | byte strings (`longstr`, not required to be UTF8) or 64 bit unsigned
|
29 | integers to be interpreted as timestamps (yeah I don't know why
|
30 | either), or arbitrary sets of key-value pairs (called `field-table`).
|
31 |
|
32 | Inside a field table the keys are `shortstr` and the values are
|
33 | prefixed with a byte tag giving the type. The types are any of the
|
34 | above except for bits (which are replaced by byte-wide `bool`), along
|
35 | with a NULL value `void`, a special fixed-precision number encoding
|
36 | (`decimal`), IEEE754 `float`s and `double`s, signed integers,
|
37 | `field-array` (a sequence of tagged values), and nested field-tables.
|
38 |
|
39 | RabbitMQ and QPid use a subset of the field-table types, and different
|
40 | value tags, established before the AMQP 0-9-1 specification was
|
41 | published. So far as I know, no-one uses the types and tags as
|
42 | published. http://www.rabbitmq.com/amqp-0-9-1-errata.html gives the
|
43 | list of field-table types.
|
44 |
|
45 | Lastly, there are (sets of) properties, only one of which is given in
|
46 | AMQP 0-9-1: `BasicProperties`. These are almost the same as methods,
|
47 | except that they appear in content header frames, which include a
|
48 | content size, and they carry a set of flags indicating which
|
49 | properties are present. This scheme can save ones of bytes per message
|
50 | (messages which take a minimum of three frames each to send).
|
51 |
|
52 | */
|
53 |
|
54 |
|
55 | require('buffer-more-ints');
|
56 |
|
57 | // JavaScript uses only doubles so what I'm testing for is whether
|
58 | // it's *better* to encode a number as a float or double. This really
|
59 | // just amounts to testing whether there's a fractional part to the
|
60 | // number, except that see below. NB I don't use bitwise operations to
|
61 | // do this 'efficiently' -- it would mask the number to 32 bits.
|
62 | //
|
63 | // At 2^50, doubles don't have sufficient precision to distinguish
|
64 | // between floating point and integer numbers (`Math.pow(2, 50) + 0.1
|
65 | // === Math.pow(2, 50)` (and, above 2^53, doubles cannot represent all
|
66 | // integers (`Math.pow(2, 53) + 1 === Math.pow(2, 53)`)). Hence
|
67 | // anything with a magnitude at or above 2^50 may as well be encoded
|
68 | // as a 64-bit integer. Except that only signed integers are supported
|
69 | // by RabbitMQ, so anything above 2^63 - 1 must be a double.
|
70 | function isFloatingPoint(n) {
|
71 | return n >= 0x8000000000000000 ||
|
72 | (Math.abs(n) < 0x4000000000000
|
73 | && Math.floor(n) !== n);
|
74 | }
|
75 |
|
76 | function encodeTable(buffer, val, offset) {
|
77 | var start = offset;
|
78 | offset += 4; // leave room for the table length
|
79 | for (var key in val) {
|
80 | if (val.hasOwnProperty(key) && val[key] !== undefined) {
|
81 | var len = Buffer.byteLength(key);
|
82 | buffer.writeUInt8(len, offset); offset++;
|
83 | buffer.write(key, offset, 'utf8'); offset += len;
|
84 | offset += encodeFieldValue(buffer, val[key], offset);
|
85 | }
|
86 | }
|
87 | var size = offset - start;
|
88 | buffer.writeUInt32BE(size - 4, start);
|
89 | return size;
|
90 | }
|
91 |
|
92 | function encodeArray(buffer, val, offset) {
|
93 | var start = offset;
|
94 | offset += 4;
|
95 | for (var i=0, num=val.length; i < num; i++) {
|
96 | offset += encodeFieldValue(buffer, val[i], offset);
|
97 | }
|
98 | var size = offset - start;
|
99 | buffer.writeUInt32BE(size - 4, start);
|
100 | return size;
|
101 | }
|
102 |
|
103 | function encodeFieldValue(buffer, value, offset) {
|
104 | var start = offset;
|
105 | var type = typeof value, val = value;
|
106 | // A trapdoor for specifying a type, e.g., timestamp
|
107 | if (value && type === 'object' && value.hasOwnProperty('!')) {
|
108 | val = value.value;
|
109 | type = value['!'];
|
110 | }
|
111 |
|
112 | function tag(t) { buffer.write(t, offset); offset++; }
|
113 |
|
114 | switch (type) {
|
115 | case 'string': // no shortstr in field tables
|
116 | var len = Buffer.byteLength(val, 'utf8');
|
117 | tag('S');
|
118 | buffer.writeUInt32BE(len, offset); offset += 4;
|
119 | buffer.write(val, offset, 'utf8'); offset += len;
|
120 | break;
|
121 | case 'object':
|
122 | if (val === null) {
|
123 | tag('V');
|
124 | }
|
125 | else if (Array.isArray(val)) {
|
126 | tag('A');
|
127 | offset += encodeArray(buffer, val, offset);
|
128 | }
|
129 | else if (Buffer.isBuffer(val)) {
|
130 | tag('x');
|
131 | buffer.writeUInt32BE(val.length, offset); offset += 4;
|
132 | val.copy(buffer, offset); offset += val.length;
|
133 | }
|
134 | else {
|
135 | tag('F');
|
136 | offset += encodeTable(buffer, val, offset);
|
137 | }
|
138 | break;
|
139 | case 'boolean':
|
140 | tag('t');
|
141 | buffer.writeUInt8((val) ? 1 : 0, offset); offset++;
|
142 | break;
|
143 | case 'number':
|
144 | // Making assumptions about the kind of number (floating point
|
145 | // v integer, signed, unsigned, size) desired is dangerous in
|
146 | // general; however, in practice RabbitMQ uses only
|
147 | // longstrings and unsigned integers in its arguments, and
|
148 | // other clients generally conflate number types anyway. So
|
149 | // the only distinction we care about is floating point vs
|
150 | // integers, preferring integers since those can be promoted
|
151 | // if necessary. If floating point is required, we may as well
|
152 | // use double precision.
|
153 | if (isFloatingPoint(val)) {
|
154 | tag('d');
|
155 | buffer.writeDoubleBE(val, offset);
|
156 | offset += 8;
|
157 | }
|
158 | else { // only signed values are used in tables by RabbitMQ,
|
159 | // except for 'byte's which are only unsigned. (The
|
160 | // errata on the RabbitMQ website is wrong on this --
|
161 | // see rabbit_binary_generator.erl)
|
162 | if (val < 256 && val >= 0) {
|
163 | tag('b');
|
164 | buffer.writeUInt8(val, offset); offset++;
|
165 | }
|
166 | else if (val >= -0x8000 && val < 0x8000) { // short
|
167 | tag('s');
|
168 | buffer.writeInt16BE(val, offset); offset += 2;
|
169 | }
|
170 | else if (val >= -0x80000000 && val < 0x80000000) { // int
|
171 | tag('I');
|
172 | buffer.writeInt32BE(val, offset); offset += 4;
|
173 | }
|
174 | else { // long
|
175 | tag('l');
|
176 | buffer.writeInt64BE(val, offset); offset += 8;
|
177 | }
|
178 | }
|
179 | break;
|
180 | // Now for exotic types, those can only be denoted by using
|
181 | // `{'!': type, value: val}
|
182 | case 'timestamp':
|
183 | tag('T');
|
184 | buffer.writeUInt64BE(val, offset); offset += 8;
|
185 | break;
|
186 | case 'float':
|
187 | tag('f');
|
188 | buffer.writeFloatBE(val, offset); offset += 4;
|
189 | break;
|
190 | case 'decimal':
|
191 | tag('D');
|
192 | if (val.hasOwnProperty('places') && val.hasOwnProperty('digits')
|
193 | && val.places >= 0 && val.places < 256) {
|
194 | buffer[offset] = val.places; offset++;
|
195 | buffer.writeUInt32BE(val.digits, offset); offset += 4;
|
196 | }
|
197 | else throw new TypeError(
|
198 | "Decimal value must be {'places': 0..255, 'digits': uint32}, " +
|
199 | "got " + JSON.stringify(val));
|
200 | break;
|
201 | default:
|
202 | throw new TypeError('Unknown type to encode: ' + type);
|
203 | }
|
204 | return offset - start;
|
205 | }
|
206 |
|
207 | // Assume we're given a slice of the buffer that contains just the
|
208 | // fields.
|
209 | function decodeFields(slice) {
|
210 | var fields = {}, offset = 0, size = slice.length;
|
211 | var len, key, val;
|
212 |
|
213 | function decodeFieldValue() {
|
214 | var tag = String.fromCharCode(slice[offset]); offset++;
|
215 | switch (tag) {
|
216 | case 'b':
|
217 | val = slice.readUInt8(offset); offset++;
|
218 | break;
|
219 | case 'S':
|
220 | len = slice.readUInt32BE(offset); offset += 4;
|
221 | val = slice.toString('utf8', offset, offset + len);
|
222 | offset += len;
|
223 | break;
|
224 | case 'I':
|
225 | val = slice.readInt32BE(offset); offset += 4;
|
226 | break;
|
227 | case 'D': // only positive decimals, apparently.
|
228 | var places = slice[offset]; offset++;
|
229 | var digits = slice.readUInt32BE(offset); offset += 4;
|
230 | val = {'!': 'decimal', value: {places: places, digits: digits}};
|
231 | break;
|
232 | case 'T':
|
233 | val = slice.readUInt64BE(offset); offset += 8;
|
234 | val = {'!': 'timestamp', value: val};
|
235 | break;
|
236 | case 'F':
|
237 | len = slice.readUInt32BE(offset); offset += 4;
|
238 | val = decodeFields(slice.slice(offset, offset + len));
|
239 | offset += len;
|
240 | break;
|
241 | case 'A':
|
242 | len = slice.readUInt32BE(offset); offset += 4;
|
243 | decodeArray(offset + len);
|
244 | // NB decodeArray will itself update offset and val
|
245 | break;
|
246 | case 'd':
|
247 | val = slice.readDoubleBE(offset); offset += 8;
|
248 | break;
|
249 | case 'f':
|
250 | val = slice.readFloatBE(offset); offset += 4;
|
251 | break;
|
252 | case 'l':
|
253 | val = slice.readInt64BE(offset); offset += 8;
|
254 | break;
|
255 | case 's':
|
256 | val = slice.readInt16BE(offset); offset += 2;
|
257 | break;
|
258 | case 't':
|
259 | val = slice[offset] != 0; offset++;
|
260 | break;
|
261 | case 'V':
|
262 | val = null;
|
263 | break;
|
264 | case 'x':
|
265 | len = slice.readUInt32BE(offset); offset += 4;
|
266 | val = slice.slice(offset, offset + len);
|
267 | offset += len;
|
268 | break;
|
269 | default:
|
270 | throw new TypeError('Unexpected type tag "' + tag +'"');
|
271 | }
|
272 | }
|
273 |
|
274 | function decodeArray(until) {
|
275 | var vals = [];
|
276 | while (offset < until) {
|
277 | decodeFieldValue();
|
278 | vals.push(val);
|
279 | }
|
280 | val = vals;
|
281 | }
|
282 |
|
283 | while (offset < size) {
|
284 | len = slice.readUInt8(offset); offset++;
|
285 | key = slice.toString('utf8', offset, offset + len);
|
286 | offset += len;
|
287 | decodeFieldValue();
|
288 | fields[key] = val;
|
289 | }
|
290 | return fields;
|
291 | }
|
292 |
|
293 | module.exports.encodeTable = encodeTable;
|
294 | module.exports.decodeFields = decodeFields;
|