From 52cc6a79d1474c20fca8437e0417704bc0230113 Mon Sep 17 00:00:00 2001 From: Dmitry Dutikov Date: Sun, 27 Feb 2022 09:40:59 +0500 Subject: [PATCH] Unicode escape chars parsing fixed --- README.md | 14 ++++++++++++-- index.cjs | 46 ++++++++++++++++++++++++++++---------------- index.d.ts | 1 + index.js | 46 ++++++++++++++++++++++++++++---------------- package.json | 2 +- test/objects.test.js | 7 +++++++ test/strings.test.js | 21 ++++++++++++++++++++ 7 files changed, 100 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index ce9db6f..f4017b9 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,16 @@ 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 @@ -96,6 +105,7 @@ Note: JSON22 cannot be used as drop in JSON object replacement due to `parse` an 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(text: string, options?: Json22ParseOptions): T; static stringify(value: any, options?: Json22StringifyOptions): string; } @@ -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 diff --git a/index.cjs b/index.cjs index 894b166..723c980 100644 --- a/index.cjs +++ b/index.cjs @@ -73,6 +73,7 @@ const State = { objectMemberValue: 'objectMemberValue', string: 'string', escape: 'escape', + escapeUnicode: 'escapeUnicode', array: 'array', arrayItem: 'arrayItem', literal: 'literal', @@ -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: @@ -250,17 +251,21 @@ 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; @@ -268,26 +273,33 @@ class JSON22 { 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: @@ -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: diff --git a/index.d.ts b/index.d.ts index 5fa2829..4ce8311 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,5 @@ export declare class JSON22 { + static readonly mimeType: string; static parse(text: string, options?: Json22ParseOptions): T; static stringify(value: any, options?: Json22StringifyOptions): string; } diff --git a/index.js b/index.js index f53c1f8..41f2f25 100644 --- a/index.js +++ b/index.js @@ -73,6 +73,7 @@ const State = { objectMemberValue: 'objectMemberValue', string: 'string', escape: 'escape', + escapeUnicode: 'escapeUnicode', array: 'array', arrayItem: 'arrayItem', literal: 'literal', @@ -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: @@ -250,17 +251,21 @@ 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; @@ -268,26 +273,33 @@ export class JSON22 { 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: @@ -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: diff --git a/package.json b/package.json index 2e17935..f5ba7ef 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/objects.test.js b/test/objects.test.js index 8bfbf40..a8c04f5 100644 --- a/test/objects.test.js +++ b/test/objects.test.js @@ -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 }')); }); diff --git a/test/strings.test.js b/test/strings.test.js index bfa97c4..ec73ba6 100644 --- a/test/strings.test.js +++ b/test/strings.test.js @@ -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"';