Jump to content
Sign in to follow this  
0x8000FFFF

Parse PIDL from Name located on portable device

Recommended Posts

Posted (edited)

There are at least 4 APIs ta parse names as PIDL or IShellItem in Shell Namespace:

 

They all work fine for regular file system items, but fail to parse names to files and folders an portable devices. I obtain these names by letting the user to select a file in TVirtualExplorerEasyListview control and calling SHGetNameFromIDList with selected (absolute) PIDL and SIGDN_DESKTOPABSOLUTEPARSING. I store the name as string because the application comes back to it later (possibly after the application was closed and opened again) to process the file. At that point I need to parse PIDL from Name and bind it to IStream object to read the content of the file. It makes me sad that I can't restore PIDL from parsing names of some items. The typical parsing name is

::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SID-{20001,,31611420672}\{703540AE-0000-0000-0000-000000000000}\{0AE02E9E-0000-0000-0000-000000000000}

which consists of these parts:

  • ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}
    This PC
  • \\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}
    Redmi 6 (device name)
  • SID-{20001,,31611420672}
    SD card
  • {703540AE-0000-0000-0000-000000000000}
    DCIM
  • {0AE02E9E-0000-0000-0000-000000000000}
    Camera

 

I came across this thread which mentions that there's something broken in Windows 10 ver. 1703 (Creators Update). It also pointed my attention to the part that starts with SID-. When I replace this part with its display name (SD card), the parsing APIs start to work.  I created this sample (commandline) application to test the parsing APIs:

program Parse;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Win.ComObj, Winapi.Windows, Winapi.ActiveX, Winapi.ShlObj;

type
  TParseFunc = function(const Name: string): PItemIDList;

function GetBindCtx: IBindCtx;
begin
  Result := nil;
end;

function ParseUsingSHParseDisplayName(const Name: string): PItemIDList;
begin
  OLECheck(SHParseDisplayName(PChar(Name), GetBindCtx, Result, 0, PULONG(nil)^));
end;

function ParseUsingSHCreateItemFromParsingName(const Name: string): PItemIDList;
var
  ShellItem: IShellItem;
begin
  OleCheck(SHCreateItemFromParsingName(PChar(Name), GetBindCtx, IShellItem, ShellItem));
  OleCheck(SHGetIDListFromObject(ShellItem, Result));
end;

function ParseUsingDesktopParseDisplayName(const Name: string): PItemIDList;
var
  Desktop: IShellFolder;
begin
  OleCheck(SHGetDesktopFolder(Desktop));
  OleCheck(Desktop.ParseDisplayName(0, GetBindCtx, PChar(Name), PULONG(nil)^, Result, PULONG(nil)^));
end;

function ParseUsingILCreateFromPath(const Name: string): PItemIDList;
begin
  Result := ILCreateFromPath(PChar(Name));
  if not Assigned(Result) then
    RaiseLastOSError;
end;

procedure TestParse(const MethodName, Name: string; Parse: TParseFunc);
var
  PIDL: PItemIDList;
  PName: PWideChar;
begin
  Writeln(MethodName);
  try
    PIDL := Parse(Name);
    try
      if Succeeded(SHGetNameFromIDList(PIDL, Integer(SIGDN_DESKTOPABSOLUTEEDITING), PName)) then
      begin
        Writeln('Display name: ', PName);
        CoTaskMemFree(PName);
      end;
      if Succeeded(SHGetNameFromIDList(PIDL, Integer(SIGDN_DESKTOPABSOLUTEPARSING), PName)) then
      begin
        Writeln('Parsing name: ', PName);
        CoTaskMemFree(PName);
      end;
    finally
      CoTaskMemFree(PIDL);
    end;
  except
    on E: Exception do
      Writeln('[', E.ClassName, '] ', E.Message);
  end;
  Writeln;
end;

procedure Main;
var
  Name: string;
