This is an automated email from the ASF dual-hosted git repository. domgarguilo pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/accumulo-access.git
The following commit(s) were added to refs/heads/main by this push: new 261b5c3 Miscellaneous Improvements (#27) 261b5c3 is described below commit 261b5c303df3bae0d311689e14e1e029ae43d849 Author: Dom G <domgargu...@apache.org> AuthorDate: Thu Oct 26 15:17:41 2023 -0400 Miscellaneous Improvements (#27) * Misc cleanup * Remove mentions of mutations and visibility * Add byte constant class and helper methods --------- Co-authored-by: Dave Marion <dlmar...@apache.org> Co-authored-by: Keith Turner <ktur...@apache.org> --- README.md | 2 +- .../apache/accumulo/access/AccessEvaluator.java | 10 ++-- .../accumulo/access/AccessEvaluatorImpl.java | 67 +++++++++++----------- .../apache/accumulo/access/AccessExpression.java | 18 +++--- .../accumulo/access/AccessExpressionImpl.java | 21 +++---- .../java/org/apache/accumulo/access/AeNode.java | 7 ++- .../org/apache/accumulo/access/Authorizations.java | 1 + .../java/org/apache/accumulo/access/ByteUtils.java | 58 +++++++++++++++++++ .../org/apache/accumulo/access/BytesWrapper.java | 14 ++++- .../java/org/apache/accumulo/access/Parser.java | 14 +++-- .../java/org/apache/accumulo/access/Tokenizer.java | 49 +++++++--------- 11 files changed, 165 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index e43991c..a754d7f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Apache Accumulo [ColumnVisibility][1] and [VisibilityEvaluator][2] classes. This functionality is provided in a standalone java library that has no dependencies (for example no Hadoop, Zookeeper, Thrift, etc dependencies). -For a conceptual overview of what an access expression is see the +For a conceptual overview of what an access expression is, see the [specification](SPECIFICATION.md) document. See the [getting started guide](contrib/getting-started/README.md) for an example of how to use this java library. diff --git a/src/main/java/org/apache/accumulo/access/AccessEvaluator.java b/src/main/java/org/apache/accumulo/access/AccessEvaluator.java index ecf1f3e..c0123ed 100644 --- a/src/main/java/org/apache/accumulo/access/AccessEvaluator.java +++ b/src/main/java/org/apache/accumulo/access/AccessEvaluator.java @@ -86,7 +86,7 @@ public interface AccessEvaluator { interface AuthorizationsBuilder { - FinalBuilder authorizations(Authorizations authorizations); + EvaluatorBuilder authorizations(Authorizations authorizations); /** * Allows providing multiple sets of authorizations. Each expression will be evaluated @@ -139,20 +139,20 @@ public interface AccessEvaluator { * * */ - FinalBuilder authorizations(Collection<Authorizations> authorizations); + EvaluatorBuilder authorizations(Collection<Authorizations> authorizations); /** * Allows specifying a single set of authorizations. */ - FinalBuilder authorizations(String... authorizations); + EvaluatorBuilder authorizations(String... authorizations); /** * Allows specifying an authorizer that is analogous to a single set of authorization. */ - FinalBuilder authorizations(Authorizer authorizer); + EvaluatorBuilder authorizations(Authorizer authorizer); } - interface FinalBuilder { + interface EvaluatorBuilder { AccessEvaluator build(); } diff --git a/src/main/java/org/apache/accumulo/access/AccessEvaluatorImpl.java b/src/main/java/org/apache/accumulo/access/AccessEvaluatorImpl.java index c27e9da..eb25206 100644 --- a/src/main/java/org/apache/accumulo/access/AccessEvaluatorImpl.java +++ b/src/main/java/org/apache/accumulo/access/AccessEvaluatorImpl.java @@ -21,6 +21,10 @@ package org.apache.accumulo.access; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toSet; import static java.util.stream.Collectors.toUnmodifiableList; +import static org.apache.accumulo.access.ByteUtils.BACKSLASH; +import static org.apache.accumulo.access.ByteUtils.QUOTE; +import static org.apache.accumulo.access.ByteUtils.isQuoteOrSlash; +import static org.apache.accumulo.access.ByteUtils.isQuoteSymbol; import java.util.Collection; import java.util.Collections; @@ -43,14 +47,14 @@ class AccessEvaluatorImpl implements AccessEvaluator { .map(auth -> AccessEvaluatorImpl.escape(auth, false)).map(BytesWrapper::new) .collect(toSet())) .map(escapedAuths -> (Predicate<BytesWrapper>) escapedAuths::contains) - .collect(Collectors.toList()); + .collect(Collectors.toUnmodifiableList()); } static String unescape(BytesWrapper auth) { int escapeCharCount = 0; for (int i = 0; i < auth.length(); i++) { byte b = auth.byteAt(i); - if (b == '"' || b == '\\') { + if (isQuoteOrSlash(b)) { escapeCharCount++; } } @@ -67,12 +71,13 @@ class AccessEvaluatorImpl implements AccessEvaluator { if (b == '\\') { i++; b = auth.byteAt(i); - if (b != '"' && b != '\\') { + if (!isQuoteOrSlash(b)) { throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth); } - } else if (b == '"') { + } else if (isQuoteSymbol(b)) { // should only see quote after a slash - throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth); + throw new IllegalArgumentException( + "Illegal character after slash in auth String : " + auth); } unescapedCopy[pos++] = b; @@ -88,31 +93,31 @@ class AccessEvaluatorImpl implements AccessEvaluator { * Properly escapes an authorization string. The string can be quoted if desired. * * @param auth authorization string, as UTF-8 encoded bytes - * @param quote true to wrap escaped authorization in quotes + * @param shouldQuote true to wrap escaped authorization in quotes * @return escaped authorization string */ - static byte[] escape(byte[] auth, boolean quote) { + static byte[] escape(byte[] auth, boolean shouldQuote) { int escapeCount = 0; for (byte value : auth) { - if (value == '"' || value == '\\') { + if (isQuoteOrSlash(value)) { escapeCount++; } } - if (escapeCount > 0 || quote) { - byte[] escapedAuth = new byte[auth.length + escapeCount + (quote ? 2 : 0)]; - int index = quote ? 1 : 0; + if (escapeCount > 0 || shouldQuote) { + byte[] escapedAuth = new byte[auth.length + escapeCount + (shouldQuote ? 2 : 0)]; + int index = shouldQuote ? 1 : 0; for (byte b : auth) { - if (b == '"' || b == '\\') { - escapedAuth[index++] = '\\'; + if (isQuoteOrSlash(b)) { + escapedAuth[index++] = BACKSLASH; } escapedAuth[index++] = b; } - if (quote) { - escapedAuth[0] = '"'; - escapedAuth[escapedAuth.length - 1] = '"'; + if (shouldQuote) { + escapedAuth[0] = QUOTE; + escapedAuth[escapedAuth.length - 1] = QUOTE; } auth = escapedAuth; @@ -121,25 +126,19 @@ class AccessEvaluatorImpl implements AccessEvaluator { } @Override - public boolean canAccess(String expression) throws IllegalArgumentException { - + public boolean canAccess(String expression) throws IllegalAccessExpressionException { return evaluate(new AccessExpressionImpl(expression)); - } @Override - public boolean canAccess(byte[] expression) throws IllegalArgumentException { - + public boolean canAccess(byte[] expression) throws IllegalAccessExpressionException { return evaluate(new AccessExpressionImpl(expression)); - } @Override - public boolean canAccess(AccessExpression expression) throws IllegalArgumentException { + public boolean canAccess(AccessExpression expression) throws IllegalAccessExpressionException { if (expression instanceof AccessExpressionImpl) { - return evaluate((AccessExpressionImpl) expression); - } else { return canAccess(expression.getExpression()); } @@ -147,12 +146,12 @@ class AccessEvaluatorImpl implements AccessEvaluator { public boolean evaluate(AccessExpressionImpl accessExpression) throws IllegalAccessExpressionException { - // The VisibilityEvaluator computes a trie from the given Authorizations, that ColumnVisibility - // expressions can be evaluated against. + // The AccessEvaluator computes a trie from the given Authorizations, that AccessExpressions can + // be evaluated against. return authorizedPredicates.stream().allMatch(accessExpression.aeNode::canAccess); } - private static class BuilderImpl implements AuthorizationsBuilder, FinalBuilder { + private static class BuilderImpl implements AuthorizationsBuilder, EvaluatorBuilder { private Authorizer authorizationsChecker; @@ -178,14 +177,14 @@ class AccessEvaluatorImpl implements AccessEvaluator { } @Override - public FinalBuilder authorizations(Authorizations authorizations) { + public EvaluatorBuilder authorizations(Authorizations authorizations) { setAuthorizations(authorizations.asSet().stream().map(auth -> auth.getBytes(UTF_8)) .collect(toUnmodifiableList())); return this; } @Override - public FinalBuilder authorizations(Collection<Authorizations> authorizationSets) { + public EvaluatorBuilder authorizations(Collection<Authorizations> authorizationSets) { setAuthorizations(authorizationSets .stream().map(authorizations -> authorizations.asSet().stream() .map(auth -> auth.getBytes(UTF_8)).collect(toUnmodifiableList())) @@ -194,14 +193,14 @@ class AccessEvaluatorImpl implements AccessEvaluator { } @Override - public FinalBuilder authorizations(String... authorizations) { + public EvaluatorBuilder authorizations(String... authorizations) { setAuthorizations(Stream.of(authorizations).map(auth -> auth.getBytes(UTF_8)) .collect(toUnmodifiableList())); return this; } @Override - public FinalBuilder authorizations(Authorizer authorizationChecker) { + public EvaluatorBuilder authorizations(Authorizer authorizationChecker) { if (authorizationSets != null) { throw new IllegalStateException("Cannot set checker and authorizations"); } @@ -212,7 +211,8 @@ class AccessEvaluatorImpl implements AccessEvaluator { @Override public AccessEvaluator build() { if (authorizationSets != null ^ authorizationsChecker == null) { - throw new IllegalStateException(); + throw new IllegalStateException( + "Exactly one of authorizationSets or authorizationsChecker must be set, not both or none."); } AccessEvaluator accessEvaluator; @@ -230,4 +230,5 @@ class AccessEvaluatorImpl implements AccessEvaluator { public static AuthorizationsBuilder builder() { return new BuilderImpl(); } + } diff --git a/src/main/java/org/apache/accumulo/access/AccessExpression.java b/src/main/java/org/apache/accumulo/access/AccessExpression.java index fa20807..c940e7c 100644 --- a/src/main/java/org/apache/accumulo/access/AccessExpression.java +++ b/src/main/java/org/apache/accumulo/access/AccessExpression.java @@ -71,14 +71,14 @@ public interface AccessExpression { * <p> * As an example of deduplication, the expression {@code X&Y&X} is equivalent to {@code X&Y} * - * @return A normalized version of the visibility expression that removes duplicates and orders - * the expression in a consistent way. + * @return A normalized version of the access expression that removes duplicates and orders the + * expression in a consistent way. */ String normalize(); /** - * @return the unique authorizations that occur in the expression. For example, for the expression - * {@code (A&B)|(A&C)|(A&D)} this method would return {@code [A,B,C,D]]} + * @return the unique set of authorizations that occur in the expression. For example, for the + * expression {@code (A&B)|(A&C)|(A&D)}, this method would return {@code [A,B,C,D]}. */ Authorizations getAuthorizations(); @@ -94,7 +94,7 @@ public interface AccessExpression { } /** - * @return an empty VisibilityExpression. + * @return an empty AccessExpression. */ static AccessExpression of() { return AccessExpressionImpl.EMPTY; @@ -104,8 +104,8 @@ public interface AccessExpression { * Authorizations occurring in an access expression can only contain the characters listed in the * <a href= * "https://github.com/apache/accumulo-access/blob/main/SPECIFICATION.md">specification</a> unless - * quoted. Use this method to quote authorizations that occur in an access expression. This method - * will only quote if it is needed. + * quoted (surrounded by quotation marks). Use this method to quote authorizations that occur in + * an access expression. This method will only quote if it is needed. */ static byte[] quote(byte[] authorization) { return AccessExpressionImpl.quote(authorization); @@ -115,8 +115,8 @@ public interface AccessExpression { * Authorizations occurring in an access expression can only contain the characters listed in the * <a href= * "https://github.com/apache/accumulo-access/blob/main/SPECIFICATION.md">specification</a> unless - * quoted. Use this method to quote authorizations that occur in an access expression. This method - * will only quote if it is needed. + * quoted (surrounded by quotation marks). Use this method to quote authorizations that occur in + * an access expression. This method will only quote if it is needed. */ static String quote(String authorization) { return AccessExpressionImpl.quote(authorization); diff --git a/src/main/java/org/apache/accumulo/access/AccessExpressionImpl.java b/src/main/java/org/apache/accumulo/access/AccessExpressionImpl.java index 3285f5a..3f82004 100644 --- a/src/main/java/org/apache/accumulo/access/AccessExpressionImpl.java +++ b/src/main/java/org/apache/accumulo/access/AccessExpressionImpl.java @@ -60,10 +60,12 @@ class AccessExpressionImpl implements AccessExpression { } /** - * Creates a column visibility for a Mutation. + * Creates an AccessExpression. * - * @param expression An expression of the rights needed to see this mutation. The expression - * syntax is defined at the class-level documentation + * @param expression An expression of the rights needed to see specific data. The expression + * syntax is defined within the <a href= + * "https://github.com/apache/accumulo-access/blob/main/SPECIFICATION.md">specification + * doc</a> */ AccessExpressionImpl(String expression) { this(expression.getBytes(UTF_8)); @@ -71,9 +73,9 @@ class AccessExpressionImpl implements AccessExpression { } /** - * Creates a column visibility for a Mutation from a string already encoded in UTF-8 bytes. + * Creates an AccessExpression from a string already encoded in UTF-8 bytes. * - * @param expression visibility expression, encoded as UTF-8 bytes + * @param expression AccessExpression, encoded as UTF-8 bytes * @see #AccessExpressionImpl(String) */ AccessExpressionImpl(byte[] expression) { @@ -98,11 +100,11 @@ class AccessExpressionImpl implements AccessExpression { } /** - * Compares two ColumnVisibilities for string equivalence, not as a meaningful comparison of terms + * Compares two AccessExpressions for string equivalence, not as a meaningful comparison of terms * and conditions. * - * @param otherLe other column visibility - * @return true if this visibility equals the other via string comparison + * @param otherLe other AccessExpression + * @return true if this AccessExpression equals the other via string comparison */ boolean equals(AccessExpressionImpl otherLe) { return Arrays.equals(expression, otherLe.expression); @@ -118,8 +120,7 @@ class AccessExpressionImpl implements AccessExpression { } /** - * Properly quotes terms in a column visibility expression. If no quoting is needed, then nothing - * is done. + * Properly quotes terms in an AccessExpression. If no quoting is needed, then nothing is done. * * @param term term to quote, encoded as UTF-8 bytes * @return quoted term (unquoted if unnecessary), encoded as UTF-8 bytes diff --git a/src/main/java/org/apache/accumulo/access/AeNode.java b/src/main/java/org/apache/accumulo/access/AeNode.java index 65023b4..dc04f6f 100644 --- a/src/main/java/org/apache/accumulo/access/AeNode.java +++ b/src/main/java/org/apache/accumulo/access/AeNode.java @@ -18,6 +18,9 @@ */ package org.apache.accumulo.access; +import static org.apache.accumulo.access.ByteUtils.AND_OPERATOR; +import static org.apache.accumulo.access.ByteUtils.OR_OPERATOR; + import java.util.List; import java.util.TreeSet; import java.util.function.Consumer; @@ -300,9 +303,9 @@ abstract class AeNode implements Comparable<AeNode> { static AeNode of(byte operator, List<AeNode> children) { switch (operator) { - case '&': + case AND_OPERATOR: return new AndNode(children); - case '|': + case OR_OPERATOR: return new OrNode(children); default: throw new IllegalArgumentException(); diff --git a/src/main/java/org/apache/accumulo/access/Authorizations.java b/src/main/java/org/apache/accumulo/access/Authorizations.java index cbeccb9..33f3f87 100644 --- a/src/main/java/org/apache/accumulo/access/Authorizations.java +++ b/src/main/java/org/apache/accumulo/access/Authorizations.java @@ -22,6 +22,7 @@ import java.util.Collection; import java.util.Set; /** + * A collection of authorization strings. * * @since 1.0.0 */ diff --git a/src/main/java/org/apache/accumulo/access/ByteUtils.java b/src/main/java/org/apache/accumulo/access/ByteUtils.java new file mode 100644 index 0000000..4f75410 --- /dev/null +++ b/src/main/java/org/apache/accumulo/access/ByteUtils.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.accumulo.access; + +/** + * This class exists to avoid repeat conversions from byte to char as well as to provide helper + * methods for comparing them. + */ +public final class ByteUtils { + public static final byte QUOTE = (byte) '"'; + public static final byte BACKSLASH = (byte) '\\'; + public static final byte AND_OPERATOR = (byte) '&'; + public static final byte OR_OPERATOR = (byte) '|'; + + private ByteUtils() { + // private constructor to prevent instantiation + } + + public static boolean isQuoteSymbol(byte b) { + return b == QUOTE; + } + + public static boolean isBackslashSymbol(byte b) { + return b == BACKSLASH; + } + + public static boolean isQuoteOrSlash(byte b) { + return isQuoteSymbol(b) || isBackslashSymbol(b); + } + + public static boolean isAndOperator(byte b) { + return b == AND_OPERATOR; + } + + public static boolean isOrOperator(byte b) { + return b == OR_OPERATOR; + } + + public static boolean isAndOrOperator(byte b) { + return isAndOperator(b) || isOrOperator(b); + } +} diff --git a/src/main/java/org/apache/accumulo/access/BytesWrapper.java b/src/main/java/org/apache/accumulo/access/BytesWrapper.java index ff5bb07..998a452 100644 --- a/src/main/java/org/apache/accumulo/access/BytesWrapper.java +++ b/src/main/java/org/apache/accumulo/access/BytesWrapper.java @@ -53,9 +53,17 @@ class BytesWrapper implements Comparable<BytesWrapper> { */ public BytesWrapper(byte[] data, int offset, int length) { - if (offset < 0 || offset > data.length || length < 0 || (offset + length) > data.length) { - throw new IllegalArgumentException(" Bad offset and/or length data.length = " + data.length - + " offset = " + offset + " length = " + length); + if (offset < 0 || offset > data.length) { + throw new IllegalArgumentException( + "Offset out of bounds. data.length = " + data.length + ", offset = " + offset); + } + if (length < 0) { + throw new IllegalArgumentException("Length cannot be negative. length = " + length); + } + if ((offset + length) > data.length) { + throw new IllegalArgumentException( + "Sum of offset and length exceeds data length. data.length = " + data.length + + ", offset = " + offset + ", length = " + length); } this.data = data; diff --git a/src/main/java/org/apache/accumulo/access/Parser.java b/src/main/java/org/apache/accumulo/access/Parser.java index 8eb822e..17b7f7a 100644 --- a/src/main/java/org/apache/accumulo/access/Parser.java +++ b/src/main/java/org/apache/accumulo/access/Parser.java @@ -18,12 +18,18 @@ */ package org.apache.accumulo.access; +import static org.apache.accumulo.access.ByteUtils.isAndOrOperator; + import java.util.ArrayList; /** * Code for parsing an access expression and creating a parse tree of type {@link AeNode} */ final class Parser { + + public static final byte OPEN_PAREN = (byte) '('; + public static final byte CLOSE_PAREN = (byte) ')'; + public static AeNode parseAccessExpression(byte[] expression) { Tokenizer tokenizer = new Tokenizer(expression); @@ -46,7 +52,7 @@ final class Parser { AeNode first = parseParenExpressionOrAuthorization(tokenizer); - if (tokenizer.hasNext() && (tokenizer.peek() == '&' || tokenizer.peek() == '|')) { + if (tokenizer.hasNext() && isAndOrOperator(tokenizer.peek())) { var nodes = new ArrayList<AeNode>(); nodes.add(first); @@ -59,7 +65,7 @@ final class Parser { } while (tokenizer.hasNext() && tokenizer.peek() == operator); - if (tokenizer.hasNext() && (tokenizer.peek() == '|' || tokenizer.peek() == '&')) { + if (tokenizer.hasNext() && isAndOrOperator(tokenizer.peek())) { // A case of mixed operators, lets give a clear error message tokenizer.error("Cannot mix '|' and '&'"); } @@ -76,10 +82,10 @@ final class Parser { .error("Expected a '(' character or an authorization token instead saw end of input"); } - if (tokenizer.peek() == '(') { + if (tokenizer.peek() == OPEN_PAREN) { tokenizer.advance(); var node = parseExpression(tokenizer); - tokenizer.next(')'); + tokenizer.next(CLOSE_PAREN); return node; } else { return AeNode.of(tokenizer.nextAuthorization()); diff --git a/src/main/java/org/apache/accumulo/access/Tokenizer.java b/src/main/java/org/apache/accumulo/access/Tokenizer.java index de5e21f..b5e3f14 100644 --- a/src/main/java/org/apache/accumulo/access/Tokenizer.java +++ b/src/main/java/org/apache/accumulo/access/Tokenizer.java @@ -19,6 +19,11 @@ package org.apache.accumulo.access; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.accumulo.access.ByteUtils.isBackslashSymbol; +import static org.apache.accumulo.access.ByteUtils.isQuoteOrSlash; +import static org.apache.accumulo.access.ByteUtils.isQuoteSymbol; + +import java.util.stream.IntStream; /** * A simple wrapper around a byte array that keeps some state and provides high level operations to @@ -30,37 +35,24 @@ final class Tokenizer { private static final boolean[] validAuthChars = new boolean[256]; static { - for (int i = 0; i < 256; i++) { - validAuthChars[i] = false; - } - - for (int i = 'a'; i <= 'z'; i++) { - validAuthChars[i] = true; - } + IntStream.range(0, 256).forEach(i -> validAuthChars[i] = false); - for (int i = 'A'; i <= 'Z'; i++) { - validAuthChars[i] = true; - } - - for (int i = '0'; i <= '9'; i++) { - validAuthChars[i] = true; - } + IntStream numbers = IntStream.rangeClosed('0', '9'); + IntStream letters = + IntStream.concat(IntStream.rangeClosed('A', 'Z'), IntStream.rangeClosed('a', 'z')); + IntStream.concat(numbers, letters).forEach(i -> validAuthChars[i] = true); - validAuthChars['_'] = true; - validAuthChars['-'] = true; - validAuthChars[':'] = true; - validAuthChars['.'] = true; - validAuthChars['/'] = true; + "_-:./".chars().forEach(c -> validAuthChars[c] = true); } static boolean isValidAuthChar(byte b) { return validAuthChars[0xff & b]; } - private byte[] expression; + private final byte[] expression; private int index; - private AuthorizationToken authorizationToken = new AuthorizationToken(); + private final AuthorizationToken authorizationToken = new AuthorizationToken(); static class AuthorizationToken { byte[] data; @@ -81,13 +73,13 @@ final class Tokenizer { index++; } - public void next(char expected) { + public void next(byte expected) { if (!hasNext()) { - error("Expected '" + expected + "' instead saw end of input"); + error("Expected '" + (char) expected + "' instead saw end of input"); } if (expression[index] != expected) { - error("Expected '" + expected + "' instead saw '" + (char) (expression[index]) + "'"); + error("Expected '" + (char) expected + "' instead saw '" + (char) (expression[index]) + "'"); } index++; } @@ -105,14 +97,13 @@ final class Tokenizer { } AuthorizationToken nextAuthorization() { - if (expression[index] == '"') { + if (isQuoteSymbol(expression[index])) { int start = ++index; - while (index < expression.length && expression[index] != '"') { - if (expression[index] == '\\') { + while (index < expression.length && !isQuoteSymbol(expression[index])) { + if (isBackslashSymbol(expression[index])) { index++; - if (index == expression.length - || (expression[index] != '\\' && expression[index] != '"')) { + if (index == expression.length || !isQuoteOrSlash(expression[index])) { error("Invalid escaping within quotes", index - 1); } }