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

JSONTokener constructor fallback #888

Merged
merged 2 commits into from
Jan 10, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -16,11 +16,14 @@
package io.jsonwebtoken.orgjson.io;

import io.jsonwebtoken.io.AbstractDeserializer;
import io.jsonwebtoken.lang.Assert;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.CharArrayReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Iterator;
@@ -33,14 +36,25 @@
*/
public class OrgJsonDeserializer extends AbstractDeserializer<Object> {

private final JSONTokenerFactory TOKENER_FACTORY;

public OrgJsonDeserializer() {
this(JSONTokenerFactory.INSTANCE);
}

private OrgJsonDeserializer(JSONTokenerFactory factory) {
this.TOKENER_FACTORY = Assert.notNull(factory, "JSONTokenerFactory cannot be null.");
}

@Override
protected Object doDeserialize(Reader reader) {
return parse(reader);
}

private Object parse(java.io.Reader reader) throws JSONException {
private Object parse(Reader reader) throws JSONException {

JSONTokener tokener = new JSONTokener(reader);
JSONTokener tokener = this.TOKENER_FACTORY.newTokener(reader);
Assert.notNull(tokener, "JSONTokener cannot be null.");

char c = tokener.nextClean(); //peak ahead
tokener.back(); //revert
@@ -94,4 +108,67 @@ private Object convertIfNecessary(Object v) {
}
return value;
}

/**
* A factory to create {@link JSONTokener} instances from {@link Reader}s.
*
* @see <a href="https://github.com/jwtk/jjwt/issues/882">JJWT Issue 882</a>.
* @since 0.12.4
*/
static class JSONTokenerFactory { // package-protected on purpose. Not to be exposed as part of public API

private static final Reader TEST_READER = new CharArrayReader("{}".toCharArray());

private static final JSONTokenerFactory INSTANCE = new JSONTokenerFactory();

private final boolean readerCtorAvailable;

// package protected visibility for testing only:
JSONTokenerFactory() {
boolean avail = true;
try {
testTokener(TEST_READER);
} catch (NoSuchMethodError err) {
avail = false;
}
this.readerCtorAvailable = avail;
}

// visible for testing only
protected void testTokener(@SuppressWarnings("SameParameterValue") Reader reader) throws NoSuchMethodError {
new JSONTokener(reader);
}

/**
* Reads all content from the specified reader and returns it as a single String.
*
* @param reader the reader to read characters from
* @return the reader content as a single string
*/
private static String toString(Reader reader) throws IOException {
StringBuilder sb = new StringBuilder(4096);
char[] buf = new char[4096];
int n = 0;
while (EOF != n) {
n = reader.read(buf);
if (n > 0) sb.append(buf, 0, n);
}
return sb.toString();
}

private JSONTokener newTokener(Reader reader) {
if (this.readerCtorAvailable) {
return new JSONTokener(reader);
}
// otherwise not available, likely Android or earlier org.json version, fall back to String ctor:
String s;
try {
s = toString(reader);
} catch (IOException ex) {
String msg = "Unable to obtain JSON String from Reader: " + ex.getMessage();
throw new JSONException(msg, ex);
}
return new JSONTokener(s);
}
}
}
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ package io.jsonwebtoken.orgjson.io

import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.io.IOException
import io.jsonwebtoken.lang.Strings
import org.junit.Before
import org.junit.Test
@@ -28,9 +29,17 @@ class OrgJsonDeserializerTest {

private OrgJsonDeserializer des

private Object fromBytes(byte[] data) {
private static Reader reader(byte[] data) {
def ins = new ByteArrayInputStream(data)
def reader = new InputStreamReader(ins, Strings.UTF_8)
return new InputStreamReader(ins, Strings.UTF_8)
}

private static Reader reader(String s) {
return reader(Strings.utf8(s))
}

private Object fromBytes(byte[] data) {
def reader = reader(data)
return des.deserialize(reader)
}

@@ -188,4 +197,64 @@ class OrgJsonDeserializerTest {
}
}

/**
* Asserts that, when the JSONTokener(Reader) constructor isn't available (e.g. on Android), that the Reader is
* converted to a String and the JSONTokener(String) constructor is used instead.
* @since 0.12.4
*/
@Test
void jsonTokenerMissingReaderConstructor() {

def json = '{"hello": "世界", "test": [1, 2]}'
def expected = read(json) // 'normal' reading

des = new OrgJsonDeserializer(new NoReaderCtorTokenerFactory())

def reader = reader('{"hello": "世界", "test": [1, 2]}')

def result = des.deserialize(reader) // should still work

assertEquals expected, result
}

/**
* Asserts that, when the JSONTokener(Reader) constructor isn't available, and conversion of the Reader to a String
* fails, that a JSONException is thrown
* @since 0.12.4
*/
@Test
void readerFallbackToStringFails() {
def causeMsg = 'Reader failed.'
def cause = new java.io.IOException(causeMsg)
def reader = new Reader() {
@Override
int read(char[] cbuf, int off, int len) throws IOException {
throw cause
}

@Override
void close() throws IOException {
}
}

des = new OrgJsonDeserializer(new NoReaderCtorTokenerFactory())

try {
des.deserialize(reader)
fail()
} catch (DeserializationException expected) {
def jsonEx = expected.getCause()
String msg = "Unable to obtain JSON String from Reader: $causeMsg"
assertEquals msg, jsonEx.getMessage()
assertSame cause, jsonEx.getCause()
}
}

private static class NoReaderCtorTokenerFactory extends OrgJsonDeserializer.JSONTokenerFactory {
@Override
protected void testTokener(Reader reader) throws NoSuchMethodError {
throw new NoSuchMethodError('Android says nope!')
}
}

}