From 8f76301d12d18d2e37afb9446f4037483e244558 Mon Sep 17 00:00:00 2001 From: Nickita Khylkouski <90287684+nickita-khylkouski@users.noreply.github.com> Date: Wed, 28 Jan 2026 13:43:07 -0800 Subject: [PATCH] BaseEncoding: Make encodingStream().close() idempotent The OutputStream returned by encodingStream() violated the Closeable.close() contract: calling close() multiple times would write additional encoded data to the underlying Writer. This adds a boolean closed field and synchronized methods to track stream state, making close() idempotent and causing write()/flush() to throw IOException after close. Fixes #5284 --- .../google/common/io/BaseEncodingTest.java | 28 +++++++++++++++++++ .../common/io/ReflectionFreeAssertThrows.java | 2 ++ .../com/google/common/io/BaseEncoding.java | 17 +++++++++-- 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/guava-tests/test/com/google/common/io/BaseEncodingTest.java b/guava-tests/test/com/google/common/io/BaseEncodingTest.java index 82c1b4e1994c..15cf4e11e1ad 100644 --- a/guava-tests/test/com/google/common/io/BaseEncodingTest.java +++ b/guava-tests/test/com/google/common/io/BaseEncodingTest.java @@ -572,6 +572,34 @@ private static void testStreamingDecodes(BaseEncoding encoding, String encoded, } } + @GwtIncompatible // Writer,OutputStream + public void testEncodingStreamCloseIsIdempotent() throws IOException { + StringWriter writer = new StringWriter(); + OutputStream encodingStream = base64().encodingStream(writer); + encodingStream.write(0); + encodingStream.close(); + assertThat(writer.toString()).isEqualTo("AA=="); + // Closing again should have no effect. + encodingStream.close(); + assertThat(writer.toString()).isEqualTo("AA=="); + } + + @GwtIncompatible // Writer,OutputStream + public void testEncodingStreamWriteAfterClose() throws IOException { + StringWriter writer = new StringWriter(); + OutputStream encodingStream = base64().encodingStream(writer); + encodingStream.close(); + assertThrows(IOException.class, () -> encodingStream.write(0)); + } + + @GwtIncompatible // Writer,OutputStream + public void testEncodingStreamFlushAfterClose() throws IOException { + StringWriter writer = new StringWriter(); + OutputStream encodingStream = base64().encodingStream(writer); + encodingStream.close(); + assertThrows(IOException.class, () -> encodingStream.flush()); + } + public void testToString() { assertThat(base64().toString()).isEqualTo("BaseEncoding.base64().withPadChar('=')"); assertThat(base32Hex().omitPadding().toString()) diff --git a/guava-tests/test/com/google/common/io/ReflectionFreeAssertThrows.java b/guava-tests/test/com/google/common/io/ReflectionFreeAssertThrows.java index 957bfb4902a1..fc391c54f8e7 100644 --- a/guava-tests/test/com/google/common/io/ReflectionFreeAssertThrows.java +++ b/guava-tests/test/com/google/common/io/ReflectionFreeAssertThrows.java @@ -23,6 +23,7 @@ import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableMap; import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.nio.charset.UnsupportedCharsetException; import java.util.ConcurrentModificationException; @@ -136,6 +137,7 @@ ImmutableMap, Predicate> exceptions() { .put(ExecutionException.class, e -> e instanceof ExecutionException) .put(IllegalArgumentException.class, e -> e instanceof IllegalArgumentException) .put(IllegalStateException.class, e -> e instanceof IllegalStateException) + .put(IOException.class, e -> e instanceof IOException) .put(IndexOutOfBoundsException.class, e -> e instanceof IndexOutOfBoundsException) .put(NoSuchElementException.class, e -> e instanceof NoSuchElementException) .put(NullPointerException.class, e -> e instanceof NullPointerException) diff --git a/guava/src/com/google/common/io/BaseEncoding.java b/guava/src/com/google/common/io/BaseEncoding.java index a12d7519f55f..6b3f80aef4e5 100644 --- a/guava/src/com/google/common/io/BaseEncoding.java +++ b/guava/src/com/google/common/io/BaseEncoding.java @@ -645,9 +645,13 @@ public OutputStream encodingStream(Writer out) { int bitBuffer = 0; int bitBufferLength = 0; int writtenChars = 0; + boolean closed = false; @Override - public void write(int b) throws IOException { + public synchronized void write(int b) throws IOException { + if (closed) { + throw new IOException("Stream is closed"); + } bitBuffer <<= 8; bitBuffer |= b & 0xFF; bitBufferLength += 8; @@ -660,12 +664,19 @@ public void write(int b) throws IOException { } @Override - public void flush() throws IOException { + public synchronized void flush() throws IOException { + if (closed) { + throw new IOException("Stream is closed"); + } out.flush(); } @Override - public void close() throws IOException { + public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; if (bitBufferLength > 0) { int charIndex = (bitBuffer << (alphabet.bitsPerChar - bitBufferLength)) & alphabet.mask; out.write(alphabet.encode(charIndex));