Kilmatead

_GetReparseTarget

13 posts in this topic

#1 ·  Posted (edited)

; #FUNCTION# ====================================================================================================================
; Name...........: _GetReparseTarget
; Description....: Resolves a Reparse-Point (Junction, Symbolic Link or Mount Point) to its target and returns that destination path
; Syntax.........: _GetReparseTarget ( $sLink[, $AbsPath = True] )
; Parameters.....: $sLink - Full path to a Reparse-Point object
;                            $AbsPath - Return an absolute path when link ID type is 2 (embedded relative path Symbolic Link)
;
; Return values..: Success - The path/filename of the target location
;
;                                    @extended returns the ID/type of the Reparse-Point itself:
;                                    0 - Unknown/Unresolved
;                                    1 - Symbolic Link (embedded Absolute-Path)
;                                    2 - Symbolic Link (embedded Relative-Path) - primary return value will be an absolute path via the $sLink container (set $AbsPath = False to return the relative path)
;                                    3 - Junction Point
;                                    4 - Mount Point - primary return value will be the Globally Unique Identifier (GUID) as \\?\Volume{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}\
;
;                            Failure - Empty string ("") and sets the @error flag:
;                                    1 - $sLink Not Found
;                                    2 - Unable to Open $sLink
;                                    3 - $sLink is not a Reparse-Point
;                                    4 - Unresolveable (Corrupted Tag / No Target details)
;
; Author.........: Kilmatead
; Modified.......:
; Remarks........: @Extended may still contain a valid ID even if the link itself failed resolution
;
;                        The $AbsPath parameter has no effect beyond relative path Symbolic Links (ID 2)
;
;                        No check is made to see if the resolved target folder or file actually exists, as even though the target-destination may have been renamed/removed or is temporarily
;                        unavailable, that doesn't invalidate the data integrity of the reparse-tag itself, especially when it may contain relative-path references
;
;                        Permission-Free access is used to open the link so as to resolve even System Links (as found in Vista+) - this can be misleading, as it does not indicate that using
;                        $sLink directly in the script outside of this function will likely fail as System Links have ACL's which deny access to everyone
;
; Related........:
; Link...........:
; Example........:
; ===============================================================================================================================

#include <APIConstants.au3>
#include <File.au3>
#include <WinAPIEx.au3>

