diff --git a/NOTICE.md b/NOTICE.md index 079325e4..fbfc898f 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -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 diff --git a/api/src/main/java/io/jsonwebtoken/JwtParser.java b/api/src/main/java/io/jsonwebtoken/JwtParser.java index b2888064..e0da4cdb 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParser.java @@ -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> { /** * 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 { * *

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.

* * @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:

* * * * @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 parse(String jwt, JwtHandler handler) throws ExpiredJwtException, UnsupportedJwtException, + T parse(CharSequence jwt, JwtHandler 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 parseContentJwt(String jwt) throws UnsupportedJwtException, MalformedJwtException, + Jwt 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 parseClaimsJwt(String jwt) throws ExpiredJwtException, UnsupportedJwtException, + Jwt 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 parseContentJws(String jws) throws UnsupportedJwtException, MalformedJwtException, SignatureException, + Jws 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 parseContentJws(String jws, byte[] unencodedPayload); + Jws 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 parseClaimsJws(String jws, byte[] unencodedPayload); + Jws 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 parseContentJws(String jws, InputStream unencodedPayload); + Jws parseContentJws(CharSequence jws, InputStream unencodedPayload); /** * Parses a JWS known to use the @@ -285,7 +286,7 @@ public interface JwtParser { *

NOTE: 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.

+ * large Claims), it is recommended to use the {@link #parseContentJws(CharSequence, InputStream)} method instead.

* *

Unencoded Non-Detached Payload

*

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 parseClaimsJws(String jws, InputStream unencodedPayload); + Jws 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 parseClaimsJws(String jws) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, + Jws 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 parseContentJwe(String jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, + Jwe 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 parseClaimsJwe(String jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, + Jwe parseClaimsJwe(CharSequence jwe) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SecurityException, IllegalArgumentException; } diff --git a/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java b/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java index a9a946e8..10526eaf 100644 --- a/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/AbstractDeserializer.java @@ -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 the type of object returned after deserialization * @since JJWT_RELEASE_VERSION @@ -48,17 +51,19 @@ public abstract class AbstractDeserializer implements Deserializer { @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 implements Deserializer { } /** - * 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; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Deserializer.java b/api/src/main/java/io/jsonwebtoken/io/Deserializer.java index 5b73b5e1..4962425e 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Deserializer.java +++ b/api/src/main/java/io/jsonwebtoken/io/Deserializer.java @@ -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 { * @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; } diff --git a/api/src/main/java/io/jsonwebtoken/io/Parser.java b/api/src/main/java/io/jsonwebtoken/io/Parser.java index 4addf0a1..0f5c3180 100644 --- a/api/src/main/java/io/jsonwebtoken/io/Parser.java +++ b/api/src/main/java/io/jsonwebtoken/io/Parser.java @@ -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. * - *

Semantically, this interface might have been more accurately named - * Unmarshaller 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:

- *
    - *
  • {@link io.jsonwebtoken.Jwts#parser() Jwts.parser()}
  • - *
  • {@link io.jsonwebtoken.security.Jwks#parser() Jwks.parser()}
  • - *
  • {@link io.jsonwebtoken.security.Jwks#setParser() Jwks.setParser()}
  • - *
- * - * @param the instance type created after parsing/unmarshalling + * @param the instance type created after parsing * @since JJWT_RELEASE_VERSION */ public interface Parser { /** - * 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: + * + *
{@link #parse(Reader) parse}(new {@link java.io.InputStreamReader
+     * InputStreamReader}(in, {@link java.nio.charset.StandardCharsets#UTF_8
+     * StandardCharsets.UTF_8});
+ * + * + * @param in the UTF-8 InputStream. + * @return the Java object represented by the specified {@link InputStream}. + */ + T parse(InputStream in); } diff --git a/api/src/test/groovy/io/jsonwebtoken/io/AbstractDeserializerTest.groovy b/api/src/test/groovy/io/jsonwebtoken/io/AbstractDeserializerTest.groovy index b139cfd9..e693d297 100644 --- a/api/src/test/groovy/io/jsonwebtoken/io/AbstractDeserializerTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/io/AbstractDeserializerTest.groovy @@ -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 } } diff --git a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java index f4987516..6565596b 100644 --- a/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java +++ b/extensions/gson/src/main/java/io/jsonwebtoken/gson/io/GsonDeserializer.java @@ -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 extends AbstractDeserializer { @@ -46,8 +43,7 @@ public class GsonDeserializer extends AbstractDeserializer { } @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); } } diff --git a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy index aca9f132..b553f3c8 100644 --- a/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy +++ b/extensions/gson/src/test/groovy/io/jsonwebtoken/gson/io/GsonDeserializerTest.groovy @@ -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 } } diff --git a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java index b32cc3a3..8314f1ef 100644 --- a/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java +++ b/extensions/jackson/src/main/java/io/jsonwebtoken/jackson/io/JacksonDeserializer.java @@ -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 extends AbstractDeserializer { } @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); } /** diff --git a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy index 3b4c3041..25e27a36 100644 --- a/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy +++ b/extensions/jackson/src/test/groovy/io/jsonwebtoken/jackson/io/JacksonDeserializerTest.groovy @@ -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' diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java index 7bb975c4..49be0675 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java @@ -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 { @Override - protected Object doDeserialize(InputStream in) { - Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8); + protected Object doDeserialize(Reader reader) { return parse(reader); } diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy index 1273afbf..5f27dcbe 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy @@ -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 } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 749322b6..51e143fa 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -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> 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 parse(String compact, JwtHandler handler) { + public T parse(CharSequence compact, JwtHandler handler) { return parse(compact, Payload.EMPTY, handler); } - private T parse(String compact, Payload unencodedPayload, JwtHandler handler) + private T parse(CharSequence compact, Payload unencodedPayload, JwtHandler 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 parseContentJwt(String compact) { + public Jwt parseContentJwt(CharSequence compact) { return parse(compact, new JwtHandlerAdapter>() { @Override public Jwt onContentJwt(Jwt jwt) { @@ -815,7 +818,7 @@ public class DefaultJwtParser implements JwtParser { } @Override - public Jwt parseClaimsJwt(String compact) { + public Jwt parseClaimsJwt(CharSequence compact) { return parse(compact, new JwtHandlerAdapter>() { @Override public Jwt onClaimsJwt(Jwt jwt) { @@ -825,7 +828,7 @@ public class DefaultJwtParser implements JwtParser { } @Override - public Jws parseContentJws(String compact) { + public Jws parseContentJws(CharSequence compact) { return parse(compact, new JwtHandlerAdapter>() { @Override public Jws onContentJws(Jws jws) { @@ -835,7 +838,7 @@ public class DefaultJwtParser implements JwtParser { } @Override - public Jws parseClaimsJws(String compact) { + public Jws parseClaimsJws(CharSequence compact) { return parse(compact, new JwtHandlerAdapter>() { @Override public Jws onClaimsJws(Jws jws) { @@ -844,7 +847,7 @@ public class DefaultJwtParser implements JwtParser { }); } - private Jws parseContentJws(String jws, Payload unencodedPayload) { + private Jws parseContentJws(CharSequence jws, Payload unencodedPayload) { return parse(jws, unencodedPayload, new JwtHandlerAdapter>() { @Override public Jws onContentJws(Jws jws) { @@ -853,7 +856,7 @@ public class DefaultJwtParser implements JwtParser { }); } - private Jws parseClaimsJws(String jws, Payload unencodedPayload) { + private Jws parseClaimsJws(CharSequence jws, Payload unencodedPayload) { unencodedPayload.setClaimsExpected(true); return parse(jws, unencodedPayload, new JwtHandlerAdapter>() { @Override @@ -864,13 +867,13 @@ public class DefaultJwtParser implements JwtParser { } @Override - public Jws parseContentJws(String jws, byte[] unencodedPayload) { + public Jws 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 parseClaimsJws(String jws, byte[] unencodedPayload) { + public Jws 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 parseContentJws(String jws, InputStream unencodedPayload) { + public Jws parseContentJws(CharSequence jws, InputStream unencodedPayload) { Assert.notNull(unencodedPayload, "unencodedPayload InputStream cannot be null."); return parseContentJws(jws, payloadFor(unencodedPayload)); } @Override - public Jws parseClaimsJws(String jws, InputStream unencodedPayload) { + public Jws 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 parseContentJwe(String compact) throws JwtException { + public Jwe parseContentJwe(CharSequence compact) throws JwtException { return parse(compact, new JwtHandlerAdapter>() { @Override public Jwe onContentJwe(Jwe jwe) { @@ -909,7 +912,7 @@ public class DefaultJwtParser implements JwtParser { } @Override - public Jwe parseClaimsJwe(String compact) throws JwtException { + public Jwe parseClaimsJwe(CharSequence compact) throws JwtException { return parse(compact, new JwtHandlerAdapter>() { @Override public Jwe onClaimsJwe(Jwe jwe) { @@ -932,8 +935,9 @@ public class DefaultJwtParser implements JwtParser { protected Map 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); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java index a0845fe5..56b29c2f 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/JwtTokenizer.java @@ -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 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 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); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParser.java b/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParser.java new file mode 100644 index 00000000..bebbb118 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/AbstractParser.java @@ -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 implements Parser { + + @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); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/CharSequenceReader.java b/impl/src/main/java/io/jsonwebtoken/impl/io/CharSequenceReader.java new file mode 100644 index 00000000..df68e1b4 --- /dev/null +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/CharSequenceReader.java @@ -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. + * + *

+ * Note: Supports {@link #mark(int)} and {@link #reset()}. + *

+ * + * @since JJWT_RELEASE_VERSION, copied from commons-io + * 2.14.0 + */ +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. + *

