UNPKG

10.7 kBJavaScriptView Raw
1//
2//
3//
4
5/*
6
7The AMQP 0-9-1 is a mess when it comes to the types that can be
8encoded on the wire.
9
10There are four encoding schemes, and three overlapping sets of types:
11frames, methods, (field-)tables, and properties.
12
13Each *frame type* has a set layout in which values of given types are
14concatenated along with sections of "raw binary" data.
15
16In frames there are `shortstr`s, that is length-prefixed strings of
17UTF8 chars, 8 bit unsigned integers (called `octet`), unsigned 16 bit
18integers (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
22Methods are encoded as a frame giving a method ID and a sequence of
23arguments of known types. The encoded method argument values are
24concatenated (with some fun complications around "packing" consecutive
25bit values into bytes).
26
27Along with the types given in frames, method arguments may be long
28byte strings (`longstr`, not required to be UTF8) or 64 bit unsigned
29integers to be interpreted as timestamps (yeah I don't know why
30either), or arbitrary sets of key-value pairs (called `field-table`).
31
32Inside a field table the keys are `shortstr` and the values are
33prefixed with a byte tag giving the type. The types are any of the
34above except for bits (which are replaced by byte-wide `bool`), along
35with 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
39RabbitMQ and QPid use a subset of the field-table types, and different
40value tags, established before the AMQP 0-9-1 specification was
41published. So far as I know, no-one uses the types and tags as
42published. http://www.rabbitmq.com/amqp-0-9-1-errata.html gives the
43list of field-table types.
44
45Lastly, there are (sets of) properties, only one of which is given in
46AMQP 0-9-1: `BasicProperties`. These are almost the same as methods,
47except that they appear in content header frames, which include a
48content size, and they carry a set of flags indicating which
49properties 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
55require('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.
70function isFloatingPoint(n) {
71 return n >= 0x8000000000000000 ||
72 (Math.abs(n) < 0x4000000000000
73 && Math.floor(n) !== n);
74}
75
76function 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
92function 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
103function 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.
209function 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
293module.exports.encodeTable = encodeTable;
294module.exports.decodeFields = decodeFields;