Topic 4.6

Symbolic Links

Hard Links, Junctions, and Reparse Points

ATTENTION
Before proceeding with this topic I would like to point out that John, the founder of PortableApps.com, explains in detail why it would be bad practice to make use of any of these concepts in this topic. TLDR: Basically a portable application can potentionally render a host machine in a much worse state if it crashes while running a portable app which uses junctions or the like.
Synopsis

Hard Links, Junction Points and Symbolic Links are all methods used by your operating system to "link" files, directories or volumes together. To a computer, you could say that symbolic links are just advance shortcuts which make a file or folder appear to be the same as another file or folder located elsewhere on the host machine—even though it's just a link pointing at the original file or folder.

NTSF Reparse Points
Windows supports five forms of links (with symlinks implemented as ntfs-reparse-points). More detailed information for each can be found further below.
  • Hard links for files (but not directories) that live on the same volume partition.
  • Exported (global) links known as directory junctions (a type of “ntfs-reparse-point” only for directories not files)
  • Imported (local) links known simply as “directory or file symbolic links” (a type of “ntfs-reparse-point”)
  • LNK files known simply as “shortcuts” (contain paths and can be tracked)
  • URL files known simply as “browser URL files” (portably work as URI forms with file://…))
Reparse Points

A reparse point for Windows is basically what Linux calls a symbolic link, or mount point. It is actually similar to a shortcut or link that people use all the time. For instance, any icon on your computer's desktop is just a file that redirects the machine and tells it where to find the application as it is located usually in the Program Files folder. A reparse point is the same concept essentially except that it is implemented at the operating system level and not at the user level. For a visual example, open a command prompt and type dir /a and press enter. You should see something similar to the following:

Microsoft Windows [Version 10.0.18362.239]
(c) 2019 Microsoft Corporation. All rights reserved.

C:\Users\daemon>dir /a

 Directory of C:\Users\daemon

 <DIR>          .
 <DIR>          ..
 <DIR>          AppData
 <JUNCTION>     Application Data [C:\Users\daemon\AppData\Roaming]
 <DIR>          Contacts
 <DIR>          Desktop
 <DIR>          Documents
 <JUNCTION>     Local Settings [C:\Users\daemon\AppData\Local]
 <DIR>          Music
 <JUNCTION>     My Documents [C:\Users\daemon\Documents]
 <DIR>          Pictures
 <JUNCTION>     SendTo [C:\Users\daemon\AppData\Roaming\Microsoft\Windows\SendTo]
 <JUNCTION>     Start Menu [C:\Users\daemon\AppData\Roaming\Microsoft\Windows\Start Menu]
 <DIR>          Videos

C:\Users\daemon>

Notice that there are a few that say JUNCTION instead of DIR. Then take note that those entries also show you where they are pointing to. If a program writes data to a directory that's actually a reparse point, it instead gets sent to the real directory all unbeknownst to the program.

Hard Links

A hard link is like an empty shell of a file which points to another. This empty shell will mirror this other file on the same machine without duplicating any of the original file's data which means no additional hard drive space is used to store the hard link. There can be multiple hard links which represent the same one file. Hard links cannot link to a file that is on a different partition, volume or drive and they cannot be used to point to directories. It should also be noted that hard links only work on partitions formatted in NTFS.

Junctions

Junctions, also referred to as soft links, are only able to reference a directory, unlike a hard link which represents a file. Junctions can be used to link directories located on a different partition and/or volume, but only on a locally hosted machine. Folders that are redirected through junctions are defined by an absolute path. All of the information required to locate the target is contained in the path string.

To put it simply, a Symlink is to a Junction in Windows as a Symlink is to a Hardlink in Unix.

Both my Discord Portable and GitHub Desktop Portable make use of Junctions to allow for better handling of it's configuration/settings speeding up the launch process. Remember, using this method calls for administrative rights so if the end user doesn't have elevated priveledges than you'll have to make the launcher fallback on copying/moving files/directories.

Symlinks

Symbolic links can be used to link both files or folders. Both these files and folders can also be located on either the locally hosted machine or on a network share, also known as UNC (i.e. "\\System\Folder\file.txt""D:\Folder\file.txt" or "\\System\Folder""D:\Folder").

Moreover

Junctions Points are limited to folders on the local system only, while Symbolic Links can create links to folders or files accessible via a UNC path or on the local system with more versatility in how those locations are designated. Symbolic Links is basically a more versatile replacement for both Junction Points and Hard Links. Plus, Symbolic Links are compatible with Unix and Linux when creating a cross platform UNC pathed link.

Junctions and Symbolic links are really doing the same thing in the same way (reparse points), aside from the aforementioned differences in how they're processed. Technically speaking, a Junction is a symbolic link and is documented on Microsoft as such.

Macros

Below you will find a couple of macros from my own codebase for Junction support including a function to find the root directory of both the portable and local directories.

Function Root
	!macro _Root _PATH _RETURN
		Push `${_PATH}`
		Call Root
		Pop ${_RETURN}
	!macroend
	!define Root `!insertmacro _Root`
	Exch $0
	Push $1
	Push $2
	Push $3
	StrCpy $1 $0 2
	StrCmp $1 '\\' +5
	StrCpy $2 $1 1 1
	StrCmp $2 ':' 0 +16
	StrCpy $0 $1
	Goto +15
	StrCpy $2 1
	StrCpy $3 ''
	IntOp $2 $2 + 1
	StrCpy $1 $0 1 $2
	StrCmp $1$3 '' +9
	StrCmp $1 '' +5
	StrCmp $1 '\' 0 -4
	StrCmp $3 '1' +3
	StrCpy $3 '1'
	Goto -7
	StrCpy $0 $0 $2
	StrCpy $2 $0 1 -1
	StrCmp $2 '\' 0 +2
	StrCpy $0 ''
	Pop $3
	Pop $2
	Pop $1
	Exch $0
FunctionEnd

!define Junction::BackupLocal "!insertmacro _Junction::BackupLocal"
!macro _Junction::BackupLocal _LOCALDIR _SUBDIR _PORTABLEDIR _KEY _VAR1 _VAR2
	RMDir /r "${_LOCALDIR}\${_SUBDIR}.BackupBy$AppID"
	Rename "${_LOCALDIR}\${_SUBDIR}` `${_LOCALDIR}\${_SUBDIR}.BackupBy$AppID"
	CreateDirectory "${_PORTABLEDIR}"
	CreateDirectory "${_LOCALDIR}"
	ExecDos::Exec /TOSTACK `"${J}" -accepteula -q "${_LOCALDIR}\${_SUBDIR}" "${_PORTABLEDIR}"`
	Pop ${_VAR1}
	${If} ${_VAR1} = 0
		${WriteRuntimeData} ${PAL} "${_KEY}" 1
	${Else}
		${GetFileAttributes} "${_LOCALDIR}\${_SUBDIR}" REPARSE_POINT ${_VAR1}
		${If} ${_VAR1} = 1
			${WriteRuntimeData} ${PAL} "${_KEY}" 1
		${Else}
			IfFileExists "${_PORTABLEDIR}" 0 +14
			Push "${_PORTABLEDIR}"
			Call Root
			Pop ${_VAR1}
			Push "${_LOCALDIR}"
			Call Root
			Pop ${_VAR2}
			StrCmp ${_VAR1} ${_VAR2} 0 +5
			CreateDirectory "${_LOCALDIR}"
			ClearErrors
			Rename "${_PORTABLEDIR}" "${_LOCALDIR}\${_SUBDIR}"
			IfErrors 0 +3
			CreateDirectory "${_LOCALDIR}\${_SUBDIR}"
			CopyFiles /SILENT "${_PORTABLEDIR}\*.*" "${_LOCALDIR}\${_SUBDIR}"
		${EndIf}
	${EndIf}
!macroend


!define Junction::RestoreLocal "!insertmacro _Junction::RestoreLocal"
!macro _Junction::RestoreLocal _LOCALDIR _SUBDIR _PORTABLEDIR _KEY _VAR1 _VAR2
	ClearErrors
	${ReadRuntimeData} ${_VAR1} ${PAL} "${_KEY}"
	${If} ${Errors}
		${GetFileAttributes} "${_LOCALDIR}\${_SUBDIR}" REPARSE_POINT ${_VAR1}
		${If} ${_VAR1} = 1
			ExecDos::Exec /TOSTACK `"${J}" -accepteula -d -q "${_LOCALDIR}\${_SUBDIR}"`
			Pop ${_VAR1}
			IntCmp ${_VAR1} 0 +2
			RMDir "${_LOCALDIR}\${_SUBDIR}"
		${Else}
			IfFileExists "${_LOCALDIR}\${_SUBDIR}" 0 +17
			Push "${_PORTABLEDIR}"
			Call Root
			Pop ${_VAR1}
			Push "${_LOCALDIR}"
			Call Root
			Pop ${_VAR2}
			RMDir /r "${_PORTABLEDIR}"
			RMDir "${_LOCALDIR}\${_SUBDIR}"
			IfFileExists "${_LOCALDIR}\${_SUBDIR}" 0 +7
			StrCmp ${_VAR1} ${_VAR2} 0 +4
			ClearErrors
			Rename "${_LOCALDIR}\${_SUBDIR}" "${_PORTABLEDIR}"
			IfErrors 0 +3
			CreateDirectory "${_PORTABLEDIR}"
			CopyFiles /SILENT "${_LOCALDIR}\${_SUBDIR}\*.*" "${_PORTABLEDIR}"
			RMDir "${_LOCALDIR}"
		${EndIf}
	${Else}
		ExecDos::Exec /TOSTACK `"${J}" -accepteula -d -q "${_LOCALDIR}\${_SUBDIR}"`
		Pop ${_VAR1}
		IntCmp ${_VAR1} 0 +2
		RMDir "${_LOCALDIR}\${_SUBDIR}"
	${EndIf}
	RMDir /r "${_LOCALDIR}\${_SUBDIR}"
	Rename "${_LOCALDIR}\${_SUBDIR}.BackupBy$AppID` `${_LOCALDIR}\${_SUBDIR}"
	RMDir "${_LOCALDIR}"
	RMDir "${_PORTABLEDIR}"
	RMDir "${_LOCALDIR}"
!macroend
Junction.exe from Sysinternals
The -q parameter is a switch to suppress any error messages when using junction.exe. Find out more at SysInternals

You can look to the custom.nsh files I use in both my GitHub Desktop Portable and Discord Portable for working implementations of the above macros. Just be sure to change any applicable code to suite your own environment.