From d984c047c771ec6c89ce23b2f29128a2a931b9db Mon Sep 17 00:00:00 2001 From: mrkutly Date: Sat, 4 Jun 2022 19:24:57 -0400 Subject: add runtime-specific error handling --- src/gleam_json_ffi.mjs | 138 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 128 insertions(+), 10 deletions(-) diff --git a/src/gleam_json_ffi.mjs b/src/gleam_json_ffi.mjs index 9d3daee..0116ab3 100644 --- a/src/gleam_json_ffi.mjs +++ b/src/gleam_json_ffi.mjs @@ -1,6 +1,12 @@ import { Ok, Error } from './gleam.mjs' import { UnexpectedByte, UnexpectedEndOfInput } from './gleam/json.mjs' +const Runtime = { + chromium: 'chromium', + spidermonkey: 'spidermonkey', + jscore: 'jscore', +} + export function json_to_string(json) { return JSON.stringify(json) } @@ -26,29 +32,141 @@ export function decode(string) { const result = JSON.parse(string) return new Ok(result) } catch (err) { - return new Error(getJsonDecodeError(err)) + return new Error(getJsonDecodeError(err, string)) } } -function getJsonDecodeError(stdErr) { - if (isUnexpectedByte(stdErr)) return new toUnexpectedByteError(stdErr) +function getJsonDecodeError(stdErr, json) { if (isUnexpectedEndOfInput(stdErr)) return new UnexpectedEndOfInput() + const unexpectedByteRuntime = getUnexpectedByteRuntime(stdErr) + if (unexpectedByteRuntime) return toUnexpectedByteError(unexpectedByteRuntime, stdErr, json) return new UnexpectedByte('', 0) } +/** + * Matches unexpected end of input messages in: + * - Chromium (edge, chrome, node) + * - Spidermonkey (firefox) + * - JavascriptCore (safari) + * + * Note that Spidermonkey and JavascriptCore will both incorrectly report some + * UnexpectedByte errors as UnexpectedEndOfInput errors. For example: + * + * @example + * // in JavascriptCore + * JSON.parse('{"a"]: "b"}) + * // => JSON Parse error: Expected ':' before value + * + * JSON.parse('{"a"') + * // => JSON Parse error: Expected ':' before value + * + * // in Chromium (correct) + * JSON.parse('{"a"]: "b"}) + * // => Unexpected token ] in JSON at position 4 + * + * JSON.parse('{"a"') + * // => Unexpected end of JSON input + * + * // in Chromium (correct) + * + */ +const unexpectedEndOfInputRegex = /((unexpected (end|eof))|(end of data)|(unterminated string)|(json( parse error|\.parse)\: expected '(\:|\}|\])'))/i + function isUnexpectedEndOfInput(err) { - return err.message.includes('Unexpected end') || err.message.includes('Unexpected EOF') + return unexpectedEndOfInputRegex.test(err.message) } -const unexpectedByteRegex = /Unexpected token (.) in JSON at position (\d+)/ -function isUnexpectedByte(err) { - return unexpectedByteRegex.test(err) +/** + * Matches unexpected byte messages in: + * - Chromium (edge, chrome, node) + * + * Matches the character and its position. + */ +const chromiumUnexpectedByteRegex = /unexpected token (.) in JSON at position (\d+)/ + +/** + * Matches unexpected byte messages in: + * - JavascriptCore (safari) + * + * JavascriptCore only reports what the character is and not its position. + */ +const jsCoreUnexpectedByteRegex = /unexpected identifier "(.)"/i + +/** + * Matches unexpected byte messages in: + * - Spidermonkey (firefox) + * + * Matches the position in a 2d grid only and not the character. + */ +const spidermonkeyUnexpectedByteRegex = /((unexpected character|expected double-quoted property name) at line (\d+) column (\d+))/i + +function getUnexpectedByteRuntime(err) { + if (chromiumUnexpectedByteRegex.test(err.message)) return Runtime.chromium + if (jsCoreUnexpectedByteRegex.test(err.message)) return Runtime.jscore + if (spidermonkeyUnexpectedByteRegex.test(err.message)) return Runtime.spidermonkey + return null +} + +function toUnexpectedByteError(runtime, err, json) { + switch (runtime) { + case Runtime.chromium: + return toChromiumUnexpectedByteError(err) + case Runtime.spidermonkey: + return toSpidermonkeyUnexpectedByteError(err, json) + case Runtime.jscore: + return toJsCoreUnexpectedByteError(err) + } } -function toUnexpectedByteError(err) { - const match = unexpectedByteRegex.exec(err.message) - const byte = "0x" + match[1].charCodeAt(0).toString(16).toUpperCase() +function toChromiumUnexpectedByteError(err) { + const match = chromiumUnexpectedByteRegex.exec(err.message) + const byte = toHex(match[1]) const position = Number(match[2]) return new UnexpectedByte(byte, position) } + +function toSpidermonkeyUnexpectedByteError(err, json) { + const match = spidermonkeyUnexpectedByteRegex.exec(err.message) + const line = Number(match[1]) + const column = Number(match[2]) + const position = getPositionFromMultiline(line, column, json) + const byte = toHex(err.message[position]) + return new UnexpectedByte(byte, position) +} + +function toJsCoreUnexpectedByteError(err) { + const match = jsCoreUnexpectedByteRegex.exec(err.message) + const byte = toHex(match[1]) + return new UnexpectedByte(byte, 0) +} + +function toHex(char) { + return "0x" + char.charCodeAt(0).toString(16).toUpperCase() +} + +/** + * Gets the position of a character in a flattened (i.e. single line) string + * from a line and column number. Note that the position is 0-indexed and + * the line and column numbers are 1-indexed. + * + * @param {number} line + * @param {number} column + * @param {string} string + */ +function getPositionFromMultiline(line, column, string) { + if (line === 1) return column - 1 + + let currentLn = 1 + let position = 0 + string.split('').find((char, idx) => { + if (char === '\n') currentLn += 1 + if (currentLn === line) { + position = idx + column + return true + } + return false + }) + + return position +} \ No newline at end of file -- cgit v1.2.3