Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new JsonWriteFeature.ESCAPE_FORWARD_SLASHES #1197

Merged
merged 7 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/com/fasterxml/jackson/core/JsonGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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;
Expand Down
52 changes: 50 additions & 2 deletions src/main/java/com/fasterxml/jackson/core/io/CharTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}