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

veithen pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-axiom.git

commit baaccaa4731d41591ca338010c77f9da6239ef0f
Author: Andreas Martens <[email protected]>
AuthorDate: Tue Oct 27 15:50:10 2026 +0000

    AXIOM-531: Fix BufferOverflowException in OutputStreamXmlWriter
    
    write(char[], int, int) and write(String) copy their input into the
    4096 char encoder buffer in chunks, but passed the total remaining
    length instead of the chunk size c to CharBuffer.put. If the buffer
    was already partly filled and the input was longer than the free
    space, this caused a BufferOverflowException.
    
    For write(String), the third argument of CharBuffer.put(CharSequence,
    int, int) is an end index rather than a length, so it must be
    offset + c.
    
    OutputStreamXmlWriter is only used for encodings other than UTF-8,
    US-ASCII and ISO-8859-1 (e.g. IBM437, UTF-16), which is why the
    problem went unnoticed. Add tests that cover large inputs, partly
    filled buffers, non-zero offsets and unmappable characters.
---
 .../serializer/writer/OutputStreamXmlWriter.java   |   5 +-
 .../writer/OutputStreamXmlWriterTest.java          | 158 +++++++++++++++++++++
 2 files changed, 161 insertions(+), 2 deletions(-)

diff --git 
a/components/core-streams/src/main/java/org/apache/axiom/core/stream/serializer/writer/OutputStreamXmlWriter.java
 
b/components/core-streams/src/main/java/org/apache/axiom/core/stream/serializer/writer/OutputStreamXmlWriter.java
index 98f932aab..aacbb072d 100644
--- 
a/components/core-streams/src/main/java/org/apache/axiom/core/stream/serializer/writer/OutputStreamXmlWriter.java
+++ 
b/components/core-streams/src/main/java/org/apache/axiom/core/stream/serializer/writer/OutputStreamXmlWriter.java
@@ -120,7 +120,8 @@ final class OutputStreamXmlWriter extends XmlWriter {
                 flush(encoderIn);
             }
             int c = Math.min(length, encoderIn.remaining());
-            encoderIn.put(src, offset, length);
+            // Note: unlike put(char[], offset, length), put(CharSequence, 
start, end) takes an end index
+            encoderIn.put(src, offset, offset + c);
             offset += c;
             length -= c;
         }
@@ -134,7 +135,7 @@ final class OutputStreamXmlWriter extends XmlWriter {
                 flush(encoderIn);
             }
             int c = Math.min(length, encoderIn.remaining());
-            encoderIn.put(src, offset, length);
+            encoderIn.put(src, offset, c);
             offset += c;
             length -= c;
         }
diff --git 
a/components/core-streams/src/test/java/org/apache/axiom/core/stream/serializer/writer/OutputStreamXmlWriterTest.java
 
