Skip to content

Commit

Permalink
Add Structure Storage reference implementation
Browse files Browse the repository at this point in the history
A thin wrapper around Windows Structured Storage using COM interop
from CsWin32.
  • Loading branch information
jeremy-visionaid committed Sep 24, 2024
1 parent e2f52b5 commit dca774e
Show file tree
Hide file tree
Showing 9 changed files with 854 additions and 0 deletions.
6 changes: 6 additions & 0 deletions OpenMcdf.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions StructuredStorage/LockBytes.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Encapsulates <c>ILockBytes</c> over an HGlobal allocation.
/// </summary>
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;
}
6 changes: 6 additions & 0 deletions StructuredStorage/NativeMethods.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"$schema": "https://aka.ms/CsWin32.schema.json",
"wideCharOnly": true,
"emitSingleFile": true,
"public": false
}
37 changes: 37 additions & 0 deletions StructuredStorage/NativeMethods.txt
Original file line number Diff line number Diff line change
@@ -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_*
50 changes: 50 additions & 0 deletions StructuredStorage/PropertySetStorage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Windows.Win32;
using Windows.Win32.System.Com.StructuredStorage;

namespace StructuredStorage;

/// <summary>
/// Wraps <c>IPropertySetStorage</c>.
/// </summary>
public sealed class PropertySetStorage
{
/// <summary>
/// PROPSETFLAG constants.
/// </summary>
[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);
}
167 changes: 167 additions & 0 deletions StructuredStorage/PropertyStorage.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Enumerates <c>STATPROPSTG</c> elements from a <c>PropertyStorage</c>.
/// </summary>
internal sealed class StatPropStgEnumerator : IEnumerator<STATPROPSTG>
{
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();
}
}

/// <summary>
/// Creates an enumerator for <c>STATPROPSTG</c> elements from a <c>PropertyStorage</c>.
/// </summary>
internal sealed class StatPropStgCollection : IEnumerable<STATPROPSTG>
{
readonly IPropertyStorage propertyStorage;

public StatPropStgCollection(IPropertyStorage propertyStorage)
{
this.propertyStorage = propertyStorage;
}

public IEnumerator<STATPROPSTG> GetEnumerator() => new StatPropStgEnumerator(propertyStorage);

IEnumerator IEnumerable.GetEnumerator() => new StatPropStgEnumerator(propertyStorage);
}

/// <summary>
/// Wraps <c>IPropertyStorage</c>.
/// </summary>
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,
},
};
}
}
Loading

0 comments on commit dca774e

Please sign in to comment.