Skip to content

Commit

Permalink
Unicode escape chars parsing fixed
Browse files Browse the repository at this point in the history
  • Loading branch information
Dmitry Dutikov committed Feb 27, 2022
1 parent 079415f commit 52cc6a7
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 37 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,24 @@ import axios from 'axios';
import { Json22RequestInterceptor } from 'json22-axios';

axios.interceptors.request.use(Json22RequestInterceptor());
// interceptor allow you to send and receive JSON22

async function geServerDate() {
try {
const resp = await axios.get('/date');
return resp.data.date;
} catch (e) {
console.error(e);
}
return null;
}
```

## API
Note: JSON22 cannot be used as drop in JSON object replacement due to `parse` and `stringify` methods
arguments incompatibility. But you may not be worried in case you are using first arguments only.
```typescript
class JSON22 {
static readonly mimeType: string;
static parse<T>(text: string, options?: Json22ParseOptions): T;
static stringify(value: any, options?: Json22StringifyOptions): string;
}
Expand Down Expand Up @@ -150,7 +160,7 @@ This is the most significant addition. It's allow you to serialize and deseriali
Out of the box it works well with date values.
```javascript
const date = new Date('2022-01-07');
JSON.stringify(date); // => '"2022-01-07T00:00:00.000Z"'
JSON.stringify(date); // => "2022-01-07T00:00:00.000Z"
JSON22.stringify(date); // => Date(1641513600000)
```
```javascript
Expand Down
46 changes: 29 additions & 17 deletions index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const State = {
objectMemberValue: 'objectMemberValue',
string: 'string',
escape: 'escape',
escapeUnicode: 'escapeUnicode',
array: 'array',
arrayItem: 'arrayItem',
literal: 'literal',
Expand Down Expand Up @@ -192,7 +193,7 @@ class JSON22 {
// ignore
break;
case QUOTATION_MARK:
valueStack.push([]);
valueStack.push([{ start: i, end: i, type: 'string' }]);
stateStack.push(State.string);
break;
case BEGIN_OBJECT:
Expand Down Expand Up @@ -250,44 +251,55 @@ class JSON22 {
case (code < 0x20 ? c : undefined):
throw new Error(`Character ${c} at position ${i} is not allowed, try to escape it`);
case ESCAPE_CHAR:
const region = valueStack.top.pop();
valueStack.top.push(text.slice(region.start+1, region.end+1));
stateStack.push(State.escape);
break;
case QUOTATION_MARK:
const chars = valueStack.pop();
const value = chars.join('');
const r = valueStack.top.pop();
valueStack.top.push(text.slice(r.start+1, r.end+1));
const substrings = valueStack.pop();
const value = substrings.join('');
valueStack.push(value);
stateStack.pop(State.string);
stateStack.pop(); // out of value or objectMemberKey
break;
default:
valueStack.top.push(c);
valueStack.top[valueStack.top.length-1].end = i;
break;
}
break;
case State.escape:
switch (c) {
case (ESCAPE_SHORTENS.indexOf(c) > -1 ? c : undefined):
valueStack.top.push(ESCAPE_MAP[c]);
valueStack.top.push({ start: i, end: i, type: 'string' });
stateStack.pop();
break;
case 'u': // unicode escape like \u0020
valueStack.push(['unicode']);
valueStack.top.push({ start: i, end: i, type: 'escapeUnicode' });
stateStack.switch(State.escape, State.escapeUnicode);
break;
default:
throw new Error(`Unexpected character ${c} at ${i}, escape sequence expected`);
}
break;
case State.escapeUnicode:
switch(c) {
case (HEXADECIMALS.indexOf(c) > -1 ? c : undefined):
if (valueStack.top[0] === 'unicode') {
if (valueStack.top.length === 5) {
const top = valueStack.pop();
top.shift();
const code = parseInt(top.join(''), 16);
valueStack.top.push(String.fromCharCode(code));
stateStack.pop();
}
} else {
throw new Error(`Unexpected character ${c} at ${i}, unicode escape sequence expected`);
const region = valueStack.top[valueStack.top.length-1];
region.end = i;
if (region.end - region.start === 4) {
valueStack.top.pop();
const codeText = text.slice(region.start+1, region.end+1);
const code = parseInt(codeText, 16);
valueStack.top.push(String.fromCharCode(code));
valueStack.top.push({ start: i, end: i, type: 'string' });
stateStack.pop(State.escapeUnicode);
}
break;
default:
throw new Error(`Unexpected character ${c} at ${i}, escape sequence expected`);
throw new Error(`Unexpected character ${c} at ${i}, unicode escape sequence expected`);
}
break;
case State.object:
Expand All @@ -309,7 +321,7 @@ class JSON22 {
// ignore
break;
case QUOTATION_MARK:
valueStack.push([]);
valueStack.push([{ start: i, end: i, type: 'string' }]);
stateStack.push(State.string);
break;
case END_OBJECT:
Expand Down
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export declare class JSON22 {
static readonly mimeType: string;
static parse<T>(text: string, options?: Json22ParseOptions): T;
static stringify(value: any, options?: Json22StringifyOptions): string;
}
Expand Down
46 changes: 29 additions & 17 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ const State = {
objectMemberValue: 'objectMemberValue',
string: 'string',
escape: 'escape',
escapeUnicode: 'escapeUnicode',
array: 'array',
arrayItem: 'arrayItem',
literal: 'literal',
Expand Down Expand Up @@ -192,7 +193,7 @@ export class JSON22 {
// ignore
break;
case QUOTATION_MARK:
valueStack.push([]);
valueStack.push([{ start: i, end: i, type: 'string' }]);
stateStack.push(State.string);
break;
case BEGIN_OBJECT:
Expand Down Expand Up @@ -250,44 +251,55 @@ export class JSON22 {
case (code < 0x20 ? c : undefined):
throw new Error(`Character ${c} at position ${i} is not allowed, try to escape it`);
case ESCAPE_CHAR:
const region = valueStack.top.pop();
valueStack.top.push(text.slice(region.start+1, region.end+1));
stateStack.push(State.escape);
break;
case QUOTATION_MARK:
const chars = valueStack.pop();
const value = chars.join('');
const r = valueStack.top.pop();
valueStack.top.push(text.slice(r.start+1, r.end+1));
const substrings = valueStack.pop();
const value = substrings.join('');
valueStack.push(value);
stateStack.pop(State.string);
stateStack.pop(); // out of value or objectMemberKey
break;
default:
valueStack.top.push(c);
valueStack.top[valueStack.top.length-1].end = i;
break;
}
break;
case State.escape:
switch (c) {
case (ESCAPE_SHORTENS.indexOf(c) > -1 ? c : undefined):
valueStack.top.push(ESCAPE_MAP[c]);
valueStack.top.push({ start: i, end: i, type: 'string' });
stateStack.pop();
break;
case 'u': // unicode escape like \u0020
valueStack.push(['unicode']);
valueStack.top.push({ start: i, end: i, type: 'escapeUnicode' });
stateStack.switch(State.escape, State.escapeUnicode);
break;
default:
throw new Error(`Unexpected character ${c} at ${i}, escape sequence expected`);
}
break;
case State.escapeUnicode:
switch(c) {
case (HEXADECIMALS.indexOf(c) > -1 ? c : undefined):
if (valueStack.top[0] === 'unicode') {
if (valueStack.top.length === 5) {
const top = valueStack.pop();
top.shift();
const code = parseInt(top.join(''), 16);
valueStack.top.push(String.fromCharCode(code));
stateStack.pop();
}
} else {
throw new Error(`Unexpected character ${c} at ${i}, unicode escape sequence expected`);
const region = valueStack.top[valueStack.top.length-1];
region.end = i;
if (region.end - region.start === 4) {
valueStack.top.pop();
const codeText = text.slice(region.start+1, region.end+1);
const code = parseInt(codeText, 16);
valueStack.top.push(String.fromCharCode(code));
valueStack.top.push({ start: i, end: i, type: 'string' });
stateStack.pop(State.escapeUnicode);
}
break;
default:
throw new Error(`Unexpected character ${c} at ${i}, escape sequence expected`);
throw new Error(`Unexpected character ${c} at ${i}, unicode escape sequence expected`);
}
break;
case State.object:
Expand All @@ -309,7 +321,7 @@ export class JSON22 {
// ignore
break;
case QUOTATION_MARK:
valueStack.push([]);
valueStack.push([{ start: i, end: i, type: 'string' }]);
stateStack.push(State.string);
break;
case END_OBJECT:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "json22",
"version": "0.0.7",
"version": "0.0.8",
"description": "JSON superset with an ability to deal with classes and extended support for number values",
"author": {
"email": "dancecoder@gmail.com",
Expand Down
7 changes: 7 additions & 0 deletions test/objects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ suite('Object values parsing tests', () => {
assert.deepEqual(parsed, o);
});

test('parsing object with escaped chars in a key', () => {
const o = {"\u0000":1234,"\u0001":"abcd","\u0002":true,"\u0003":null,"last":false};
const json = '{"\\u0000":1234,"\\u0001":"abcd","\\u0002":true,"\\u0003":null,"last":false}';
const parsed = JSON22.parse(json);
assert.deepEqual(parsed, o);
});

test('check for incorrect char after entry value', () => {
assert.throws(() => JSON22.parse('{ "k": "v" Z }'));
});
Expand Down
21 changes: 21 additions & 0 deletions test/strings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ suite('String values parsing tests', () => {
assert.equal(parsed, s);
});

test('parsing escape only string', () => {
const s = '\t\t\t\t\t';
const json = '"\\t\\t\\t\\t\\t"';
const parsed = JSON22.parse(json);
assert.equal(parsed, s);
});

test('parsing unicode escape sequences', () => {
const s = 'key:\u0030value';
const json = '"key:\\u0030value"';
const parsed = JSON22.parse(json);
assert.equal(parsed, s);
});

test('parsing unicode escape only string', () => {
const s = '\u0030\u0030\u0030\u0030\u0030';
const json = '"\\u0030\\u0030\\u0030\\u0030\\u0030"';
const parsed = JSON22.parse(json);
assert.equal(parsed, s);
});

test('check for unsupported characters', () => {
// NOTE: characters with code < 0x20 are not allowed (be JSON spec)
const json = '"\u0019 not allowed"';
Expand Down

0 comments on commit 52cc6a7

Please sign in to comment.