| /* |
| * lib/jsprim.js: utilities for primitive JavaScript types |
| */ |
| |
| var mod_assert = require('assert-plus'); |
| var mod_util = require('util'); |
| |
| var mod_extsprintf = require('extsprintf'); |
| var mod_verror = require('verror'); |
| var mod_jsonschema = require('json-schema'); |
| |
| /* |
| * Public interface |
| */ |
| exports.deepCopy = deepCopy; |
| exports.deepEqual = deepEqual; |
| exports.isEmpty = isEmpty; |
| exports.hasKey = hasKey; |
| exports.forEachKey = forEachKey; |
| exports.pluck = pluck; |
| exports.flattenObject = flattenObject; |
| exports.flattenIter = flattenIter; |
| exports.validateJsonObject = validateJsonObjectJS; |
| exports.validateJsonObjectJS = validateJsonObjectJS; |
| exports.randElt = randElt; |
| exports.extraProperties = extraProperties; |
| exports.mergeObjects = mergeObjects; |
| |
| exports.startsWith = startsWith; |
| exports.endsWith = endsWith; |
| |
| exports.parseInteger = parseInteger; |
| |
| exports.iso8601 = iso8601; |
| exports.rfc1123 = rfc1123; |
| exports.parseDateTime = parseDateTime; |
| |
| exports.hrtimediff = hrtimeDiff; |
| exports.hrtimeDiff = hrtimeDiff; |
| exports.hrtimeAccum = hrtimeAccum; |
| exports.hrtimeAdd = hrtimeAdd; |
| exports.hrtimeNanosec = hrtimeNanosec; |
| exports.hrtimeMicrosec = hrtimeMicrosec; |
| exports.hrtimeMillisec = hrtimeMillisec; |
| |
| |
| /* |
| * Deep copy an acyclic *basic* Javascript object. This only handles basic |
| * scalars (strings, numbers, booleans) and arbitrarily deep arrays and objects |
| * containing these. This does *not* handle instances of other classes. |
| */ |
| function deepCopy(obj) |
| { |
| var ret, key; |
| var marker = '__deepCopy'; |
| |
| if (obj && obj[marker]) |
| throw (new Error('attempted deep copy of cyclic object')); |
| |
| if (obj && obj.constructor == Object) { |
| ret = {}; |
| obj[marker] = true; |
| |
| for (key in obj) { |
| if (key == marker) |
| continue; |
| |
| ret[key] = deepCopy(obj[key]); |
| } |
| |
| delete (obj[marker]); |
| return (ret); |
| } |
| |
| if (obj && obj.constructor == Array) { |
| ret = []; |
| obj[marker] = true; |
| |
| for (key = 0; key < obj.length; key++) |
| ret.push(deepCopy(obj[key])); |
| |
| delete (obj[marker]); |
| return (ret); |
| } |
| |
| /* |
| * It must be a primitive type -- just return it. |
| */ |
| return (obj); |
| } |
| |
| function deepEqual(obj1, obj2) |
| { |
| if (typeof (obj1) != typeof (obj2)) |
| return (false); |
| |
| if (obj1 === null || obj2 === null || typeof (obj1) != 'object') |
| return (obj1 === obj2); |
| |
| if (obj1.constructor != obj2.constructor) |
| return (false); |
| |
| var k; |
| for (k in obj1) { |
| if (!obj2.hasOwnProperty(k)) |
| return (false); |
| |
| if (!deepEqual(obj1[k], obj2[k])) |
| return (false); |
| } |
| |
| for (k in obj2) { |
| if (!obj1.hasOwnProperty(k)) |
| return (false); |
| } |
| |
| return (true); |
| } |
| |
| function isEmpty(obj) |
| { |
| var key; |
| for (key in obj) |
| return (false); |
| return (true); |
| } |
| |
| function hasKey(obj, key) |
| { |
| mod_assert.equal(typeof (key), 'string'); |
| return (Object.prototype.hasOwnProperty.call(obj, key)); |
| } |
| |
| function forEachKey(obj, callback) |
| { |
| for (var key in obj) { |
| if (hasKey(obj, key)) { |
| callback(key, obj[key]); |
| } |
| } |
| } |
| |
| function pluck(obj, key) |
| { |
| mod_assert.equal(typeof (key), 'string'); |
| return (pluckv(obj, key)); |
| } |
| |
| function pluckv(obj, key) |
| { |
| if (obj === null || typeof (obj) !== 'object') |
| return (undefined); |
| |
| if (obj.hasOwnProperty(key)) |
| return (obj[key]); |
| |
| var i = key.indexOf('.'); |
| if (i == -1) |
| return (undefined); |
| |
| var key1 = key.substr(0, i); |
| if (!obj.hasOwnProperty(key1)) |
| return (undefined); |
| |
| return (pluckv(obj[key1], key.substr(i + 1))); |
| } |
| |
| /* |
| * Invoke callback(row) for each entry in the array that would be returned by |
| * flattenObject(data, depth). This is just like flattenObject(data, |
| * depth).forEach(callback), except that the intermediate array is never |
| * created. |
| */ |
| function flattenIter(data, depth, callback) |
| { |
| doFlattenIter(data, depth, [], callback); |
| } |
| |
| function doFlattenIter(data, depth, accum, callback) |
| { |
| var each; |
| var key; |
| |
| if (depth === 0) { |
| each = accum.slice(0); |
| each.push(data); |
| callback(each); |
| return; |
| } |
| |
| mod_assert.ok(data !== null); |
| mod_assert.equal(typeof (data), 'object'); |
| mod_assert.equal(typeof (depth), 'number'); |
| mod_assert.ok(depth >= 0); |
| |
| for (key in data) { |
| each = accum.slice(0); |
| each.push(key); |
| doFlattenIter(data[key], depth - 1, each, callback); |
| } |
| } |
| |
| function flattenObject(data, depth) |
| { |
| if (depth === 0) |
| return ([ data ]); |
| |
| mod_assert.ok(data !== null); |
| mod_assert.equal(typeof (data), 'object'); |
| mod_assert.equal(typeof (depth), 'number'); |
| mod_assert.ok(depth >= 0); |
| |
| var rv = []; |
| var key; |
| |
| for (key in data) { |
| flattenObject(data[key], depth - 1).forEach(function (p) { |
| rv.push([ key ].concat(p)); |
| }); |
| } |
| |
| return (rv); |
| } |
| |
| function startsWith(str, prefix) |
| { |
| return (str.substr(0, prefix.length) == prefix); |
| } |
| |
| function endsWith(str, suffix) |
| { |
| return (str.substr( |
| str.length - suffix.length, suffix.length) == suffix); |
| } |
| |
| function iso8601(d) |
| { |
| if (typeof (d) == 'number') |
| d = new Date(d); |
| mod_assert.ok(d.constructor === Date); |
| return (mod_extsprintf.sprintf('%4d-%02d-%02dT%02d:%02d:%02d.%03dZ', |
| d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), |
| d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), |
| d.getUTCMilliseconds())); |
| } |
| |
| var RFC1123_MONTHS = [ |
| 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', |
| 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; |
| var RFC1123_DAYS = [ |
| 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; |
| |
| function rfc1123(date) { |
| return (mod_extsprintf.sprintf('%s, %02d %s %04d %02d:%02d:%02d GMT', |
| RFC1123_DAYS[date.getUTCDay()], date.getUTCDate(), |
| RFC1123_MONTHS[date.getUTCMonth()], date.getUTCFullYear(), |
| date.getUTCHours(), date.getUTCMinutes(), |
| date.getUTCSeconds())); |
| } |
| |
| /* |
| * Parses a date expressed as a string, as either a number of milliseconds since |
| * the epoch or any string format that Date accepts, giving preference to the |
| * former where these two sets overlap (e.g., small numbers). |
| */ |
| function parseDateTime(str) |
| { |
| /* |
| * This is irritatingly implicit, but significantly more concise than |
| * alternatives. The "+str" will convert a string containing only a |
| * number directly to a Number, or NaN for other strings. Thus, if the |
| * conversion succeeds, we use it (this is the milliseconds-since-epoch |
| * case). Otherwise, we pass the string directly to the Date |
| * constructor to parse. |
| */ |
| var numeric = +str; |
| if (!isNaN(numeric)) { |
| return (new Date(numeric)); |
| } else { |
| return (new Date(str)); |
| } |
| } |
| |
| |
| /* |
| * Number.*_SAFE_INTEGER isn't present before node v0.12, so we hardcode |
| * the ES6 definitions here, while allowing for them to someday be higher. |
| */ |
| var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; |
| var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; |
| |
| |
| /* |
| * Default options for parseInteger(). |
| */ |
| var PI_DEFAULTS = { |
| base: 10, |
| allowSign: true, |
| allowPrefix: false, |
| allowTrailing: false, |
| allowImprecise: false, |
| trimWhitespace: false, |
| leadingZeroIsOctal: false |
| }; |
| |
| var CP_0 = 0x30; |
| var CP_9 = 0x39; |
| |
| var CP_A = 0x41; |
| var CP_B = 0x42; |
| var CP_O = 0x4f; |
| var CP_T = 0x54; |
| var CP_X = 0x58; |
| var CP_Z = 0x5a; |
| |
| var CP_a = 0x61; |
| var CP_b = 0x62; |
| var CP_o = 0x6f; |
| var CP_t = 0x74; |
| var CP_x = 0x78; |
| var CP_z = 0x7a; |
| |
| var PI_CONV_DEC = 0x30; |
| var PI_CONV_UC = 0x37; |
| var PI_CONV_LC = 0x57; |
| |
| |
| /* |
| * A stricter version of parseInt() that provides options for changing what |
| * is an acceptable string (for example, disallowing trailing characters). |
| */ |
| function parseInteger(str, uopts) |
| { |
| mod_assert.string(str, 'str'); |
| mod_assert.optionalObject(uopts, 'options'); |
| |
| var baseOverride = false; |
| var options = PI_DEFAULTS; |
| |
| if (uopts) { |
| baseOverride = hasKey(uopts, 'base'); |
| options = mergeObjects(options, uopts); |
| mod_assert.number(options.base, 'options.base'); |
| mod_assert.ok(options.base >= 2, 'options.base >= 2'); |
| mod_assert.ok(options.base <= 36, 'options.base <= 36'); |
| mod_assert.bool(options.allowSign, 'options.allowSign'); |
| mod_assert.bool(options.allowPrefix, 'options.allowPrefix'); |
| mod_assert.bool(options.allowTrailing, |
| 'options.allowTrailing'); |
| mod_assert.bool(options.allowImprecise, |
| 'options.allowImprecise'); |
| mod_assert.bool(options.trimWhitespace, |
| 'options.trimWhitespace'); |
| mod_assert.bool(options.leadingZeroIsOctal, |
| 'options.leadingZeroIsOctal'); |
| |
| if (options.leadingZeroIsOctal) { |
| mod_assert.ok(!baseOverride, |
| '"base" and "leadingZeroIsOctal" are ' + |
| 'mutually exclusive'); |
| } |
| } |
| |
| var c; |
| var pbase = -1; |
| var base = options.base; |
| var start; |
| var mult = 1; |
| var value = 0; |
| var idx = 0; |
| var len = str.length; |
| |
| /* Trim any whitespace on the left side. */ |
| if (options.trimWhitespace) { |
| while (idx < len && isSpace(str.charCodeAt(idx))) { |
| ++idx; |
| } |
| } |
| |
| /* Check the number for a leading sign. */ |
| if (options.allowSign) { |
| if (str[idx] === '-') { |
| idx += 1; |
| mult = -1; |
| } else if (str[idx] === '+') { |
| idx += 1; |
| } |
| } |
| |
| /* Parse the base-indicating prefix if there is one. */ |
| if (str[idx] === '0') { |
| if (options.allowPrefix) { |
| pbase = prefixToBase(str.charCodeAt(idx + 1)); |
| if (pbase !== -1 && (!baseOverride || pbase === base)) { |
| base = pbase; |
| idx += 2; |
| } |
| } |
| |
| if (pbase === -1 && options.leadingZeroIsOctal) { |
| base = 8; |
| } |
| } |
| |
| /* Parse the actual digits. */ |
| for (start = idx; idx < len; ++idx) { |
| c = translateDigit(str.charCodeAt(idx)); |
| if (c !== -1 && c < base) { |
| value *= base; |
| value += c; |
| } else { |
| break; |
| } |
| } |
| |
| /* If we didn't parse any digits, we have an invalid number. */ |
| if (start === idx) { |
| return (new Error('invalid number: ' + JSON.stringify(str))); |
| } |
| |
| /* Trim any whitespace on the right side. */ |
| if (options.trimWhitespace) { |
| while (idx < len && isSpace(str.charCodeAt(idx))) { |
| ++idx; |
| } |
| } |
| |
| /* Check for trailing characters. */ |
| if (idx < len && !options.allowTrailing) { |
| return (new Error('trailing characters after number: ' + |
| JSON.stringify(str.slice(idx)))); |
| } |
| |
| /* If our value is 0, we return now, to avoid returning -0. */ |
| if (value === 0) { |
| return (0); |
| } |
| |
| /* Calculate our final value. */ |
| var result = value * mult; |
| |
| /* |
| * If the string represents a value that cannot be precisely represented |
| * by JavaScript, then we want to check that: |
| * |
| * - We never increased the value past MAX_SAFE_INTEGER |
| * - We don't make the result negative and below MIN_SAFE_INTEGER |
| * |
| * Because we only ever increment the value during parsing, there's no |
| * chance of moving past MAX_SAFE_INTEGER and then dropping below it |
| * again, losing precision in the process. This means that we only need |
| * to do our checks here, at the end. |
| */ |
| if (!options.allowImprecise && |
| (value > MAX_SAFE_INTEGER || result < MIN_SAFE_INTEGER)) { |
| return (new Error('number is outside of the supported range: ' + |
| JSON.stringify(str.slice(start, idx)))); |
| } |
| |
| return (result); |
| } |
| |
| |
| /* |
| * Interpret a character code as a base-36 digit. |
| */ |
| function translateDigit(d) |
| { |
| if (d >= CP_0 && d <= CP_9) { |
| /* '0' to '9' -> 0 to 9 */ |
| return (d - PI_CONV_DEC); |
| } else if (d >= CP_A && d <= CP_Z) { |
| /* 'A' - 'Z' -> 10 to 35 */ |
| return (d - PI_CONV_UC); |
| } else if (d >= CP_a && d <= CP_z) { |
| /* 'a' - 'z' -> 10 to 35 */ |
| return (d - PI_CONV_LC); |
| } else { |
| /* Invalid character code */ |
| return (-1); |
| } |
| } |
| |
| |
| /* |
| * Test if a value matches the ECMAScript definition of trimmable whitespace. |
| */ |
| function isSpace(c) |
| { |
| return (c === 0x20) || |
| (c >= 0x0009 && c <= 0x000d) || |
| (c === 0x00a0) || |
| (c === 0x1680) || |
| (c === 0x180e) || |
| (c >= 0x2000 && c <= 0x200a) || |
| (c === 0x2028) || |
| (c === 0x2029) || |
| (c === 0x202f) || |
| (c === 0x205f) || |
| (c === 0x3000) || |
| (c === 0xfeff); |
| } |
| |
| |
| /* |
| * Determine which base a character indicates (e.g., 'x' indicates hex). |
| */ |
| function prefixToBase(c) |
| { |
| if (c === CP_b || c === CP_B) { |
| /* 0b/0B (binary) */ |
| return (2); |
| } else if (c === CP_o || c === CP_O) { |
| /* 0o/0O (octal) */ |
| return (8); |
| } else if (c === CP_t || c === CP_T) { |
| /* 0t/0T (decimal) */ |
| return (10); |
| } else if (c === CP_x || c === CP_X) { |
| /* 0x/0X (hexadecimal) */ |
| return (16); |
| } else { |
| /* Not a meaningful character */ |
| return (-1); |
| } |
| } |
| |
| |
| function validateJsonObjectJS(schema, input) |
| { |
| var report = mod_jsonschema.validate(input, schema); |
| |
| if (report.errors.length === 0) |
| return (null); |
| |
| /* Currently, we only do anything useful with the first error. */ |
| var error = report.errors[0]; |
| |
| /* The failed property is given by a URI with an irrelevant prefix. */ |
| var propname = error['property']; |
| var reason = error['message'].toLowerCase(); |
| var i, j; |
| |
| /* |
| * There's at least one case where the property error message is |
| * confusing at best. We work around this here. |
| */ |
| if ((i = reason.indexOf('the property ')) != -1 && |
| (j = reason.indexOf(' is not defined in the schema and the ' + |
| 'schema does not allow additional properties')) != -1) { |
| i += 'the property '.length; |
| if (propname === '') |
| propname = reason.substr(i, j - i); |
| else |
| propname = propname + '.' + reason.substr(i, j - i); |
| |
| reason = 'unsupported property'; |
| } |
| |
| var rv = new mod_verror.VError('property "%s": %s', propname, reason); |
| rv.jsv_details = error; |
| return (rv); |
| } |
| |
| function randElt(arr) |
| { |
| mod_assert.ok(Array.isArray(arr) && arr.length > 0, |
| 'randElt argument must be a non-empty array'); |
| |
| return (arr[Math.floor(Math.random() * arr.length)]); |
| } |
| |
| function assertHrtime(a) |
| { |
| mod_assert.ok(a[0] >= 0 && a[1] >= 0, |
| 'negative numbers not allowed in hrtimes'); |
| mod_assert.ok(a[1] < 1e9, 'nanoseconds column overflow'); |
| } |
| |
| /* |
| * Compute the time elapsed between hrtime readings A and B, where A is later |
| * than B. hrtime readings come from Node's process.hrtime(). There is no |
| * defined way to represent negative deltas, so it's illegal to diff B from A |
| * where the time denoted by B is later than the time denoted by A. If this |
| * becomes valuable, we can define a representation and extend the |
| * implementation to support it. |
| */ |
| function hrtimeDiff(a, b) |
| { |
| assertHrtime(a); |
| assertHrtime(b); |
| mod_assert.ok(a[0] > b[0] || (a[0] == b[0] && a[1] >= b[1]), |
| 'negative differences not allowed'); |
| |
| var rv = [ a[0] - b[0], 0 ]; |
| |
| if (a[1] >= b[1]) { |
| rv[1] = a[1] - b[1]; |
| } else { |
| rv[0]--; |
| rv[1] = 1e9 - (b[1] - a[1]); |
| } |
| |
| return (rv); |
| } |
| |
| /* |
| * Convert a hrtime reading from the array format returned by Node's |
| * process.hrtime() into a scalar number of nanoseconds. |
| */ |
| function hrtimeNanosec(a) |
| { |
| assertHrtime(a); |
| |
| return (Math.floor(a[0] * 1e9 + a[1])); |
| } |
| |
| /* |
| * Convert a hrtime reading from the array format returned by Node's |
| * process.hrtime() into a scalar number of microseconds. |
| */ |
| function hrtimeMicrosec(a) |
| { |
| assertHrtime(a); |
| |
| return (Math.floor(a[0] * 1e6 + a[1] / 1e3)); |
| } |
| |
| /* |
| * Convert a hrtime reading from the array format returned by Node's |
| * process.hrtime() into a scalar number of milliseconds. |
| */ |
| function hrtimeMillisec(a) |
| { |
| assertHrtime(a); |
| |
| return (Math.floor(a[0] * 1e3 + a[1] / 1e6)); |
| } |
| |
| /* |
| * Add two hrtime readings A and B, overwriting A with the result of the |
| * addition. This function is useful for accumulating several hrtime intervals |
| * into a counter. Returns A. |
| */ |
| function hrtimeAccum(a, b) |
| { |
| assertHrtime(a); |
| assertHrtime(b); |
| |
| /* |
| * Accumulate the nanosecond component. |
| */ |
| a[1] += b[1]; |
| if (a[1] >= 1e9) { |
| /* |
| * The nanosecond component overflowed, so carry to the seconds |
| * field. |
| */ |
| a[0]++; |
| a[1] -= 1e9; |
| } |
| |
| /* |
| * Accumulate the seconds component. |
| */ |
| a[0] += b[0]; |
| |
| return (a); |
| } |
| |
| /* |
| * Add two hrtime readings A and B, returning the result as a new hrtime array. |
| * Does not modify either input argument. |
| */ |
| function hrtimeAdd(a, b) |
| { |
| assertHrtime(a); |
| |
| var rv = [ a[0], a[1] ]; |
| |
| return (hrtimeAccum(rv, b)); |
| } |
| |
| |
| /* |
| * Check an object for unexpected properties. Accepts the object to check, and |
| * an array of allowed property names (strings). Returns an array of key names |
| * that were found on the object, but did not appear in the list of allowed |
| * properties. If no properties were found, the returned array will be of |
| * zero length. |
| */ |
| function extraProperties(obj, allowed) |
| { |
| mod_assert.ok(typeof (obj) === 'object' && obj !== null, |
| 'obj argument must be a non-null object'); |
| mod_assert.ok(Array.isArray(allowed), |
| 'allowed argument must be an array of strings'); |
| for (var i = 0; i < allowed.length; i++) { |
| mod_assert.ok(typeof (allowed[i]) === 'string', |
| 'allowed argument must be an array of strings'); |
| } |
| |
| return (Object.keys(obj).filter(function (key) { |
| return (allowed.indexOf(key) === -1); |
| })); |
| } |
| |
| /* |
| * Given three sets of properties "provided" (may be undefined), "overrides" |
| * (required), and "defaults" (may be undefined), construct an object containing |
| * the union of these sets with "overrides" overriding "provided", and |
| * "provided" overriding "defaults". None of the input objects are modified. |
| */ |
| function mergeObjects(provided, overrides, defaults) |
| { |
| var rv, k; |
| |
| rv = {}; |
| if (defaults) { |
| for (k in defaults) |
| rv[k] = defaults[k]; |
| } |
| |
| if (provided) { |
| for (k in provided) |
| rv[k] = provided[k]; |
| } |
| |
| if (overrides) { |
| for (k in overrides) |
| rv[k] = overrides[k]; |
| } |
| |
| return (rv); |
| } |