Skip to content

Commit 2faa248

Browse files
committed
update: Add support for x-www-form-urlencoded style separators
1 parent b7c01c4 commit 2faa248

File tree

5 files changed

+309
-60
lines changed

5 files changed

+309
-60
lines changed

src/JsonURL.js

Lines changed: 145 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const CHAR_QUESTION = 0x3f;
5555
const CHAR_AT = 0x40;
5656
const CHAR_UNDERSCORE = 0x5f;
5757
const CHAR_TILDE = 0x7e;
58+
const CHAR_EQUALS = 0x3d;
59+
const CHAR_AMP = 0x26;
5860

5961
const CHAR_0 = 0x30;
6062
const CHAR_E = 0x45;
@@ -77,6 +79,7 @@ const STATE_IN_OBJECT = 6;
7779

7880
const ERR_MSG_EXPECT_STRUCTCHAR =
7981
"JSON->URL: expected comma, open paren, or close paren";
82+
const ERR_MSG_EXPECT_MOREARRAY = "JSON->URL: expected comma or close paren";
8083
const ERR_MSG_EXPECT_VALUE = "JSON->URL: expected value";
8184
const ERR_MSG_EXPECT_LITERAL = "JSON->URL: expected literal value";
8285
const ERR_MSG_EXPECT_OBJVALUE = "JSON->URL: expected object value";
@@ -187,18 +190,18 @@ function errorMessage(msg, pos) {
187190
return msg + " at position " + pos;
188191
}
189192

