codec.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. 'use strict';
  2. var codec = require('../lib/codec');
  3. var defs = require('../lib/defs');
  4. var assert = require('assert');
  5. var ints = require('buffer-more-ints');
  6. var C = require('claire');
  7. var forAll = C.forAll;
  8. // These just test known encodings; to generate the answers I used
  9. // RabbitMQ's binary generator module.
  10. var testCases = [
  11. // integers
  12. ['byte', {byte: 112}, [4,98,121,116,101,98,112]],
  13. ['byte max value', {byte: 127}, [4,98,121,116,101,98,127]],
  14. ['byte min value', {byte: -128}, [4,98,121,116,101,98,128]],
  15. ['< -128 promoted to signed short', {short: -129}, [5,115,104,111,114,116,115,255,127]],
  16. ['> 127 promoted to short', {short: 128}, [5,115,104,111,114,116,115,0,128]],
  17. ['< 2^15 still a short', {short: 0x7fff}, [5,115,104,111,114,116,115,127,255]],
  18. ['-2^15 still a short', {short: -0x8000}, [5,115,104,111,114,116,115,128,0]],
  19. ['>= 2^15 promoted to int', {int: 0x8000}, [3,105,110,116,73,0,0,128,0]],
  20. ['< -2^15 promoted to int', {int: -0x8001}, [3,105,110,116,73,255,255,127,255]],
  21. ['< 2^31 still an int', {int: 0x7fffffff}, [3,105,110,116,73,127,255,255,255]],
  22. ['>= -2^31 still an int', {int: -0x80000000}, [3,105,110,116,73,128,0,0,0]],
  23. ['>= 2^31 promoted to long', {long: 0x80000000}, [4,108,111,110,103,108,0,0,0,0,128,0,0,0]],
  24. ['< -2^31 promoted to long', {long: -0x80000001}, [4,108,111,110,103,108,255,255,255,255,127,255,255,255]],
  25. // floating point
  26. ['float value', {double: 0.5}, [6,100,111,117,98,108,101,100,63,224,0,0,0,0,0,0]],
  27. ['negative float value', {double: -0.5}, [6,100,111,117,98,108,101,100,191,224,0,0,0,0,0,0]],
  28. // %% test some boundaries of precision?
  29. // string
  30. ['string', {string: "boop"}, [6,115,116,114,105,110,103,83,0,0,0,4,98,111,111,112]],
  31. // buffer -> byte array
  32. ['byte array from buffer', {bytes: Buffer.from([1,2,3,4])},
  33. [5,98,121,116,101,115,120,0,0,0,4,1,2,3,4]],
  34. // boolean, void
  35. ['true', {bool: true}, [4,98,111,111,108,116,1]],
  36. ['false', {bool: false}, [4,98,111,111,108,116,0]],
  37. ['null', {'void': null}, [4,118,111,105,100,86]],
  38. // array, object
  39. ['array', {array: [6, true, "foo"]},[5,97,114,114,97,121,65,0,0,0,12,98,6,116,1,83,0,0,0,3,102,111,111]],
  40. ['object', {object: {foo: "bar", baz: 12}},[6,111,98,106,101,99,116,70,0,0,0,18,3,102,111,111,83,0,0,0,3,98,97,114,3,98,97,122,98,12]],
  41. // exotic types
  42. ['timestamp', {timestamp: {'!': 'timestamp', value: 1357212277527}},[9,116,105,109,101,115,116,97,109,112,84,0,0,1,60,0,39,219,23]],
  43. ['decimal', {decimal: {'!': 'decimal', value: {digits: 2345, places: 2}}},[7,100,101,99,105,109,97,108,68,2,0,0,9,41]],
  44. ['float', {float: {'!': 'float', value: 0.1}},[5,102,108,111,97,116,102,61,204,204,205]],
  45. ['unsignedbyte', {unsignedbyte:{'!': 'unsignedbyte', value: 255}}, [12,117,110,115,105,103,110,101,100,98,121,116,101,66,255]],
  46. ['unsignedshort', {unsignedshort:{'!': 'unsignedshort', value: 65535}}, [13,117,110,115,105,103,110,101,100,115,104,111,114,116,117,255,255]],
  47. ['unsignedint', {unsignedint:{'!': 'unsignedint', value: 4294967295}}, [11,117,110,115,105,103,110,101,100,105,110,116,105,255,255,255,255]],
  48. ];
  49. function bufferToArray(b) {
  50. return Array.prototype.slice.call(b);
  51. }
  52. suite("Implicit encodings", function() {
  53. testCases.forEach(function(tc) {
  54. var name = tc[0], val = tc[1], expect = tc[2];
  55. test(name, function() {
  56. var buffer = Buffer.alloc(1000);
  57. var size = codec.encodeTable(buffer, val, 0);
  58. var result = buffer.subarray(4, size);
  59. assert.deepEqual(expect, bufferToArray(result));
  60. });
  61. });
  62. });
  63. // Whole frames
  64. var amqp = require('./data');
  65. function roundtrip_table(t) {
  66. var buf = Buffer.alloc(4096);
  67. var size = codec.encodeTable(buf, t, 0);
  68. var decoded = codec.decodeFields(buf.subarray(4, size)); // ignore the length-prefix
  69. try {
  70. assert.deepEqual(removeExplicitTypes(t), decoded);
  71. }
  72. catch (e) { return false; }
  73. return true;
  74. }
  75. function roundtrips(T) {
  76. return forAll(T).satisfy(function(v) { return roundtrip_table({value: v}); });
  77. }
  78. suite("Roundtrip values", function() {
  79. [
  80. amqp.Octet,
  81. amqp.ShortStr,
  82. amqp.LongStr,
  83. amqp.UShort,
  84. amqp.ULong,
  85. amqp.ULongLong,
  86. amqp.UShort,
  87. amqp.Short,
  88. amqp.Long,
  89. amqp.Bit,
  90. amqp.Decimal,
  91. amqp.Timestamp,
  92. amqp.UnsignedByte,
  93. amqp.UnsignedShort,
  94. amqp.UnsignedInt,
  95. amqp.Double,
  96. amqp.Float,
  97. amqp.FieldArray,
  98. amqp.FieldTable
  99. ].forEach(function(T) {
  100. test(T.toString() + ' roundtrip', roundtrips(T).asTest());
  101. });
  102. });
  103. // When encoding, you can supply explicitly-typed fields like `{'!':
  104. // int32, 50}`. Most of these do not appear in the decoded values, so
  105. // to compare like-to-like we have to remove them from the input.
  106. function removeExplicitTypes(input) {
  107. switch (typeof input) {
  108. case 'object':
  109. if (input == null) {
  110. return null;
  111. }
  112. if (Array.isArray(input)) {
  113. var newArr = [];
  114. for (var i = 0; i < input.length; i++) {
  115. newArr[i] = removeExplicitTypes(input[i]);
  116. }
  117. return newArr;
  118. }
  119. if (Buffer.isBuffer(input)) {
  120. return input;
  121. }
  122. switch (input['!']) {
  123. case 'timestamp':
  124. case 'decimal':
  125. case 'float':
  126. return input;
  127. case undefined:
  128. var newObj = {}
  129. for (var k in input) {
  130. newObj[k] = removeExplicitTypes(input[k]);
  131. }
  132. return newObj;
  133. default:
  134. return input.value;
  135. }
  136. default:
  137. return input;
  138. }
  139. }
  140. // Asserts that the decoded fields are equal to the original fields,
  141. // or equal to a default where absent in the original. The defaults
  142. // depend on the type of method or properties.
  143. //
  144. // This works slightly different for methods and properties: for
  145. // methods, each field must have a value, so the default is
  146. // substituted for undefined values when encoding; for properties,
  147. // fields may be absent in the encoded value, so a default is
  148. // substituted for missing fields when decoding. The effect is the
  149. // same so far as these tests are concerned.
  150. function assertEqualModuloDefaults(original, decodedFields) {
  151. var args = defs.info(original.id).args;
  152. for (var i=0; i < args.length; i++) {
  153. var arg = args[i];
  154. var originalValue = original.fields[arg.name];
  155. var decodedValue = decodedFields[arg.name];
  156. try {
  157. if (originalValue === undefined) {
  158. // longstr gets special treatment here, since the defaults are
  159. // given as strings rather than buffers, but the decoded values
  160. // will be buffers.
  161. assert.deepEqual((arg.type === 'longstr') ?
  162. Buffer.from(arg.default) : arg.default,
  163. decodedValue);
  164. }
  165. else {
  166. assert.deepEqual(removeExplicitTypes(originalValue), decodedValue);
  167. }
  168. }
  169. catch (assertionErr) {
  170. var methodOrProps = defs.info(original.id).name;
  171. assertionErr.message += ' (frame ' + methodOrProps +
  172. ' field ' + arg.name + ')';
  173. throw assertionErr;
  174. }
  175. }
  176. // %%% TODO make sure there's no surplus fields
  177. return true;
  178. }
  179. // This is handy for elsewhere
  180. module.exports.assertEqualModuloDefaults = assertEqualModuloDefaults;
  181. function roundtripMethod(Method) {
  182. return forAll(Method).satisfy(function(method) {
  183. var buf = defs.encodeMethod(method.id, 0, method.fields);
  184. // FIXME depends on framing, ugh
  185. var fs1 = defs.decode(method.id, buf.subarray(11, buf.length));
  186. assertEqualModuloDefaults(method, fs1);
  187. return true;
  188. });
  189. }
  190. function roundtripProperties(Properties) {
  191. return forAll(Properties).satisfy(function(properties) {
  192. var buf = defs.encodeProperties(properties.id, 0, properties.size,
  193. properties.fields);
  194. // FIXME depends on framing, ugh
  195. var fs1 = defs.decode(properties.id, buf.subarray(19, buf.length));
  196. assert.equal(properties.size, ints.readUInt64BE(buf, 11));
  197. assertEqualModuloDefaults(properties, fs1);
  198. return true;
  199. });
  200. }
  201. suite("Roundtrip methods", function() {
  202. amqp.methods.forEach(function(Method) {
  203. test(Method.toString() + ' roundtrip',
  204. roundtripMethod(Method).asTest());
  205. });
  206. });
  207. suite("Roundtrip properties", function() {
  208. amqp.properties.forEach(function(Properties) {
  209. test(Properties.toString() + ' roundtrip',
  210. roundtripProperties(Properties).asTest());
  211. });
  212. });