//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // // [....] // [....] //------------------------------------------------------------------------------ namespace System.Data.SqlClient { using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Data.Common; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Xml; using System.Reflection; using System.Runtime.CompilerServices; sealed internal class SqlStream : Stream { private SqlDataReader _reader; // reader we will stream off private int _columnOrdinal; private long _bytesCol; int _bom; private byte[] _bufferedData; private bool _processAllRows; private bool _advanceReader; private bool _readFirstRow = false; private bool _endOfColumn = false; internal SqlStream(SqlDataReader reader, bool addByteOrderMark, bool processAllRows) : this(0, reader, addByteOrderMark, processAllRows, true) { } internal SqlStream(int columnOrdinal, SqlDataReader reader, bool addByteOrderMark , bool processAllRows, bool advanceReader) { _columnOrdinal = columnOrdinal; _reader = reader; _bom = addByteOrderMark ? 0xfeff : 0; _processAllRows = processAllRows; _advanceReader = advanceReader; } override public bool CanRead { get { return true; } } override public bool CanSeek { get { return false; } } override public bool CanWrite { get { return false; } } override public long Length { get { throw ADP.NotSupported(); } } override public long Position { get { throw ADP.NotSupported(); } set { throw ADP.NotSupported(); } } override protected void Dispose(bool disposing) { try { if (disposing && _advanceReader && _reader != null && !_reader.IsClosed) { _reader.Close(); } _reader = null; } finally { base.Dispose(disposing); } } override public void Flush() { throw ADP.NotSupported(); } override public int Read(byte[] buffer, int offset, int count) { int intCount = 0; int cBufferedData = 0; if ((null == _reader)) { throw ADP.StreamClosed(ADP.Read); } if (null == buffer) { throw ADP.ArgumentNull(ADP.ParameterBuffer); } if ((offset < 0) || (count < 0)) { throw ADP.ArgumentOutOfRange(String.Empty, (offset < 0 ? ADP.ParameterOffset : ADP.ParameterCount)); } if (buffer.Length - offset < count) { throw ADP.ArgumentOutOfRange(ADP.ParameterCount); } // Need to find out if we should add byte order mark or not. // We need to add this if we are getting ntext xml, not if we are getting binary xml // Binary Xml always begins with the bytes 0xDF and 0xFF // If we aren't getting these, then we are getting unicode xml if (_bom > 0 ) { // Read and buffer the first two bytes _bufferedData = new byte[2]; cBufferedData = ReadBytes(_bufferedData, 0, 2); // Check to se if we should add the byte order mark if ((cBufferedData < 2) || ((_bufferedData[0] == 0xDF) && (_bufferedData[1] == 0xFF))){ _bom = 0; } while (count > 0) { if (_bom > 0) { buffer[offset] = (byte)_bom; _bom >>= 8; offset++; count--; intCount++; } else { break; } } } if (cBufferedData > 0) { while (count > 0) { buffer[offset++] = _bufferedData[0]; intCount++; count--; if ((cBufferedData > 1) && (count > 0)) { buffer[offset++] = _bufferedData[1]; intCount++; count--; break; } } _bufferedData = null; } intCount += ReadBytes(buffer, offset, count); return intCount; } private static bool AdvanceToNextRow(SqlDataReader reader) { Debug.Assert(reader != null && !reader.IsClosed); // this method skips empty result sets do { if (reader.Read()) { return true; } } while (reader.NextResult()); // no more rows return false; } private int ReadBytes(byte[] buffer, int offset, int count) { bool gotData = true; int intCount = 0; int cb = 0; if (_reader.IsClosed || _endOfColumn) { return 0; } try { while (count > 0) { // if I haven't read any bytes, get the next row if (_advanceReader && (0 == _bytesCol)) { gotData = false; if (_readFirstRow && !_processAllRows) { // for XML column, stop processing after the first row // no op here - reader is closed after the end of this loop } else if (AdvanceToNextRow(_reader)) { _readFirstRow = true; if (_reader.IsDBNull(_columnOrdinal)) { // VSTFDEVDIV 479659: handle row with DBNULL as empty data // for XML column, processing is stopped on the next loop since _readFirstRow is true continue; } // the value is not null, read it gotData = true; } // else AdvanceToNextRow has returned false - no more rows or result sets remained, stop processing } if (gotData) { cb = (int) _reader.GetBytesInternal(_columnOrdinal, _bytesCol, buffer, offset, count); if (cb < count) { _bytesCol = 0; gotData = false; if (!_advanceReader) { _endOfColumn = true; } } else { Debug.Assert(cb == count); _bytesCol += cb; } // we are guaranteed that cb is < Int32.Max since we always pass in count which is of type Int32 to // our getbytes interface count -= (int)cb; offset += (int)cb; intCount += (int)cb; } else { break; // no more data available, we are done } } if (!gotData && _advanceReader) { _reader.Close(); // Need to close the reader if we are done reading } } catch (Exception e) { if (_advanceReader && ADP.IsCatchableExceptionType(e)) { _reader.Close(); } throw; } return intCount; } internal XmlReader ToXmlReader() { // Dev11 Bug #315513: Exception type breaking change from 4.0 RTM when calling GetChars on null xml // We need to wrap all exceptions inside a TargetInvocationException to simulate calling CreateSqlReader via MethodInfo.Invoke return SqlTypes.SqlXml.CreateSqlXmlReader(this, closeInput: true, throwTargetInvocationExceptions: true); } override public long Seek(long offset, SeekOrigin origin) { throw ADP.NotSupported(); } override public void SetLength(long value) { throw ADP.NotSupported(); } override public void Write(byte[] buffer, int offset, int count) { throw ADP.NotSupported(); } } // XmlTextReader does not read all the bytes off the network buffers, so we have to cache it here in the random access // case. This causes double buffering and is a perf hit, but this is not the high perf way for accessing this type of data. // In the case of sequential access, we do not have to do any buffering since the XmlTextReader we return can become // invalid as soon as we move off the current column. sealed internal class SqlCachedStream : Stream { int _currentPosition; // Position within the current array byte int _currentArrayIndex; // Index into the _cachedBytes ArrayList List _cachedBytes; long _totalLength; // Reads off from the network buffer and caches bytes. Only reads one column value in the current row. internal SqlCachedStream(SqlCachedBuffer sqlBuf ) { _cachedBytes = sqlBuf.CachedBytes; } override public bool CanRead { get { return true; } } override public bool CanSeek { get { return true; } } override public bool CanWrite { get { return false; } } override public long Length { get { return TotalLength; } } override public long Position { get { long pos = 0; if (_currentArrayIndex > 0) { for (int ii = 0 ; ii < _currentArrayIndex ; ii++) { pos += _cachedBytes[ii].Length; } } pos += _currentPosition; return pos; } set { if (null == _cachedBytes) { throw ADP.StreamClosed(ADP.ParameterSetPosition); } SetInternalPosition(value, ADP.ParameterSetPosition); } } override protected void Dispose(bool disposing) { try { if (disposing && _cachedBytes != null) _cachedBytes.Clear(); _cachedBytes = null; _currentPosition = 0; _currentArrayIndex = 0; _totalLength = 0; } finally { base.Dispose(disposing); } } override public void Flush() { throw ADP.NotSupported(); } override public int Read(byte[] buffer, int offset, int count) { int cb; int intCount = 0; if (null == _cachedBytes) { throw ADP.StreamClosed(ADP.Read); } if (null == buffer) { throw ADP.ArgumentNull(ADP.ParameterBuffer); } if ((offset < 0) || (count < 0)) { throw ADP.ArgumentOutOfRange(String.Empty, (offset < 0 ? ADP.ParameterOffset : ADP.ParameterCount)); } if (buffer.Length - offset < count) { throw ADP.ArgumentOutOfRange(ADP.ParameterCount); } if (_cachedBytes.Count <= _currentArrayIndex) { return 0; // Everything is read! } while (count > 0) { if (_cachedBytes[_currentArrayIndex].Length <= _currentPosition) { _currentArrayIndex++; // We are done reading this chunk, go to next if (_cachedBytes.Count > _currentArrayIndex) { _currentPosition = 0; } else { break; } } cb = _cachedBytes[_currentArrayIndex].Length - _currentPosition; if (cb > count) cb = count; Array.Copy(_cachedBytes[_currentArrayIndex], _currentPosition, buffer, offset, cb); _currentPosition += cb; count -= (int)cb; offset += (int)cb; intCount += (int)cb; } return intCount; } override public long Seek(long offset, SeekOrigin origin) { long pos = 0; if (null == _cachedBytes) { throw ADP.StreamClosed(ADP.Read); } switch(origin) { case SeekOrigin.Begin: SetInternalPosition(offset, ADP.ParameterOffset); break; case SeekOrigin.Current: pos = offset + Position; SetInternalPosition(pos, ADP.ParameterOffset); break; case SeekOrigin.End: pos = TotalLength + offset; SetInternalPosition(pos, ADP.ParameterOffset); break; default: throw ADP.InvalidSeekOrigin(ADP.ParameterOffset); } return pos; } override public void SetLength(long value) { throw ADP.NotSupported(); } override public void Write(byte[] buffer, int offset, int count) { throw ADP.NotSupported(); } private void SetInternalPosition(long lPos, string argumentName) { long pos = lPos; if (pos < 0) { throw new ArgumentOutOfRangeException(argumentName); } for (int ii = 0 ; ii < _cachedBytes.Count ; ii++) { if (pos > _cachedBytes[ii].Length) { pos -= _cachedBytes[ii].Length; } else { _currentArrayIndex = ii; _currentPosition = (int)pos; return; } } if (pos > 0) throw new ArgumentOutOfRangeException(argumentName); } private long TotalLength { get { if ((_totalLength == 0) && (_cachedBytes != null)) { long pos = 0; for (int ii = 0 ; ii < _cachedBytes.Count ; ii++) { pos += _cachedBytes[ii].Length; } _totalLength = pos; } return _totalLength; } } } sealed internal class SqlStreamingXml { int _columnOrdinal; SqlDataReader _reader; XmlReader _xmlReader; XmlWriter _xmlWriter; StringWriter _strWriter; long _charsRemoved; public SqlStreamingXml(int i, SqlDataReader reader) { _columnOrdinal = i; _reader = reader; } public void Close() { ((IDisposable)_xmlWriter).Dispose(); ((IDisposable)_xmlReader).Dispose(); _reader = null; _xmlReader = null; _xmlWriter = null; _strWriter = null; } public int ColumnOrdinal { get { return _columnOrdinal; } } public long GetChars(long dataIndex, char[] buffer, int bufferIndex, int length) { if (_xmlReader == null) { SqlStream sqlStream = new SqlStream( _columnOrdinal, _reader, true /* addByteOrderMark */, false /* processAllRows*/, false /*advanceReader*/); _xmlReader = sqlStream.ToXmlReader(); _strWriter = new StringWriter((System.IFormatProvider)null); XmlWriterSettings writerSettings = new XmlWriterSettings(); writerSettings.CloseOutput = true; // close the memory stream when done writerSettings.ConformanceLevel = ConformanceLevel.Fragment; _xmlWriter = XmlWriter.Create(_strWriter, writerSettings); } int charsToSkip = 0; int cnt = 0; if (dataIndex < _charsRemoved) { throw ADP.NonSeqByteAccess(dataIndex, _charsRemoved, ADP.GetChars); } else if (dataIndex > _charsRemoved) { charsToSkip = (int)(dataIndex - _charsRemoved); } // If buffer parameter is null, we have to return -1 since there is no way for us to know the // total size up front without reading and converting the XML. if (buffer == null) { return (long)(-1); } StringBuilder strBldr = _strWriter.GetStringBuilder(); while (!_xmlReader.EOF) { if (strBldr.Length >= (length+ charsToSkip)) { break; } // Can't call _xmlWriter.WriteNode here, since it reads all of the data in before returning the first char. // Do own implementation of WriteNode instead that reads just enough data to return the required number of chars //_xmlWriter.WriteNode(_xmlReader, true); // _xmlWriter.Flush(); WriteXmlElement(); if (charsToSkip > 0) { // Aggressively remove the characters we want to skip to avoid growing StringBuilder size too much cnt = strBldr.Length < charsToSkip ? strBldr.Length : charsToSkip; strBldr.Remove(0, cnt); charsToSkip -= cnt; _charsRemoved +=(long)cnt; } } if (charsToSkip > 0) { cnt = strBldr.Length < charsToSkip ? strBldr.Length : charsToSkip; strBldr.Remove(0, cnt); charsToSkip -= cnt; _charsRemoved +=(long)cnt; } if (strBldr.Length == 0) { return 0; } // At this point charsToSkip must be 0 Debug.Assert(charsToSkip == 0); cnt = strBldr.Length < length ? strBldr.Length : length; for (int i = 0 ; i < cnt ; i++) { buffer[bufferIndex + i] = strBldr[i]; } // Remove the characters we have already returned strBldr.Remove(0, cnt); _charsRemoved += (long)cnt; return (long)cnt; } // This method duplicates the work of XmlWriter.WriteNode except that it reads one element at a time // instead of reading the entire node like XmlWriter. private void WriteXmlElement() { if (_xmlReader.EOF) return; bool canReadChunk = _xmlReader.CanReadValueChunk; char[] writeNodeBuffer = null; // Constants const int WriteNodeBufferSize = 1024; _xmlReader.Read(); switch (_xmlReader.NodeType) { case XmlNodeType.Element: _xmlWriter.WriteStartElement(_xmlReader.Prefix, _xmlReader.LocalName, _xmlReader.NamespaceURI); _xmlWriter.WriteAttributes(_xmlReader, true); if (_xmlReader.IsEmptyElement) { _xmlWriter.WriteEndElement(); break; } break; case XmlNodeType.Text: if (canReadChunk) { if (writeNodeBuffer == null) { writeNodeBuffer = new char[WriteNodeBufferSize]; } int read; while ((read = _xmlReader.ReadValueChunk(writeNodeBuffer, 0, WriteNodeBufferSize)) > 0) { _xmlWriter.WriteChars(writeNodeBuffer, 0, read); } } else { _xmlWriter.WriteString(_xmlReader.Value); } break; case XmlNodeType.Whitespace: case XmlNodeType.SignificantWhitespace: _xmlWriter.WriteWhitespace(_xmlReader.Value); break; case XmlNodeType.CDATA: _xmlWriter.WriteCData(_xmlReader.Value); break; case XmlNodeType.EntityReference: _xmlWriter.WriteEntityRef(_xmlReader.Name); break; case XmlNodeType.XmlDeclaration: case XmlNodeType.ProcessingInstruction: _xmlWriter.WriteProcessingInstruction(_xmlReader.Name, _xmlReader.Value); break; case XmlNodeType.DocumentType: _xmlWriter.WriteDocType(_xmlReader.Name, _xmlReader.GetAttribute("PUBLIC"), _xmlReader.GetAttribute("SYSTEM"), _xmlReader.Value); break; case XmlNodeType.Comment: _xmlWriter.WriteComment(_xmlReader.Value); break; case XmlNodeType.EndElement: _xmlWriter.WriteFullEndElement(); break; } _xmlWriter.Flush(); } } }