Skip to content

Commit

Permalink
improvement: allowing more granular control of reading behaviour for …
Browse files Browse the repository at this point in the history
…base64 (#646)

Fix #500: allow optional padding
  • Loading branch information
pavan-kalyan authored Oct 26, 2020
1 parent 2e11f0c commit d169161
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 13 deletions.
104 changes: 96 additions & 8 deletions src/main/java/com/fasterxml/jackson/core/Base64Variant.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ public final class Base64Variant
*/
private final transient boolean _usesPadding;

/**
* Whether padding characters should be required or not while decoding
*/
private final PaddingReadBehaviour _paddingReadBehaviour;

/**
* Character used for padding, if any ({@link #PADDING_CHAR_NONE} if not).
*/
Expand Down Expand Up @@ -136,6 +141,12 @@ public Base64Variant(String name, String base64Alphabet, boolean usesPadding, ch
if (usesPadding) {
_asciiToBase64[(int) paddingChar] = BASE64_VALUE_PADDING;
}

if (usesPadding) {
this._paddingReadBehaviour = PaddingReadBehaviour.PADDING_REQUIRED;
} else {
this._paddingReadBehaviour = PaddingReadBehaviour.PADDING_FORBIDDEN;
}
}

/**
Expand All @@ -154,6 +165,11 @@ public Base64Variant(Base64Variant base, String name, int maxLineLength)
* line length) differ
*/
public Base64Variant(Base64Variant base, String name, boolean usesPadding, char paddingChar, int maxLineLength)
{
this(base, name, usesPadding, paddingChar, base._paddingReadBehaviour, maxLineLength);
}

private Base64Variant(Base64Variant base, String name, boolean usesPadding, char paddingChar, PaddingReadBehaviour paddingReadBehaviour, int maxLineLength)
{
_name = name;
byte[] srcB = base._base64ToAsciiB;
Expand All @@ -166,6 +182,56 @@ public Base64Variant(Base64Variant base, String name, boolean usesPadding, char
_usesPadding = usesPadding;
_paddingChar = paddingChar;
_maxLineLength = maxLineLength;
this._paddingReadBehaviour = paddingReadBehaviour;
}

private Base64Variant(Base64Variant base, PaddingReadBehaviour paddingReadBehaviour) {
this(base, base._name, base._usesPadding, base._paddingChar, paddingReadBehaviour, base._maxLineLength);
}

/**
* @return Base64Variant which does not require padding on read
* @since 2.12
*/
public Base64Variant withPaddingAllowed() {
return new Base64Variant(this, PaddingReadBehaviour.PADDING_ALLOWED);
}

/**
* @return Base64Variant which requires padding on read
* @since 2.12
*/
public Base64Variant withPaddingRequired() {
return new Base64Variant(this, PaddingReadBehaviour.PADDING_REQUIRED);
}

/**
* @return Base64Variant which does not accept padding on read
* @since 2.12
*/
public Base64Variant withPaddingForbidden() {
return new Base64Variant(this, PaddingReadBehaviour.PADDING_FORBIDDEN);
}

/**
* @param writePadding Determines if padding is output on write
* @return Base64Variant which writes padding or not depending on writePadding
* @since 2.12
*/
public Base64Variant withWritePadding(boolean writePadding) {
return new Base64Variant(this, this._name, writePadding, this._paddingChar, this._maxLineLength);

}

/**
* Defines how the Base64Variant deals with Padding while reading
* @since 2.12
*/
public enum PaddingReadBehaviour {
PADDING_FORBIDDEN,
PADDING_REQUIRED,
PADDING_ALLOWED
;
}

/*
Expand Down Expand Up @@ -193,6 +259,7 @@ protected Object readResolve() {
public boolean usesPadding() { return _usesPadding; }
public boolean usesPaddingChar(char c) { return c == _paddingChar; }
public boolean usesPaddingChar(int ch) { return ch == (int) _paddingChar; }
public PaddingReadBehaviour paddingReadBehaviour() { return _paddingReadBehaviour; }
public char getPaddingChar() { return _paddingChar; }
public byte getPaddingByte() { return (byte)_paddingChar; }

Expand Down Expand Up @@ -275,7 +342,7 @@ public int encodeBase64Partial(int bits, int outputBytes, char[] buffer, int out
{
buffer[outPtr++] = _base64ToAsciiC[(bits >> 18) & 0x3F];
buffer[outPtr++] = _base64ToAsciiC[(bits >> 12) & 0x3F];
if (_usesPadding) {
if (usesPadding()) {
buffer[outPtr++] = (outputBytes == 2) ?
_base64ToAsciiC[(bits >> 6) & 0x3F] : _paddingChar;
buffer[outPtr++] = _paddingChar;
Expand All @@ -291,7 +358,7 @@ public void encodeBase64Partial(StringBuilder sb, int bits, int outputBytes)
{
sb.append(_base64ToAsciiC[(bits >> 18) & 0x3F]);
sb.append(_base64ToAsciiC[(bits >> 12) & 0x3F]);
if (_usesPadding) {
if (usesPadding()) {
sb.append((outputBytes == 2) ?
_base64ToAsciiC[(bits >> 6) & 0x3F] : _paddingChar);
sb.append(_paddingChar);
Expand Down Expand Up @@ -333,7 +400,7 @@ public int encodeBase64Partial(int bits, int outputBytes, byte[] buffer, int out
{
buffer[outPtr++] = _base64ToAsciiB[(bits >> 18) & 0x3F];
buffer[outPtr++] = _base64ToAsciiB[(bits >> 12) & 0x3F];
if (_usesPadding) {
if (usesPadding()) {
byte pb = (byte) _paddingChar;
buffer[outPtr++] = (outputBytes == 2) ?
_base64ToAsciiB[(bits >> 6) & 0x3F] : pb;
Expand Down Expand Up @@ -529,8 +596,8 @@ public void decode(String str, ByteArrayBuilder builder) throws IllegalArgumentE
decodedData = (decodedData << 6) | bits;
// third base64 char; can be padding, but not ws
if (ptr >= len) {
// but as per [JACKSON-631] can be end-of-input, iff not using padding
if (!usesPadding()) {
// but as per [JACKSON-631] can be end-of-input, iff padding is not required
if (!paddingReadBehaviour().equals(PaddingReadBehaviour.PADDING_REQUIRED)) {
decodedData >>= 4;
builder.append(decodedData);
break;
Expand All @@ -545,6 +612,9 @@ public void decode(String str, ByteArrayBuilder builder) throws IllegalArgumentE
if (bits != Base64Variant.BASE64_VALUE_PADDING) {
_reportInvalidBase64(ch, 2, null);
}
if (paddingReadBehaviour().equals(PaddingReadBehaviour.PADDING_FORBIDDEN)) {
_reportBase64UnexpectedPadding();
}
// Ok, must get padding
if (ptr >= len) {
_reportBase64EOF();
Expand All @@ -562,8 +632,8 @@ public void decode(String str, ByteArrayBuilder builder) throws IllegalArgumentE
decodedData = (decodedData << 6) | bits;
// fourth and last base64 char; can be padding, but not ws
if (ptr >= len) {
// but as per [JACKSON-631] can be end-of-input, iff not using padding
if (!usesPadding()) {
// but as per [JACKSON-631] can be end-of-input, iff padding on read is not required
if (!paddingReadBehaviour().equals(PaddingReadBehaviour.PADDING_REQUIRED)) {
decodedData >>= 2;
builder.appendTwoBytes(decodedData);
break;
Expand All @@ -576,6 +646,9 @@ public void decode(String str, ByteArrayBuilder builder) throws IllegalArgumentE
if (bits != Base64Variant.BASE64_VALUE_PADDING) {
_reportInvalidBase64(ch, 3, null);
}
if (paddingReadBehaviour().equals(PaddingReadBehaviour.PADDING_FORBIDDEN)) {
_reportBase64UnexpectedPadding();
}
decodedData >>= 2;
builder.appendTwoBytes(decodedData);
} else {
Expand Down Expand Up @@ -640,14 +713,29 @@ protected void _reportBase64EOF() throws IllegalArgumentException {
throw new IllegalArgumentException(missingPaddingMessage());
}

protected void _reportBase64UnexpectedPadding() throws IllegalArgumentException {
throw new IllegalArgumentException(unexpectedPaddingMessage());
}

/**
* Helper method that will construct a message to use in exceptions for cases where input ends
* prematurely in place where padding is not expected.
*
* @since 2.12
*/
public String unexpectedPaddingMessage() {
return String.format("Unexpected end of base64-encoded String: base64 variant '%s' expects no padding at the end while decoding. This Base64Variant might have been incorrectly configured",
getName());
}

/**
* Helper method that will construct a message to use in exceptions for cases where input ends
* prematurely in place where padding would be expected.
*
* @since 2.10
*/
public String missingPaddingMessage() {
return String.format("Unexpected end of base64-encoded String: base64 variant '%s' expects padding (one or more '%c' characters) at the end",
return String.format("Unexpected end of base64-encoded String: base64 variant '%s' expects padding (one or more '%c' characters) at the end. This Base64Variant might have been incorrectly configured",
getName(), getPaddingChar());
}
}
Expand Down
9 changes: 8 additions & 1 deletion src/main/java/com/fasterxml/jackson/core/Base64Variants.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
* <li> {@link #PEM}
* <li> {@link #MODIFIED_FOR_URL}
* </ul>
*
*
* If a Base64Variant with default configuration outputs padding it also expects it on reading.
* If it does not output padding it will not accept padding on read.
*
* @author Tatu Saloranta
*/
public final class Base64Variants
Expand All @@ -28,6 +31,7 @@ public final class Base64Variants
* Note that although this can be thought of as the standard variant,
* it is <b>not</b> the default for Jackson: no-linefeeds alternative
* is because of JSON requirement of escaping all linefeeds.
* writes padding on output; does not accept padding when reading (may change later with a call to {@link Base64Variant#withWritePadding(boolean)}])
*/
public final static Base64Variant MIME;
static {
Expand All @@ -39,6 +43,7 @@ public final class Base64Variants
* use linefeeds (max line length set to infinite). Useful when linefeeds
* wouldn't work well (possibly in attributes), or for minor space savings
* (save 1 linefeed per 76 data chars, ie. ~1.4% savings).
* writes padding on output; does not accept padding when reading (may change later with a call to {@link Base64Variant#withWritePadding(boolean)}])
*/
public final static Base64Variant MIME_NO_LINEFEEDS;
static {
Expand All @@ -48,6 +53,7 @@ public final class Base64Variants
/**
* This variant is the one that predates {@link #MIME}: it is otherwise
* identical, except that it mandates shorter line length.
* writes padding on output; does not accept padding when reading (may change later with a call to {@link Base64Variant#withWritePadding(boolean)}])
*/
public final static Base64Variant PEM = new Base64Variant(MIME, "PEM", true, '=', 64);

Expand All @@ -61,6 +67,7 @@ public final class Base64Variants
* line length set to infinite). And finally, two characters (plus and
* slash) that would need quoting in URLs are replaced with more
* optimal alternatives (hyphen and underscore, respectively).
* writes padding on output; does not accept padding when reading (may change later with a call to {@link Base64Variant#withWritePadding(boolean)}])
*/
public final static Base64Variant MODIFIED_FOR_URL;
static {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ public void testCharEncoding() throws Exception

assertEquals(Base64Variant.BASE64_VALUE_INVALID, std.decodeBase64Byte((byte) '?'));
assertEquals(Base64Variant.BASE64_VALUE_INVALID, std.decodeBase64Byte((byte) 0xA0));

assertEquals(0, std.decodeBase64Char('A'));
assertEquals(1, std.decodeBase64Char((int) 'B'));
assertEquals(2, std.decodeBase64Char((byte)'C'));

assertEquals(0, std.decodeBase64Byte((byte) 'A'));
assertEquals(1, std.decodeBase64Byte((byte) 'B'));
assertEquals(2, std.decodeBase64Byte((byte)'C'));

assertEquals('/', std.encodeBase64BitsAsChar(63));
assertEquals((byte) 'b', std.encodeBase64BitsAsByte(27));

Expand All @@ -82,7 +82,7 @@ public void testConvenienceMethods() throws Exception

byte[] input = new byte[] { 1, 2, 34, 127, -1 };
String encoded = std.encode(input, false);
byte[] decoded = std.decode(encoded);
byte[] decoded = std.decode(encoded);
Assert.assertArrayEquals(input, decoded);

assertEquals(quote(encoded), std.encode(input, true));
Expand Down Expand Up @@ -115,7 +115,7 @@ public void testConvenienceMethodWithLFs() throws Exception
}
sb.append("AQ==");
final String exp = sb.toString();

// first, JSON standard
assertEquals(exp.replace("##", "\\n"), std.encode(data, false));

Expand Down Expand Up @@ -148,4 +148,67 @@ public void testErrors() throws Exception
verifyException(iae, "Illegal character");
}
}

public void testPaddingReadBehaviour() throws Exception {

for (Base64Variant variant: Arrays.asList(Base64Variants.MIME, Base64Variants.MIME_NO_LINEFEEDS, Base64Variants.PEM)) {

final String BASE64_HELLO = "aGVsbG8=";
try {
variant.withPaddingForbidden().decode(BASE64_HELLO);
fail("Should not pass");
} catch (IllegalArgumentException iae) {
verifyException(iae, "no padding");
}

variant.withPaddingAllowed().decode(BASE64_HELLO);
variant.withPaddingRequired().decode(BASE64_HELLO);

final String BASE64_HELLO_WITHOUT_PADDING = "aGVsbG8";
try {
variant.withPaddingRequired().decode(BASE64_HELLO_WITHOUT_PADDING);
fail("Should not pass");
} catch (IllegalArgumentException iae) {
verifyException(iae, "expects padding");
}
variant.withPaddingAllowed().decode(BASE64_HELLO_WITHOUT_PADDING);
variant.withPaddingForbidden().decode(BASE64_HELLO_WITHOUT_PADDING);
}

//testing for MODIFIED_FOR_URL

final String BASE64_HELLO = "aGVsbG8=";
try {
Base64Variants.MODIFIED_FOR_URL.withPaddingForbidden().decode(BASE64_HELLO);
fail("Should not pass");
} catch (IllegalArgumentException iae) {
verifyException(iae, "illegal character");
}

try {
Base64Variants.MODIFIED_FOR_URL.withPaddingAllowed().decode(BASE64_HELLO);
fail("Should not pass");
} catch (IllegalArgumentException iae) {
verifyException(iae, "illegal character");
}

try {
Base64Variants.MODIFIED_FOR_URL.withPaddingRequired().decode(BASE64_HELLO);
fail("Should not pass");
} catch (IllegalArgumentException iae) {
verifyException(iae, "illegal character");
}

final String BASE64_HELLO_WITHOUT_PADDING = "aGVsbG8";
try {
Base64Variants.MODIFIED_FOR_URL.withPaddingRequired().decode(BASE64_HELLO_WITHOUT_PADDING);
fail("Should not pass");
} catch (IllegalArgumentException iae) {
verifyException(iae, "expects padding");
}

Base64Variants.MODIFIED_FOR_URL.withPaddingAllowed().decode(BASE64_HELLO_WITHOUT_PADDING);
Base64Variants.MODIFIED_FOR_URL.withPaddingForbidden().decode(BASE64_HELLO_WITHOUT_PADDING);

}
}

0 comments on commit d169161

Please sign in to comment.