[Yaml] Escape strings that contain special characters

Fixes encoding errors or ambiguity in some text editors

Signed-off-by: Irmo van den Berge <irmo.vandenberge@ziggo.nl>
This commit is contained in:
Irmo van den Berge committed 2024-09-30 19:09:50 +02:00
1 parent 3db69e3225
commit 2e69527175
2 files changed
+87 -5

No files matched your search

@@ -20,6 +20,7 @@ import org.yaml.snakeyaml.events.StreamEndEvent;
import org.yaml.snakeyaml.events.StreamStartEvent;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.representer.BaseRepresenter;
import org.yaml.snakeyaml.representer.Represent;
import org.yaml.snakeyaml.resolver.Resolver;
import org.yaml.snakeyaml.serializer.Serializer;
@@ -54,6 +55,7 @@ public class YamlSerializer {
/**
* Creates a new YamlSerializer
*/
@SuppressWarnings("unchecked")
public YamlSerializer() {
headerPrefixes = new String[] { "#> ", "# " };
dumperOptions = new DumperOptions();
@@ -61,11 +63,40 @@ public class YamlSerializer {
dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
indentStr = StringUtil.getFilledString(" ", dumperOptions.getIndent());
resolver = new Resolver();
representer = new YamlRepresenter();
representer.setDefaultFlowStyle(dumperOptions.getDefaultFlowStyle());
representer.setDefaultScalarStyle(dumperOptions.getDefaultScalarStyle());
representer.getPropertyUtils().setAllowReadOnlyProperties(dumperOptions.isAllowReadOnlyProperties());
representer.setTimeZone(dumperOptions.getTimeZone());
representer = createRepresenter(dumperOptions);
// Alter the registered String representer, making it quote strings that contain []{}&$
// See: stringMustBeQuoted(String)
try {
Field representersField = org.yaml.snakeyaml.representer.BaseRepresenter.class.getDeclaredField("representers");
representersField.setAccessible(true);
// Create a RepresentString instance that always quotes
final Represent quotedStringRepresent;
{
YamlRepresenter quotedRepresenter = createRepresenter(dumperOptions);
quotedRepresenter.setDefaultScalarStyle(DumperOptions.ScalarStyle.SINGLE_QUOTED);
Map<Class<?>, Represent> quotedRepresentersByType = (Map<Class<?>, Represent>) representersField.get(quotedRepresenter);
quotedStringRepresent = quotedRepresentersByType.get(String.class);
if (quotedStringRepresent == null) {
throw new IllegalStateException("No String representer");
}
}
// Update the String representer, and switch dynamically between quoted and the default
// depending on the input data value
Map<Class<?>, Represent> representersByType = (Map<Class<?>, Represent>) representersField.get(representer);
representersByType.computeIfPresent(String.class, (clsKey, defaultStringRepresent) -> data -> {
if (stringMustBeQuoted((String) data)) {
return quotedStringRepresent.representData(data);
} else {
return defaultStringRepresent.representData(data);
}
});
} catch (Throwable t) {
Logging.LOGGER_CONFIG.log(Level.WARNING, "[Yaml] Failed to enable escaping of special characters", t);
}
output = new StringBuilderWriter();
resetMethod = new FastMethod<Void>();
builder = output.getBuilder();
@@ -204,6 +235,15 @@ public class YamlSerializer {
}
}
private static YamlRepresenter createRepresenter(DumperOptions dumperOptions) {
YamlRepresenter representer = new YamlRepresenter();
representer.setDefaultFlowStyle(dumperOptions.getDefaultFlowStyle());
representer.setDefaultScalarStyle(dumperOptions.getDefaultScalarStyle());
representer.getPropertyUtils().setAllowReadOnlyProperties(dumperOptions.isAllowReadOnlyProperties());
representer.setTimeZone(dumperOptions.getTimeZone());
return representer;
}
private String build() {
String str = this.output.toString();
builder.setLength(0);
@@ -486,6 +526,18 @@ public class YamlSerializer {
"false", "False", "FALSE"
));
// Whether the String must always be quoted.
private static boolean stringMustBeQuoted(String str) {
int len = str.length();
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
if (c == '[' || c == ']' || c == '{' || c == '}' || c == '&' || c == '$') {
return true;
}
}
return false;
}
// Checks whether a String can be written as a String literal, without ' or "
// This is a fast check for the most common case of only a-zA-Z key names
private boolean canWriteStringLiteral(String str, boolean isKey) {
@@ -31,6 +31,36 @@ import com.bergerkiller.mountiplex.MountiplexUtil;
public class YamlTest {
@Test
public void testYamlSpecialStringEscaping() {
YamlNode root = new YamlNode();
root.set("test1", " Word");
root.set("test2", "Word ");
root.set("test3", "Word:");
root.set("test4", "Word[]");
root.set("test5", "Word{}");
root.set("test6", "Word:[]");
root.set("test7", "Word:{}");
root.set("test8", "'Quoted'");
root.set("test9", "\"Quoted\"");
root.set("test10", "$Hello World");
root.set("test11", "&Hello World");
root.set("test12", "");
assertEquals("" +
"test1: ' Word'\n" +
"test2: 'Word '\n" +
"test3: 'Word:'\n" +
"test4: 'Word[]'\n" +
"test5: 'Word{}'\n" +
"test6: 'Word:[]'\n" +
"test7: 'Word:{}'\n" +
"test8: '''Quoted'''\n" +
"test9: '\"Quoted\"'\n" +
"test10: '$Hello World'\n" +
"test11: '&Hello World'\n" +
"test12: ''\n", root.toString());
}
@Test
public void testYamlKeyAPIs() {
YamlNode root = new YamlNode();