Func _GetReparseTarget($sLink, $AbsPath = True)
    Local Enum $ID_UNKNOWN, $ID_SYMLINK, $ID_SYMLINK_RELATIVE, $ID_JUNCTION, $ID_MOUNT_POINT
    Local Enum $NOTFOUND = 1, $ACCESSDENIED, $NOTREPARSE, $NOTRESOLVED

    Local $tFindData = DllStructCreate($tagWIN32_FIND_DATA)
    Local $hFile = _WinAPI_FindFirstFile($sLink, DllStructGetPtr($tFindData)) ; Retrieve the attributes / verify existence / obtain the ReparseTag identifier
    If @error Then Return SetError($NOTFOUND, $ID_UNKNOWN, "")
    _WinAPI_FindClose($hFile)

    If BitAND(DllStructGetData($tFindData, "dwFileAttributes"), $FILE_ATTRIBUTE_REPARSE_POINT) Then
        Local Const $IO_REPARSE_TAG_SYMLINK = 0xA000000C
        Local Const $IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003

        Local $Ret = "", $TypeID = $ID_UNKNOWN
        Local $Tag = _WinAPI_LoWord(DllStructGetData($tFindData, "dwReserved0"))

        Local $tREPARSE_GUID_DATA_BUFFER = _
                "dword ReparseTag;" & _
                "word ReparseDataLength;" & _
                "word Reserved; " & _
                "word SubstituteNameOffset;" & _
                "word SubstituteNameLength;" & _
                "word PrintNameOffset;" & _
                "word PrintNameLength;"

        Select
            Case BitAND($Tag, $IO_REPARSE_TAG_SYMLINK)
                $TypeID = $ID_SYMLINK
                $tREPARSE_GUID_DATA_BUFFER &= "dword Flags;" ; Convert (default) struct MountPointReparseBuffer to struct SymbolicLinkReparseBuffer

            Case BitAND($Tag, $IO_REPARSE_TAG_MOUNT_POINT)
                $TypeID = $ID_JUNCTION

            Case Else
                Return SetError($NOTRESOLVED, $ID_UNKNOWN, "")
        EndSelect

        $hFile = _WinAPI_CreateFileEx($sLink, $OPEN_EXISTING, 0, BitOR($FILE_SHARE_READ, $FILE_SHARE_WRITE, $FILE_SHARE_DELETE), _ ; dwDesiredAccess 0 (permission-free)
                BitOR($FILE_FLAG_BACKUP_SEMANTICS, $FILE_FLAG_OPEN_REPARSE_POINT))
        If @error Then Return SetError($ACCESSDENIED, $TypeID, "")

        Local $RGDB = DllStructCreate($tREPARSE_GUID_DATA_BUFFER & "wchar PathBuffer[4096]")
        _WinAPI_DeviceIoControl($hFile, $FSCTL_GET_REPARSE_POINT, 0, 0, DllStructGetPtr($RGDB), DllStructGetSize($RGDB))

        If Not @error Then
            Local Const $SYMLINK_FLAG_RELATIVE = 0x00000001
            Local Const $SIZEOF_WCHAR = 2

            Local $sBuffer = DllStructGetData($RGDB, "PathBuffer") ; Buffer "may" contain multiple strings "in any order" [MSDN]...
            Local $iOffset = DllStructGetData($RGDB, "SubstituteNameOffset") / $SIZEOF_WCHAR
            Local $iLength = DllStructGetData($RGDB, "SubstituteNameLength") / $SIZEOF_WCHAR

            $Ret = StringMid($sBuffer, 1 + $iOffset, $iLength) ; ...so always extract SubstituteName (despite its moniker) as the path-proper

            If StringLeft($Ret, 2) = "\?" Then $Ret = "\\" & StringMid($Ret, 3) ; DeviceIoControl loves substituting \??\ for more common \\?\, so we substitute it right back

            If $TypeID = $ID_SYMLINK And DllStructGetData($RGDB, "Flags") = $SYMLINK_FLAG_RELATIVE Then
                $TypeID = $ID_SYMLINK_RELATIVE
                If $Ret <> "" And $AbsPath Then $Ret = _PathFull($Ret, StringLeft($sLink, StringInStr($sLink, "\", 0, -1))) ; Convert to absolute path based from $sLink container
            EndIf

            Select ; Regulate possible mapped/unmapped UNC prefix genera or verify Mounted Volume ID by format
                Case StringRegExp($Ret, "(?i)\\Volume\{[a-f\d]{8}-([a-f\d]{4}-){3}[a-f\d]{12}\}\\$") ; "\Volume{GUID}\"
                    $TypeID = $ID_MOUNT_POINT
                Case StringLeft($Ret, 8) = "\\?\UNC\"
                    $Ret = StringReplace($Ret, "?\UNC\", "", 1) ; "\\?\UNC\server\share" -> "\\server\share"
                Case StringLeft($Ret, 4) = "\\?\" And StringMid($Ret, 6, 1) = ":"
                    $Ret = StringTrimLeft($Ret, 4) ; "\\?\C:\FolderObject" -> "C:\FolderObject"
            EndSelect
        EndIf

        _WinAPI_CloseHandle($hFile)
        $RGDB = 0

        If $Ret = "" Then Return SetError($NOTRESOLVED, $TypeID, "")

        Return SetExtended($TypeID, $Ret)
    EndIf

    Return SetError($NOTREPARSE, $ID_UNKNOWN, "")
EndFunc

Does what it says on the tin.  Compatible with XP, Vista, Win7, etc.  For a ridiculously long explanation on how it works (more or less for beginners with a sense of humour), and why GetFinalPathNameByHandle is not the way to go, and to see an extended example project it was created for, please see this.

Simple example:

#include "_GetReparseTarget.au3"

$aList = _FileListToArray(@UserProfileDir)

For $i = 1 To $aList[0]
    If BitAND(_WinAPI_GetFileAttributes(@UserProfileDir & "\" & $aList[$i]), $FILE_ATTRIBUTE_REPARSE_POINT) Then
        ConsoleWrite($aList[$i] & "     >>>>>     " & _GetReparseTarget(@UserProfileDir & "\" & $aList[$i]) & @LF)
    EndIf
Next
Edited by Kilmatead
2 people like this

Share this post


Link to post
Share on other sites



#4 ·  Posted (edited)

I wonder if a similar script can be made to delete de repare points as described here: http://msdn.microsoft.com/en-us/library/windows/desktop/aa364560%28v=vs.85%29.aspx

 

Simple enough to do.  You don't even need to derive the GUID of the reparse-point first, as MSDN would suggest.

Technically (since Vista SP2) this sort of thing isn't necessary as the standard FileDelete() or DirRemove() functions are safe to use on reparse points (and will only remove the link itself, not the target).  Not sure what happens in XP when you use those commands (once upon a time it would rudely delete the target as well), so for reliable backward compatibility (or just for fun) doing it "the hard way" is probably the best solution. :rolleyes:

; #FUNCTION# ====================================================================================================================
; Name...........: _DeleteReparsePoint
; Description....: Neutralises a Per-User Reparse Point and removes the lefover generic object
; Syntax.........: _DeleteReparsePoint ( $sLink[, $LeaveFinalObject = False] )
; Parameters.....: $sLink - Full path to a Reparse-Point object
;                            $LeaveFinalObject - Neutralising the Reparse tag leaves an empty file or folder in its place, ordinarily this would be removed as well, but may optionally be left extant
;
; Return values..: Success - 1
;
;                            Failure - 0 and sets the @error flag:
;                                    1 - $sLink Not Found
;                                    2 - Unable to Open $sLink (Access Denied)
;                                    3 - $sLink is not a Reparse-Point
;                                    4 - Unable to remove the Reparse Tag
;                                    5 - Unable to remove the Final Object
;
; Author.........: Kilmatead
; Modified.......:
; Remarks........:
; Related........:
; Link...........:
; Example........:
; ===============================================================================================================================

#include <APIConstants.au3>
#include <WinAPIEx.au3>

Func _DeleteReparsePoint($sLink, $LeaveFinalObject = False)
    Local Enum $NOTFOUND = 1, $ACCESSDENIED, $NOTREPARSE, $TAGNOTDELETED, $FINALOBJECTDELETEFAILURE

    Local $tFindData = DllStructCreate($tagWIN32_FIND_DATA)
    Local $hFile = _WinAPI_FindFirstFile($sLink, DllStructGetPtr($tFindData))
    If @error Then Return SetError($NOTFOUND, 0, 0)
    _WinAPI_FindClose($hFile)

    If BitAND(DllStructGetData($tFindData, "dwFileAttributes"), $FILE_ATTRIBUTE_REPARSE_POINT) Then
        Local $tREPARSE_GUID_DATA_BUFFER = "dword ReparseTag; word ReparseDataLength; word Reserved; byte ReparseGuid[16];"

        $hFile = _WinAPI_CreateFileEx($sLink, $OPEN_EXISTING, $GENERIC_WRITE, BitOR($FILE_SHARE_READ, $FILE_SHARE_WRITE, $FILE_SHARE_DELETE), _
                BitOR($FILE_FLAG_BACKUP_SEMANTICS, $FILE_FLAG_OPEN_REPARSE_POINT))
        If @error Then Return SetError($ACCESSDENIED, 0, 0)

        Local $RGDB = DllStructCreate($tREPARSE_GUID_DATA_BUFFER) ; REPARSE_GUID_DATA_BUFFER_HEADER_SIZE = 24

        DllStructSetData($RGDB, "ReparseTag", DllStructGetData($tFindData, "dwReserved0"))

        _WinAPI_DeviceIoControl($hFile, $FSCTL_DELETE_REPARSE_POINT, DllStructGetPtr($RGDB), DllStructGetSize($RGDB)) ; Destroy the Tag, generic object remains
        Local $Ret = @error

        _WinAPI_CloseHandle($hFile)
        $RGDB = 0

        If $Ret = 0 Then
            If $LeaveFinalObject Then Return 1

            If BitAND(DllStructGetData($tFindData, "dwFileAttributes"), $FILE_ATTRIBUTE_DIRECTORY) Then
                If DirRemove($sLink) Then Return 1
            Else
                If FileDelete($sLink) Then Return 1
            EndIf

            Return SetError($FINALOBJECTDELETEFAILURE, 0, 0)
        EndIf

        Return SetError($TAGNOTDELETED, 0, 0)
    EndIf

    Return SetError($NOTREPARSE, 0, 0)
EndFunc
Edited by Kilmatead

Share this post


Link to post
Share on other sites

Very nice function Kilmatead.

But strange, I get ACCESSDENIED error until I replace the $GENERIC_READ in _WinAPI_CreateFileEx() with 0.

Share this post


Link to post
Share on other sites

What type of reparse point are you attempting to open?  Denial of access is almost always associated with someone trying to open one of the system links (in their user-folder, for example) which Windows itself creates upon installation.  These have their ACL's set to deny access to everyone, so generally cannot be resolved unless you change the ACL's themselves, or add your username to the SYSTEM group - neither of which is particularly recommended (though essentially harmless).

I would imagine using an access-type of 0 would not do anything at all. :

These functions are aimed at "per-user" reparse points, i.e., the ones you create yourself, which are the only really useful ones at the end of the day anyway.

Share this post


Link to post
Share on other sites

"...you have to open it as a file, using the CreateFile function without any special access permissions."

 

Well, that's interesting - and it worked?  May need to experiment with this - I wouldn't have classed GENERIC_READ as a "special access" anything, but the world is a strange place...  Thanks.

Share this post


Link to post
Share on other sites

It is just excellent. The function does exactly what I expected. I had tried for hours and didn't even get close to this script. Thank you Kilmatead. :)

Share this post


Link to post
Share on other sites

#10 ·  Posted (edited)

As remarked in MSDN for CreateFile()...

If [dwDesiredAccess] is zero, the application can query certain metadata such as file, directory, or device attributes without accessing that file or device, even if GENERIC_READ access would have been denied.

 

I've tested this on all types of Reparse-Points (System/Per-User) including Mount Points, and it works fine, so I've updated the original function to reflect this small but important change.

The remarks were also changed to reflect the caveat that it can be rather misleading for this function to work fine when resolving system reparse-points, yet if the user were to use that exact same path in any other context in the same script, it would most likely fail (due to ACL permissions).

In other words,

_FileListToArray(_GetReparseTarget("C:Documents and Settings"))

will work as expected, but

_FileListToArray("C:Documents and Settings")

will fail miserably, even though a user-created reparse-point would operate transparently in both contexts.

Anyone using resolved system reparse-points to drive a recursive function should be careful of possible Ouroboros loops (no it's not an official term, but it sounds good) where subfolders contain reparse-points which actually reference an element within their own parental path, thus leading to infinite recursions across the same namespace breadth.

For example, C:ProgramData contains a reparse-point called Application Data which actually resolves back to C:ProgramData.  Win7 seems to delight in these things, as the user's AppDataLocal folder itself contains another Application Data link which resolves back to AppDataLocal, and so on.

If one were to bypass the permissions associated with these links, and browse them in windows explorer, you can end up in the amusing position of constantly clicking C:ProgramDataApplication Data only to see the addressbar registering your current location (via breadcrumbs) as  C:ProgramDataApplication DataApplication DataApplication DataApplication DataApplication DataApplication DataApplication DataApplication Data ad infinitum.

Which is interesting, yet ultimately it's really just... navel-gazing. :)

Thanks to JFX for pointing out this corrective oversight in the original function.

Edited by Kilmatead

Share this post


Link to post
Share on other sites

#11 ·  Posted (edited)

Thank you for this script, saved me a lot of time scanning all directories for inifinite loops.

This is a bit off topic, but I can't figure it out - how do NTFS junctions work on mapped network drives? On network drives it shows junctions as native path of the remote computer, therefore such paths are not accessible remotely.

For example, a remote computer shares one folder:

D:sharedfolder

it's accessible as:

servermyshare

and mapped on local computer as:

Z: -> servermyshare

That folder has a NTFS junction pointed to another drive on that remote computer (junction created on remote computer itself):

D:sharedfoldermyjunction -> F:anotherfolder

this junction is perfectly accessible on local computer via:

Z:myjunction

When run _GetReparseTarget("Z:myjunction") however, it shows the full path on that remote computer:

F:anotherfolder

Obviusly that path shouldn't be accessible on this local computer, but Windows 7 has no issues with it, so how does this actually work then?

P.S.

My script that detects loops only works on local computer, and I'm trying make it work on remote shares as well.

Loops such as:

D:sharedfoldermyjunction -> F:anotherfolder

F:anotherfolderanotherjunction -> D:sharefolder\

Edited by VAN0

Share this post


Link to post
Share on other sites

My knowledge and experience with networking is limited, so I can't comment directly on how NTFS interprets its own mapping.

I can, however, say that the tag-data which defines a reparse-point is composed of (in the case of the buffer) literal text, so when you state that the target junction was created on the remote computer itself, the text "F:anotherfolder" is exactly that - it's not interpreted in any way, and is already embedded within the link-tag as static text when the link was created.  Thus, that's what you get.  It's also why links can be "broken" yet remain (target) readable even if the target destination does not happen to exist.

Only mount-points are defined (at the time of creation) by their own GUID's, so Windows handles/converts those internally.  Relative SymLinks are also stored as literal text, just without any prefixed path, so again, Windows handles the practical concatenation internally.

I can only say that have great sympathy for your task though - identifying (what I call) Ourobourous Loops in real time when stepping through reparse-chains (one link leading directly to another) is an astonishingly mind-numbing exercise in itself, never mind introducing an extra layer of extrapolation (network shares).  In the case of potential chaining, it is especially important to check the returned value of any _GetReparseTarget() result for the FILE_ATTRIBUTE_REPARSE_POINT attribute immediately, so you can then identify the next intermediate step (by shoving it back through another _GetReparseTarget call), else you'll just end up with NTFS handling the reparse-tag before you can get to it and giving you a final destination (which would be unhelpful for loop-checking).

Good-luck! :D

Share this post


Link to post
Share on other sites

Thanks very much for the _GetReparseTarget() function - it's a life saver. :)

(I need to determine if a particular symlink points to the directory I'm about to install into: it should be pointing elsewhere. )

Share this post


Link to post
Share on other sites

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