Kilmatead Posted July 13, 2013 Share Posted July 13, 2013 (edited) expandcollapse popup; #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 December 26, 2013 by Kilmatead Wilenty and JFX 2 Link to comment Share on other sites More sharing options...
Factfinder Posted December 3, 2013 Share Posted December 3, 2013 This is great. It requires some scripting ability to do it. 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 Link to comment Share on other sites More sharing options...
joakim Posted December 3, 2013 Share Posted December 3, 2013 We also implemented FSCTL_SET_REPARSE_POINT when making the NTFS file extractor, if you want an example for that;http://mft2csv.googlecode.com/files/NTFS_File_Extractor_v4.0.0.2.zip Link to comment Share on other sites More sharing options...
Kilmatead Posted December 5, 2013 Author Share Posted December 5, 2013 (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. expandcollapse popup; #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 December 26, 2013 by Kilmatead Link to comment Share on other sites More sharing options...
JFX Posted December 6, 2013 Share Posted December 6, 2013 Very nice function Kilmatead. But strange, I get ACCESSDENIED error until I replace the $GENERIC_READ in _WinAPI_CreateFileEx() with 0. Link to comment Share on other sites More sharing options...
Kilmatead Posted December 6, 2013 Author Share Posted December 6, 2013 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. Link to comment Share on other sites More sharing options...
JFX Posted December 6, 2013 Share Posted December 6, 2013 (edited) You are right, it only failed with the system created junctions in the C: root. I really thought admin rights would have been enough for read access, my fault. The 0 access type I had read here about http://www.codeproject.com/Articles/20071/Vista-Directory-Links-in-NET Edited December 6, 2013 by JFX Link to comment Share on other sites More sharing options...
Kilmatead Posted December 6, 2013 Author Share Posted December 6, 2013 "...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. Link to comment Share on other sites More sharing options...
Factfinder Posted December 6, 2013 Share Posted December 6, 2013 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. Link to comment Share on other sites More sharing options...
Kilmatead Posted December 7, 2013 Author Share Posted December 7, 2013 (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 December 7, 2013 by Kilmatead Link to comment Share on other sites More sharing options...
VAN0 Posted December 8, 2014 Share Posted December 8, 2014 (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:sharedfolderit's accessible as:servermyshareand mapped on local computer as:Z: -> servermyshareThat folder has a NTFS junction pointed to another drive on that remote computer (junction created on remote computer itself):D:sharedfoldermyjunction -> F:anotherfolderthis junction is perfectly accessible on local computer via:Z:myjunctionWhen run _GetReparseTarget("Z:myjunction") however, it shows the full path on that remote computer:F:anotherfolderObviusly 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:anotherfolderF:anotherfolderanotherjunction -> D:sharefolder\ Edited December 8, 2014 by VAN0 Link to comment Share on other sites More sharing options...
Kilmatead Posted December 9, 2014 Author Share Posted December 9, 2014 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! Link to comment Share on other sites More sharing options...
tremolux66 Posted October 27, 2016 Share Posted October 27, 2016 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. ) When the going gets tough, the tough start coding. Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now