using System; using System.Data.Common; using System.Diagnostics; using System.Text; using System.Threading; using System.Threading.Tasks; namespace System.Data.SqlClient { sealed internal class SqlSequentialTextReader : System.IO.TextReader { private SqlDataReader _reader; // The SqlDataReader that we are reading data from private int _columnIndex; // The index of out column in the table private Encoding _encoding; // Encoding for this character stream private Decoder _decoder; // Decoder based on the encoding (NOTE: Decoders are stateful as they are designed to process streams of data) private byte[] _leftOverBytes; // Bytes leftover from the last Read() operation - this can be null if there were no bytes leftover (Possible optimization: re-use the same array?) private int _peekedChar; // The last character that we peeked at (or -1 if we haven't peeked at anything) private Task _currentTask; // The current async task private CancellationTokenSource _disposalTokenSource; // Used to indicate that a cancellation is requested due to disposal internal SqlSequentialTextReader(SqlDataReader reader, int columnIndex, Encoding encoding) { Debug.Assert(reader != null, "Null reader when creating sequential textreader"); Debug.Assert(columnIndex >= 0, "Invalid column index when creating sequential textreader"); Debug.Assert(encoding != null, "Null encoding when creating sequential textreader"); _reader = reader; _columnIndex = columnIndex; _encoding = encoding; _decoder = encoding.GetDecoder(); _leftOverBytes = null; _peekedChar = -1; _currentTask = null; _disposalTokenSource = new CancellationTokenSource(); } internal int ColumnIndex { get { return _columnIndex; } } public override int Peek() { if (_currentTask != null) { throw ADP.AsyncOperationPending(); } if (IsClosed) { throw ADP.ObjectDisposed(this); } if (!HasPeekedChar) { _peekedChar = Read(); } Debug.Assert(_peekedChar == -1 || ((_peekedChar >= char.MinValue) && (_peekedChar <= char.MaxValue)), string.Format("Bad peeked character: {0}", _peekedChar)); return _peekedChar; } public override int Read() { if (_currentTask != null) { throw ADP.AsyncOperationPending(); } if (IsClosed) { throw ADP.ObjectDisposed(this); } int readChar = -1; // If there is already a peeked char, then return it if (HasPeekedChar) { readChar = _peekedChar; _peekedChar = -1; } // If there is data available try to read a char else { char[] tempBuffer = new char[1]; int charsRead = InternalRead(tempBuffer, 0, 1); if (charsRead == 1) { readChar = tempBuffer[0]; } } Debug.Assert(readChar == -1 || ((readChar >= char.MinValue) && (readChar <= char.MaxValue)), string.Format("Bad read character: {0}", readChar)); return readChar; } public override int Read(char[] buffer, int index, int count) { ValidateReadParameters(buffer, index, count); if (IsClosed) { throw ADP.ObjectDisposed(this); } if (_currentTask != null) { throw ADP.AsyncOperationPending(); } int charsRead = 0; int charsNeeded = count; // Load in peeked char if ((charsNeeded > 0) && (HasPeekedChar)) { Debug.Assert((_peekedChar >= char.MinValue) && (_peekedChar <= char.MaxValue), string.Format("Bad peeked character: {0}", _peekedChar)); buffer[index + charsRead] = (char)_peekedChar; charsRead++; charsNeeded--; _peekedChar = -1; } // If we need more data and there is data avaiable, read charsRead += InternalRead(buffer, index + charsRead, charsNeeded); return charsRead; } public override Task ReadAsync(char[] buffer, int index, int count) { ValidateReadParameters(buffer, index, count); TaskCompletionSource completion = new TaskCompletionSource(); if (IsClosed) { completion.SetException(ADP.ExceptionWithStackTrace(ADP.ObjectDisposed(this))); } else { try { Task original = Interlocked.CompareExchange(ref _currentTask, completion.Task, null); if (original != null) { completion.SetException(ADP.ExceptionWithStackTrace(ADP.AsyncOperationPending())); } else { bool completedSynchronously = true; int charsRead = 0; int adjustedIndex = index; int charsNeeded = count; // Load in peeked char if ((HasPeekedChar) && (charsNeeded > 0)) { // Take a copy of _peekedChar in case it is cleared during close int peekedChar = _peekedChar; if (peekedChar >= char.MinValue) { Debug.Assert((_peekedChar >= char.MinValue) && (_peekedChar <= char.MaxValue), string.Format("Bad peeked character: {0}", _peekedChar)); buffer[adjustedIndex] = (char)peekedChar; adjustedIndex++; charsRead++; charsNeeded--; _peekedChar = -1; } } int byteBufferUsed; byte[] byteBuffer = PrepareByteBuffer(charsNeeded, out byteBufferUsed); // Permit a 0 byte read in order to advance the reader to the correct column if ((byteBufferUsed < byteBuffer.Length) || (byteBuffer.Length == 0)) { int bytesRead; var reader = _reader; if (reader != null) { Task getBytesTask = reader.GetBytesAsync(_columnIndex, byteBuffer, byteBufferUsed, byteBuffer.Length - byteBufferUsed, Timeout.Infinite, _disposalTokenSource.Token, out bytesRead); if (getBytesTask == null) { byteBufferUsed += bytesRead; } else { // We need more data - setup the callback, and mark this as not completed [....] completedSynchronously = false; getBytesTask.ContinueWith((t) => { _currentTask = null; // If we completed but the textreader is closed, then report cancellation if ((t.Status == TaskStatus.RanToCompletion) && (!IsClosed)) { try { int bytesReadFromStream = t.Result; byteBufferUsed += bytesReadFromStream; if (byteBufferUsed > 0) { charsRead += DecodeBytesToChars(byteBuffer, byteBufferUsed, buffer, adjustedIndex, charsNeeded); } completion.SetResult(charsRead); } catch (Exception ex) { completion.SetException(ex); } } else if (IsClosed) { completion.SetException(ADP.ExceptionWithStackTrace(ADP.ObjectDisposed(this))); } else if (t.Status == TaskStatus.Faulted) { if (t.Exception.InnerException is SqlException) { // ReadAsync can't throw a SqlException, so wrap it in an IOException completion.SetException(ADP.ExceptionWithStackTrace(ADP.ErrorReadingFromStream(t.Exception.InnerException))); } else { completion.SetException(t.Exception.InnerException); } } else { completion.SetCanceled(); } }, TaskScheduler.Default); } if ((completedSynchronously) && (byteBufferUsed > 0)) { // No more data needed, decode what we have charsRead += DecodeBytesToChars(byteBuffer, byteBufferUsed, buffer, adjustedIndex, charsNeeded); } } else { // Reader is null, close must of happened in the middle of this read completion.SetException(ADP.ExceptionWithStackTrace(ADP.ObjectDisposed(this))); } } if (completedSynchronously) { _currentTask = null; if (IsClosed) { completion.SetCanceled(); } else { completion.SetResult(charsRead); } } } } catch (Exception ex) { // In case of any errors, ensure that the completion is completed and the task is set back to null if we switched it completion.TrySetException(ex); Interlocked.CompareExchange(ref _currentTask, null, completion.Task); throw; } } return completion.Task; } protected override void Dispose(bool disposing) { if (disposing) { // Set the textreader as closed SetClosed(); } base.Dispose(disposing); } /// /// Forces the TextReader to act as if it was closed /// This does not actually close the stream, read off the rest of the data or dispose this /// internal void SetClosed() { _disposalTokenSource.Cancel(); _reader = null; _peekedChar = -1; // Wait for pending task var currentTask = _currentTask; if (currentTask != null) { ((IAsyncResult)currentTask).AsyncWaitHandle.WaitOne(); } } /// /// Performs the actual reading and converting /// NOTE: This assumes that buffer, index and count are all valid, we're not closed (!IsClosed) and that there is data left (IsDataLeft()) /// /// /// /// /// private int InternalRead(char[] buffer, int index, int count) { Debug.Assert(buffer != null, "Null output buffer"); Debug.Assert((index >= 0) && (count >= 0) && (index + count <= buffer.Length), string.Format("Bad count: {0} or index: {1}", count, index)); Debug.Assert(!IsClosed, "Can't read while textreader is closed"); try { int byteBufferUsed; byte[] byteBuffer = PrepareByteBuffer(count, out byteBufferUsed); byteBufferUsed += _reader.GetBytesInternalSequential(_columnIndex, byteBuffer, byteBufferUsed, byteBuffer.Length - byteBufferUsed); if (byteBufferUsed > 0) { return DecodeBytesToChars(byteBuffer, byteBufferUsed, buffer, index, count); } else { // Nothing to read, or nothing read return 0; } } catch (SqlException ex) { // Read can't throw a SqlException - so wrap it in an IOException throw ADP.ErrorReadingFromStream(ex); } } /// /// Creates a byte array large enough to store all bytes for the characters in the current encoding, then fills it with any leftover bytes /// /// Number of characters that are to be read /// Number of bytes pre-filled by the leftover bytes /// A byte array of the correct size, pre-filled with leftover bytes private byte[] PrepareByteBuffer(int numberOfChars, out int byteBufferUsed) { Debug.Assert(numberOfChars >= 0, "Can't prepare a byte buffer for negative characters"); byte[] byteBuffer; if (numberOfChars == 0) { byteBuffer = new byte[0]; byteBufferUsed = 0; } else { int byteBufferSize = _encoding.GetMaxByteCount(numberOfChars); if (_leftOverBytes != null) { // If we have more leftover bytes than we need for this conversion, then just re-use the leftover buffer if (_leftOverBytes.Length > byteBufferSize) { byteBuffer = _leftOverBytes; byteBufferUsed = byteBuffer.Length; } else { // Otherwise, copy over the leftover buffer byteBuffer = new byte[byteBufferSize]; Array.Copy(_leftOverBytes, byteBuffer, _leftOverBytes.Length); byteBufferUsed = _leftOverBytes.Length; } } else { byteBuffer = new byte[byteBufferSize]; byteBufferUsed = 0; } } return byteBuffer; } /// /// Decodes the given bytes into characters, and stores the leftover bytes for later use /// /// Buffer of bytes to decode /// Number of bytes to decode from the inBuffer /// Buffer to write the characters to /// Offset to start writing to outBuffer at /// Maximum number of characters to decode /// The actual number of characters decoded private int DecodeBytesToChars(byte[] inBuffer, int inBufferCount, char[] outBuffer, int outBufferOffset, int outBufferCount) { Debug.Assert(inBuffer != null, "Null input buffer"); Debug.Assert((inBufferCount > 0) && (inBufferCount <= inBuffer.Length), string.Format("Bad inBufferCount: {0}", inBufferCount)); Debug.Assert(outBuffer != null, "Null output buffer"); Debug.Assert((outBufferOffset >= 0) && (outBufferCount > 0) && (outBufferOffset + outBufferCount <= outBuffer.Length), string.Format("Bad outBufferCount: {0} or outBufferOffset: {1}", outBufferCount, outBufferOffset)); int charsRead; int bytesUsed; bool completed; _decoder.Convert(inBuffer, 0, inBufferCount, outBuffer, outBufferOffset, outBufferCount, false, out bytesUsed, out charsRead, out completed); // completed may be false and there is no spare bytes if the Decoder has stored bytes to use later if ((!completed) && (bytesUsed < inBufferCount)) { _leftOverBytes = new byte[inBufferCount - bytesUsed]; Array.Copy(inBuffer, bytesUsed, _leftOverBytes, 0, _leftOverBytes.Length); } else { // If Convert() sets completed to true, then it must have used all of the bytes we gave it Debug.Assert(bytesUsed >= inBufferCount, "Converted completed, but not all bytes were used"); _leftOverBytes = null; } Debug.Assert(((_reader == null) || (_reader.ColumnDataBytesRemaining() > 0) || (!completed) || (_leftOverBytes == null)), "Stream has run out of data and the decoder finished, but there are leftover bytes"); Debug.Assert(charsRead > 0, "Converted no chars. Bad encoding?"); return charsRead; } /// /// True if this TextReader is supposed to be closed /// private bool IsClosed { get { return (_reader == null); } } /// /// True if there is data left to read /// /// private bool IsDataLeft { get { return ((_leftOverBytes != null) || (_reader.ColumnDataBytesRemaining() > 0)); } } /// /// True if there is a peeked character available /// private bool HasPeekedChar { get { return (_peekedChar >= char.MinValue); } } /// /// Checks the the parameters passed into a Read() method are valid /// /// /// /// internal static void ValidateReadParameters(char[] buffer, int index, int count) { if (buffer == null) { throw ADP.ArgumentNull(ADP.ParameterBuffer); } if (index < 0) { throw ADP.ArgumentOutOfRange(ADP.ParameterIndex); } if (count < 0) { throw ADP.ArgumentOutOfRange(ADP.ParameterCount); } try { if (checked(index + count) > buffer.Length) { throw ExceptionBuilder.InvalidOffsetLength(); } } catch (OverflowException) { // If we've overflowed when adding index and count, then they never would have fit into buffer anyway throw ExceptionBuilder.InvalidOffsetLength(); } } } sealed internal class SqlUnicodeEncoding : UnicodeEncoding { private static SqlUnicodeEncoding _singletonEncoding = new SqlUnicodeEncoding(); private SqlUnicodeEncoding() : base(bigEndian: false, byteOrderMark: false, throwOnInvalidBytes: false) {} public override Decoder GetDecoder() { return new SqlUnicodeDecoder(); } public override int GetMaxByteCount(int charCount) { // SQL Server never sends a BOM, so we can assume that its 2 bytes per char return charCount * 2; } public static Encoding SqlUnicodeEncodingInstance { get { return _singletonEncoding; } } sealed private class SqlUnicodeDecoder : Decoder { public override int GetCharCount(byte[] bytes, int index, int count) { // SQL Server never sends a BOM, so we can assume that its 2 bytes per char return count / 2; } public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex) { // This method is required - simply call Convert() int bytesUsed; int charsUsed; bool completed; Convert(bytes, byteIndex, byteCount, chars, charIndex, chars.Length - charIndex, true, out bytesUsed, out charsUsed, out completed); return charsUsed; } public override void Convert(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex, int charCount, bool flush, out int bytesUsed, out int charsUsed, out bool completed) { // Assume 2 bytes per char and no BOM charsUsed = Math.Min(charCount, byteCount / 2); bytesUsed = charsUsed * 2; completed = (bytesUsed == byteCount); // BlockCopy uses offsets\length measured in bytes, not the actual array index Buffer.BlockCopy(bytes, byteIndex, chars, charIndex * 2, bytesUsed); } } } }