PSJunction.psm1

<#
.DESCRIPTION
  A module to get the current target of a junction point, set a new target, create a new junction point or to find junction points in any given path.
 
.NOTES
  Version: 1.0.5
  Author: Mattias Cedervall
  Creation Date: 2021-06-22
  Purpose/Change: Fixed Find-JunctionPoints not to include loops
 
#>


Import-Module PSJunction.psd1

function New-JunctionPoint {

<#
.DESCRIPTION
  Creates a junction point without using any .exe nor PS-command to do so.
  I am aware of "New-item -Path 'Path' -Value 'Target' -ItemType Junction"
   
  Just wanted to do this in .NET.
  And besides, this function is a tiny bit better...
 
    C:\>dir testjunc*
    Volume in drive C is OSDisk
    Volume Serial Number is 2EC7-BCAE
 
    Directory of C:\
 
    2021-06-22 20:59 <JUNCTION> testJuncPS [\??\C:\temp]
    2021-06-22 21:08 <JUNCTION> testjunc_New-JunctionPoint [c:\temp]
 
    New-item doesn't set the PrintName, this does.
 
.PARAMETER Path
    The path of the junction point.
 
.PARAMETER Target
    The target of the junction point.
 
.NOTES
  Version: 1.0.5
  Author: Mattias Cedervall
  Creation Date: 2021-06-22
  Purpose/Change: Edited the description.
 
.EXAMPLE
  New-JunctionPoint -Path C:\MyJunction -Target C:\temp
#>


param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Target
)


if ([System.IO.Directory]::Exists($Path))
{
    $CurrentJunctionPoint=(Get-Item $Path -Force -ErrorAction SilentlyContinue)
    if ($CurrentJunctionPoint -ne $null)
    {
        $ret=-4
        PrintError -err $ret
    }

}

$junction=[System.IO.Junction]::new()
$ret=$junction.CreateJunction($Path,$Target)

