Bug 5410 - XSD regular expressions are interpreted as Java regexes (1/2) 99/52999/6
authorPeter Kajsa <pkajsa@cisco.com>
Wed, 8 Mar 2017 12:54:22 +0000 (13:54 +0100)
committerPeter Kajsa <pkajsa@cisco.com>
Thu, 9 Mar 2017 12:58:53 +0000 (12:58 +0000)
As both '^' and '$' are special anchor characters in java regular
expressions which are implicitly present in XSD regular expressions,
we need to escape them in case they are not defined as part of
character ranges i.e. inside regular square brackets.

Change-Id: Iafbf350f88ebdf96c30e1ccedbd00b90a93d521a
Signed-off-by: Peter Kajsa <pkajsa@cisco.com>
yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/type/PatternConstraint.java
yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/SchemaContextEmitter.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/PatternStatementImpl.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/effective/type/AbstractConstraintEffectiveStatement.java
yang/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/effective/type/PatternConstraintEffectiveImpl.java
yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/Bug5410Test.java [new file with mode: 0644]
yang/yang-parser-impl/src/test/resources/bugs/bug5410/foo.yang [new file with mode: 0644]

index 14844c64a9d776aeabcc78d86dc0a1694bac6bfb..a20c4d383494d66c919b311ad416640bdaf84da8 100644 (file)
@@ -17,13 +17,23 @@ import org.opendaylight.yangtools.yang.model.api.ConstraintMetaDefinition;
 public interface PatternConstraint extends ConstraintMetaDefinition {
 
     /**
-     * Returns a regular expression (pattern).
+     * Returns a java regular expression (pattern).
      *
-     * @return string with regular expression which is equal to the argument of
+     * @return string with java regular expression which is equal to the argument of
      *         the YANG <code>pattern</code> substatement
      */
     String getRegularExpression();
 
+    /**
+     * Returns a raw regular expression as it was declared in a source.
+     *
+     * @return argument of pattern statement as it was declared in a source.
+     */
+    // FIXME: version 2.0.0: make this method non-default
+    default String getRawRegularExpression() {
+        return getRegularExpression();
+    }
+
     /**
      * All implementations should override this method.
      * The default definition of this method is used only in YANG 1.0 (RFC6020)
index ead600cbc7edac3b7d034646b7ce72f802b3e532..9ec95a5b119b1489acd0ac2d645a1313b752bb51 100644 (file)
@@ -573,7 +573,7 @@ class SchemaContextEmitter {
     }
 
     private void emitPatternNode(final PatternConstraint pattern) {
-        writer.startPatternNode(pattern.getRegularExpression());
+        writer.startPatternNode(pattern.getRawRegularExpression());
         emitErrorMessageNode(pattern.getErrorMessage()); // FIXME: BUG-2444: Optional
         emitErrorAppTagNode(pattern.getErrorAppTag()); // FIXME: BUG-2444: Optional
         emitDescriptionNode(pattern.getDescription());
index e711624e070a67573fa23d6a241714bf0e419dd2..d899ed879fbc1d8044f239e9ca45663f92f9443a 100644 (file)
@@ -53,7 +53,7 @@ public class PatternStatementImpl extends AbstractDeclaredStatement<PatternConst
 
         @Override
         public PatternConstraint parseArgumentValue(final StmtContext<?, ?, ?> ctx, final String value) {
-            final String pattern = "^" + Utils.fixUnicodeScriptPattern(value) + '$';
+            final String pattern = getJavaRegexFromXSD(value);
 
             try {
                 Pattern.compile(pattern);
@@ -62,7 +62,58 @@ public class PatternStatementImpl extends AbstractDeclaredStatement<PatternConst
                 return null;
             }
 
-            return new PatternConstraintEffectiveImpl(pattern, Optional.absent(), Optional.absent());
+            return new PatternConstraintEffectiveImpl(pattern, value, Optional.absent(), Optional.absent());
+        }
+
+        static String getJavaRegexFromXSD(final String xsdRegex) {
+            return "^" + Utils.fixUnicodeScriptPattern(escapeChars(xsdRegex)) + '$';
+        }
+
+        /*
+         * As both '^' and '$' are special anchor characters in java regular
+         * expressions which are implicitly present in XSD regular expressions,
+         * we need to escape them in case they are not defined as part of
+         * character ranges i.e. inside regular square brackets.
+         */
+        private static String escapeChars(final String regex) {
+            final StringBuilder result = new StringBuilder(regex.length());
+            int bracket = 0;
+            boolean escape = false;
+            for (int i = 0; i < regex.length(); i++) {
+                final char ch = regex.charAt(i);
+                switch (ch) {
+                case '[':
+                    if (!escape) {
+                        bracket++;
+                    }
+                    escape = false;
+                    result.append(ch);
+                    break;
+                case ']':
+                    if (!escape) {
+                        bracket--;
+                    }
+                    escape = false;
+                    result.append(ch);
+                    break;
+                case '\\':
+                    escape = !escape;
+                    result.append(ch);
+                    break;
+                case '^':
+                case '$':
+                    if (bracket == 0) {
+                        result.append('\\');
+                    }
+                    escape = false;
+                    result.append(ch);
+                    break;
+                default:
+                    escape = false;
+                    result.append(ch);
+                }
+            }
+            return result.toString();
         }
 
         @Override
index a852e4e8390d2eb3dbdfdb31d57942b52316fde7..d534bbb4b06cff3d7f84fdc4dbb97090b144a881 100644 (file)
@@ -152,7 +152,8 @@ final class PatternConstraintFactory extends ConstraintFactory<PatternConstraint
 
     private static PatternConstraint createCustomizedConstraint(final PatternConstraint patternConstraint,
             final AbstractConstraintEffectiveStatement<?, ?> stmt) {
-        return new PatternConstraintEffectiveImpl(patternConstraint.getRegularExpression(), stmt.getDescription(),
-                stmt.getReference(), stmt.getErrorAppTag(), stmt.getErrorMessage(), stmt.getModifier());
+        return new PatternConstraintEffectiveImpl(patternConstraint.getRegularExpression(),
+                patternConstraint.getRawRegularExpression(), stmt.getDescription(), stmt.getReference(),
+                stmt.getErrorAppTag(), stmt.getErrorMessage(), stmt.getModifier());
     }
 }
