index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. 'use strict';
  2. // Load modules
  3. const Assert = require('assert');
  4. const Crypto = require('crypto');
  5. const Path = require('path');
  6. const DeepEqual = require('./deep-equal');
  7. const Escape = require('./escape');
  8. // Declare internals
  9. const internals = {};
  10. // Deep object or array comparison
  11. exports.deepEqual = DeepEqual;
  12. // Clone object or array
  13. exports.clone = function (obj, options = {}, _seen = null) {
  14. if (typeof obj !== 'object' ||
  15. obj === null) {
  16. return obj;
  17. }
  18. const seen = _seen || new Map();
  19. const lookup = seen.get(obj);
  20. if (lookup) {
  21. return lookup;
  22. }
  23. let newObj;
  24. let cloneDeep = false;
  25. const isArray = Array.isArray(obj);
  26. if (!isArray) {
  27. if (Buffer.isBuffer(obj)) {
  28. newObj = Buffer.from(obj);
  29. }
  30. else if (obj instanceof Date) {
  31. newObj = new Date(obj.getTime());
  32. }
  33. else if (obj instanceof RegExp) {
  34. newObj = new RegExp(obj);
  35. }
  36. else {
  37. if (options.prototype !== false) { // Defaults to true
  38. const proto = Object.getPrototypeOf(obj);
  39. if (proto &&
  40. proto.isImmutable) {
  41. newObj = obj;
  42. }
  43. else {
  44. newObj = Object.create(proto);
  45. cloneDeep = true;
  46. }
  47. }
  48. else {
  49. newObj = {};
  50. cloneDeep = true;
  51. }
  52. }
  53. }
  54. else {
  55. newObj = [];
  56. cloneDeep = true;
  57. }
  58. seen.set(obj, newObj);
  59. if (cloneDeep) {
  60. const keys = internals.keys(obj, options);
  61. for (let i = 0; i < keys.length; ++i) {
  62. const key = keys[i];
  63. if (isArray && key === 'length') {
  64. continue;
  65. }
  66. const descriptor = Object.getOwnPropertyDescriptor(obj, key);
  67. if (descriptor &&
  68. (descriptor.get ||
  69. descriptor.set)) {
  70. Object.defineProperty(newObj, key, descriptor);
  71. }
  72. else {
  73. Object.defineProperty(newObj, key, {
  74. enumerable: descriptor ? descriptor.enumerable : true,
  75. writable: true,
  76. configurable: true,
  77. value: exports.clone(obj[key], options, seen)
  78. });
  79. }
  80. }
  81. if (isArray) {
  82. newObj.length = obj.length;
  83. }
  84. }
  85. return newObj;
  86. };
  87. internals.keys = function (obj, options = {}) {
  88. return options.symbols ? Reflect.ownKeys(obj) : Object.getOwnPropertyNames(obj);
  89. };
  90. // Merge all the properties of source into target, source wins in conflict, and by default null and undefined from source are applied
  91. exports.merge = function (target, source, isNullOverride /* = true */, isMergeArrays /* = true */) {
  92. exports.assert(target && typeof target === 'object', 'Invalid target value: must be an object');
  93. exports.assert(source === null || source === undefined || typeof source === 'object', 'Invalid source value: must be null, undefined, or an object');
  94. if (!source) {
  95. return target;
  96. }
  97. if (Array.isArray(source)) {
  98. exports.assert(Array.isArray(target), 'Cannot merge array onto an object');
  99. if (isMergeArrays === false) { // isMergeArrays defaults to true
  100. target.length = 0; // Must not change target assignment
  101. }
  102. for (let i = 0; i < source.length; ++i) {
  103. target.push(exports.clone(source[i]));
  104. }
  105. return target;
  106. }
  107. const keys = internals.keys(source);
  108. for (let i = 0; i < keys.length; ++i) {
  109. const key = keys[i];
  110. if (key === '__proto__' ||
  111. !Object.prototype.propertyIsEnumerable.call(source, key)) {
  112. continue;
  113. }
  114. const value = source[key];
  115. if (value &&
  116. typeof value === 'object') {
  117. if (!target[key] ||
  118. typeof target[key] !== 'object' ||
  119. (Array.isArray(target[key]) !== Array.isArray(value)) ||
  120. value instanceof Date ||
  121. Buffer.isBuffer(value) ||
  122. value instanceof RegExp) {
  123. target[key] = exports.clone(value);
  124. }
  125. else {
  126. exports.merge(target[key], value, isNullOverride, isMergeArrays);
  127. }
  128. }
  129. else {
  130. if (value !== null &&
  131. value !== undefined) { // Explicit to preserve empty strings
  132. target[key] = value;
  133. }
  134. else if (isNullOverride !== false) { // Defaults to true
  135. target[key] = value;
  136. }
  137. }
  138. }
  139. return target;
  140. };
  141. // Apply options to a copy of the defaults
  142. exports.applyToDefaults = function (defaults, options, isNullOverride) {
  143. exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object');
  144. exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object');
  145. if (!options) { // If no options, return null
  146. return null;
  147. }
  148. const copy = exports.clone(defaults);
  149. if (options === true) { // If options is set to true, use defaults
  150. return copy;
  151. }
  152. return exports.merge(copy, options, isNullOverride === true, false);
  153. };
  154. // Clone an object except for the listed keys which are shallow copied
  155. exports.cloneWithShallow = function (source, keys, options) {
  156. if (!source ||
  157. typeof source !== 'object') {
  158. return source;
  159. }
  160. const storage = internals.store(source, keys); // Move shallow copy items to storage
  161. const copy = exports.clone(source, options); // Deep copy the rest
  162. internals.restore(copy, source, storage); // Shallow copy the stored items and restore
  163. return copy;
  164. };
  165. internals.store = function (source, keys) {
  166. const storage = new Map();
  167. for (let i = 0; i < keys.length; ++i) {
  168. const key = keys[i];
  169. const value = exports.reach(source, key);
  170. if (typeof value === 'object' ||
  171. typeof value === 'function') {
  172. storage.set(key, value);
  173. internals.reachSet(source, key, undefined);
  174. }
  175. }
  176. return storage;
  177. };
  178. internals.restore = function (copy, source, storage) {
  179. for (const [key, value] of storage) {
  180. internals.reachSet(copy, key, value);
  181. internals.reachSet(source, key, value);
  182. }
  183. };
  184. internals.reachSet = function (obj, key, value) {
  185. const path = Array.isArray(key) ? key : key.split('.');
  186. let ref = obj;
  187. for (let i = 0; i < path.length; ++i) {
  188. const segment = path[i];
  189. if (i + 1 === path.length) {
  190. ref[segment] = value;
  191. }
  192. ref = ref[segment];
  193. }
  194. };
  195. // Apply options to defaults except for the listed keys which are shallow copied from option without merging
  196. exports.applyToDefaultsWithShallow = function (defaults, options, keys) {
  197. exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object');
  198. exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object');
  199. exports.assert(keys && Array.isArray(keys), 'Invalid keys');
  200. if (!options) { // If no options, return null
  201. return null;
  202. }
  203. const copy = exports.cloneWithShallow(defaults, keys);
  204. if (options === true) { // If options is set to true, use defaults
  205. return copy;
  206. }
  207. const storage = internals.store(options, keys); // Move shallow copy items to storage
  208. exports.merge(copy, options, false, false); // Deep copy the rest
  209. internals.restore(copy, options, storage); // Shallow copy the stored items and restore
  210. return copy;
  211. };
  212. // Find the common unique items in two arrays
  213. exports.intersect = function (array1, array2, justFirst) {
  214. if (!array1 ||
  215. !array2) {
  216. return (justFirst ? null : []);
  217. }
  218. const common = [];
  219. const hash = (Array.isArray(array1) ? new Set(array1) : array1);
  220. const found = new Set();
  221. for (const value of array2) {
  222. if (internals.has(hash, value) &&
  223. !found.has(value)) {
  224. if (justFirst) {
  225. return value;
  226. }
  227. common.push(value);
  228. found.add(value);
  229. }
  230. }
  231. return (justFirst ? null : common);
  232. };
  233. internals.has = function (ref, key) {
  234. if (typeof ref.has === 'function') {
  235. return ref.has(key);
  236. }
  237. return ref[key] !== undefined;
  238. };
  239. // Test if the reference contains the values
  240. exports.contain = function (ref, values, options = {}) { // options: { deep, once, only, part, symbols }
  241. /*
  242. string -> string(s)
  243. array -> item(s)
  244. object -> key(s)
  245. object -> object (key:value)
  246. */
  247. let valuePairs = null;
  248. if (typeof ref === 'object' &&
  249. typeof values === 'object' &&
  250. !Array.isArray(ref) &&
  251. !Array.isArray(values)) {
  252. valuePairs = values;
  253. const symbols = Object.getOwnPropertySymbols(values).filter(Object.prototype.propertyIsEnumerable.bind(values));
  254. values = [...Object.keys(values), ...symbols];
  255. }
  256. else {
  257. values = [].concat(values);
  258. }
  259. exports.assert(typeof ref === 'string' || typeof ref === 'object', 'Reference must be string or an object');
  260. exports.assert(values.length, 'Values array cannot be empty');
  261. let compare;
  262. let compareFlags;
  263. if (options.deep) {
  264. compare = exports.deepEqual;
  265. const hasOnly = options.hasOwnProperty('only');
  266. const hasPart = options.hasOwnProperty('part');
  267. compareFlags = {
  268. prototype: hasOnly ? options.only : hasPart ? !options.part : false,
  269. part: hasOnly ? !options.only : hasPart ? options.part : false
  270. };
  271. }
  272. else {
  273. compare = (a, b) => a === b;
  274. }
  275. let misses = false;
  276. const matches = new Array(values.length);
  277. for (let i = 0; i < matches.length; ++i) {
  278. matches[i] = 0;
  279. }
  280. if (typeof ref === 'string') {
  281. let pattern = '(';
  282. for (let i = 0; i < values.length; ++i) {
  283. const value = values[i];
  284. exports.assert(typeof value === 'string', 'Cannot compare string reference to non-string value');
  285. pattern += (i ? '|' : '') + exports.escapeRegex(value);
  286. }
  287. const regex = new RegExp(pattern + ')', 'g');
  288. const leftovers = ref.replace(regex, ($0, $1) => {
  289. const index = values.indexOf($1);
  290. ++matches[index];
  291. return ''; // Remove from string
  292. });
  293. misses = !!leftovers;
  294. }
  295. else if (Array.isArray(ref)) {
  296. const onlyOnce = !!(options.only && options.once);
  297. if (onlyOnce && ref.length !== values.length) {
  298. return false;
  299. }
  300. for (let i = 0; i < ref.length; ++i) {
  301. let matched = false;
  302. for (let j = 0; j < values.length && matched === false; ++j) {
  303. if (!onlyOnce || matches[j] === 0) {
  304. matched = compare(values[j], ref[i], compareFlags) && j;
  305. }
  306. }
  307. if (matched !== false) {
  308. ++matches[matched];
  309. }
  310. else {
  311. misses = true;
  312. }
  313. }
  314. }
  315. else {
  316. const keys = internals.keys(ref, options);
  317. for (let i = 0; i < keys.length; ++i) {
  318. const key = keys[i];
  319. const pos = values.indexOf(key);
  320. if (pos !== -1) {
  321. if (valuePairs &&
  322. !compare(valuePairs[key], ref[key], compareFlags)) {
  323. return false;
  324. }
  325. ++matches[pos];
  326. }
  327. else {
  328. misses = true;
  329. }
  330. }
  331. }
  332. if (options.only) {
  333. if (misses || !options.once) {
  334. return !misses;
  335. }
  336. }
  337. let result = false;
  338. for (let i = 0; i < matches.length; ++i) {
  339. result = result || !!matches[i];
  340. if ((options.once && matches[i] > 1) ||
  341. (!options.part && !matches[i])) {
  342. return false;
  343. }
  344. }
  345. return result;
  346. };
  347. // Flatten array
  348. exports.flatten = function (array, target) {
  349. const result = target || [];
  350. for (let i = 0; i < array.length; ++i) {
  351. if (Array.isArray(array[i])) {
  352. exports.flatten(array[i], result);
  353. }
  354. else {
  355. result.push(array[i]);
  356. }
  357. }
  358. return result;
  359. };
  360. // Convert an object key chain string ('a.b.c') to reference (object[a][b][c])
  361. exports.reach = function (obj, chain, options) {
  362. if (chain === false ||
  363. chain === null ||
  364. typeof chain === 'undefined') {
  365. return obj;
  366. }
  367. options = options || {};
  368. if (typeof options === 'string') {
  369. options = { separator: options };
  370. }
  371. const isChainArray = Array.isArray(chain);
  372. exports.assert(!isChainArray || !options.separator, 'Separator option no valid for array-based chain');
  373. const path = isChainArray ? chain : chain.split(options.separator || '.');
  374. let ref = obj;
  375. for (let i = 0; i < path.length; ++i) {
  376. let key = path[i];
  377. if (Array.isArray(ref)) {
  378. const number = Number(key);
  379. if (Number.isInteger(number) && number < 0) {
  380. key = ref.length + number;
  381. }
  382. }
  383. if (!ref ||
  384. !((typeof ref === 'object' || typeof ref === 'function') && key in ref) ||
  385. (typeof ref !== 'object' && options.functions === false)) { // Only object and function can have properties
  386. exports.assert(!options.strict || i + 1 === path.length, 'Missing segment', key, 'in reach path ', chain);
  387. exports.assert(typeof ref === 'object' || options.functions === true || typeof ref !== 'function', 'Invalid segment', key, 'in reach path ', chain);
  388. ref = options.default;
  389. break;
  390. }
  391. ref = ref[key];
  392. }
  393. return ref;
  394. };
  395. exports.reachTemplate = function (obj, template, options) {
  396. return template.replace(/{([^}]+)}/g, ($0, chain) => {
  397. const value = exports.reach(obj, chain, options);
  398. return (value === undefined || value === null ? '' : value);
  399. });
  400. };
  401. exports.assert = function (condition, ...args) {
  402. if (condition) {
  403. return;
  404. }
  405. if (args.length === 1 && args[0] instanceof Error) {
  406. throw args[0];
  407. }
  408. const msgs = args
  409. .filter((arg) => arg !== '')
  410. .map((arg) => {
  411. return typeof arg === 'string' ? arg : arg instanceof Error ? arg.message : exports.stringify(arg);
  412. });
  413. throw new Assert.AssertionError({
  414. message: msgs.join(' ') || 'Unknown error',
  415. actual: false,
  416. expected: true,
  417. operator: '==',
  418. stackStartFunction: exports.assert
  419. });
  420. };
  421. exports.Bench = function () {
  422. this.ts = 0;
  423. this.reset();
  424. };
  425. exports.Bench.prototype.reset = function () {
  426. this.ts = exports.Bench.now();
  427. };
  428. exports.Bench.prototype.elapsed = function () {
  429. return exports.Bench.now() - this.ts;
  430. };
  431. exports.Bench.now = function () {
  432. const ts = process.hrtime();
  433. return (ts[0] * 1e3) + (ts[1] / 1e6);
  434. };
  435. // Escape string for Regex construction
  436. exports.escapeRegex = function (string) {
  437. // Escape ^$.*+-?=!:|\/()[]{},
  438. return string.replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&');
  439. };
  440. // Escape attribute value for use in HTTP header
  441. exports.escapeHeaderAttribute = function (attribute) {
  442. // Allowed value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9, \, "
  443. exports.assert(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~\"\\]*$/.test(attribute), 'Bad attribute value (' + attribute + ')');
  444. return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); // Escape quotes and slash
  445. };
  446. exports.escapeHtml = function (string) {
  447. return Escape.escapeHtml(string);
  448. };
  449. exports.escapeJson = function (string) {
  450. return Escape.escapeJson(string);
  451. };
  452. exports.once = function (method) {
  453. if (method._hoekOnce) {
  454. return method;
  455. }
  456. let once = false;
  457. const wrapped = function (...args) {
  458. if (!once) {
  459. once = true;
  460. method(...args);
  461. }
  462. };
  463. wrapped._hoekOnce = true;
  464. return wrapped;
  465. };
  466. exports.ignore = function () { };
  467. exports.uniqueFilename = function (path, extension) {
  468. if (extension) {
  469. extension = extension[0] !== '.' ? '.' + extension : extension;
  470. }
  471. else {
  472. extension = '';
  473. }
  474. path = Path.resolve(path);
  475. const name = [Date.now(), process.pid, Crypto.randomBytes(8).toString('hex')].join('-') + extension;
  476. return Path.join(path, name);
  477. };
  478. exports.stringify = function (...args) {
  479. try {
  480. return JSON.stringify.apply(null, args);
  481. }
  482. catch (err) {
  483. return '[Cannot display object: ' + err.message + ']';
  484. }
  485. };
  486. exports.wait = function (timeout) {
  487. return new Promise((resolve) => setTimeout(resolve, timeout));
  488. };
  489. exports.block = function () {
  490. return new Promise(exports.ignore);
  491. };