/* * Copyright (c) 2015 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.rfc7950.repo; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; import com.google.common.base.VerifyException; import java.util.List; import org.eclipse.jdt.annotation.NonNull; import org.opendaylight.yangtools.yang.common.YangVersion; import org.opendaylight.yangtools.yang.ir.IRArgument; import org.opendaylight.yangtools.yang.ir.IRArgument.Concatenation; import org.opendaylight.yangtools.yang.ir.IRArgument.Single; import org.opendaylight.yangtools.yang.model.api.meta.StatementSourceReference; import org.opendaylight.yangtools.yang.parser.spi.source.SourceException; /** * Utilities for dealing with YANG statement argument strings, encapsulated in ANTLR grammar's ArgumentContext. */ abstract class ArgumentContextUtils { /** * YANG 1.0 version of strings, which were not completely clarified in * RFC6020. */ private static final class RFC6020 extends ArgumentContextUtils { private static final @NonNull RFC6020 INSTANCE = new RFC6020(); @Override void checkDoubleQuoted(final String str, final StatementSourceReference ref, final int backslash) { // No-op } @Override void checkUnquoted(final String str, final StatementSourceReference ref) { // No-op } } /** * YANG 1.1 version of strings, which were clarified in * RFC7950. */ // NOTE: the differences clarified lead to a proper ability to delegate this to ANTLR lexer, but that does not // understand versions and needs to work with both. private static final class RFC7950 extends ArgumentContextUtils { private static final CharMatcher ANYQUOTE_MATCHER = CharMatcher.anyOf("'\""); private static final @NonNull RFC7950 INSTANCE = new RFC7950(); @Override void checkDoubleQuoted(final String str, final StatementSourceReference ref, final int backslash) { if (backslash < str.length() - 1) { int index = backslash; while (index != -1) { final var escape = str.charAt(index + 1); index = switch (escape) { case 'n', 't', '\\', '\"' -> str.indexOf('\\', index + 2); default -> throw new SourceException(ref, """ YANG 1.1: illegal double quoted string (%s). In double quoted string the backslash must be \ followed by one of the following character [n,t,",\\], but was '%s'.""", str, escape); }; } } } @Override void checkUnquoted(final String str, final StatementSourceReference ref) { SourceException.throwIf(ANYQUOTE_MATCHER.matchesAnyOf(str), ref, "YANG 1.1: unquoted string (%s) contains illegal characters", str); } } private ArgumentContextUtils() { // Hidden on purpose } static @NonNull ArgumentContextUtils forVersion(final YangVersion version) { return switch (version) { case VERSION_1 -> RFC6020.INSTANCE; case VERSION_1_1 -> RFC7950.INSTANCE; }; } // TODO: teach the only caller about versions, or provide common-enough idioms for its use case static @NonNull ArgumentContextUtils rfc6020() { return RFC6020.INSTANCE; } /* * NOTE: this method we do not use convenience methods provided by generated parser code, but instead are making * based on the grammar assumptions. While this is more verbose, it cuts out a number of unnecessary code, * such as intermediate List allocation et al. */ final @NonNull String stringFromStringContext(final IRArgument argument, final StatementSourceReference ref) { if (argument instanceof final Single single) { final var str = single.string(); if (single.needQuoteCheck()) { checkUnquoted(str, ref); } return single.needUnescape() ? unescape(str, ref) : str; } else if (argument instanceof Concatenation concat) { return concatStrings(concat.parts(), ref); } else { throw new VerifyException("Unexpected argument " + argument); } } private @NonNull String concatStrings(final List parts, final StatementSourceReference ref) { final var sb = new StringBuilder(); for (var part : parts) { sb.append(part.needUnescape() ? unescape(part.string(), ref) : part.string()); } return sb.toString(); } /* * NOTE: Enforcement and transformation logic done by these methods should logically reside in the lexer and ANTLR * account the for it with lexer modes. We do not want to force a re-lexing phase in the parser just because * we decided to let ANTLR do the work. */ abstract void checkDoubleQuoted(String str, StatementSourceReference ref, int backslash); abstract void checkUnquoted(String str, StatementSourceReference ref); private @NonNull String unescape(final String str, final StatementSourceReference ref) { // Now we need to perform some amount of unescaping. This serves as a pre-check before we dispatch // validation and processing (which will reuse the work we have done) final int backslash = str.indexOf('\\'); return backslash == -1 ? str : unescape(ref, str, backslash); } /* * Unescape escaped double quotes, tabs, new line and backslash in the inner string and trim the result. */ private @NonNull String unescape(final StatementSourceReference ref, final String str, final int backslash) { checkDoubleQuoted(str, ref, backslash); final var sb = new StringBuilder(str.length()); unescapeBackslash(sb, str, backslash); return sb.toString(); } @VisibleForTesting static void unescapeBackslash(final StringBuilder sb, final String str, final int backslash) { String substring = str; int backslashIndex = backslash; while (true) { int nextIndex = backslashIndex + 1; if (backslashIndex != -1 && nextIndex < substring.length()) { replaceBackslash(sb, substring, nextIndex); substring = substring.substring(nextIndex + 1); if (substring.length() > 0) { backslashIndex = substring.indexOf('\\'); } else { break; } } else { sb.append(substring); break; } } } private static void replaceBackslash(final StringBuilder sb, final String str, final int nextAfterBackslash) { int backslash = nextAfterBackslash - 1; sb.append(str, 0, backslash); final char c = str.charAt(nextAfterBackslash); switch (c) { case '\\', '"' -> sb.append(c); case 't' -> sb.append('\t'); case 'n' -> sb.append('\n'); default -> sb.append(str, backslash, nextAfterBackslash + 1); } } }