/**
  * Read the list of ranges from the file.
  * @param file the file to read
  * @param base the base of the stripe
  * @param ranges the disk ranges within the stripe to read
  * @return the bytes read for each disk range, which is the same length as
  *    ranges
  * @
  */
 static DiskRangeList readDiskRanges(Stream file,
                                long @base,
                                DiskRangeList range,
                                bool doForceDirect)
 {
     if (range == null) return null;
     DiskRangeList prev = range.prev;
     if (prev == null)
     {
         prev = new DiskRangeList.MutateHelper(range);
     }
     while (range != null)
     {
         if (range.hasData())
         {
             range = range.next;
             continue;
         }
         int len = (int)(range.getEnd() - range.getOffset());
         long off = range.getOffset();
         if (doForceDirect)
         {
             file.Seek(@base + off, SeekOrigin.Current);
             ByteBuffer directBuf = ByteBuffer.allocateDirect(len);
             readDirect(file, len, directBuf);
             range = range.replaceSelfWith(new RecordReaderImpl.BufferChunk(directBuf, range.getOffset()));
         }
         else
         {
             byte[] buffer = new byte[len];
             file.readFully((@base + off), buffer, 0, buffer.Length);
             range = range.replaceSelfWith(new RecordReaderImpl.BufferChunk(ByteBuffer.wrap(buffer), range.getOffset()));
         }
         range = range.next;
     }
     return prev.next;
 }
 internal static List<DiskRange> getStreamBuffers(DiskRangeList range, long offset, long length)
 {
     // This assumes sorted ranges (as do many other parts of ORC code.
     List<DiskRange> buffers = new List<DiskRange>();
     if (length == 0) return buffers;
     long streamEnd = offset + length;
     bool inRange = false;
     while (range != null)
     {
         if (!inRange)
         {
             if (range.getEnd() <= offset)
             {
                 range = range.next;
                 continue; // Skip until we are in range.
             }
             inRange = true;
             if (range.getOffset() < offset)
             {
                 // Partial first buffer, add a slice of it.
                 buffers.Add(range.sliceAndShift(offset, Math.Min(streamEnd, range.getEnd()), -offset));
                 if (range.getEnd() >= streamEnd) break; // Partial first buffer is also partial last buffer.
                 range = range.next;
                 continue;
             }
         }
         else if (range.getOffset() >= streamEnd)
         {
             break;
         }
         if (range.getEnd() > streamEnd)
         {
             // Partial last buffer (may also be the first buffer), add a slice of it.
             buffers.Add(range.sliceAndShift(range.getOffset(), streamEnd, -offset));
             break;
         }
         // Buffer that belongs entirely to one stream.
         // TODO: ideally we would want to reuse the object and remove it from the list, but we cannot
         //       because bufferChunks is also used by clearStreams for zcr. Create a useless dup.
         buffers.Add(range.sliceAndShift(range.getOffset(), range.getEnd(), -offset));
         if (range.getEnd() == streamEnd) break;
         range = range.next;
     }
     return buffers;
 }