The reality is a little different, of course - for these young men are victims of the same psychological frailties as form conspiracy-theorists, politicians, and girls with artificially ebullient personalities: Insecurity and Fear. In short, they live in a world which they are just beginning to understand is far beyond their capacity to understand or influence, where the whims of the few disturb the greater urges of youth in ways psychologists have yet to comprehend.
And then one day they discover that in the world of computers, everything is there for them to command and control - there are no errant and dishevelled lager-lout ruffians roaming the countryside terrorising the villages - there is only the opportunity to form a perfect world according to your own rules, where the satisfaction of accomplishment takes a backseat to getting the girl of their dreams in - well - the backseat.
To that end, the knickers of Reparse-Point Rachel and the mysteries of Jennifer's Junction-Point have a higher priority than the family-friendly tone the moderators of the world would prefer. So sayeth the post-adolescent mind of the mischievously-humorous and ever-curious control-freak coder, anyway.
"So what are you on about this time, is it yet another utility no one but hopeless anoraks and the emotionally damaged will ever find a use for?"
Well obviously! The day I produce anything practical and beneficial to humanity is the day I feed myself to the lions for the shame I would so feel. Except this time we will actually take a closer look at one particular function, created in a fit of blind frustration, which (if nothing else) the other emotionally damaged misfit coders of the world may appreciate.
Download: TouchReparsePoint (source-code included for the curious and bored)
"So what's it do?" you ask.
Well - nothing, really, it's more of an elaborate proof-of-concept which allows us to see behind the frilly black lacing of Rachel's lingerie, to indulge our understanding of accessing the mysteries therein, and (more importantly) to see just how far we can stretch a smart-arse metaphor before it breaks and snaps us cruelly on the chin.
"Get to the point quickly, dude, my attention span isn't what it used to be."
In essence, if you've ever tried to manually change the timestamps on a reparse-point object, you'd find that your new timestamp would not actually be applied to the Symbolic Link or Junction Point you're looking at, but rather it would zip remotely through the wormhole and onto the target object itself. Normally at this point anyone else would just shrug and turn back to their cold oatmeal and soggy toast to look across the breakfast table and wonder if asking that sad and glintless woman to be your wife was really a good idea or not.
But not today! Today we will lift our sorrowful faces into the light to see what lies on the other side of that wormhole, and how it may transform our understanding of what a good breakfast is all about and why it really is the most important meal of the day.
"You're kidding, right? I mean, timestamps of junction points? Who gives a toss?"
<Sigh> Oh ye of little vision - look at the big picture, I beg of you. But first let's get the usual nonsense of the utility out of the way: Once you know what you're looking at, it should all be pretty self-explanatory...
For simplicity this one has a couple of entry-points - you may either call it with command-line arguments via a user-command ('> TRP.exe $A') or just run the executable and Drag-&-Drop one or more symbolic links, junction points, or mount point objects directly into the window. It will happily identify the link-types, display the target objects (relative path viewing optional), and let you play with syncing their timestamp inheritance values. If a non-reparse object is used, it's just treated like any normal timestamp assignment task.
"Oh my god - you're actually serious - is that all it does?"
Yes, but once again, I ask you to look at...
"No! I am the voice of the ever-demanding Great Unwashed! I want to know if I've just wasted 20 minutes reading all this nonsense only to discover it's of no interest to me!"
Look, if you just let me explain...
"Bugger that! Get to the point already! Why should I be interested in this? I'm just a guy who writes the odd script to help him manage file and folder tasks - what good is this to me?"
Look closer at the information displayed in the window. Say you've just written a script that recursively parses a folder structure searching for files (or whatever it does), and you're all very proud of yourself that your script works as expected. So you give it out to your friends with great pride thinking it's idiot-proof. And then the next day you start experimenting and discover that if it encounters a reparse-point object, your happy little recursion routine can accidentally end up only God-knows-where, and in a worse-case scenario, in an endless Ouroboros loop - and you think to yourself: If I only knew how to retrieve the target of a reparse-point before I entered it, I might have been able to avoid this mess, or at the very least I'd be better able to predict where/what my script was processing and how it got there.
"Well, Ok, I see what you mean, but I think you made that 'Ouroboros' word up. All I want to know is how to scripturally retrieve the targets of Symbolic Links and Junction Points. Are you saying you can tell me how to do this?"
Exactly! Welcome to my breakfast table, sit right down, pour yourself a coffee, light a cigarette, and we'll see how this works.
Like most things in life, there are actually two ways to do this: the easy way and the hard way. We'll look at the easy way first, just to get an idea of what's happening. I, of course, am using the AutoIt scripting language to do this, but as what we're doing is 99% Win32 API calls, it can be easily translated into any language, be that AHK, C, or the verbiage your grandmother used to mutter warblingly in her sleep. All you need to know is how the desired language interfaces with the stock kernel32.dll file when calling the functions.
I consider the first method the "Where's Wally?" approach as all it does is jump through the wormhole, then query the filesystem about where it ended up. Seems simple enough:
Code: Select all
$hFile = _WinAPI_CreateFileEx($sLink, $OPEN_EXISTING, 0, BitOR($FILE_SHARE_READ, $FILE_SHARE_WRITE, $FILE_SHARE_DELETE), $FILE_FLAG_BACKUP_SEMANTICS)
$Target = _WinAPI_GetFinalPathNameByHandleEx($hFile)
It's not, really. In Windows, files are opened using the CreateFileW function ('W' for "Wide character", meaning unicode) - despite its name, it is used for opening and reading objects too, hence the OPEN_EXISTING flag. One interesting thing to note here is that the third parameter normally contains either GENERIC_READ or GENERIC_WRITE constants to indicate the access-type - but in this case we're using zero ("permissions-free"), because that allows the attributes of certain files to be read (such as reparse-tags), without truly "opening" the object, thus not causing an access-denial error (this allows us to resolve system reparse-points as well as the more traditional per-user links without having to mess with their ACL's). FILE_FLAG_BACKUP_SEMANTICS is one of those wonderful things that actually tastes better than it sounds (like Pizza with Barbecue-Sauce or Mud Pie Ice-Cream) and basically is best thought of as a means of obtaining an open handle to a folder. (It is actually about verifying to the OS that the object is being opened for a backup or restore operation and requests that the OS deals with the necessary permissions accordingly - but at the end of the day, it's predominantly used to open Folder-objects without generating spurious access-errors.)
Once we have the object handle, we just call GetFinalPathNameByHandleW and it returns a full path to whatever we just opened.
"Now wait, didn't we just open the Reparse-Point by name, you mean we didn't actually open the thing we called?"
That's right - the way NTFS reparse-points work is that they are essentially normal file/folder objects with special user-defined "Tag-data" attached to them which (when found) is then immediately interpreted by the filesystem "filter" assigned to that type of tag before the user get can get his grubby hands on them - that's why when you click on a junction in a file manager it's the target you actually end up looking at, not the folder itself, or play the media-file the symlink points to, and not just end up looking at zero-size file content.
Now obviously anyone using a decent file-manager or shell extension (or even just those stuck using MKLINK.EXE) to create and manipulate Symlinks and Junctions know "how" the paradigm works, that's what makes the objects useful to us, but most don't know "why" it works the way it does - and most don't care. But for our purposes, it's useful background knowledge to have.
"Ok, so this method got us the target-string - that's all we wanted, wasn't it? What's wrong with it?"
A couple of drawbacks - for one, GetFinalPathNameByHandle can't be used on Windows XP as it doesn't exist in the API, so it's only good for Vista and above. Also, that function only works properly if the link target actually exists - if (for some reason) the reparse tag points to an object that has been moved or is otherwise unavailable (on a detached drive, for example), GetFinalPathNameByHandle is useless because the filter will have failed to resolve the destination upon the CreateFile call, and thus there is no object path to return. Also, symlink tags may have embedded "relative" paths, not absolute ones, and occasionally (especially in the case of being unable to resolve a path) it may be useful to allow the user to see this, so he can figure out what went wrong, and allow your script to do some better housekeeping.
Obviously, to anyone used to reparse-points, there are many ways for the user himself to "see" the destination path, even if it doesn't exist (shell columns, shell extensions, FSUTIL.EXE, etc) - but it's not so simple for your poor lonesome script to "see" this information itself, in a practical sense.
"So this is where it gets interesting then, yeah? When do we get to the part where I get to raise Rachel's skirt?"
Hold your horses, slow and steady wins the day - if you just show how desperate you really are and jump the gun the girl will never acquiesce to your charms... and before you know it, you're rejected, cold, alone, and you've gone and signed up for the Légion étrangère like many a broken-hearted youth before ye. So, before that happens, let's see how this courtship thing works first, shall we? As the saying goes, you can climb any tree once it's down - it's just a matter of getting it down with as little fuss as possible first.
Now, about those Tags. There are actually many types of ReparseTag-Identifiers available under NTFS, but we're mainly just interested in two of them: IO_REPARSE_TAG_MOUNT_POINT (which gives us access to Junctions and actual Volume Mount Points themselves), and IO_REPARSE_TAG_SYMLINK which references both relative and absolute paths related to Symlinks.
When building the function to retrieve reparse targets, the best place to start is the API function FindFirstFileW.
Code: Select all
$tFindData = DllStructCreate($tagWIN32_FIND_DATA)
$hFile = _WinAPI_FindFirstFile($sLink, DllStructGetPtr($tFindData))
"Huh? What the heck are those things?"
Reparse buffers are the things that actually contain the information we want - there are actually 3 different kinds (Symbolic, Mount, and Generic) but we only need concern ourselves with the first two. The "official" C-language syntax structure would look something like this:
Code: Select all
typedef struct _REPARSE_DATA_BUFFER {
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
union {
struct {
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[1];
} SymbolicLinkReparseBuffer;
struct {
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
WCHAR PathBuffer[1];
} MountPointReparseBuffer;
struct {
UCHAR DataBuffer[1];
} GenericReparseBuffer;
};
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
Of the other elements, we're only really interested in the "SubstituteName" offset values, as they will allow us to dissect the DataBuffer itself, which is where our "target" resides.
"So how do we suck the marrow out of the bones of the Reparse Point and stuff it in our carefully defined structure?"
Well, we go back to the API...
Code: Select all
$hFile = _WinAPI_CreateFileEx($sLink, $OPEN_EXISTING, 0, BitOR($FILE_SHARE_READ, $FILE_SHARE_WRITE, $FILE_SHARE_DELETE), BitOR($FILE_FLAG_BACKUP_SEMANTICS, $FILE_FLAG_OPEN_REPARSE_POINT))
WinAPI_DeviceIoControl($hFile, $FSCTL_GET_REPARSE_POINT, 0, 0, DllStructGetPtr($RGDB), DllStructGetSize($RGDB))
If you remember in the "Where's Wally?" approach we had to open the reparse point first, and we have to do it now as well, except this time instead of allowing the NTFS reparse filter to transfer us to the target automatically (which would obviously defeat the purpose) we have to open the actual reparse point itself, by using (unsurprisingly) FILE_FLAG_OPEN_REPARSE_POINT.
We then need to use DeviceIoControl to read the contents of the tag... again, unsurprisingly, FSCTL_GET_REPARSE_POINT is the file-system control-code (FSCTL) to do this - the attentive reader will note that at this point if we were to open the reparse object using GENERIC_WRITE instead of GENERIC_READ, and substitute FSCTL_SET_REPARSE_POINT (or even FSCTL_DELETE_REPARSE_POINT), we could really stretch our knickers metaphor to the breaking point. You get the idea - courtship is really just a roadmap - what happens after the lights go out and the skirts go up is entirely up to the consenting individuals involved.
"For Odin's sake my good man, are we done yet? I've been reading this rubbish for hours - if you stayed on topic we might get there faster!"
As it happens, we are done - the DeviceIoControl call will deposit the SubstituteName and/or the PrintName strings in the Buffer so all we have to do is read the one we want.
"What's the difference between a SubstituteName and a PrintName? Should I care?"
Yes, you should care - for one thing, Junctions/Mount Points will only return a SubstituteName string, while Symbolic Links will return both - effectively SubstituteName strings will contain the full UNC path of the target objects (or the relative path, if need be), while PrintNames are the "cleaned up" versions of those - but since Junctions have no PrintNames, just use SubstituteName.
The "offset" and "length" values pertaining to SubstituteName are calculated via the size of the wchar type itself (wchar = unicode = 2 bytes), so just extract the string position in the Buffer by dividing everything by 2.
And after a little string clean-up, and/or providing a more accurate Link-type identifier to the user (is the target a Junction or a Mount Point?, if Symlink does it have a relative or absolute path?) we're done. For simplicity I've allowed for the automatic conversion of relative paths into absolute based on the link container itself, so it's easier for the rest of the user's script to deal with, but that's all normal stuff.
While the full script of the utility (as a usage-example) and the _GetReparseTarget() function source-code is included in the download itself, I'll add the function code here just for those "curious" but "not curious enough" to wonder what material Rachel's knickers were made of.
I hope you've enjoyed this little explanation of reparse-points - if you found reading it all too difficult, imagine what it was like trying to figure this stuff out by piecing together stuff from the web to get a cohesive function that works on both XP and Vista+, is compatible with UNC pathways, and was reliable enough for a confident public release. Believe it or not, there is no rule-book for dealing with reparse-points - it's every man for himself when it comes to deciphering the MSDN references.
I thank the users fgagnon and RightPaddock for their invaluable beta-testing and bug detection, and nikos for pointing me back in the direction of FILE_FLAG_BACKUP_SEMANTICS which I had dismissed as irrelevant upon first glance, but which ultimately lead to everything that came afterward.
Code: Select all
; #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