Skip to content

Commit 22aefc9

Browse files
gregwlachlan-robertslorban
authored
Refine offset, length size handling in Content.Sources (#13690)
Fix #14685 by handling zero length resources * Propose the checkOffsetLengthSize contract * More forgiving offsetLengthSize contract * Update jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java Co-authored-by: Ludovic Orban <[email protected]> --------- Signed-off-by: Lachlan Roberts <[email protected]> Signed-off-by: Ludovic Orban <[email protected]> Co-authored-by: Lachlan Roberts <[email protected]> Co-authored-by: Ludovic Orban <[email protected]>
1 parent aeb7c61 commit 22aefc9

File tree

19 files changed

+938
-135
lines changed

19 files changed

+938
-135
lines changed

jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/MultiPart.java

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,9 @@ public final Content.Source createContentSource()
354354
@Override
355355
public Content.Source newContentSource(ByteBufferPool.Sized bufferPool, long offset, long length)
356356
{
357-
return newContentSource();
357+
// We call the deprecated newContentSource() to support existing subclasses.
358+
// All current implementations of Part do override newContentSource(ByteBufferPool.Sized, long, long).
359+
return Content.Source.from(newContentSource(), offset, length);
358360
}
359361

360362
public long getLength()
@@ -499,10 +501,19 @@ public ByteBufferPart(String name, String fileName, HttpFields fields, List<Byte
499501
this.content = content;
500502
}
501503

504+
@Override
505+
public long getLength()
506+
{
507+
long length = 0;
508+
for (ByteBuffer b : content)
509+
length += BufferUtil.length(b);
510+
return length;
511+
}
512+
502513
@Override
503514
public Content.Source newContentSource(ByteBufferPool.Sized bufferPool, long offset, long length)
504515
{
505-
return new ByteBufferContentSource(content);
516+
return new ByteBufferContentSource(content, offset, length);
506517
}
507518

508519
@Override
@@ -535,9 +546,21 @@ public ChunksPart(String name, String fileName, HttpFields fields, List<Content.
535546
content.forEach(Content.Chunk::retain);
536547
}
537548

549+
@Override
550+
public long getLength()
551+
{
552+
long length = 0;
553+
for (Content.Chunk c : content)
554+
length += c.size();
555+
return length;
556+
}
557+
538558
@Override
539559
public Content.Source newContentSource(ByteBufferPool.Sized bufferPool, long offset, long length)
540560
{
561+
long size = getLength();
562+
length = TypeUtil.checkOffsetLengthSize(offset, length, size);
563+
541564
try (AutoLock ignored = lock.lock())
542565
{
543566
if (closed)
@@ -556,7 +579,7 @@ public Content.Source newContentSource(ByteBufferPool.Sized bufferPool, long off
556579
ChunksContentSource newContentSource = new ChunksContentSource(chunks);
557580
chunks.forEach(Content.Chunk::release);
558581
contentSources.add(newContentSource);
559-
return newContentSource;
582+
return Content.Source.from(newContentSource, offset, length);
560583
}
561584
}
562585

@@ -650,12 +673,19 @@ public ContentSourcePart(String name, String fileName, HttpFields fields, Conten
650673
this.content = Objects.requireNonNull(content);
651674
}
652675

676+
@Override
677+
public long getLength()
678+
{
679+
return content.getLength();
680+
}
681+
653682
@Override
654683
public Content.Source newContentSource(ByteBufferPool.Sized bufferPool, long offset, long length)
655684
{
685+
length = TypeUtil.checkOffsetLengthSize(offset, length, content.getLength());
656686
Content.Source c = content;
657687
content = null;
658-
return c;
688+
return Content.Source.from(c, offset, length);
659689
}
660690

661691
@Override

jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/content/CachingHttpContentFactory.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.concurrent.ConcurrentMap;
2323
import java.util.concurrent.atomic.AtomicBoolean;
2424
import java.util.concurrent.atomic.AtomicLong;
25+
import java.util.concurrent.atomic.AtomicReference;
2526

2627
import org.eclipse.jetty.http.CompressedContentFormat;
2728
import org.eclipse.jetty.http.HttpField;
@@ -36,6 +37,7 @@
3637
import org.eclipse.jetty.util.Callback;
3738
import org.eclipse.jetty.util.NanoTime;
3839
import org.eclipse.jetty.util.StringUtil;
40+
import org.eclipse.jetty.util.TypeUtil;
3941
import org.eclipse.jetty.util.resource.Resource;
4042
import org.slf4j.Logger;
4143
import org.slf4j.LoggerFactory;
@@ -300,7 +302,7 @@ protected interface CachingHttpContent extends HttpContent
300302

301303
protected class CachedHttpContent extends HttpContent.Wrapper implements CachingHttpContent
302304
{
303-
private final RetainableByteBuffer _buffer;
305+
private final AtomicReference<RetainableByteBuffer> _buffer = new AtomicReference<>();
304306
private final String _cacheKey;
305307
private final HttpField _etagField;
306308
private volatile long _lastAccessed;
@@ -333,7 +335,7 @@ public CachedHttpContent(String key, HttpContent httpContent)
333335
throw new IllegalArgumentException("Resource is too large: length " + contentLengthValue + " > " + _maxCachedFileSize);
334336

335337
// Read the content into memory
336-
_buffer = IOResources.toRetainableByteBuffer(httpContent.getResource(), _bufferPool);
338+
_buffer.set(IOResources.toRetainableByteBuffer(httpContent.getResource(), _bufferPool));
337339

338340
_characterEncoding = httpContent.getCharacterEncoding();
339341
_compressedFormats = httpContent.getPreCompressedContentFormats();
@@ -364,12 +366,20 @@ public String getKey()
364366
@Override
365367
public void writeTo(Content.Sink sink, long offset, long length, Callback callback)
366368
{
369+
RetainableByteBuffer buffer = _buffer.get();
370+
if (buffer == null)
371+
{
372+
super.writeTo(sink, offset, length, callback);
373+
return;
374+
}
375+
367376
boolean retained = false;
368377
try
369378
{
379+
length = TypeUtil.checkOffsetLengthSize(offset, length, buffer.remaining());
370380
retained = tryRetain();
371381
if (retained)
372-
sink.write(true, BufferUtil.slice(_buffer.getByteBuffer(), Math.toIntExact(offset), Math.toIntExact(length)), Callback.from(this::release, callback));
382+
sink.write(true, BufferUtil.slice(buffer.getByteBuffer(), Math.toIntExact(offset), Math.toIntExact(length)), Callback.from(this::release, callback));
373383
else
374384
getWrapped().writeTo(sink, offset, length, callback);
375385
}
@@ -392,15 +402,18 @@ private boolean tryRetain()
392402
{
393403
return _cache.computeIfPresent(_cacheKey, (s, cachingHttpContent) ->
394404
{
395-
_buffer.retain();
405+
RetainableByteBuffer buffer = _buffer.get();
406+
if (buffer == null)
407+
return null;
408+
buffer.retain();
396409
return cachingHttpContent;
397410
}) != null;
398411
}
399412

400413
@Override
401414
public void release()
402415
{
403-
_buffer.release();
416+
_buffer.getAndUpdate(buffer -> (buffer != null && buffer.release()) ? null : buffer);
404417
}
405418

406419
@Override
@@ -436,7 +449,7 @@ public HttpField getContentLength()
436449
@Override
437450
public long getContentLengthValue()
438451
{
439-
return _buffer.remaining();
452+
return _contentLength.getLongValue();
440453
}
441454

442455
@Override

jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/content/FileMappingHttpContentFactory.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.eclipse.jetty.util.BufferUtil;
2525
import org.eclipse.jetty.util.Callback;
2626
import org.eclipse.jetty.util.IteratingNestedCallback;
27+
import org.eclipse.jetty.util.TypeUtil;
2728
import org.slf4j.Logger;
2829
import org.slf4j.LoggerFactory;
2930

@@ -110,6 +111,7 @@ public void writeTo(Content.Sink sink, long offset, long length, Callback callba
110111
{
111112
try
112113
{
114+
length = TypeUtil.checkOffsetLengthSize(offset, length, _buffer.remaining());
113115
sink.write(true, BufferUtil.slice(_buffer, Math.toIntExact(offset), Math.toIntExact(length)), callback);
114116
}
115117
catch (Throwable x)
@@ -175,10 +177,7 @@ public void writeTo(Content.Sink sink, long offset, long length, Callback callba
175177
{
176178
try
177179
{
178-
if (offset > getContentLengthValue())
179-
throw new IllegalArgumentException("Offset outside of mapped file range");
180-
if (length > -1 && length + offset > getContentLengthValue())
181-
throw new IllegalArgumentException("Offset / length outside of mapped file range");
180+
length = TypeUtil.checkOffsetLengthSize(offset, length, _contentLengthValue);
182181

183182
int beginIndex = Math.toIntExact(offset / maxBufferSize);
184183
int firstOffset = Math.toIntExact(offset % maxBufferSize);

jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/content/HttpContent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ public interface HttpContent
119119
* @param sink the sink to write to.
120120
* @param offset the offset byte of the resource to start from.
121121
* @param length the length of the resource's contents to copy, -1 for the full length.
122+
* If the length is longer than the available content, the write is truncated to the available length.
122123
* @param callback the callback to notify when writing is done.
123124
*/
124125
void writeTo(Content.Sink sink, long offset, long length, Callback callback);

jetty-core/jetty-http/src/test/java/org/eclipse/jetty/http/content/FileMappingHttpContentFactoryTest.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,14 @@ public void testMultiBufferFileMappedOffsetAndLength() throws Exception
9090

9191
HttpContent content = fileMappingHttpContentFactory.getContent("file.txt");
9292

93-
assertThrows(IllegalArgumentException.class, () -> writeToString(content, 0, 31));
94-
assertThrows(IllegalArgumentException.class, () -> writeToString(content, 30, 1));
95-
assertThrows(IllegalArgumentException.class, () -> writeToString(content, 31, 0));
93+
assertThrows(IllegalArgumentException.class, () -> writeToString(content, -1, 0));
94+
assertThrows(IndexOutOfBoundsException.class, () -> writeToString(content, 31, 1));
9695

96+
assertThat(writeToString(content, 0, 100), is("0123456789abcdefghijABCDEFGHIJ"));
97+
assertThat(writeToString(content, 0, 31), is("0123456789abcdefghijABCDEFGHIJ"));
9798
assertThat(writeToString(content, 0, 30), is("0123456789abcdefghijABCDEFGHIJ"));
9899
assertThat(writeToString(content, 29, 1), is("J"));
100+
assertThat(writeToString(content, 30, 1), is(""));
99101
assertThat(writeToString(content, 0, 0), is(""));
100102
assertThat(writeToString(content, 10, 0), is(""));
101103
assertThat(writeToString(content, 15, 0), is(""));

jetty-core/jetty-io/src/main/java/org/eclipse/jetty/io/Content.java

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.eclipse.jetty.io.internal.ContentCopier;
4343
import org.eclipse.jetty.io.internal.ContentSourceByteBuffer;
4444
import org.eclipse.jetty.io.internal.ContentSourceConsumer;
45+
import org.eclipse.jetty.io.internal.ContentSourceRange;
4546
import org.eclipse.jetty.io.internal.ContentSourceRetainableByteBuffer;
4647
import org.eclipse.jetty.io.internal.ContentSourceString;
4748
import org.eclipse.jetty.util.Blocker;
@@ -169,9 +170,13 @@ interface Factory
169170
* Creates a new {@link Content.Source}.
170171
*
171172
* @param bufferPool the {@link ByteBufferPool.Sized} to get buffers from. {@code null} means allocate new buffers as needed.
172-
* @param offset the offset byte of the resource to start from.
173+
* @param offset the offset byte of the content to start from.
174+
* Must be greater than or equal to 0 and less than the content length (if known).
173175
* @param length the length of the content to make available, -1 for the full length.
176+
* If the size of the content is known, the length may be truncated to the content size minus the offset.
174177
* @return a {@link Content.Source}.
178+
* @throws IndexOutOfBoundsException if the offset or length are out of range.
179+
* @see TypeUtil#checkOffsetLengthSize(long, long, long)
175180
*/
176181
Content.Source newContentSource(ByteBufferPool.Sized bufferPool, long offset, long length);
177182
}
@@ -198,16 +203,42 @@ static Content.Source from(Path path)
198203

199204
/**
200205
* Create a {@code Content.Source} from a {@link Path}.
206+
*
201207
* @param path The {@link Path}s to use as the source.
202-
* @param offset The offset in bytes from which to start the source
203-
* @param length The length in bytes of the source.
204-
* @return A {@code Content.Source}
208+
* @param offset the offset byte of the content to start from.
209+
* Must be greater than or equal to 0 and less than the content length (if known).
210+
* @param length the length of the content to make available, -1 for the full length.
211+
* If the size of the content is known, the length may be truncated to the content size minus the offset.
212+
* @return a {@link Content.Source}.
213+
* @throws IndexOutOfBoundsException if the offset or length are out of range.
214+
* @see TypeUtil#checkOffsetLengthSize(long, long, long)
205215
*/
206216
static Content.Source from(Path path, long offset, long length)
207217
{
208218
return from(null, path, offset, length);
209219
}
210220

221+
/**
222+
* Wrap a {@link Content.Source} to make it appear as a sub-range of the original.
223+
*
224+
* @param source The {@link Content.Source} to wrap.
225+
* @param offset the offset byte of the content to start from.
226+
* Must be greater than or equal to 0 and less than the content length (if known).
227+
* @param length the length of the content to make available, -1 for the full length.
228+
* If the size of the content is known, the length may be truncated to the content size minus the offset.
229+
* @return a {@link Content.Source}.
230+
* @throws IndexOutOfBoundsException if the offset or length are out of range.
231+
* @see TypeUtil#checkOffsetLengthSize(long, long, long)
232+
*/
233+
static Content.Source from(Content.Source source, long offset, long length)
234+
{
235+
// If the offset and length include the full content, then do not wrap.
236+
if (offset == 0 && (length == -1 || length == source.getLength()))
237+
return source;
238+
239+
return new ContentSourceRange(source, offset, length);
240+
}
241+
211242
/**
212243
* Create a {@code Content.Source} from a {@link Path}.
213244
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
@@ -223,9 +254,13 @@ static Content.Source from(ByteBufferPool.Sized byteBufferPool, Path path)
223254
* Create a {@code Content.Source} from a {@link Path}.
224255
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
225256
* @param path The {@link Path}s to use as the source.
226-
* @param offset The offset in bytes from which to start the source
227-
* @param length The length in bytes of the source, -1 for the full length.
228-
* @return A {@code Content.Source}
257+
* @param offset the offset byte of the content to start from.
258+
* Must be greater than or equal to 0 and less than the content length (if known).
259+
* @param length the length of the content to make available, -1 for the full length.
260+
* If the size of the content is known, the length may be truncated to the content size minus the offset.
261+
* @return a {@link Content.Source}.
262+
* @throws IndexOutOfBoundsException if the offset or length are out of range.
263+
* @see TypeUtil#checkOffsetLengthSize(long, long, long)
229264
*/
230265
static Content.Source from(ByteBufferPool.Sized byteBufferPool, Path path, long offset, long length)
231266
{
@@ -247,9 +282,13 @@ static Content.Source from(ByteBufferPool.Sized byteBufferPool, ByteChannel byte
247282
* Create a {@code Content.Source} from a {@link ByteChannel}.
248283
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
249284
* @param seekableByteChannel The {@link ByteChannel}s to use as the source.
250-
* @param offset The offset in bytes from which to start the source
251-
* @param length The length in bytes of the source.
252-
* @return A {@code Content.Source}
285+
* @param offset the offset byte of the content to start from.
286+
* Must be greater than or equal to 0 and less than the content length (if known).
287+
* @param length the length of the content to make available, -1 for the full length.
288+
* If the size of the content is known, the length may be truncated to the content size minus the offset.
289+
* @return a {@link Content.Source}.
290+
* @throws IndexOutOfBoundsException if the offset or length are out of range.
291+
* @see TypeUtil#checkOffsetLengthSize(long, long, long)
253292
*/
254293
static Content.Source from(ByteBufferPool.Sized byteBufferPool, SeekableByteChannel seekableByteChannel, long offset, long length)
255294
{
@@ -276,9 +315,13 @@ static Content.Source from(ByteBufferPool.Sized byteBufferPool, InputStream inpu
276315
* Create a {@code Content.Source} from an {@link InputStream}.
277316
* @param byteBufferPool The {@link org.eclipse.jetty.io.ByteBufferPool.Sized} to use for any internal buffers.
278317
* @param inputStream The {@link InputStream}s to use as the source.
279-
* @param offset The offset in bytes from which to start the source
280-
* @param length The number of bytes to read from the source, or -1 to read to the end of the stream
281-
* @return A {@code Content.Source}
318+
* @param offset the offset byte of the resource to start from.
319+
* Must be greater than or equal to 0 and less than the resource size (if known).
320+
* @param length the length of the content to make available, or -1 for the full length available.
321+
* The length may be truncated if the stream ends sooner.
322+
* @return a {@link Content.Source}.
323+
* @throws IndexOutOfBoundsException if the offset or length are out of range.
324+
* @see TypeUtil#checkOffsetLengthSize(long, long, long)
282325
*/
283326
static Content.Source from(ByteBufferPool.Sized byteBufferPool, InputStream inputStream, long offset, long length)
284327
{
@@ -961,6 +1004,19 @@ static Chunk from(ByteBuffer byteBuffer, boolean last)
9611004
return last ? EOF : EMPTY;
9621005
}
9631006

1007+
/**
1008+
* <p>Creates a Chunk with the given RetainableByteBuffer</p>
1009+
* <p>The returned Chunk is not {@link #retain() retained} and {@link #release() releasing it
1010+
* will release the passed buffer}.</p>
1011+
* @param buffer the RetainableByteBuffer to use to back the returned Chunk
1012+
* @param last whether the Chunk is the last one
1013+
* @return a buffer as a Chunk
1014+
*/
1015+
static Chunk from(RetainableByteBuffer buffer, boolean last)
1016+
{
1017+
return new ByteBufferChunk.WithRetainableByteBuffer(buffer, last);
1018+
}
1019+
9641020
/**
9651021
* <p>Creates a Chunk with the given ByteBuffer.</p>
9661022
* <p>The returned Chunk must be {@link #release() released}.</p>

0 commit comments

Comments
 (0)