/* Copyright 2010-2015 MongoDB Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace MongoDB.Bson.IO { /// /// An IByteBuffer that is backed by multiple chunks. /// public sealed class MultiChunkBuffer : IByteBuffer { // private fields private int _capacity; private int _chunkIndex; private List _chunks; private readonly IBsonChunkSource _chunkSource; private bool _disposed; private bool _isReadOnly; private int _length; private List _positions; // constructors /// /// Initializes a new instance of the class. /// /// The chunk pool. /// chunkPool public MultiChunkBuffer(IBsonChunkSource chunkSource) { if (chunkSource == null) { throw new ArgumentNullException("chunkSource"); } _chunks = new List(); _chunkSource = chunkSource; _length = 0; _positions = new List { 0 }; } /// /// Initializes a new instance of the class. /// /// The chunks. /// The length. /// Whether the buffer is read only. /// chunks public MultiChunkBuffer(IEnumerable chunks, int? length = null, bool isReadOnly = false) { if (chunks == null) { throw new ArgumentNullException("chunks"); } var materializedList = new List(chunks); var capacity = 0; var positions = new List { 0 }; foreach (var chunk in materializedList) { capacity += chunk.Bytes.Count; positions.Add(capacity); } if (length.HasValue && (length.Value < 0 || length.Value > capacity)) { throw new ArgumentOutOfRangeException("length"); } _capacity = capacity; _chunks = materializedList; _isReadOnly = isReadOnly; _length = length ?? capacity; _positions = positions; } // public properties /// public int Capacity { get { ThrowIfDisposed(); return _isReadOnly ? _length : _capacity; } } /// /// Gets the chunk source. /// /// /// The chunk source. /// public IBsonChunkSource ChunkSource { get { return _chunkSource; } } /// public bool IsReadOnly { get { ThrowIfDisposed(); return _isReadOnly; } } /// public int Length { get { ThrowIfDisposed(); return _length; } set { ThrowIfDisposed(); if (value < 0 || value > _capacity) { throw new ArgumentOutOfRangeException("value"); } EnsureIsWritable(); _length = value; } } // public methods /// public ArraySegment AccessBackingBytes(int position) { ThrowIfDisposed(); if (position < 0 || position > _length) { throw new ArgumentOutOfRangeException("position"); } var chunkIndex = GetChunkIndex(position); if (chunkIndex < _chunks.Count) { var segment = _chunks[chunkIndex].Bytes; var chunkOffset = position - _positions[chunkIndex]; var chunkRemaining = segment.Count - chunkOffset; return new ArraySegment(segment.Array, segment.Offset + chunkOffset, chunkRemaining); } else { if (_chunks.Count > 0) { var segment = _chunks[chunkIndex - 1].Bytes; return new ArraySegment(segment.Array, segment.Offset + segment.Count, 0); } else { return new ArraySegment(new byte[0], 0, 0); } } } /// public void Clear(int position, int count) { ThrowIfDisposed(); if (position < 0 || position > _length) { throw new ArgumentOutOfRangeException("position"); } if (count < 0 || position + count > _length) { throw new ArgumentOutOfRangeException("count"); } EnsureIsWritable(); var chunkIndex = GetChunkIndex(position); var chunkOffset = position - _positions[chunkIndex]; while (count > 0) { var segment = _chunks[chunkIndex].Bytes; var chunkRemaining = segment.Count - chunkOffset; var partialCount = Math.Min(count, chunkRemaining); Array.Clear(segment.Array, segment.Offset + chunkOffset, partialCount); chunkIndex += 1; chunkOffset = 0; count -= partialCount; } } /// public void Dispose() { if (!_disposed) { _disposed = true; foreach (var chunk in _chunks) { chunk.Dispose(); } _chunks = null; _positions = null; } } /// public void EnsureCapacity(int minimumCapacity) { if (minimumCapacity < 0) { throw new ArgumentOutOfRangeException("minimumCapacity"); } ThrowIfDisposed(); EnsureIsWritable(); if (_capacity < minimumCapacity) { ExpandCapacity(minimumCapacity); } } /// public byte GetByte(int position) { ThrowIfDisposed(); if (position < 0 || position >= _length) { throw new ArgumentOutOfRangeException("position"); } var chunkIndex = GetChunkIndex(position); var chunkOffset = position - _positions[chunkIndex]; var segment = _chunks[chunkIndex].Bytes; return segment.Array[segment.Offset + chunkOffset]; } /// public void GetBytes(int position, byte[] destination, int offset, int count) { ThrowIfDisposed(); if (position < 0 || position > _length) { throw new ArgumentOutOfRangeException("position"); } if (destination == null) { throw new ArgumentNullException("destination"); } if (offset < 0 || offset > destination.Length) { throw new ArgumentOutOfRangeException("offset"); } if (count < 0 || position + count > _length || offset + count > destination.Length) { throw new ArgumentOutOfRangeException("count"); } var chunkIndex = GetChunkIndex(position); var chunkOffset = position - _positions[chunkIndex]; while (count > 0) { var segment = _chunks[chunkIndex].Bytes; var chunkRemaining = segment.Count - chunkOffset; var partialCount = Math.Min(count, chunkRemaining); Buffer.BlockCopy(segment.Array, segment.Offset + chunkOffset, destination, offset, partialCount); chunkIndex += 1; chunkOffset = 0; count -= partialCount; offset += partialCount; } } /// public IByteBuffer GetSlice(int position, int length) { ThrowIfDisposed(); if (position < 0 || position > _length) { throw new ArgumentOutOfRangeException("position"); } if (length < 0 || position + length > _length) { throw new ArgumentOutOfRangeException("length"); } EnsureIsReadOnly(); if (length == 0) { return new ByteArrayBuffer(new byte[0]); } var firstChunkIndex = GetChunkIndex(position); var lastChunkIndex = GetChunkIndex(position + length - 1); IByteBuffer forkedBuffer; if (firstChunkIndex == lastChunkIndex) { var forkedChunk = _chunks[firstChunkIndex].Fork(); forkedBuffer = new SingleChunkBuffer(forkedChunk, forkedChunk.Bytes.Count, isReadOnly: true); } else { var forkedChunks = _chunks.Skip(firstChunkIndex).Take(lastChunkIndex - firstChunkIndex + 1).Select(c => c.Fork()); var forkedBufferLength = _positions[lastChunkIndex + 1] - _positions[firstChunkIndex]; forkedBuffer = new MultiChunkBuffer(forkedChunks, forkedBufferLength, isReadOnly: true); } var offset = position - _positions[firstChunkIndex]; return new ByteBufferSlice(forkedBuffer, offset, length); } /// public void MakeReadOnly() { ThrowIfDisposed(); _isReadOnly = true; } /// public void SetByte(int position, byte value) { ThrowIfDisposed(); if (position < 0 || position >= _length) { throw new ArgumentOutOfRangeException("position"); } EnsureIsWritable(); var chunkIndex = GetChunkIndex(position); var chunkOffset = position - _positions[chunkIndex]; var segment = _chunks[chunkIndex].Bytes; segment.Array[segment.Offset + chunkOffset] = value; } /// public void SetBytes(int position, byte[] source, int offset, int count) { ThrowIfDisposed(); if (position < 0 || position > _length) { throw new ArgumentOutOfRangeException("position"); } if (source == null) { throw new ArgumentNullException("source"); } if (offset < 0 || offset > source.Length) { throw new ArgumentOutOfRangeException("offset"); } if (count < 0 || position + count > _length || offset + count > source.Length) { throw new ArgumentOutOfRangeException("count"); } EnsureIsWritable(); var chunkIndex = GetChunkIndex(position); var chunkOffset = position - _positions[chunkIndex]; while (count > 0) { var segment = _chunks[chunkIndex].Bytes; var chunkRemaining = segment.Count - chunkOffset; var partialCount = Math.Min(count, chunkRemaining); Buffer.BlockCopy(source, offset, segment.Array, segment.Offset + chunkOffset, partialCount); chunkIndex += 1; chunkOffset = 0; offset += partialCount; count -= partialCount; } } // private methods private void EnsureIsReadOnly() { if (!_isReadOnly) { throw new InvalidOperationException("MultiChunkBuffer is not read only."); } } private void EnsureIsWritable() { if (_isReadOnly) { throw new InvalidOperationException("MultiChunkBuffer is not writable."); } } private void ExpandCapacity(int minimumCapacity) { if (_chunkSource == null) { throw new InvalidOperationException("Capacity cannot be expanded because this buffer was created without specifying a chunk source."); } while (_capacity < minimumCapacity) { var chunk = _chunkSource.GetChunk(minimumCapacity); _chunks.Add(chunk); var newCapacity = (long)_capacity + (long)chunk.Bytes.Count; if (newCapacity > int.MaxValue) { throw new InvalidOperationException("Capacity is limited to 2GB."); } _capacity = (int)newCapacity; _positions.Add(_capacity); } } private int GetChunkIndex(int position) { // locality of reference means this loop will only execute once most of the time while (true) { if (_chunkIndex + 1 < _positions.Count && position >= _positions[_chunkIndex + 1]) { _chunkIndex++; } else if (position < _positions[_chunkIndex]) { _chunkIndex--; } else { return _chunkIndex; } } } private void ThrowIfDisposed() { if (_disposed) { throw new ObjectDisposedException(GetType().Name); } } } }