'use strict'; // Load modules const Assert = require('assert'); const Crypto = require('crypto'); const Path = require('path'); const DeepEqual = require('./deep-equal'); const Escape = require('./escape'); const Types = require('./types'); // Declare internals const internals = { needsProtoHack: new Set([Types.set, Types.map, Types.weakSet, Types.weakMap]) }; // Deep object or array comparison exports.deepEqual = DeepEqual; // Clone object or array exports.clone = function (obj, options = {}, _seen = null) { if (typeof obj !== 'object' || obj === null) { return obj; } const seen = _seen || new Map(); const lookup = seen.get(obj); if (lookup) { return lookup; } const baseProto = Types.getInternalProto(obj); let newObj; switch (baseProto) { case Types.buffer: return Buffer.from(obj); case Types.date: return new Date(obj.getTime()); case Types.regex: return new RegExp(obj); case Types.array: newObj = []; break; default: if (options.prototype !== false) { // Defaults to true const proto = Object.getPrototypeOf(obj); if (proto && proto.isImmutable) { return obj; } if (internals.needsProtoHack.has(baseProto)) { newObj = new proto.constructor(); if (proto !== baseProto) { Object.setPrototypeOf(newObj, proto); } } else { newObj = Object.create(proto); } } else if (internals.needsProtoHack.has(baseProto)) { newObj = new baseProto.constructor(); } else { newObj = {}; } } seen.set(obj, newObj); // Set seen, since obj could recurse if (baseProto === Types.set) { for (const value of obj) { newObj.add(exports.clone(value, options, seen)); } } else if (baseProto === Types.map) { for (const [key, value] of obj) { newObj.set(key, exports.clone(value, options, seen)); } } const keys = internals.keys(obj, options); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; if (baseProto === Types.array && key === 'length') { continue; } const descriptor = Object.getOwnPropertyDescriptor(obj, key); if (descriptor && (descriptor.get || descriptor.set)) { Object.defineProperty(newObj, key, descriptor); } else { Object.defineProperty(newObj, key, { enumerable: descriptor ? descriptor.enumerable : true, writable: true, configurable: true, value: exports.clone(obj[key], options, seen) }); } } if (baseProto === Types.array) { newObj.length = obj.length; } return newObj; }; internals.keys = function (obj, options = {}) { return options.symbols ? Reflect.ownKeys(obj) : Object.getOwnPropertyNames(obj); }; // Merge all the properties of source into target, source wins in conflict, and by default null and undefined from source are applied exports.merge = function (target, source, isNullOverride = true, isMergeArrays = true) { exports.assert(target && typeof target === 'object', 'Invalid target value: must be an object'); exports.assert(source === null || source === undefined || typeof source === 'object', 'Invalid source value: must be null, undefined, or an object'); if (!source) { return target; } if (Array.isArray(source)) { exports.assert(Array.isArray(target), 'Cannot merge array onto an object'); if (!isMergeArrays) { target.length = 0; // Must not change target assignment } for (let i = 0; i < source.length; ++i) { target.push(exports.clone(source[i])); } return target; } const keys = internals.keys(source); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; if (key === '__proto__' || !Object.prototype.propertyIsEnumerable.call(source, key)) { continue; } const value = source[key]; if (value && typeof value === 'object') { if (!target[key] || typeof target[key] !== 'object' || (Array.isArray(target[key]) !== Array.isArray(value)) || value instanceof Date || Buffer.isBuffer(value) || value instanceof RegExp) { target[key] = exports.clone(value); } else { exports.merge(target[key], value, isNullOverride, isMergeArrays); } } else { if (value !== null && value !== undefined) { // Explicit to preserve empty strings target[key] = value; } else if (isNullOverride) { target[key] = value; } } } return target; }; // Apply options to a copy of the defaults exports.applyToDefaults = function (defaults, options, isNullOverride = false) { exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object'); exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object'); if (!options) { // If no options, return null return null; } const copy = exports.clone(defaults); if (options === true) { // If options is set to true, use defaults return copy; } return exports.merge(copy, options, isNullOverride, false); }; // Clone an object except for the listed keys which are shallow copied exports.cloneWithShallow = function (source, keys, options) { if (!source || typeof source !== 'object') { return source; } const storage = internals.store(source, keys); // Move shallow copy items to storage const copy = exports.clone(source, options); // Deep copy the rest internals.restore(copy, source, storage); // Shallow copy the stored items and restore return copy; }; internals.store = function (source, keys) { const storage = new Map(); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const value = exports.reach(source, key); if (typeof value === 'object' || typeof value === 'function') { storage.set(key, value); internals.reachSet(source, key, undefined); } } return storage; }; internals.restore = function (copy, source, storage) { for (const [key, value] of storage) { internals.reachSet(copy, key, value); internals.reachSet(source, key, value); } }; internals.reachSet = function (obj, key, value) { const path = Array.isArray(key) ? key : key.split('.'); let ref = obj; for (let i = 0; i < path.length; ++i) { const segment = path[i]; if (i + 1 === path.length) { ref[segment] = value; } ref = ref[segment]; } }; // Apply options to defaults except for the listed keys which are shallow copied from option without merging exports.applyToDefaultsWithShallow = function (defaults, options, keys) { exports.assert(defaults && typeof defaults === 'object', 'Invalid defaults value: must be an object'); exports.assert(!options || options === true || typeof options === 'object', 'Invalid options value: must be true, falsy or an object'); exports.assert(keys && Array.isArray(keys), 'Invalid keys'); if (!options) { // If no options, return null return null; } const copy = exports.cloneWithShallow(defaults, keys); if (options === true) { // If options is set to true, use defaults return copy; } const storage = internals.store(options, keys); // Move shallow copy items to storage exports.merge(copy, options, false, false); // Deep copy the rest internals.restore(copy, options, storage); // Shallow copy the stored items and restore return copy; }; // Find the common unique items in two arrays exports.intersect = function (array1, array2, justFirst = false) { if (!array1 || !array2) { return (justFirst ? null : []); } const common = []; const hash = (Array.isArray(array1) ? new Set(array1) : array1); const found = new Set(); for (const value of array2) { if (internals.has(hash, value) && !found.has(value)) { if (justFirst) { return value; } common.push(value); found.add(value); } } return (justFirst ? null : common); }; internals.has = function (ref, key) { if (typeof ref.has === 'function') { return ref.has(key); } return ref[key] !== undefined; }; // Test if the reference contains the values exports.contain = function (ref, values, options = {}) { // options: { deep, once, only, part, symbols } /* string -> string(s) array -> item(s) object -> key(s) object -> object (key:value) */ let valuePairs = null; if (typeof ref === 'object' && typeof values === 'object' && !Array.isArray(ref) && !Array.isArray(values)) { valuePairs = values; const symbols = Object.getOwnPropertySymbols(values).filter(Object.prototype.propertyIsEnumerable.bind(values)); values = [...Object.keys(values), ...symbols]; } else { values = [].concat(values); } exports.assert(typeof ref === 'string' || typeof ref === 'object', 'Reference must be string or an object'); exports.assert(values.length, 'Values array cannot be empty'); let compare; let compareFlags; if (options.deep) { compare = exports.deepEqual; const hasOnly = options.only !== undefined; const hasPart = options.part !== undefined; compareFlags = { prototype: hasOnly ? options.only : hasPart ? !options.part : false, part: hasOnly ? !options.only : hasPart ? options.part : false }; } else { compare = (a, b) => a === b; } let misses = false; const matches = new Array(values.length); for (let i = 0; i < matches.length; ++i) { matches[i] = 0; } if (typeof ref === 'string') { let pattern = '('; for (let i = 0; i < values.length; ++i) { const value = values[i]; exports.assert(typeof value === 'string', 'Cannot compare string reference to non-string value'); pattern += (i ? '|' : '') + exports.escapeRegex(value); } const regex = new RegExp(pattern + ')', 'g'); const leftovers = ref.replace(regex, ($0, $1) => { const index = values.indexOf($1); ++matches[index]; return ''; // Remove from string }); misses = !!leftovers; } else if (Array.isArray(ref)) { const onlyOnce = !!(options.only && options.once); if (onlyOnce && ref.length !== values.length) { return false; } for (let i = 0; i < ref.length; ++i) { let matched = false; for (let j = 0; j < values.length && matched === false; ++j) { if (!onlyOnce || matches[j] === 0) { matched = compare(values[j], ref[i], compareFlags) && j; } } if (matched !== false) { ++matches[matched]; } else { misses = true; } } } else { const keys = internals.keys(ref, options); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const pos = values.indexOf(key); if (pos !== -1) { if (valuePairs && !compare(valuePairs[key], ref[key], compareFlags)) { return false; } ++matches[pos]; } else { misses = true; } } } if (options.only) { if (misses || !options.once) { return !misses; } } let result = false; for (let i = 0; i < matches.length; ++i) { result = result || !!matches[i]; if ((options.once && matches[i] > 1) || (!options.part && !matches[i])) { return false; } } return result; }; // Flatten array exports.flatten = function (array, target) { const result = target || []; for (let i = 0; i < array.length; ++i) { if (Array.isArray(array[i])) { exports.flatten(array[i], result); } else { result.push(array[i]); } } return result; }; // Convert an object key chain string ('a.b.c') to reference (object[a][b][c]) exports.reach = function (obj, chain, options) { if (chain === false || chain === null || chain === undefined) { return obj; } options = options || {}; if (typeof options === 'string') { options = { separator: options }; } const isChainArray = Array.isArray(chain); exports.assert(!isChainArray || !options.separator, 'Separator option no valid for array-based chain'); const path = isChainArray ? chain : chain.split(options.separator || '.'); let ref = obj; for (let i = 0; i < path.length; ++i) { let key = path[i]; if (Array.isArray(ref)) { const number = Number(key); if (Number.isInteger(number) && number < 0) { key = ref.length + number; } } if (!ref || !((typeof ref === 'object' || typeof ref === 'function') && key in ref) || (typeof ref !== 'object' && options.functions === false)) { // Only object and function can have properties exports.assert(!options.strict || i + 1 === path.length, 'Missing segment', key, 'in reach path ', chain); exports.assert(typeof ref === 'object' || options.functions === true || typeof ref !== 'function', 'Invalid segment', key, 'in reach path ', chain); ref = options.default; break; } ref = ref[key]; } return ref; }; exports.reachTemplate = function (obj, template, options) { return template.replace(/{([^}]+)}/g, ($0, chain) => { const value = exports.reach(obj, chain, options); return (value === undefined || value === null ? '' : value); }); }; exports.assert = function (condition, ...args) { if (condition) { return; } if (args.length === 1 && args[0] instanceof Error) { throw args[0]; } const msgs = args .filter((arg) => arg !== '') .map((arg) => { return typeof arg === 'string' ? arg : arg instanceof Error ? arg.message : exports.stringify(arg); }); throw new Assert.AssertionError({ message: msgs.join(' ') || 'Unknown error', actual: false, expected: true, operator: '==', stackStartFunction: exports.assert }); }; exports.Bench = class { constructor() { this.ts = 0; this.reset(); } reset() { this.ts = exports.Bench.now(); } elapsed() { return exports.Bench.now() - this.ts; } static now() { const ts = process.hrtime(); return (ts[0] * 1e3) + (ts[1] / 1e6); } }; // Escape string for Regex construction exports.escapeRegex = function (string) { // Escape ^$.*+-?=!:|\/()[]{}, return string.replace(/[\^\$\.\*\+\-\?\=\!\:\|\\\/\(\)\[\]\{\}\,]/g, '\\$&'); }; // Escape attribute value for use in HTTP header exports.escapeHeaderAttribute = function (attribute) { // Allowed value characters: !#$%&'()*+,-./:;<=>?@[]^_`{|}~ and space, a-z, A-Z, 0-9, \, " exports.assert(/^[ \w\!#\$%&'\(\)\*\+,\-\.\/\:;<\=>\?@\[\]\^`\{\|\}~\"\\]*$/.test(attribute), 'Bad attribute value (' + attribute + ')'); return attribute.replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); // Escape quotes and slash }; exports.escapeHtml = function (string) { return Escape.escapeHtml(string); }; exports.escapeJson = function (string) { return Escape.escapeJson(string); }; exports.once = function (method) { if (method._hoekOnce) { return method; } let once = false; const wrapped = function (...args) { if (!once) { once = true; method(...args); } }; wrapped._hoekOnce = true; return wrapped; }; exports.ignore = function () { }; exports.uniqueFilename = function (path, extension) { if (extension) { extension = extension[0] !== '.' ? '.' + extension : extension; } else { extension = ''; } path = Path.resolve(path); const name = [Date.now(), process.pid, Crypto.randomBytes(8).toString('hex')].join('-') + extension; return Path.join(path, name); }; exports.stringify = function (...args) { try { return JSON.stringify.apply(null, args); } catch (err) { return '[Cannot display object: ' + err.message + ']'; } }; exports.wait = function (timeout) { return new Promise((resolve) => setTimeout(resolve, timeout)); }; exports.block = function () { return new Promise(exports.ignore); };