Skip to content

Commit 5c196f3

Browse files
committed
Reworking SaferParser to use full lexing
1 parent 8cc1243 commit 5c196f3

File tree

1 file changed

+132
-74
lines changed
  • src/fprime_gds/flask/static/js

1 file changed

+132
-74
lines changed

src/fprime_gds/flask/static/js/json.js

Lines changed: 132 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,78 @@
88
*/
99

1010

11+
/**
12+
* Lexer for JSON built using JSON
13+
*/
14+
class RegExLexer {
15+
static TOKEN_EXPRESSIONS = new Map([
16+
// String tokens: " then
17+
// any number of:
18+
// not a quote or \
19+
// \ followed by not a quote
20+
// even number of \
21+
// odd number of \ then " (escaped quotation)
22+
// then even number of \ then " (terminating non-escaped ")
23+
["STRING", /^"([^"\\]|(\\[^"\\])|((\\\\)*)|(\\(\\\\)*)")*(?!\\(\\\\)*)"/],
24+
// Floating point tokens
25+
["NUMBER", /^-?\d+(\.\d+)?([eE][+-]?\d+)?/],
26+
// Infinity token
27+
["INFINITY", /^-?Infinity/],
28+
// Null token
29+
["NULL", /^null/],
30+
// NaN token
31+
["NULL", /^null/],
32+
// boolean token
33+
["BOOLEAN", /^(true)|(false)/],
34+
// Open object token
35+
["OPEN_OBJECT", /^\{/],
36+
// Close object token
37+
["CLOSE_OBJECT", /^}/],
38+
// Field separator token
39+
["FIELD_SEPERATOR", /^,/],
40+
// Open list token
41+
["OPEN_ARRAY", /^\[/],
42+
// Close a list token
43+
["CLOSE_ARRAY", /^]/],
44+
// Key Value Seperator
45+
["VALUE_SEPERATOR", /^:/],
46+
// Any amount of whitespace is an implicit token
47+
["WHITESPACE", /^\s+/]
48+
]);
49+
50+
/**
51+
* Tokenize the input string based on JSON tokens.
52+
* @param input_string: input string to tokenize
53+
* @return {*[]}: list of tokens in-order
54+
*/
55+
static tokenize(original_string) {
56+
let tokens = [];
57+
let input_string = original_string;
58+
// Consume the whole string
59+
while (input_string !== "") {
60+
let matched_something = false;
61+
for (let [token_type, token_matcher] of RegExLexer.TOKEN_EXPRESSIONS.entries()) {
62+
let match = token_matcher.exec(input_string)
63+
64+
// Token detected
65+
if (match != null) {
66+
matched_something = true;
67+
let matched = match[0];
68+
tokens.push([token_type, matched]);
69+
// Consume the string
70+
input_string = input_string.substring(matched.length);
71+
break;
72+
}
73+
}
74+
// Check for no token match
75+
if (!matched_something) {
76+
throw SyntaxError("Failed to match valid token: '" + input_string.substring(0, 20) + "'...");
77+
}
78+
}
79+
return tokens;
80+
}
81+
}
82+
1183
/**
1284
* Helper to determine if value is a string
1385
* @param value: value to check.
@@ -27,18 +99,22 @@ function isFunction(value) {
2799
}
28100

29101
/**
30-
* Conversion function for converting from string to BigInt or Number depending on size
31-
* @param {*} string_value: value to convert
32-
* @returns: Number for small values and BigInt for large values
102+
* Convert a string to a number
103+
* @param value: value to convert
104+
* @return {bigint|number}: number to return
33105
*/
34-
function convertInt(string_value) {
35-
string_value = string_value.trim();
36-
let number_value = Number.parseInt(string_value);
106+
function stringToNumber(value) {
107+
value = value.trim(); // Should be unnecessary
108+
// Process floats (containing . e or E)
109+
if (value.search(/[.eE]/) !== -1) {
110+
return Number.parseFloat(value);
111+
}
112+
let number_value = Number.parseInt(value);
37113
// When the big and normal numbers match, then return the normal number
38-
if (string_value == number_value.toString()) {
39-
return number_value;
114+
if (value !== number_value.toString()) {
115+
return BigInt(value);
40116
}
41-
return BigInt(string_value);
117+
return number_value;
42118
}
43119

44120
/**
@@ -56,24 +132,23 @@ function convertInt(string_value) {
56132
* - BigInt
57133
*/
58134
export class SaferParser {
59-
/**
60-
* States representing QUOTED or UNQUOTED text
61-
* @type {{QUOTED: number, UNQUOTED: number}}
62-
*/
63-
static STATES = {
64-
UNQUOTED: 0,
65-
QUOTED: 1
66-
};
67-
/**
68-
* List of mapping tuples for clean parsing: string match, replacement type, and real (post parse) type
69-
*/
70-
static MAPPINGS = [
71-
[/(-Infinity)/, -Infinity],
72-
[/(Infinity)/, Infinity],
73-
[/(NaN)/, NaN],
74-
[/(null)/, null],
75-
[/( -?\d{10,})/, "bigint", convertInt]
76-
];
135+
static CONVERSION_KEY = "fprime{replacement";
136+
137+
static CONVERSION_MAP = new Map([
138+
["INFINITY", (value) => (value[0] === "-") ? -Infinity : Infinity],
139+
["NAN", NaN],
140+
["NULL", null],
141+
["NUMBER", stringToNumber]
142+
]);
143+
144+
static STRINGIFY_TOKENS = [
145+
Infinity,
146+
-Infinity,
147+
NaN,
148+
"number",
149+
"bigint",
150+
null
151+
]
77152

78153

79154
// Store the language variants the first time
@@ -101,7 +176,7 @@ export class SaferParser {
101176
* @return {{}}: Javascript Object representation of data safely represented in JavaScript types
102177
*/
103178
static parse(json_string, reviver) {
104-
let converted_data = SaferParser.processUnquoted(json_string, SaferParser.replaceFromString);
179+
let converted_data = SaferParser.preprocess(json_string);
105180
// Set up a composite reviver of the one passed in and ours
106181
let input_reviver = reviver || ((key, value) => value);
107182
let full_reviver = (key, value) => input_reviver(key, SaferParser.reviver(key, value));
@@ -170,15 +245,17 @@ export class SaferParser {
170245

171246
/**
172247
* Get replacement object from a JavaScript type
248+
* @param _: unused
173249
* @param value: value to replace
174250
*/
175251
static replaceFromObject(_, value) {
176-
for (let i = 0; i < SaferParser.MAPPINGS.length; i++) {
177-
let mapper_type = SaferParser.MAPPINGS[i][1];
178-
let mapper_is_string = isString(mapper_type);
179-
// Check if the mapping matches the value, if so substitute a replacement object
180-
if ((!mapper_is_string && value == mapper_type) || (mapper_is_string && typeof value == mapper_type)) {
181-
return {"fprime{replacement": (value == null) ? "null" : value.toString()};
252+
for (let i = 0; i < SaferParser.STRINGIFY_TOKENS.length; i++) {
253+
let replacer_type = SaferParser.STRINGIFY_TOKENS[i];
254+
let mapper_is_string = isString(replacer_type);
255+
if ((!mapper_is_string && value === replacer_type) || (mapper_is_string && typeof value === replacer_type)) {
256+
let replace_object = {};
257+
replace_object[SaferParser.CONVERSION_KEY] = (value == null) ? "null" : value.toString();
258+
return replace_object;
182259
}
183260
}
184261
return value;
@@ -194,41 +271,28 @@ export class SaferParser {
194271
* @return reworked JSON string
195272
*/
196273
static postReplacer(json_string) {
197-
return json_string.replace(/\{\s*"fprime\{replacement"\s*:\s*"([^"]+)"\s*\}/sg, "$1");
198-
}
199-
200-
/**
201-
* Replace string occurrences of our gnarly types with a mapping equivalent
202-
* @param string_value: value to replace
203-
*/
204-
static replaceFromString(string_value) {
205-
for (let i = 0; i < SaferParser.MAPPINGS.length; i++) {
206-
let mapper = SaferParser.MAPPINGS[i];
207-
string_value = string_value.replace(mapper[0], "{\"fprime{replacement\": \"$1\"}");
208-
}
209-
return string_value;
274+
return json_string.replace(/\{\s*"fprime\{replacement"\s*:\s*"([^"]+)"\s*}/sg, "$1");
210275
}
211276

212277
/**
213278
* Apply process function to raw json string only for data that is not qu
214-
* @param json_string
215-
* @param process_function
279+
* @param json_string: JSON string to preprocess
216280
* @return {string}
217281
*/
218-
static processUnquoted(json_string, process_function) {
219-
// The initial state of any JSON string is unquoted
220-
let state = SaferParser.STATES.UNQUOTED;
221-
let unprocessed = json_string;
222-
let transformed_data = "";
223-
224-
while (unprocessed.length > 0) {
225-
let next_quote = unprocessed.indexOf("\"");
226-
let section = (next_quote !== -1) ? unprocessed.substring(0, next_quote + 1) : unprocessed.substring(0);
227-
unprocessed = unprocessed.substring(section.length);
228-
transformed_data += (state === SaferParser.STATES.QUOTED) ? section : process_function(section);
229-
state = (state === SaferParser.STATES.QUOTED) ? SaferParser.STATES.UNQUOTED : SaferParser.STATES.QUOTED;
230-
}
231-
return transformed_data;
282+
static preprocess(json_string) {
283+
const CONVERSION_KEYS = Array.from(SaferParser.CONVERSION_MAP.keys());
284+
let tokens = RegExLexer.tokenize(json_string);
285+
let converted_text = tokens.map(
286+
([token_type, token_text]) => {
287+
if (CONVERSION_KEYS.indexOf(token_type) !== -1) {
288+
let replacement_object = {};
289+
replacement_object[SaferParser.CONVERSION_KEY] = token_type;
290+
replacement_object["value"] = token_text;
291+
return JSON.stringify(replacement_object)
292+
}
293+
return token_text;
294+
});
295+
return converted_text.join("");
232296
}
233297

234298
/**
@@ -239,19 +303,13 @@ export class SaferParser {
239303
*/
240304
static reviver(key, value) {
241305
// Look for fprime-replacement and quickly abort if not there
242-
let string_value = value["fprime{replacement"];
243-
if (typeof string_value === "undefined") {
306+
let replacement_type = value[SaferParser.CONVERSION_KEY];
307+
if (typeof replacement_type === "undefined") {
244308
return value;
245309
}
246-
// Run the mappings looking for a match
247-
for (let i = 0; i < SaferParser.MAPPINGS.length; i++) {
248-
let mapper = SaferParser.MAPPINGS[i];
249-
if (mapper[0].test(string_value)) {
250-
// Run the conversion function if it exists, otherwise return the mapped constant value
251-
return (mapper.length >= 3) ? mapper[2](string_value) : mapper[1];
252-
}
253-
}
254-
return value;
310+
let string_value = value["value"];
311+
let replacer = SaferParser.CONVERSION_MAP.get(replacement_type);
312+
return isFunction(replacer) ? replacer(string_value) : replacer;
255313
}
256314

257315
/**

0 commit comments

Comments
 (0)