Extracting metadata from Windows Property System – Part I

Windows Property System (WPS) encapsulates potentially dozens of metadata entries for shell items. Some of information provided from these entries can be observed by right clicking on the column header in detail view of Windows Explorer and selecting the desired properties. Depending on the particular item, these entries can range from ‘Number of audio channels’ to ‘Thumbnails’ and from ‘Modified Date’ to ‘Message CC Name’.

In this article, I will show you how you can extract some of these metadata using IShellItem2 interface and shell’s SHCreateItemFromParsingName method in C# and WPF. Please note that, both the interface and the API method require Windows Vista/Server 2008 or above.

Windows API Declarations

We will start with the declaration of IShellItem2 interface which will be used to extract metadata from WPS. Add a class into your project and name it ‘IShellItem2.cs’ and paste the following declarations into your code: (Please note that you can place the code in any file you like, but I like to keep them in individual files to reduce clutter. So this is merely a suggestion).

#region IShellItem2
[
ComImport,
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"),
]
public interface IShellItem
{
[return: MarshalAs(UnmanagedType.Interface)]
object BindToHandler(IBindCtx pbc, [In] ref Guid bhid, [In] ref Guid riid);

IShellItem GetParent();

[return: MarshalAs(UnmanagedType.LPWStr)]
string GetDisplayName(SIGDN sigdnName);

uint GetAttributes(SFGAO sfgaoMask);

int Compare(IShellItem psi, uint hint);
}

[
ComImport,
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("7E9FB0D3-919F-4307-AB2E-9B1860310C93"),
]
internal interface IShellItem2 : IShellItem
{
[return: MarshalAs(UnmanagedType.Interface)]
new object BindToHandler(IBindCtx pbc, [In] ref Guid bhid, [In] ref Guid riid);
new IShellItem GetParent();
[return: MarshalAs(UnmanagedType.LPWStr)]
new string GetDisplayName(SIGDN sigdnName);

new SFGAO GetAttributes(SFGAO sfgaoMask);

new int Compare(IShellItem psi, uint hint);

[return: MarshalAs(UnmanagedType.Interface)]
object GetPropertyStore(
uint flags,
[In] ref Guid riid);

[return: MarshalAs(UnmanagedType.Interface)]
object GetPropertyStoreWithCreateObject(
uint flags,
[MarshalAs(UnmanagedType.IUnknown)] object punkCreateObject,
[In] ref Guid riid);

[return: MarshalAs(UnmanagedType.Interface)]
object GetPropertyStoreForKeys(
IntPtr rgKeys,
uint cKeys,
uint flags,
[In] ref Guid riid);

[return: MarshalAs(UnmanagedType.Interface)]
object GetPropertyDescriptionList(
IntPtr keyType,
[In] ref Guid riid);

void Update(IBindCtx pbc);

[SecurityCritical]
void GetProperty(ref PROPERTYKEY key, [In, Out] PROPVARIANT pv);

Guid GetCLSID(ref PROPERTYKEY key);

System.Runtime.InteropServices.ComTypes.FILETIME GetFileTime(ref PROPERTYKEY key);

int GetInt32(ref PROPERTYKEY key);

[return: MarshalAs(UnmanagedType.LPWStr)]
string GetString(ref PROPERTYKEY key);

uint GetUInt32(ref PROPERTYKEY key);

ulong GetUInt64(ref PROPERTYKEY key);

[return: MarshalAs(UnmanagedType.Bool)]
bool GetBool(ref PROPERTYKEY key);
}
#endregion

Copied to clipboard!

We also need to define a couple of associated flags. Below is the declaration of these flags. You can paste these flags into the ‘IShellItem2.cs’ file above or alternatively into a new class file. In the case of the latter, ensure that flags and IShellItem2 declarations are within the same namespace.

