Correct instance-identifier escaping 57/103957/4
authorRobert Varga <robert.varga@pantheon.tech>
Tue, 3 Jan 2023 15:39:04 +0000 (16:39 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sat, 7 Jan 2023 19:07:28 +0000 (20:07 +0100)
Dealing with single-quoted and double-quoted strings in
instance-identifier differs quite a bit. In order to deal with these
strings, we need to differentiate them in lexer, for which we create
separate modes.

This forces us to explicitly define lexer lokens and only match raw
strings in their mode -- ensuring there are no surprises.

JIRA: YANGTOOLS-1458
Change-Id: I3f58c10f068da1128d8a7c1c5bed3917bfea0c78
Signed-off-by: Ruslan Kashapov <ruslan.kashapov@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit 45e8951a576c6bf2cb2bd0290167619b8be2fc5a)

xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierLexer.g4 [moved from xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifier.g4 with 51% similarity]
xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierParser.g4 [new file with mode: 0644]
xpath/yang-xpath-impl/src/main/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParser.java
xpath/yang-xpath-impl/src/test/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParserTest.java [new file with mode: 0644]

similarity index 51%
rename from xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifier.g4
rename to xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierLexer.g4
index 389cc4437668a9a12b0927e6438f7eafaec60085..051e7bddb28cc0498f7e8e4a99def9f8685471a0 100644 (file)
@@ -1,4 +1,4 @@
-grammar instanceIdentifier;
+lexer grammar instanceIdentifierLexer;
 
 /*
  * YANG 1.1 instance-identifier grammar, as defined in
@@ -10,59 +10,53 @@ grammar instanceIdentifier;
  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
  * and is available at http://www.eclipse.org/legal/epl-v10.html
  */
-instanceIdentifier : ('/' pathArgument)+ EOF
-  ;
-
-pathArgument : nodeIdentifier predicate?
-  ;
-
-nodeIdentifier : Identifier ':' Identifier
-  ;
-
-predicate : keyPredicate+
-  | leafListPredicate
-  | pos
-  ;
-
-keyPredicate : '[' WSP? keyPredicateExpr WSP? ']'
-  ;
-
-keyPredicateExpr  : nodeIdentifier eqQuotedString
-  ;
-
-leafListPredicate : '[' WSP? leafListPredicateExpr WSP? ']'
-  ;
-
-leafListPredicateExpr : '.' eqQuotedString
-  ;
-
-// Common tail of leafListPredicateExpr and keyPredicateExpr
-eqQuotedString : WSP? '=' WSP? quotedString
-  ;
-
-pos : '[' WSP? PositiveIntegerValue WSP? ']'
-  ;
-
-quotedString : '\'' STRING '\''
-  | '"' STRING '"'
-  ;
+COLON : ':' ;
+DOT : '.' ;
+EQ : '=' ;
+LBRACKET : '[' ;
+RBRACKET : ']' ;
+SLASH : '/' ;
 
 Identifier : [a-zA-Z][a-zA-Z0-9_\-.]*
   ;
 
+// Note: XPath elements are counted from 1, not from 0, as per
+//       https://mailarchive.ietf.org/arch/msg/netmod/xSR_hu6ry4EWfrvIY5NqcmC7ZoM/
 PositiveIntegerValue : [1-9][0-9]*
   ;
 
-STRING : YANGCHAR+
-  ;
-
 WSP : [ \t]+
   ;
 