\ No newline at end of file
index ace88ab63f54c62761ddeeee96cbe16bb58d7fa0..95c4d89f99bc708b2704c0d27bfe078c4a06f8ee 100644 (file)
@@ -17,21 +17,23 @@ import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
 public class PatternConstraintEffectiveImpl implements PatternConstraint {
 
     private final String regEx;
+    private final String rawRegEx;
     private final String description;
     private final String reference;
     private final String errorAppTag;
     private final String errorMessage;
     private final ModifierKind modifier;
 
-    public PatternConstraintEffectiveImpl(final String regex, final Optional<String> description,
-            final Optional<String> reference) {
-        this(regex, description.orNull(), reference.orNull(), null, null, null);
+    public PatternConstraintEffectiveImpl(final String regex, final String rawRegex,
+            final Optional<String> description, final Optional<String> reference) {
+        this(regex, rawRegex, description.orNull(), reference.orNull(), null, null, null);
     }
 
-    public PatternConstraintEffectiveImpl(final String regex, final String description, final String reference,
-            final String errorAppTag, final String errorMessage, final ModifierKind modifier) {
+    public PatternConstraintEffectiveImpl(final String regex, final String rawRegex, final String description,
+            final String reference, final String errorAppTag, final String errorMessage, final ModifierKind modifier) {
         super();
         this.regEx = Preconditions.checkNotNull(regex, "regex must not be null.");
+        this.rawRegEx = Preconditions.checkNotNull(rawRegex, "raw regex must not be null.");
         this.description = description;
         this.reference = reference;
         this.errorAppTag = errorAppTag != null ? errorAppTag : "invalid-regular-expression";
@@ -45,6 +47,11 @@ public class PatternConstraintEffectiveImpl implements PatternConstraint {
         return regEx;
     }
 
+    @Override
+    public String getRawRegularExpression() {
+        return rawRegEx;
+    }
+
     @Override
     public String getDescription() {
         return description;
diff --git a/yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/Bug5410Test.java b/yang/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/parser/stmt/rfc6020/Bug5410Test.java
new file mode 100644 (file)
index 0000000..525302b
--- /dev/null
@@ -0,0 +1,238 @@
+/*
+ * Copyright (c) 2017 Cisco Systems, Inc. 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.parser.stmt.rfc6020;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
+import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
+import org.opendaylight.yangtools.yang.stmt.StmtTestUtils;
+
+public class Bug5410Test {
+    private static final String FOO_NS = "foo";
+    private static final String FOO_REV = "1970-01-01";
+
+    @Test
+    public void testJavaRegexFromXSD() {
+        testPattern("^[^:]+$", "^\\^[^:]+\\$$", ImmutableList.of("^a$", "^abc$"),
+                ImmutableList.of("abc$", "^abc", "^a:bc$"));
+        testPattern("^[$^]$", "^\\^[$^]\\$$", ImmutableList.of("^^$", "^$$"), ImmutableList.of("^^", "^$", "$^", "$$"));
+        testPattern("[$-%]+", "^[$-%]+$", ImmutableList.of("$", "%", "%$"), ImmutableList.of("$-", "$-%", "-", "^"));
+        testPattern("[$-&]+", "^[$-&]+$", ImmutableList.of("$", "%&", "%$", "$%&"), ImmutableList.of("#", "$-&", "'"));
+
+        testPattern("[a-z&&[^m-p]]+", "^[a-z&&[^m-p]]+$", ImmutableList.of("a", "z", "az"),
+                ImmutableList.of("m", "anz", "o"));
+        testPattern("^[\\[-b&&[^^-a]]+$", "^\\^[\\[-b&&[^^-a]]+\\$$", ImmutableList.of("^[$", "^\\$", "^]$", "^b$"),
+                ImmutableList.of("^a$", "^^$", "^_$"));
+
+        testPattern("[^^-~&&[^$-^]]", "^[^^-~&&[^$-^]]$", ImmutableList.of("!", "\"", "#"),
+                ImmutableList.of("a", "A", "z", "Z", "$", "%", "^", "}"));
+        testPattern("\\\\\\[^[^^-~&&[^$-^]]", "^\\\\\\[\\^[^^-~&&[^$-^]]$",
+                ImmutableList.of("\\[^ ", "\\[^!", "\\[^\"", "\\[^#"),
+                ImmutableList.of("\\[^a", "\\[^A", "\\[^z", "\\[^Z", "\\[^$", "\\[^%", "\\[^^", "\\[^}"));
+        testPattern("^\\[^\\\\[^^-b&&[^\\[-\\]]]\\]^", "^\\^\\[\\^\\\\[^^-b&&[^\\[-\\]]]\\]\\^$",
+                ImmutableList.of("^[^\\c]^", "^[^\\Z]^"),
+                ImmutableList.of("^[^\\[]^", "^[^\\\\]^", "^[^\\]]^", "^[^\\^]^", "^[^\\_]^", "^[^\\b]^"));
+        testPattern("[\\^]$", "^[\\^]\\$$", ImmutableList.of("^$"),
+                ImmutableList.of("^", "$", "$^", "\\", "\\^", "\\^\\", "\\^\\$"));
+    }
+
+    @Test
+    public void testInvalidXSDRegexes() throws UnsupportedEncodingException {
+        testInvalidPattern("$^a^[$^\\]", "Unclosed character class");
+        testInvalidPattern("$(\\)", "Unclosed group");
+    }
+
+    @Test
+    public void testJavaPattern() {
+        testPattern("^[$^]+$", ImmutableList.of("$^", "^", "$"), ImmutableList.of("\\", "a"));
+        testPattern("^[^$-^]$", ImmutableList.of("a", "_", "#"), ImmutableList.of("%", "^", "$", "]", "\\"));
+    }
+
+    @Test
+    public void testYangPattern() throws Exception {
+        final SchemaContext context = StmtTestUtils.parseYangSources("/bugs/bug5410");
+        assertNotNull(context);
+
+        final PatternConstraint pattern = getPatternConstraintOf(context, "leaf-with-pattern");
+
+        final String rawRegex = pattern.getRawRegularExpression();
+        final String expectedYangRegex = "$0$.*|$1$[a-zA-Z0-9./]{1,8}$[a-zA-Z0-9./]{22}|$5$(rounds=\\d+$)?[a-zA-Z0-9./]{1,16}$[a-zA-Z0-9./]{43}|$6$(rounds=\\d+$)?[a-zA-Z0-9./]{1,16}$[a-zA-Z0-9./]{86}";
+        assertEquals(expectedYangRegex, rawRegex);
+
+        final String javaRegexFromYang = pattern.getRegularExpression();
+        final String expectedJavaRegex = "^\\$0\\$.*|\\$1\\$[a-zA-Z0-9./]{1,8}\\$[a-zA-Z0-9./]{22}|\\$5\\$(rounds=\\d+\\$)?[a-zA-Z0-9./]{1,16}\\$[a-zA-Z0-9./]{43}|\\$6\\$(rounds=\\d+\\$)?[a-zA-Z0-9./]{1,16}\\$[a-zA-Z0-9./]{86}$";
+        assertEquals(expectedJavaRegex, javaRegexFromYang);
+
+        final String value = "$6$AnrKGc0V$B/0/A.pWg4HrrA6YiEJOtFGibQ9Fmm5.4rI/00gEz3QeB7joSxBU3YtbHDm6NSkS1dKTQy3BWhwKKDS8nB5S//";
+        testPattern(javaRegexFromYang, ImmutableList.of(value), ImmutableList.of());
+    }
+
+    @Test
+    public void testCaret() {
+        testPattern("^", "\\^");
+    }
+
+    @Test
+    public void testTextCaret() {
+        testPattern("abc^", "abc\\^");
+    }
+
+    @Test
+    public void testTextDollar() {
+        testPattern("abc$", "abc\\$");
+    }
+
+    @Test
+    public void testCaretCaret() {
+        testPattern("^^", "\\^\\^");
+    }
+
+    @Test
+    public void testCaretDollar() {
+        testPattern("^$", "\\^\\$");
+    }
+
+    @Test
+    public void testDot() {
+        testPattern(".", ".");
+    }
+
+    @Test
+    public void testNotColon() {
+        testPattern("[^:]+", "[^:]+");
+    }
+
+    @Test
+    public void testDollar() {
+        testPattern("$", "\\$");
+    }
+
+    @Test
+    public void testDollarOneDollar() {
+        testPattern("$1$", "\\$1\\$");
+    }
+
+    @Test
+    public void testDollarPercentRange() {
+        testPattern("[$-%]+", "[$-%]+");
+    }
+
+    @Test
+    public void testDollarRange() {
+        testPattern("[$$]+", "[$$]+");
+    }
+
+    @Test
+    public void testDollarCaretRange() {
+        testPattern("[$^]+", "[$^]+");
+    }
+
+    @Test
+    public void testSimple() {
+        testPattern("abc", "abc");
+    }
+
+    @Test
+    public void testDotPlus() {
+        testPattern(".+", ".+");
+    }
+
+    @Test
+    public void testDotStar() {
+        testPattern(".*", ".*");
+    }
+
+    @Test
+    public void testSimpleOptional() {
+        testPattern("a?", "a?");
+    }
+
+    @Test
+    public void testRangeOptional() {
+        testPattern("[a-z]?", "[a-z]?");
+    }
+
+    private static void testPattern(final String xsdRegex, final String expectedJavaRegex,
+            final List<String> positiveMatches, final List<String> negativeMatches) {
+        final String javaRegexFromXSD = javaRegexFromXSD(xsdRegex);
+        assertEquals(expectedJavaRegex, javaRegexFromXSD);
+
+        for (final String value : positiveMatches) {
+            assertTrue("Value '" + value + "' does not match java regex '" + javaRegexFromXSD + "'",
+                    testMatch(javaRegexFromXSD, value));
+        }
+        for (final String value : negativeMatches) {
+            assertFalse("Value '" + value + "' matches java regex '" + javaRegexFromXSD + "'",
+                    testMatch(javaRegexFromXSD, value));
+        }
+    }
+
+    private static void testPattern(final String javaRegex, final List<String> positiveMatches,
+            final List<String> negativeMatches) {
+        for (final String value : positiveMatches) {
+            assertTrue("Value '" + value + "' does not match java regex '" + javaRegex + "'",
+                    testMatch(javaRegex, value));
+        }
+        for (final String value : negativeMatches) {
+            assertFalse("Value '" + value + "' matches java regex '" + javaRegex + "'", testMatch(javaRegex, value));
+        }
+    }
+
+    private static String javaRegexFromXSD(final String xsdRegex) {
+        return PatternStatementImpl.Definition.getJavaRegexFromXSD(xsdRegex);
+    }
+
+    private static boolean testMatch(final String javaRegex, final String value) {
+        return value.matches(javaRegex);
+    }
+
+    private static void testPattern(final String xsdRegex, final String unanchoredJavaRegex) {
+        testPattern(xsdRegex, '^' + unanchoredJavaRegex + '$', ImmutableList.of(), ImmutableList.of());
+    }
+
+    private static PatternConstraint getPatternConstraintOf(final SchemaContext context, final String leafName) {
+        final DataSchemaNode dataChildByName = context.getDataChildByName(foo(leafName));
+        assertTrue(dataChildByName instanceof LeafSchemaNode);
+        final LeafSchemaNode leaf = (LeafSchemaNode) dataChildByName;
+        final TypeDefinition<? extends TypeDefinition<?>> type = leaf.getType();
+        assertTrue(type instanceof StringTypeDefinition);
+        final StringTypeDefinition strType = (StringTypeDefinition) type;
+        return strType.getPatternConstraints().iterator().next();
+    }
+
+    private static QName foo(final String localName) {
+        return QName.create(FOO_NS, FOO_REV, localName);
+    }
+
+    private static void testInvalidPattern(final String xsdRegex, final String expectedMessage) throws UnsupportedEncodingException {
+        final PrintStream stdout = System.out;
+        final ByteArrayOutputStream output = new ByteArrayOutputStream();
+        System.setOut(new PrintStream(output, true, "UTF-8"));
+
+        javaRegexFromXSD(xsdRegex);
+
+        final String testLog = output.toString();
+        assertTrue(testLog.contains(expectedMessage));
+        System.setOut(stdout);
+    }
+}
diff --git a/yang/yang-parser-impl/src/test/resources/bugs/bug5410/foo.yang b/yang/yang-parser-impl/src/test/resources/bugs/bug5410/foo.yang
new file mode 100644 (file)
index 0000000..8c47a13
--- /dev/null
@@ -0,0 +1,10 @@
+module foo {
+    namespace foo;
+    prefix foo;
+
+    leaf leaf-with-pattern {
+        type string {
+            pattern "$0$.*|$1$[a-zA-Z0-9./]{1,8}$[a-zA-Z0-9./]{22}|$5$(rounds=\d+$)?[a-zA-Z0-9./]{1,16}$[a-zA-Z0-9./]{43}|$6$(rounds=\d+$)?[a-zA-Z0-9./]{1,16}$[a-zA-Z0-9./]{86}";
+        }
+    }
+}