#region IShellItem Flags
// for details see:
// https://docs.microsoft.com/en-us/windows/desktop/shell/sfgao
[Flags]
public enum SFGAO : uint
{
CANCOPY = 0x00000001,
CANMOVE = 0x00000002,
CANLINK = 0x00000004,
STORAGE = 0x00000008,
CANRENAME = 0x00000010,
CANDELETE = 0x00000020,
HASPROPSHEET = 0x00000040,
DROPTARGET = 0x00000100,
CAPABILITYMASK = 0x00000177,
SYSTEM = 0x00001000,
ENCRYPTED = 0x00002000,
ISSLOW = 0x00004000,
GHOSTED = 0x00008000,
LINK = 0x00010000,
SHARE = 0x00020000,
READONLY = 0x00040000,
HIDDEN = 0x00080000,
DISPLAYATTRMASK = 0x000FC000,
FILESYSANCESTOR = 0x10000000,
FOLDER = 0x20000000,
FILESYSTEM = 0x40000000,
HASSUBFOLDER = 0x80000000,
CONTENTSMASK = 0x80000000,
VALIDATE = 0x01000000,
REMOVABLE = 0x02000000,
COMPRESSED = 0x04000000,
BROWSABLE = 0x08000000,
NONENUMERATED = 0x00100000,
NEWCONTENT = 0x00200000,
CANMONIKER = 0x00400000,
HASSTORAGE = 0x00400000,
STREAM = 0x00400000,
STORAGEANCESTOR = 0x00800000,
STORAGECAPMASK = 0x70C50008,
PKEYSFGAOMASK = 0x81044000,
}
// for details see:
// https://docs.microsoft.com/en-us/windows/desktop/api/shobjidl_core/ne-shobjidl_core-_sigdn
public enum SIGDN : uint
{
NORMAL = 0x00000000,
PARENTRELATIVEPARSING = 0x80018001,
DESKTOPABSOLUTEPARSING = 0x80028000,
PARENTRELATIVEEDITING = 0x80031001,
DESKTOPABSOLUTEEDITING = 0x8004c000,
FILESYSPATH = 0x80058000,
URL = 0x80068000,
PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
PARENTRELATIVE = 0x80080001,
}
#endregion

We also need to define three structures for our API calls. The first two describe the PROPVARIANT and PROPARRAY structures. The explanation of these structures is beyond the scope of this article, hence won’t be covered here. I suggest you place the code below into a class named ‘PropVariant.cs’.

[StructLayout(LayoutKind.Explicit, Pack = 1)]
struct PROPVARIANT
{
[FieldOffset(0)]
public System.Runtime.InteropServices.VarEnum VarType;
[FieldOffset(2)]
public ushort wReserved1;
[FieldOffset(4)]
public ushort wReserved2;
[FieldOffset(6)]
public ushort wReserved3;

[FieldOffset(8)]
public byte bVal;
[FieldOffset(8)]
public sbyte cVal;
[FieldOffset(8)]
public ushort uiVal;
[FieldOffset(8)]
public short iVal;
[FieldOffset(8)]
public UInt32 uintVal;
[FieldOffset(8)]
public Int32 intVal;
[FieldOffset(8)]
public UInt64 ulVal;
[FieldOffset(8)]
public Int64 lVal;
[FieldOffset(8)]
public float fltVal;
[FieldOffset(8)]
public double dblVal;
[FieldOffset(8)]
public short boolVal;
[FieldOffset(8)]
public IntPtr pclsidVal;
[FieldOffset(8)]
public IntPtr pszVal;
[FieldOffset(8)]
public IntPtr pwszVal;
[FieldOffset(8)]
public IntPtr punkVal;
[FieldOffset(8)]
public PROPARRAY ca;
[FieldOffset(8)]
public System.Runtime.InteropServices.ComTypes.FILETIME filetime;
}


The final structure we need to define is the PROPERTYKEY struct. Please note that the structure itself has only two fields; two constructor and ToString methods are optional.
[StructLayout(LayoutKind.Sequential, Pack = 4)]
public struct PROPERTYKEY
{
public Guid fmtid;
public uint pid;
public PROPERTYKEY(Guid g, uint id)
{
fmtid = g;
pid = id;
}
public PROPERTYKEY(string sGuid, uint id)
{
fmtid = new Guid(sGuid);
pid = id;
}
public new string ToString()
{
return this.fmtid.ToString() + "::" + this.pid.ToString();
}
}

We will mainly be using this structure to extract metadata, hence we will have more to say about it in subsequent sections.
Final piece of declaration we need is for the SHCreateItemFromParsingName method of shell32. This is the only API call we will need for all our extractions. This method should be placed in a static class named NativeMethods (as suggested).
static class NativeMethods
{
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
[return: MarshalAs(UnmanagedType.Interface)]
public static extern IShellItem2 SHCreateItemFromParsingName(
[In][MarshalAs(UnmanagedType.LPWStr)] string pszPath,
[In] IntPtr pbc,
[In][MarshalAs(UnmanagedType.LPStruct)] Guid riid);
}

That is it for the API declarations. Please continue on to Part 2.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: