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