Updated ComStream to support IStream implementation

pull/221/head
dahall 2021-03-28 16:34:49 -06:00
parent e47f7c883c
commit ce5e582d25
1 changed files with 442 additions and 84 deletions

View File

@ -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
{
/// <summary>Implements a .NET stream over a COM IStream instance.</summary>
/// <summary>Implements a .NET stream derivation and a COM IStream instance.</summary>
/// <seealso cref="System.IO.Stream"/>
public class ComStream : Stream
/// <seealso cref="System.Runtime.InteropServices.ComTypes.IStream"/>
/// <seealso cref="System.IDisposable"/>
public class ComStream : Stream, IStream, IDisposable
{
/// <summary>Initializes a new instance of the <see cref="ComStream"/> class.</summary>
/// <param name="stream">The supporting COM <see cref="IStream"/> instance.</param>
/// <exception cref="System.ArgumentNullException">stream</exception>
public ComStream(IStream stream) => IStream = stream ?? throw new ArgumentNullException(nameof(stream));
private readonly IStream comStream = null;
private readonly Stream netStream = null;
/// <summary>Gets a value indicating whether the current stream supports reading.</summary>
public override bool CanRead => true;
/// <summary>Initializes a new instance of the ComStream class.</summary>
/// <param name="stream">An IO.Stream</param>
/// <exception cref="System.ArgumentNullException">stream - Stream cannot be null</exception>
/// <exception cref="ArgumentNullException">Stream cannot be null</exception>
public ComStream(Stream stream)
{
if (stream is null)
throw new ArgumentNullException(nameof(stream), "Stream cannot be null");
/// <summary>Gets a value indicating whether the current stream supports seeking.</summary>
public override bool CanSeek => true;
netStream = stream;
}
/// <summary>Gets a value indicating whether the current stream supports writing.</summary>
public override bool CanWrite => true;
/// <summary>Initializes a new instance of the ComStream class.</summary>
/// <param name="stream">A ComTypes.IStream</param>
/// <exception cref="System.ArgumentNullException">stream - IStream cannot be null</exception>
/// <exception cref="ArgumentNullException">Stream cannot be null</exception>
public ComStream(IStream stream)
{
if (stream is null)
throw new ArgumentNullException(nameof(stream), "IStream cannot be null");
/// <summary>Gets the underlying stream.</summary>
/// <value>The underlying stream.</value>
public IStream IStream { get; private set; }
comStream = stream;
}
/// <summary>Gets the length in bytes of the stream.</summary>
/// <exception cref="System.ObjectDisposedException">iComStream</exception>
// Default constructor. Should not be used to create an ComStream object.
private ComStream()
{
}
/// <summary>Finalizes an instance of the <see cref="ComStream"/> class.</summary>
~ComStream()
{
netStream?.Close();
}
/// <summary>When overridden in a derived class, gets the length in bytes of the stream.</summary>
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;
}
}
}
/// <summary>Gets or sets the position within the current stream.</summary>
/// <summary>Gets or sets the position.</summary>
/// <value>The position.</value>
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;
}
}
}
/// <summary>Gets a value indicating whether this instance can read.</summary>
/// <value><see langword="true"/> if this instance can read; otherwise, <see langword="false"/>.</value>
public override bool CanRead => comStream is not null || netStream.CanRead;
/// <summary>Gets a value indicating whether this instance can seek.</summary>
/// <value><see langword="true"/> if this instance can seek; otherwise, <see langword="false"/>.</value>
public override bool CanSeek => comStream is not null || netStream.CanSeek;
/// <summary>Gets a value indicating whether this instance can timeout.</summary>
/// <value><see langword="true"/> if this instance can timeout; otherwise, <see langword="false"/>.</value>
public override bool CanTimeout => comStream is null && netStream.CanTimeout;
/// <summary>Gets a value indicating whether this instance can write.</summary>
/// <value><see langword="true"/> if this instance can write; otherwise, <see langword="false"/>.</value>
public override bool CanWrite => comStream is not null || netStream.CanWrite;
/// <summary>Gets the <see cref="IStream"/> instance from the <paramref name="stream"/> value, if possible.</summary>
/// <param name="stream">The stream.</param>
/// <returns>An <see cref="IStream"/> instance or <see langword="null"/> if not available.</returns>
public static IStream ToIStream(object stream) => stream is Stream ? new ComStream(stream as Stream) : stream is IStream ? stream as IStream : null;
/// <summary>Gets the <see cref="Stream"/> instance from the <paramref name="stream"/> value, if possible.</summary>
/// <param name="stream">The stream.</param>
/// <returns>An <see cref="Stream"/> instance or <see langword="null"/> if not available.</returns>
public static Stream ToStream(object stream) => stream is Stream ? stream as Stream : stream is IStream ? new ComStream(stream as IStream) : null;
/// <summary>Creates a new stream object with its own seek pointer that references the same bytes as the original stream.</summary>
/// <param name="ppstm">When successful, pointer to the location of an IStream pointer to the new stream object.</param>
/// <returns></returns>
/// <exception cref="NotSupportedException">The IO.Streamtream cannot be cloned.</exception>
/// <remarks>This method is not used and always throws the exception.</remarks>
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);
}
/// <summary>Closes the current stream and releases any resources (such as the Stream) associated with the current IStream.</summary>
/// <returns></returns>
/// <remarks>This method is not a member in IStream.</remarks>
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);
}
/// <summary>Ensures that any changes made to an stream object that is open in transacted mode are reflected in the parent storage.</summary>
/// <param name="grfCommitFlags">
/// Controls how the changes for the stream object are committed. See the STGC enumeration for a definition of these values.
/// </param>
/// <returns></returns>
/// <exception cref="IOException">An I/O error occurs.</exception>
/// <remarks>The <paramref name="grfCommitFlags"/> parameter is not used and this method only does Stream.Flush()</remarks>
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);
}
}
/// <summary>
/// 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.
/// </summary>
public override void Close()
/// <param name="pstm">The destination stream. The pstm stream can be a new stream or a clone of the source stream.</param>
/// <param name="cb">The number of bytes to copy from the source stream.</param>
/// <param name="pcbRead">
/// 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.
/// </param>
/// <param name="pcbWritten">
/// 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.
/// </param>
/// <returns>
/// The actual number of bytes read ( <paramref name="pcbRead"/>) and written ( <paramref name="pcbWritten"/>) from the source.
/// </returns>
/// <exception cref="ArgumentException">The sum of offset and count is larger than the buffer length.</exception>
/// <exception cref="ArgumentNullException">buffer is a null reference.</exception>
/// <exception cref="ArgumentOutOfRangeException">offset or count is negative.</exception>
/// <exception cref="IOException">An I/O error occurs.</exception>
/// <exception cref="NotSupportedException">The stream does not support reading.</exception>
/// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
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);
}
}
/// <summary>Clears all buffers for this stream and causes any buffered data to be written to the underlying device.</summary>
public override void Flush() => IStream?.Commit(0);
/// <returns></returns>
public override void Flush() => ((IStream)this).Commit(0 /*STGC_DEFAULT*/);
/// <summary>Restricts access to a specified range of bytes in the stream.</summary>
/// <param name="libOffset">Integer that specifies the byte offset for the beginning of the range.</param>
/// <param name="cb">Integer that specifies the length of the range, in bytes, to be restricted.</param>
/// <param name="dwLockType">Specifies the restrictions being requested on accessing the range.</param>
/// <returns></returns>
/// <exception cref="NotSupportedException">The IO.Stream does not support locking.</exception>
/// <remarks>This method is not used and always throws the exception.</remarks>
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);
}
/// <summary>
/// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
/// </summary>
/// <param name="buffer">
/// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between
/// <paramref name="offset"/> and ( <paramref name="offset"/> + <paramref name="count"/> - 1) replaced by the bytes read from the
/// current source.
/// </param>
/// <param name="offset">
/// The zero-based byte offset in <paramref name="buffer"/> 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.
/// </param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin storing the data read from the current stream.</param>
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
/// <returns>
/// 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.
/// </returns>
/// <exception cref="System.ObjectDisposedException">iComStream</exception>
/// <exception cref="System.NotSupportedException">Offsets other than zero are not supported.</exception>
/// <exception cref="NotSupportedException">Only a zero offset is supported.</exception>
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.");
/// <summary>Reads a specified number of bytes from the stream object into memory starting at the current seek pointer.</summary>
/// <param name="pv">The buffer which the stream data is read into.</param>
/// <param name="cb">The number of bytes of data to read from the stream object.</param>
/// <param name="pcbRead">
/// 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.
/// </param>
/// <returns>The actual number of bytes read ( <paramref name="pcbRead"/>) from the source.</returns>
/// <exception cref="ArgumentException">The sum of offset and count is larger than the buffer length.</exception>
/// <exception cref="ArgumentNullException">buffer is a null reference.</exception>
/// <exception cref="ArgumentOutOfRangeException">offset or count is negative.</exception>
/// <exception cref="IOException">An I/O error occurs.</exception>
/// <exception cref="NotSupportedException">The stream does not support reading.</exception>
/// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
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;
/// <summary>Discards all changes that have been made to a transacted stream since the last stream.Commit call</summary>
/// <returns></returns>
/// <exception cref="NotSupportedException">The IO.Stream does not support reverting.</exception>
/// <remarks>This method is not used and always throws the exception.</remarks>
void IStream.Revert()
{
if (netStream is not null)
throw new NotSupportedException("Stream does not support reverting.");
comStream.Revert();
}
/// <summary>Sets the position within the current stream.</summary>
/// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param>
/// <param name="origin">
/// A value of type <see cref="T:System.IO.SeekOrigin"/> indicating the reference point used to obtain the new position.
/// </param>
/// <param name="offset">A byte offset relative to the origin parameter.</param>
/// <param name="origin">A value of type SeekOrigin indicating the reference point used to obtain the new position.</param>
/// <returns>The new position within the current stream.</returns>
/// <exception cref="System.ObjectDisposedException">iComStream</exception>
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;
/// <summary>
/// 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
/// </summary>
/// <param name="dlibMove">
/// 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.
/// </param>
/// <param name="dwOrigin">
/// 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).
/// </param>
/// <param name="plibNewPosition">
/// 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.
/// </param>
/// <returns>
/// Returns in <paramref name="plibNewPosition"/> the location where this method writes the value of the new seek pointer from the
/// beginning of the stream.
/// </returns>
/// <exception cref="IOException">An I/O error occurs.</exception>
/// <exception cref="NotSupportedException">The stream does not support reading.</exception>
/// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
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);
}
}
/// <summary>Sets the length of the current stream.</summary>
/// <param name="value">The desired length of the current stream in bytes.</param>
/// <exception cref="System.ObjectDisposedException">iComStream</exception>
/// <returns></returns>
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);
/// <summary>Changes the size of the stream object.</summary>
/// <param name="libNewSize">Specifies the new size of the stream as a number of bytes.</param>
/// <returns></returns>
/// <exception cref="IOException">An I/O error occurs.</exception>
/// <exception cref="NotSupportedException">The stream does not support reading.</exception>
/// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
void IStream.SetSize(long libNewSize) => SetLength(libNewSize);
/// <summary>Retrieves the STATSTG structure for this stream.</summary>
/// <param name="pstatstg">The STATSTG structure where this method places information about this stream object.</param>
/// <param name="grfStatFlag">
/// 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.
/// </param>
/// <returns></returns>
/// <exception cref="NotSupportedException">The stream does not support reading.</exception>
/// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
/// <remarks>The <paramref name="grfStatFlag"/> parameter is not used</remarks>
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);
}
}
/// <summary>Removes the access restriction on a range of bytes previously restricted with the LockRegion method.</summary>
/// <param name="libOffset">Specifies the byte offset for the beginning of the range.</param>
/// <param name="cb">Specifies, in bytes, the length of the range to be restricted.</param>
/// <param name="dwLockType">Specifies the access restrictions previously placed on the range.</param>
/// <returns></returns>
/// <exception cref="NotSupportedException">The IO.Stream does not support unlocking.</exception>
/// <remarks>This method is not used and always throws the exception.</remarks>
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);
}
/// <summary>
/// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written.
/// </summary>
/// <param name="buffer">
/// An array of bytes. This method copies <paramref name="count"/> bytes from <paramref name="buffer"/> to the current stream.
/// </param>
/// <param name="offset">
/// The zero-based byte offset in <paramref name="buffer"/> at which to begin copying bytes to the current stream.
/// </param>
/// <param name="buffer">An array of bytes. This method copies count bytes from buffer to the current stream.</param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
/// <param name="count">The number of bytes to be written to the current stream.</param>
/// <exception cref="System.ObjectDisposedException">iComStream</exception>
/// <exception cref="System.NotSupportedException">Offsets other than zero are not supported.</exception>
/// <returns></returns>
/// <exception cref="NotSupportedException">Only a zero offset is supported.</exception>
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);
}
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="T:System.IO.Stream"/> and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
protected override void Dispose(bool disposing)
/// <summary>Writes a specified number of bytes into the stream object starting at the current seek pointer.</summary>
/// <param name="pv">
/// 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.
/// </param>
/// <param name="cb">The number of bytes of data to attempt to write into the stream. This value can be zero.</param>
/// <param name="pcbWritten">
/// 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.
/// </param>
/// <returns>The actual number of bytes written ( <paramref name="pcbWritten"/>).</returns>
/// <exception cref="ArgumentException">The sum of offset and count is larger than the buffer length.</exception>
/// <exception cref="ArgumentNullException">buffer is a null reference.</exception>
/// <exception cref="ArgumentOutOfRangeException">offset or count is negative.</exception>
/// <exception cref="IOException">An I/O error occurs.</exception>
/// <exception cref="NotSupportedException">The IO.Stream does not support reading.</exception>
/// <exception cref="ObjectDisposedException">Methods were called after the stream was closed.</exception>
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);
}
}
/// <summary>Releases all resources used by the Stream object.</summary>
void IDisposable.Dispose() => Close();
}
}