b/components/core-streams/src/test/java/org/apache/axiom/core/stream/serializer/writer/OutputStreamXmlWriterTest.java
index 2d3340eaf..5cf1c799e 100644
--- 
a/components/core-streams/src/test/java/org/apache/axiom/core/stream/serializer/writer/OutputStreamXmlWriterTest.java
+++ 
b/components/core-streams/src/test/java/org/apache/axiom/core/stream/serializer/writer/OutputStreamXmlWriterTest.java
@@ -21,8 +21,13 @@ package org.apache.axiom.core.stream.serializer.writer;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.io.ByteArrayOutputStream;
+import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.stream.Stream;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 
 public class OutputStreamXmlWriterTest {
     @Test
@@ -34,4 +39,157 @@ public class OutputStreamXmlWriterTest {
         writer.flushBuffer();
         assertThat(baos.toString("iso-8859-1")).isEqualTo("abc&#x20ac;def");
     }
+
+    private static final String PREFIX = "<a>";
+
+    private static Stream<Arguments> charsets() {
+        return Stream.of(Arguments.of(StandardCharsets.UTF_8), 
Arguments.of(StandardCharsets.ISO_8859_1));
+    }
+
+    /** Sizes around the length of the internal buffer (4096). */
+    private static Stream<Arguments> prefixedPayloadSizes() {
+        return Stream.of(4093, 4094, 4095, 4096, 4097, 8192, 
10000).map(Arguments::of);
+    }
+
+    /**
+     * Generates a string with an incrementing pattern of printable ASCII 
characters (period 95,
+     * which is coprime with the buffer size), so that any wrong slice shows 
up as a mismatch.
+     */
+    private static String pattern(int length) {
+        StringBuilder sb = new StringBuilder(length);
+        for (int i = 0; i < length; i++) {
+            sb.append((char) (0x20 + i % 95));
+        }
+        return sb.toString();
+    }
+
+    private static String roundTrip(Charset charset, WriterAction action) 
throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        XmlWriter writer = new OutputStreamXmlWriter(baos, charset);
+        action.run(writer);
+        writer.flushBuffer();
+        return baos.toString(charset.name());
+    }
+
+    private interface WriterAction {
+        void run(XmlWriter writer) throws Exception;
+    }
+
+    @ParameterizedTest
+    @MethodSource("charsets")
+    public void testLargeStringIntoEmptyBuffer(Charset charset) throws 
Exception {
+        String payload = pattern(10500);
+        assertThat(roundTrip(charset, writer -> 
writer.write(payload))).isEqualTo(payload);
+    }
+
+    @ParameterizedTest
+    @MethodSource("charsets")
+    public void testLargeCharArrayIntoEmptyBuffer(Charset charset) throws 
Exception {
+        String payload = pattern(10500);
+        char[] chars = payload.toCharArray();
+        assertThat(roundTrip(charset, writer -> writer.write(chars, 0, 
chars.length)))
+                .isEqualTo(payload);
+    }
+
+    @ParameterizedTest
+    @MethodSource("prefixedPayloadSizes")
+    public void testStringIntoPartlyFilledBuffer(int size) throws Exception {
+        String payload = pattern(size);
+        assertThat(roundTrip(StandardCharsets.UTF_8, writer -> {
+                    writer.write(PREFIX);
+                    writer.write(payload);
+                }))
+                .isEqualTo(PREFIX + payload);
+    }
+
+    @ParameterizedTest
+    @MethodSource("prefixedPayloadSizes")
+    public void testCharArrayIntoPartlyFilledBuffer(int size) throws Exception 
{
+        char[] payload = pattern(size).toCharArray();
+        assertThat(roundTrip(StandardCharsets.ISO_8859_1, writer -> {
+                    writer.write(PREFIX);
+                    writer.write(payload, 0, payload.length);
+                }))
+                .isEqualTo(PREFIX + new String(payload));
+    }
+
+    @ParameterizedTest
+    @MethodSource("prefixedPayloadSizes")
+    public void testCharArrayWithOffset(int size) throws Exception {
+        String full = pattern(size + 1000);
+        char[] chars = full.toCharArray();
+        assertThat(roundTrip(StandardCharsets.UTF_8, writer -> {
+                    writer.write(PREFIX);
+                    writer.write(chars, 777, size);
+                }))
+                .isEqualTo(PREFIX + full.substring(777, 777 + size));
+    }
+
+    @ParameterizedTest
+    @MethodSource("prefixedPayloadSizes")
+    public void testRepeatedLargeStringWrites(int size) throws Exception {
+        // XmlWriter has no write(String, int, int); the internal offset into 
the source string
+        // advances across several chunks here, following a prefix and a 
previous large write.
+        String payload = pattern(size);
+        assertThat(roundTrip(StandardCharsets.UTF_8, writer -> {
+                    writer.write(PREFIX);
+                    writer.write(payload);
+                    writer.write(payload);
+                }))
+                .isEqualTo(PREFIX + payload + payload);
+    }
+
+    @ParameterizedTest
+    @MethodSource("charsets")
+    public void testInterleavedSmallAndLargeWrites(Charset charset) throws 
Exception {
+        StringBuilder expected = new StringBuilder();
+        String large = pattern(5000);
+        char[] largeChars = pattern(9000).toCharArray();
+        String result = roundTrip(charset, writer -> {
+            for (int round = 0; round < 3; round++) {
+                for (int i = 0; i < 1000; i++) {
+                    writer.write((char) ('a' + i % 26));
+                    expected.append((char) ('a' + i % 26));
+                    writer.write("xy");
+                    expected.append("xy");
+                }
+                writer.write(large);
+                expected.append(large);
+                writer.write(largeChars, 0, largeChars.length);
+                expected.append(largeChars);
+            }
+        });
+        assertThat(result).isEqualTo(expected.toString());
+    }
+
+    @ParameterizedTest
+    @MethodSource("prefixedPayloadSizes")
+    public void testLargeInputWithUnmappableCharacters(int size) throws 
Exception {
+        // Place a euro sign every 97 characters
+        StringBuilder input = new StringBuilder();
+        StringBuilder expected = new StringBuilder();
+        String base = pattern(size);
+        for (int i = 0; i < size; i++) {
+            if (i % 97 == 96) {
+                input.append('\u20AC');
+                expected.append("&#x20ac;");
+            } else {
+                input.append(base.charAt(i));
+                expected.append(base.charAt(i));
+            }
+        }
+        char[] chars = input.toString().toCharArray();
+        for (boolean useString : new boolean[] {false, true}) {
+            String result = roundTrip(StandardCharsets.ISO_8859_1, writer -> {
+                
writer.setUnmappableCharacterHandler(UnmappableCharacterHandler.CONVERT_TO_CHARACTER_REFERENCE);
+                writer.write(PREFIX);
+                if (useString) {
+                    writer.write(input.toString());
+                } else {
+                    writer.write(chars, 0, chars.length);
+                }
+            });
+            assertThat(result).isEqualTo(PREFIX + expected);
+        }
+    }
 }

Reply via email to