Skip to content

Commit

Permalink
Fix an issue where the InputStream returned by BaseEncoding.decodingS…
Browse files Browse the repository at this point in the history
…tream(Reader) could fail to throw DecodingException while decoding an invalid string.

This was caused by the default behavior of InputStream.read(byte[], int, int), which swallows any IOException thrown by any call to the single-byte read() method other than the first. To fix it, just override that method with an implementation that does not swallow any exceptions.

Fixes #3542

RELNOTES=n/a

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=261712883
  • Loading branch information
cgdecker authored and netdpb committed Aug 16, 2019
1 parent a3c9f2c commit ddd4a49
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 30 deletions.
83 changes: 68 additions & 15 deletions android/guava-tests/test/com/google/common/io/BaseEncodingTest.java
Expand Up @@ -31,6 +31,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import junit.framework.TestCase;
Expand Down Expand Up @@ -389,23 +390,75 @@ private static void assertFailsToDecode(BaseEncoding encoding, String cannotDeco

private static void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @NullableDecl String expectedMessage) {
assertFalse(encoding.canDecode(cannotDecode));
try {
encoding.decode(cannotDecode);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
if (expectedMessage != null) {
assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo(expectedMessage);
}
// We use this somewhat weird pattern with an enum for each assertion we want to make as a way
// of dealing with the fact that one of the assertions is @GwtIncompatible but we don't want to
// have to have duplicate @GwtIncompatible test methods just to make that assertion.
for (AssertFailsToDecodeStrategy strategy : AssertFailsToDecodeStrategy.values()) {
strategy.assertFailsToDecode(encoding, cannotDecode, expectedMessage);
}
try {
encoding.decodeChecked(cannotDecode);
fail("Expected DecodingException");
} catch (DecodingException expected) {
if (expectedMessage != null) {
assertThat(expected).hasMessageThat().isEqualTo(expectedMessage);
}

enum AssertFailsToDecodeStrategy {
@GwtIncompatible // decodingStream(Reader)
DECODING_STREAM {
@Override
void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @NullableDecl String expectedMessage) {
// Regression test for case where DecodingException was swallowed by default implementation
// of
// InputStream.read(byte[], int, int)
// See https://github.com/google/guava/issues/3542
Reader reader = new StringReader(cannotDecode);
InputStream decodingStream = encoding.decodingStream(reader);
try {
ByteStreams.exhaust(decodingStream);
fail("Expected DecodingException");
} catch (DecodingException expected) {
// Don't assert on the expectedMessage; the messages for exceptions thrown from the
// decoding stream may differ from the messages for the decode methods.
} catch (IOException e) {
fail("Expected DecodingException but got: " + e);
}
}
}
},
CAN_DECODE {
@Override
void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @NullableDecl String expectedMessage) {
assertFalse(encoding.canDecode(cannotDecode));
}
},
DECODE {
@Override
void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @NullableDecl String expectedMessage) {
try {
encoding.decode(cannotDecode);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
if (expectedMessage != null) {
assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo(expectedMessage);
}
}
}
},
DECODE_CHECKED {
@Override
void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @NullableDecl String expectedMessage) {
try {
encoding.decodeChecked(cannotDecode);
fail("Expected DecodingException");
} catch (DecodingException expected) {
if (expectedMessage != null) {
assertThat(expected).hasMessageThat().isEqualTo(expectedMessage);
}
}
}
};

abstract void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @NullableDecl String expectedMessage);
}

@GwtIncompatible // Reader/Writer
Expand Down
21 changes: 21 additions & 0 deletions android/guava/src/com/google/common/io/BaseEncoding.java
Expand Up @@ -771,6 +771,27 @@ public int read() throws IOException {
}
}

@Override
public int read(byte[] buf, int off, int len) throws IOException {
// Overriding this to work around the fact that InputStream's default implementation of
// this method will silently swallow exceptions thrown by the single-byte read() method
// (other than on the first call to it), which in this case can cause invalid encoded
// strings to not throw an exception.
// See https://github.com/google/guava/issues/3542
checkPositionIndexes(off, off + len, buf.length);

int i = off;
for (; i < off + len; i++) {
int b = read();
if (b == -1) {
int read = i - off;
return read == 0 ? -1 : read;
}
buf[i] = (byte) b;
}
return i - off;
}

