jsprim.js (17071B)
1 /* 2 * lib/jsprim.js: utilities for primitive JavaScript types 3 */ 4 5 var mod_assert = require('assert-plus'); 6 var mod_util = require('util'); 7 8 var mod_extsprintf = require('extsprintf'); 9 var mod_verror = require('verror'); 10 var mod_jsonschema = require('json-schema'); 11 12 /* 13 * Public interface 14 */ 15 exports.deepCopy = deepCopy; 16 exports.deepEqual = deepEqual; 17 exports.isEmpty = isEmpty; 18 exports.hasKey = hasKey; 19 exports.forEachKey = forEachKey; 20 exports.pluck = pluck; 21 exports.flattenObject = flattenObject; 22 exports.flattenIter = flattenIter; 23 exports.validateJsonObject = validateJsonObjectJS; 24 exports.validateJsonObjectJS = validateJsonObjectJS; 25 exports.randElt = randElt; 26 exports.extraProperties = extraProperties; 27 exports.mergeObjects = mergeObjects; 28 29 exports.startsWith = startsWith; 30 exports.endsWith = endsWith; 31 32 exports.parseInteger = parseInteger; 33 34 exports.iso8601 = iso8601; 35 exports.rfc1123 = rfc1123; 36 exports.parseDateTime = parseDateTime; 37 38 exports.hrtimediff = hrtimeDiff; 39 exports.hrtimeDiff = hrtimeDiff; 40 exports.hrtimeAccum = hrtimeAccum; 41 exports.hrtimeAdd = hrtimeAdd; 42 exports.hrtimeNanosec = hrtimeNanosec; 43 exports.hrtimeMicrosec = hrtimeMicrosec; 44 exports.hrtimeMillisec = hrtimeMillisec; 45 46 47 /* 48 * Deep copy an acyclic *basic* Javascript object. This only handles basic 49 * scalars (strings, numbers, booleans) and arbitrarily deep arrays and objects 50 * containing these. This does *not* handle instances of other classes. 51 */ 52 function deepCopy(obj) 53 { 54 var ret, key; 55 var marker = '__deepCopy'; 56 57 if (obj && obj[marker]) 58 throw (new Error('attempted deep copy of cyclic object')); 59 60 if (obj && obj.constructor == Object) { 61 ret = {}; 62 obj[marker] = true; 63 64 for (key in obj) { 65 if (key == marker) 66 continue; 67 68 ret[key] = deepCopy(obj[key]); 69 } 70 71 delete (obj[marker]); 72 return (ret); 73 } 74 75 if (obj && obj.constructor == Array) { 76 ret = []; 77 obj[marker] = true; 78 79 for (key = 0; key < obj.length; key++) 80 ret.push(deepCopy(obj[key])); 81 82 delete (obj[marker]); 83 return (ret); 84 } 85 86 /* 87 * It must be a primitive type -- just return it. 88 */ 89 return (obj); 90 } 91 92 function deepEqual(obj1, obj2) 93 { 94 if (typeof (obj1) != typeof (obj2)) 95 return (false); 96 97 if (obj1 === null || obj2 === null || typeof (obj1) != 'object') 98 return (obj1 === obj2); 99 100 if (obj1.constructor != obj2.constructor) 101 return (false); 102 103 var k; 104 for (k in obj1) { 105 if (!obj2.hasOwnProperty(k)) 106 return (false); 107 108 if (!deepEqual(obj1[k], obj2[k])) 109 return (false); 110 } 111 112 for (k in obj2) { 113 if (!obj1.hasOwnProperty(k)) 114 return (false); 115 } 116 117 return (true); 118 } 119 120 function isEmpty(obj) 121 { 122 var key; 123 for (key in obj) 124 return (false); 125 return (true); 126 } 127 128 function hasKey(obj, key) 129 { 130 mod_assert.equal(typeof (key), 'string'); 131 return (Object.prototype.hasOwnProperty.call(obj, key)); 132 } 133 134 function forEachKey(obj, callback) 135 { 136 for (var key in obj) { 137 if (hasKey(obj, key)) { 138 callback(key, obj[key]); 139 } 140 } 141 } 142 143 function pluck(obj, key) 144 { 145 mod_assert.equal(typeof (key), 'string'); 146 return (pluckv(obj, key)); 147 } 148 149 function pluckv(obj, key) 150 { 151 if (obj === null || typeof (obj) !== 'object') 152 return (undefined); 153 154 if (obj.hasOwnProperty(key)) 155 return (obj[key]); 156 157 var i = key.indexOf('.'); 158 if (i == -1) 159 return (undefined); 160 161 var key1 = key.substr(0, i); 162 if (!obj.hasOwnProperty(key1)) 163 return (undefined); 164 165 return (pluckv(obj[key1], key.substr(i + 1))); 166 } 167 168 /* 169 * Invoke callback(row) for each entry in the array that would be returned by 170 * flattenObject(data, depth). This is just like flattenObject(data, 171 * depth).forEach(callback), except that the intermediate array is never 172 * created. 173 */ 174 function flattenIter(data, depth, callback) 175 { 176 doFlattenIter(data, depth, [], callback); 177 } 178 179 function doFlattenIter(data, depth, accum, callback) 180 { 181 var each; 182 var key; 183 184 if (depth === 0) { 185 each = accum.slice(0); 186 each.push(data); 187 callback(each); 188 return; 189 } 190 191 mod_assert.ok(data !== null); 192 mod_assert.equal(typeof (data), 'object'); 193 mod_assert.equal(typeof (depth), 'number'); 194 mod_assert.ok(depth >= 0); 195 196 for (key in data) { 197 each = accum.slice(0); 198 each.push(key); 199 doFlattenIter(data[key], depth - 1, each, callback); 200 } 201 } 202 203 function flattenObject(data, depth) 204 { 205 if (depth === 0) 206 return ([ data ]); 207 208 mod_assert.ok(data !== null); 209 mod_assert.equal(typeof (data), 'object'); 210 mod_assert.equal(typeof (depth), 'number'); 211 mod_assert.ok(depth >= 0); 212 213 var rv = []; 214 var key; 215 216 for (key in data) { 217 flattenObject(data[key], depth - 1).forEach(function (p) { 218 rv.push([ key ].concat(p)); 219 }); 220 } 221 222 return (rv); 223 } 224 225 function startsWith(str, prefix) 226 { 227 return (str.substr(0, prefix.length) == prefix); 228 } 229 230 function endsWith(str, suffix) 231 { 232 return (str.substr( 233 str.length - suffix.length, suffix.length) == suffix); 234 } 235 236 function iso8601(d) 237 { 238 if (typeof (d) == 'number') 239 d = new Date(d); 240 mod_assert.ok(d.constructor === Date); 241 return (mod_extsprintf.sprintf('%4d-%02d-%02dT%02d:%02d:%02d.%03dZ', 242 d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate(), 243 d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), 244 d.getUTCMilliseconds())); 245 } 246 247 var RFC1123_MONTHS = [ 248 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 249 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; 250 var RFC1123_DAYS = [ 251 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; 252 253 function rfc1123(date) { 254 return (mod_extsprintf.sprintf('%s, %02d %s %04d %02d:%02d:%02d GMT', 255 RFC1123_DAYS[date.getUTCDay()], date.getUTCDate(), 256 RFC1123_MONTHS[date.getUTCMonth()], date.getUTCFullYear(), 257 date.getUTCHours(), date.getUTCMinutes(), 258 date.getUTCSeconds())); 259 } 260 261 /* 262 * Parses a date expressed as a string, as either a number of milliseconds since 263 * the epoch or any string format that Date accepts, giving preference to the 264 * former where these two sets overlap (e.g., small numbers). 265 */ 266 function parseDateTime(str) 267 { 268 /* 269 * This is irritatingly implicit, but significantly more concise than 270 * alternatives. The "+str" will convert a string containing only a 271 * number directly to a Number, or NaN for other strings. Thus, if the 272 * conversion succeeds, we use it (this is the milliseconds-since-epoch 273 * case). Otherwise, we pass the string directly to the Date 274 * constructor to parse. 275 */ 276 var numeric = +str; 277 if (!isNaN(numeric)) { 278 return (new Date(numeric)); 279 } else { 280 return (new Date(str)); 281 } 282 } 283 284 285 /* 286 * Number.*_SAFE_INTEGER isn't present before node v0.12, so we hardcode 287 * the ES6 definitions here, while allowing for them to someday be higher. 288 */ 289 var MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991; 290 var MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991; 291 292 293 /* 294 * Default options for parseInteger(). 295 */ 296 var PI_DEFAULTS = { 297 base: 10, 298 allowSign: true, 299 allowPrefix: false, 300 allowTrailing: false, 301 allowImprecise: false, 302 trimWhitespace: false, 303 leadingZeroIsOctal: false 304 }; 305 306 var CP_0 = 0x30; 307 var CP_9 = 0x39; 308 309 var CP_A = 0x41; 310 var CP_B = 0x42; 311 var CP_O = 0x4f; 312 var CP_T = 0x54; 313 var CP_X = 0x58; 314 var CP_Z = 0x5a; 315 316 var CP_a = 0x61; 317 var CP_b = 0x62; 318 var CP_o = 0x6f; 319 var CP_t = 0x74; 320 var CP_x = 0x78; 321 var CP_z = 0x7a; 322 323 var PI_CONV_DEC = 0x30; 324 var PI_CONV_UC = 0x37; 325 var PI_CONV_LC = 0x57; 326 327 328 /* 329 * A stricter version of parseInt() that provides options for changing what 330 * is an acceptable string (for example, disallowing trailing characters). 331 */ 332 function parseInteger(str, uopts) 333 { 334 mod_assert.string(str, 'str'); 335 mod_assert.optionalObject(uopts, 'options'); 336 337 var baseOverride = false; 338 var options = PI_DEFAULTS; 339 340 if (uopts) { 341 baseOverride = hasKey(uopts, 'base'); 342 options = mergeObjects(options, uopts); 343 mod_assert.number(options.base, 'options.base'); 344 mod_assert.ok(options.base >= 2, 'options.base >= 2'); 345 mod_assert.ok(options.base <= 36, 'options.base <= 36'); 346 mod_assert.bool(options.allowSign, 'options.allowSign'); 347 mod_assert.bool(options.allowPrefix, 'options.allowPrefix'); 348 mod_assert.bool(options.allowTrailing, 349 'options.allowTrailing'); 350 mod_assert.bool(options.allowImprecise, 351 'options.allowImprecise'); 352 mod_assert.bool(options.trimWhitespace, 353 'options.trimWhitespace'); 354 mod_assert.bool(options.leadingZeroIsOctal, 355 'options.leadingZeroIsOctal'); 356 357 if (options.leadingZeroIsOctal) { 358 mod_assert.ok(!baseOverride, 359 '"base" and "leadingZeroIsOctal" are ' + 360 'mutually exclusive'); 361 } 362 } 363 364 var c; 365 var pbase = -1; 366 var base = options.base; 367 var start; 368 var mult = 1; 369 var value = 0; 370 var idx = 0; 371 var len = str.length; 372 373 /* Trim any whitespace on the left side. */ 374 if (options.trimWhitespace) { 375 while (idx < len && isSpace(str.charCodeAt(idx))) { 376 ++idx; 377 } 378 } 379 380 /* Check the number for a leading sign. */ 381 if (options.allowSign) { 382 if (str[idx] === '-') { 383 idx += 1; 384 mult = -1; 385 } else if (str[idx] === '+') { 386 idx += 1; 387 } 388 } 389 390 /* Parse the base-indicating prefix if there is one. */ 391 if (str[idx] === '0') { 392 if (options.allowPrefix) { 393 pbase = prefixToBase(str.charCodeAt(idx + 1)); 394 if (pbase !== -1 && (!baseOverride || pbase === base)) { 395 base = pbase; 396 idx += 2; 397 } 398 } 399 400 if (pbase === -1 && options.leadingZeroIsOctal) { 401 base = 8; 402 } 403 } 404 405 /* Parse the actual digits. */ 406 for (start = idx; idx < len; ++idx) { 407 c = translateDigit(str.charCodeAt(idx)); 408 if (c !== -1 && c < base) { 409 value *= base; 410 value += c; 411 } else { 412 break; 413 } 414 } 415 416 /* If we didn't parse any digits, we have an invalid number. */ 417 if (start === idx) { 418 return (new Error('invalid number: ' + JSON.stringify(str))); 419 } 420 421 /* Trim any whitespace on the right side. */ 422 if (options.trimWhitespace) { 423 while (idx < len && isSpace(str.charCodeAt(idx))) { 424 ++idx; 425 } 426 } 427 428 /* Check for trailing characters. */ 429 if (idx < len && !options.allowTrailing) { 430 return (new Error('trailing characters after number: ' + 431 JSON.stringify(str.slice(idx)))); 432 } 433 434 /* If our value is 0, we return now, to avoid returning -0. */ 435 if (value === 0) { 436 return (0); 437 } 438 439 /* Calculate our final value. */ 440 var result = value * mult; 441 442 /* 443 * If the string represents a value that cannot be precisely represented 444 * by JavaScript, then we want to check that: 445 * 446 * - We never increased the value past MAX_SAFE_INTEGER 447 * - We don't make the result negative and below MIN_SAFE_INTEGER 448 * 449 * Because we only ever increment the value during parsing, there's no 450 * chance of moving past MAX_SAFE_INTEGER and then dropping below it 451 * again, losing precision in the process. This means that we only need 452 * to do our checks here, at the end. 453 */ 454 if (!options.allowImprecise && 455 (value > MAX_SAFE_INTEGER || result < MIN_SAFE_INTEGER)) { 456 return (new Error('number is outside of the supported range: ' + 457 JSON.stringify(str.slice(start, idx)))); 458 } 459 460 return (result); 461 } 462 463 464 /* 465 * Interpret a character code as a base-36 digit. 466 */ 467 function translateDigit(d) 468 { 469 if (d >= CP_0 && d <= CP_9) { 470 /* '0' to '9' -> 0 to 9 */ 471 return (d - PI_CONV_DEC); 472 } else if (d >= CP_A && d <= CP_Z) { 473 /* 'A' - 'Z' -> 10 to 35 */ 474 return (d - PI_CONV_UC); 475 } else if (d >= CP_a && d <= CP_z) { 476 /* 'a' - 'z' -> 10 to 35 */ 477 return (d - PI_CONV_LC); 478 } else { 479 /* Invalid character code */ 480 return (-1); 481 } 482 } 483 484 485 /* 486 * Test if a value matches the ECMAScript definition of trimmable whitespace. 487 */ 488 function isSpace(c) 489 { 490 return (c === 0x20) || 491 (c >= 0x0009 && c <= 0x000d) || 492 (c === 0x00a0) || 493 (c === 0x1680) || 494 (c === 0x180e) || 495 (c >= 0x2000 && c <= 0x200a) || 496 (c === 0x2028) || 497 (c === 0x2029) || 498 (c === 0x202f) || 499 (c === 0x205f) || 500 (c === 0x3000) || 501 (c === 0xfeff); 502 } 503 504 505 /* 506 * Determine which base a character indicates (e.g., 'x' indicates hex). 507 */ 508 function prefixToBase(c) 509 { 510 if (c === CP_b || c === CP_B) { 511 /* 0b/0B (binary) */ 512 return (2); 513 } else if (c === CP_o || c === CP_O) { 514 /* 0o/0O (octal) */ 515 return (8); 516 } else if (c === CP_t || c === CP_T) { 517 /* 0t/0T (decimal) */ 518 return (10); 519 } else if (c === CP_x || c === CP_X) { 520 /* 0x/0X (hexadecimal) */ 521 return (16); 522 } else { 523 /* Not a meaningful character */ 524 return (-1); 525 } 526 } 527 528 529 function validateJsonObjectJS(schema, input) 530 { 531 var report = mod_jsonschema.validate(input, schema); 532 533 if (report.errors.length === 0) 534 return (null); 535 536 /* Currently, we only do anything useful with the first error. */ 537 var error = report.errors[0]; 538 539 /* The failed property is given by a URI with an irrelevant prefix. */ 540 var propname = error['property']; 541 var reason = error['message'].toLowerCase(); 542 var i, j; 543 544 /* 545 * There's at least one case where the property error message is 546 * confusing at best. We work around this here. 547 */ 548 if ((i = reason.indexOf('the property ')) != -1 && 549 (j = reason.indexOf(' is not defined in the schema and the ' + 550 'schema does not allow additional properties')) != -1) { 551 i += 'the property '.length; 552 if (propname === '') 553 propname = reason.substr(i, j - i); 554 else 555 propname = propname + '.' + reason.substr(i, j - i); 556 557 reason = 'unsupported property'; 558 } 559 560 var rv = new mod_verror.VError('property "%s": %s', propname, reason); 561 rv.jsv_details = error; 562 return (rv); 563 } 564 565 function randElt(arr) 566 { 567 mod_assert.ok(Array.isArray(arr) && arr.length > 0, 568 'randElt argument must be a non-empty array'); 569 570 return (arr[Math.floor(Math.random() * arr.length)]); 571 } 572 573 function assertHrtime(a) 574 { 575 mod_assert.ok(a[0] >= 0 && a[1] >= 0, 576 'negative numbers not allowed in hrtimes'); 577 mod_assert.ok(a[1] < 1e9, 'nanoseconds column overflow'); 578 } 579 580 /* 581 * Compute the time elapsed between hrtime readings A and B, where A is later 582 * than B. hrtime readings come from Node's process.hrtime(). There is no 583 * defined way to represent negative deltas, so it's illegal to diff B from A 584 * where the time denoted by B is later than the time denoted by A. If this 585 * becomes valuable, we can define a representation and extend the 586 * implementation to support it. 587 */ 588 function hrtimeDiff(a, b) 589 { 590 assertHrtime(a); 591 assertHrtime(b); 592 mod_assert.ok(a[0] > b[0] || (a[0] == b[0] && a[1] >= b[1]), 593 'negative differences not allowed'); 594 595 var rv = [ a[0] - b[0], 0 ]; 596 597 if (a[1] >= b[1]) { 598 rv[1] = a[1] - b[1]; 599 } else { 600 rv[0]--; 601 rv[1] = 1e9 - (b[1] - a[1]); 602 } 603 604 return (rv); 605 } 606 607 /* 608 * Convert a hrtime reading from the array format returned by Node's 609 * process.hrtime() into a scalar number of nanoseconds. 610 */ 611 function hrtimeNanosec(a) 612 { 613 assertHrtime(a); 614 615 return (Math.floor(a[0] * 1e9 + a[1])); 616 } 617 618 /* 619 * Convert a hrtime reading from the array format returned by Node's 620 * process.hrtime() into a scalar number of microseconds. 621 */ 622 function hrtimeMicrosec(a) 623 { 624 assertHrtime(a); 625 626 return (Math.floor(a[0] * 1e6 + a[1] / 1e3)); 627 } 628 629 /* 630 * Convert a hrtime reading from the array format returned by Node's 631 * process.hrtime() into a scalar number of milliseconds. 632 */ 633 function hrtimeMillisec(a) 634 { 635 assertHrtime(a); 636 637 return (Math.floor(a[0] * 1e3 + a[1] / 1e6)); 638 } 639 640 /* 641 * Add two hrtime readings A and B, overwriting A with the result of the 642 * addition. This function is useful for accumulating several hrtime intervals 643 * into a counter. Returns A. 644 */ 645 function hrtimeAccum(a, b) 646 { 647 assertHrtime(a); 648 assertHrtime(b); 649 650 /* 651 * Accumulate the nanosecond component. 652 */ 653 a[1] += b[1]; 654 if (a[1] >= 1e9) { 655 /* 656 * The nanosecond component overflowed, so carry to the seconds 657 * field. 658 */ 659 a[0]++; 660 a[1] -= 1e9; 661 } 662 663 /* 664 * Accumulate the seconds component. 665 */ 666 a[0] += b[0]; 667 668 return (a); 669 } 670 671 /* 672 * Add two hrtime readings A and B, returning the result as a new hrtime array. 673 * Does not modify either input argument. 674 */ 675 function hrtimeAdd(a, b) 676 { 677 assertHrtime(a); 678 679 var rv = [ a[0], a[1] ]; 680 681 return (hrtimeAccum(rv, b)); 682 } 683 684 685 /* 686 * Check an object for unexpected properties. Accepts the object to check, and 687 * an array of allowed property names (strings). Returns an array of key names 688 * that were found on the object, but did not appear in the list of allowed 689 * properties. If no properties were found, the returned array will be of 690 * zero length. 691 */ 692 function extraProperties(obj, allowed) 693 { 694 mod_assert.ok(typeof (obj) === 'object' && obj !== null, 695 'obj argument must be a non-null object'); 696 mod_assert.ok(Array.isArray(allowed), 697 'allowed argument must be an array of strings'); 698 for (var i = 0; i < allowed.length; i++) { 699 mod_assert.ok(typeof (allowed[i]) === 'string', 700 'allowed argument must be an array of strings'); 701 } 702 703 return (Object.keys(obj).filter(function (key) { 704 return (allowed.indexOf(key) === -1); 705 })); 706 } 707 708 /* 709 * Given three sets of properties "provided" (may be undefined), "overrides" 710 * (required), and "defaults" (may be undefined), construct an object containing 711 * the union of these sets with "overrides" overriding "provided", and 712 * "provided" overriding "defaults". None of the input objects are modified. 713 */ 714 function mergeObjects(provided, overrides, defaults) 715 { 716 var rv, k; 717 718 rv = {}; 719 if (defaults) { 720 for (k in defaults) 721 rv[k] = defaults[k]; 722 } 723 724 if (provided) { 725 for (k in provided) 726 rv[k] = provided[k]; 727 } 728 729 if (overrides) { 730 for (k in overrides) 731 rv[k] = overrides[k]; 732 } 733 734 return (rv); 735 }