707 lines
18 KiB
JavaScript
707 lines
18 KiB
JavaScript
|
'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);
|
||
|
};
|