// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Generic; using System.Data.SqlTypes; using System.IO; using System.Text; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage { /// /// Reader for service buffer formatted file streams /// public class ServiceBufferFileStreamReader : IFileStreamReader { private const int DefaultBufferSize = 8192; #region Member Variables private byte[] buffer; private readonly Stream fileStream; private readonly Dictionary> readMethods; #endregion /// /// Constructs a new ServiceBufferFileStreamReader and initializes its state /// /// The filestream to read from public ServiceBufferFileStreamReader(Stream stream) { // Open file for reading/writing if (!stream.CanRead || !stream.CanSeek) { throw new InvalidOperationException("Stream must be readable and seekable"); } fileStream = stream; // Create internal buffer buffer = new byte[DefaultBufferSize]; // Create the methods that will be used to read back readMethods = new Dictionary> { {typeof(string), ReadString}, {typeof(short), ReadInt16}, {typeof(int), ReadInt32}, {typeof(long), ReadInt64}, {typeof(byte), ReadByte}, {typeof(char), ReadChar}, {typeof(bool), ReadBoolean}, {typeof(double), ReadDouble}, {typeof(float), ReadSingle}, {typeof(decimal), ReadDecimal}, {typeof(DateTime), ReadDateTime}, {typeof(DateTimeOffset), ReadDateTimeOffset}, {typeof(TimeSpan), ReadTimeSpan}, {typeof(byte[]), ReadBytes}, {typeof(SqlString), ReadString}, {typeof(SqlInt16), ReadInt16}, {typeof(SqlInt32), ReadInt32}, {typeof(SqlInt64), ReadInt64}, {typeof(SqlByte), ReadByte}, {typeof(SqlBoolean), ReadBoolean}, {typeof(SqlDouble), ReadDouble}, {typeof(SqlSingle), ReadSingle}, {typeof(SqlDecimal), ReadSqlDecimal}, {typeof(SqlDateTime), ReadDateTime}, {typeof(SqlBytes), ReadBytes}, {typeof(SqlBinary), ReadBytes}, {typeof(SqlGuid), ReadGuid}, {typeof(SqlMoney), ReadMoney}, }; } #region IFileStreamStorage Implementation /// /// Reads a row from the file, based on the columns provided /// /// Offset into the file where the row starts /// The columns that were encoded /// The objects from the row, ready for output to the client public IList ReadRow(long fileOffset, IEnumerable columns) { // Initialize for the loop long currentFileOffset = fileOffset; List results = new List(); // Iterate over the columns foreach (DbColumnWrapper column in columns) { // We will pivot based on the type of the column Type colType; if (column.IsSqlVariant) { // For SQL Variant columns, the type is written first in string format FileStreamReadResult sqlVariantTypeResult = ReadString(currentFileOffset); currentFileOffset += sqlVariantTypeResult.TotalLength; string sqlVariantType = (string)sqlVariantTypeResult.Value.RawObject; // If the typename is null, then the whole value is null if (sqlVariantTypeResult.Value == null || string.IsNullOrEmpty(sqlVariantType)) { results.Add(sqlVariantTypeResult.Value); continue; } // The typename is stored in the string colType = Type.GetType(sqlVariantType); // Workaround .NET bug, see sqlbu# 440643 and vswhidbey# 599834 // TODO: Is this workaround necessary for .NET Core? if (colType == null && sqlVariantType == "System.Data.SqlTypes.SqlSingle") { colType = typeof(SqlSingle); } } else { colType = column.DataType; } // Use the right read function for the type to read the data from the file Func readFunc; if(!readMethods.TryGetValue(colType, out readFunc)) { // Treat everything else as a string readFunc = ReadString; } FileStreamReadResult result = readFunc(currentFileOffset); currentFileOffset += result.TotalLength; results.Add(result.Value); } return results; } /// /// Reads a short from the file at the offset provided /// /// Offset into the file to read the short from /// A short public FileStreamReadResult ReadInt16(long fileOffset) { return ReadCellHelper(fileOffset, length => BitConverter.ToInt16(buffer, 0)); } /// /// Reads a int from the file at the offset provided /// /// Offset into the file to read the int from /// An int public FileStreamReadResult ReadInt32(long fileOffset) { return ReadCellHelper(fileOffset, length => BitConverter.ToInt32(buffer, 0)); } /// /// Reads a long from the file at the offset provided /// /// Offset into the file to read the long from /// A long public FileStreamReadResult ReadInt64(long fileOffset) { return ReadCellHelper(fileOffset, length => BitConverter.ToInt64(buffer, 0)); } /// /// Reads a byte from the file at the offset provided /// /// Offset into the file to read the byte from /// A byte public FileStreamReadResult ReadByte(long fileOffset) { return ReadCellHelper(fileOffset, length => buffer[0]); } /// /// Reads a char from the file at the offset provided /// /// Offset into the file to read the char from /// A char public FileStreamReadResult ReadChar(long fileOffset) { return ReadCellHelper(fileOffset, length => BitConverter.ToChar(buffer, 0)); } /// /// Reads a bool from the file at the offset provided /// /// Offset into the file to read the bool from /// A bool public FileStreamReadResult ReadBoolean(long fileOffset) { return ReadCellHelper(fileOffset, length => buffer[0] == 0x1); } /// /// Reads a single from the file at the offset provided /// /// Offset into the file to read the single from /// A single public FileStreamReadResult ReadSingle(long fileOffset) { return ReadCellHelper(fileOffset, length => BitConverter.ToSingle(buffer, 0)); } /// /// Reads a double from the file at the offset provided /// /// Offset into the file to read the double from /// A double public FileStreamReadResult ReadDouble(long fileOffset) { return ReadCellHelper(fileOffset, length => BitConverter.ToDouble(buffer, 0)); } /// /// Reads a SqlDecimal from the file at the offset provided /// /// Offset into the file to read the SqlDecimal from /// A SqlDecimal public FileStreamReadResult ReadSqlDecimal(long offset) { return ReadCellHelper(offset, length => { int[] arrInt32 = new int[(length - 3) / 4]; Buffer.BlockCopy(buffer, 3, arrInt32, 0, length - 3); return new SqlDecimal(buffer[0], buffer[1], buffer[2] == 1, arrInt32); }); } /// /// Reads a decimal from the file at the offset provided /// /// Offset into the file to read the decimal from /// A decimal public FileStreamReadResult ReadDecimal(long offset) { return ReadCellHelper(offset, length => { int[] arrInt32 = new int[length / 4]; Buffer.BlockCopy(buffer, 0, arrInt32, 0, length); return new decimal(arrInt32); }); } /// /// Reads a DateTime from the file at the offset provided /// /// Offset into the file to read the DateTime from /// A DateTime public FileStreamReadResult ReadDateTime(long offset) { return ReadCellHelper(offset, length => { long ticks = BitConverter.ToInt64(buffer, 0); return new DateTime(ticks); }); } /// /// Reads a DateTimeOffset from the file at the offset provided /// /// Offset into the file to read the DateTimeOffset from /// A DateTimeOffset public FileStreamReadResult ReadDateTimeOffset(long offset) { // DateTimeOffset is represented by DateTime.Ticks followed by TimeSpan.Ticks // both as Int64 values return ReadCellHelper(offset, length => { long dtTicks = BitConverter.ToInt64(buffer, 0); long dtOffset = BitConverter.ToInt64(buffer, 8); return new DateTimeOffset(new DateTime(dtTicks), new TimeSpan(dtOffset)); }); } /// /// Reads a TimeSpan from the file at the offset provided /// /// Offset into the file to read the TimeSpan from /// A TimeSpan public FileStreamReadResult ReadTimeSpan(long offset) { return ReadCellHelper(offset, length => { long ticks = BitConverter.ToInt64(buffer, 0); return new TimeSpan(ticks); }); } /// /// Reads a string from the file at the offset provided /// /// Offset into the file to read the string from /// A string public FileStreamReadResult ReadString(long offset) { return ReadCellHelper(offset, length => length > 0 ? Encoding.Unicode.GetString(buffer, 0, length) : string.Empty, totalLength => totalLength == 1); } /// /// Reads bytes from the file at the offset provided /// /// Offset into the file to read the bytes from /// A byte array public FileStreamReadResult ReadBytes(long offset) { return ReadCellHelper(offset, length => { byte[] output = new byte[length]; Buffer.BlockCopy(buffer, 0, output, 0, length); return output; }, totalLength => totalLength == 1, bytes => { StringBuilder sb = new StringBuilder("0x"); foreach (byte b in bytes) { sb.AppendFormat("{0:X2}", b); } return sb.ToString(); }); } /// /// Reads the bytes that make up a GUID at the offset provided /// /// Offset into the file to read the bytes from /// A guid type object public FileStreamReadResult ReadGuid(long offset) { return ReadCellHelper(offset, length => { byte[] output = new byte[length]; Buffer.BlockCopy(buffer, 0, output, 0, length); return new SqlGuid(output); }, totalLength => totalLength == 1); } /// /// Reads a SqlMoney type from the offset provided /// into a /// /// /// A sql money type object public FileStreamReadResult ReadMoney(long offset) { return ReadCellHelper(offset, length => { int[] arrInt32 = new int[length / 4]; Buffer.BlockCopy(buffer, 0, arrInt32, 0, length); return new SqlMoney(new decimal(arrInt32)); }); } /// /// Reads the length of a field at the specified offset in the file /// /// Offset into the file to read the field length from /// A LengthResult internal LengthResult ReadLength(long offset) { // read in length information int lengthValue; fileStream.Seek(offset, SeekOrigin.Begin); int lengthLength = fileStream.Read(buffer, 0, 1); if (buffer[0] != 0xFF) { // one byte is enough lengthValue = Convert.ToInt32(buffer[0]); } else { // read in next 4 bytes lengthLength += fileStream.Read(buffer, 0, 4); // reconstruct the length lengthValue = BitConverter.ToInt32(buffer, 0); } return new LengthResult {LengthLength = lengthLength, ValueLength = lengthValue}; } #endregion #region Private Helpers /// /// Creates a new buffer that is of the specified length if the buffer is not already /// at least as long as specified. /// /// The minimum buffer size private void AssureBufferLength(int newBufferLength) { if (buffer.Length < newBufferLength) { buffer = new byte[newBufferLength]; } } /// /// Reads the value of a cell from the file wrapper, checks to see if it null using /// , and converts it to the proper output type using /// . /// /// Offset into the file to read from /// Function to use to convert the buffer to the target type /// /// If provided, this function will be used to determine if the value is null /// /// Optional function to use to convert the object to a string. /// The expected type of the cell. Used to keep the code honest /// The object, a display value, and the length of the value + its length private FileStreamReadResult ReadCellHelper(long offset, Func convertFunc, Func isNullFunc = null, Func toStringFunc = null) { LengthResult length = ReadLength(offset); DbCellValue result = new DbCellValue(); if (isNullFunc == null ? length.ValueLength == 0 : isNullFunc(length.TotalLength)) { result.RawObject = null; result.DisplayValue = null; } else { AssureBufferLength(length.ValueLength); fileStream.Read(buffer, 0, length.ValueLength); T resultObject = convertFunc(length.ValueLength); result.RawObject = resultObject; result.DisplayValue = toStringFunc == null ? result.RawObject.ToString() : toStringFunc(resultObject); } return new FileStreamReadResult(result, length.TotalLength); } #endregion /// /// Internal struct used for representing the length of a field from the file /// internal struct LengthResult { /// /// How many bytes the length takes up /// public int LengthLength { get; set; } /// /// How many bytes the value takes up /// public int ValueLength { get; set; } /// /// + /// public int TotalLength { get { return LengthLength + ValueLength; } } } #region IDisposable Implementation private bool disposed; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposed) { return; } if (disposing) { fileStream.Dispose(); } disposed = true; } ~ServiceBufferFileStreamReader() { Dispose(false); } #endregion } }