Fix String value parsing/serialization
[yangtools.git] / data / yang-data-util / src / main / java / org / opendaylight / yangtools / yang / data / util / XpathStringParsingPathArgumentBuilder.java
index 11b54ddc1e7e78c09fb5d3830bf344bde1a66531..84000eea48959da96950ee3fac854e38dd9aa0e1 100644 (file)
@@ -52,14 +52,15 @@ final class XpathStringParsingPathArgumentBuilder implements Mutable {
     private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
             .or(CharMatcher.anyOf(".-")).precomputed();
 
-    private static final CharMatcher QUOTE = CharMatcher.anyOf("'\"");
-
     private static final char SLASH = '/';
+    private static final char BACKSLASH = '\\';
     private static final char COLON = ':';
     private static final char DOT = '.';
     private static final char EQUALS = '=';
     private static final char PRECONDITION_START = '[';
     private static final char PRECONDITION_END = ']';
+    private static final char SQUOT = '\'';
+    private static final char DQUOT = '"';
 
     private final List<PathArgument> product = new ArrayList<>();
     private final AbstractStringInstanceIdentifierCodec codec;
@@ -231,27 +232,84 @@ final class XpathStringParsingPathArgumentBuilder implements Mutable {
      */
     private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
         if (!condition) {
-            throw new IllegalArgumentException(String.format(
-                "Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s", data, offset,
-                String.format(errorMsg, attributes)));
+            throw iae(errorMsg, attributes);
         }
     }
 
+    private @NonNull IllegalArgumentException iae(final String errorMsg, final Object... attributes) {
+        return new IllegalArgumentException("Could not parse Instance Identifier '%s'. Offset: %s : Reason: %s"
+            .formatted(data, offset, errorMsg.formatted(attributes)));
+    }
+
     /**
      * Returns following value of quoted literal (without quotes) and sets offset after literal.
      *
      * @return String literal
      */
     private String nextQuotedValue() {
-        final char quoteChar = currentChar();
-        checkValid(QUOTE.matches(quoteChar), "Value must be qoute escaped with ''' or '\"'.");
+        return switch (currentChar()) {
+            case SQUOT -> nextSingleQuotedValue();
+            case DQUOT -> nextDoubleQuotedValue();
+            default -> throw iae("Value must be quote escaped with ''' or '\"'.");
+        };
+    }
+
+    // Simple: just look for the matching single quote and return substring
+    private String nextSingleQuotedValue() {
         skipCurrentChar();
-        final int valueStart = offset;
-        final int endQoute = data.indexOf(quoteChar, offset);
-        final String value = data.substring(valueStart, endQoute);
-        offset = endQoute;
+        final int start = offset;
+        final int end = data.indexOf(SQUOT, start);
+        checkValid(end != -1, "Closing single quote not found");
+        offset = end;
         skipCurrentChar();
-        return value;
+        return data.substring(start, end);
+    }
+
+    // Complicated: we need to potentially un-escape
+    private String nextDoubleQuotedValue() {
+        skipCurrentChar();
+
+        final int maxIndex = data.length() - 1;
+        final var sb = new StringBuilder();
+        while (true) {
+            final int nextStart = offset;
+
+            // Find next double quotes
+            final int nextEnd = data.indexOf(DQUOT, nextStart);
+            checkValid(nextEnd != -1, "Closing double quote not found");
+            offset = nextEnd;
+
+            // Find next backslash
+            final int nextBackslash = data.indexOf(BACKSLASH, nextStart);
+            if (nextBackslash == -1 || nextBackslash > nextEnd) {
+                // No backslash between nextStart and nextEnd -- just copy characters and terminate
+                offset = nextEnd;
+                skipCurrentChar();
+                return sb.append(data, nextStart, nextEnd).toString();
+            }
+
+            // Validate escape completeness and append buffer
+            checkValid(nextBackslash != maxIndex, "Incomplete escape");
+            sb.append(data, nextStart, nextBackslash);
+
+            // Adjust offset before potentially referencing it and
+            offset = nextBackslash;
+            sb.append(unescape(data.charAt(nextBackslash + 1)));
+
+            // Rinse and repeat
+            offset = nextBackslash + 2;
+        }
+    }
+
+    // As per https://www.rfc-editor.org/rfc/rfc7950#section-6.1.3
+    private char unescape(final char escape) {
+        return switch (escape) {
+            case 'n' -> '\n';
+            case 't' -> '\t';
+            case DQUOT -> DQUOT;
+            case BACKSLASH -> BACKSLASH;
+            default -> throw iae("Unrecognized escape");
+        };
     }
 
     /**