dahall / Vanara

A set of .NET libraries for Windows implementing PInvoke calls to many native Windows APIs with supporting wrappers.
MIT License
1.79k stars 193 forks source link

Read-only ShellItemPropertyStore throws System.Runtime.InteropServices.COMException - GPS_BESTEFFORT flag resolves issue #378

Closed matthijsr closed 1 year ago

matthijsr commented 1 year ago

Describe the bug and how to reproduce

Sometimes, attempting to retrieve a property from a ShellItem will result in a System.Runtime.InteropServices.COMException. (Using ShellItem.Properties.GetValueOrDefault)

I've noticed this happening with .exe files most often (e.g., reading the items in a WinPython or Adobe Acrobat Reader install directory), though it's not consistent and I haven't been able to find the common thread between them.

No current configuration of ShellItemPropertyStore seems to resolve the issue. However, creating a read-only PropertyStore with the GPS_BESTEFFORT flag (instead of the current GPS_DEFAULT) does work.

What code is involved

https://github.com/dahall/Vanara/blob/master/Windows.Shell.Common/ShellProperties/ShellItemPropertyStore.cs

For comparison, the following code results in a COMException* when tested against certain files (for instance, AdobeGenuineSlimInstaller.exe from an Acrobat Reader DC installation):

    public static IDictionary<string, string> GetProperties1(string path)
    {
        var result = new Dictionary<string, string>();

        using var item = new ShellItem(path);
        using var propertyDescriptions = item.GetPropertyDescriptionList();
        foreach (var description in propertyDescriptions.Where(description => item.Properties.ContainsKey(description.PropertyKey)))
        {
            var key = description.CanonicalName;
            var value = item.Properties.GetValueOrDefault(description.PropertyKey)?.ToString();
            if (!string.IsNullOrEmpty(value))
            {
                result.Add(key, value);
            }
        }

        return result;
    }

Whereas this does not:

    public static IDictionary<string, string> GetProperties2(string path)
    {
        var result = new Dictionary<string, string>();

        using var item = new ShellItem(path);
        using var propertyDescriptions = item.GetPropertyDescriptionList();

        var propertyStore = ((IShellItem2)item.IShellItem).GetPropertyStore(GETPROPERTYSTOREFLAGS.GPS_BESTEFFORT, typeof(IPropertyStore).GUID);

        foreach (var description in propertyDescriptions)
        {
            var key = description.CanonicalName;

            using var pv = new PROPVARIANT();
            propertyStore.GetValue(description.PropertyKey, pv);
            var value = description.FormatForDisplay(pv, PROPDESC_FORMAT_FLAGS.PDFF_DEFAULT);

            if (key != null && !string.IsNullOrEmpty(value))
            {
                result.Add(key, value);
            }
        }

        Marshal.ReleaseComObject(propertyStore);

        return result;
    }

*The specific exception is:

System.Runtime.InteropServices.COMException : The specified resource type cannot be found in the image file. (0x80070715)
Stack Trace:
     at Vanara.PInvoke.Shell32.IShellItem2.GetPropertyStore(GETPROPERTYSTOREFLAGS flags, Guid& riid)
     at Vanara.Windows.Shell.ShellItemPropertyStore.GetIPropertyStore()
     at Vanara.Windows.Shell.ReadOnlyPropertyStore.Run[T](Func`2 action)
     at Vanara.Windows.Shell.ReadOnlyPropertyStore.get_Keys()
     at Vanara.Windows.Shell.ReadOnlyPropertyStore.ContainsKey(PROPERTYKEY key)

Expected behavior

The intention of this code was to replace a PowerShell script which performed ShellFolder.GetDetailsOf calls. GetDetailsOf would consistently succeed, where ShellItem.Properties would throw an exception.

I would prefer ShellItemPropertyStore to safely retrieve properties, or at least provide a means of setting the GPS_BESTEFFORT flag without also setting ReadOnly to false.

dahall commented 1 year ago

Since you have this running, would you mind testing the following?

using var item = new ShellItem(path);
using var propertyDescriptions = item.GetPropertyDescriptionList();
propertyDescriptions.ReadOnly = false;
foreach (var description in ...
matthijsr commented 1 year ago

I'm assuming you mean:

        using var item = new ShellItem(path);
        using var propertyDescriptions = item.GetPropertyDescriptionList();
        item.Properties.ReadOnly = false;
        foreach (var description in ...

Since there's no ReadOnly property on PropertyDescriptionList*.

This throws:

System.Runtime.InteropServices.COMException : Access Denied. (0x80030005 (STG_E_ACCESSDENIED))
Stack Trace:
     at Vanara.PInvoke.Shell32.IShellItem2.GetPropertyStore(GETPROPERTYSTOREFLAGS flags, Guid& riid)
     at Vanara.Windows.Shell.ShellItemPropertyStore.GetIPropertyStore()
     at Vanara.Windows.Shell.ReadOnlyPropertyStore.Run[T](Func`2 action)
     at Vanara.Windows.Shell.ReadOnlyPropertyStore.get_Keys()
     at Vanara.Windows.Shell.ReadOnlyPropertyStore.ContainsKey(PROPERTYKEY key)

For that same file.

*(That's on me, really. Sorry for the slightly wonky example using keys from PropertyDescriptionList to access the ShellItemPropertyStore - I was working backwards from some other attempts at resolving the issue when I wrote that example.)

Some other things I've tried:

  1. The property filter lists in PROPERTYKEY.System.PropList. None worked.
  2. Putting a try-catch block around each Get to see if specific keys were triggering the issue (this is where the roundabout PropertyDescriptionList solution came from). But it seems to be all-or-nothing for this specific issue.
dahall commented 1 year ago

I made a change to ShellItemPropertyStore so it defaults to GPS_BESTEFFORT instead of GPS_DEFAULT and it now works with your first example w/o exceptions.