Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: near/assemblyscript-json
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: BetterStackHQ/assemblyscript-json
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Able to merge. These branches can be automatically merged.
  • 2 commits
  • 2 files changed
  • 1 contributor

Commits on Nov 12, 2024

  1. Fix JSON escaping, more tests

    gyfis committed Nov 12, 2024
    Copy the full SHA
    dce879b View commit details
  2. Fix the build

    gyfis committed Nov 12, 2024
    Copy the full SHA
    09590d7 View commit details
Showing with 108 additions and 19 deletions.
  1. +67 −17 assembly/JSON.ts
  2. +41 −2 assembly/__tests__/string_escape.spec.ts
84 changes: 67 additions & 17 deletions assembly/JSON.ts
Original file line number Diff line number Diff line change
@@ -94,6 +94,8 @@ namespace _JSON {

/** Parses a string or Uint8Array and returns a Json Value. */
export function parse<T = Uint8Array>(str: T): Value {
_JSON.decoder.handler.reset();

var arr: Uint8Array;
if (isString<T>(str)) {
arr = Buffer.fromString(<string>str);
@@ -174,25 +176,60 @@ export abstract class Value {
abstract stringify(): string;

/**
*
* @returns A AS string corresponding to the value.
*
* @returns A AS string corresponding to the value.
*/
toString(): string {
return this.stringify();
}
}
function isHexDigit(char: string): bool {
return ('0123456789abcdefABCDEF'.indexOf(char) >= 0);
}

function isEscapeSequence(str: string, index: i32): bool {
if (str.charAt(index) !== '\\') return false;
if (index + 1 >= str.length) return false;

const next = str.charAt(index + 1);
// Check for standard escapes
if ('"\\/bfnrt'.includes(next)) return true;

// Check for unicode escape
if (next === 'u' && index + 5 < str.length) {
// Verify next 4 chars are valid hex digits
for (let i = 0; i < 4; i++) {
if (!isHexDigit(str.charAt(index + 2 + i))) {
return false;
}
}
return true;
}

return false;
}

function escapeChar(char: string, index: i32, str: string): string {
// If this character is part of an existing escape sequence, return it as-is
if (isEscapeSequence(str, index)) {
return char;
}

function escapeChar(char: string): string {
const charCode = char.charCodeAt(0);
if (charCode < 32 || charCode === 0x7F) { // Include DEL character (0x7F)
// Handle all control characters with unicode escapes
switch (charCode) {
case 0x08: return "\\b"; // Backspace
case 0x09: return "\\t"; // Tab
case 0x0A: return "\\n"; // Line feed
case 0x0C: return "\\f"; // Form feed
case 0x0D: return "\\r"; // Carriage return
default: return "\\u" + charCode.toString(16).padStart(4, "0");
}
}
switch (charCode) {
case 0x22: return '\\"';
case 0x5C: return "\\\\";
case 0x08: return "\\b";
case 0x0A: return "\\n";
case 0x0D: return "\\r";
case 0x09: return "\\t";
case 0x0C: return "\\f";
case 0x0B: return "\\u000b";
case 0x22: return '\\"'; // Quote
case 0x5C: return "\\\\"; // Backslash
default: return char;
}
}
@@ -204,10 +241,23 @@ export class Str extends Value {
}

stringify(): string {
let escaped: string[] = new Array(this._str.length);
for (let i = 0; i < this._str.length; i++) {
const char = this._str.at(i);
escaped[i] = escapeChar(char);
let escaped: string[] = [];
let i = 0;
while (i < this._str.length) {
if (isEscapeSequence(this._str, i)) {
// Keep existing escape sequence
if (this._str.charAt(i + 1) === 'u') {
// Single unicode escape
escaped.push(this._str.substr(i, 6));
i += 6;
} else {
escaped.push(this._str.substr(i, 2));
i += 2;
}
} else {
escaped.push(escapeChar(this._str.charAt(i), i, this._str));
i++;
}
}
return `"${escaped.join('')}"`;
}
@@ -261,7 +311,7 @@ export class Null extends Value {
return "null";
}

valueOf(): null {
valueOf(): Null | null {
return null;
}
}
@@ -326,7 +376,7 @@ export class Obj extends Value {
for (let i: i32 = 0; i < keys.length; i++) {
const key = keys[i];
const value = this._obj.get(key);
// Currently must get the string value before interpolation
// Currently must get the string value before interpolation
// see: https://github.com/AssemblyScript/assemblyscript/issues/1944
const valStr = value.stringify();
objs[i] = `"${key}":${valStr}`;
43 changes: 41 additions & 2 deletions assembly/__tests__/string_escape.spec.ts
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ describe('Escaped characters', () => {
const strings = ['"', '\\', '"\\"', '\\"\\"'];
// Computed using javascript's JSON as implemented in mozilla firefox 90.0 (64-bit)
const expected = ["\"\\\"\"", "\"\\\\\"", "\"\\\"\\\\\\\"\"", "\"\\\\\\\"\\\\\\\"\""];

for(let i=0; i<strings.length; i++){
const jsonStr = new JSON.Str(strings[i]);
expect(jsonStr.stringify()).toBe(expected[i]);
@@ -41,4 +41,43 @@ describe('Escaped characters', () => {
expect(jsonStr.stringify()).toBe(expected[i]);
}
});
});

it('Does not escape ANSI escape sequences', () => {
const input = '{"message":"test message with \\u001b[31m color coding"}';
const parsed = JSON.parse(input);
expect(parsed.stringify()).toBe(input);
});

it('Maintains characters above 0xFFFF', () => {
const input = '{"emoji":"😀"}';
const parsed = JSON.parse(input);
expect(parsed.stringify()).toBe(input);
});

it('Does not crash when surrogate pair escape sequences', () => {
const input = '{"emoji":"\ud83d\ude00"}'; // Escaped grinning face emoji
const parsed = JSON.parse(input);
expect(parsed.stringify()).toBe(input);
});

it('Escapes control characters with unicode escape sequences', () => {
// Test some control characters that aren't special cases
const inputs = [
'{"control":"\\u0001"}',
'{"control":"\\u0002"}',
'{"control":"\\u0003"}',
'{"control":"\\u001f"}'
];
const expected = [
'{"control":"\\u0001"}',
'{"control":"\\u0002"}',
'{"control":"\\u0003"}',
'{"control":"\\u001f"}'
];

for(let i=0; i<inputs.length; i++) {
const parsed = JSON.parse(inputs[i]);
expect(parsed.stringify()).toBe(expected[i]);
}
});
});