begin
  Name := ParamStr(1);
  Writeln('Name: ', Name);
  TestParse('SHParseDisplayName', Name, ParseUsingSHParseDisplayName);
  TestParse('SHCreateItemFromParsingName', Name, ParseUsingSHCreateItemFromParsingName);
  TestParse('Desktop.ParseDisplayName', Name, ParseUsingDesktopParseDisplayName);
  TestParse('ILCreateFromPath', Name, ParseUsingILCreateFromPath);
end;

begin
  CoInitialize(nil);
  Main;
  CoUninitialize;
end.

This is what I got with various names on input:

Parse.exe

Quote

Name:
SHParseDisplayName
Display name: This PC
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}

 

SHCreateItemFromParsingName
Display name: This PC
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}

 

Desktop.ParseDisplayName
Display name: This PC
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}

 

ILCreateFromPath
Display name: This PC
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}

Parse.exe C:\

Quote

Name: C:\
SHParseDisplayName
Display name: C:\
Parsing name: C:\

 

SHCreateItemFromParsingName
Display name: C:\
Parsing name: C:\

 

Desktop.ParseDisplayName
Display name: C:\
Parsing name: C:\

 

ILCreateFromPath
Display name: C:\
Parsing name: C:\

Parse.exe "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}"

Quote

Name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}
SHParseDisplayName
Display name: This PC\Redmi 6
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}

 

SHCreateItemFromParsingName
Display name: This PC\Redmi 6
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}

 

Desktop.ParseDisplayName
Display name: This PC\Redmi 6
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}

 

ILCreateFromPath
Display name: This PC\Redmi 6
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}

Parse.exe "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SID-{20001,,31611420672}"

Quote

Name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SID-{20001,,31611420672}
SHParseDisplayName
[EOleSysError] The parameter is incorrect

 

SHCreateItemFromParsingName
[EOleSysError] The parameter is incorrect

 

Desktop.ParseDisplayName
[EOleSysError] The parameter is incorrect

 

ILCreateFromPath
[Exception] ILCreateFromPath failed.

Parse.exe "::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SD card"

Quote

Name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SD card
SHParseDisplayName
Display name: This PC\Redmi 6\SD card
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SID-{20001,,31611420672}

 

SHCreateItemFromParsingName
Display name: This PC\Redmi 6\SD card
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SID-{20001,,31611420672}

 

Desktop.ParseDisplayName
Display name: This PC\Redmi 6\SD card
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SID-{20001,,31611420672}

 

ILCreateFromPath
Display name: This PC\Redmi 6\SD card
Parsing name: ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SID-{20001,,31611420672}

At this point if I use folder name (DCIM\Camera) or GUID ({703540AE-0000-0000-0000-000000000000}\{0AE02E9E-0000-0000-0000-000000000000}) from original parsing name beyond SD card, all of that will work. There is also Internal storage at the same level as SD card, which has name for parsing SID-{10001,,25710370816}. Parsing APIs fail to parse this name either. I have also tried other devices with the same results. I don't have pre-1703 Windows 10 or older Windows system at hand to try that, but I want my application to work on any Windows 7+ platform.

 

Can anybody explain what is going on here or point me to some relevant resources?

 
Edited by 0x8000FFFF

Share this post


Link to post

strange activity you are performing. for myself havent tried it direct like you.

if you like, you can try my FileShell tool on your device, it is basical a PIDL walker.

Share this post


Link to post
Posted (edited)

I wasn't able to identify the culprit, so I had to implement my own method for parsing PIDLs from names. There were two challenges involved:

  1. Split the name (path) of shell item into parts.
  2. Resolve relative PIDL of each part.

Splitting the name by path delimiter isn't as easy as it may seem - especially when one needs to deal with ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}. I created simple enumerator for that.

 

To get relative PIDL of each part the parsing method uses IShellFolder.ParseDisplayName. If that fails (e.g. SID-{20001,,31611420672}) it uses IShellFolder::EnumObjects to find the part either by display name or parsing name. Here's the complete code:

unit ShellUtils;

interface

uses
  System.SysUtils, System.Win.ComObj, Winapi.Windows, Winapi.ActiveX, Winapi.ShlObj, Winapi.ShLwApi;