@Override
public void close() throws IOException {
reader.close();
Expand Down
83 changes: 68 additions & 15 deletions guava-tests/test/com/google/common/io/BaseEncodingTest.java
Expand Up @@ -31,6 +31,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import junit.framework.TestCase;
Expand Down Expand Up @@ -389,23 +390,75 @@ private static void assertFailsToDecode(BaseEncoding encoding, String cannotDeco

private static void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @Nullable String expectedMessage) {
assertFalse(encoding.canDecode(cannotDecode));
try {
encoding.decode(cannotDecode);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
if (expectedMessage != null) {
assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo(expectedMessage);
}
// We use this somewhat weird pattern with an enum for each assertion we want to make as a way
// of dealing with the fact that one of the assertions is @GwtIncompatible but we don't want to
// have to have duplicate @GwtIncompatible test methods just to make that assertion.
for (AssertFailsToDecodeStrategy strategy : AssertFailsToDecodeStrategy.values()) {
strategy.assertFailsToDecode(encoding, cannotDecode, expectedMessage);
}
try {
encoding.decodeChecked(cannotDecode);
fail("Expected DecodingException");
} catch (DecodingException expected) {
if (expectedMessage != null) {
assertThat(expected).hasMessageThat().isEqualTo(expectedMessage);
}

enum AssertFailsToDecodeStrategy {
@GwtIncompatible // decodingStream(Reader)
DECODING_STREAM {
@Override
void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @Nullable String expectedMessage) {
// Regression test for case where DecodingException was swallowed by default implementation
// of
// InputStream.read(byte[], int, int)
// See https://github.com/google/guava/issues/3542
Reader reader = new StringReader(cannotDecode);
InputStream decodingStream = encoding.decodingStream(reader);
try {
ByteStreams.exhaust(decodingStream);
fail("Expected DecodingException");
} catch (DecodingException expected) {
// Don't assert on the expectedMessage; the messages for exceptions thrown from the
// decoding stream may differ from the messages for the decode methods.
} catch (IOException e) {
fail("Expected DecodingException but got: " + e);
}
}
}
},
CAN_DECODE {
@Override
void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @Nullable String expectedMessage) {
assertFalse(encoding.canDecode(cannotDecode));
}
},
DECODE {
@Override
void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @Nullable String expectedMessage) {
try {
encoding.decode(cannotDecode);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException expected) {
if (expectedMessage != null) {
assertThat(expected).hasCauseThat().hasMessageThat().isEqualTo(expectedMessage);
}
}
}
},
DECODE_CHECKED {
@Override
void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @Nullable String expectedMessage) {
try {
encoding.decodeChecked(cannotDecode);
fail("Expected DecodingException");
} catch (DecodingException expected) {
if (expectedMessage != null) {
assertThat(expected).hasMessageThat().isEqualTo(expectedMessage);
}
}
}
};

abstract void assertFailsToDecode(
BaseEncoding encoding, String cannotDecode, @Nullable String expectedMessage);
}

@GwtIncompatible // Reader/Writer
Expand Down
21 changes: 21 additions & 0 deletions guava/src/com/google/common/io/BaseEncoding.java
Expand Up @@ -771,6 +771,27 @@ public int read() throws IOException {
}
}

@Override
public int read(byte[] buf, int off, int len) throws IOException {
// Overriding this to work around the fact that InputStream's default implementation of
// this method will silently swallow exceptions thrown by the single-byte read() method
// (other than on the first call to it), which in this case can cause invalid encoded
// strings to not throw an exception.
// See https://github.com/google/guava/issues/3542
checkPositionIndexes(off, off + len, buf.length);

int i = off;
for (; i < off + len; i++) {
int b = read();
if (b == -1) {
int read = i - off;
return read == 0 ? -1 : read;
}
buf[i] = (byte) b;
}
return i - off;
}

@Override
public void close() throws IOException {
reader.close();
Expand Down

0 comments on commit ddd4a49

Please sign in to comment.