diff --git a/release-notes/CREDITS-2.x b/release-notes/CREDITS-2.x index 53f2344d06..f0074cc5c8 100644 --- a/release-notes/CREDITS-2.x +++ b/release-notes/CREDITS-2.x @@ -395,6 +395,9 @@ Armin Samii (@artoonie) Joo Hyuk Kim (@JooHyukKim) * Contributed #1067: Add `ErrorReportConfiguration` (2.16.0) + * Contributed #507: Add `JsonWriteFeature.ESCAPE_FORWARD_SLASHES` + to allow escaping of '/' for String values + (2.17.0) David Schlosnagle (@schlosna) * Contributed #1081: Make `ByteSourceJsonBootstrapper` use `StringReader` for < 8KiB diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 9db029a6fa..a3f4dc8057 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -16,6 +16,9 @@ a pure JSON library. 2.17.0 (not yet released) +#507: Add `JsonWriteFeature.ESCAPE_FORWARD_SLASHES` to allow escaping of '/' for + String values + (contributed by Joo-Hyuk K) #1137: Improve detection of "is a NaN" to only consider explicit cases, not `double` overflow/underflow #1145: `JsonPointer.appendProperty(String)` does not escape the property name diff --git a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java index 4853960af4..d4e18d64ba 100644 --- a/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java +++ b/src/main/java/com/fasterxml/jackson/core/JsonGenerator.java @@ -266,7 +266,16 @@ public enum Feature { * @deprecated Use {@link com.fasterxml.jackson.core.json.JsonWriteFeature#WRITE_HEX_UPPER_CASE} instead */ @Deprecated - WRITE_HEX_UPPER_CASE(true); + WRITE_HEX_UPPER_CASE(true), + + /** + * Feature that specifies whether {@link JsonGenerator} should escape forward slashes. + *

