Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,6 @@ Sahana (@Sahana2524)
* Fixed #899: Return `null` from `nextTextValue()` at end-of-input (instead of
throwing `IllegalStateException`)
(2.23.0)
* Fixed #894: `ToXmlGenerator.writeBinary(Base64Variant, InputStream, int)` fails with
`NegativeArraySizeException`/`IndexOutOfBoundsException` for unknown (negative) length
(2.23.0)
3 changes: 3 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Project: jackson-dataformat-xml
#899: Return `null` from `nextTextValue()` at end-of-input (instead of
throwing `IllegalStateException`)
(fix by @Sahana2524)
#894: `ToXmlGenerator.writeBinary(Base64Variant, InputStream, int)` fails with
`NegativeArraySizeException`/`IndexOutOfBoundsException` for unknown (negative) length
(fix by @Sahana2524)

2.22.2 (16-Aug-2026)
2.22.1 (07-Jul-2026)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.fasterxml.jackson.core.base.GeneratorBase;
import com.fasterxml.jackson.core.io.IOContext;
import com.fasterxml.jackson.core.json.JsonWriteContext;
import com.fasterxml.jackson.core.util.ByteArrayBuilder;
import com.fasterxml.jackson.core.util.JacksonFeatureSet;
import com.fasterxml.jackson.dataformat.xml.XmlPrettyPrinter;
import com.fasterxml.jackson.dataformat.xml.util.DefaultXmlPrettyPrinter;
Expand Down Expand Up @@ -1023,57 +1024,83 @@ public int writeBinary(Base64Variant b64variant, InputStream data, int dataLengt
handleMissingName();
}
final org.codehaus.stax2.typed.Base64Variant stax2base64v = StaxUtil.toStax2Base64Variant(b64variant);
// 22-Aug-2026, sahana: negative `dataLength` means "unknown, read to the end";
// stream where we can, buffer only where Stax2 needs a full buffer
int written = dataLength;
try {
if (_nextIsAttribute) {
// Stax2 API only has 'full buffer' write method:
byte[] fullBuffer = toFullBuffer(data, dataLength);
byte[] fullBuffer = (dataLength < 0) ? toFullBuffer(data) : toFullBuffer(data, dataLength);
written = fullBuffer.length;
_xmlWriter.writeBinaryAttribute(stax2base64v,
"", _nextName.getNamespaceURI(), _nextName.getLocalPart(), fullBuffer);
} else if (checkNextIsUnwrapped()) {
// should we consider pretty-printing or not?
writeStreamAsBinary(stax2base64v, data, dataLength);
written = writeStreamAsBinary(stax2base64v, data, dataLength);

} else {
if (_xmlPrettyPrinter != null) {
byte[] fullBuffer = (dataLength < 0) ? toFullBuffer(data) : toFullBuffer(data, dataLength);
written = fullBuffer.length;
_xmlPrettyPrinter.writeLeafElement(_xmlWriter,
_nextName.getNamespaceURI(), _nextName.getLocalPart(),
stax2base64v, toFullBuffer(data, dataLength), 0, dataLength);
stax2base64v, fullBuffer, 0, written);
} else {
_xmlWriter.writeStartElement(_nextName.getNamespaceURI(), _nextName.getLocalPart());
writeStreamAsBinary(stax2base64v, data, dataLength);
written = writeStreamAsBinary(stax2base64v, data, dataLength);
_xmlWriter.writeEndElement();
}
}
} catch (XMLStreamException e) {
StaxUtil.throwAsGenerationException(e, this);
}

return dataLength;
return written;
}

