'use strict'; // Load modules const Crypto = require('crypto'); const Path = require('path'); const Util = require('util'); const Escape = require('./escape'); // Declare internals const internals = {}; // Clone object or array exports.clone = function (obj, seen) { if (typeof obj !== 'object' || obj === null) { return obj; } seen = seen || new Map(); const lookup = seen.get(obj); if (lookup) { return lookup; } let newObj; let cloneDeep = false; if (!Array.isArray(obj)) { if (Buffer.isBuffer(obj)) { newObj = new Buffer(obj); } else if (obj instanceof Date) { newObj = new Date(obj.getTime()); } else if (obj instanceof RegExp) { newObj = new RegExp(obj); } else { const proto = Object.getPrototypeOf(obj); if (proto && proto.isImmutable) { newObj = obj; } else { newObj = Object.create(proto); cloneDeep = true; } } } else { newObj = []; cloneDeep = true; } seen.set(obj, newObj); if (cloneDeep) { const keys = Object.getOwnPropertyNames(obj); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const descriptor = Object.getOwnPropertyDescriptor(obj, key); if (descriptor && (descriptor.get || descriptor.set)) { Object.defineProperty(newObj, key, descriptor); } else { newObj[key] = exports.clone(obj[key], seen); } } } return newObj; }; // Merge all the properties of source into target, source wins in conflict, and by default null and undefined from source are applied /*eslint-disable */ exports.merge = function (target, source, isNullOverride /* = true */, isMergeArrays /* = true */) { /*eslint-enable */ 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 === false) { // isMergeArrays defaults to true 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 = Object.keys(source); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; if (key === '__proto__') { 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 !== false) { // Defaults to true target[key] = value; } } } return target; }; // Apply options to a copy of the defaults exports.applyToDefaults = function (defaults, options, isNullOverride) { 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 === true, false); }; // Clone an object except for the listed keys which are shallow copied exports.cloneWithShallow = function (source, keys) { if (!source || typeof source !== 'object') { return source; } const storage = internals.store(source, keys); // Move shallow copy items to storage const copy = exports.clone(source); // 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 = {}; for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const value = exports.reach(source, key); if (value !== undefined) { storage[key] = value; internals.reachSet(source, key, undefined); } } return storage; }; internals.restore = function (copy, source, storage) { const keys = Object.keys(storage); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; internals.reachSet(copy, key, storage[key]); internals.reachSet(source, key, storage[key]); } }; internals.reachSet = function (obj, key, value) { const path = 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; }; // Deep object or array comparison exports.deepEqual = function (obj, ref, options, seen) { options = options || { prototype: true }; const type = typeof obj; if (type !== typeof ref) { return false; } if (type !== 'object' || obj === null || ref === null) { if (obj === ref) { // Copied from Deep-eql, copyright(c) 2013 Jake Luer, jake@alogicalparadox.com, MIT Licensed, https://github.com/chaijs/deep-eql return obj !== 0 || 1 / obj === 1 / ref; // -0 / +0 } return obj !== obj && ref !== ref; // NaN } seen = seen || []; if (seen.indexOf(obj) !== -1) { return true; // If previous comparison failed, it would have stopped execution } seen.push(obj); if (Array.isArray(obj)) { if (!Array.isArray(ref)) { return false; } if (!options.part && obj.length !== ref.length) { return false; } for (let i = 0; i < obj.length; ++i) { if (options.part) { let found = false; for (let j = 0; j < ref.length; ++j) { if (exports.deepEqual(obj[i], ref[j], options)) { found = true; break; } } return found; } if (!exports.deepEqual(obj[i], ref[i], options)) { return false; } } return true; } if (Buffer.isBuffer(obj)) { if (!Buffer.isBuffer(ref)) { return false; } if (obj.length !== ref.length) { return false; } for (let i = 0; i < obj.length; ++i) { if (obj[i] !== ref[i]) { return false; } } return true; } if (obj instanceof Date) { return (ref instanceof Date && obj.getTime() === ref.getTime()); } if (obj instanceof RegExp) { return (ref instanceof RegExp && obj.toString() === ref.toString()); } if (options.prototype) { if (Object.getPrototypeOf(obj) !== Object.getPrototypeOf(ref)) { return false; } } const keys = Object.getOwnPropertyNames(obj); if (!options.part && keys.length !== Object.getOwnPropertyNames(ref).length) { return false; } for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const descriptor = Object.getOwnPropertyDescriptor(obj, key); if (descriptor.get) { if (!exports.deepEqual(descriptor, Object.getOwnPropertyDescriptor(ref, key), options, seen)) { return false; } } else if (!exports.deepEqual(obj[key], ref[key], options, seen)) { return false; } } return true; }; // Remove duplicate items from array exports.unique = (array, key) => { let result; if (key) { result = []; const index = new Set(); array.forEach((item) => { const identifier = item[key]; if (!index.has(identifier)) { index.add(identifier); result.push(item); } }); } else { result = Array.from(new Set(array)); } return result; }; // Convert array into object exports.mapToObject = function (array, key) { if (!array) { return null; } const obj = {}; for (let i = 0; i < array.length; ++i) { if (key) { if (array[i][key]) { obj[array[i][key]] = true; } } else { obj[array[i]] = true; } } return obj; }; // Find the common unique items in two arrays exports.intersect = function (array1, array2, justFirst) { if (!array1 || !array2) { return []; } const common = []; const hash = (Array.isArray(array1) ? exports.mapToObject(array1) : array1); const found = {}; for (let i = 0; i < array2.length; ++i) { if (hash[array2[i]] && !found[array2[i]]) { if (justFirst) { return array2[i]; } common.push(array2[i]); found[array2[i]] = true; } } return (justFirst ? null : common); }; // Test if the reference contains the values exports.contain = function (ref, values, options) { /* 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; values = Object.keys(values); } else { values = [].concat(values); } options = options || {}; // deep, once, only, part exports.assert(arguments.length >= 2, 'Insufficient arguments'); 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.hasOwnProperty('only'); const hasPart = options.hasOwnProperty('part'); compareFlags = { prototype: hasOnly ? options.only : hasPart ? !options.part : false, part: hasOnly ? !options.only : hasPart ? options.part : true }; } 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)) { for (let i = 0; i < ref.length; ++i) { let matched = false; for (let j = 0; j < values.length && matched === false; ++j) { matched = compare(values[j], ref[i], compareFlags) && j; } if (matched !== false) { ++matches[matched]; } else { misses = true; } } } else { const keys = Object.getOwnPropertyNames(ref); 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; } } } 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; } } if (options.only && misses) { 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 || typeof chain === 'undefined') { return obj; } options = options || {}; if (typeof options === 'string') { options = { separator: options }; } const path = chain.split(options.separator || '.'); let ref = obj; for (let i = 0; i < path.length; ++i) { let key = path[i]; if (key[0] === '-' && Array.isArray(ref)) { key = key.slice(1, key.length); key = ref.length - key; } 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.formatStack = function (stack) { const trace = []; for (let i = 0; i < stack.length; ++i) { const item = stack[i]; trace.push([item.getFileName(), item.getLineNumber(), item.getColumnNumber(), item.getFunctionName(), item.isConstructor()]); } return trace; }; exports.formatTrace = function (trace) { const display = []; for (let i = 0; i < trace.length; ++i) { const row = trace[i]; display.push((row[4] ? 'new ' : '') + row[3] + ' (' + row[0] + ':' + row[1] + ':' + row[2] + ')'); } return display; }; exports.callStack = function (slice) { // http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi const v8 = Error.prepareStackTrace; Error.prepareStackTrace = function (_, stack) { return stack; }; const capture = {}; Error.captureStackTrace(capture, this); // arguments.callee is not supported in strict mode so we use this and slice the trace of this off the result const stack = capture.stack; Error.prepareStackTrace = v8; const trace = exports.formatStack(stack); return trace.slice(1 + slice); }; exports.displayStack = function (slice) { const trace = exports.callStack(slice === undefined ? 1 : slice + 1); return exports.formatTrace(trace); }; exports.abortThrow = false; exports.abort = function (message, hideStack) { if (process.env.NODE_ENV === 'test' || exports.abortThrow === true) { throw new Error(message || 'Unknown error'); } let stack = ''; if (!hideStack) { stack = exports.displayStack(1).join('\n\t'); } console.log('ABORT: ' + message + '\n\t' + stack); process.exit(1); }; exports.assert = function (condition /*, msg1, msg2, msg3 */) { if (condition) { return; } if (arguments.length === 2 && arguments[1] instanceof Error) { throw arguments[1]; } let msgs = []; for (let i = 1; i < arguments.length; ++i) { if (arguments[i] !== '') { msgs.push(arguments[i]); // Avoids Array.slice arguments leak, allowing for V8 optimizations } } msgs = msgs.map((msg) => { return typeof msg === 'string' ? msg : msg instanceof Error ? msg.message : exports.stringify(msg); }); throw new Error(msgs.join(' ') || 'Unknown error'); }; exports.Timer = function () { this.ts = 0; this.reset(); }; exports.Timer.prototype.reset = function () { this.ts = Date.now(); }; exports.Timer.prototype.elapsed = function () { return Date.now() - this.ts; }; exports.Bench = function () { this.ts = 0; this.reset(); }; exports.Bench.prototype.reset = function () { this.ts = exports.Bench.now(); }; exports.Bench.prototype.elapsed = function () { return exports.Bench.now() - this.ts; }; exports.Bench.now = function () { 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, '\\$&'); }; // Base64url (RFC 4648) encode exports.base64urlEncode = function (value, encoding) { exports.assert(typeof value === 'string' || Buffer.isBuffer(value), 'value must be string or buffer'); const buf = (Buffer.isBuffer(value) ? value : new Buffer(value, encoding || 'binary')); return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, ''); }; // Base64url (RFC 4648) decode exports.base64urlDecode = function (value, encoding) { if (typeof value !== 'string') { return new Error('Value not a string'); } if (!/^[\w\-]*$/.test(value)) { return new Error('Invalid character'); } const buf = new Buffer(value, 'base64'); return (encoding === 'buffer' ? buf : buf.toString(encoding || 'binary')); }; // 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.escapeJavaScript = function (string) { return Escape.escapeJavaScript(string); }; exports.escapeJson = function (string) { return Escape.escapeJson(string); }; exports.nextTick = function (callback) { return function () { const args = arguments; process.nextTick(() => { callback.apply(null, args); }); }; }; exports.once = function (method) { if (method._hoekOnce) { return method; } let once = false; const wrapped = function () { if (!once) { once = true; method.apply(null, arguments); } }; wrapped._hoekOnce = true; return wrapped; }; exports.isInteger = Number.isSafeInteger; exports.ignore = function () { }; exports.inherits = Util.inherits; exports.format = Util.format; exports.transform = function (source, transform, options) { exports.assert(source === null || source === undefined || typeof source === 'object' || Array.isArray(source), 'Invalid source object: must be null, undefined, an object, or an array'); const separator = (typeof options === 'object' && options !== null) ? (options.separator || '.') : '.'; if (Array.isArray(source)) { const results = []; for (let i = 0; i < source.length; ++i) { results.push(exports.transform(source[i], transform, options)); } return results; } const result = {}; const keys = Object.keys(transform); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const path = key.split(separator); const sourcePath = transform[key]; exports.assert(typeof sourcePath === 'string', 'All mappings must be "." delineated strings'); let segment; let res = result; while (path.length > 1) { segment = path.shift(); if (!res[segment]) { res[segment] = {}; } res = res[segment]; } segment = path.shift(); res[segment] = exports.reach(source, sourcePath, options); } return result; }; 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 () { try { return JSON.stringify.apply(null, arguments); } catch (err) { return '[Cannot display object: ' + err.message + ']'; } }; exports.shallow = function (source) { const target = {}; const keys = Object.keys(source); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; target[key] = source[key]; } return target; };