if ($ret -eq 1)
{
    $CurrentTarget=$junction.GetTarget($Path)

    if ($CurrentTarget.Target -like  ("\??\"+$Target).TrimEnd("\").ToLower())
    {
        return $true
    }
    
}

PrintError $ret
return $ret

}

Function Set-JunctionPoint {

<#
.DESCRIPTION
  Can set (change) the target of a junction point instead of needing to delete it and create a new one.
  Still needing privileges to do so of course.
  With this function you don't need to keep the acl in mind which you do need to if you decide to delete and recreate a junction.
 
.PARAMETER Path
    The path of the junction point.
 
.PARAMETER NewTarget
    The new target of the junction point.
 
.NOTES
  Version: 1.0.5Alpha
  Author: Mattias Cedervall
  Creation Date: 2021-06-22
  Purpose/Change: Fixed description
 
.EXAMPLE
  Set-JunctionPoint -Path C:\program -NewTarget C:\temp
#>


param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$NewTarget
)

if ([System.IO.Directory]::Exists($Path))
{
    $CurrentJunctionPoint=Get-JunctionPoint -Path $Path
    if ($CurrentJunctionPoint.IsReparsePoint -ne $true)
    {
        $ret=-3
        PrintError -err $ret
    }

}

$junction=[System.IO.Junction]::new()
$ret=$junction.ChangeTarget($Path,$NewTarget)

if ($ret -eq 1)
{
    $CurrentTarget=$junction.GetTarget($Path)
    if ($CurrentTarget.Target -like  ("\??\"+$NewTarget).TrimEnd("\").ToLower())
    {
        return $true
    }
    
}

PrintError -err $ret
return $ret

}

Function Get-JunctionPoint {

<#
.DESCRIPTION
  20H2 bug with the Refresh scenario + non UEFI causes all default junctions to point to D:\ after the OSD is done if the OSDisk is D:\ in WinPE.
  While trying to detect how many computers that were affected by this I used "(Get-Item C:\Program -force).Target" but the Target was blank no matter
  if it was run by an User, an Admin or as "Local System".
 
  PS C:\WINDOWS\system32> whoami
  nt instans\system
  PS C:\WINDOWS\system32> (Get-Item C:\Program -force) | fl
 
  Name : Program
  CreationTime : 2017-12-12 00:25:58
  LastWriteTime : 2017-12-12 00:25:58
  LastAccessTime : 2017-12-12 00:25:58
  Mode : d--hsl
  LinkType :
  Target :
 
 
  This function can get the target even while running as a normal user.
 
  PS D:\PSJunction> Get-JunctionPoint C:\program
 
  Path : c:\program
  Target : \??\c:\program files
  PrintName : C:\Program Files
  IsReparsePoint : True
  TagType : IO_REPARSE_TAG_MOUNT_POINT
  Err : 0
  ErrMsg :
 
.PARAMETER Path
    The path of the junction point
 
.NOTES
  Version: 1.0Alpha
  Author: Mattias Cedervall
  Creation Date: 2021-06-11
  Purpose/Change: Initial script development
 
.EXAMPLE
  Get-JunctionPoint -Path C:\program
#>



param(
[Parameter(Mandatory = $true)]
[string]$Path
)

$junction=[System.IO.Junction]::new()
$ret=$junction.GetTarget($Path)
return $ret

}

Function Find-JunctionPoints
{
<#
.DESCRIPTION
  Tries to get all the juntions points within the given path and skipping paths that point to eachother causing loops.
  Also tries to find the target and the replace it in the output if .Target is null from Get-(child)Item.
 
.PARAMETER Path
    The path of the directory to search.
 
.PARAMETER Recurse
    Tries to find all junction points recursive without causing loops.
 
.PARAMETER AutoCorrectTarget
    Sets the output to display (Get-JunctionPoint).PrintName as the target if the target from Get-item is $null.
 
.NOTES
  Version: 1.0.4Alpha
  Author: Mattias Cedervall
  Creation Date: 2021-06-17
  Purpose/Change: Initial script development
 
.EXAMPLE
  Find-JunctionPoints -Path C:\programdata -Recurse
#>


param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $false)]
[switch]$Recurse,
[Parameter(Mandatory = $false)]
[bool]$AutoCorrectTarget=$true
)

    [Array]$RPFromPath=(Get-Item -Path $Path -Force -ErrorAction SilentlyContinue | where {$_.attributes -match "ReparsePoint"})
    
    if ($Recurse)
    {
        #[Array]$RPs=(Get-ChildItem $Path\* -Attributes ReparsePoint -Force -Directory -Recurse -ErrorAction SilentlyContinue)
        [Array]$RPs=(Get-ChildItem $Path\* -Attributes ReparsePoint -Force -Directory -ErrorAction SilentlyContinue)
    }
    else
    {
        [Array]$RPs=(Get-ChildItem $Path\* -Attributes ReparsePoint -Force -Directory -ErrorAction SilentlyContinue)
    }

    if ($RPs.Count -gt 0)
    {
        $Result=foreach ($RP in $RPs.GetEnumerator())
        {
            ($RP.Name)
        

        if ($Recurse)
        {
            [Array]$JunctionsInRootJunctions=foreach ($RP in $RPs.GetEnumerator())
            {
                Get-ChildItem $RP -Attributes ReparsePoint -Force  -Directory -ErrorAction SilentlyContinue
            }
        }
        $Targets=$RPs.GetEnumerator().Target
        }
    }
    
    <# 2021-06-07
    if ($RPs.Count -gt 0)
    {
        $Result=foreach ($RP in $RPs.GetEnumerator())
        {
            ($RP.Name)
        }
 
        if ($Recurse)
        {
            [Array]$JunctionsInRootJunctions=foreach ($RP in $RPs.GetEnumerator())
            {
                Get-ChildItem $RP -Attributes ReparsePoint -Force -Directory -ErrorAction SilentlyContinue
            }
        }
        $Targets=$RPs.GetEnumerator().Target
         
    }
    #>

    else
    {
        $Result=""
    }
    
    if ($Recurse)
    {
        #if ($Path.EndsWith(":\"))
        if ($Path.EndsWith("\*") -ne $true)
        {
            ##Bug within include/exclude if Path is a root dir,e.g. "C:\"
            $Path=$Path.TrimEnd('\')
            $Path=$Path+"\*"
        }
        $RPsRecursive=(Get-ChildItem $Path -Exclude $Result -Directory -ErrorAction SilentlyContinue -Force) | Get-ChildItem -Recurse -Attributes ReparsePoint -Force -ErrorAction SilentlyContinue -Directory
        #$RPsRecursive=(Get-ChildItem $Path -Exclude $Result -Directory -ErrorAction SilentlyContinue -Force -Attributes ReparsePoint -Recurse)
    }

    if ($JunctionsInRootJunctions.Count -gt 0)
    {
        $JunctionsInRootJunctions=$JunctionsInRootJunctions.GetEnumerator() | Where {$_.Target -notin $Targets}
    }

    #[Microsoft.PowerShell.Commands.InternalSymbolicLinkLinkCodeMethods]::GetLinkType($psobject 'get-item')
    $AllRPs=$RPFromPath + $RPs + $RPsRecursive + $JunctionsInRootJunctions

    $AllRPs=foreach ($RPFound in $AllRPs)
    {
        if ($RPFound.FullName -notin ($null,""))
        {
            $CurJunc=(Get-JunctionPoint -Path $($RPFound.FullName) -ErrorAction SilentlyContinue)
            $IsJunction=$CurJunc -ne $null
            if ($IsJunction -eq $true)
            {
                $RPFound
            }
        }
    }

    $ret=$($AllRPs | where {$_.LinkType -ne "SymbolicLink"})
    
    if ($AutoCorrectTarget -eq $true)
    {
        $ret= [psobject[]]($ret| Select-Object *)
    
        foreach ($R in $ret)
        {
            if ($R.Target -eq $null)
            {
                $CurJunc=(Get-JunctionPoint -Path $($R.FullName) -ErrorAction SilentlyContinue)
                $R.Target=$($CurJunc.PrintName)
            }
        }
    }

    return [psobject[]]($ret| Select-Object *)
}

Function PrintError([int]$err)
{

    if($err -eq -1)
    {  
       
       $ret=[System.IO.IOException]::new("The junction path is not valid.",$err)
       Throw $ret
    }

    if ($err -eq -2)
    {
        $ret=[System.IO.IOException]::new("Target does not exist or is not a valid target.",$err)
        Throw $ret
    
    }

    if ($err -eq -3)
    {
        $ret=[System.IO.IOException]::new("The path is not a junction point.",$err)
        Throw $ret
    
    }

    if ($err -eq -4)
    {
        $ret=[System.IO.IOException]::new("The path already exists.",$err)
        Throw $ret
    
    }

#Write-host "Error $err"
}

Export-ModuleMember -Function Get-JunctionPoint, Set-JunctionPoint, New-JunctionPoint, Find-JunctionPoints