type
  TNamePartsEnumerator = record
  private
    FName: PChar;
    FStart: PChar;
    FEnd: PChar;
    function GetCurrent: string;
    function GetEnumerated: string;
    function GetTail: string;
  public
    constructor Create(Name: PChar);
    function GetEnumerator: TNamePartsEnumerator;
    function MoveNext: Boolean;
    procedure Reset;

    property Current: string read GetCurrent;
    property Enumerated: string read GetEnumerated;
    property Tail: string read GetTail;
  end;

  TParseResult = record
  private
    FError: string;
    FOleError: HRESULT;
    FPIDL: PItemIDList;
  public
    procedure RaiseException;
    property Error: string read FError;
    property OleError: HRESULT read FOleError;
    property PIDL: PItemIDList read FPIDL;
  end;

function ParseDisplayName(const Name: string): PItemIDList;
function TryParseDisplayName(const Name: string; out ParseResult: TParseResult): Boolean; overload;
function TryParseDisplayName(const Name: string; out PIDL: PItemIDList): Boolean; overload;

implementation

{ TNamePartsEnumerator }

{$POINTERMATH ON}

constructor TNamePartsEnumerator.Create(Name: PChar);
begin
  FName := Name;
  Reset;
end;

function TNamePartsEnumerator.GetCurrent: string;
begin
  if Assigned(FStart) then
    SetString(Result, FStart, FEnd - FStart)
  else
    Result := string.Empty;
end;

function TNamePartsEnumerator.GetEnumerated: string;
begin
  SetString(Result, FName, FEnd - FName);
end;

function TNamePartsEnumerator.GetEnumerator: TNamePartsEnumerator;
begin
  Result := Self;
end;

function TNamePartsEnumerator.GetTail: string;
begin
  Result := FEnd;
end;

function TNamePartsEnumerator.MoveNext: Boolean;
var
  NewStart: PChar;
