You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

735 lines
17 KiB

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