index.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. 'use strict';
  2. var crypto = require('crypto');
  3. /**
  4. * Exported function
  5. *
  6. * Options:
  7. *
  8. * - `algorithm` hash algo to be used by this instance: *'sha1', 'md5'
  9. * - `excludeValues` {true|*false} hash object keys, values ignored
  10. * - `encoding` hash encoding, supports 'buffer', '*hex', 'binary', 'base64'
  11. * - `ignoreUnknown` {true|*false} ignore unknown object types
  12. * - `replacer` optional function that replaces values before hashing
  13. * - `respectFunctionProperties` {*true|false} consider function properties when hashing
  14. * - `respectFunctionNames` {*true|false} consider 'name' property of functions for hashing
  15. * - `respectType` {*true|false} Respect special properties (prototype, constructor)
  16. * when hashing to distinguish between types
  17. * - `unorderedArrays` {true|*false} Sort all arrays before hashing
  18. * - `unorderedSets` {*true|false} Sort `Set` and `Map` instances before hashing
  19. * * = default
  20. *
  21. * @param {object} object value to hash
  22. * @param {object} options hashing options
  23. * @return {string} hash value
  24. * @api public
  25. */
  26. exports = module.exports = objectHash;
  27. function objectHash(object, options){
  28. options = applyDefaults(object, options);
  29. return hash(object, options);
  30. }
  31. /**
  32. * Exported sugar methods
  33. *
  34. * @param {object} object value to hash
  35. * @return {string} hash value
  36. * @api public
  37. */
  38. exports.sha1 = function(object){
  39. return objectHash(object);
  40. };
  41. exports.keys = function(object){
  42. return objectHash(object, {excludeValues: true, algorithm: 'sha1', encoding: 'hex'});
  43. };
  44. exports.MD5 = function(object){
  45. return objectHash(object, {algorithm: 'md5', encoding: 'hex'});
  46. };
  47. exports.keysMD5 = function(object){
  48. return objectHash(object, {algorithm: 'md5', encoding: 'hex', excludeValues: true});
  49. };
  50. // Internals
  51. var hashes = crypto.getHashes ? crypto.getHashes().slice() : ['sha1', 'md5'];
  52. hashes.push('passthrough');
  53. var encodings = ['buffer', 'hex', 'binary', 'base64'];
  54. function applyDefaults(object, options){
  55. options = options || {};
  56. options.algorithm = options.algorithm || 'sha1';
  57. options.encoding = options.encoding || 'hex';
  58. options.excludeValues = options.excludeValues ? true : false;
  59. options.algorithm = options.algorithm.toLowerCase();
  60. options.encoding = options.encoding.toLowerCase();
  61. options.ignoreUnknown = options.ignoreUnknown !== true ? false : true; // default to false
  62. options.respectType = options.respectType === false ? false : true; // default to true
  63. options.respectFunctionNames = options.respectFunctionNames === false ? false : true;
  64. options.respectFunctionProperties = options.respectFunctionProperties === false ? false : true;
  65. options.unorderedArrays = options.unorderedArrays !== true ? false : true; // default to false
  66. options.unorderedSets = options.unorderedSets === false ? false : true; // default to false
  67. options.unorderedObjects = options.unorderedObjects === false ? false : true; // default to true
  68. options.replacer = options.replacer || undefined;
  69. options.excludeKeys = options.excludeKeys || undefined;
  70. if(typeof object === 'undefined') {
  71. throw new Error('Object argument required.');
  72. }
  73. // if there is a case-insensitive match in the hashes list, accept it
  74. // (i.e. SHA256 for sha256)
  75. for (var i = 0; i < hashes.length; ++i) {
  76. if (hashes[i].toLowerCase() === options.algorithm.toLowerCase()) {
  77. options.algorithm = hashes[i];
  78. }
  79. }
  80. if(hashes.indexOf(options.algorithm) === -1){
  81. throw new Error('Algorithm "' + options.algorithm + '" not supported. ' +
  82. 'supported values: ' + hashes.join(', '));
  83. }
  84. if(encodings.indexOf(options.encoding) === -1 &&
  85. options.algorithm !== 'passthrough'){
  86. throw new Error('Encoding "' + options.encoding + '" not supported. ' +
  87. 'supported values: ' + encodings.join(', '));
  88. }
  89. return options;
  90. }
  91. /** Check if the given function is a native function */
  92. function isNativeFunction(f) {
  93. if ((typeof f) !== 'function') {
  94. return false;
  95. }
  96. var exp = /^function\s+\w*\s*\(\s*\)\s*{\s+\[native code\]\s+}$/i;
  97. return exp.exec(Function.prototype.toString.call(f)) != null;
  98. }
  99. function hash(object, options) {
  100. var hashingStream;
  101. if (options.algorithm !== 'passthrough') {
  102. hashingStream = crypto.createHash(options.algorithm);
  103. } else {
  104. hashingStream = new PassThrough();
  105. }
  106. if (typeof hashingStream.write === 'undefined') {
  107. hashingStream.write = hashingStream.update;
  108. hashingStream.end = hashingStream.update;
  109. }
  110. var hasher = typeHasher(options, hashingStream);
  111. hasher.dispatch(object);
  112. if (!hashingStream.update)
  113. hashingStream.end('')
  114. if (hashingStream.digest) {
  115. return hashingStream.digest(options.encoding === 'buffer' ? undefined : options.encoding);
  116. }
  117. var buf = hashingStream.read();
  118. if (options.encoding === 'buffer') {
  119. return buf;
  120. }
  121. return buf.toString(options.encoding);
  122. }
  123. /**
  124. * Expose streaming API
  125. *
  126. * @param {object} object Value to serialize
  127. * @param {object} options Options, as for hash()
  128. * @param {object} stream A stream to write the serializiation to
  129. * @api public
  130. */
  131. exports.writeToStream = function(object, options, stream) {
  132. if (typeof stream === 'undefined') {
  133. stream = options;
  134. options = {};
  135. }
  136. options = applyDefaults(object, options);
  137. return typeHasher(options, stream).dispatch(object);
  138. };
  139. function typeHasher(options, writeTo, context){
  140. context = context || [];
  141. var write = function(str) {
  142. if (writeTo.update)
  143. return writeTo.update(str, 'utf8');
  144. else
  145. return writeTo.write(str, 'utf8');
  146. }
  147. return {
  148. dispatch: function(value){
  149. if (options.replacer) {
  150. value = options.replacer(value);
  151. }
  152. var type = typeof value;
  153. if (value === null) {
  154. type = 'null';
  155. }
  156. //console.log("[DEBUG] Dispatch: ", value, "->", type, " -> ", "_" + type);
  157. return this['_' + type](value);
  158. },
  159. _object: function(object) {
  160. var pattern = (/\[object (.*)\]/i);
  161. var objString = Object.prototype.toString.call(object);
  162. var objType = pattern.exec(objString);
  163. if (!objType) { // object type did not match [object ...]
  164. objType = 'unknown:[' + objString + ']';
  165. } else {
  166. objType = objType[1]; // take only the class name
  167. }
  168. objType = objType.toLowerCase();
  169. var objectNumber = null;
  170. if ((objectNumber = context.indexOf(object)) >= 0) {
  171. return this.dispatch('[CIRCULAR:' + objectNumber + ']');
  172. } else {
  173. context.push(object);
  174. }
  175. if (typeof Buffer !== 'undefined' && Buffer.isBuffer && Buffer.isBuffer(object)) {
  176. write('buffer:');
  177. return write(object);
  178. }
  179. if(objType !== 'object' && objType !== 'function') {
  180. if(this['_' + objType]) {
  181. this['_' + objType](object);
  182. } else if (options.ignoreUnknown) {
  183. return write('[' + objType + ']');
  184. } else {
  185. throw new Error('Unknown object type "' + objType + '"');
  186. }
  187. }else{
  188. var keys = Object.keys(object);
  189. if (options.unorderedObjects) {
  190. keys = keys.sort();
  191. }
  192. // Make sure to incorporate special properties, so
  193. // Types with different prototypes will produce
  194. // a different hash and objects derived from
  195. // different functions (`new Foo`, `new Bar`) will
  196. // produce different hashes.
  197. // We never do this for native functions since some
  198. // seem to break because of that.
  199. if (options.respectType !== false && !isNativeFunction(object)) {
  200. keys.splice(0, 0, 'prototype', '__proto__', 'constructor');
  201. }
  202. if (options.excludeKeys) {
  203. keys = keys.filter(function(key) { return !options.excludeKeys(key); });
  204. }
  205. write('object:' + keys.length + ':');
  206. var self = this;
  207. return keys.forEach(function(key){
  208. self.dispatch(key);
  209. write(':');
  210. if(!options.excludeValues) {
  211. self.dispatch(object[key]);
  212. }
  213. write(',');
  214. });
  215. }
  216. },
  217. _array: function(arr, unordered){
  218. unordered = typeof unordered !== 'undefined' ? unordered :
  219. options.unorderedArrays !== false; // default to options.unorderedArrays
  220. var self = this;
  221. write('array:' + arr.length + ':');
  222. if (!unordered || arr.length <= 1) {
  223. return arr.forEach(function(entry) {
  224. return self.dispatch(entry);
  225. });
  226. }
  227. // the unordered case is a little more complicated:
  228. // since there is no canonical ordering on objects,
  229. // i.e. {a:1} < {a:2} and {a:1} > {a:2} are both false,
  230. // we first serialize each entry using a PassThrough stream
  231. // before sorting.
  232. // also: we can’t use the same context array for all entries
  233. // since the order of hashing should *not* matter. instead,
  234. // we keep track of the additions to a copy of the context array
  235. // and add all of them to the global context array when we’re done
  236. var contextAdditions = [];
  237. var entries = arr.map(function(entry) {
  238. var strm = new PassThrough();
  239. var localContext = context.slice(); // make copy
  240. var hasher = typeHasher(options, strm, localContext);
  241. hasher.dispatch(entry);
  242. // take only what was added to localContext and append it to contextAdditions
  243. contextAdditions = contextAdditions.concat(localContext.slice(context.length));
  244. return strm.read().toString();
  245. });
  246. context = context.concat(contextAdditions);
  247. entries.sort();
  248. return this._array(entries, false);
  249. },
  250. _date: function(date){
  251. return write('date:' + date.toJSON());
  252. },
  253. _symbol: function(sym){
  254. return write('symbol:' + sym.toString());
  255. },
  256. _error: function(err){
  257. return write('error:' + err.toString());
  258. },
  259. _boolean: function(bool){
  260. return write('bool:' + bool.toString());
  261. },
  262. _string: function(string){
  263. write('string:' + string.length + ':');
  264. write(string.toString());
  265. },
  266. _function: function(fn){
  267. write('fn:');
  268. if (isNativeFunction(fn)) {
  269. this.dispatch('[native]');
  270. } else {
  271. this.dispatch(fn.toString());
  272. }
  273. if (options.respectFunctionNames !== false) {
  274. // Make sure we can still distinguish native functions
  275. // by their name, otherwise String and Function will
  276. // have the same hash
  277. this.dispatch("function-name:" + String(fn.name));
  278. }
  279. if (options.respectFunctionProperties) {
  280. this._object(fn);
  281. }
  282. },
  283. _number: function(number){
  284. return write('number:' + number.toString());
  285. },
  286. _xml: function(xml){
  287. return write('xml:' + xml.toString());
  288. },
  289. _null: function() {
  290. return write('Null');
  291. },
  292. _undefined: function() {
  293. return write('Undefined');
  294. },
  295. _regexp: function(regex){
  296. return write('regex:' + regex.toString());
  297. },
  298. _uint8array: function(arr){
  299. write('uint8array:');
  300. return this.dispatch(Array.prototype.slice.call(arr));
  301. },
  302. _uint8clampedarray: function(arr){
  303. write('uint8clampedarray:');
  304. return this.dispatch(Array.prototype.slice.call(arr));
  305. },
  306. _int8array: function(arr){
  307. write('uint8array:');
  308. return this.dispatch(Array.prototype.slice.call(arr));
  309. },
  310. _uint16array: function(arr){
  311. write('uint16array:');
  312. return this.dispatch(Array.prototype.slice.call(arr));
  313. },
  314. _int16array: function(arr){
  315. write('uint16array:');
  316. return this.dispatch(Array.prototype.slice.call(arr));
  317. },
  318. _uint32array: function(arr){
  319. write('uint32array:');
  320. return this.dispatch(Array.prototype.slice.call(arr));
  321. },
  322. _int32array: function(arr){
  323. write('uint32array:');
  324. return this.dispatch(Array.prototype.slice.call(arr));
  325. },
  326. _float32array: function(arr){
  327. write('float32array:');
  328. return this.dispatch(Array.prototype.slice.call(arr));
  329. },
  330. _float64array: function(arr){
  331. write('float64array:');
  332. return this.dispatch(Array.prototype.slice.call(arr));
  333. },
  334. _arraybuffer: function(arr){
  335. write('arraybuffer:');
  336. return this.dispatch(new Uint8Array(arr));
  337. },
  338. _url: function(url) {
  339. return write('url:' + url.toString(), 'utf8');
  340. },
  341. _map: function(map) {
  342. write('map:');
  343. var arr = Array.from(map);
  344. return this._array(arr, options.unorderedSets !== false);
  345. },
  346. _set: function(set) {
  347. write('set:');
  348. var arr = Array.from(set);
  349. return this._array(arr, options.unorderedSets !== false);
  350. },
  351. _blob: function() {
  352. if (options.ignoreUnknown) {
  353. return write('[blob]');
  354. }
  355. throw Error('Hashing Blob objects is currently not supported\n' +
  356. '(see https://github.com/puleos/object-hash/issues/26)\n' +
  357. 'Use "options.replacer" or "options.ignoreUnknown"\n');
  358. },
  359. _domwindow: function() { return write('domwindow'); },
  360. /* Node.js standard native objects */
  361. _process: function() { return write('process'); },
  362. _timer: function() { return write('timer'); },
  363. _pipe: function() { return write('pipe'); },
  364. _tcp: function() { return write('tcp'); },
  365. _udp: function() { return write('udp'); },
  366. _tty: function() { return write('tty'); },
  367. _statwatcher: function() { return write('statwatcher'); },
  368. _securecontext: function() { return write('securecontext'); },
  369. _connection: function() { return write('connection'); },
  370. _zlib: function() { return write('zlib'); },
  371. _context: function() { return write('context'); },
  372. _nodescript: function() { return write('nodescript'); },
  373. _httpparser: function() { return write('httpparser'); },
  374. _dataview: function() { return write('dataview'); },
  375. _signal: function() { return write('signal'); },
  376. _fsevent: function() { return write('fsevent'); },
  377. _tlswrap: function() { return write('tlswrap'); }
  378. };
  379. }
  380. // Mini-implementation of stream.PassThrough
  381. // We are far from having need for the full implementation, and we can
  382. // make assumptions like "many writes, then only one final read"
  383. // and we can ignore encoding specifics
  384. function PassThrough() {
  385. return {
  386. buf: '',
  387. write: function(b) {
  388. this.buf += b;
  389. },
  390. end: function(b) {
  391. this.buf += b;
  392. },
  393. read: function() {
  394. return this.buf;
  395. }
  396. };
  397. }