diff --git a/Core/InteropServices/ComStream.cs b/Core/InteropServices/ComStream.cs index 30eb10ee..b6f28361 100644 --- a/Core/InteropServices/ComStream.cs +++ b/Core/InteropServices/ComStream.cs @@ -1,157 +1,515 @@ using System; using System.IO; +using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; +using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG; namespace Vanara.InteropServices { - /// Implements a .NET stream over a COM IStream instance. + /// Implements a .NET stream derivation and a COM IStream instance. /// - public class ComStream : Stream + /// + /// + public class ComStream : Stream, IStream, IDisposable { - /// Initializes a new instance of the class. - /// The supporting COM instance. - /// stream - public ComStream(IStream stream) => IStream = stream ?? throw new ArgumentNullException(nameof(stream)); + private readonly IStream comStream = null; + private readonly Stream netStream = null; - /// Gets a value indicating whether the current stream supports reading. - public override bool CanRead => true; + /// Initializes a new instance of the ComStream class. + /// An IO.Stream + /// stream - Stream cannot be null + /// Stream cannot be null + public ComStream(Stream stream) + { + if (stream is null) + throw new ArgumentNullException(nameof(stream), "Stream cannot be null"); - /// Gets a value indicating whether the current stream supports seeking. - public override bool CanSeek => true; + netStream = stream; + } - /// Gets a value indicating whether the current stream supports writing. - public override bool CanWrite => true; + /// Initializes a new instance of the ComStream class. + /// A ComTypes.IStream + /// stream - IStream cannot be null + /// Stream cannot be null + public ComStream(IStream stream) + { + if (stream is null) + throw new ArgumentNullException(nameof(stream), "IStream cannot be null"); - /// Gets the underlying stream. - /// The underlying stream. - public IStream IStream { get; private set; } + comStream = stream; + } - /// Gets the length in bytes of the stream. - /// iComStream + // Default constructor. Should not be used to create an ComStream object. + private ComStream() + { + } + + /// Finalizes an instance of the class. + ~ComStream() + { + netStream?.Close(); + } + + /// When overridden in a derived class, gets the length in bytes of the stream. public override long Length { get { - if (IStream is null) - throw new ObjectDisposedException(nameof(IStream)); - - IStream.Stat(out var statstg, 1 /* STATSFLAG_NONAME*/ ); - return statstg.cbSize; + if (comStream is not null) + { + // Call IStream.Stat to retrieve info about the stream, which includes the length. STATFLAG_NONAME means that we don't + // care about the name (STATSTG.pwcsName), so there is no need for the method to allocate memory for the string. + comStream.Stat(out var statstg, 1); + return statstg.cbSize; + } + else + { + return netStream.Length; + } } } - /// Gets or sets the position within the current stream. + /// Gets or sets the position. + /// The position. public override long Position { - get => Seek(0, SeekOrigin.Current); - set => Seek(value, SeekOrigin.Begin); + get => comStream is not null ? Seek(0, SeekOrigin.Current) : netStream.Position; + set + { + if (comStream is not null) + { + Seek(value, SeekOrigin.Begin); + } + else + { + netStream.Position = value; + } + } + } + + /// Gets a value indicating whether this instance can read. + /// if this instance can read; otherwise, . + public override bool CanRead => comStream is not null || netStream.CanRead; + + /// Gets a value indicating whether this instance can seek. + /// if this instance can seek; otherwise, . + public override bool CanSeek => comStream is not null || netStream.CanSeek; + + /// Gets a value indicating whether this instance can timeout. + /// if this instance can timeout; otherwise, . + public override bool CanTimeout => comStream is null && netStream.CanTimeout; + + /// Gets a value indicating whether this instance can write. + /// if this instance can write; otherwise, . + public override bool CanWrite => comStream is not null || netStream.CanWrite; + + /// Gets the instance from the value, if possible. + /// The stream. + /// An instance or if not available. + public static IStream ToIStream(object stream) => stream is Stream ? new ComStream(stream as Stream) : stream is IStream ? stream as IStream : null; + + /// Gets the instance from the value, if possible. + /// The stream. + /// An instance or if not available. + public static Stream ToStream(object stream) => stream is Stream ? stream as Stream : stream is IStream ? new ComStream(stream as IStream) : null; + + /// Creates a new stream object with its own seek pointer that references the same bytes as the original stream. + /// When successful, pointer to the location of an IStream pointer to the new stream object. + /// + /// The IO.Streamtream cannot be cloned. + /// This method is not used and always throws the exception. + void IStream.Clone(out IStream ppstm) + { + if (netStream is not null) + throw new NotSupportedException("A System.IO.Stream instance cannot be cloned."); + + comStream.Clone(out ppstm); + } + + /// Closes the current stream and releases any resources (such as the Stream) associated with the current IStream. + /// + /// This method is not a member in IStream. + public override void Close() + { + if (netStream is not null) + { + netStream.Close(); + } + else + { + comStream.Commit(0 /*STGC_DEFAULT*/); + // Marshal.ReleaseComObject(TheIStream); // Investigate this because we cannot release an IStream to the stash file + } + GC.SuppressFinalize(this); + } + + /// Ensures that any changes made to an stream object that is open in transacted mode are reflected in the parent storage. + /// + /// Controls how the changes for the stream object are committed. See the STGC enumeration for a definition of these values. + /// + /// + /// An I/O error occurs. + /// The parameter is not used and this method only does Stream.Flush() + void IStream.Commit(int grfCommitFlags) + { + // Clears all buffers for this stream and causes any buffered data to be written to the underlying device. + if (netStream is not null) + { + netStream.Flush(); + } + else + { + comStream.Commit(grfCommitFlags); + } } /// - /// Closes the current stream and releases any resources (such as sockets and file handles) associated with the current stream. + /// Copies a specified number of bytes from the current seek pointer in the stream to the current seek pointer in another stream. /// - public override void Close() + /// The destination stream. The pstm stream can be a new stream or a clone of the source stream. + /// The number of bytes to copy from the source stream. + /// + /// The actual number of bytes read from the source. It can be set to IntPtr.Zero. In this case, this method does not provide the + /// actual number of bytes read. + /// + /// + /// The actual number of bytes written to the destination. It can be set this to IntPtr.Zero. In this case, this method does not + /// provide the actual number of bytes written. + /// + /// + /// The actual number of bytes read ( ) and written ( ) from the source. + /// + /// The sum of offset and count is larger than the buffer length. + /// buffer is a null reference. + /// offset or count is negative. + /// An I/O error occurs. + /// The stream does not support reading. + /// Methods were called after the stream was closed. + void IStream.CopyTo(IStream pstm, long cb, IntPtr pcbRead, IntPtr pcbWritten) { - if (IStream is null) return; - IStream.Commit(0); - IStream = null; + if (netStream is not null) + { + var sourceBytes = new byte[cb]; + long totalBytesRead = 0; + long totalBytesWritten = 0; + + IntPtr bw = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(int))); + Marshal.WriteInt32(bw, 0); + + while (totalBytesWritten < cb) + { + var currentBytesRead = netStream.Read(sourceBytes, 0, (int)(cb - totalBytesWritten)); + + // Has the end of the stream been reached? + if (currentBytesRead == 0) break; + + totalBytesRead += currentBytesRead; + + pstm.Write(sourceBytes, currentBytesRead, bw); + var currentBytesWritten = Marshal.ReadInt32(bw); + if (currentBytesWritten != currentBytesRead) + { + System.Diagnostics.Debug.WriteLine("ERROR!: The IStream Write is not writing all the bytes needed!"); + } + totalBytesWritten += currentBytesWritten; + } + + Marshal.FreeHGlobal(bw); + + if (pcbRead != IntPtr.Zero) Marshal.WriteInt64(pcbRead, totalBytesRead); + if (pcbWritten != IntPtr.Zero) Marshal.WriteInt64(pcbWritten, totalBytesWritten); + } + else + { + comStream.CopyTo(pstm, cb, pcbRead, pcbWritten); + } } /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. - public override void Flush() => IStream?.Commit(0); + /// + public override void Flush() => ((IStream)this).Commit(0 /*STGC_DEFAULT*/); + + /// Restricts access to a specified range of bytes in the stream. + /// Integer that specifies the byte offset for the beginning of the range. + /// Integer that specifies the length of the range, in bytes, to be restricted. + /// Specifies the restrictions being requested on accessing the range. + /// + /// The IO.Stream does not support locking. + /// This method is not used and always throws the exception. + void IStream.LockRegion(long libOffset, long cb, int dwLockType) + { + if (netStream is not null) + throw new NotSupportedException("Stream does not support locking."); + + comStream.LockRegion(libOffset, cb, dwLockType); + } /// /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. /// /// - /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between - /// and ( + - 1) replaced by the bytes read from the - /// current source. - /// - /// - /// The zero-based byte offset in at which to begin storing the data read from the current stream. + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between offset and + /// (offset + count - 1) replaced by the bytes read from the current source. /// + /// The zero-based byte offset in buffer at which to begin storing the data read from the current stream. /// The maximum number of bytes to be read from the current stream. /// /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not /// currently available, or zero (0) if the end of the stream has been reached. /// - /// iComStream - /// Offsets other than zero are not supported. + /// Only a zero offset is supported. public override int Read(byte[] buffer, int offset, int count) { - if (IStream is null) - throw new ObjectDisposedException(nameof(IStream)); + if (comStream is not null) + { + var rdBuf = buffer; + if (offset != 0) +#if NETCOREAPP3_0_OR_GREATER || NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + rdBuf = buffer[offset..]; +#else + throw new InvalidOperationException("Offsets cannot be handled by IStream."); +#endif + unsafe + { + int bytesRead = 0; + comStream.Read(rdBuf, count, (IntPtr)(void*)&bytesRead); + return bytesRead; + } + } + else + { + return netStream.Read(buffer, offset, count); + } + } - if (offset != 0) - throw new NotSupportedException("Offsets other than zero are not supported."); + /// Reads a specified number of bytes from the stream object into memory starting at the current seek pointer. + /// The buffer which the stream data is read into. + /// The number of bytes of data to read from the stream object. + /// + /// A pointer to a ULONG variable that receives the actual number of bytes read from the stream object. It can be set to + /// IntPtr.Zero. In this case, this method does not return the number of bytes read. + /// + /// The actual number of bytes read ( ) from the source. + /// The sum of offset and count is larger than the buffer length. + /// buffer is a null reference. + /// offset or count is negative. + /// An I/O error occurs. + /// The stream does not support reading. + /// Methods were called after the stream was closed. + void IStream.Read(byte[] pv, int cb, IntPtr pcbRead) + { + if (netStream is not null) + { + var bytesRead = netStream.Read(pv, 0, cb); + if (pcbRead != IntPtr.Zero) + Marshal.WriteInt32(pcbRead, bytesRead); + } + else + { + comStream.Read(pv, cb, pcbRead); + } + } - var bytesRead = 0; - using (var address = new PinnedObject(bytesRead)) - IStream.Read(buffer, count, address); - return bytesRead; + /// Discards all changes that have been made to a transacted stream since the last stream.Commit call + /// + /// The IO.Stream does not support reverting. + /// This method is not used and always throws the exception. + void IStream.Revert() + { + if (netStream is not null) + throw new NotSupportedException("Stream does not support reverting."); + + comStream.Revert(); } /// Sets the position within the current stream. - /// A byte offset relative to the parameter. - /// - /// A value of type indicating the reference point used to obtain the new position. - /// + /// A byte offset relative to the origin parameter. + /// A value of type SeekOrigin indicating the reference point used to obtain the new position. /// The new position within the current stream. - /// iComStream public override long Seek(long offset, SeekOrigin origin) { - if (IStream is null) - throw new ObjectDisposedException(nameof(IStream)); + if (comStream is not null) + { + unsafe + { + long position = 0; + // The enum values of SeekOrigin match the enum values of STREAM_SEEK, so we can just cast the origin to an integer. + comStream.Seek(offset, (int)origin, (IntPtr)(void*)&position); + return position; + } + } + else + { + return netStream.Seek(offset, origin); + } + } - var position = 0L; - using (var address = new PinnedObject(position)) - IStream.Seek(offset, (int)origin, address); - return position; + /// + /// Changes the seek pointer to a new location relative to the beginning of the stream, the end of the stream, or the current seek pointer + /// + /// + /// The displacement to be added to the location indicated by the dwOrigin parameter. If dwOrigin is STREAM_SEEK_SET, this is + /// interpreted as an unsigned value rather than a signed value. + /// + /// + /// The origin for the displacement specified in dlibMove. The origin can be the beginning of the file (STREAM_SEEK_SET), the + /// current seek pointer (STREAM_SEEK_CUR), or the end of the file (STREAM_SEEK_END). + /// + /// + /// The location where this method writes the value of the new seek pointer from the beginning of the stream. It can be set to + /// IntPtr.Zero. In this case, this method does not provide the new seek pointer. + /// + /// + /// Returns in the location where this method writes the value of the new seek pointer from the + /// beginning of the stream. + /// + /// An I/O error occurs. + /// The stream does not support reading. + /// Methods were called after the stream was closed. + void IStream.Seek(long dlibMove, int dwOrigin, IntPtr plibNewPosition) + { + if (netStream is not null) + { + // The enum values of SeekOrigin match the enum values of STREAM_SEEK, so we can just cast the dwOrigin to a SeekOrigin + var origin = Enum.IsDefined(typeof(SeekOrigin), dwOrigin) ? (SeekOrigin)dwOrigin : SeekOrigin.Begin; + var newPos = netStream.Seek(dlibMove, origin); + if (plibNewPosition == IntPtr.Zero) + Marshal.WriteInt64(plibNewPosition, newPos); + } + else + { + comStream.Seek(dlibMove, dwOrigin, plibNewPosition); + } } /// Sets the length of the current stream. /// The desired length of the current stream in bytes. - /// iComStream + /// public override void SetLength(long value) { - if (IStream is null) - throw new ObjectDisposedException(nameof(IStream)); + if (comStream is not null) + { + comStream.SetSize(value); + } + else + { + netStream.SetLength(value); + } + } - IStream.SetSize(value); + /// Changes the size of the stream object. + /// Specifies the new size of the stream as a number of bytes. + /// + /// An I/O error occurs. + /// The stream does not support reading. + /// Methods were called after the stream was closed. + void IStream.SetSize(long libNewSize) => SetLength(libNewSize); + + /// Retrieves the STATSTG structure for this stream. + /// The STATSTG structure where this method places information about this stream object. + /// + /// Specifies that this method does not return some of the members in the STATSTG structure, thus saving a memory allocation + /// operation. This parameter is not used internally. + /// + /// + /// The stream does not support reading. + /// Methods were called after the stream was closed. + /// The parameter is not used + void IStream.Stat(out STATSTG pstatstg, int grfStatFlag) + { + if (netStream is not null) + { + pstatstg = new STATSTG + { + type = 2, // STGTY_STREAM + // Gets the length in bytes of the stream. + cbSize = netStream.Length, + grfMode = 2, // STGM_READWRITE; + grfLocksSupported = 2 // LOCK_EXCLUSIVE + }; + } + else + { + comStream.Stat(out pstatstg, grfStatFlag); + } + } + + /// Removes the access restriction on a range of bytes previously restricted with the LockRegion method. + /// Specifies the byte offset for the beginning of the range. + /// Specifies, in bytes, the length of the range to be restricted. + /// Specifies the access restrictions previously placed on the range. + /// + /// The IO.Stream does not support unlocking. + /// This method is not used and always throws the exception. + void IStream.UnlockRegion(long libOffset, long cb, int dwLockType) + { + if (netStream is not null) + throw new NotSupportedException("Stream does not support unlocking."); + + comStream.UnlockRegion(libOffset, cb, dwLockType); } /// /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. /// - /// - /// An array of bytes. This method copies bytes from to the current stream. - /// - /// - /// The zero-based byte offset in at which to begin copying bytes to the current stream. - /// + /// An array of bytes. This method copies count bytes from buffer to the current stream. + /// The zero-based byte offset in buffer at which to begin copying bytes to the current stream. /// The number of bytes to be written to the current stream. - /// iComStream - /// Offsets other than zero are not supported. + /// + /// Only a zero offset is supported. public override void Write(byte[] buffer, int offset, int count) { - if (IStream is null) - throw new ObjectDisposedException(nameof(IStream)); - - if (offset != 0) - throw new NotSupportedException("Offsets other than zero are not supported."); - - IStream.Write(buffer, count, IntPtr.Zero); + if (buffer.Length < count - offset) + throw new ArgumentOutOfRangeException(nameof(count)); + if (comStream is not null) + { + var wrBuf = buffer; + if (offset == 0) + { + wrBuf = new byte[count]; + Array.Copy(buffer, offset, wrBuf, 0, count); + } + comStream.Write(wrBuf, count, IntPtr.Zero); + } + else + { + netStream.Write(buffer, offset, count); + } } - /// - /// Releases the unmanaged resources used by the and optionally releases the managed resources. - /// - /// true to release both managed and unmanaged resources; false to release only unmanaged resources. - protected override void Dispose(bool disposing) + /// Writes a specified number of bytes into the stream object starting at the current seek pointer. + /// + /// The buffer that contains the data that is to be written to the stream. A valid buffer must be provided for this parameter even + /// when cb is zero. + /// + /// The number of bytes of data to attempt to write into the stream. This value can be zero. + /// + /// A variable where this method writes the actual number of bytes written to the stream object. The caller can set this to + /// IntPtr.Zero, in which case this method does not provide the actual number of bytes written. + /// + /// The actual number of bytes written ( ). + /// The sum of offset and count is larger than the buffer length. + /// buffer is a null reference. + /// offset or count is negative. + /// An I/O error occurs. + /// The IO.Stream does not support reading. + /// Methods were called after the stream was closed. + void IStream.Write(byte[] pv, int cb, IntPtr pcbWritten) { - if (disposing) Close(); - base.Dispose(disposing); + if (netStream is not null) + { + var currentPosition = netStream.Position; + netStream.Write(pv, 0, cb); + if (pcbWritten != IntPtr.Zero) + Marshal.WriteInt32(pcbWritten, (int)(netStream.Position - currentPosition)); + } + else + { + comStream.Write(pv, cb, pcbWritten); + } } + + /// Releases all resources used by the Stream object. + void IDisposable.Dispose() => Close(); } } \ No newline at end of file