Skip to content

Commit d95a40f

Browse files
ndemiancclaude
andcommitted
fix(json-paste): measure the size guard in UTF-8 bytes, not code units
Addresses the PR #27 review (Copilot). The guard is named/documented as a byte cap (MAX_BYTES / maxBytes) but checked `trimmed.length` — UTF-16 code units. For non-ASCII pastes that undercounts the real size (a CJK char is 1 code unit but 3 UTF-8 bytes), so a payload up to ~3× the intended cap could slip past and get parsed/stringified on paste, defeating the ext-host-stall protection. Switched the check to Buffer.byteLength(trimmed, 'utf8') so the cap means what its name says, and added a regression test: a JSON blob whose .length is under the cap but whose byte size is over it is now blocked (and still beautifies when the cap is raised above its byte size). Verified: 13 tests pass; a 58-code-unit / 158-byte payload is now too-large at maxBytes=100, where a .length guard would have parsed it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4324812 commit d95a40f

2 files changed

Lines changed: 16 additions & 1 deletion

File tree

extensions/levelcode-npp-pack/jsonBeautify.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
// Don't parse a paste larger than this — JSON.parse + stringify on a huge blob would block the
1313
// extension host mid-paste. 5 MB is far past any hand-pasted JSON; bigger pastes just paste normally.
14+
// Measured in real UTF-8 BYTES (Buffer.byteLength), NOT String#length — a code-unit count undercounts
15+
// multibyte content (a CJK char is 1 code unit but 3 bytes), which would let a much bigger payload through.
1416
const MAX_BYTES = 5 * 1024 * 1024;
1517

1618
/**
@@ -36,7 +38,7 @@ function analyzePaste(text, opts) {
3638
// isn't JSON, and rules out bare scalars without parsing them.
3739
if (trimmed.length < 2 || (trimmed[0] !== '{' && trimmed[0] !== '[')) { return { beautify: false, reason: 'not-container' }; }
3840
const maxBytes = (opts && typeof opts.maxBytes === 'number') ? opts.maxBytes : MAX_BYTES;
39-
if (trimmed.length > maxBytes) { return { beautify: false, reason: 'too-large' }; }
41+
if (Buffer.byteLength(trimmed, 'utf8') > maxBytes) { return { beautify: false, reason: 'too-large' }; }
4042
let parsed;
4143
try { parsed = JSON.parse(trimmed); }
4244
catch { return { beautify: false, reason: 'invalid-json' }; }

extensions/levelcode-npp-pack/test/jsonBeautify.test.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,17 @@ test('honors the size guard (parses nothing past maxBytes)', () => {
8787
assert.ok(MAX_BYTES >= 1024 * 1024, 'default cap should be sizeable');
8888
});
8989

90+
test('size guard counts UTF-8 BYTES, not code units — multibyte payloads cannot slip past', () => {
91+
// A CJK char is 1 UTF-16 code unit but 3 UTF-8 bytes. Build JSON whose .length is UNDER the cap but
92+
// whose byte size is OVER it — a String#length guard would wrongly allow it through and parse it.
93+
const json = '{"k":"' + '実'.repeat(50) + '"}';
94+
assert.ok(json.length < 100, 'precondition: under cap by code units');
95+
assert.ok(Buffer.byteLength(json, 'utf8') > 100, 'precondition: over cap by bytes');
96+
const r = analyzePaste(json, { maxBytes: 100 });
97+
assert.strictEqual(r.beautify, false);
98+
assert.strictEqual(r.reason, 'too-large');
99+
// Sanity: the SAME payload beautifies when the cap is raised above its byte size.
100+
assert.strictEqual(analyzePaste(json, { maxBytes: 1000 }).beautify, true);
101+
});
102+
90103
console.log('\njsonBeautify.js: ' + n + ' tests passed.');

0 commit comments

Comments
 (0)