Skip to content

Commit d7e524b

Browse files
committed
fix: 🐛 fix UTF8 encoding test
1 parent ecd297d commit d7e524b

File tree

2 files changed

+90
-3
lines changed

2 files changed

+90
-3
lines changed

src/json/JsonEncoder.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ export class JsonEncoder implements BinaryJsonEncoder, StreamingBinaryJsonEncode
109109
this.writeNumber(float);
110110
}
111111

112+
/**
113+
* Write UTF-8 string directly using Buffer to avoid writer.utf8() bugs
114+
* with buffer reallocation and stale offsets.
115+
*/
116+
private writeUtf8(str: string): void {
117+
const writer = this.writer;
118+
const buf = Buffer.from(str, 'utf-8');
119+
writer.ensureCapacity(buf.length);
120+
writer.uint8.set(buf, writer.x);
121+
writer.x += buf.length;
122+
}
123+
112124
public writeBin(buf: Uint8Array): void {
113125
const writer = this.writer;
114126
const length = buf.length;
@@ -146,7 +158,8 @@ export class JsonEncoder implements BinaryJsonEncoder, StreamingBinaryJsonEncode
146158
const length = str.length;
147159
writer.ensureCapacity(length * 4 + 2);
148160
if (length < 256) {
149-
let x = writer.x;
161+
const startX = writer.x;
162+
let x = startX;
150163
const uint8 = writer.uint8;
151164
uint8[x++] = 0x22; // "
152165
for (let i = 0; i < length; i++) {
@@ -158,15 +171,16 @@ export class JsonEncoder implements BinaryJsonEncoder, StreamingBinaryJsonEncode
158171
break;
159172
}
160173
if (code < 32 || code > 126) {
161-
writer.utf8(JSON.stringify(str));
174+
writer.x = startX;
175+
this.writeUtf8(JSON.stringify(str));
162176
return;
163177
} else uint8[x++] = code;
164178
}
165179
uint8[x++] = 0x22; // "
166180
writer.x = x;
167181
return;
168182
}
169-
writer.utf8(JSON.stringify(str));
183+
this.writeUtf8(JSON.stringify(str));
170184
}
171185

172186
public writeAsciiStr(str: string): void {

src/json/__tests__/JsonEncoder.spec.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,76 @@ describe('nested object', () => {
180180
});
181181
});
182182
});
183+
184+
describe('buffer reallocation stress tests', () => {
185+
test('strings with non-ASCII triggering fallback (reproduces writer.x bug)', () => {
186+
// This specifically tests the bug where writer.x is not reset before fallback
187+
// When a short string (<256) contains non-ASCII, it triggers writer.utf8()
188+
// but writer.x has already been incremented by writing the opening quote
189+
for (let round = 0; round < 50; round++) {
190+
const smallWriter = new Writer(64);
191+
const smallEncoder = new JsonEncoder(smallWriter);
192+
193+
for (let i = 0; i < 500; i++) {
194+
// Create strings < 256 chars with non-ASCII character to trigger fallback
195+
const asciiPart = 'a'.repeat(Math.floor(Math.random() * 200));
196+
const value = {foo: asciiPart + '\u0001' + asciiPart}; // control char triggers fallback
197+
const encoded = smallEncoder.encode(value);
198+
const json = Buffer.from(encoded).toString('utf-8');
199+
const decoded = JSON.parse(json);
200+
expect(decoded).toEqual(value);
201+
}
202+
}
203+
});
204+
205+
test('many iterations with long strings (reproduces writer.utf8 bug)', () => {
206+
// Run multiple test rounds to increase chance of hitting the bug
207+
for (let round = 0; round < 10; round++) {
208+
const smallWriter = new Writer(64);
209+
const smallEncoder = new JsonEncoder(smallWriter);
210+
211+
for (let i = 0; i < 1000; i++) {
212+
const value = {
213+
foo: 'a'.repeat(Math.round(32000 * Math.random()) + 10),
214+
};
215+
const encoded = smallEncoder.encode(value);
216+
const json = Buffer.from(encoded).toString('utf-8');
217+
const decoded = JSON.parse(json);
218+
expect(decoded).toEqual(value);
219+
}
220+
}
221+
});
222+
223+
test('repeated long strings >= 256 chars (reproduces writer.utf8 bug)', () => {
224+
// Run multiple test rounds to increase chance of hitting the bug
225+
for (let round = 0; round < 20; round++) {
226+
const smallWriter = new Writer(64);
227+
const smallEncoder = new JsonEncoder(smallWriter);
228+
229+
for (let i = 0; i < 100; i++) {
230+
const length = 256 + Math.floor(Math.random() * 10000);
231+
const value = {foo: 'a'.repeat(length)};
232+
const encoded = smallEncoder.encode(value);
233+
const json = Buffer.from(encoded).toString('utf-8');
234+
const decoded = JSON.parse(json);
235+
expect(decoded).toEqual(value);
236+
}
237+
}
238+
});
239+
240+
test('many short strings with buffer growth (reproduces writer.utf8 bug)', () => {
241+
// Run multiple test rounds to increase chance of hitting the bug
242+
for (let round = 0; round < 10; round++) {
243+
const smallWriter = new Writer(64);
244+
const smallEncoder = new JsonEncoder(smallWriter);
245+
246+
for (let i = 0; i < 1000; i++) {
247+
const value = {foo: 'test' + i};
248+
const encoded = smallEncoder.encode(value);
249+
const json = Buffer.from(encoded).toString('utf-8');
250+
const decoded = JSON.parse(json);
251+
expect(decoded).toEqual(value);
252+
}
253+
}
254+
});
255+
});

0 commit comments

Comments
 (0)