Skip to content

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

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

Merged
merged 7 commits into from
Oct 26, 2020
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);

}
}