Skip to content

Commit 03b6828

Browse files
committed
Tokenizer uses single process step and length-based exhaustion check
Issue: SPR-16032
1 parent b2017bb commit 03b6828

File tree

3 files changed

+70
-63
lines changed

3 files changed

+70
-63
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,7 @@ protected SpelExpression doParseExpression(String expressionString, @Nullable Pa
124124
try {
125125
this.expressionString = expressionString;
126126
Tokenizer tokenizer = new Tokenizer(expressionString);
127-
tokenizer.process();
128-
this.tokenStream = tokenizer.getTokens();
127+
this.tokenStream = tokenizer.process();
129128
this.tokenStreamLength = this.tokenStream.size();
130129
this.tokenStreamPointer = 0;
131130
this.constructedNodes.clear();

spring-expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java

Lines changed: 58 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,15 @@
2828
* Lex some input data into a stream of tokens that can then be parsed.
2929
*
3030
* @author Andy Clement
31+
* @author Juergen Hoeller
3132
* @author Phillip Webb
3233
* @since 3.0
3334
*/
3435
class Tokenizer {
3536

36-
// if this is changed, it must remain sorted
37-
private static final String[] ALTERNATIVE_OPERATOR_NAMES = { "DIV", "EQ", "GE", "GT",
38-
"LE", "LT", "MOD", "NE", "NOT" };
37+
// If this gets changed, it must remain sorted...
38+
private static final String[] ALTERNATIVE_OPERATOR_NAMES =
39+
{"DIV", "EQ", "GE", "GT", "LE", "LT", "MOD", "NE", "NOT"};
3940

4041
private static final byte FLAGS[] = new byte[256];
4142

@@ -64,29 +65,28 @@ class Tokenizer {
6465
}
6566

6667

67-
String expressionString;
68+
private String expressionString;
6869

69-
char[] toProcess;
70+
private char[] charsToProcess;
7071

71-
int pos;
72+
private int pos;
7273

73-
int max;
74+
private int max;
7475

75-
List<Token> tokens = new ArrayList<>();
76+
private List<Token> tokens = new ArrayList<>();
7677

7778

7879
public Tokenizer(String inputData) {
7980
this.expressionString = inputData;
80-
this.toProcess = (inputData + "\0").toCharArray();
81-
this.max = this.toProcess.length;
81+
this.charsToProcess = (inputData + "\0").toCharArray();
82+
this.max = this.charsToProcess.length;
8283
this.pos = 0;
83-
process();
8484
}
8585

8686

87-
public void process() {
87+
public List<Token> process() {
8888
while (this.pos < this.max) {
89-
char ch = this.toProcess[this.pos];
89+
char ch = this.charsToProcess[this.pos];
9090
if (isAlphabetic(ch)) {
9191
lexIdentifier();
9292
}
@@ -190,9 +190,7 @@ else if (isTwoCharToken(TokenKind.PROJECT)) {
190190
break;
191191
case '|':
192192
if (!isTwoCharToken(TokenKind.SYMBOLIC_OR)) {
193-
throw new InternalParseException(new SpelParseException(
194-
this.expressionString, this.pos, SpelMessage.MISSING_CHARACTER,
195-
"|"));
193+
raiseParseException(this.pos, SpelMessage.MISSING_CHARACTER, "|");
196194
}
197195
pushPairToken(TokenKind.SYMBOLIC_OR);
198196
break;
@@ -261,41 +259,38 @@ else if (isTwoCharToken(TokenKind.SAFE_NAVI)) {
261259
break;
262260
case 0:
263261
// hit sentinel at end of value
264-
this.pos++; // will take us to the end
262+
this.pos++; // will take us to the end
265263
break;
266264
case '\\':
267-
throw new InternalParseException(
268-
new SpelParseException(this.expressionString, this.pos, SpelMessage.UNEXPECTED_ESCAPE_CHAR));
265+
raiseParseException(this.pos, SpelMessage.UNEXPECTED_ESCAPE_CHAR);
266+
break;
269267
default:
270268
throw new IllegalStateException("Cannot handle (" + Integer.valueOf(ch) + ") '" + ch + "'");
271269
}
272270
}
273271
}
274-
}
275-
276-
public List<Token> getTokens() {
277272
return this.tokens;
278273
}
279274

275+
280276
// STRING_LITERAL: '\''! (APOS|~'\'')* '\''!;
281277
private void lexQuotedStringLiteral() {
282278
int start = this.pos;
283279
boolean terminated = false;
284280
while (!terminated) {
285281
this.pos++;
286-
char ch = this.toProcess[this.pos];
282+
char ch = this.charsToProcess[this.pos];
287283
if (ch == '\'') {
288284
// may not be the end if the char after is also a '
289-
if (this.toProcess[this.pos + 1] == '\'') {
290-
this.pos++; // skip over that too, and continue
285+
if (this.charsToProcess[this.pos + 1] == '\'') {
286+
this.pos++; // skip over that too, and continue
291287
}
292288
else {
293289
terminated = true;
294290
}
295291
}
296-
if (ch == 0) {
297-
throw new InternalParseException(new SpelParseException(this.expressionString, start,
298-
SpelMessage.NON_TERMINATING_QUOTED_STRING));
292+
if (isExhausted()) {
293+
raiseParseException(start, SpelMessage.NON_TERMINATING_QUOTED_STRING);
299294
}
300295
}
301296
this.pos++;
@@ -308,19 +303,18 @@ private void lexDoubleQuotedStringLiteral() {
308303
boolean terminated = false;
309304
while (!terminated) {
310305
this.pos++;
311-
char ch = this.toProcess[this.pos];
306+
char ch = this.charsToProcess[this.pos];
312307
if (ch == '"') {
313308
// may not be the end if the char after is also a "
314-
if (this.toProcess[this.pos + 1] == '"') {
315-
this.pos++; // skip over that too, and continue
309+
if (this.charsToProcess[this.pos + 1] == '"') {
310+
this.pos++; // skip over that too, and continue
316311
}
317312
else {
318313
terminated = true;
319314
}
320315
}
321-
if (ch == 0) {
322-
throw new InternalParseException(new SpelParseException(this.expressionString,
323-
start, SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING));
316+
if (isExhausted()) {
317+
raiseParseException(start, SpelMessage.NON_TERMINATING_DOUBLE_QUOTED_STRING);
324318
}
325319
}
326320
this.pos++;
@@ -346,7 +340,7 @@ private void lexDoubleQuotedStringLiteral() {
346340
private void lexNumericLiteral(boolean firstCharIsZero) {
347341
boolean isReal = false;
348342
int start = this.pos;
349-
char ch = this.toProcess[this.pos + 1];
343+
char ch = this.charsToProcess[this.pos + 1];
350344
boolean isHex = ch == 'x' || ch == 'X';
351345

352346
// deal with hexadecimal
@@ -355,7 +349,7 @@ private void lexNumericLiteral(boolean firstCharIsZero) {
355349
do {
356350
this.pos++;
357351
}
358-
while (isHexadecimalDigit(this.toProcess[this.pos]));
352+
while (isHexadecimalDigit(this.charsToProcess[this.pos]));
359353
if (isChar('L', 'l')) {
360354
pushHexIntToken(subarray(start + 2, this.pos), true, start, this.pos);
361355
this.pos++;
@@ -372,18 +366,18 @@ private void lexNumericLiteral(boolean firstCharIsZero) {
372366
do {
373367
this.pos++;
374368
}
375-
while (isDigit(this.toProcess[this.pos]));
369+
while (isDigit(this.charsToProcess[this.pos]));
376370

377371
// a '.' indicates this number is a real
378-
ch = this.toProcess[this.pos];
372+
ch = this.charsToProcess[this.pos];
379373
if (ch == '.') {
380374
isReal = true;
381375
int dotpos = this.pos;
382376
// carry on consuming digits
383377
do {
384378
this.pos++;
385379
}
386-
while (isDigit(this.toProcess[this.pos]));
380+
while (isDigit(this.charsToProcess[this.pos]));
387381
if (this.pos == dotpos + 1) {
388382
// the number is something like '3.'. It is really an int but may be
389383
// part of something like '3.toString()'. In this case process it as
@@ -398,19 +392,18 @@ private void lexNumericLiteral(boolean firstCharIsZero) {
398392

399393
// Now there may or may not be an exponent
400394

401-
// is it a long ?
395+
// Is it a long ?
402396
if (isChar('L', 'l')) {
403-
if (isReal) { // 3.4L - not allowed
404-
throw new InternalParseException(new SpelParseException(this.expressionString,
405-
start, SpelMessage.REAL_CANNOT_BE_LONG));
397+
if (isReal) { // 3.4L - not allowed
398+
raiseParseException(start, SpelMessage.REAL_CANNOT_BE_LONG);
406399
}
407400
pushIntToken(subarray(start, endOfNumber), true, start, endOfNumber);
408401
this.pos++;
409402
}
410-
else if (isExponentChar(this.toProcess[this.pos])) {
411-
isReal = true; // if it wasn't before, it is now
403+
else if (isExponentChar(this.charsToProcess[this.pos])) {
404+
isReal = true; // if it wasn't before, it is now
412405
this.pos++;
413-
char possibleSign = this.toProcess[this.pos];
406+
char possibleSign = this.charsToProcess[this.pos];
414407
if (isSign(possibleSign)) {
415408
this.pos++;
416409
}
@@ -419,19 +412,19 @@ else if (isExponentChar(this.toProcess[this.pos])) {
419412
do {
420413
this.pos++;
421414
}
422-
while (isDigit(this.toProcess[this.pos]));
415+
while (isDigit(this.charsToProcess[this.pos]));
423416
boolean isFloat = false;
424-
if (isFloatSuffix(this.toProcess[this.pos])) {
417+
if (isFloatSuffix(this.charsToProcess[this.pos])) {
425418
isFloat = true;
426419
endOfNumber = ++this.pos;
427420
}
428-
else if (isDoubleSuffix(this.toProcess[this.pos])) {
421+
else if (isDoubleSuffix(this.charsToProcess[this.pos])) {
429422
endOfNumber = ++this.pos;
430423
}
431424
pushRealToken(subarray(start, this.pos), isFloat, start, this.pos);
432425
}
433426
else {
434-
ch = this.toProcess[this.pos];
427+
ch = this.charsToProcess[this.pos];
435428
boolean isFloat = false;
436429
if (isFloatSuffix(ch)) {
437430
isReal = true;
@@ -456,7 +449,7 @@ private void lexIdentifier() {
456449
do {
457450
this.pos++;
458451
}
459-
while (isIdentifier(this.toProcess[this.pos]));
452+
while (isIdentifier(this.charsToProcess[this.pos]));
460453
char[] subarray = subarray(start, this.pos);
461454

462455
// Check if this is the alternative (textual) representation of an operator (see
@@ -484,14 +477,10 @@ private void pushIntToken(char[] data, boolean isLong, int start, int end) {
484477
private void pushHexIntToken(char[] data, boolean isLong, int start, int end) {
485478
if (data.length == 0) {
486479
if (isLong) {
487-
throw new InternalParseException(new SpelParseException(this.expressionString,
488-
start, SpelMessage.NOT_A_LONG, this.expressionString.substring(start,
489-
end + 1)));
480+
raiseParseException(start, SpelMessage.NOT_A_LONG, this.expressionString.substring(start, end + 1));
490481
}
491482
else {
492-
throw new InternalParseException(new SpelParseException(this.expressionString,
493-
start, SpelMessage.NOT_AN_INTEGER, this.expressionString.substring(
494-
start, end)));
483+
raiseParseException(start, SpelMessage.NOT_AN_INTEGER, this.expressionString.substring(start, end));
495484
}
496485
}
497486
if (isLong) {
@@ -513,7 +502,7 @@ private void pushRealToken(char[] data, boolean isFloat, int start, int end) {
513502

514503
private char[] subarray(int start, int end) {
515504
char[] result = new char[end - start];
516-
System.arraycopy(this.toProcess, start, result, 0, end - start);
505+
System.arraycopy(this.charsToProcess, start, result, 0, end - start);
517506
return result;
518507
}
519508

@@ -522,8 +511,8 @@ private char[] subarray(int start, int end) {
522511
*/
523512
private boolean isTwoCharToken(TokenKind kind) {
524513
return (kind.tokenChars.length == 2 &&
525-
this.toProcess[this.pos] == kind.tokenChars[0] &&
526-
this.toProcess[this.pos + 1] == kind.tokenChars[1]);
514+
this.charsToProcess[this.pos] == kind.tokenChars[0] &&
515+
this.charsToProcess[this.pos + 1] == kind.tokenChars[1]);
527516
}
528517

529518
/**
@@ -552,7 +541,7 @@ private boolean isIdentifier(char ch) {
552541
}
553542

554543
private boolean isChar(char a, char b) {
555-
char ch = this.toProcess[this.pos];
544+
char ch = this.charsToProcess[this.pos];
556545
return ch == a || ch == b;
557546
}
558547

@@ -593,4 +582,12 @@ private boolean isHexadecimalDigit(char ch) {
593582
return (FLAGS[ch] & IS_HEXDIGIT) != 0;
594583
}
595584

585+
private boolean isExhausted() {
586+
return (this.pos == this.max - 1);
587+
}
588+
589+
private void raiseParseException(int start, SpelMessage msg, Object... inserts) {
590+
throw new InternalParseException(new SpelParseException(this.expressionString, start, msg, inserts));
591+
}
592+
596593
}

spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
import org.springframework.expression.spel.support.StandardEvaluationContext;
6060
import org.springframework.expression.spel.support.StandardTypeLocator;
6161
import org.springframework.expression.spel.testresources.le.div.mod.reserved.Reserver;
62+
import org.springframework.util.ObjectUtils;
6263

6364
import static org.hamcrest.Matchers.*;
6465
import static org.junit.Assert.*;
@@ -2095,6 +2096,16 @@ public void SPR13918() {
20952096
assertEquals(StandardCharsets.UTF_8, result);
20962097
}
20972098

2099+
@Test
2100+
public void SPR16032() {
2101+
EvaluationContext context = new StandardEvaluationContext();
2102+
context.setVariable("str", "a\0b");
2103+
2104+
Expression ex = parser.parseExpression("#str?.split('\0')");
2105+
Object result = ex.getValue(context);
2106+
assertTrue(ObjectUtils.nullSafeEquals(result, new String[] {"a", "b"}));
2107+
}
2108+
20982109

20992110
public static class ListOf {
21002111

0 commit comments

Comments
 (0)