+ * Feature is disabled by default for Jackson 2.x version, and enabled by default in Jackson 3.0. + * + * @since 2.17 + */ + ESCAPE_FORWARD_SLASHES(false); private final boolean _defaultState; private final int _mask; diff --git a/src/main/java/com/fasterxml/jackson/core/io/CharTypes.java b/src/main/java/com/fasterxml/jackson/core/io/CharTypes.java index abd2d2ea8b..2b2305e1eb 100644 --- a/src/main/java/com/fasterxml/jackson/core/io/CharTypes.java +++ b/src/main/java/com/fasterxml/jackson/core/io/CharTypes.java @@ -176,6 +176,16 @@ public final class CharTypes sOutputEscapes128 = table; } + /** + * Lookup table same as {@link #sOutputEscapes128} except that + * forward slash ('/') is also escaped + */ + protected final static int[] sOutputEscapes128WithSlash; + static { + sOutputEscapes128WithSlash = Arrays.copyOf(sOutputEscapes128, sOutputEscapes128.length); + sOutputEscapes128WithSlash['/'] = '/'; + } + /** * Lookup table for the first 256 Unicode characters (ASCII / UTF-8) * range. For actual hex digits, contains corresponding value; @@ -233,6 +243,28 @@ public static int[] get7BitOutputEscapes(int quoteChar) { return AltEscapes.instance.escapesFor(quoteChar); } + /** + * Alternative to {@link #get7BitOutputEscapes()} when either a non-standard + * quote character is used, or forward slash is to be escaped. + * + * @param quoteChar Character used for quoting textual values and property names; + * usually double-quote but sometimes changed to single-quote (apostrophe) + * @param escapeSlash + * + * @return 128-entry {@code int[]} that contains escape definitions + * + * @since 2.17 + */ + public static int[] get7BitOutputEscapes(int quoteChar, boolean escapeSlash) { + if (quoteChar == '"') { + if (escapeSlash) { + return sOutputEscapes128WithSlash; + } + return sOutputEscapes128; + } + return AltEscapes.instance.escapesFor(quoteChar, escapeSlash); + } + public static int charToHex(int ch) { // 08-Nov-2019, tatu: As per [core#540] and [core#578], changed to @@ -246,7 +278,6 @@ public static char hexToChar(int ch) return HC[ch]; } - /** * Helper method for appending JSON-escaped version of contents * into specific {@link StringBuilder}, using default JSON specification @@ -328,6 +359,9 @@ private static class AltEscapes { private int[][] _altEscapes = new int[128][]; + // @since 2.17 + private int[][] _altEscapesWithSlash = new int[128][]; + public int[] escapesFor(int quoteChar) { int[] esc = _altEscapes[quoteChar]; if (esc == null) { @@ -340,6 +374,20 @@ public int[] escapesFor(int quoteChar) { } return esc; } + + // @since 2.17 + public int[] escapesFor(int quoteChar, boolean escapeSlash) + { + if (!escapeSlash) { + return escapesFor(quoteChar); + } + int[] esc = _altEscapesWithSlash[quoteChar]; + if (esc == null) { + esc = escapesFor(quoteChar); + esc['/'] = '/'; + _altEscapesWithSlash[quoteChar] = esc; + } + return esc; + } } } - diff --git a/src/main/java/com/fasterxml/jackson/core/json/JsonWriteFeature.java b/src/main/java/com/fasterxml/jackson/core/json/JsonWriteFeature.java index 64f22b0372..f310ad9575 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/JsonWriteFeature.java +++ b/src/main/java/com/fasterxml/jackson/core/json/JsonWriteFeature.java @@ -108,6 +108,15 @@ public enum JsonWriteFeature */ // ESCAPE_UTF8_SURROGATES(false, JsonGenerator.Feature.ESCAPE_UTF8_SURROGATES), + /** + * Feature that specifies whether {@link JsonGenerator} should escape forward slashes. + *

+ * Feature is disabled by default for Jackson 2.x version, and enabled by default in Jackson 3.0. + * + * @since 2.17 + */ + ESCAPE_FORWARD_SLASHES(false, JsonGenerator.Feature.ESCAPE_FORWARD_SLASHES), + ; final private boolean _defaultState; diff --git a/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java index 553fb7be93..201f163f8d 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java +++ b/src/main/java/com/fasterxml/jackson/core/json/UTF8JsonGenerator.java @@ -122,10 +122,11 @@ public UTF8JsonGenerator(IOContext ctxt, int features, ObjectCodec codec, super(ctxt, features, codec); _outputStream = out; _quoteChar = (byte) quoteChar; - if (quoteChar != '"') { // since 2.10 - _outputEscapes = CharTypes.get7BitOutputEscapes(quoteChar); - } + boolean escapeSlash = isEnabled(JsonWriteFeature.ESCAPE_FORWARD_SLASHES.mappedFeature()); + if (quoteChar != '"' || escapeSlash) { + _outputEscapes = CharTypes.get7BitOutputEscapes(quoteChar, escapeSlash); + } _bufferRecyclable = true; _outputBuffer = ctxt.allocWriteEncodingBuffer(); _outputEnd = _outputBuffer.length; @@ -153,8 +154,9 @@ public UTF8JsonGenerator(IOContext ctxt, int features, ObjectCodec codec, super(ctxt, features, codec); _outputStream = out; _quoteChar = (byte) quoteChar; - if (quoteChar != '"') { // since 2.10 - _outputEscapes = CharTypes.get7BitOutputEscapes(quoteChar); + boolean escapeSlash = isEnabled(JsonWriteFeature.ESCAPE_FORWARD_SLASHES.mappedFeature()); + if (quoteChar != '"' || escapeSlash) { + _outputEscapes = CharTypes.get7BitOutputEscapes(quoteChar, escapeSlash); } _bufferRecyclable = bufferRecyclable; diff --git a/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java b/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java index b1628c92b3..456c64a9d0 100644 --- a/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java +++ b/src/main/java/com/fasterxml/jackson/core/json/WriterBasedJsonGenerator.java @@ -105,15 +105,15 @@ public WriterBasedJsonGenerator(IOContext ctxt, int features, public WriterBasedJsonGenerator(IOContext ctxt, int features, ObjectCodec codec, Writer w, char quoteChar) - { super(ctxt, features, codec); _writer = w; _outputBuffer = ctxt.allocConcatBuffer(); _outputEnd = _outputBuffer.length; _quoteChar = quoteChar; - if (quoteChar != '"') { // since 2.10 - _outputEscapes = CharTypes.get7BitOutputEscapes(quoteChar); + boolean escapeSlash = isEnabled(JsonWriteFeature.ESCAPE_FORWARD_SLASHES.mappedFeature()); + if (quoteChar != '"' || escapeSlash) { + _outputEscapes = CharTypes.get7BitOutputEscapes(quoteChar, escapeSlash); } } diff --git a/src/test/java/com/fasterxml/jackson/core/write/JsonWriteFeatureEscapeForwardSlashTest.java b/src/test/java/com/fasterxml/jackson/core/write/JsonWriteFeatureEscapeForwardSlashTest.java new file mode 100644 index 0000000000..2933898d41 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/core/write/JsonWriteFeatureEscapeForwardSlashTest.java @@ -0,0 +1,68 @@ +package com.fasterxml.jackson.core.write; + +import java.io.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.json.JsonWriteFeature; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @since 2.17 + */ +public class JsonWriteFeatureEscapeForwardSlashTest +{ + @Test + public void testDontEscapeForwardSlash() throws Exception { + final JsonFactory jsonF = JsonFactory.builder() + .disable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES) + .build(); + final String expJson = "{\"url\":\"http://example.com\"}"; + + _testWithStringWriter(jsonF, expJson); + _testWithByteArrayOutputStream(jsonF, expJson); // Also test with byte-backed output + } + + @Test + public void testEscapeForwardSlash() throws Exception { + final JsonFactory jsonF = JsonFactory.builder() + .enable(JsonWriteFeature.ESCAPE_FORWARD_SLASHES) + .build(); + final String expJson = "{\"url\":\"http:\\/\\/example.com\"}"; + + _testWithStringWriter(jsonF, expJson); + _testWithByteArrayOutputStream(jsonF, expJson); // Also test with byte-backed output + } + + private void _testWithStringWriter(JsonFactory jsonF, String expJson) throws Exception { + // Given + Writer jsonWriter = new StringWriter(); + // When + try (JsonGenerator generator = jsonF.createGenerator(jsonWriter)) { + _writeDoc(generator); + } + // Then + assertEquals(expJson, jsonWriter.toString()); + } + + private void _testWithByteArrayOutputStream(JsonFactory jsonF, String expJson) throws Exception { + // Given + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + // When + try (JsonGenerator generator = jsonF.createGenerator(bytes)) { + _writeDoc(generator); + } + // Then + assertEquals(expJson, bytes.toString()); + } + + private void _writeDoc(JsonGenerator generator) throws Exception + { + generator.writeStartObject(); // start object + generator.writeStringField("url", "http://example.com"); + generator.writeEndObject(); // end object + } +}