diff --git a/src/lib/isByteLength.js b/src/lib/isByteLength.js index 619d7f604..829420b72 100644 --- a/src/lib/isByteLength.js +++ b/src/lib/isByteLength.js @@ -1,5 +1,29 @@ import assertString from './util/assertString'; +function utf8ByteLength(str) { + let length = 0; + + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + + if (code <= 0x7F) { + length += 1; + } else if (code <= 0x7FF) { + length += 2; + } else if (code >= 0xD800 && code <= 0xDBFF && + i + 1 < str.length && + str.charCodeAt(i + 1) >= 0xDC00 && str.charCodeAt(i + 1) <= 0xDFFF) { + length += 4; + i += 1; + } else { + // UTF-8 encoders replace unpaired surrogates with U+FFFD (three bytes). + length += 3; + } + } + + return length; +} + /* eslint-disable prefer-rest-params */ export default function isByteLength(str, options) { assertString(str); @@ -12,6 +36,6 @@ export default function isByteLength(str, options) { min = arguments[1]; max = arguments[2]; } - const len = encodeURI(str).split(/%..|./).length - 1; + const len = utf8ByteLength(str); return len >= min && (typeof max === 'undefined' || len <= max); } diff --git a/test/validators.test.js b/test/validators.test.js index f489105dd..1645fdf96 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -5757,6 +5757,34 @@ describe('Validators', () => { }); }); + it('should handle unpaired UTF-16 surrogates without throwing', () => { + test({ + validator: 'isByteLength', + args: [{ min: 3, max: 3 }], + valid: ['\uD800', '\uDC00'], + }); + test({ + validator: 'isByteLength', + args: [{ max: 2 }], + invalid: ['\uD800', '\uDC00'], + }); + }); + + it('should count UTF-16 surrogate pairs as four UTF-8 bytes', () => { + test({ + validator: 'isByteLength', + args: [{ min: 4, max: 4 }], + valid: ['😀'], + }); + }); + + it('should reject emails containing unpaired UTF-16 surrogates without throwing', () => { + test({ + validator: 'isEmail', + invalid: ['\uD800@example.com', '\uDC00@example.com'], + }); + }); + it('should validate ULIDs', () => { test({ validator: 'isULID',