From 4824f1c505d546cb9fc0dff3367c65e1160d8e0e Mon Sep 17 00:00:00 2001 From: "Mark S. Miller" Date: Thu, 25 Jan 2024 13:04:46 -0800 Subject: [PATCH] fix(marshal)!: compare strings by codepoint --- packages/marshal/index.js | 1 + packages/marshal/src/rankOrder.js | 43 +++++++++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/marshal/index.js b/packages/marshal/index.js index 3e9b3afc87..f8bc9ca22e 100644 --- a/packages/marshal/index.js +++ b/packages/marshal/index.js @@ -17,6 +17,7 @@ export { export { trivialComparator, + compareByCodePoints, assertRankSorted, compareRank, isRankSorted, diff --git a/packages/marshal/src/rankOrder.js b/packages/marshal/src/rankOrder.js index f8d8d9d95c..41d91cb04d 100644 --- a/packages/marshal/src/rankOrder.js +++ b/packages/marshal/src/rankOrder.js @@ -46,9 +46,46 @@ const { entries, fromEntries, setPrototypeOf, is } = Object; */ const sameValueZero = (x, y) => x === y || is(x, y); +/** + * @param {any} left + * @param {any} right + * @returns {RankComparison} + */ export const trivialComparator = (left, right) => // eslint-disable-next-line no-nested-ternary, @endo/restrict-comparison-operands left < right ? -1 : left === right ? 0 : 1; +harden(trivialComparator); + +// Apparently eslint confused about whether the function can ever exit +// without an explicit return. +// eslint-disable-next-line jsdoc/require-returns-check +/** + * @param {string} left + * @param {string} right + * @returns {RankComparison} + */ +export const compareByCodePoints = (left, right) => { + const leftIter = left[Symbol.iterator](); + const rightIter = right[Symbol.iterator](); + for (;;) { + const { value: leftChar } = leftIter.next(); + const { value: rightChar } = rightIter.next(); + if (leftChar === undefined && rightChar === undefined) { + return 0; + } else if (leftChar === undefined) { + // left is a prefix of right. + return -1; + } else if (rightChar === undefined) { + // right is a prefix of left. + return 1; + } + const leftCodepoint = /** @type {number} */ (leftChar.codePointAt(0)); + const rightCodepoint = /** @type {number} */ (rightChar.codePointAt(0)); + if (leftCodepoint < rightCodepoint) return -1; + if (leftCodepoint > rightCodepoint) return 1; + } +}; +harden(compareByCodePoints); /** * @typedef {Record} PassStyleRanksRecord @@ -140,8 +177,7 @@ export const makeComparatorKit = (compareRemotables = (_x, _y) => 0) => { return 0; } case 'boolean': - case 'bigint': - case 'string': { + case 'bigint': { // Within each of these passStyles, the rank ordering agrees with // JavaScript's relational operators `<` and `>`. if (left < right) { @@ -151,6 +187,9 @@ export const makeComparatorKit = (compareRemotables = (_x, _y) => 0) => { return 1; } } + case 'string': { + return compareByCodePoints(left, right); + } case 'symbol': { return comparator( nameForPassableSymbol(left),