@@ -389,19 +389,22 @@ final class ChunkedInputStream : InputStream
389389 private {
390390 InterfaceProxy! InputStream m_in;
391391 ulong m_bytesInCurrentChunk = 0 ;
392+ bool m_chunkHeaderRead = false ;
392393 }
393394
394395 // / private
395396 this (InterfaceProxy! InputStream stream, bool dummy)
396397 {
397398 assert (!! stream);
398399 m_in = stream;
399- readChunk();
400400 }
401401
402- @property bool empty() const { return m_bytesInCurrentChunk == 0 ; }
402+ @property bool empty() { return leastSize == 0 ; }
403403
404- @property ulong leastSize() const { return m_bytesInCurrentChunk; }
404+ @property ulong leastSize() {
405+ if (! m_chunkHeaderRead) readChunk();
406+ return m_bytesInCurrentChunk;
407+ }
405408
406409 @property bool dataAvailableForRead() { return m_bytesInCurrentChunk > 0 && m_in.dataAvailableForRead; }
407410
@@ -417,21 +420,20 @@ final class ChunkedInputStream : InputStream
417420 size_t nbytes = 0 ;
418421
419422 while (dst.length > 0 ) {
420- enforceBadRequest(m_bytesInCurrentChunk > 0 , " Reading past end of chunked HTTP stream." );
423+ enforceBadRequest(leastSize > 0 , " Reading past end of chunked HTTP stream." );
421424
422425 auto sz = cast (size_t )min(m_bytesInCurrentChunk, dst.length);
423426 m_in.read(dst[0 .. sz]);
424427 dst = dst[sz .. $];
425428 m_bytesInCurrentChunk -= sz;
426429 nbytes += sz;
427430
428- // FIXME: this blocks, but shouldn't for IOMode.once/immediat
429431 if ( m_bytesInCurrentChunk == 0 ){
430432 // skip current chunk footer and read next chunk
431433 ubyte [2 ] crlf;
432434 m_in.read(crlf);
433435 enforceBadRequest(crlf[0 ] == ' \r ' && crlf[1 ] == ' \n ' );
434- readChunk() ;
436+ m_chunkHeaderRead = false ;
435437 }
436438
437439 if (mode != IOMode.all) break ;
@@ -444,12 +446,13 @@ final class ChunkedInputStream : InputStream
444446
445447 private void readChunk ()
446448 {
447- assert (m_bytesInCurrentChunk == 0 );
449+ assert (m_bytesInCurrentChunk == 0 && ! m_chunkHeaderRead );
448450 // read chunk header
449451 logTrace(" read next chunk header" );
450452 auto ln = () @trusted { return cast (string )m_in.readLine(); } ();
451453 logTrace(" got chunk header: %s" , ln);
452454 m_bytesInCurrentChunk = parse! ulong (ln, 16u );
455+ m_chunkHeaderRead = true ;
453456
454457 if ( m_bytesInCurrentChunk == 0 ){
455458 // empty chunk denotes the end
@@ -633,6 +636,34 @@ FreeListRef!ChunkedOutputStream createChunkedOutputStreamFL(OS, Allocator)(OS de
633636 return FreeListRef! ChunkedOutputStream(interfaceProxy! OutputStream (destination_stream), allocator, true );
634637}
635638
639+ unittest {
640+ import vibe.stream.memory : createMemoryStream;
641+
642+ ubyte [] bytes = new ubyte [](100 * 100 );
643+ foreach (i, ref bt; bytes) bt = (i + i / 100 ) % 256 ;
644+ ubyte [] buf = new ubyte [](bytes.length);
645+
646+ auto dst = createMemoryStream(new ubyte [4 * 1024 * 1024 ], true , 0 );
647+
648+ // write data with varying buffer and chunk sizes
649+ auto outp = createChunkedOutputStreamFL(dst);
650+ foreach (i; 0 .. 100 ) {
651+ outp.write(bytes[0 .. i * 100 ]);
652+ if (i % 5 == 0 )
653+ outp.flush();
654+ }
655+ outp.finalize();
656+
657+ // ensure decoding yields the same byte sequence
658+ dst.seek(0 );
659+ auto inp = createChunkedInputStreamFL(dst);
660+ foreach (i; 0 .. 100 ) {
661+ inp.read(buf[0 .. i * 100 ]);
662+ assert (buf[0 .. i * 100 ] == bytes[0 .. i * 100 ]);
663+ }
664+ }
665+
666+
636667// / Parses the cookie from a header field, returning the name of the cookie.
637668// / Implements an algorithm equivalent to https://tools.ietf.org/html/rfc6265#section-5.2
638669// / Returns: the cookie name as return value, populates the dst argument or allocates on the GC for the tuple overload.
0 commit comments