diff --git a/src/tools/schema.js b/src/tools/schema.js index b6e8e2fd0..f857825a1 100644 --- a/src/tools/schema.js +++ b/src/tools/schema.js @@ -106,12 +106,20 @@ export function validateValue(schema, value, path = 'arguments') { fail('must be a string'); break; } - let length = 0; - for (const character of value) length += 1; - if (schema.minLength != null && length < schema.minLength) - fail(`must be at least ${schema.minLength} characters`); - if (schema.maxLength != null && length > schema.maxLength) - fail(`must be at most ${schema.maxLength} characters`); + if (schema.minLength != null || schema.maxLength != null) { + // Code points, as JSON Schema counts. Counting stops once both bounds + // are decided, so a huge argument costs no more than its limit. + const limit = Math.max( + schema.minLength ?? 0, + schema.maxLength != null ? schema.maxLength + 1 : 0, + ); + let length = 0; + for (const _ of value) if (++length >= limit) break; + if (schema.minLength != null && length < schema.minLength) + fail(`must be at least ${schema.minLength} characters`); + if (schema.maxLength != null && length > schema.maxLength) + fail(`must be at most ${schema.maxLength} characters`); + } if ( schema.pattern !== undefined && !new RegExp(schema.pattern, 'u').test(value) diff --git a/src/tools/tool-schema-codepoints.test.mjs b/src/tools/tool-schema-codepoints.test.mjs index c379b77e9..27af78fa3 100644 --- a/src/tools/tool-schema-codepoints.test.mjs +++ b/src/tools/tool-schema-codepoints.test.mjs @@ -18,3 +18,16 @@ test('tool string schemas count Unicode characters rather than UTF-16 units', () ); assert.equal(validateValue({ type: 'string', minLength: 2 }, '𠮷').length, 1); }); + +test('tool string bounds hold at their edges and on huge arguments', () => { + const string = (bounds) => ({ type: 'string', ...bounds }); + assert.deepEqual(validateValue(string({ maxLength: 3 }), '😀😀😀'), []); + assert.equal(validateValue(string({ maxLength: 3 }), '😀😀😀😀').length, 1); + assert.deepEqual(validateValue(string({ minLength: 3 }), '😀😀😀'), []); + assert.deepEqual(validateValue(string({}), 'x'.repeat(1e6)), []); + // Counting all of it takes roughly 0.7 s; stopping at the limit takes none. + const huge = 'x'.repeat(1e8); + const started = performance.now(); + assert.equal(validateValue(string({ maxLength: 60 }), huge).length, 1); + assert.ok(performance.now() - started < 200, 'stops counting past the limit'); +});