private void writeStreamAsBinary(org.codehaus.stax2.typed.Base64Variant stax2base64v,
/**
* Helper method for encoding contents of given stream: at most {@code len}
* bytes if non-negative, or until end-of-stream if negative.
*
* @return Number of bytes read and encoded
*/
private int writeStreamAsBinary(org.codehaus.stax2.typed.Base64Variant stax2base64v,
InputStream data, int len) throws IOException, XMLStreamException
{
// base64 encodes up to 3 bytes into a 4 bytes string
byte[] tmp = new byte[3];
int offset = 0;
int read;
while((read = data.read(tmp, offset, Math.min(3 - offset, len))) != -1) {
offset += read;
len -= read;
if(offset == 3) {
offset = 0;
_xmlWriter.writeBinary(stax2base64v, tmp, 0, 3);
}
if (len == 0) {
break;
final byte[] buf = _ioContext.allocBase64Buffer();
int total = 0;
try {
int end = 0; // number of buffered bytes not yet written
while (true) {
int max = buf.length - end;
if (len >= 0 && len < max) {
max = len;
}
int count = (max == 0) ? -1 : data.read(buf, end, max);
if (count < 0) { // end-of-stream, or requested length reached
if (end > 0) {
_xmlWriter.writeBinary(stax2base64v, buf, 0, end);
}
break;
}
end += count;
total += count;
if (len > 0) {
len -= count;
}
// base64 encodes 3 bytes into 4 characters: write complete triplets,
// keep the remainder for the next round
int full = end - (end % 3);
if (full > 0) {
_xmlWriter.writeBinary(stax2base64v, buf, 0, full);
end -= full;
System.arraycopy(buf, full, buf, 0, end);
}
}
} finally {
_ioContext.releaseBase64Buffer(buf);
}

// we still have < 3 bytes in the buffer
if(offset > 0) {
_xmlWriter.writeBinary(stax2base64v, tmp, 0, offset);
}
return total;
}

private byte[] toFullBuffer(byte[] data, int offset, int len)
Expand Down Expand Up @@ -1104,6 +1131,22 @@ private byte[] toFullBuffer(InputStream data, final int len) throws IOException
return result;
}

// Variant for "unknown length": read until end-of-stream
private byte[] toFullBuffer(InputStream data) throws IOException
{
final ByteArrayBuilder bb = new ByteArrayBuilder(_ioContext.bufferRecycler());
final byte[] tmp = _ioContext.allocBase64Buffer();
try {
int count;
while ((count = data.read(tmp)) >= 0) {
bb.write(tmp, 0, count);
}
} finally {
_ioContext.releaseBase64Buffer(tmp);
}
return bb.getClearAndRelease();
}

/*
/**********************************************************
/* Output method implementations, primitive
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package com.fasterxml.jackson.dataformat.xml.stream;

import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;

import javax.xml.namespace.QName;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.core.Base64Variant;
import com.fasterxml.jackson.core.Base64Variants;

import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.XmlTestUtil;
import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;

// [dataformat-xml#894]: `JsonGenerator.writeBinary(InputStream, dataLength)` documents a
// negative `dataLength` as "length unknown, read to end" (the JSON backend honors it);
// verify the XML backend streams to end instead of failing with a raw runtime exception.
public class BinaryUnknownLengthWriteTest extends XmlTestUtil
{
// Stream that hands out a single byte per read() call
static class ShortReadInputStream extends FilterInputStream
{
ShortReadInputStream(InputStream in) { super(in); }

@Override
public int read(byte[] b, int off, int len) throws IOException {
return super.read(b, off, Math.min(len, 1));
}
}

private final XmlMapper MAPPER = newMapper();

private final byte[] DATA = utf8Bytes("hello, binary world");
// base64 of DATA
private final String ENCODED = "aGVsbG8sIGJpbmFyeSB3b3JsZA==";

@Test
public void testElementUnknownLength() throws Exception
{
StringWriter out = new StringWriter();
try (ToXmlGenerator gen = (ToXmlGenerator) MAPPER.createGenerator(out)) {
gen.setNextName(new QName("root"));
gen.writeStartObject();
gen.writeFieldName("bin");
assertEquals(DATA.length,
gen.writeBinary(Base64Variants.MIME, new ByteArrayInputStream(DATA), -1));
gen.writeEndObject();
}
assertEquals("<root><bin>" + ENCODED + "</bin></root>", removeSjsxpNamespace(out.toString()));
}

@Test
public void testAttributeUnknownLength() throws Exception
{
StringWriter out = new StringWriter();
try (ToXmlGenerator gen = (ToXmlGenerator) MAPPER.createGenerator(out)) {
gen.setNextName(new QName("root"));
gen.writeStartObject();
gen.setNextIsAttribute(true);
gen.writeFieldName("bin");
assertEquals(DATA.length,
gen.writeBinary(Base64Variants.MIME, new ByteArrayInputStream(DATA), -1));
gen.writeEndObject();
}
assertEquals("<root bin=\"" + ENCODED + "\"/>", removeSjsxpNamespace(out.toString()));
}

@Test
public void testUnwrappedUnknownLength() throws Exception
{
StringWriter out = new StringWriter();
try (ToXmlGenerator gen = (ToXmlGenerator) MAPPER.createGenerator(out)) {
gen.setNextName(new QName("root"));
gen.writeStartObject();
gen.writeFieldName("bin");
gen.setNextIsUnwrapped(true);
assertEquals(DATA.length,
gen.writeBinary(Base64Variants.MIME, new ByteArrayInputStream(DATA), -1));
gen.writeEndObject();
}
assertEquals("<root>" + ENCODED + "</root>", removeSjsxpNamespace(out.toString()));
}

@Test
public void testPrettyPrintedUnknownLength() throws Exception
{
StringWriter out = new StringWriter();
try (ToXmlGenerator gen = (ToXmlGenerator) MAPPER.writerWithDefaultPrettyPrinter()
.createGenerator(out)) {
gen.setNextName(new QName("root"));
gen.writeStartObject();
gen.writeFieldName("bin");
assertEquals(DATA.length,
gen.writeBinary(Base64Variants.MIME, new ByteArrayInputStream(DATA), -1));
gen.writeEndObject();
}
assertEquals("<root>\n <bin>" + ENCODED + "</bin>\n</root>\n",
removeSjsxpNamespace(out.toString()));
}

// Empty stream should render the same whether length is given as 0 or unknown
@Test
public void testEmptyStream() throws Exception
{
final byte[] empty = new byte[0];
assertEquals("<root><bin/></root>", _writeElement(Base64Variants.MIME, empty, 0));
assertEquals("<root><bin/></root>", _writeElement(Base64Variants.MIME, empty, -1));
assertEquals("<root bin=\"\"/>", _writeAttribute(Base64Variants.MIME, empty, 0));
assertEquals("<root bin=\"\"/>", _writeAttribute(Base64Variants.MIME, empty, -1));
}

// Payload bigger than the recycled read buffer, and not a multiple of 3, so that
// partial triplets have to be carried over between reads
@Test
public void testLargePayloadUnknownLength() throws Exception
{
final byte[] big = new byte[7001];
for (int i = 0; i < big.length; i++) {
big[i] = (byte) i;
}
// Use variant without line feeds: the Stax2 writer starts a new line counter
// for each chunk written, so with MIME only the line break positions would differ
final Base64Variant b64v = Base64Variants.MIME_NO_LINEFEEDS;
final String expected = _writeElement(b64v, big);
final String actual = _writeElement(b64v, big, -1);
assertEquals(expected, actual);
assertEquals(expected, _writeElement(b64v, big, big.length));
assertEquals(_writeAttribute(b64v, big, big.length), _writeAttribute(b64v, big, -1));

// and finally, make sure it decodes back
String encoded = actual.substring("<root><bin>".length(), actual.length() - "</bin></root>".length());
assertArrayEquals(big, b64v.decode(encoded));
}

@Test
public void testShortReadsUnknownLength() throws Exception
{
StringWriter out = new StringWriter();
try (ToXmlGenerator gen = (ToXmlGenerator) MAPPER.createGenerator(out)) {
gen.setNextName(new QName("root"));
gen.writeStartObject();
gen.writeFieldName("bin");
assertEquals(DATA.length, gen.writeBinary(Base64Variants.MIME,
new ShortReadInputStream(new ByteArrayInputStream(DATA)), -1));
gen.writeEndObject();
}
assertEquals("<root><bin>" + ENCODED + "</bin></root>", removeSjsxpNamespace(out.toString()));
}

/*
/**********************************************************************
/* Helper methods
/**********************************************************************
*/

private String _writeElement(Base64Variant b64v, byte[] data) throws Exception
{
StringWriter out = new StringWriter();
try (ToXmlGenerator gen = (ToXmlGenerator) MAPPER.createGenerator(out)) {
gen.setNextName(new QName("root"));
gen.writeStartObject();
gen.writeFieldName("bin");
gen.writeBinary(b64v, data, 0, data.length);
gen.writeEndObject();
}
return removeSjsxpNamespace(out.toString());
}

private String _writeElement(Base64Variant b64v, byte[] data, int dataLength) throws Exception
{
StringWriter out = new StringWriter();
try (ToXmlGenerator gen = (ToXmlGenerator) MAPPER.createGenerator(out)) {
gen.setNextName(new QName("root"));
gen.writeStartObject();
gen.writeFieldName("bin");
assertEquals(data.length,
gen.writeBinary(b64v, new ByteArrayInputStream(data), dataLength));
gen.writeEndObject();
}
return removeSjsxpNamespace(out.toString());
}

private String _writeAttribute(Base64Variant b64v, byte[] data, int dataLength) throws Exception
{
StringWriter out = new StringWriter();
try (ToXmlGenerator gen = (ToXmlGenerator) MAPPER.createGenerator(out)) {
gen.setNextName(new QName("root"));
gen.writeStartObject();
gen.setNextIsAttribute(true);
gen.writeFieldName("bin");
assertEquals(data.length,
gen.writeBinary(b64v, new ByteArrayInputStream(data), dataLength));
gen.writeEndObject();
}
return removeSjsxpNamespace(out.toString());
}
}