perf(tools): stop counting string code points once the bounds are decided

Strings without length bounds are not counted, and counting stops at the
larger bound, so a huge tool argument costs no more than its limit.
This commit is contained in:
Sameh Khamis committed 2026-10-14 15:05:53 -07:00
1 parent d74556a801
commit c12938cd05
2 files changed
+27 -6

No files matched your search

+14 -6
View File
@@ -106,12 +106,20 @@ export function validateValue(schema, value, path = 'arguments') {
fail('must be a string'); fail('must be a string');
break; break;
} }
let length = 0; if (schema.minLength != null || schema.maxLength != null) {
for (const character of value) length += 1; // Code points, as JSON Schema counts. Counting stops once both bounds
if (schema.minLength != null && length < schema.minLength) // are decided, so a huge argument costs no more than its limit.
fail(`must be at least ${schema.minLength} characters`); const limit = Math.max(
if (schema.maxLength != null && length > schema.maxLength) schema.minLength ?? 0,
fail(`must be at most ${schema.maxLength} characters`); 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 ( if (
schema.pattern !== undefined && schema.pattern !== undefined &&
!new RegExp(schema.pattern, 'u').test(value) !new RegExp(schema.pattern, 'u').test(value)
+13
View File
@@ -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); 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');
});