+ * 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. + *

+ * + * @see #start() + * @since 2.7 + */ + private final int start; + + /** + * The end index in the character sequence, exclusive. + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

+ * + * @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. + *

+ * 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. + *

+ * + * @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(); + } +} diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/ConvertingParser.java b/impl/src/main/java/io/jsonwebtoken/impl/io/ConvertingParser.java index 66ae6f17..232bc6eb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/ConvertingParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/ConvertingParser.java @@ -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 implements Parser { +public class ConvertingParser extends AbstractParser { - private final Function> deserializer; + private final Function> deserializer; private final Converter converter; - public ConvertingParser(Function> deserializer, Converter converter) { + public ConvertingParser(Function> deserializer, Converter 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 m = this.deserializer.apply(in); + public final T parse(Reader reader) { + Assert.notNull(reader, "Reader cannot be null."); + Map m = this.deserializer.apply(reader); return this.converter.applyFrom(m); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/JsonObjectDeserializer.java b/impl/src/main/java/io/jsonwebtoken/impl/io/JsonObjectDeserializer.java index d9fa7436..a79a50aa 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/JsonObjectDeserializer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/JsonObjectDeserializer.java @@ -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> { +public class JsonObjectDeserializer implements Function> { 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 apply(InputStream in) { + public Map apply(Reader in) { Assert.notNull(in, "InputStream argument cannot be null."); Object value; try { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/io/Streams.java b/impl/src/main/java/io/jsonwebtoken/impl/io/Streams.java index ae04e645..122c44db 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/io/Streams.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/io/Streams.java @@ -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); } diff --git a/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy b/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy index 2f3390ed..52d1e4a2 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/RFC7515AppendixETest.groovy @@ -39,7 +39,7 @@ class RFC7515AppendixETest { } static T deser(String s) { - T t = deserializer.deserialize(Streams.of(s)) as T + T t = deserializer.deserialize(Streams.reader(s)) as T return t } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index 1c6382ed..4e8b79f7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -52,7 +52,8 @@ class DefaultJwtBuilderTest { } private static Map deser(byte[] data) { - Map m = Services.loadFirst(Deserializer).deserialize(Streams.of(data)) as Map + def reader = Streams.reader(data) + Map m = Services.loadFirst(Deserializer).deserialize(reader) as Map 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) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy index 931ec58b..ad0c4ba4 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserBuilderTest.groovy @@ -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) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index 6fb9886a..12329b35 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -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) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy index 54862117..dadc1102 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/JwtTokenizerTest.groovy @@ -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 diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy index 768bb16b..548cf408 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/RfcTests.groovy @@ -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 jsonToMap(String json) { - Map m = Services.loadFirst(Deserializer).deserialize(Streams.of(json)) as Map + Reader r = new CharSequenceReader(json) + Map m = Services.loadFirst(Deserializer).deserialize(r) as Map return m } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/io/JsonObjectDeserializerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/io/JsonObjectDeserializerTest.groovy index eff5e967..0df6d49b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/io/JsonObjectDeserializerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/io/JsonObjectDeserializerTest.groovy @@ -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') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy index a969c90c..0d43218f 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkParserBuilderTest.groovy @@ -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> deser = createMock(Deserializer) - def m = RFC7516AppendixA3Test.KEK_VALUES // any test key will do - expect(deser.deserialize((InputStream)anyObject(InputStream))).andReturn(m) + Deserializer> 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') } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilderTest.groovy index 7fbb9476..5d26ee7d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultJwkSetParserBuilderTest.groovy @@ -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') } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy index 8e4f3a61..5ae4212c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/JwkSerializationTest.groovy @@ -47,8 +47,7 @@ class JwkSerializationTest { } static Map deserialize(Deserializer des, String value) { - def ins = new ByteArrayInputStream(Strings.utf8(value)) - return des.deserialize(ins) as Map + return des.deserialize(new StringReader(value)) as Map } @Test diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy index 93c54460..b07905bc 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/RFC7518AppendixCTest.groovy @@ -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 fromJson(String s) { - return Services.loadFirst(Deserializer).deserialize(Streams.of(s)) as Map + return Services.loadFirst(Deserializer).deserialize(new StringReader(s)) as Map } private static EcPrivateJwk readJwk(String json) { diff --git a/pom.xml b/pom.xml index 75b4002e..54cb8cb9 100644 --- a/pom.xml +++ b/pom.xml @@ -309,6 +309,7 @@ io/jsonwebtoken/impl/io/Base64InputStream.java io/jsonwebtoken/impl/io/FilteredInputStream.java io/jsonwebtoken/impl/io/FilteredOutputStream.java + io/jsonwebtoken/impl/io/CharSequenceReader.java 100.000000% 100.000000%