Expanded Parser method argument support (#844)

Closes #328

- Ensured Parser worked with CharSequence, InputStream and Reader, not just String
- Changed Deserializer#deserialize(InputStream) to deserialize(Reader) 
- JwtParser now extends from Parser to support these additional methods.
- Changed remaining JwtParser.parse* methods to accept CharSequence arguments instead of String args.
This commit is contained in:
lhazlewood 2023-09-30 11:46:41 -07:00 committed by GitHub
parent 36a6e1383b
commit 05717d0a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 699 additions and 253 deletions

View File

@ -55,6 +55,7 @@ The Apache Software Foundation (https://www.apache.org/).
```
Also, the following classes were copied from the Apache Commons-IO project, with further JJWT-specific modifications:
* io.jsonwebtoken.impl.io.CharSequenceReader
* io.jsonwebtoken.impl.io.FilteredInputStream
* io.jsonwebtoken.impl.io.FilteredOutputStream
* io.jsonwebtoken.impl.io.ClosedInputStream

View File

@ -15,6 +15,7 @@
*/
package io.jsonwebtoken;
import io.jsonwebtoken.io.Parser;
import io.jsonwebtoken.security.SecurityException;
import io.jsonwebtoken.security.SignatureException;
@ -25,7 +26,7 @@ import java.io.InputStream;
*
* @since 0.1
*/
public interface JwtParser {
public interface JwtParser extends Parser<Jwt<?, ?>> {
/**
* Returns {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
@ -38,7 +39,7 @@ public interface JwtParser {
* @return {@code true} if the specified JWT compact string represents a signed JWT (aka a 'JWS'), {@code false}
* otherwise.
*/
boolean isSigned(String compact);
boolean isSigned(CharSequence compact);
/**
* Parses the specified compact serialized JWT string based on the builder's current configuration state and
@ -46,7 +47,7 @@ public interface JwtParser {
*
* <p>This method returns a JWT, JWS, or JWE based on the parsed string. Because it may be cumbersome to
* determine if it is a JWT, JWS or JWE, or if the payload is a Claims or byte array with {@code instanceof} checks,
* the {@link #parse(String, JwtHandler) parse(String,JwtHandler)} method allows for a type-safe callback approach
* the {@link #parse(CharSequence, JwtHandler) parse(String,JwtHandler)} method allows for a type-safe callback approach
* that may help reduce code or instanceof checks.</p>
*
* @param jwt the compact serialized JWT to parse
@ -59,15 +60,15 @@ public interface JwtParser {
* @throws ExpiredJwtException if the specified JWT is a Claims JWT and the Claims has an expiration time
* before the time this method is invoked.
* @throws IllegalArgumentException if the specified string is {@code null} or empty or only whitespace.
* @see #parse(String, JwtHandler)
* @see #parseContentJwt(String)
* @see #parseClaimsJwt(String)
* @see #parseContentJws(String)
* @see #parseClaimsJws(String)
* @see #parseContentJwe(String)
* @see #parseClaimsJwe(String)
* @see #parse(CharSequence, JwtHandler)
* @see #parseContentJwt(CharSequence)
* @see #parseClaimsJwt(CharSequence)
* @see #parseContentJws(CharSequence)
* @see #parseClaimsJws(CharSequence)
* @see #parseContentJwe(CharSequence)
* @see #parseClaimsJwe(CharSequence)
*/
Jwt<?, ?> parse(String jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException,
Jwt<?, ?> parse(CharSequence jwt) throws ExpiredJwtException, MalformedJwtException, SignatureException,
SecurityException, IllegalArgumentException;
/**
@ -93,12 +94,12 @@ public interface JwtParser {
* following convenience methods instead of this one:</p>
*
* <ul>
* <li>{@link #parseContentJwt(String)}</li>
* <li>{@link #parseClaimsJwt(String)}</li>
* <li>{@link #parseContentJws(String)}</li>
* <li>{@link #parseClaimsJws(String)}</li>
* <li>{@link #parseContentJwe(String)}</li>
* <li>{@link #parseClaimsJwe(String)}</li>
* <li>{@link #parseContentJwt(CharSequence)}</li>
* <li>{@link #parseClaimsJwt(CharSequence)}</li>
* <li>{@link #parseContentJws(CharSequence)}</li>
* <li>{@link #parseClaimsJws(CharSequence)}</li>
* <li>{@link #parseContentJwe(CharSequence)}</li>
* <li>{@link #parseClaimsJwe(CharSequence)}</li>
* </ul>
*
* @param jwt the compact serialized JWT to parse
@ -114,16 +115,16 @@ public interface JwtParser {
* before the time this method is invoked.
* @throws IllegalArgumentException if the specified string is {@code null} or empty or only whitespace, or if the
* {@code handler} is {@code null}.
* @see #parseContentJwt(String)
* @see #parseClaimsJwt(String)
* @see #parseContentJws(String)
* @see #parseClaimsJws(String)
* @see #parseContentJwe(String)
* @see #parseClaimsJwe(String)
* @see #parse(String)
* @see #parseContentJwt(CharSequence)
* @see #parseClaimsJwt(CharSequence)
* @see #parseContentJws(CharSequence)
* @see #parseClaimsJws(CharSequence)
* @see #parseContentJwe(CharSequence)
* @see #parseClaimsJwe(CharSequence)
* @see #parse(CharSequence)
* @since 0.2
*/
<T> T parse(String jwt, JwtHandler<T> handler) throws ExpiredJwtException, UnsupportedJwtException,
<T> T parse(CharSequence jwt, JwtHandler<T> handler) throws ExpiredJwtException, UnsupportedJwtException,
MalformedJwtException, SignatureException, SecurityException, IllegalArgumentException;
/**
@ -148,14 +149,14 @@ public interface JwtParser {
* @throws SignatureException if the {@code jwt} string is actually a JWS and signature validation fails
* @throws SecurityException if the {@code jwt} string is actually a JWE and decryption fails
* @throws IllegalArgumentException if the {@code jwt} string is {@code null} or empty or only whitespace
* @see #parseClaimsJwt(String)
* @see #parseContentJws(String)
* @see #parseClaimsJws(String)
* @see #parse(String, JwtHandler)
* @see #parse(String)
* @see #parseClaimsJwt(CharSequence)
* @see #parseContentJws(CharSequence)
* @see #parseClaimsJws(CharSequence)
* @see #parse(CharSequence, JwtHandler)
* @see #parse(CharSequence)
* @since 0.2
*/
Jwt<Header, byte[]> parseContentJwt(String jwt) throws UnsupportedJwtException, MalformedJwtException,
Jwt<Header, byte[]> parseContentJwt(CharSequence jwt) throws UnsupportedJwtException, MalformedJwtException,
SignatureException, SecurityException, IllegalArgumentException;
/**
@ -178,14 +179,14 @@ public interface JwtParser {
* @throws ExpiredJwtException if the specified JWT is a Claims JWT and the Claims has an expiration time
* before the time this method is invoked.
* @throws IllegalArgumentException if the {@code jwt} string is {@code null} or empty or only whitespace
* @see #parseContentJwt(String)
* @see #parseContentJws(String)
* @see #parseClaimsJws(String)
* @see #parse(String, JwtHandler)
* @see #parse(String)
* @see #parseContentJwt(CharSequence)
* @see #parseContentJws(CharSequence)
* @see #parseClaimsJws(CharSequence)
* @see #parse(CharSequence, JwtHandler)
* @see #parse(CharSequence)
* @since 0.2
*/
Jwt<Header, Claims> parseClaimsJwt(String jwt) throws ExpiredJwtException, UnsupportedJwtException,
Jwt<Header, Claims> parseClaimsJwt(CharSequence jwt) throws ExpiredJwtException, UnsupportedJwtException,
MalformedJwtException, SignatureException, SecurityException, IllegalArgumentException;
/**
@ -207,16 +208,16 @@ public interface JwtParser {
* @throws SignatureException if the {@code jws} JWS signature validation fails
* @throws SecurityException if the {@code jws} string is actually a JWE and decryption fails
* @throws IllegalArgumentException if the {@code jws} string is {@code null} or empty or only whitespace
* @see #parseContentJwt(String)
* @see #parseContentJwe(String)
* @see #parseClaimsJwt(String)
* @see #parseClaimsJws(String)
* @see #parseClaimsJwe(String)
* @see #parse(String, JwtHandler)
* @see #parse(String)
* @see #parseContentJwt(CharSequence)
* @see #parseContentJwe(CharSequence)
* @see #parseClaimsJwt(CharSequence)
* @see #parseClaimsJws(CharSequence)
* @see #parseClaimsJwe(CharSequence)
* @see #parse(CharSequence, JwtHandler)
* @see #parse(CharSequence)
* @since 0.2
*/
Jws<byte[]> parseContentJws(String jws) throws UnsupportedJwtException, MalformedJwtException, SignatureException,
Jws<byte[]> parseContentJws(CharSequence jws) throws UnsupportedJwtException, MalformedJwtException, SignatureException,
SecurityException, IllegalArgumentException;
/**
@ -234,7 +235,7 @@ public interface JwtParser {
* @param unencodedPayload the JWS's associated required unencoded payload used for signature verification.
* @return the parsed Unencoded Payload.
*/
Jws<byte[]> parseContentJws(String jws, byte[] unencodedPayload);
Jws<byte[]> parseContentJws(CharSequence jws, byte[] unencodedPayload);
/**
* Parses a JWS known to use the
@ -251,7 +252,7 @@ public interface JwtParser {
* @param unencodedPayload the JWS's associated required unencoded payload used for signature verification.
* @return the parsed Unencoded Payload.
*/
Jws<Claims> parseClaimsJws(String jws, byte[] unencodedPayload);
Jws<Claims> parseClaimsJws(CharSequence jws, byte[] unencodedPayload);
/**
* Parses a JWS known to use the
@ -274,7 +275,7 @@ public interface JwtParser {
* @param unencodedPayload the JWS's associated required unencoded payload used for signature verification.
* @return the parsed Unencoded Payload.
*/
Jws<byte[]> parseContentJws(String jws, InputStream unencodedPayload);
Jws<byte[]> parseContentJws(CharSequence jws, InputStream unencodedPayload);
/**
* Parses a JWS known to use the
@ -285,7 +286,7 @@ public interface JwtParser {
* <p><b>NOTE:</b> however, because calling this method indicates a completed
* {@link Claims} instance is desired, the specified {@code unencodedPayload} JSON stream will be fully
* read into a Claims instance. If this will be problematic for your application (perhaps if you expect extremely
* large Claims), it is recommended to use the {@link #parseContentJws(String, InputStream)} method instead.</p>
* large Claims), it is recommended to use the {@link #parseContentJws(CharSequence, InputStream)} method instead.</p>
*
* <p><b>Unencoded Non-Detached Payload</b></p>
* <p>Note that if the JWS contains a valid unencoded Payload string (what RFC 7797 calls an
@ -297,7 +298,7 @@ public interface JwtParser {
* @param unencodedPayload the JWS's associated required unencoded payload used for signature verification.
* @return the parsed Unencoded Payload.
*/
Jws<Claims> parseClaimsJws(String jws, InputStream unencodedPayload);
Jws<Claims> parseClaimsJws(CharSequence jws, InputStream unencodedPayload);
/**
* Parses the specified compact serialized JWS string based on the builder's current configuration state and
@ -318,16 +319,16 @@ public interface JwtParser {
* @throws ExpiredJwtException if the specified JWT is a Claims JWT and the Claims has an expiration time
* before the time this method is invoked.
* @throws IllegalArgumentException if the {@code claimsJws} string is {@code null} or empty or only whitespace
* @see #parseContentJwt(String)
* @see #parseContentJws(String)
* @see #parseContentJwe(String)
* @see #parseClaimsJwt(String)
* @see #parseClaimsJwe(String)
* @see #parse(String, JwtHandler)
* @see #parse(String)
* @see #parseContentJwt(CharSequence)
* @see #parseContentJws(CharSequence)
* @see #parseContentJwe(CharSequence)
* @see #parseClaimsJwt(CharSequence)
* @see #parseClaimsJwe(CharSequence)
* @see #parse(CharSequence, JwtHandler)
* @see #parse(CharSequence)
* @since 0.2
*/
Jws<Claims> parseClaimsJws(String jws) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException,
Jws<Claims> parseClaimsJws(CharSequence jws) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException,
SignatureException, SecurityException, IllegalArgumentException;
/**
@ -348,16 +349,16 @@ public interface JwtParser {
* @throws MalformedJwtException if the {@code jwe} string is not a valid JWE
* @throws SecurityException if the {@code jwe} JWE decryption fails
* @throws IllegalArgumentException if the {@code jwe} string is {@code null} or empty or only whitespace
* @see #parseContentJwt(String)
* @see #parseContentJws(String)
* @see #parseClaimsJwt(String)
* @see #parseClaimsJws(String)
* @see #parseClaimsJwe(String)
* @see #parse(String, JwtHandler)
* @see #parse(String)
* @see #parseContentJwt(CharSequence)
* @see #parseContentJws(CharSequence)
* @see #parseClaimsJwt(CharSequence)
* @see #parseClaimsJws(CharSequence)
* @see #parseClaimsJwe(CharSequence)
* @see #parse(CharSequence, JwtHandler)
* @see #parse(CharSequence)
* @since JJWT_RELEASE_VERSION
*/
Jwe<byte[]> parseContentJwe(String jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException,
Jwe<byte[]> parseContentJwe(CharSequence jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException,
SecurityException, IllegalArgumentException;
/**
@ -378,15 +379,15 @@ public interface JwtParser {
* @throws ExpiredJwtException if the specified JWT is a Claims JWE and the Claims has an expiration time
* before the time this method is invoked.
* @throws IllegalArgumentException if the {@code claimsJwe} string is {@code null} or empty or only whitespace
* @see #parseContentJwt(String)
* @see #parseContentJws(String)
* @see #parseContentJwe(String)
* @see #parseClaimsJwt(String)
* @see #parseClaimsJws(String)
* @see #parse(String, JwtHandler)
* @see #parse(String)
* @see #parseContentJwt(CharSequence)
* @see #parseContentJws(CharSequence)
* @see #parseContentJwe(CharSequence)
* @see #parseClaimsJwt(CharSequence)
* @see #parseClaimsJws(CharSequence)
* @see #parse(CharSequence, JwtHandler)
* @see #parse(CharSequence)
* @since JJWT_RELEASE_VERSION
*/
Jwe<Claims> parseClaimsJwe(String jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException,
Jwe<Claims> parseClaimsJwe(CharSequence jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException,
SecurityException, IllegalArgumentException;
}

View File

@ -19,10 +19,13 @@ import io.jsonwebtoken.lang.Assert;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
/**
* Convenient base class to use to implement {@link Deserializer}s, with subclasses only needing to implement
* {@link #doDeserialize(InputStream)}.
* {@link #doDeserialize(Reader)}.
*
* @param <T> the type of object returned after deserialization
* @since JJWT_RELEASE_VERSION
@ -48,17 +51,19 @@ public abstract class AbstractDeserializer<T> implements Deserializer<T> {
@Override
public final T deserialize(byte[] bytes) throws DeserializationException {
bytes = bytes == null ? EMPTY_BYTES : bytes; // null safe
return deserialize(new ByteArrayInputStream(bytes));
InputStream in = new ByteArrayInputStream(bytes);
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
return deserialize(reader);
}
/**
* {@inheritDoc}
*/
@Override
public final T deserialize(InputStream in) throws DeserializationException {
Assert.notNull(in, "InputStream argument cannot be null.");
public final T deserialize(Reader reader) throws DeserializationException {
Assert.notNull(reader, "Reader argument cannot be null.");
try {
return doDeserialize(in);
return doDeserialize(reader);
} catch (Throwable t) {
if (t instanceof DeserializationException) {
throw (DeserializationException) t;
@ -69,11 +74,11 @@ public abstract class AbstractDeserializer<T> implements Deserializer<T> {
}
/**
* Reads the specified {@code InputStream} and returns the corresponding Java object.
* Reads the specified character stream and returns the corresponding Java object.
*
* @param in the input stream to read
* @param reader the reader to use to read the character stream
* @return the deserialized Java object
* @throws Exception if there is a problem reading the stream or creating the expected Java object
*/
protected abstract T doDeserialize(InputStream in) throws Exception;
protected abstract T doDeserialize(Reader reader) throws Exception;
}

View File

@ -15,7 +15,7 @@
*/
package io.jsonwebtoken.io;
import java.io.InputStream;
import java.io.Reader;
/**
* A {@code Deserializer} is able to convert serialized byte streams into Java objects.
@ -31,18 +31,18 @@ public interface Deserializer<T> {
* @param bytes the formatted data byte array to convert
* @return the reconstituted Java object
* @throws DeserializationException if there is a problem converting the byte array to an object.
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #deserialize(InputStream)}
* @deprecated since JJWT_RELEASE_VERSION in favor of {@link #deserialize(Reader)}
*/
@Deprecated
T deserialize(byte[] bytes) throws DeserializationException;
/**
* Reads the specified {@code InputStream} and returns the corresponding Java object.
* Reads the specified character stream and returns the corresponding Java object.
*
* @param in the input stream to read
* @param reader the reader to use to read the character stream
* @return the deserialized Java object
* @throws DeserializationException if there is a problem reading the stream or creating the expected Java object
* @since JJWT_RELEASE_VERSION
*/
T deserialize(InputStream in) throws DeserializationException;
T deserialize(Reader reader) throws DeserializationException;
}

View File

@ -15,32 +15,53 @@
*/
package io.jsonwebtoken.io;
import java.io.InputStream;
import java.io.Reader;
/**
* A Parser converts character input into a Java object.
* A Parser converts a character stream into a Java object.
*
* <p>Semantically, this interface might have been more accurately named
* <a href="https://en.wikipedia.org/wiki/Marshalling_(computer_science)">Unmarshaller</a> because it technically
* converts a content stream into a Java object. However, the {@code Parser} name was chosen for consistency with the
* {@link io.jsonwebtoken.JwtParser JwtParser} concept (which is a 'real' parser that scans text for tokens). This
* helps avoid confusion when trying to find similar concepts in the JJWT API by using the same taxonomy, for
* example:</p>
* <ul>
* <li>{@link io.jsonwebtoken.Jwts#parser() Jwts.parser()}</li>
* <li>{@link io.jsonwebtoken.security.Jwks#parser() Jwks.parser()}</li>
* <li>{@link io.jsonwebtoken.security.Jwks#setParser() Jwks.setParser()}</li>
* </ul>
*
* @param <T> the instance type created after parsing/unmarshalling
* @param <T> the instance type created after parsing
* @since JJWT_RELEASE_VERSION
*/
public interface Parser<T> {
/**
* Parse the specified input into a Java object.
* Parse the specified character sequence into a Java object.
*
* @param input the string to parse into a Java object.
* @param input the character sequence to parse into a Java object.
* @return the Java object represented by the specified {@code input} stream.
*/
T parse(String input);
T parse(CharSequence input);
/**
* @param input The character sequence, may be {@code null}
* @param start The start index in the character sequence, inclusive
* @param end The end index in the character sequence, exclusive
* @return the Java object represented by the specified sequence bounds
* @throws IllegalArgumentException if the start index is negative, or if the end index is smaller than the start index
*/
T parse(CharSequence input, int start, int end);
/**
* Parse the specified character sequence into a Java object.
*
* @param reader the reader to use to parse a Java object.
* @return the Java object represented by the specified {@code input} stream.
*/
T parse(Reader reader);
/**
* Parses the specified {@link InputStream} assuming {@link java.nio.charset.StandardCharsets#UTF_8 UTF_8} encoding.
* This is a convenience alias for:
*
* <blockquote><pre>{@link #parse(Reader) parse}(new {@link java.io.InputStreamReader
* InputStreamReader}(in, {@link java.nio.charset.StandardCharsets#UTF_8
* StandardCharsets.UTF_8});</pre></blockquote>
*
*
* @param in the UTF-8 InputStream.
* @return the Java object represented by the specified {@link InputStream}.
*/
T parse(InputStream in);
}

View File

@ -26,8 +26,8 @@ class AbstractDeserializerTest {
boolean invoked = false
def deser = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
assertEquals EOF, inputStream.read()
protected Object doDeserialize(Reader reader) throws Exception {
assertEquals EOF, reader.read()
invoked = true
}
}
@ -40,8 +40,8 @@ class AbstractDeserializerTest {
boolean invoked = false
def deser = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
assertEquals EOF, inputStream.read()
protected Object doDeserialize(Reader reader) throws Exception {
assertEquals EOF, reader.read()
invoked = true
}
}
@ -56,8 +56,8 @@ class AbstractDeserializerTest {
bytes[0] = b
def des = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
assertEquals b, inputStream.read()
protected Object doDeserialize(Reader reader) throws Exception {
assertEquals b, reader.read()
return 42
}
}
@ -70,7 +70,7 @@ class AbstractDeserializerTest {
def ex = new RuntimeException('foo')
def des = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
protected Object doDeserialize(Reader reader) throws Exception {
throw ex
}
}

View File

@ -19,10 +19,7 @@ import com.google.gson.Gson;
import io.jsonwebtoken.io.AbstractDeserializer;
import io.jsonwebtoken.lang.Assert;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
public class GsonDeserializer<T> extends AbstractDeserializer<T> {
@ -46,8 +43,7 @@ public class GsonDeserializer<T> extends AbstractDeserializer<T> {
}
@Override
protected T doDeserialize(InputStream in) throws Exception {
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
protected T doDeserialize(Reader reader) {
return gson.fromJson(reader, returnType);
}
}

View File

@ -30,7 +30,9 @@ class GsonDeserializerTest {
private GsonDeserializer deserializer
private def deser(byte[] data) {
deserializer.deserialize(new ByteArrayInputStream(data))
def ins = new ByteArrayInputStream(data)
def reader = new InputStreamReader(ins, Strings.UTF_8)
deserializer.deserialize(reader)
}
private def deser(String s) {
@ -76,7 +78,7 @@ class GsonDeserializerTest {
def ex = new IOException('foo')
deserializer = new GsonDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
protected Object doDeserialize(Reader reader) throws Exception {
throw ex
}
}

View File

@ -24,7 +24,7 @@ import io.jsonwebtoken.io.AbstractDeserializer;
import io.jsonwebtoken.lang.Assert;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.Collections;
import java.util.Map;
@ -100,8 +100,8 @@ public class JacksonDeserializer<T> extends AbstractDeserializer<T> {
}
@Override
protected T doDeserialize(InputStream in) throws Exception {
return objectMapper.readValue(in, returnType);
protected T doDeserialize(Reader reader) throws Exception {
return objectMapper.readValue(reader, returnType);
}
/**

View File

@ -22,7 +22,6 @@ import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.io.Encoders
import io.jsonwebtoken.jackson.io.stubs.CustomBean
import io.jsonwebtoken.lang.Maps
import io.jsonwebtoken.lang.Strings
import org.junit.Before
import org.junit.Test
@ -62,9 +61,9 @@ class JacksonDeserializerTest {
@Test
void testDeserialize() {
byte[] data = Strings.utf8('{"hello":"世界"}')
def reader = new StringReader('{"hello":"世界"}')
def expected = [hello: '世界']
def result = deserializer.deserialize(new ByteArrayInputStream(data))
def result = deserializer.deserialize(reader)
assertEquals expected, result
}
@ -97,8 +96,6 @@ class JacksonDeserializerTest {
}
"""
byte[] serialized = Strings.utf8(json)
CustomBean expectedCustomBean = new CustomBean()
.setByteArrayValue("bytes".getBytes("UTF-8"))
.setByteValue(0xF as byte)
@ -119,7 +116,7 @@ class JacksonDeserializerTest {
def expected = [oneKey: "oneValue", custom: expectedCustomBean]
def result = new JacksonDeserializer(Maps.of("custom", CustomBean).build())
.deserialize(new ByteArrayInputStream(serialized))
.deserialize(new StringReader(json))
assertEquals expected, result
}
@ -154,8 +151,8 @@ class JacksonDeserializerTest {
typeMap.put("custom", CustomBean)
def deserializer = new JacksonDeserializer(typeMap)
def ins = new ByteArrayInputStream(Strings.utf8('{"alg":"HS256"}'))
def result = deserializer.deserialize(ins)
def reader = new StringReader('{"alg":"HS256"}')
def result = deserializer.deserialize(reader)
assertEquals(["alg": "HS256"], result)
}
@ -171,12 +168,12 @@ class JacksonDeserializerTest {
deserializer = new JacksonDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
protected Object doDeserialize(Reader reader) throws Exception {
throw ex
}
}
try {
deserializer.deserialize(new ByteArrayInputStream(Strings.utf8('{"hello":"世界"}')))
deserializer.deserialize(new StringReader('{"hello":"世界"}'))
fail()
} catch (DeserializationException se) {
String msg = 'Unable to deserialize: foo'

View File

@ -21,10 +21,7 @@ import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
@ -37,8 +34,7 @@ import java.util.Map;
public class OrgJsonDeserializer extends AbstractDeserializer<Object> {
@Override
protected Object doDeserialize(InputStream in) {
Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8);
protected Object doDeserialize(Reader reader) {
return parse(reader);
}

View File

@ -29,7 +29,9 @@ class OrgJsonDeserializerTest {
private OrgJsonDeserializer des
private Object fromBytes(byte[] data) {
return des.deserialize(new ByteArrayInputStream(data))
def ins = new ByteArrayInputStream(data)
def reader = new InputStreamReader(ins, Strings.UTF_8)
return des.deserialize(reader)
}
private Object read(String s) {
@ -38,7 +40,7 @@ class OrgJsonDeserializerTest {
@Test(expected = IllegalArgumentException)
void testNullArgument() {
des.deserialize((InputStream) null)
des.deserialize((Reader) null)
}
@Test(expected = DeserializationException)
@ -157,7 +159,7 @@ class OrgJsonDeserializerTest {
@Test(expected = IllegalArgumentException)
void deserializeNull() {
des.deserialize((InputStream) null)
des.deserialize((Reader) null)
}
@Test(expected = DeserializationException)
@ -172,7 +174,7 @@ class OrgJsonDeserializerTest {
des = new OrgJsonDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) {
protected Object doDeserialize(Reader reader) {
throw t
}
}

View File

@ -39,6 +39,8 @@ import io.jsonwebtoken.PrematureJwtException;
import io.jsonwebtoken.ProtectedHeader;
import io.jsonwebtoken.SigningKeyResolver;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.impl.io.AbstractParser;
import io.jsonwebtoken.impl.io.CharSequenceReader;
import io.jsonwebtoken.impl.io.JsonObjectDeserializer;
import io.jsonwebtoken.impl.io.Streams;
import io.jsonwebtoken.impl.io.UncloseableInputStream;
@ -76,6 +78,7 @@ import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.Reader;
import java.io.SequenceInputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
@ -91,7 +94,7 @@ import java.util.Map;
import java.util.Set;
@SuppressWarnings("unchecked")
public class DefaultJwtParser implements JwtParser {
public class DefaultJwtParser extends AbstractParser<Jwt<?, ?>> implements JwtParser {
static final char SEPARATOR_CHAR = '.';
@ -248,12 +251,12 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public boolean isSigned(String compact) {
if (compact == null) {
public boolean isSigned(CharSequence compact) {
if (!Strings.hasText(compact)) {
return false;
}
try {
final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact);
final TokenizedJwt tokenized = jwtTokenizer.tokenize(new CharSequenceReader(compact));
return !(tokenized instanceof TokenizedJwe) && Strings.hasText(tokenized.getDigest());
} catch (MalformedJwtException e) {
return false;
@ -356,15 +359,15 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jwt<?, ?> parse(String compact) {
CharBuffer buffer = Strings.wrap(compact); // so compact.subsequence calls don't add new Strings on the heap
return parse(buffer, Payload.EMPTY);
public Jwt<?, ?> parse(Reader reader) {
Assert.notNull(reader, "Reader cannot be null.");
return parse(reader, Payload.EMPTY);
}
private Jwt<?, ?> parse(CharSequence compact, Payload unencodedPayload)
private Jwt<?, ?> parse(Reader compact, Payload unencodedPayload)
throws ExpiredJwtException, MalformedJwtException, SignatureException {
Assert.hasText(compact, "JWT String cannot be null or empty.");
Assert.notNull(compact, "Compact reader cannot be null.");
Assert.stateNotNull(unencodedPayload, "internal error: unencodedPayload is null.");
final TokenizedJwt tokenized = jwtTokenizer.tokenize(compact);
@ -767,16 +770,16 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public <T> T parse(String compact, JwtHandler<T> handler) {
public <T> T parse(CharSequence compact, JwtHandler<T> handler) {
return parse(compact, Payload.EMPTY, handler);
}
private <T> T parse(String compact, Payload unencodedPayload, JwtHandler<T> handler)
private <T> T parse(CharSequence compact, Payload unencodedPayload, JwtHandler<T> handler)
throws ExpiredJwtException, MalformedJwtException, SignatureException {
Assert.notNull(handler, "JwtHandler argument cannot be null.");
Assert.hasText(compact, "JWT String argument cannot be null or empty.");
Jwt<?, ?> jwt = parse(compact, unencodedPayload);
Jwt<?, ?> jwt = parse(new CharSequenceReader(compact), unencodedPayload);
if (jwt instanceof Jws) {
Jws<?> jws = (Jws<?>) jwt;
@ -805,7 +808,7 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jwt<Header, byte[]> parseContentJwt(String compact) {
public Jwt<Header, byte[]> parseContentJwt(CharSequence compact) {
return parse(compact, new JwtHandlerAdapter<Jwt<Header, byte[]>>() {
@Override
public Jwt<Header, byte[]> onContentJwt(Jwt<Header, byte[]> jwt) {
@ -815,7 +818,7 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jwt<Header, Claims> parseClaimsJwt(String compact) {
public Jwt<Header, Claims> parseClaimsJwt(CharSequence compact) {
return parse(compact, new JwtHandlerAdapter<Jwt<Header, Claims>>() {
@Override
public Jwt<Header, Claims> onClaimsJwt(Jwt<Header, Claims> jwt) {
@ -825,7 +828,7 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jws<byte[]> parseContentJws(String compact) {
public Jws<byte[]> parseContentJws(CharSequence compact) {
return parse(compact, new JwtHandlerAdapter<Jws<byte[]>>() {
@Override
public Jws<byte[]> onContentJws(Jws<byte[]> jws) {
@ -835,7 +838,7 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jws<Claims> parseClaimsJws(String compact) {
public Jws<Claims> parseClaimsJws(CharSequence compact) {
return parse(compact, new JwtHandlerAdapter<Jws<Claims>>() {
@Override
public Jws<Claims> onClaimsJws(Jws<Claims> jws) {
@ -844,7 +847,7 @@ public class DefaultJwtParser implements JwtParser {
});
}
private Jws<byte[]> parseContentJws(String jws, Payload unencodedPayload) {
private Jws<byte[]> parseContentJws(CharSequence jws, Payload unencodedPayload) {
return parse(jws, unencodedPayload, new JwtHandlerAdapter<Jws<byte[]>>() {
@Override
public Jws<byte[]> onContentJws(Jws<byte[]> jws) {
@ -853,7 +856,7 @@ public class DefaultJwtParser implements JwtParser {
});
}
private Jws<Claims> parseClaimsJws(String jws, Payload unencodedPayload) {
private Jws<Claims> parseClaimsJws(CharSequence jws, Payload unencodedPayload) {
unencodedPayload.setClaimsExpected(true);
return parse(jws, unencodedPayload, new JwtHandlerAdapter<Jws<Claims>>() {
@Override
@ -864,13 +867,13 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jws<byte[]> parseContentJws(String jws, byte[] unencodedPayload) {
public Jws<byte[]> parseContentJws(CharSequence jws, byte[] unencodedPayload) {
Assert.notEmpty(unencodedPayload, "unencodedPayload argument cannot be null or empty.");
return parseContentJws(jws, new Payload(unencodedPayload, null));
}
@Override
public Jws<Claims> parseClaimsJws(String jws, byte[] unencodedPayload) {
public Jws<Claims> parseClaimsJws(CharSequence jws, byte[] unencodedPayload) {
Assert.notEmpty(unencodedPayload, "unencodedPayload argument cannot be null or empty.");
return parseClaimsJws(jws, new Payload(unencodedPayload, null));
}
@ -885,13 +888,13 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jws<byte[]> parseContentJws(String jws, InputStream unencodedPayload) {
public Jws<byte[]> parseContentJws(CharSequence jws, InputStream unencodedPayload) {
Assert.notNull(unencodedPayload, "unencodedPayload InputStream cannot be null.");
return parseContentJws(jws, payloadFor(unencodedPayload));
}
@Override
public Jws<Claims> parseClaimsJws(String jws, InputStream unencodedPayload) {
public Jws<Claims> parseClaimsJws(CharSequence jws, InputStream unencodedPayload) {
Assert.notNull(unencodedPayload, "unencodedPayload InputStream cannot be null.");
byte[] bytes = Streams.bytes(unencodedPayload,
"Unable to obtain Claims bytes from unencodedPayload InputStream");
@ -899,7 +902,7 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jwe<byte[]> parseContentJwe(String compact) throws JwtException {
public Jwe<byte[]> parseContentJwe(CharSequence compact) throws JwtException {
return parse(compact, new JwtHandlerAdapter<Jwe<byte[]>>() {
@Override
public Jwe<byte[]> onContentJwe(Jwe<byte[]> jwe) {
@ -909,7 +912,7 @@ public class DefaultJwtParser implements JwtParser {
}
@Override
public Jwe<Claims> parseClaimsJwe(String compact) throws JwtException {
public Jwe<Claims> parseClaimsJwe(CharSequence compact) throws JwtException {
return parse(compact, new JwtHandlerAdapter<Jwe<Claims>>() {
@Override
public Jwe<Claims> onClaimsJwe(Jwe<Claims> jwe) {
@ -932,8 +935,9 @@ public class DefaultJwtParser implements JwtParser {
protected Map<String, ?> deserialize(InputStream in, final String name) {
try {
Reader reader = Streams.reader(in);
JsonObjectDeserializer deserializer = new JsonObjectDeserializer(this.deserializer, name);
return deserializer.apply(in);
return deserializer.apply(reader);
} finally {
Objects.nullSafeClose(in);
}

View File

@ -16,9 +16,13 @@
package io.jsonwebtoken.impl;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.impl.io.Streams;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import java.io.IOException;
import java.io.Reader;
public class JwtTokenizer {
static final char DELIMITER = '.';
@ -26,52 +30,70 @@ public class JwtTokenizer {
private static final String DELIM_ERR_MSG_PREFIX = "Invalid compact JWT string: Compact JWSs must contain " +
"exactly 2 period characters, and compact JWEs must contain exactly 4. Found: ";
@SuppressWarnings("unchecked")
public <T extends TokenizedJwt> T tokenize(CharSequence jwt) {
private static int read(Reader r, char[] buf) {
try {
return r.read(buf);
} catch (IOException e) {
String msg = "Unable to read compact JWT: " + e.getMessage();
throw new MalformedJwtException(msg, e);
}
}
Assert.hasText(jwt, "Argument cannot be null or empty.");
@SuppressWarnings("unchecked")
public <T extends TokenizedJwt> T tokenize(Reader reader) {
Assert.notNull(reader, "Reader argument cannot be null.");
CharSequence protectedHeader = Strings.EMPTY; //Both JWS and JWE
CharSequence body = Strings.EMPTY; //JWS payload or JWE Ciphertext
CharSequence encryptedKey = Strings.EMPTY; //JWE only
CharSequence iv = Strings.EMPTY; //JWE only
CharSequence digest; //JWS Signature or JWE AAD Tag
CharSequence digest = Strings.EMPTY; //JWS Signature or JWE AAD Tag
int delimiterCount = 0;
int start = 0;
char[] buf = new char[4096];
int len = 0;
StringBuilder sb = new StringBuilder(4096);
while (len != Streams.EOF) {
for (int i = 0; i < jwt.length(); i++) {
len = read(reader, buf);
char c = jwt.charAt(i);
for (int i = 0; i < len; i++) {
if (Character.isWhitespace(c)) {
String msg = "Compact JWT strings may not contain whitespace.";
throw new MalformedJwtException(msg);
}
char c = buf[i];
if (c == DELIMITER) {
CharSequence token = jwt.subSequence(start, i);
start = i + 1;
switch (delimiterCount) {
case 0:
protectedHeader = token;
break;
case 1:
body = token; //for JWS
encryptedKey = token; //for JWE
break;
case 2:
body = Strings.EMPTY; //clear out value set for JWS
iv = token;
break;
case 3:
body = token;
break;
if (Character.isWhitespace(c)) {
String msg = "Compact JWT strings may not contain whitespace.";
throw new MalformedJwtException(msg);
}
delimiterCount++;
if (c == DELIMITER) {
CharSequence seq = Strings.clean(sb);
String token = seq != null ? seq.toString() : Strings.EMPTY;
switch (delimiterCount) {
case 0:
protectedHeader = token;
break;
case 1:
body = token; //for JWS
encryptedKey = token; //for JWE
break;
case 2:
body = Strings.EMPTY; //clear out value set for JWS
iv = token;
break;
case 3:
body = token;
break;
}
delimiterCount++;
sb.setLength(0);
} else {
sb.append(c);
}
}
}
@ -80,7 +102,9 @@ public class JwtTokenizer {
throw new MalformedJwtException(msg);
}
digest = jwt.subSequence(start, jwt.length());
if (sb.length() > 0) {
digest = sb.toString();
}
if (delimiterCount == 2) {
return (T) new DefaultTokenizedJwt(protectedHeader, body, digest);

View File

@ -0,0 +1,45 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.io.Parser;
import io.jsonwebtoken.lang.Assert;
import java.io.InputStream;
import java.io.Reader;
public abstract class AbstractParser<T> implements Parser<T> {
@Override
public final T parse(CharSequence input) {
Assert.hasText(input, "CharSequence cannot be null or empty.");
return parse(input, 0, input.length());
}
@Override
public T parse(CharSequence input, int start, int end) {
Assert.hasText(input, "CharSequence cannot be null or empty.");
Reader reader = new CharSequenceReader(input, start, end);
return parse(reader);
}
@Override
public final T parse(InputStream in) {
Assert.notNull(in, "InputStream cannot be null.");
Reader reader = Streams.reader(in);
return parse(reader);
}
}

View File

@ -0,0 +1,297 @@
/*
* Copyright © 2023 jsonwebtoken.io
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jsonwebtoken.impl.io;
import java.io.Reader;
import java.io.Serializable;
import java.util.Objects;
/**
* {@link Reader} implementation that can read from String, StringBuffer, StringBuilder or CharBuffer.
*
* <p>
* <strong>Note:</strong> Supports {@link #mark(int)} and {@link #reset()}.
* </p>
*
* @since JJWT_RELEASE_VERSION, copied from commons-io
* <a href="https://github.com/apache/commons-io/blob/e67946c81a55069dcd32dd588faa57dd1532455f/src/main/java/org/apache/commons/io/input/CharSequenceInputStream.java">2.14.0</a>
*/
public class CharSequenceReader extends Reader implements Serializable {
private static final long serialVersionUID = 3724187752191401220L;
private final CharSequence charSequence;
private int idx;
private int mark;
/**
* The start index in the character sequence, inclusive.
* <p>
* When de-serializing a CharSequenceReader that was serialized before
* this fields was added, this field will be initialized to 0, which
* gives the same behavior as before: start reading from the start.
* </p>
*
* @see #start()
* @since 2.7
*/
private final int start;
/**
* The end index in the character sequence, exclusive.
* <p>
* When de-serializing a CharSequenceReader that was serialized before
* this fields was added, this field will be initialized to {@code null},
* which gives the same behavior as before: stop reading at the
* CharSequence's length.
* If this field was an int instead, it would be initialized to 0 when the
* CharSequenceReader is de-serialized, causing it to not return any
* characters at all.
* </p>
*
* @see #end()
* @since 2.7
*/
private final Integer end;
/**
* Constructs a new instance with the specified character sequence.
*
* @param charSequence The character sequence, may be {@code null}
*/
public CharSequenceReader(final CharSequence charSequence) {
this(charSequence, 0);
}
/**
* Constructs a new instance with a portion of the specified character sequence.
* <p>
* The start index is not strictly enforced to be within the bounds of the
* character sequence. This allows the character sequence to grow or shrink
* in size without risking any {@link IndexOutOfBoundsException} to be thrown.
* Instead, if the character sequence grows smaller than the start index, this
* instance will act as if all characters have been read.
* </p>
*
* @param charSequence The character sequence, may be {@code null}
* @param start The start index in the character sequence, inclusive
* @throws IllegalArgumentException if the start index is negative
*/
public CharSequenceReader(final CharSequence charSequence, final int start) {
this(charSequence, start, Integer.MAX_VALUE);
}
/**
* Constructs a new instance with a portion of the specified character sequence.
* <p>
* The start and end indexes are not strictly enforced to be within the bounds
* of the character sequence. This allows the character sequence to grow or shrink
* in size without risking any {@link IndexOutOfBoundsException} to be thrown.
* Instead, if the character sequence grows smaller than the start index, this
* instance will act as if all characters have been read; if the character sequence
* grows smaller than the end, this instance will use the actual character sequence
* length.
* </p>
*
* @param charSequence The character sequence, may be {@code null}
* @param start The start index in the character sequence, inclusive
* @param end The end index in the character sequence, exclusive
* @throws IllegalArgumentException if the start index is negative, or if the end index is smaller than the start index
*/
public CharSequenceReader(final CharSequence charSequence, final int start, final int end) {
if (start < 0) {
throw new IllegalArgumentException("Start index is less than zero: " + start);
}
if (end < start) {
throw new IllegalArgumentException("End index is less than start " + start + ": " + end);
}
// Don't check the start and end indexes against the CharSequence,
// to let it grow and shrink without breaking existing behavior.
this.charSequence = charSequence != null ? charSequence : "";
this.start = start;
this.end = end;
this.idx = start;
this.mark = start;
}
/**
* Close resets the file back to the start and removes any marked position.
*/
@Override
public void close() {
idx = start;
mark = start;
}
/**
* Returns the index in the character sequence to end reading at, taking into account its length.
*
* @return The end index in the character sequence (exclusive).
*/
private int end() {
/*
* end == null for de-serialized instances that were serialized before start and end were added.
* Use Integer.MAX_VALUE to get the same behavior as before - use the entire CharSequence.
*/
return Math.min(charSequence.length(), end == null ? Integer.MAX_VALUE : end);
}
/**
* Mark the current position.
*
* @param readAheadLimit ignored
*/
@Override
public void mark(final int readAheadLimit) {
mark = idx;
}
/**
* Mark is supported (returns true).
*
* @return {@code true}
*/
@Override
public boolean markSupported() {
return true;
}
/**
* Read a single character.
*
* @return the next character from the character sequence
* or -1 if the end has been reached.
*/
@Override
public int read() {
if (idx >= end()) {
return Streams.EOF;
}
return charSequence.charAt(idx++);
}
/**
* Read the specified number of characters into the array.
*
* @param array The array to store the characters in
* @param offset The starting position in the array to store
* @param length The maximum number of characters to read
* @return The number of characters read or -1 if there are
* no more
*/
@Override
public int read(final char[] array, final int offset, final int length) {
if (idx >= end()) {
return Streams.EOF;
}
Objects.requireNonNull(array, "array");
if (length < 0 || offset < 0 || offset + length > array.length) {
throw new IndexOutOfBoundsException("Array Size=" + array.length +
", offset=" + offset + ", length=" + length);
}
if (charSequence instanceof String) {
final int count = Math.min(length, end() - idx);
((String) charSequence).getChars(idx, idx + count, array, offset);
idx += count;
return count;
}
if (charSequence instanceof StringBuilder) {
final int count = Math.min(length, end() - idx);
((StringBuilder) charSequence).getChars(idx, idx + count, array, offset);
idx += count;
return count;
}
if (charSequence instanceof StringBuffer) {
final int count = Math.min(length, end() - idx);
((StringBuffer) charSequence).getChars(idx, idx + count, array, offset);
idx += count;
return count;
}
int count = 0;
for (int i = 0; i < length; i++) {
final int c = read();
if (c == Streams.EOF) {
return count;
}
array[offset + i] = (char) c;
count++;
}
return count;
}
/**
* Tells whether this stream is ready to be read.
*
* @return {@code true} if more characters from the character sequence are available, or {@code false} otherwise.
*/
@Override
public boolean ready() {
return idx < end();
}
/**
* Reset the reader to the last marked position (or the beginning if
* mark has not been called).
*/
@Override
public void reset() {
idx = mark;
}
/**
* Skip the specified number of characters.
*
* @param n The number of characters to skip
* @return The actual number of characters skipped
*/
@Override
public long skip(final long n) {
if (n < 0) {
throw new IllegalArgumentException("Number of characters to skip is less than zero: " + n);
}
if (idx >= end()) {
return 0;
}
final int dest = (int) Math.min(end(), idx + n);
final int count = dest - idx;
idx = dest;
return count;
}
/**
* Returns the index in the character sequence to start reading from, taking into account its length.
*
* @return The start index in the character sequence (inclusive).
*/
private int start() {
return Math.min(charSequence.length(), start);
}
/**
* Return a String representation of the underlying
* character sequence.
*
* @return The contents of the character sequence
*/
@Override
public String toString() {
final CharSequence subSequence = charSequence.subSequence(start(), end());
return subSequence.toString();
}
}

View File

@ -17,33 +17,25 @@ package io.jsonwebtoken.impl.io;
import io.jsonwebtoken.impl.lang.Converter;
import io.jsonwebtoken.impl.lang.Function;
import io.jsonwebtoken.io.Parser;
import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Strings;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.Reader;
import java.util.Map;
public class ConvertingParser<T> implements Parser<T> {
public class ConvertingParser<T> extends AbstractParser<T> {
private final Function<InputStream, Map<String, ?>> deserializer;
private final Function<Reader, Map<String, ?>> deserializer;
private final Converter<T, Object> converter;
public ConvertingParser(Function<InputStream, Map<String, ?>> deserializer, Converter<T, Object> converter) {
public ConvertingParser(Function<Reader, Map<String, ?>> deserializer, Converter<T, Object> converter) {
this.deserializer = Assert.notNull(deserializer, "Deserializer function cannot be null.");
this.converter = Assert.notNull(converter, "Converter cannot be null.");
}
@Override
public final T parse(String input) {
Assert.hasText(input, "Parse input String cannot be null or empty.");
InputStream in = new ByteArrayInputStream(Strings.utf8(input));
return parse(in);
}
public final T parse(InputStream in) {
Map<String, ?> m = this.deserializer.apply(in);
public final T parse(Reader reader) {
Assert.notNull(reader, "Reader cannot be null.");
Map<String, ?> m = this.deserializer.apply(reader);
return this.converter.applyFrom(m);
}
}

View File

@ -21,7 +21,7 @@ import io.jsonwebtoken.io.DeserializationException;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.lang.Assert;
import java.io.InputStream;
import java.io.Reader;
import java.util.Map;
/**
@ -29,7 +29,7 @@ import java.util.Map;
*
* @since 0.11.3 (renamed from JwtDeserializer)
*/
public class JsonObjectDeserializer implements Function<InputStream, Map<String, ?>> {
public class JsonObjectDeserializer implements Function<Reader, Map<String, ?>> {
private static final String MALFORMED_ERROR = "Malformed %s JSON: %s";
private static final String MALFORMED_COMPLEX_ERROR = "Malformed or excessively complex %s JSON. " +
@ -45,7 +45,7 @@ public class JsonObjectDeserializer implements Function<InputStream, Map<String,
}
@Override
public Map<String, ?> apply(InputStream in) {
public Map<String, ?> apply(Reader in) {
Assert.notNull(in, "InputStream argument cannot be null.");
Object value;
try {

View File

@ -27,7 +27,9 @@ import java.io.ByteArrayOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.util.concurrent.Callable;
/**
@ -57,6 +59,18 @@ public class Streams {
return of(Strings.utf8(seq));
}
public static Reader reader(byte[] bytes) {
return reader(new ByteArrayInputStream(bytes));
}
public static Reader reader(InputStream in) {
return new InputStreamReader(in, Strings.UTF_8);
}
public static Reader reader(CharSequence seq) {
return new CharSequenceReader(seq);
}
public static void flush(Flushable... flushables) {
Objects.nullSafeFlush(flushables);
}

View File

@ -39,7 +39,7 @@ class RFC7515AppendixETest {
}
static <T> T deser(String s) {
T t = deserializer.deserialize(Streams.of(s)) as T
T t = deserializer.deserialize(Streams.reader(s)) as T
return t
}

View File

@ -52,7 +52,8 @@ class DefaultJwtBuilderTest {
}
private static Map<String, ?> deser(byte[] data) {
Map<String, ?> m = Services.loadFirst(Deserializer).deserialize(Streams.of(data)) as Map<String, ?>
def reader = Streams.reader(data)
Map<String, ?> m = Services.loadFirst(Deserializer).deserialize(reader) as Map<String, ?>
return m
}
@ -598,7 +599,7 @@ class DefaultJwtBuilderTest {
def jwt = builder.audience().single(audienceSingleString).compact()
// can't use the parser here to validate because it coerces the string value into an array automatically,
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = deser(bytes)
@ -617,7 +618,7 @@ class DefaultJwtBuilderTest {
def jwt = builder.audience().single(first).audience().single(second).compact()
// can't use the parser here to validate because it coerces the string value into an array automatically,
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = deser(bytes)
@ -753,9 +754,9 @@ class DefaultJwtBuilderTest {
// can't use the parser here to validate because it coerces the string value into an array automatically,
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = Services.loadFirst(Deserializer).deserialize(Streams.of(bytes))
def claims = Services.loadFirst(Deserializer).deserialize(Streams.reader(bytes))
assertEquals two, claims.aud
}
@ -790,7 +791,7 @@ class DefaultJwtBuilderTest {
// can't use the parser here to validate because it coerces the string value into an array automatically,
// so we need to check the raw payload:
def encoded = new JwtTokenizer().tokenize(jwt).getPayload()
def encoded = new JwtTokenizer().tokenize(Streams.reader(jwt)).getPayload()
byte[] bytes = Decoders.BASE64URL.decode(encoded)
def claims = deser(bytes)

View File

@ -121,8 +121,8 @@ class DefaultJwtParserBuilderTest {
void testDeserializeJsonWithCustomSerializer() {
def deserializer = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
return OBJECT_MAPPER.readValue(inputStream, Map.class)
protected Object doDeserialize(Reader reader) throws Exception {
return OBJECT_MAPPER.readValue(reader, Map.class)
}
}
def p = builder.deserializeJsonWith(deserializer)

View File

@ -75,9 +75,9 @@ class DefaultJwtParserTest {
boolean invoked = false
def deserializer = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
protected Object doDeserialize(Reader reader) throws Exception {
invoked = true
return OBJECT_MAPPER.readValue(inputStream, Map.class)
return OBJECT_MAPPER.readValue(reader, Map.class)
}
}
def pb = Jwts.parser().deserializeJsonWith(deserializer)

View File

@ -16,6 +16,7 @@
package io.jsonwebtoken.impl
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.impl.io.Streams
import org.junit.Before
import org.junit.Test
@ -32,40 +33,63 @@ class JwtTokenizerTest {
tokenizer = new JwtTokenizer()
}
private def tokenize(CharSequence s) {
return tokenizer.tokenize(Streams.reader(s))
}
@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlHeader() {
def input = 'header .body.signature'
tokenizer.tokenize(input)
tokenize(input)
}
@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlBody() {
def input = 'header. body.signature'
tokenizer.tokenize(input)
tokenize(input)
}
@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlSignature() {
def input = 'header.body. signature'
tokenizer.tokenize(input)
tokenize(input)
}
@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlJweBody() {
def input = 'header.encryptedKey.initializationVector. body.authenticationTag'
tokenizer.tokenize(input)
tokenize(input)
}
@Test(expected = MalformedJwtException)
void testParseWithWhitespaceInBase64UrlJweTag() {
def input = 'header.encryptedKey.initializationVector.body. authenticationTag'
tokenizer.tokenize(input)
tokenize(input)
}
@Test
void readerExceptionResultsInMalformedJwtException() {
IOException ioe = new IOException('foo')
def reader = new StringReader('hello') {
@Override
int read(char[] chars) throws IOException {
throw ioe
}
}
try {
JwtTokenizer.read(reader, new char[0])
fail()
} catch (MalformedJwtException expected) {
String msg = 'Unable to read compact JWT: foo'
assertEquals msg, expected.message
assertSame ioe, expected.cause
}
}
@Test
void testEmptyJws() {
def input = CharBuffer.wrap('header..digest'.toCharArray())
def t = tokenizer.tokenize(input)
def t = tokenize(input)
assertTrue t instanceof TokenizedJwt
assertFalse t instanceof TokenizedJwe
assertEquals 'header', t.getProtected().toString()
@ -78,7 +102,7 @@ class JwtTokenizerTest {
def input = 'header.encryptedKey.initializationVector.body.authenticationTag'
def t = tokenizer.tokenize(input)
def t = tokenize(input)
assertNotNull t
assertTrue t instanceof TokenizedJwe

View File

@ -15,7 +15,7 @@
*/
package io.jsonwebtoken.impl
import io.jsonwebtoken.impl.io.Streams
import io.jsonwebtoken.impl.io.CharSequenceReader
import io.jsonwebtoken.impl.lang.Services
import io.jsonwebtoken.impl.security.Randoms
import io.jsonwebtoken.io.Decoders
@ -37,7 +37,8 @@ class RfcTests {
}
static final Map<String, ?> jsonToMap(String json) {
Map<String, ?> m = Services.loadFirst(Deserializer).deserialize(Streams.of(json)) as Map<String, ?>
Reader r = new CharSequenceReader(json)
Map<String, ?> m = Services.loadFirst(Deserializer).deserialize(r) as Map<String, ?>
return m
}

View File

@ -16,9 +16,9 @@
package io.jsonwebtoken.impl.io
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.impl.lang.Bytes
import io.jsonwebtoken.io.DeserializationException
import io.jsonwebtoken.io.Deserializer
import io.jsonwebtoken.lang.Strings
import org.junit.Test
import static org.junit.Assert.*
@ -40,14 +40,14 @@ class JsonObjectDeserializerTest {
}
@Override
Object deserialize(InputStream inputStream) throws DeserializationException {
Object deserialize(Reader reader) throws DeserializationException {
throw err
}
}
try {
// doesn't matter for this test, just has to be non-null:
def ins = new ByteArrayInputStream(Bytes.EMPTY)
new JsonObjectDeserializer(deser, 'claims').apply(ins)
def r = new StringReader(Strings.EMPTY)
new JsonObjectDeserializer(deser, 'claims').apply(r)
fail()
} catch (DeserializationException e) {
String msg = String.format(JsonObjectDeserializer.MALFORMED_COMPLEX_ERROR, 'claims', 'claims', 'foo')
@ -67,15 +67,16 @@ class JsonObjectDeserializerTest {
fail() // should not be called in this test
return null
}
@Override
Object deserialize(InputStream inputStream) throws DeserializationException {
Object deserialize(Reader reader) throws DeserializationException {
throw ex
}
}
try {
// doesn't matter for this test, just has to be non-null:
def ins = new ByteArrayInputStream(Bytes.EMPTY)
new JsonObjectDeserializer(deser, 'claims').apply(ins)
def r = new StringReader(Strings.EMPTY)
new JsonObjectDeserializer(deser, 'claims').apply(r)
fail()
} catch (MalformedJwtException e) {
String msg = String.format(JsonObjectDeserializer.MALFORMED_ERROR, 'claims', 'foo')

View File

@ -15,6 +15,7 @@
*/
package io.jsonwebtoken.impl.security
import io.jsonwebtoken.impl.io.CharSequenceReader
import io.jsonwebtoken.impl.io.ConvertingParser
import io.jsonwebtoken.io.AbstractDeserializer
import io.jsonwebtoken.io.DeserializationException
@ -49,6 +50,16 @@ class DefaultJwkParserBuilderTest {
KipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"
}''')
@Test(expected = IllegalArgumentException)
void parseNull() {
Jwks.parser().build().parse((CharSequence)null)
}
@Test(expected = IllegalArgumentException)
void parseEmpty() {
Jwks.parser().build().parse(Strings.EMPTY)
}
@Test
void testStaticFactoryMethod() {
assertTrue Jwks.parser() instanceof DefaultJwkParserBuilder
@ -63,9 +74,9 @@ class DefaultJwkParserBuilderTest {
@Test
void testDeserializer() {
Deserializer<Map<String,?>> deser = createMock(Deserializer)
def m = RFC7516AppendixA3Test.KEK_VALUES // any test key will do
expect(deser.deserialize((InputStream)anyObject(InputStream))).andReturn(m)
Deserializer<Map<String, ?>> deser = createMock(Deserializer)
def m = RFC7516AppendixA3Test.KEK_VALUES // any test key will do
expect(deser.deserialize((Reader) anyObject(Reader))).andReturn(m)
replay deser
def jwk = Jwks.parser().json(deser).build().parse('foo')
verify deser
@ -110,7 +121,19 @@ class DefaultJwkParserBuilderTest {
//noinspection GroovyAssignabilityCheck
def jwk = Jwks.builder().key(key).build()
String json = Jwks.UNSAFE_JSON(jwk)
def parsed = Jwks.parser().build().parse(json)
def parser = Jwks.parser().build()
// CharSequence parsing:
def parsed = parser.parse(json)
assertEquals jwk, parsed
// Reader parsing:
parsed = parser.parse(new CharSequenceReader(json))
assertEquals jwk, parsed
// InputStream parsing:
parsed = parser.parse(new ByteArrayInputStream(Strings.utf8(json)))
assertEquals jwk, parsed
}
}
@ -141,7 +164,7 @@ class DefaultJwkParserBuilderTest {
void testDeserializationFailure() {
def deser = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
protected Object doDeserialize(Reader reader) throws Exception {
throw new DeserializationException('test')
}
}

View File

@ -68,7 +68,7 @@ class DefaultJwkSetParserBuilderTest {
void testDeserializeException() {
def deser = new AbstractDeserializer() {
@Override
protected Object doDeserialize(InputStream inputStream) throws Exception {
protected Object doDeserialize(Reader reader) throws Exception {
throw new DeserializationException('foo')
}
}

View File

@ -47,8 +47,7 @@ class JwkSerializationTest {
}
static Map<String, ?> deserialize(Deserializer des, String value) {
def ins = new ByteArrayInputStream(Strings.utf8(value))
return des.deserialize(ins) as Map<String, ?>
return des.deserialize(new StringReader(value)) as Map<String, ?>
}
@Test

View File

@ -18,7 +18,6 @@ package io.jsonwebtoken.impl.security
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jwe
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.impl.io.Streams
import io.jsonwebtoken.impl.lang.Services
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.io.Deserializer
@ -44,7 +43,7 @@ class RFC7518AppendixCTest {
}
private static final Map<String, ?> fromJson(String s) {
return Services.loadFirst(Deserializer).deserialize(Streams.of(s)) as Map<String, ?>
return Services.loadFirst(Deserializer).deserialize(new StringReader(s)) as Map<String, ?>
}
private static EcPrivateJwk readJwk(String json) {

View File

@ -309,6 +309,7 @@
<exclude>io/jsonwebtoken/impl/io/Base64InputStream.java</exclude>
<exclude>io/jsonwebtoken/impl/io/FilteredInputStream.java</exclude>
<exclude>io/jsonwebtoken/impl/io/FilteredOutputStream.java</exclude>
<exclude>io/jsonwebtoken/impl/io/CharSequenceReader.java</exclude>
</excludes>
<methodPercentage>100.000000%</methodPercentage>
<statementPercentage>100.000000%</statementPercentage>