From c41a621c5aff69ba663d2491e6fbc861e338e5b5 Mon Sep 17 00:00:00 2001 From: Timo Rohrberg Date: Thu, 12 Mar 2020 22:22:56 +0100 Subject: [PATCH 1/3] FIX #180: YAML Generator now quotes strings containing special chars When using the YAMLGenerator in the MINIMIZE_QUOTES mode, the generator serializes strings in plain style, i.e. without quoting them. However, as stated in the YAML Spec [1] such plain style strings cannot contain some special characters such as ':', ',', and others. [1] https://yaml.org/spec/1.2/spec.html#id2788859 As with the serialization of boolean-like strings, the YAMLGenerator should be adjusted to always quote strings containing those special characters. I will shortly try to provide a pull request to fix this issue. --- .../dataformat/yaml/YAMLGenerator.java | 20 ++++++++- .../yaml/ser/GeneratorWithMinimizeTest.java | 44 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java index 8ded2e16..186b32a1 100644 --- a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java +++ b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java @@ -196,6 +196,14 @@ private Feature(boolean defaultState) { "on", "On", "ON", "off", "Off", "OFF", "null", "Null", "NULL" )); + + /** + * As per YAML Plain Styleunquoted strings are + * restriced to a reduced charset and must be quoted in case they contain one of the following characters. + */ + private final static Set SPECIAL_CHARS = new HashSet<>(Arrays.asList( + ":", "#", "[", "]", "{", "}", "," + )); /* /********************************************************** @@ -480,7 +488,7 @@ public void close() throws IOException if (!isClosed()) { // 11-Dec-2019, tatu: Should perhaps check if content is to be auto-closed... // but need END_DOCUMENT regardless - + _emitEndDocument(); _emit(new StreamEndEvent(null, null)); super.close(); @@ -981,6 +989,16 @@ private boolean _valueNeedsQuoting(String name) { case 'Y': // Y/Yes/YES return MUST_QUOTE_VALUES.contains(name); } + return stringContainsItemFromList(name, SPECIAL_CHARS.toArray(new String[]{})); + } + + private static boolean stringContainsItemFromList(String inputStr, String[] items) { + for(int i =0; i < items.length; i++) { + if (inputStr.contains( items[i] )) { + return true; + } + } + return false; } diff --git a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java index 948e9745..f84ea202 100644 --- a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java +++ b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java @@ -91,6 +91,50 @@ public void testMinimizeQuotesWithNulls() throws Exception "key: nuLL", yaml); } + public void testMinimizeQuotesWithStringsContainingSpecialChars() throws Exception { + Map content = new HashMap(); + content.put("key", "a:b"); + String yaml = MINIM_MAPPER.writeValueAsString(content).trim(); + assertEquals("---\n" + + "key: \"a:b\"", yaml); + + content.clear(); + content.put("key", "a#b"); + yaml = MINIM_MAPPER.writeValueAsString(content).trim(); + assertEquals("---\n" + + "key: \"a#b\"", yaml); + + content.clear(); + content.put("key", "a[b"); + yaml = MINIM_MAPPER.writeValueAsString(content).trim(); + assertEquals("---\n" + + "key: \"a[b\"", yaml); + + content.clear(); + content.put("key", "a]b"); + yaml = MINIM_MAPPER.writeValueAsString(content).trim(); + assertEquals("---\n" + + "key: \"a]b\"", yaml); + + content.clear(); + content.put("key", "a{b"); + yaml = MINIM_MAPPER.writeValueAsString(content).trim(); + assertEquals("---\n" + + "key: \"a{b\"", yaml); + + content.clear(); + content.put("key", "a}b"); + yaml = MINIM_MAPPER.writeValueAsString(content).trim(); + assertEquals("---\n" + + "key: \"a}b\"", yaml); + + content.clear(); + content.put("key", "a,b"); + yaml = MINIM_MAPPER.writeValueAsString(content).trim(); + assertEquals("---\n" + + "key: \"a,b\"", yaml); + } + public void testLiteralStringsMultiLine() throws Exception { Map content = new HashMap(); From ee1a1d0debfdb69a549dbba0b1c78df2d4ca3bdd Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 16 Mar 2020 10:06:14 -0700 Subject: [PATCH 2/3] Add release notes, minor changes to #180 --- release-notes/CREDITS-2.x | 4 +++ release-notes/VERSION-2.x | 7 +++-- .../dataformat/yaml/YAMLGenerator.java | 30 +++++++++++-------- .../yaml/ser/GeneratorWithMinimizeTest.java | 23 ++++++-------- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 3b231d55..da543d72 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -98,3 +98,7 @@ Tyler Carpenter-Rivers (tyler2cr@github) #7: Add `CsvParser.Feature.EMPTY_STRING_AS_NULL` to allow coercing empty Strings into `null` values (2.11.0) + +* Reported, constributed fix for #180: (yaml) YAMLGenerator serializes string with special + chars unquoted when using `MINIMIZE_QUOTES` mode + (2.11.0) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 7f864600..07327bf8 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -10,11 +10,14 @@ Modules: 2.11.0 (not yet released) -#7: Add `CsvParser.Feature.EMPTY_STRING_AS_NULL` to allow coercing empty Strings +#7: (csv) Add `CsvParser.Feature.EMPTY_STRING_AS_NULL` to allow coercing empty Strings into `null` values (contributed by Tyler C-R) -#115: JsonProperty index is not honored by CsvSchema builder +#115: (csv) JsonProperty index is not honored by CsvSchema builder -- actually fixed by [databind#2555] +#180: (yaml) YAMLGenerator serializes string with special chars unquoted when + using `MINIMIZE_QUOTES` mode + (reported, fix contributed by Timo R) 2.10.4 (not yet released) diff --git a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java index 186b32a1..d740722e 100644 --- a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java +++ b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java @@ -197,14 +197,6 @@ private Feature(boolean defaultState) { "null", "Null", "NULL" )); - /** - * As per YAML Plain Styleunquoted strings are - * restriced to a reduced charset and must be quoted in case they contain one of the following characters. - */ - private final static Set SPECIAL_CHARS = new HashSet<>(Arrays.asList( - ":", "#", "[", "]", "{", "}", "," - )); - /* /********************************************************** /* Configuration @@ -989,16 +981,28 @@ private boolean _valueNeedsQuoting(String name) { case 'Y': // Y/Yes/YES return MUST_QUOTE_VALUES.contains(name); } - return stringContainsItemFromList(name, SPECIAL_CHARS.toArray(new String[]{})); + return _valueHasQuotableChar(name); } - private static boolean stringContainsItemFromList(String inputStr, String[] items) { - for(int i =0; i < items.length; i++) { - if (inputStr.contains( items[i] )) { + /** + * As per YAML Plain Styleunquoted + * strings are restricted to a reduced charset and must be quoted in case they contain + * one of the following characters. + */ + private static boolean _valueHasQuotableChar(String inputStr) { + for (int i = 0, end = inputStr.length(); i < end; ++i) { + switch (inputStr.charAt(i)) { + case ':': + case '#': + case '[': + case ']': + case '{': + case '}': + case ',': return true; + default: } } - return false; } diff --git a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java index f84ea202..0a814234 100644 --- a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java +++ b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java @@ -92,44 +92,39 @@ public void testMinimizeQuotesWithNulls() throws Exception } public void testMinimizeQuotesWithStringsContainingSpecialChars() throws Exception { - Map content = new HashMap(); - content.put("key", "a:b"); + Map content; + + content = Collections.singletonMap("key", "a:b"); String yaml = MINIM_MAPPER.writeValueAsString(content).trim(); assertEquals("---\n" + "key: \"a:b\"", yaml); - content.clear(); - content.put("key", "a#b"); + content = Collections.singletonMap("key", "a#b"); yaml = MINIM_MAPPER.writeValueAsString(content).trim(); assertEquals("---\n" + "key: \"a#b\"", yaml); - content.clear(); - content.put("key", "a[b"); + content = Collections.singletonMap("key", "a[b"); yaml = MINIM_MAPPER.writeValueAsString(content).trim(); assertEquals("---\n" + "key: \"a[b\"", yaml); - content.clear(); - content.put("key", "a]b"); + content = Collections.singletonMap("key", "a]b"); yaml = MINIM_MAPPER.writeValueAsString(content).trim(); assertEquals("---\n" + "key: \"a]b\"", yaml); - content.clear(); - content.put("key", "a{b"); + content = Collections.singletonMap("key", "a{b"); yaml = MINIM_MAPPER.writeValueAsString(content).trim(); assertEquals("---\n" + "key: \"a{b\"", yaml); - content.clear(); - content.put("key", "a}b"); + content = Collections.singletonMap("key", "a}b"); yaml = MINIM_MAPPER.writeValueAsString(content).trim(); assertEquals("---\n" + "key: \"a}b\"", yaml); - content.clear(); - content.put("key", "a,b"); + content = Collections.singletonMap("key", "a,b"); yaml = MINIM_MAPPER.writeValueAsString(content).trim(); assertEquals("---\n" + "key: \"a,b\"", yaml); From a4501bc5949e72f53463aeee6cbadb348bc02b14 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 16 Mar 2020 10:10:22 -0700 Subject: [PATCH 3/3] one minor tweak wrt #180 --- .../fasterxml/jackson/dataformat/yaml/YAMLGenerator.java | 5 ++++- .../dataformat/yaml/ser/GeneratorWithMinimizeTest.java | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java index d740722e..fe1af4d9 100644 --- a/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java +++ b/yaml/src/main/java/com/fasterxml/jackson/dataformat/yaml/YAMLGenerator.java @@ -979,7 +979,10 @@ private boolean _valueNeedsQuoting(String name) { case 'N': // Null/NULL/N/No/NO case 'T': // True/TRUE case 'Y': // Y/Yes/YES - return MUST_QUOTE_VALUES.contains(name); + if (MUST_QUOTE_VALUES.contains(name)) { + return true; + } + break; } return _valueHasQuotableChar(name); } diff --git a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java index 0a814234..008537eb 100644 --- a/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java +++ b/yaml/src/test/java/com/fasterxml/jackson/dataformat/yaml/ser/GeneratorWithMinimizeTest.java @@ -124,10 +124,14 @@ public void testMinimizeQuotesWithStringsContainingSpecialChars() throws Excepti assertEquals("---\n" + "key: \"a}b\"", yaml); - content = Collections.singletonMap("key", "a,b"); - yaml = MINIM_MAPPER.writeValueAsString(content).trim(); + yaml = MINIM_MAPPER.writeValueAsString(Collections.singletonMap("key", "a,b")).trim(); assertEquals("---\n" + "key: \"a,b\"", yaml); + + // plus also some edge cases (wrt "false" etc checking + yaml = MINIM_MAPPER.writeValueAsString(Collections.singletonMap("key", "f:off")).trim(); + assertEquals("---\n" + + "key: \"f:off\"", yaml); } public void testLiteralStringsMultiLine() throws Exception