diff --git a/OpenMcdf.sln b/OpenMcdf.sln
index d6e3a65c..23cafb86 100644
--- a/OpenMcdf.sln
+++ b/OpenMcdf.sln
@@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Test", "sources\Te
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenMcdf.Benchmark", "sources\Test\OpenMcdf.Benchmark\OpenMcdf.Benchmark.csproj", "{B3645D34-1E22-4BCC-8956-A8A56FA9F114}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StructuredStorage", "StructuredStorage\StructuredStorage.csproj", "{543A2F13-F9B1-4B9F-B982-EECC6F44F0E0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -103,6 +105,10 @@ Global
{B3645D34-1E22-4BCC-8956-A8A56FA9F114}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3645D34-1E22-4BCC-8956-A8A56FA9F114}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3645D34-1E22-4BCC-8956-A8A56FA9F114}.Release|Any CPU.Build.0 = Release|Any CPU
+ {543A2F13-F9B1-4B9F-B982-EECC6F44F0E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {543A2F13-F9B1-4B9F-B982-EECC6F44F0E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {543A2F13-F9B1-4B9F-B982-EECC6F44F0E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {543A2F13-F9B1-4B9F-B982-EECC6F44F0E0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/StructuredStorage/LockBytes.cs b/StructuredStorage/LockBytes.cs
new file mode 100644
index 00000000..dd514822
--- /dev/null
+++ b/StructuredStorage/LockBytes.cs
@@ -0,0 +1,42 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.System.Com.StructuredStorage;
+
+namespace StructuredStorage;
+
+///
+/// Encapsulates ILockBytes over an HGlobal allocation.
+///
+internal sealed class LockBytes : IDisposable
+{
+ readonly ILockBytes lockBytes;
+ private bool disposedValue;
+
+ public LockBytes(int count)
+ {
+ IntPtr hGlobal = Marshal.AllocHGlobal(count);
+ HRESULT hr = PInvoke.CreateILockBytesOnHGlobal((HGLOBAL)hGlobal, true, out lockBytes);
+ hr.ThrowOnFailure();
+ }
+
+ public void Dispose()
+ {
+ if (disposedValue)
+ return;
+
+ int count = Marshal.ReleaseComObject(lockBytes);
+ Debug.Assert(count == 0);
+
+ disposedValue = true;
+ GC.SuppressFinalize(this);
+ }
+
+ ~LockBytes()
+ {
+ Dispose();
+ }
+
+ internal ILockBytes ILockBytes => lockBytes;
+}
diff --git a/StructuredStorage/NativeMethods.json b/StructuredStorage/NativeMethods.json
new file mode 100644
index 00000000..66108bf1
--- /dev/null
+++ b/StructuredStorage/NativeMethods.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://aka.ms/CsWin32.schema.json",
+ "wideCharOnly": true,
+ "emitSingleFile": true,
+ "public": false
+}
diff --git a/StructuredStorage/NativeMethods.txt b/StructuredStorage/NativeMethods.txt
new file mode 100644
index 00000000..9a9892f0
--- /dev/null
+++ b/StructuredStorage/NativeMethods.txt
@@ -0,0 +1,37 @@
+// Structured storage
+
+// Functions
+CreateILockBytesOnHGlobal
+PropVariantToVariant
+ReadClassStg
+ReadClassStm
+SHCreateItemFromParsingName
+SHCreateStreamOnFile
+StgCreateDocfileOnILockBytes
+StgCreateStorageEx
+StgIsStorageFile
+StgOpenStorage
+StgOpenStorageEx
+VariantClear
+VariantToPropVariant
+WriteClassStg
+WriteClassStm
+
+// Interfaces
+IEnumSTATPROPSETSTG
+IEnumSTATPROPSTG
+IEnumSTATSTG
+ILockBytes
+IPropertySetStorage
+IPropertyStorage
+IRootStorage
+IStorage
+IStream
+
+// Enumerations
+PROPSETFLAG_*
+STGTY
+
+// Constants
+STG_E_*
+PROPSETFLAG_*
diff --git a/StructuredStorage/PropertySetStorage.cs b/StructuredStorage/PropertySetStorage.cs
new file mode 100644
index 00000000..7c6df922
--- /dev/null
+++ b/StructuredStorage/PropertySetStorage.cs
@@ -0,0 +1,50 @@
+using Windows.Win32;
+using Windows.Win32.System.Com.StructuredStorage;
+
+namespace StructuredStorage;
+
+///
+/// Wraps IPropertySetStorage.
+///
+public sealed class PropertySetStorage
+{
+ ///
+ /// PROPSETFLAG constants.
+ ///
+ [Flags]
+#pragma warning disable CA1008
+ public enum Flags
+ {
+ Default = (int)PInvoke.PROPSETFLAG_DEFAULT,
+ NonSimple = (int)PInvoke.PROPSETFLAG_NONSIMPLE,
+ ANSI = (int)PInvoke.PROPSETFLAG_ANSI,
+ Unbuffered = (int)PInvoke.PROPSETFLAG_UNBUFFERED,
+ CaseSensitive = (int)PInvoke.PROPSETFLAG_CASE_SENSITIVE,
+ }
+#pragma warning restore CA1008
+
+ private readonly IPropertySetStorage propSet; // Cast of IStorage does not need disposal
+
+ internal PropertySetStorage(IStorage storage)
+ {
+ propSet = (IPropertySetStorage)storage;
+ }
+
+ public PropertyStorage Create(Guid formatID, StorageModes mode) => Create(formatID, Flags.Default, mode, Guid.Empty);
+
+ public PropertyStorage Create(Guid formatID, Flags flags = Flags.Default, StorageModes mode = StorageModes.ShareExclusive | StorageModes.AccessReadWrite) => Create(formatID, flags, mode, Guid.Empty);
+
+ public unsafe PropertyStorage Create(Guid formatID, Flags flags, StorageModes mode, Guid classID)
+ {
+ propSet.Create(&formatID, &classID, (uint)flags, (uint)mode, out IPropertyStorage stg);
+ return new(stg);
+ }
+
+ public unsafe PropertyStorage Open(Guid formatID, StorageModes mode = StorageModes.ShareExclusive | StorageModes.AccessReadWrite)
+ {
+ propSet.Open(&formatID, (uint)mode, out IPropertyStorage propStorage);
+ return new(propStorage);
+ }
+
+ public unsafe void Remove(Guid formatID) => propSet.Delete(&formatID);
+}
diff --git a/StructuredStorage/PropertyStorage.cs b/StructuredStorage/PropertyStorage.cs
new file mode 100644
index 00000000..d713aca0
--- /dev/null
+++ b/StructuredStorage/PropertyStorage.cs
@@ -0,0 +1,167 @@
+using System.Collections;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.System.Com.StructuredStorage;
+
+namespace StructuredStorage;
+
+///
+/// Enumerates STATPROPSTG elements from a PropertyStorage.
+///
+internal sealed class StatPropStgEnumerator : IEnumerator
+{
+ readonly IEnumSTATPROPSTG enumerator;
+ STATPROPSTG propStat;
+
+ public STATPROPSTG Current => propStat;
+
+ object IEnumerator.Current => propStat;
+
+ public unsafe StatPropStgEnumerator(IPropertyStorage propertyStorage)
+ {
+ propertyStorage.Enum(out enumerator);
+ }
+
+ public unsafe void Dispose()
+ {
+ FreeName();
+
+ Marshal.ReleaseComObject(enumerator);
+ }
+
+ private unsafe void FreeName()
+ {
+ Marshal.FreeCoTaskMem((nint)propStat.lpwstrName.Value);
+ propStat.lpwstrName = null;
+ }
+
+ public unsafe bool MoveNext()
+ {
+ FreeName();
+
+ fixed (STATPROPSTG* statPtr = &propStat)
+ {
+ uint fetched;
+ enumerator.Next(1, statPtr, &fetched);
+ return fetched > 0;
+ }
+ }
+
+ public void Reset()
+ {
+ FreeName();
+
+ enumerator.Reset();
+ }
+}
+
+///
+/// Creates an enumerator for STATPROPSTG elements from a PropertyStorage.
+///
+internal sealed class StatPropStgCollection : IEnumerable
+{
+ readonly IPropertyStorage propertyStorage;
+
+ public StatPropStgCollection(IPropertyStorage propertyStorage)
+ {
+ this.propertyStorage = propertyStorage;
+ }
+
+ public IEnumerator GetEnumerator() => new StatPropStgEnumerator(propertyStorage);
+
+ IEnumerator IEnumerable.GetEnumerator() => new StatPropStgEnumerator(propertyStorage);
+}
+
+///
+/// Wraps IPropertyStorage.
+///
+public sealed class PropertyStorage : IDisposable
+{
+ private readonly IPropertyStorage propertyStorage;
+ private bool disposed;
+
+ internal unsafe PropertyStorage(IPropertyStorage propertyStorage)
+ {
+ this.propertyStorage = propertyStorage;
+ StatPropStgCollection = new(propertyStorage);
+
+ STATPROPSETSTG prop;
+ this.propertyStorage.Stat(&prop);
+ }
+
+ #region IDisposable Members
+
+ public void Dispose()
+ {
+ if (disposed)
+ return;
+
+ int count = Marshal.ReleaseComObject(propertyStorage);
+ Debug.Assert(count == 0);
+
+ disposed = true;
+ }
+
+ #endregion
+
+ internal StatPropStgCollection StatPropStgCollection { get; }
+
+ public void Flush(CommitFlags flags = CommitFlags.Default) => propertyStorage.Commit((uint)flags);
+
+ public unsafe void Remove(int propertyID)
+ {
+ PROPSPEC propspec = new()
+ {
+ ulKind = PROPSPEC_KIND.PRSPEC_PROPID,
+ Anonymous = new PROPSPEC._Anonymous_e__Union()
+ {
+ propid = (uint)propertyID,
+ },
+ };
+ propertyStorage.DeleteMultiple(1, &propspec);
+ }
+
+ public void Revert() => propertyStorage.Revert();
+
+ public unsafe object? this[int propertyID]
+ {
+ get
+ {
+ PROPSPEC spec = PropVariantExtensions.CreatePropSpec(PROPSPEC_KIND.PRSPEC_PROPID, propertyID);
+
+ var variants = new PROPVARIANT[1];
+ propertyStorage.ReadMultiple(1, &spec, variants);
+ HRESULT hr = PInvoke.PropVariantToVariant(variants[0], out object variant);
+ hr.ThrowOnFailure();
+ return variant;
+ }
+
+ set
+ {
+ PROPSPEC spec = PropVariantExtensions.CreatePropSpec(PROPSPEC_KIND.PRSPEC_PROPID, propertyID);
+
+ HRESULT hr = PInvoke.VariantToPropVariant(value, out PROPVARIANT pv);
+ hr.ThrowOnFailure();
+
+ PROPVARIANT[] pvs = [pv];
+ propertyStorage.WriteMultiple(1, &spec, pvs, 2);
+ }
+ }
+}
+
+static class PropVariantExtensions
+{
+ public static PROPSPEC CreatePropSpec(PROPSPEC_KIND kind, int propertyID)
+ {
+ return new PROPSPEC
+ {
+ ulKind = kind,
+ Anonymous = new PROPSPEC._Anonymous_e__Union
+ {
+ propid = (uint)propertyID,
+ },
+ };
+ }
+}
diff --git a/StructuredStorage/Storage.cs b/StructuredStorage/Storage.cs
new file mode 100644
index 00000000..13a21832
--- /dev/null
+++ b/StructuredStorage/Storage.cs
@@ -0,0 +1,343 @@
+using System.Collections;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.Security;
+using Windows.Win32.System.Com;
+using Windows.Win32.System.Com.StructuredStorage;
+
+namespace StructuredStorage;
+
+#pragma warning disable CA1069 // Enums values should not be duplicated
+#pragma warning disable CA1724 // Type names should not match namespaces
+#pragma warning disable CA1028 // Enum storage should be Int32
+#pragma warning disable CA1008 // Enums should have zero value
+
+///
+/// STGC constants.
+///
+[Flags]
+public enum CommitFlags : uint
+{
+ Default = STGC.STGC_DEFAULT,
+ Overwrite = STGC.STGC_OVERWRITE,
+ OnlyIfCurrent = STGC.STGC_ONLYIFCURRENT,
+ DangerouslyCommitMerelyToDiskCache = STGC.STGC_DANGEROUSLYCOMMITMERELYTODISKCACHE,
+ Consolidate = STGC.STGC_CONSOLIDATE,
+}
+
+///
+/// STGM constants.
+///
+[Flags]
+public enum StorageModes : uint
+{
+ FailIfThere = STGM.STGM_FAILIFTHERE,
+ Direct = STGM.STGM_DIRECT,
+ AccessRead = STGM.STGM_READ,
+ AccessWrite = STGM.STGM_WRITE,
+ AccessReadWrite = STGM.STGM_READWRITE,
+ ShareExclusive = STGM.STGM_SHARE_EXCLUSIVE,
+ ShareDenyWrite = STGM.STGM_SHARE_DENY_WRITE,
+ ShareDenyRead = STGM.STGM_SHARE_DENY_READ,
+ ShareDenyNone = STGM.STGM_SHARE_DENY_NONE,
+ Create = STGM.STGM_CREATE,
+ Transacted = STGM.STGM_TRANSACTED,
+ Convert = STGM.STGM_CONVERT,
+ Priority = STGM.STGM_PRIORITY,
+ NoScratch = STGM.STGM_NOSCRATCH,
+ NoSnapShot = STGM.STGM_NOSNAPSHOT,
+ DirectSWMR = STGM.STGM_DIRECT_SWMR,
+ DeleteOnRelease = STGM.STGM_DELETEONRELEASE,
+ ModeSimple = STGM.STGM_SIMPLE,
+}
+
+///
+/// Enumerates STATSTG elements from a Storage.
+///
+internal sealed class StatStgEnumerator : IEnumerator
+{
+ readonly IEnumSTATSTG enumerator;
+ STATSTG stat;
+
+ public STATSTG Current => stat;
+
+ object IEnumerator.Current => stat;
+
+ public unsafe StatStgEnumerator(IStorage storage)
+ {
+ storage.EnumElements(0, null, 0, out enumerator);
+ }
+
+ public unsafe void Dispose()
+ {
+ FreeName();
+
+ Marshal.ReleaseComObject(enumerator);
+ }
+
+ private unsafe void FreeName()
+ {
+ Marshal.FreeCoTaskMem((nint)stat.pwcsName.Value);
+ stat.pwcsName = null;
+ }
+
+ public unsafe bool MoveNext()
+ {
+ FreeName();
+
+ fixed (STATSTG* statPtr = &stat)
+ {
+ uint fetched;
+ enumerator.Next(1, statPtr, &fetched);
+ return fetched > 0;
+ }
+ }
+
+ public void Reset()
+ {
+ FreeName();
+
+ enumerator.Reset();
+ }
+}
+
+///
+/// Creates an enumerator for STATSTG elements from a Storage.
+///
+internal sealed class StatStgCollection : IEnumerable
+{
+ readonly IStorage storage;
+
+ public StatStgCollection(IStorage storage)
+ {
+ this.storage = storage;
+ }
+
+ public IEnumerator GetEnumerator() => new StatStgEnumerator(storage);
+
+ IEnumerator IEnumerable.GetEnumerator() => new StatStgEnumerator(storage);
+}
+
+///
+/// Wraps a COM structured storage object.
+///
+public sealed class Storage : IDisposable
+{
+ static readonly Guid IStorageGuid = typeof(IStorage).GUID;
+
+ static STGOPTIONS DefaultOptions => new()
+ {
+ usVersion = 2,
+ reserved = 0,
+ ulSectorSize = 4096,
+ pwcsTemplateFile = null,
+ };
+
+ readonly IStorage storage;
+ readonly LockBytes? lockBytes; // Prevents garbage collection of in-memory storage
+
+ public Storage? Parent { get; }
+
+ public PropertySetStorage PropertySetStorage { get; }
+
+ internal StatStgCollection StatStgCollection { get; }
+
+ bool disposed;
+
+ // Methods
+ internal Storage(IStorage storage, Storage? parent = null, LockBytes? lockBytes = null)
+ {
+ this.storage = storage;
+ Parent = parent;
+ this.lockBytes = lockBytes;
+ PropertySetStorage = new(storage);
+ StatStgCollection = new StatStgCollection(storage);
+ }
+
+ public static unsafe Storage Create(string fileName, StorageModes modes = StorageModes.ShareExclusive | StorageModes.AccessReadWrite)
+ {
+ STGOPTIONS opts = DefaultOptions;
+ HRESULT hr = PInvoke.StgCreateStorageEx(fileName, (STGM)modes, STGFMT.STGFMT_DOCFILE, 0, &opts, (PSECURITY_DESCRIPTOR)null, IStorageGuid, out void* ptr);
+ hr.ThrowOnFailure();
+
+ var iStorage = (IStorage)Marshal.GetObjectForIUnknown((nint)ptr);
+ Marshal.Release((nint)ptr);
+ return new(iStorage);
+ }
+
+ public static Storage CreateInMemory(int capacity)
+ {
+ LockBytes lockBytes = new(capacity);
+ HRESULT hr = PInvoke.StgCreateDocfileOnILockBytes(lockBytes.ILockBytes, STGM.STGM_READWRITE | STGM.STGM_SHARE_EXCLUSIVE | STGM.STGM_CREATE, 0, out IStorage storage);
+ hr.ThrowOnFailure();
+ return new(storage, null, lockBytes);
+ }
+
+ public static unsafe Storage Open(string fileName, StorageModes modes = StorageModes.ShareExclusive | StorageModes.AccessReadWrite)
+ {
+ STGOPTIONS opts = DefaultOptions;
+ HRESULT hr = PInvoke.StgOpenStorageEx(fileName, (STGM)modes, STGFMT.STGFMT_DOCFILE, 0, &opts, (PSECURITY_DESCRIPTOR)null, IStorageGuid, out void* ptr);
+ if (hr == HRESULT.STG_E_FILENOTFOUND)
+ throw new FileNotFoundException(null, fileName);
+ if (hr == HRESULT.STG_E_FILEALREADYEXISTS)
+ hr = HRESULT.STG_E_DOCFILECORRUPT;
+ hr.ThrowOnFailure();
+
+ var iStorage = (IStorage)Marshal.GetObjectForIUnknown((nint)ptr);
+ Marshal.Release((nint)ptr);
+ return new(iStorage);
+ }
+
+ #region IDisposable Members
+
+ public void Dispose()
+ {
+ if (disposed)
+ return;
+
+ int count = Marshal.ReleaseComObject(storage);
+ Debug.Assert(count == 0);
+
+ lockBytes?.Dispose();
+
+ disposed = true;
+ }
+
+ #endregion
+
+ public unsafe Storage CreateStorage(string name, StorageModes flags = StorageModes.Create | StorageModes.ShareExclusive | StorageModes.AccessReadWrite)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ fixed (char* namePtr = name)
+ {
+ storage.CreateStorage(namePtr, (STGM)flags, 0, 0, out IStorage childStorage);
+ return new Storage(childStorage, this);
+ }
+ }
+
+ public unsafe Stream CreateStream(string name, StorageModes flags = StorageModes.Create | StorageModes.ShareExclusive | StorageModes.AccessReadWrite)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ fixed (char* namePtr = name)
+ {
+ storage.CreateStream(namePtr, (STGM)flags, 0, 0, out IStream stm);
+ return new Stream(stm, this);
+ }
+ }
+
+ internal StatStgEnumerator CreateStatStgEnumerator() => new(storage);
+
+ public void DestroyElement(string name)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ storage.DestroyElement(name);
+ }
+
+ public void DestroyElementIfExists(string name)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ if (ContainsElement(name))
+ storage.DestroyElement(name);
+ }
+
+ public bool ContainsElement(string name)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ return StatStgCollection.Any(s => s.pwcsName.AsSpan().SequenceEqual(name));
+ }
+
+ public bool ContainsStream(string name)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ return StatStgCollection.Any(s => (STGTY)s.type == STGTY.STGTY_STREAM && s.pwcsName.AsSpan().SequenceEqual(name));
+ }
+
+ public void Commit(CommitFlags flags = CommitFlags.Default)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ storage.Commit((uint)flags);
+ }
+
+ public void MoveElement(string name, Storage destination) => MoveElement(name, destination, name);
+
+ public void MoveElement(string name, Storage destination, string newName)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ storage.MoveElementTo(name, destination.storage, newName, 0);
+ }
+
+ public unsafe Storage OpenStorage(string name, StorageModes flags = StorageModes.AccessReadWrite | StorageModes.ShareExclusive)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ fixed (char* namePtr = name)
+ {
+ storage.OpenStorage(namePtr, null, (STGM)flags, null, 0, out IStorage childStorage);
+ return new Storage(childStorage, this);
+ }
+ }
+
+ public unsafe Stream OpenStream(string name, StorageModes flags = StorageModes.AccessReadWrite | StorageModes.ShareExclusive)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ fixed (char* namePtr = name)
+ {
+ storage.OpenStream(namePtr, null, (STGM)flags, 0, out IStream iStream);
+ return new Stream(iStream, this);
+ }
+ }
+
+ public Stream OpenOrCreateStream(string name, StorageModes flags = StorageModes.AccessReadWrite | StorageModes.ShareExclusive)
+ => ContainsStream(name) ? OpenStream(name, flags) : CreateStream(name, flags);
+
+ public void Revert()
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ storage.Revert();
+ }
+
+ public unsafe void SwitchToFile(string fileName)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ fixed (char* fileNamePtr = fileName)
+ {
+ if (storage is not IRootStorage rootStorage)
+ throw new InvalidOperationException("Not file storage");
+ rootStorage.SwitchToFile(fileNamePtr);
+ }
+ }
+
+ // Properties
+ public Guid Id
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ HRESULT hr = PInvoke.ReadClassStg(storage, out Guid guid);
+ hr.ThrowOnFailure();
+ return guid;
+ }
+
+ set
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ HRESULT hr = PInvoke.WriteClassStg(storage, value);
+ hr.ThrowOnFailure();
+ }
+ }
+}
diff --git a/StructuredStorage/Stream.cs b/StructuredStorage/Stream.cs
new file mode 100644
index 00000000..4b1df3f4
--- /dev/null
+++ b/StructuredStorage/Stream.cs
@@ -0,0 +1,185 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.System.Com;
+
+namespace StructuredStorage;
+
+///
+/// Implements Stream on an COM IStream.
+///
+public sealed class Stream : System.IO.Stream
+{
+ public Storage Parent { get; }
+
+ readonly IStream stream;
+ bool disposed;
+
+ internal Stream(IStream stream, Storage parent)
+ {
+ this.stream = stream;
+ Parent = parent;
+
+ STGM mode = Stat.grfMode;
+ CanRead = mode.HasFlag(STGM.STGM_READWRITE) || !mode.HasFlag(STGM.STGM_WRITE);
+ CanWrite = mode.HasFlag(STGM.STGM_READWRITE) || mode.HasFlag(STGM.STGM_WRITE);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposed)
+ return;
+
+ if (disposing)
+ {
+ Flush();
+
+ int count = Marshal.ReleaseComObject(stream);
+ Debug.Assert(count == 0);
+ }
+
+ disposed = true;
+
+ base.Dispose(disposing);
+ }
+
+ public override void Flush() => Flush(CommitFlags.Default);
+
+ public void Flush(CommitFlags flags)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ stream.Commit((STGC)flags);
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ Span slice = buffer.AsSpan(offset, count);
+ return Read(slice);
+ }
+
+ public override unsafe int Read(Span buffer)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ fixed (byte* ptr = buffer)
+ {
+ uint read;
+ HRESULT hr = stream.Read(ptr, (uint)buffer.Length, &read);
+ hr.ThrowOnFailure();
+ return (int)read;
+ }
+ }
+
+ public void Revert()
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ stream.Revert();
+ }
+
+ public override unsafe long Seek(long offset, SeekOrigin origin)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ ulong pos;
+ stream.Seek(offset, origin, &pos);
+ return (long)pos;
+ }
+
+ public override void SetLength(long value)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ stream.SetSize((ulong)value);
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ ReadOnlySpan slice = buffer.AsSpan(offset, count);
+ Write(slice);
+ }
+
+ public override unsafe void Write(ReadOnlySpan buffer)
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ fixed (byte* ptr = buffer)
+ {
+ uint written;
+ HRESULT result = stream.Write(ptr, (uint)buffer.Length, &written);
+ result.ThrowOnFailure();
+ }
+ }
+
+ // Properties
+ public override bool CanRead { get; }
+
+ public override bool CanSeek => true;
+
+ public override bool CanWrite { get; }
+
+ public override long Length
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ return (long)Stat.cbSize;
+ }
+ }
+
+ public override unsafe long Position
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ ulong pos;
+ stream.Seek(0L, SeekOrigin.Current, &pos);
+ return (long)pos;
+ }
+
+ set
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ stream.Seek(value, SeekOrigin.Begin, null);
+ }
+ }
+
+ public Guid Id
+ {
+ get
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ HRESULT hr = PInvoke.ReadClassStm(stream, out Guid guid);
+ hr.ThrowOnFailure();
+ return guid;
+ }
+
+ set
+ {
+ ObjectDisposedException.ThrowIf(disposed, this);
+
+ HRESULT hr = PInvoke.WriteClassStm(stream, value);
+ hr.ThrowOnFailure();
+ }
+ }
+
+ internal unsafe STATSTG Stat
+ {
+ get
+ {
+ STATSTG stat;
+ stream.Stat(&stat, STATFLAG.STATFLAG_NONAME);
+ return stat;
+ }
+ }
+}
diff --git a/StructuredStorage/StructuredStorage.csproj b/StructuredStorage/StructuredStorage.csproj
new file mode 100644
index 00000000..40393c66
--- /dev/null
+++ b/StructuredStorage/StructuredStorage.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0-windows
+ enable
+ enable
+ 12.0
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+