begin
  if (not Assigned(FEnd)) or (FEnd^ = #0) then
    Exit(False);
  if Assigned(FStart) then
    NewStart := FEnd + 1
  else
    NewStart := FEnd;
  // special cases: \\?\ and \\.\
  if (NewStart^ = '\') and ((NewStart + 1)^ = '\') and (((NewStart + 2)^ = '?') or ((NewStart + 2)^ = '.')) and ((NewStart + 1)^ = '\') then
    FEnd :=  NewStart + 4
  else
  begin
    // skip consecutive path delimiters
    while NewStart^ = '\' do
      Inc(NewStart);
    FEnd := NewStart;
    if FEnd^ = #0  then
      Exit(False);
  end;
  FStart := NewStart;
  while (FEnd^ <> #0) and (FEnd^ <> '\') do
    FEnd := CharNext(FEnd);
  Result := True;
end;

procedure TNamePartsEnumerator.Reset;
begin
  FStart := nil;
  FEnd := FName;
end;

{$POINTERMATH OFF}

{ TParseResult }

procedure TParseResult.RaiseException;
begin
  if not FError.IsEmpty then
    raise Exception.Create(FError);
  OleCheck(FOleError);
end;

function GetDisplayName(const Folder: IShellFolder; PIDL: PItemIDList; SHGDNFlags: DWORD): string;
var
  StrRet: TStrRet;
  DisplayName: PChar;
begin
  if Succeeded(Folder.GetDisplayNameOf(PIDL, SHGDNFlags, StrRet)) and Succeeded(StrRetToStr(@StrRet, PIDL, DisplayName)) then
  begin
    Result := DisplayName;
    CoTaskMemFree(DisplayName);
  end
  else
    Result := '';
end;

function ParseDisplayName(const Name: string): PItemIDList;
var
  ParseResult: TParseResult;
begin
  if not TryParseDisplayName(Name, ParseResult) then
    ParseResult.RaiseException;
  Result := ParseResult.PIDL;
end;

function TryParseDisplayName(const Name: string; out ParseResult: TParseResult): Boolean;
const
  SHCONTF_INCLUDESUPERHIDDEN = $10000;
var
  Folder, Subfolder: IShellFolder;
  Enumerator: TNamePartsEnumerator;
  RelativePIDL, CombinedPIDL: PItemIDList;
  EnumIDList: IEnumIDList;
  Part: string;
begin
  Result := False;
  ParseResult := Default(TParseResult);
  // take short path, if possible
  if Succeeded(SHParseDisplayName(PChar(Name), nil, ParseResult.FPIDL, 0, PDWORD(nil)^)) then
  begin
    Result := True;
    Exit;
  end;

  ParseResult.FOleError := SHGetDesktopFolder(Folder);
  if not Succeeded(ParseResult.FOleError) then
    Exit;
  ParseResult.FOleError := SHGetIDListFromObject(Folder, ParseResult.FPIDL);
  if not Succeeded(ParseResult.FOleError) then
    Exit;
  try
    Enumerator := TNamePartsEnumerator.Create(PChar(Name));
    while Enumerator.MoveNext do
    begin
      Part := Enumerator.Current;
      if not Succeeded(Folder.ParseDisplayName(0, nil, PChar(Part), PULONG(nil)^, RelativePIDL, PULONG(nil)^)) then
      begin
        RelativePIDL := nil;
        ParseResult.FOleError := Folder.EnumObjects(0, SHCONTF_FOLDERS or SHCONTF_INCLUDEHIDDEN or SHCONTF_NONFOLDERS or SHCONTF_STORAGE or SHCONTF_INCLUDESUPERHIDDEN, EnumIDList);
        if not Succeeded(ParseResult.FOleError) then
          Exit;
        while EnumIDList.Next(1, RelativePIDL, PULONG(nil)^) = S_OK do
        begin
          if SameFileName(Part, GetDisplayName(Folder, RelativePIDL, SHGDN_INFOLDER or SHGDN_FORPARSING)) or
             SameFileName(Part, GetDisplayName(Folder, RelativePIDL, SHGDN_INFOLDER or SHGDN_FORADDRESSBAR)) then
            Break;
          CoTaskMemFree(RelativePIDL);
          RelativePIDL := nil;
        end;
      end;
      if not Assigned(RelativePIDL) then
      begin
        ParseResult.FError := 'Can''t find ' + Enumerator.Enumerated;
        Exit;
      end;
      try
        CombinedPIDL := ILCombine(ParseResult.FPIDL, RelativePIDL);
        CoTaskMemFree(ParseResult.FPIDL);
        ParseResult.FPIDL := CombinedPIDL;
        if not Enumerator.Tail.IsEmpty then
        begin
          ParseResult.FOleError := Folder.BindToObject(RelativePIDL, nil, IShellFolder, Subfolder);
          if not Succeeded(ParseResult.FOleError) then
            Exit;
          Folder := Subfolder;
        end;
      finally
        CoTaskMemFree(RelativePIDL);
      end;
    end;
    Result := True;
  finally
    // release intermediate PIDL in case of failure
    if (not Result) and Assigned(ParseResult.FPIDL) then
    begin
      CoTaskMemFree(ParseResult.FPIDL);
      ParseResult.FPIDL := nil;
    end;
  end;
end;

function TryParseDisplayName(const Name: string; out PIDL: PItemIDList): Boolean;
var
  ParseResult: TParseResult;
begin
  Result := TryParseDisplayName(Name, ParseResult);
  PIDL := ParseResult.PIDL;
end;

end.

For the names like ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}\\\?\usb#vid_2717&pid_ff40#062539717d28#{6ac27878-a6fa-4155-ba85-f98f491d4f33}\SID-{20001,,31611420672}\{703540AE-0000-0000-0000-000000000000}\{0AE02E9E-0000-0000-0000-000000000000} it falls back to EnumObjects only for SID-{20001,,31611420672} part. This method can also parse display names like This PC\Redmi 6\SD card\DCIM\Camera, because it looks up items by display name. That is also what Raymond Chen suggested in his comment under How to use SHCreateItemFromParsingName with names from the shell namespace keeping in mind that display names are ambiguous. Another thing to point out is that looking up the item by display name via EnumObjects is inefficient and it can eventually be slow.

 

Edited by 0x8000FFFF

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×