Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

improvement: allowing more granular control of reading behaviour for base64 #646

Merged
merged 7 commits into from
Oct 26, 2020
95 changes: 87 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)
{
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved
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,47 @@ 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);
}

/**
* @since 2.12
*/
public Base64Variant withPaddingAllowed() {
return new Base64Variant(this, PaddingReadBehaviour.PADDING_ALLOWED);
}

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

/**
* @since 2.12
*/
public Base64Variant withPaddingForbidden() {
return new Base64Variant(this, PaddingReadBehaviour.PADDING_FORBIDDEN);
}

/**
* @since 2.12
*/
public Base64Variant withWritePadding(boolean writePadding) {
return new Base64Variant(this, this._name, writePadding, this._paddingChar, this._maxLineLength);

}

private enum PaddingReadBehaviour {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since exposed by public accessor, needs to be public type (I can change this post-merge).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed and added comments and @since

PADDING_FORBIDDEN,
PADDING_REQUIRED,
PADDING_ALLOWED
;
}

/*
Expand Down Expand Up @@ -193,6 +250,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 +333,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 +349,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 +391,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 +587,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 +603,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 +623,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 +637,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 +704,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
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);

}
}