From dca774e1363eab179a4b94dd25b9d14cf5470417 Mon Sep 17 00:00:00 2001 From: Jeremy Powell Date: Tue, 24 Sep 2024 12:51:47 +1200 Subject: [PATCH] Add Structure Storage reference implementation A thin wrapper around Windows Structured Storage using COM interop from CsWin32. --- OpenMcdf.sln | 6 + StructuredStorage/LockBytes.cs | 42 +++ StructuredStorage/NativeMethods.json | 6 + StructuredStorage/NativeMethods.txt | 37 +++ StructuredStorage/PropertySetStorage.cs | 50 +++ StructuredStorage/PropertyStorage.cs | 167 ++++++++++ StructuredStorage/Storage.cs | 343 +++++++++++++++++++++ StructuredStorage/Stream.cs | 185 +++++++++++ StructuredStorage/StructuredStorage.csproj | 18 ++ 9 files changed, 854 insertions(+) create mode 100644 StructuredStorage/LockBytes.cs create mode 100644 StructuredStorage/NativeMethods.json create mode 100644 StructuredStorage/NativeMethods.txt create mode 100644 StructuredStorage/PropertySetStorage.cs create mode 100644 StructuredStorage/PropertyStorage.cs create mode 100644 StructuredStorage/Storage.cs create mode 100644 StructuredStorage/Stream.cs create mode 100644 StructuredStorage/StructuredStorage.csproj 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..f49a54dc --- /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); + + int hr = PInvoke.WriteClassStm(stream, value); + Marshal.ThrowExceptionForHR(hr); + } + } + + 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 + + + +