190-
function parseLiteralLength(text, i, len, errmsg) {
193+
function parseLiteralLength(text, i, end, errmsg) {
191194
var isQuote = false;
192195
var start = i;
193196

194-
if (i === len) {
197+
if (i === end) {
195198
throw new SyntaxError(errorMessage(errmsg, i));
196199
}
197200
if (text.charCodeAt(i) === CHAR_QUOTE) {
198201
isQuote = true;
199202
i++;
200203
}
201-
for (; i < len; i++) {
204+
for (; i < end; i++) {
202205
var c = text.charCodeAt(i);
203206
if (c >= 0x41 && c <= 0x5a) {
204207
// A-Z
@@ -243,12 +246,21 @@ function parseLiteralLength(text, i, len, errmsg) {
243246
return i;
244247
}
245248
continue;
249+
case CHAR_AMP:
250+
case CHAR_EQUALS:
251+
//
252+
// these are forbidden, quoted or otherwise.
253+
//
254+
if (i === start) {
255+
throw new SyntaxError(errorMessage(errmsg, start));
256+
}
257+
return i;
246258
default:
247259
throw new SyntaxError(errorMessage(ERR_MSG_BADCHAR, i));
248260
}
249261
}
250262

251-
return len;
263+
return end;
252264
}
253265

254266
function toJsonURLText_Boolean() {
@@ -500,6 +512,13 @@ class ValueStack extends Array {
500512
class JsonURL {
501513
/**
502514
* Construct a new JsonURL class.
515+
*
516+
* Each instance of this class contains a number of properties that manage
517+
* the behavior of the parser and the values it returns; these are documented
518+
* below. The class instance does not manage parse state -- that state is
519+
* local to the parse() function itself. As long as you don't need different
520+
* properties (e.g. limits, null value, etc) you may re-use the same Parser
521+
* instance, even by multiple Workers.
503522
* @param {Object} prop Initialization properties.
504523
* You may provide zero more more of the following. Reasonable defaults
505524
* are assumed.
@@ -643,6 +662,27 @@ class JsonURL {
643662
/**
644663
* Parse JSON->URL text.
645664
* @param {string} text The text to parse.
665+
* @param {Object} options parse options.
666+
* You may provide zero more more of the following.
667+
* @param {array} options.impliedArray An implied array.
668+
* The parse() method implements a parser for the grammar oulined in
669+
* section 2.7 of the JSON->URL specification. The given parse text
670+
* is assumed to be an array, and the leading and trailing parens must
671+
* not be present. The given prop.impliedArray value will be populated
672+
* and returned.
673+
* @param {object} options.impliedObject An implied object.
674+
* The parse() method implements a parser for the grammar oulined in
675+
* section 2.8 of the JSON->URL specification. The given parse text
676+
* is assumed to be an object, and the leading and trailing parens must
677+
* not be present. The given prop.impliedObject value will be populated
678+
* and returned.
679+
* @param {boolean} options.wwwFormUrlEncoded Enable support for
680+
* x-www-form-urlencoded content.
681+
* The parse() method implements a parser for the grammar oulined in
682+
* section 2.9 of the JSON->URL specification. The given parse text
683+
* is may use ampersand and equal characters as the value and member
684+
* separator characters, respetively, at the top-level. This may be
685+
* combined with prop.impliedArray or prop.impliedObject.
646686
* @throws SyntaxError if there is a syntax error in the given text
647687
* @throws Error if a limit given in the constructor (or its default)
648688
* is exceeded.
@@ -664,10 +704,10 @@ class JsonURL {
664704
let stateStack = new StateStack(this);
665705
let pos = 0;
666706

667-
if (options.impliedObject) {
707+
if (options.impliedObject !== undefined) {
668708
valueStack.push(options.impliedObject);
669709
stateStack.push(STATE_IN_OBJECT);
670-
} else if (options.impliedArray) {
710+
} else if (options.impliedArray !== undefined) {
671711
valueStack.push(options.impliedArray);
672712
stateStack.push(STATE_IN_ARRAY);
673713
} else if (text.charCodeAt(0) !== CHAR_PAREN_OPEN) {
@@ -725,18 +765,26 @@ class JsonURL {
725765
continue;
726766

727767
case CHAR_PAREN_CLOSE:
768+
pos++;
769+
728770
if (stateStack.depth(true) === -1) {
729-
if (pos + 1 != end) {
730-
throw new SyntaxError(errorMessage(ERR_MSG_EXTRACHARS, pos));
731-
}
732-
if (valueStack.length === 0) {
771+
if (pos === end) {
733772
return newEmptyValue(this);
734773
}
735-
return valueStack[0];
774+
throw new SyntaxError(errorMessage(ERR_MSG_EXTRACHARS, pos));
736775
}
737776

738777
valueStack.appendArrayValue(pos, newEmptyValue(this));
739-
pos++;
778+
779+
if (pos === end && stateStack.depth() === 0) {
780+
if (options.impliedArray) {
781+
return valueStack.popArrayValue();
782+
}
783+
if (options.impliedObject) {
784+
return valueStack.popObjectValue();
785+
}
786+
throw new SyntaxError(errorMessage(ERR_MSG_STILLOPEN, pos));
787+
}
740788
continue;
741789

742790
default:
@@ -762,6 +810,12 @@ class JsonURL {
762810
pos = lvpos;
763811

764812
switch (c) {
813+
case CHAR_AMP:
814+
if (!options.wwwFormUrlEncoded || stateStack.depth() > 0) {
815+
throw new SyntaxError(errorMessage(ERR_MSG_BADCHAR, pos));
816+
}
817+
// fall through
818+
765819
case CHAR_COMMA:
766820
//
767821
// multi-element array
@@ -772,21 +826,44 @@ class JsonURL {
772826
continue;
773827

774828
case CHAR_PAREN_CLOSE:
829+
pos++;
830+
775831
//
776832
// single element array
777833
//
778834
valueStack.appendArrayValue(pos, [lv]);
779835

780-
if (stateStack.depth(true) === -1) {
781-
if (pos + 1 === end) {
782-
return valueStack[0];
783-
}
784-
throw new SyntaxError(errorMessage(ERR_MSG_EXTRACHARS, pos));
836+
switch (stateStack.depth(true)) {
837+
case -1:
838+
if (pos === end) {
839+
return valueStack[0];
840+
}
841+
throw new SyntaxError(errorMessage(ERR_MSG_EXTRACHARS, pos));
842+
843+
case 0:
844+
if (pos === end) {
845+
if (options.impliedArray) {
846+
return valueStack.popArrayValue();
847+
}
848+
if (options.impliedObject) {
849+
return valueStack.popObjectValue();
850+
}
851+
throw new SyntaxError(errorMessage(ERR_MSG_STILLOPEN, pos));
852+
}
853+
break;
854+
855+
default:
856+
break;
785857
}
786858

787-
pos++;
788859
continue;
789860

861+
case CHAR_EQUALS:
862+
if (!options.wwwFormUrlEncoded || stateStack.depth() > 0) {
863+
throw new SyntaxError(errorMessage(ERR_MSG_BADCHAR, pos));
864+
}
865+
// fall through
866+
790867
case CHAR_COLON:
791868
//
792869
// key name for object
@@ -817,7 +894,7 @@ class JsonURL {
817894
lv = this.parseLiteral(text, pos, lvpos, false);
818895
pos = lvpos;
819896

820-
if (lvpos === end) {
897+
if (pos === end) {
821898
if (stateStack.depth() === 0 && options.impliedArray) {
822899
return valueStack.popArrayValue(lv);
823900
}
@@ -832,18 +909,26 @@ class JsonURL {
832909
valueStack.popArrayValue();
833910

834911
switch (c) {
912+
case CHAR_AMP:
913+
if (!options.wwwFormUrlEncoded || stateStack.depth() > 0) {
914+
throw new SyntaxError(errorMessage(ERR_MSG_BADCHAR, pos));
915+
}
916+
// fall through
917+
835918
case CHAR_COMMA:
836919
stateStack.replace(STATE_IN_ARRAY);
837920
pos++;
838921
continue;
839922

840923
case CHAR_PAREN_CLOSE:
924+
pos++;
925+
841926
switch (stateStack.depth(true)) {
842927
case -1:
843928
//
844929
// end of a "real" composite
845930
//
846-
if (pos + 1 == end) {
931+
if (pos === end && !options.impliedArray) {
847932
return valueStack[0];
848933
}
849934
throw new SyntaxError(errorMessage(ERR_MSG_EXTRACHARS, pos));
@@ -852,21 +937,21 @@ class JsonURL {
852937
//
853938
// end of an implied composite
854939
//
855-
if (pos + 1 == end) {
940+
if (pos === end) {
856941
if (options.impliedArray) {
857942
return valueStack.popArrayValue();
858943
}
859944
if (options.impliedObject) {
860945
return valueStack.popObjectValue();
861946
}
947+
throw new SyntaxError(errorMessage(ERR_MSG_STILLOPEN, pos));
862948
}
863949
break;
864950
}
865951

866-
pos++;
867952
continue;
868953
}
869-
throw new SyntaxError(errorMessage(ERR_MSG_EXPECT_STRUCTCHAR, pos));
954+
throw new SyntaxError(errorMessage(ERR_MSG_EXPECT_MOREARRAY, pos));
870955

871956
case STATE_OBJECT_HAVE_KEY:
872957
if (c === CHAR_PAREN_OPEN) {
@@ -900,50 +985,79 @@ class JsonURL {
900985
valueStack.popObjectValue();
901986

902987
switch (c) {
988+
case CHAR_AMP:
989+
if (!options.wwwFormUrlEncoded || stateStack.depth() > 0) {
990+
throw new SyntaxError(errorMessage(ERR_MSG_BADCHAR, pos));
991+
}
992+
// fall through
993+
903994
case CHAR_COMMA:
904995
stateStack.replace(STATE_IN_OBJECT);
905996
pos++;
906997
continue;
907998

908999
case CHAR_PAREN_CLOSE:
1000+
pos++;
1001+
9091002
switch (stateStack.depth(true)) {
9101003
case -1:
911-
if (pos + 1 === end) {
1004+
if (pos === end && !options.impliedObject) {
9121005
//
9131006
// end of a "real" object
9141007
//
9151008
return valueStack[0];
9161009
}
9171010
throw new SyntaxError(errorMessage(ERR_MSG_EXTRACHARS, pos));
1011+
9181012
case 0:
9191013
//
9201014
// end of an implied composite
9211015
//
922-
if (pos + 1 == end) {
1016+
if (pos === end) {
9231017
if (options.impliedArray) {
9241018
return valueStack.popArrayValue();
9251019
}
9261020
if (options.impliedObject) {
9271021
return valueStack.popObjectValue();
9281022
}
1023+
throw new SyntaxError(
1024+
errorMessage(ERR_MSG_EXTRACHARS, pos)
1025+
);
9291026
}
9301027
break;
1028+
1029+
default:
1030+
break;
9311031
}
9321032

933-
pos++;
9341033
continue;
9351034
}
9361035
throw new SyntaxError(errorMessage(ERR_MSG_EXPECT_STRUCTCHAR, pos));
9371036

9381037
case STATE_IN_OBJECT:
9391038
lvpos = parseLiteralLength(text, pos, end, ERR_MSG_EXPECT_LITERAL);
9401039
if (lvpos === end) {
1040+
//
1041+
// I don't know that this is actually possible -- I haven't
1042+
// found a test case yet. But, if it is possible, it's an error.
1043+
//
9411044
throw new SyntaxError(errorMessage(ERR_MSG_STILLOPEN, end));
9421045
}
9431046

9441047
c = text.charCodeAt(lvpos);
945-
if (c !== CHAR_COLON) {
946-
throw new SyntaxError((ERR_MSG_EXPECT_OBJVALUE, lvpos));
1048+
1049+
switch (c) {
1050+
case CHAR_EQUALS:
1051+
if (!options.wwwFormUrlEncoded || stateStack.depth() > 0) {
1052+
throw new SyntaxError(errorMessage(ERR_MSG_BADCHAR, pos));
1053+
}
1054+
// fall through
1055+
1056+
case CHAR_COLON:
1057+
break;
1058+
1059+
default:
1060+
throw new SyntaxError((ERR_MSG_EXPECT_OBJVALUE, lvpos));
9471061
}
9481062

9491063
lv = this.parseLiteral(text, pos, lvpos, true);
@@ -954,6 +1068,9 @@ class JsonURL {
9541068
continue;
9551069

9561070
default:
1071+
//
1072+
// this shouldn't be possible, but handle it just in case
1073+
//
9571074
throw new SyntaxError(errorMessage(ERR_MSG_INTERNAL, pos));
9581075
}
9591076
}

0 commit comments

Comments
 (0)