This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-codec.git

commit 0130064aa803637090adff8beea3c33d2c44708d
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 18 05:52:32 2026 -0700

    Ensure BaseNCodecOutputStream closes underlying resources on failure
    
    Close the underlying stream even when final conversion or flushing
    fails.
    Preserve the original exception and suppress any additional close
    failure.
    Add regression tests for cleanup and exception handling.
---
 src/changes/changes.xml                            |  1 +
 .../codec/binary/BaseNCodecOutputStream.java       | 10 ++-
 .../codec/binary/BaseNCodecOutputStreamTest.java   | 78 ++++++++++++++++++++++
 3 files changed, 86 insertions(+), 3 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 25e3558f..8e3a2410 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -58,6 +58,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Limit Base58 
decoding to 8192 encoded bytes by default, checking cumulative stream input 
before buffering. Use Base58.Builder.setMaxDecodeLength(int) for larger trusted 
input. Encoding defaults to a configurable 8192-byte binary input limit through 
Base58.Builder.setMaxEncodeLength(int).</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Grow Base58 
accumulation buffers geometrically within the configured input limits and 
validate actual accumulated length.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Implement Base58 
encoded-length calculation and explicitly reject unsupported line 
chunking.</action>
+      <action type="fix" dev="ggregory" due-to="Gary Gregory">Close the 
underlying BaseNCodecOutputStream output even when final conversion or flushing 
fails, preserving suppressed close exceptions.</action>
       <!-- ADD -->
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add and use 
PhoneticEngine.Builder and deprecate old constructors.</action>
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
BeiderMorseEncoder.Builder and deprecate old constructor.</action>
diff --git 
a/src/main/java/org/apache/commons/codec/binary/BaseNCodecOutputStream.java 
b/src/main/java/org/apache/commons/codec/binary/BaseNCodecOutputStream.java
index d037c1e6..49993819 100644
--- a/src/main/java/org/apache/commons/codec/binary/BaseNCodecOutputStream.java
+++ b/src/main/java/org/apache/commons/codec/binary/BaseNCodecOutputStream.java
@@ -123,6 +123,9 @@ public class BaseNCodecOutputStream<C extends BaseNCodec, T 
extends BaseNCodecOu
     /**
      * Closes this output stream and releases any system resources associated 
with the stream.
      * <p>
+     * The underlying stream is closed even if final conversion or flushing 
fails. If closing also fails, its exception is suppressed on the original 
exception.
+     * </p>
+     * <p>
      * To write the EOF marker without closing the stream, call {@link #eof()} 
or use an <a href="https://commons.apache.org/proper/commons-io/";>Apache Commons
      * IO</a>
      * <a href= 
"https://commons.apache.org/proper/commons-io/apidocs/org/apache/commons/io/output/CloseShieldOutputStream.html";
 >CloseShieldOutputStream</a>.
@@ -132,9 +135,10 @@ public class BaseNCodecOutputStream<C extends BaseNCodec, 
T extends BaseNCodecOu
      */
     @Override
     public void close() throws IOException {
-        eof();
-        flush();
-        out.close();
+        try (OutputStream outputStream = out) { // NOPMD
+            eof();
+            flush();
+        }
     }
 
     /**
diff --git 
a/src/test/java/org/apache/commons/codec/binary/BaseNCodecOutputStreamTest.java 
b/src/test/java/org/apache/commons/codec/binary/BaseNCodecOutputStreamTest.java
index b044961c..366e91ec 100644
--- 
a/src/test/java/org/apache/commons/codec/binary/BaseNCodecOutputStreamTest.java
+++ 
b/src/test/java/org/apache/commons/codec/binary/BaseNCodecOutputStreamTest.java
@@ -17,9 +17,17 @@
 
 package org.apache.commons.codec.binary;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.io.OutputStream;
 
+import org.junit.jupiter.api.Test;
+
 /**
  * Tests {@link BaseNCodecOutputStream}.
  */
@@ -29,4 +37,74 @@ public class BaseNCodecOutputStreamTest extends 
AbstractBaseNOutputStreamTest {
     OutputStream newOutputStream() {
         return new BaseNCodecOutputStream<>(new ByteArrayOutputStream(), new 
NoOpBaseNCodec(), true);
     }
+
+    @Test
+    void testCloseAfterInvalidBase58() throws IOException {
+        final boolean[] closed = { false };
+        final ByteArrayOutputStream sink = new ByteArrayOutputStream() {
+            @Override
+            public void close() {
+                closed[0] = true;
+            }
+        };
+        final Base58OutputStream stream = 
Base58OutputStream.builder().setOutputStream(sink).setEncode(false).get();
+        stream.write('0');
+        final IOException failure = assertThrows(IOException.class, 
stream::close);
+        assertTrue(failure.getCause() instanceof IllegalArgumentException);
+        assertTrue(closed[0]);
+        assertEquals(0, sink.size());
+    }
+
+    @Test
+    void testCloseFailureAfterSuccessfulConversion() throws IOException {
+        final IOException closeFailure = new IOException("close");
+        final ByteArrayOutputStream sink = new ByteArrayOutputStream() {
+            @Override
+            public void close() throws IOException {
+                throw closeFailure;
+            }
+        };
+        final Base58OutputStream stream = new Base58OutputStream(sink);
+        stream.write(1);
+        assertSame(closeFailure, assertThrows(IOException.class, 
stream::close));
+        assertEquals("2", sink.toString("US-ASCII"));
+    }
+
+    @Test
+    void testCloseFailureSuppressedAfterFlushFailure() {
+        final IOException flushFailure = new IOException("flush");
+        final IOException closeFailure = new IOException("close");
+        final ByteArrayOutputStream sink = new ByteArrayOutputStream() {
+            @Override
+            public void close() throws IOException {
+                throw closeFailure;
+            }
+
+            @Override
+            public void flush() throws IOException {
+                throw flushFailure;
+            }
+        };
+        final Base64OutputStream stream = new Base64OutputStream(sink);
+        assertSame(flushFailure, assertThrows(IOException.class, 
stream::close));
+        assertEquals(1, flushFailure.getSuppressed().length);
+        assertSame(closeFailure, flushFailure.getSuppressed()[0]);
+    }
+
+    @Test
+    void testCloseFailureSuppressedAfterInvalidBase58() throws IOException {
+        final IOException closeFailure = new IOException("close");
+        final ByteArrayOutputStream sink = new ByteArrayOutputStream() {
+            @Override
+            public void close() throws IOException {
+                throw closeFailure;
+            }
+        };
+        final Base58OutputStream stream = 
Base58OutputStream.builder().setOutputStream(sink).setEncode(false).get();
+        stream.write('0');
+        final IOException failure = assertThrows(IOException.class, 
stream::close);
+        assertTrue(failure.getCause() instanceof IllegalArgumentException);
+        assertEquals(1, failure.getSuppressed().length);
+        assertSame(closeFailure, failure.getSuppressed()[0]);
+    }
 }

Reply via email to