diff --git a/assembly/decoder.ts b/assembly/decoder.ts index 62db96c..78e7c18 100644 --- a/assembly/decoder.ts +++ b/assembly/decoder.ts @@ -72,6 +72,10 @@ export class ThrowingJSONHandler extends JSONHandler { const TRUE_STR = "true"; const FALSE_STR = "false"; const NULL_STR = "null"; +let CHAR_0 = "0".charCodeAt(0); +let CHAR_9 = "9".charCodeAt(0); +let CHAR_A = "A".charCodeAt(0); +let CHAR_A_LOWER = "a".charCodeAt(0); export class JSONDecoder { @@ -178,28 +182,79 @@ export class JSONDecoder { private readString(): string { assert(this.readChar() == '"'.charCodeAt(0), "Expected double-quoted string"); let savedIndex = this.readIndex; + let stringParts: Array = new Array(); for (;;) { let byte = this.readChar(); assert(byte >= 0x20, "Unexpected control character"); // TODO: Make sure unicode handled properly if (byte == '"'.charCodeAt(0)) { - return String.fromUTF8(this.buffer.buffer.data + savedIndex, this.readIndex - savedIndex - 1); + stringParts.push( + String.fromUTF8(this.buffer.buffer.data + savedIndex, this.readIndex - savedIndex - 1)); + return stringParts.join(""); } if (byte == "\\".charCodeAt(0)) { - // TODO: Decode string properly - let skipCount = 1; - if (this.peekChar() == "u".charCodeAt(0)) { - skipCount += 4; - } - for (; skipCount > 0; skipCount--) { - this.readChar(); + if (this.readIndex > savedIndex + 1) { + stringParts.push( + String.fromUTF8(this.buffer.buffer.data + savedIndex, this.readIndex - savedIndex - 1)); } + stringParts.push(this.readEscapedChar()); + savedIndex = this.readIndex; } } // Should never happen return ""; } + private readEscapedChar(): string { + let byte = this.readChar(); + // TODO: Use lookup table for anything except \u + if (byte == '"'.charCodeAt(0)) { + return '"'; + } + if (byte == "\\".charCodeAt(0)) { + return "\\"; + } + if (byte == "/".charCodeAt(0)) { + return "/"; + } + if (byte == "b".charCodeAt(0)) { + return "\b"; + } + if (byte == "n".charCodeAt(0)) { + return "\n"; + } + if (byte == "r".charCodeAt(0)) { + return "\r"; + } + if (byte == "t".charCodeAt(0)) { + return "\t"; + } + if (byte == "u".charCodeAt(0)) { + let d1 = this.readHexDigit(); + let d2 = this.readHexDigit(); + let d3 = this.readHexDigit(); + let d4 = this.readHexDigit(); + let charCode = d1 * 0x1000 + d2 * 0x100 + d3 * 0x10 + d4; + return String.fromCodePoint(charCode); + } + assert(false, "Unexpected escaped character: " + String.fromCharCode(byte)); + return ""; + } + + private readHexDigit(): i32 { + let byte = this.readChar(); + let digit = byte - CHAR_0; + if (digit > 9) { + digit = byte - CHAR_A + 10; + if (digit < 10 || digit > 15) { + digit = byte - CHAR_A_LOWER + 10; + } + } + let arr: Array = [byte, digit]; + assert(digit >= 0 && digit < 16, "Unexpected \\u digit"); + return digit; + } + private parseNumber(): bool { // TODO: Parse floats let number: i32 = 0; @@ -209,10 +264,10 @@ export class JSONDecoder { this.readChar(); } let digits = 0; - while ("0".charCodeAt(0) <= this.peekChar() && this.peekChar() <= "9".charCodeAt(0) ) { + while (CHAR_0 <= this.peekChar() && this.peekChar() <= CHAR_9 ) { let byte = this.readChar(); number *= 10; - number += byte - "0".charCodeAt(0); + number += byte - CHAR_0; digits++; } if (digits > 0) { diff --git a/tests/assembly/decoder.spec.as.ts b/tests/assembly/decoder.spec.as.ts index 522ed66..5a0d221 100644 --- a/tests/assembly/decoder.spec.as.ts +++ b/tests/assembly/decoder.spec.as.ts @@ -67,9 +67,33 @@ class JSONTestHandler extends JSONHandler { } private writeString(str: string): void { - // TODO: Implement encoding this.write('"'); - this.write(str); + let savedIndex = 0; + for (let i = 0; i < str.length; i++) { + let char = str.charCodeAt(i); + let needsEscaping = char < 0x20 || char == '"'.charCodeAt(0) || char == '\\'.charCodeAt(0); + if (needsEscaping) { + this.write(str.substring(savedIndex, i)); + savedIndex = i + 1; + if (char == '"'.charCodeAt(0)) { + this.write('\\"'); + } else if (char == "\\".charCodeAt(0)) { + this.write("\\\\"); + } else if (char == "\b".charCodeAt(0)) { + this.write("\\b"); + } else if (char == "\n".charCodeAt(0)) { + this.write("\\n"); + } else if (char == "\r".charCodeAt(0)) { + this.write("\\r"); + } else if (char == "\t".charCodeAt(0)) { + this.write("\\t"); + } else { + // TODO: Implement encoding for other contol characters + assert(false, "Unsupported control chracter"); + } + } + } + this.write(str.substring(savedIndex, str.length)); this.write('"'); } @@ -132,6 +156,18 @@ export class StringConversionTests { return this.roundripTest('{"str":"foo"}'); } + static shouldHandleStringEscaped(): bool { + return this.roundripTest('"\\"\\\\\\/\\n\\t\\b\\r\\t"', '"\\"\\\\/\\n\\t\\b\\r\\t"'); + } + + static shouldHandleStringUnicodeEscaped1(): bool { + return this.roundripTest('"\\u0022"', '"\\""'); + } + + static shouldHandleStringUnicodeEscaped2(): bool { + return this.roundripTest('"\u041f\u043e\u043b\u0442\u043e\u0440\u0430 \u0417\u0435\u043c\u043b\u0435\u043a\u043e\u043f\u0430"', '"Полтора Землекопа"'); + } + static shouldMultipleKeys(): bool { return this.roundripTest('{"str":"foo","bar":"baz"}'); }