Add support for escaping declarative config env var substitution (#7033)

This commit is contained in:
jack-berg 2025-04-11 10:46:59 -05:00 committed by GitHub
parent 2d1c14ee56
commit dc0b4acd86
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 75 additions and 11 deletions

View File

@ -29,6 +29,7 @@ import java.util.logging.Logger;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.snakeyaml.engine.v2.api.Load;
import org.snakeyaml.engine.v2.api.LoadSettings;
import org.snakeyaml.engine.v2.common.ScalarStyle;
@ -308,6 +309,10 @@ public final class DeclarativeConfiguration {
return mapping;
}
private static final String ESCAPE_SEQUENCE = "$$";
private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE.length();
private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$';
private Object constructValueObject(Node node) {
Object value = constructObject(node);
if (!(node instanceof ScalarNode)) {
@ -318,14 +323,53 @@ public final class DeclarativeConfiguration {
}
String val = (String) value;
ScalarStyle scalarStyle = ((ScalarNode) node).getScalarStyle();
// Iterate through val left to right, search for escape sequence "$$"
// For the substring of val between the last escape sequence and the next found, perform
// environment variable substitution
// Add the escape replacement character '$' in place of each escape sequence found
int lastEscapeIndexEnd = 0;
StringBuilder newVal = null;
while (true) {
int escapeIndex = val.indexOf(ESCAPE_SEQUENCE, lastEscapeIndexEnd);
int substitutionEndIndex = escapeIndex == -1 ? val.length() : escapeIndex;
newVal = envVarSubstitution(newVal, val, lastEscapeIndexEnd, substitutionEndIndex);
if (escapeIndex == -1) {
break;
} else {
newVal.append(ESCAPE_SEQUENCE_REPLACEMENT);
}
lastEscapeIndexEnd = escapeIndex + ESCAPE_SEQUENCE_LENGTH;
if (lastEscapeIndexEnd >= val.length()) {
break;
}
}
// If the value was double quoted, retain the double quotes so we don't change a value
// intended to be a string to a different type after environment variable substitution
if (scalarStyle == ScalarStyle.DOUBLE_QUOTED) {
newVal.insert(0, "\"");
newVal.append("\"");
}
return load.loadFromString(newVal.toString());
}
private StringBuilder envVarSubstitution(
@Nullable StringBuilder newVal, String source, int startIndex, int endIndex) {
String val = source.substring(startIndex, endIndex);
Matcher matcher = ENV_VARIABLE_REFERENCE.matcher(val);
if (!matcher.find()) {
return value;
return newVal == null ? new StringBuilder(val) : newVal.append(val);
}
if (newVal == null) {
newVal = new StringBuilder();
}
int offset = 0;
StringBuilder newVal = new StringBuilder();
ScalarStyle scalarStyle = ((ScalarNode) node).getScalarStyle();
do {
MatchResult matchResult = matcher.toMatchResult();
String envVarKey = matcher.group(1);
@ -340,13 +384,8 @@ public final class DeclarativeConfiguration {
if (offset != val.length()) {
newVal.append(val, offset, val.length());
}
// If the value was double quoted, retain the double quotes so we don't change a value
// intended to be a string to a different type after environment variable substitution
if (scalarStyle == ScalarStyle.DOUBLE_QUOTED) {
newVal.insert(0, "\"");
newVal.append("\"");
}
return load.loadFromString(newVal.toString());
return newVal;
}
}
}

View File

@ -593,8 +593,10 @@ class DeclarativeConfigurationParseTest {
@MethodSource("envVarSubstitutionArgs")
void envSubstituteAndLoadYaml(String rawYaml, Object expectedYamlResult) {
Map<String, String> environmentVariables = new HashMap<>();
environmentVariables.put("FOO", "BAR");
environmentVariables.put("STR_1", "value1");
environmentVariables.put("STR_2", "value2");
environmentVariables.put("VALUE_WITH_ESCAPE", "value$$");
environmentVariables.put("EMPTY_STR", "");
environmentVariables.put("BOOL", "true");
environmentVariables.put("INT", "1");
@ -657,7 +659,30 @@ class DeclarativeConfigurationParseTest {
Arguments.of("key1: \"${EMPTY_STR}\"\n", mapOf(entry("key1", ""))),
Arguments.of("key1: \"${BOOL}\"\n", mapOf(entry("key1", "true"))),
Arguments.of("key1: \"${INT}\"\n", mapOf(entry("key1", "1"))),
Arguments.of("key1: \"${FLOAT}\"\n", mapOf(entry("key1", "1.1"))));
Arguments.of("key1: \"${FLOAT}\"\n", mapOf(entry("key1", "1.1"))),
// Escaped
Arguments.of("key1: ${FOO}\n", mapOf(entry("key1", "BAR"))),
Arguments.of("key1: $${FOO}\n", mapOf(entry("key1", "${FOO}"))),
Arguments.of("key1: $$${FOO}\n", mapOf(entry("key1", "$BAR"))),
Arguments.of("key1: $$$${FOO}\n", mapOf(entry("key1", "$${FOO}"))),
Arguments.of("key1: a $$ b\n", mapOf(entry("key1", "a $ b"))),
Arguments.of("key1: $$ b\n", mapOf(entry("key1", "$ b"))),
Arguments.of("key1: a $$\n", mapOf(entry("key1", "a $"))),
Arguments.of("key1: a $ b\n", mapOf(entry("key1", "a $ b"))),
Arguments.of("key1: $${STR_1}\n", mapOf(entry("key1", "${STR_1}"))),
Arguments.of("key1: $${STR_1}$${STR_1}\n", mapOf(entry("key1", "${STR_1}${STR_1}"))),
Arguments.of("key1: $${STR_1}$$\n", mapOf(entry("key1", "${STR_1}$"))),
Arguments.of("key1: $$${STR_1}\n", mapOf(entry("key1", "$value1"))),
Arguments.of("key1: \"$${STR_1}\"\n", mapOf(entry("key1", "${STR_1}"))),
Arguments.of("key1: $${STR_1} ${STR_2}\n", mapOf(entry("key1", "${STR_1} value2"))),
Arguments.of("key1: $${STR_1} $${STR_2}\n", mapOf(entry("key1", "${STR_1} ${STR_2}"))),
Arguments.of("key1: $${NOT_SET:-value1}\n", mapOf(entry("key1", "${NOT_SET:-value1}"))),
Arguments.of("key1: $${STR_1:-fallback}\n", mapOf(entry("key1", "${STR_1:-fallback}"))),
Arguments.of("key1: $${STR_1:-${STR_1}}\n", mapOf(entry("key1", "${STR_1:-value1}"))),
Arguments.of("key1: ${NOT_SET:-${FALLBACK}}\n", mapOf(entry("key1", "${FALLBACK}"))),
Arguments.of(
"key1: ${NOT_SET:-$${FALLBACK}}\n", mapOf(entry("key1", "${NOT_SET:-${FALLBACK}}"))),
Arguments.of("key1: ${VALUE_WITH_ESCAPE}\n", mapOf(entry("key1", "value$$"))));
}
private static <K, V> Map.Entry<K, V> entry(K key, @Nullable V value) {