Issue #11080 - copy ByteBuffer for non-retainable chunks

Signed-off-by: Lachlan Roberts <lachlan@webtide.com>
This commit is contained in:
Lachlan Roberts 2023-12-19 15:11:43 +11:00
parent e481b45cda
commit 592ccf7da0
2 changed files with 99 additions and 5 deletions

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.http; package org.eclipse.jetty.http;
import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -27,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.io.Content; import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.io.content.AsyncContent; import org.eclipse.jetty.io.content.AsyncContent;
import org.eclipse.jetty.io.content.InputStreamContentSource;
import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
@ -934,6 +936,48 @@ public class MultiPartFormDataTest
assertInstanceOf(IllegalArgumentException.class, cause); assertInstanceOf(IllegalArgumentException.class, cause);
} }
@Test
public void testNonRetainableContent() throws Exception
{
String body = """
--AaB03x\r
content-disposition: form-data; name="field1"\r
\r
Joe Blow\r
--AaB03x\r
content-disposition: form-data; name="stuff"; filename="foo.txt"\r
Content-Type: text/plain\r
\r
aaaabbbbb\r
--AaB03x--\r
""";
Content.Source source = new InputStreamContentSource(new ByteArrayInputStream(body.getBytes(ISO_8859_1)))
{
@Override
public Content.Chunk read()
{
Content.Chunk chunk = super.read();
return new NonRetainableChunk(chunk);
}
};
MultiPartFormData.Parser formData = new MultiPartFormData.Parser("AaB03x");
formData.setFilesDirectory(_tmpDir);
try (MultiPartFormData.Parts parts = formData.parse(source).get(5, TimeUnit.SECONDS))
{
assertThat(parts.size(), is(2));
MultiPart.Part part1 = parts.getFirst("field1");
assertThat(part1, notNullValue());
Content.Source partContent = part1.getContentSource();
assertThat(Content.Source.asString(partContent), is("Joe Blow"));
MultiPart.Part part2 = parts.getFirst("stuff");
assertThat(part2, notNullValue());
partContent = part2.getContentSource();
assertThat(Content.Source.asString(partContent), is("aaaabbbbb"));
}
}
private class TestContent extends AsyncContent private class TestContent extends AsyncContent
{ {
@Override @Override
@ -945,4 +989,43 @@ public class MultiPartFormDataTest
return chunk; return chunk;
} }
} }
private static class NonRetainableChunk implements Content.Chunk
{
private final ByteBuffer _content;
private final boolean _isLast;
private final Throwable _failure;
public NonRetainableChunk(Content.Chunk chunk)
{
_content = BufferUtil.copy(chunk.getByteBuffer());
_isLast = chunk.isLast();
_failure = chunk.getFailure();
chunk.release();
}
@Override
public ByteBuffer getByteBuffer()
{
return _content;
}
@Override
public boolean isLast()
{
return _isLast;
}
@Override
public Throwable getFailure()
{
return _failure;
}
@Override
public void retain()
{
throw new UnsupportedOperationException();
}
}
} }

View File

@ -41,6 +41,8 @@ import org.eclipse.jetty.util.FutureCallback;
import org.eclipse.jetty.util.FuturePromise; import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.Promise; import org.eclipse.jetty.util.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* <p>Namespace class that contains the definitions of a {@link Source content source}, * <p>Namespace class that contains the definitions of a {@link Source content source},
@ -48,6 +50,8 @@ import org.eclipse.jetty.util.Promise;
*/ */
public class Content public class Content
{ {
private static final Logger LOG = LoggerFactory.getLogger(Content.class);
private Content() private Content()
{ {
} }
@ -666,15 +670,22 @@ public class Content
* @param last whether the Chunk is the last one * @param last whether the Chunk is the last one
* @param retainable the Retainable this Chunk links to * @param retainable the Retainable this Chunk links to
* @return a new Chunk * @return a new Chunk
* @throws IllegalArgumentException if the {@code Retainable}
* {@link Retainable#canRetain() cannot be retained}
*/ */
static Chunk asChunk(ByteBuffer byteBuffer, boolean last, Retainable retainable) static Chunk asChunk(ByteBuffer byteBuffer, boolean last, Retainable retainable)
{ {
if (!retainable.canRetain())
throw new IllegalArgumentException("Cannot create chunk from non-retainable " + retainable);
if (byteBuffer.hasRemaining()) if (byteBuffer.hasRemaining())
return new ByteBufferChunk.WithRetainable(byteBuffer, last, Objects.requireNonNull(retainable)); {
if (retainable.canRetain())
{
return new ByteBufferChunk.WithRetainable(byteBuffer, last, Objects.requireNonNull(retainable));
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("Copying buffer because could not retain");
return new ByteBufferChunk.WithReferenceCount(BufferUtil.copy(byteBuffer), last);
}
}
retainable.release(); retainable.release();
return last ? EOF : EMPTY; return last ? EOF : EMPTY;
} }