+// Double/single-quoted strings. We deal with these using specialized modes.
+DQUOT_START : '"' -> pushMode(DQUOT_STRING_MODE), skip;
+SQUOT_START : '\'' -> pushMode(SQUOT_STRING_MODE), skip;
+
+//
+// Double-quoted string lexing mode. We interpret \n, \t, \", \\ only, as per RFC7950.
+//
+mode DQUOT_STRING_MODE;
+DQUOT_STRING : (YANGCHAR | '\'' | ('\\' [nt"\\]))+ ;
+DQUOT_END : '"' -> popMode;
+
+//
+// Single-quoted string lexing mode. We do not interpret anything within single
+// quotes.
+//
+mode SQUOT_STRING_MODE;
+SQUOT_STRING : (YANGCHAR | '"' | '\\')+ ;
+SQUOT_END : '\'' -> popMode;
+
 fragment
 YANGCHAR : '\t'..'\n'
   | '\r'
-  | '\u0020'..'\uD7FF'
+
+  // '\u0020'..'\uD7FF' without "'", '"' and '\'
+  | '\u0020'..'\u0021' // 0x22 = "
+  | '\u0023'..'\u0026' // 0x27 = '
+  | '\u0028'..'\u005B' // 0x5C = \
+  | '\u005D'..'\uD7FF'
+
   | '\uE000'..'\uFDCF'
   | '\uFDF0'..'\uFFFD'
   | '\u{10000}'..'\u{1FFFD}'
diff --git a/xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierParser.g4 b/xpath/yang-xpath-impl/src/main/antlr4/org/opendaylight/yangtools/yang/xpath/antlr/instanceIdentifierParser.g4
new file mode 100644 (file)
index 0000000..223b0b4
--- /dev/null
@@ -0,0 +1,53 @@
+parser grammar instanceIdentifierParser;
+
+/*
+ * YANG 1.1 instance-identifier grammar, as defined in
+ * https://tools.ietf.org/html/rfc7950#section-9.13
+ *
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+options {
+    tokenVocab = instanceIdentifierLexer;
+}
+
+instanceIdentifier : (SLASH pathArgument)+ EOF
+  ;
+
+pathArgument : nodeIdentifier predicate?
+  ;
+
+nodeIdentifier : Identifier COLON Identifier
+  ;
+
+predicate : keyPredicate+
+  | leafListPredicate
+  | pos
+  ;
+
+keyPredicate : LBRACKET WSP? keyPredicateExpr WSP? RBRACKET
+  ;
+
+keyPredicateExpr : nodeIdentifier eqQuotedString
+  ;
+
+leafListPredicate : LBRACKET WSP? leafListPredicateExpr WSP? RBRACKET
+  ;
+
+leafListPredicateExpr : DOT eqQuotedString
+  ;
+
+// Common tail of leafListPredicateExpr and keyPredicateExpr
+eqQuotedString : WSP? EQ WSP? quotedString
+  ;
+
+pos : LBRACKET WSP? PositiveIntegerValue WSP? RBRACKET
+  ;
+
+quotedString : SQUOT_STRING? SQUOT_END
+  | DQUOT_STRING? DQUOT_END
+  ;
+
index ab149ad662492a9b10501de96994a9ae0049b63f..077107063a153e3fe9e839e2a008eb5938056d6e 100644 (file)
@@ -10,8 +10,10 @@ package org.opendaylight.yangtools.yang.xpath.impl;
 import static java.util.Objects.requireNonNull;
 import static org.opendaylight.yangtools.yang.xpath.impl.ParseTreeUtils.getChild;
 import static org.opendaylight.yangtools.yang.xpath.impl.ParseTreeUtils.illegalShape;
+import static org.opendaylight.yangtools.yang.xpath.impl.ParseTreeUtils.verifyTerminal;
 import static org.opendaylight.yangtools.yang.xpath.impl.ParseTreeUtils.verifyToken;
 
+import com.google.common.base.VerifyException;
 import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -25,7 +27,6 @@ import org.opendaylight.yangtools.yang.common.YangNamespaceContext;
 import org.opendaylight.yangtools.yang.xpath.antlr.instanceIdentifierLexer;
 import org.opendaylight.yangtools.yang.xpath.antlr.instanceIdentifierParser;
 import org.opendaylight.yangtools.yang.xpath.antlr.instanceIdentifierParser.EqQuotedStringContext;
-import org.opendaylight.yangtools.yang.xpath.antlr.instanceIdentifierParser.InstanceIdentifierContext;
 import org.opendaylight.yangtools.yang.xpath.antlr.instanceIdentifierParser.KeyPredicateContext;
 import org.opendaylight.yangtools.yang.xpath.antlr.instanceIdentifierParser.KeyPredicateExprContext;
 import org.opendaylight.yangtools.yang.xpath.antlr.instanceIdentifierParser.LeafListPredicateContext;
@@ -92,15 +93,15 @@ abstract class InstanceIdentifierParser {
     }
 
     final Absolute interpretAsInstanceIdentifier(final YangLiteralExpr expr) throws XPathExpressionException {
-        final instanceIdentifierLexer lexer = new instanceIdentifierLexer(CharStreams.fromString(expr.getLiteral()));
-        final instanceIdentifierParser parser = new instanceIdentifierParser(new CommonTokenStream(lexer));
-        final CapturingErrorListener listener = new CapturingErrorListener();
+        final var lexer = new instanceIdentifierLexer(CharStreams.fromString(expr.getLiteral()));
+        final var parser = new instanceIdentifierParser(new CommonTokenStream(lexer));
+        final var listener = new CapturingErrorListener();
         lexer.removeErrorListeners();
         lexer.addErrorListener(listener);
         parser.removeErrorListeners();
         parser.addErrorListener(listener);
 
-        final InstanceIdentifierContext id = parser.instanceIdentifier();
+        final var id = parser.instanceIdentifier();
         listener.reportError();
 
         final int length = id.getChildCount();
@@ -166,7 +167,66 @@ abstract class InstanceIdentifierParser {
     }
 
     private static YangLiteralExpr parseEqStringValue(final EqQuotedStringContext expr) {
-        return YangLiteralExpr.of(verifyToken(getChild(expr, QuotedStringContext.class, expr.getChildCount() - 1), 1,
-            instanceIdentifierParser.STRING).getText());
+        final var quotedString = getChild(expr, QuotedStringContext.class, expr.getChildCount() - 1);
+        switch (quotedString.getChildCount()) {
+            case 1:
+                return YangLiteralExpr.empty();
+            case 2:
+                final var terminal = verifyTerminal(quotedString.getChild(0));
+                final var token = terminal.getSymbol();
+                final String literal;
+                switch (token.getType()) {
+                    case instanceIdentifierParser.DQUOT_STRING:
+                        literal = unescape(token.getText());
+                        break;
+                    case instanceIdentifierParser.SQUOT_STRING:
+                        literal = token.getText();
+                        break;
+                    default:
+                        throw illegalShape(terminal);
+                }
+                return YangLiteralExpr.of(literal);
+            default:
+                throw illegalShape(quotedString);
+        }
+    }
+
+    private static String unescape(final String escaped) {
+        final int firstEscape = escaped.indexOf('\\');
+        return firstEscape == -1 ? escaped : unescape(escaped, firstEscape);
+    }
+
+    private static String unescape(final String escaped, final int firstEscape) {
+        // Sizing: optimize allocation for only a single escape
+        final int length = escaped.length();
+        final var sb = new StringBuilder(length - 1).append(escaped, 0, firstEscape);
+
+        int esc = firstEscape;
+        while (true) {
+            sb.append(replace(escaped.charAt(esc + 1)));
+
+            final int start = esc + 2;
+            esc = escaped.indexOf('\\', start);
+            if (esc == -1) {
+                return sb.append(escaped, start, length).toString();
+            }
+
+            sb.append(escaped, start, esc);
+        }
+    }
+
+    private static char replace(final char ch) {
+        switch (ch) {
+            case 'n':
+                return '\n';
+            case 't':
+                return '\t';
+            case '"':
+                return '"';
+            case '\\':
+                return '\\';
+            default:
+                throw new VerifyException("Unexpected escaped char '" + ch + "'");
+        }
     }
 }
diff --git a/xpath/yang-xpath-impl/src/test/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParserTest.java b/xpath/yang-xpath-impl/src/test/java/org/opendaylight/yangtools/yang/xpath/impl/InstanceIdentifierParserTest.java
new file mode 100644 (file)
index 0000000..32e0c5f
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.yangtools.yang.xpath.impl;
+
+import com.google.common.collect.ImmutableBiMap;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.common.BiMapYangNamespaceContext;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.common.XMLNamespace;
+import org.opendaylight.yangtools.yang.common.YangNamespaceContext;
+import org.opendaylight.yangtools.yang.xpath.api.YangLiteralExpr;
+import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.Absolute;
+import org.opendaylight.yangtools.yang.xpath.api.YangXPathMathMode;
+
+class InstanceIdentifierParserTest {
+    private static final QNameModule DEFNS = QNameModule.create(XMLNamespace.of("defaultns"));
+    private static final YangNamespaceContext CONTEXT = new BiMapYangNamespaceContext(ImmutableBiMap.of(
+        "def", DEFNS,
+        "foo", QNameModule.create(XMLNamespace.of("foo")),
+        "bar", QNameModule.create(XMLNamespace.of("bar"))));
+
+    private static final InstanceIdentifierParser PARSER =
+        new InstanceIdentifierParser.Qualified(YangXPathMathMode.IEEE754, CONTEXT);
+
+    @Test
+    void smokeTests() throws Exception {
+        assertPath("/foo:foo");
+        assertPath("/foo:foo[.='abcd']");
+        assertPath("/foo:foo[.=\"abcd\"]");
+    }
+
+    private static Absolute assertPath(final String literal) throws Exception {
+        return PARSER.interpretAsInstanceIdentifier(YangLiteralExpr.of(literal));
+    }
+}