|
| 1 | +import { getEnvironmentOption } from '@endo/env-options'; |
| 2 | +import { Fail } from '@endo/errors'; |
| 3 | + |
| 4 | +// @ts-expect-error TS builtin `String` type does not yet |
| 5 | +// know about`isWellFormed` |
| 6 | +const hasWellFormedStringMethod = !!String.prototype.isWellFormed; |
| 7 | + |
| 8 | +/** |
| 9 | + * Is the argument a well-formed string? |
| 10 | + * |
| 11 | + * Unfortunately, the |
| 12 | + * [standard built-in `String.prototype.isWellFormed`](https://github.com/tc39/proposal-is-usv-string) |
| 13 | + * does a ToString on its input, causing it to judge non-strings to be |
| 14 | + * well-formed strings if they coerce to a well-formed strings. This |
| 15 | + * recapitulates the mistake in having the global `isNaN` coerce its inputs, |
| 16 | + * causing it to judge non-string to be NaN if they coerce to NaN. |
| 17 | + * |
| 18 | + * This `isWellFormedString` function only judges well-formed strings to be |
| 19 | + * well-formed strings. For all non-strings it returns false. |
| 20 | + * |
| 21 | + * @param {unknown} str |
| 22 | + * @returns {str is string} |
| 23 | + */ |
| 24 | +export const isWellFormedString = hasWellFormedStringMethod |
| 25 | + ? // @ts-expect-error TS does not yet know about `isWellFormed` |
| 26 | + str => typeof str === 'string' && str.isWellFormed() |
| 27 | + : str => { |
| 28 | + if (typeof str !== 'string') { |
| 29 | + return false; |
| 30 | + } |
| 31 | + for (const ch of str) { |
| 32 | + // The string iterator iterates by Unicode code point, not |
| 33 | + // UTF16 code unit. But if it encounters an unpaired surrogate, |
| 34 | + // it will produce it. |
| 35 | + const cp = /** @type {number} */ (ch.codePointAt(0)); |
| 36 | + if (cp >= 0xd800 && cp <= 0xdfff) { |
| 37 | + // All surrogates are in this range. The string iterator only |
| 38 | + // produces a character in this range for unpaired surrogates, |
| 39 | + // which only happens if the string is not well-formed. |
| 40 | + return false; |
| 41 | + } |
| 42 | + } |
| 43 | + return true; |
| 44 | + }; |
| 45 | +harden(isWellFormedString); |
| 46 | + |
| 47 | +/** |
| 48 | + * Returns normally when `isWellFormedString(str)` would return true. |
| 49 | + * Throws a diagnostic error when `isWellFormedString(str)` would return false. |
| 50 | + * |
| 51 | + * @param {unknown} str |
| 52 | + * @returns {asserts str is string} |
| 53 | + */ |
| 54 | +export const assertWellFormedString = str => { |
| 55 | + isWellFormedString(str) || Fail`Expected well-formed unicode string: ${str}`; |
| 56 | +}; |
| 57 | +harden(assertWellFormedString); |
| 58 | + |
| 59 | +const ONLY_WELL_FORMED_STRINGS_PASSABLE = |
| 60 | + getEnvironmentOption('ONLY_WELL_FORMED_STRINGS_PASSABLE', 'disabled', [ |
| 61 | + 'enabled', |
| 62 | + ]) === 'enabled'; |
| 63 | + |
| 64 | +/** |
| 65 | + * For now, |
| 66 | + * if `ONLY_WELL_FORMED_STRINGS_PASSABLE` environment option is `'enabled'`, |
| 67 | + * then `assertPassableString` is the same as `assertWellFormedString`. |
| 68 | + * Otherwise `assertPassableString` just asserts that `str` is a string. |
| 69 | + * |
| 70 | + * Currently, `ONLY_WELL_FORMED_STRINGS_PASSABLE` defaults to `'disabled'` |
| 71 | + * because we do not yet know the performance impact. Later, if we decide we |
| 72 | + * can afford it, we'll first change the default to `'enabled'` and ultimately |
| 73 | + * remove the switch altogether. Be prepared for these changes. |
| 74 | + * |
| 75 | + * TODO once the switch is removed, simplify `assertPassableString` to |
| 76 | + * simply be `assertWellFormedString`. |
| 77 | + * |
| 78 | + * TODO update https://github.com/Agoric/agoric-sdk/blob/master/docs/env.md |
| 79 | + * which is unfortunately in the wrong repo to be updated in the same change. |
| 80 | + * |
| 81 | + * @param { unknown } str |
| 82 | + * @returns {asserts str is string } |
| 83 | + */ |
| 84 | +export const assertPassableString = str => { |
| 85 | + typeof str === 'string' || Fail`Expected string ${str}`; |
| 86 | + !ONLY_WELL_FORMED_STRINGS_PASSABLE || assertWellFormedString(str); |
| 87 | +}; |
| 88 | +harden(assertPassableString); |
0 commit comments