BAMCIS.FileIO.psm1

Function Set-FileSecurity {
    <#
        .SYNOPSIS
            Sets permissions on a file or directory.
 
        .DESCRIPTION
            Will add or replace the supplied rules to the specified file or directory. The default behavior is that the rules are just added to the current ACL of the object.
 
        .PARAMETER Path
            The path to the file to set permissions on.
 
        .PARAMETER Rules
            An array of File Access Rules to apply to the path.
 
        .PARAMETER ReplaceAllRules
            Indictates if all permissions on the path should be replaced with these.
 
        .PARAMETER ReplaceNonInherited
            Replaces all existing rules that are not inherited from a parent directory.
 
        .PARAMETER ReplaceRulesForUser
            Indicates if the supplied rules should replace existing rules for matching users. For example, if the Rules parameter has a Full Control rule for System and a Read rules for
            Administrators, existing rules for System and Administrators would be removed and replaced with the new rules.
 
        .PARAMETER AddIfNotPresent
            Add the rules if they do not already exist on the path. The rules are matched based on all properties including FileSystemRights, PropagationFlags, InheritanceFlags, etc.
 
        .PARAMETER ForceChildInheritance
            Indicates if all permissions of child items should have their permissions replaced with the parent if the target is a directory.
 
        .PARAMETER EnableChildInheritance
            Indicates that child items should have inheritance enabled, but will still preserve existing permissions. This parameter is ignored if ForceChildInheritance is specified.
 
        .PARAMETER ResetInheritance
            Indicates that all explicitly set permissions will be removed from the path and inheritance from its parent will be forced.
 
        .EXAMPLE
            PS C:\>Set-Permissions -Path "c:\test.txt" -Rules $Rules
 
            Creates the rule set on the test.txt file.
 
        .EXAMPLE
            PS C:\>Set-Permissions -Path "c:\test" -ResetInheritance
 
            Resets inherited permissions on the c:\test directory.
 
        .EXAMPLE
            PS C:\>Set-Permissions -Path "c:\test" -Rules $Rules -ReplaceAllRules -ForceChildInheritance
 
            Replaces all existing rules on the c:\test directory with the newly supplied rules and forces child objects to inherit those permissions. This removes existing explicit permissions on child objects.
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 2/27/2017
    #>


    [CmdletBinding(DefaultParameterSetName = "Add")]
    [Alias("Set-FilePermissions")]
    [OutputType()]
    Param 
    (
        [Parameter(Position=0,Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Path,

        [Parameter(ParameterSetName = "ReplaceAll")]
        [Parameter(ParameterSetName = "Replace")]
        [Parameter(ParameterSetName = "Add")]
        [Parameter(ParameterSetName = "AddIfNotPresent")]
        [Parameter(ParameterSetName = "ReplaceNonInherited")]
        [Parameter(ParameterSetName = "AddIfNotPresentAndReplace")]
        [Alias("Rules")]
        [ValidateNotNull()]
        [System.Security.AccessControl.FileSystemAccessRule[]]$AccessRules,

        [Parameter(ParameterSetName = "ReplaceAll")]
        [Parameter(ParameterSetName = "Replace")]
        [Parameter(ParameterSetName = "Add")]
        [Parameter(ParameterSetName = "AddIfNotPresent")]
        [Parameter(ParameterSetName = "ReplaceNonInherited")]
        [Parameter(ParameterSetName = "AddIfNotPresentAndReplace")]
        [ValidateNotNull()]
        [System.Security.AccessControl.FileSystemAuditRule[]]$AuditRules,

        [Parameter(ParameterSetName = "ReplaceAll")]
        [Switch]$ReplaceAllRules,

        [Parameter(ParameterSetName = "ReplaceNonInherited")]
        [Switch]$ReplaceNonInheritedRules,

        [Parameter(ParameterSetName = "Replace")]
        [Switch]$ReplaceRulesForUser,

        [Parameter(ParameterSetName = "AddIfNotPresent")]
        [Switch]$AddIfNotPresent,

        [Parameter(ParameterSetName = "AddIfNotPresentAndReplace")]
        [Switch]$AddIfNotPresentAndReplace,

        [Parameter()]
        [Switch]$ForceChildInheritance,

        [Parameter()]
        [Switch]$EnableChildInheritance,

        [Parameter(ParameterSetName = "Reset")]
        [Switch]$ResetInheritance
    )

    Begin 
    {           
        Function Convert-FileSystemRights {
            Param(
                [Parameter(Mandatory = $true, Position = 0)]
                [System.Security.AccessControl.FileSystemRights]$Rights
            )

            Begin {
            }

            Process {
                [System.Security.AccessControl.FileSystemRights]$ExistingFileSystemRights = $Rights
                [System.Int32]$Temp = $Rights

                switch ($Temp)
                {
                    #268435456
                    0x10000000 {
                        $ExistingFileSystemRights = [System.Security.AccessControl.FileSystemRights]::FullControl
                        break
                    }
                    #-1610612736
                    0xA0000000 {
                        $ExistingFileSystemRights = @([System.Security.AccessControl.FileSystemRights]::ReadAndExecute, [System.Security.AccessControl.FileSystemRights]::Synchronize)
                        break
                    }
                    #-536805376
                    0xE0010000 {
                        $ExistingFileSystemRights = @([System.Security.AccessControl.FileSystemRights]::Modify, [System.Security.AccessControl.FileSystemRights]::Synchronize)
                        break
                    }
                    default {
                        $ExistingFileSystemRights = $Rights
                        break
                    }
                }

                Write-Output -InputObject $ExistingFileSystemRights
            }

            End {
            }
        }

        Function Get-AuthorizationRuleComparison {
            Param(
                [Parameter(Mandatory = $true, Position = 0)]
                [System.Security.AccessControl.AuthorizationRule]$Rule1,

                [Parameter(Mandatory = $true, Position = 1)]
                [System.Security.AccessControl.AuthorizationRule]$Rule2
            )

            Begin {
            }

            Process {
                $Equal = $false

                try
                {
                    [System.Security.AccessControl.FileSystemRights]$ExistingFileSystemRights1  = Convert-FileSystemRights -Rights $Rule1.FileSystemRights
                    [System.Security.AccessControl.FileSystemRights]$ExistingFileSystemRights2  = Convert-FileSystemRights -Rights $Rule2.FileSystemRights

                    if ($ExistingFileSystemRights1 -eq $ExistingFileSystemRights2 -and `
                        $Rule1.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -eq $Rule2.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) -and `
                        $Rule1.AccessControlType -eq $Rule2.AccessControlType -and `
                        $Rule1.InheritanceFlags -eq $Rule2.InheritanceFlags -and `
                        $Rule1.PropagationFlags -eq $Rule2.PropagationFlags)
                    {
                        $Equal = $true
                    }
                }
                catch [Exception]
                {
                    Write-Log -Message "Error evaluating access rule : `nExisting $($Rule1 | FL | Out-String) `nNew $($Rule2 | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                }

                Write-Output -InputObject $Equal
            }

            End {
            }
        }

        Set-TokenPrivilege -Privileges SeSecurityPrivilege -Enable
    }

    Process
    {
        if ($PSCmdlet.ParameterSetName -eq "Add" -and $AccessRules.Length -eq 0 -and $AuditRules.Length -eq 0)
        {
            throw "Either a set of access rules or audit rules must be provided to add to the path."
        }

        Write-Log -Message "Setting access and audit rules on $Path" -Level VERBOSE
        Push-Location -Path $env:SystemDrive

        [System.Boolean]$IsProtectedFromInheritance = $false

        #This is ignored if IsProtectedFromInheritance is false
        [System.Boolean]$PreserveInheritedRules = $false

        try
        {
            #$Acl = Get-Acl -Path $Path
            $Item = Get-Item -Path $Path
            [System.Security.AccessControl.FileSystemSecurity]$Acl = $Item.GetAccessControl(@([System.Security.AccessControl.AccessControlSections]::Access, [System.Security.AccessControl.AccessControlSections]::Audit))

            if ($Acl -ne $null)
            {
                switch ($PSCmdlet.ParameterSetName) {
                    "ReplaceAll" {

                        if ($AccessRules.Length -gt 0)
                        {
                            Write-Log -Message "Disabling access rule inheritance on $Path" -Level VERBOSE
                            $Acl.SetAccessRuleProtection($IsProtectedFromInheritance, $PreserveInheritedRules)

                            [System.Security.AccessControl.AuthorizationRuleCollection]$OldAcls = $Acl.Access

                            foreach ($Rule in $OldAcls)
                            {
                                try 
                                {
                                    $Acl.RemoveAccessRule($Rule) | Out-Null
                                }
                                catch [Exception] 
                                {
                                    Write-Log -Message "Error removing access rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                }
                            }
                        }

                        if ($AuditRules.Length -gt 0)
                        {
                            Write-Log -Message "Disabling audit rule inheritance on $Path" -Level VERBOSE
                            $Acl.SetAuditRuleProtection($IsProtectedFromInheritance, $PreserveInheritedRules)

                            Write-Log -Message "Getting audit rules" -Level VERBOSE
                            [System.Security.AccessControl.AuthorizationRuleCollection]$OldAuditRules = $Acl.GetAuditRules($script:EXPLICIT_TRUE,  $script:INHERITED_FALSE, [System.Security.Principal.NTAccount])

                            foreach ($Rule in $OldAuditRules)
                            {
                                try
                                {
                                    $Acl.RemoveAuditRule($Rule) | Out-Null
                                }
                                catch [Exception]
                                {
                                    Write-Log -Message "Error removing audit rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                }
                            }
                        }

                        break
                    }
                    "ReplaceNonInherited" {

                        if ($AccessRules.Length -gt 0)
                        {
                            [System.Security.AccessControl.AuthorizationRuleCollection]$OldAcls = $Acl.Access

                            foreach ($Rule in ($OldAcls | Where-Object {$_.IsInherited -eq $false}))
                            {
                                try 
                                {
                                    $Acl.RemoveAccessRule($Rule) | Out-Null
                                }
                                catch [Exception] 
                                {
                                    Write-Log -Message "Error removing access rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                }
                            }
                        }

                        if ($AuditRules.Length -gt 0)
                        {
                            Write-Log -Message "Disabling audit rule inheritance on $Path" -Level VERBOSE

                            Write-Log -Message "Getting non inherited audit rules" -Level VERBOSE
                            [System.Security.AccessControl.AuthorizationRuleCollection]$OldAuditRules = $Acl.GetAuditRules($script:EXPLICIT_TRUE,  $script:INHERITED_FALSE, [System.Security.Principal.NTAccount])

                            foreach ($Rule in $OldAuditRules)
                            {
                                try
                                {
                                    $Acl.RemoveAuditRule($Rule) | Out-Null
                                }
                                catch [Exception]
                                {
                                    Write-Log -Message "Error removing audit rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                }
                            }
                        }

                        break
                    }
                    "Replace" {
                        
                        [System.Security.Principal.SecurityIdentifier[]]$Identities = $AccessRules | Select-Object -Property @{Name = "ID"; Expression = { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]) } } | Select-Object -ExpandProperty ID
                        foreach ($Sid in $Identities)
                        {
                            $Acl.PurgeAccessRules($Sid)
                            $Acl.PurgeAuditRules($Sid)
                        }
                        
                        break
                    }
                    "Add" {
                        #Do Nothing
                        break
                    }
                    "Reset" {
                        [System.Security.AccessControl.AuthorizationRuleCollection]$OldAcls = $Acl.Access

                        foreach ($Rule in $OldAcls)
                        {
                            $Acl.RemoveAccessRule($Rule) | Out-Null
                        }
                
                        $Acl.SetAccessRuleProtection($IsProtectedFromInheritance, $PreserveInheritedRules)            

                        [System.Security.AccessControl.AuthorizationRuleCollection]$OldAuditRules = $Acl.GetAuditRules($script:EXPLICIT_TRUE,  $script:INHERITED_FALSE, [System.Security.Principal.NTAccount])

                        foreach ($Rule in $OldAuditRules)
                        {
                            $Acl.RemoveAuditRule($Rule) | Out-Null
                        }

                        $Acl.SetAuditRuleProtection($IsProtectedFromInheritance, $PreserveInheritedRules)
                        
                        #Call set ACL since no additional rules are provided
                        $Item.SetAccessControl($Acl)
                    }
                    "AddIfNotPresent" {
                        if ($AccessRules.Length -gt 0)
                        {
                            foreach ($Rule in $AccessRules)
                            {
                                [System.Boolean]$Found = $false

                                foreach ($ExistingRule in $Acl.Access)
                                {
                                    $Found = Get-AuthorizationRuleComparison -Rule1 $ExistingRule -Rule2 $Rule
                                    if ($Found -eq $true)
                                    {
                                        Write-Log -Message "Found matching access rule, no need to add this one" -Level VERBOSE
                                        break
                                    }
                                }

                                if ($Found -eq $false)
                                {
                                    try
                                    {
                                        $Acl.AddAccessRule($Rule)
                                    }
                                    catch [Exception]
                                    {
                                        Write-Log -Message "Error adding access rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                    }
                                }
                            }

                            #Call set access control since we've already added the rules
                            $Item.SetAccessControl($Acl)
                        }    

                        if ($AuditRules.Length -gt 0)
                        {
                            foreach ($Rule in $AuditRules)
                            {
                                [System.Boolean]$Found = $false

                                foreach ($ExistingRule in $Acl.GetAuditRules($script:EXPLICIT_TRUE, $script:INHERITED_FALSE, [System.Security.Principal.NTAccount]))
                                {
                                    $Found = Get-AuthorizationRuleComparison -Rule1 $ExistingRule -Rule2 $Rule

                                    if ($Found -eq $true)
                                    {
                                        break
                                    }
                                }

                                if ($Found -eq $false)
                                {
                                    try
                                    {
                                        $Acl.AddAuditRule($Rule)
                                    }
                                    catch [Exception]
                                    {
                                        Write-Log -Message "Error adding audit rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                    }
                                }
                            }
                            #Call set access control since we've already added the rules
                            $Item.SetAccessControl($Acl)
                        }
                        break
                    }
                    "AddIfNotPresentAndReplace" {
                        if ($AccessRules.Length -gt 0)
                        {
                            foreach ($ExistingRule in ($Acl.Access | Where-Object {$_.IsInherited -eq $false }))
                            {
                                [System.Boolean]$Found = $false

                                foreach ($Rule in $AccessRules)
                                {
                                    $Found = Get-AuthorizationRuleComparison -Rule1 $ExistingRule -Rule2 $Rule

                                    #The existing rule did match a new rule
                                    if ($Found -eq $true)
                                    {
                                        break
                                    }
                                }

                                #The existing rule did not match a new rule, remove it
                                if ($Found -eq $false)
                                {
                                    try
                                    {
                                        Write-Log -Message "Removing rule $($Rule | FL | Out-String)" -Level VERBOSE
                                        $Acl.RemoveAccessRule($ExistingRule)
                                    }
                                    catch [Exception]
                                    {
                                        Write-Log -Message "Error removing access rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                    }
                                }
                            }


                            foreach ($Rule in $AccessRules)
                            {
                                [System.Boolean]$Found = $false

                                foreach ($ExistingRule in $Acl.Access)
                                {
                                    $Found = Get-AuthorizationRuleComparison -Rule1 $ExistingRule -Rule2 $Rule

                                    if ($Found -eq $true)
                                    {
                                        break
                                    }
                                }

                                #Did not find a matching, existing rule
                                if ($Found -eq $false)
                                {
                                    try
                                    {
                                        Write-Log -Message "Adding rule $($Rule | FL | Out-String)" -Level VERBOSE
                                        $Acl.AddAccessRule($Rule)
                                    }
                                    catch [Exception]
                                    {
                                        Write-Log -Message "Error adding access rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING 
                                    }
                                }
                            }

                            #Call set access control since we've already added the rules
                            $Item.SetAccessControl($Acl)
                        }    

                        if ($AuditRules.Length -gt 0)
                        {
                            foreach ($ExistingRule in $Acl.GetAuditRules($script:EXPLICIT_TRUE, $script:INHERITED_FALSE, [System.Security.Principal.NTAccount]))
                            {
                                [System.Boolean]$Found = $false

                                foreach ($Rule in $AccessRules)
                                {
                                    $Found = Get-AuthorizationRuleComparison -Rule1 $ExistingRule -Rule2 $Rule

                                    #The existing rule did match a new rule
                                    if ($Found -eq $true)
                                    {
                                        break
                                    }
                                }

                                #The existing rule did not match a new rule, remove it
                                if ($Found -eq $false)
                                {
                                    try
                                    {
                                        Write-Log -Message "Removing rule $($Rule | FL | Out-String)" -Level VERBOSE
                                        $Acl.RemoveAuditRule($ExistingRule)
                                    }
                                    catch [Exception]
                                    {
                                        Write-Log -Message "Error removing audit rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING 
                                    }
                                }
                            }

                            foreach ($Rule in $AuditRules)
                            {
                                [System.Boolean]$Found = $false

                                foreach ($ExistingRule in ($Acl.GetAuditRules($script:EXPLICIT_TRUE, $true, [System.Security.Principal.NTAccount]) | Where-Object {$_.IsInherited -eq $false }))
                                {
                                    $Found = Get-AuthorizationRuleComparison -Rule1 $ExistingRule -Rule2 $Rule

                                    if ($Found -eq $true)
                                    {
                                        break
                                    }
                                }

                                #Did not find a matching, existing rule
                                if ($Found -eq $false)
                                {
                                    try
                                    {
                                        Write-Log -Message "Adding audit rule $($Rule | FL | Out-String)" -Level VERBOSE
                                        $Acl.AddAuditRule($Rule)
                                    }
                                    catch [Exception]
                                    {
                                        Write-Log -Message "Error adding audit rule : $($Rule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                    }
                                }
                            }

                            #Call set access control since we've already added the rules
                            $Item.SetAccessControl($Acl)
                        }

                        break
                    }
                    default {
                        throw "Could not determine parameter set name"
                    }
                }
                
                if ($PSCmdlet.ParameterSetName -like "Replace*" -or $PSCmdlet.ParameterSetName -eq "Add")
                {
                    #Add new access rules
                    if($AccessRules.Length -gt 0)
                    {
                        foreach ($Rule in $AccessRules) 
                        {
                            $Acl.AddAccessRule($Rule)
                        }

                        $Item.SetAccessControl($Acl)
                    }

                    #Add new audit rules
                    if ($AuditRules.Length -gt 0)
                    {
                        foreach ($Rule in $AuditRules)
                        {
                            $Acl.AddAuditRule($Rule)
                        }

                        $Item.SetAccessControl($Acl)
                    }    
                }

                #If child permissions should be forced to inherit
                if (($ForceChildInheritance -or $EnableChildInheritance) -and [System.IO.Directory]::Exists($Path))
                {
                    Write-Log -Message "Evaluating child items" -Level VERBOSE
                    Get-ChildItem -Path $Path -Recurse -Force | ForEach-Object {

                        $ChildItem = Get-Item -Path $_.FullName
                        [System.Security.AccessControl.FileSystemSecurity]$ChildAcl = $ChildItem.GetAccessControl(@([System.Security.AccessControl.AccessControlSections]::Access, [System.Security.AccessControl.AccessControlSections]::Audit))

                        if ($AccessRules.Length -gt 0 -or $PSCmdlet.ParameterSetName -eq "Reset")
                        {
                            if ($ForceChildInheritance)
                            {
                                Write-Log -Message "Forcing access rule inheritance on $($ChildItem.FullName)" -Level VERBOSE

                                foreach ($ChildRule in ($ChildAcl.Access | Where-Object {$_.IsInherited -eq $false }))
                                {
                                    try
                                    {
                                        $ChildAcl.RemoveAccessRule($ChildRule) | Out-Null
                                    }
                                    catch [Exception]
                                    {
                                        Write-Log -Message "Error removing ACL from $($ChildItem.FullName)`: $($ChildRule | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                    }
                                }
                            }

                            try
                            {
                                $ChildAcl.SetAccessRuleProtection($IsProtectedFromInheritance, $PreserveInheritedRules)
                                $ChildItem.SetAccessControl($ChildAcl)
                            }
                            catch [Exception]
                            {
                                Write-Log -Message "Could not set ACL on path $ChildPath." -ErrorRecord $_ -Level WARNING
                            }
                        }

                        if ($AuditRules.Length -gt 0 -or $PSCmdlet.ParameterSetName -eq "Reset")
                        {
                            Write-Log -Message "Forcing audit rule inheritance on $($ChildItem.FullName)" -Level VERBOSE

                            [System.Security.AccessControl.AuthorizationRuleCollection]$OldChildAuditRules = $ChildAcl.GetAuditRules($script:EXPLICIT_TRUE, $script:INHERITED_FALSE, [System.Security.Principal.NTAccount])

                            if ($ForceChildInheritance)
                            {
                                foreach ($ChildAudit in $OldChildAuditRules)
                                {
                                    try
                                    {
                                        $ChildAcl.RemoveAuditRule($ChildAudit) | Out-Null
                                    }
                                    catch [Exception]
                                    {
                                        Write-Log -Message "Error removing audit from $($ChildItem.FullName)`: $($ChildAudit | FL | Out-String)" -ErrorRecord $_ -Level WARNING
                                    }
                                }
                            }

                            try
                            {
                                $ChildAcl.SetAccessRuleProtection($IsProtectedFromInheritance, $PreserveInheritedRules)
                                $ChildItem.SetAccessControl($ChildAcl)
                            }
                            catch [Exception]
                            {
                                Write-Log -Message "Could not set ACL on path $ChildPath." -ErrorRecord $_ -Level WARNING
                            }
                        }
                    }
                }                   
            }
            else
            {
                Write-Log -Message "Could not retrieve the ACL for $Path." -Level WARNING
            }
        }
        catch [System.Exception]
        {
            Write-Log -ErrorRecord $_ -Level WARNING
        }

        Pop-Location
    }
    
    End {
    }
}

Function Set-Owner {
    <#
        .SYNOPSIS
            Changes owner of a file or folder to another user or group.
 
        .DESCRIPTION
            Changes owner of a file or folder to another user or group.
 
        .PARAMETER Path
            The folder or file that will have the owner changed.
 
        .PARAMETER Account
            Optional parameter to change owner of a file or folder to specified account.
 
            Default value is 'Builtin\Administrators'
 
        .PARAMETER Recurse
            Recursively set ownership on subfolders and files beneath given folder. If the specified path is a file, this parameter is ignored.
 
        .EXAMPLE
            PS C:\>Set-Owner -Path C:\temp\test.txt
 
            Changes the owner of test.txt to Builtin\Administrators
 
        .EXAMPLE
            PS C:\>Set-Owner -Path C:\temp\test.txt -Account Domain\user
 
            Changes the owner of test.txt to Domain\user
 
        .EXAMPLE
            PS C:\>Set-Owner -Path C:\temp -Recurse
 
            Changes the owner of all files and folders under C:\Temp to Builtin\Administrators
 
        .EXAMPLE
            PS C:\>Get-ChildItem C:\Temp | Set-Owner -Recurse -Account 'Domain\Administrator'
 
            Changes the owner of all files and folders under C:\Temp to Domain\Administrator
 
        .INPUTS
            None
 
        .OUTPUTS
            None
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/23/2017
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param (
        [Parameter(Position = 0, ValueFromPipeline = $true, Mandatory = $true)]
        [Alias("FullName")]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Path,

        [Parameter(Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Account = 'BUILTIN\Administrators',

        [Parameter()]
        [Switch]$Recurse,

        [Parameter()]
        [Switch]$Force
    )

    Begin {
        if (-not (Test-IsLocalAdmin)) {
            throw "Run the cmdlet with elevated credentials."
        }

        Set-TokenPrivilege -Privileges @("SeRestorePrivilege","SeBackupPrivilege","SeTakeOwnershipPrivilege") -Enable
    }

    Process {
        Write-Log -Message "Set Owner Path: $Path" -Level VERBOSE
        $Account = Get-AccountTranslatedNTName -UserName $Account
        Write-Log -Message "Account Name: $Account" -Level VERBOSE

        #The ACL objects do not like being used more than once, so re-create them on the Process block
        $DirOwner = New-Object System.Security.AccessControl.DirectorySecurity
        $DirOwner.SetOwner((New-Object -TypeName System.Security.Principal.NTAccount($Account)))
        
        $FileOwner = New-Object System.Security.AccessControl.FileSecurity
        $FileOwner.SetOwner((New-Object -TypeName System.Security.Principal.NTAccount($Account)))
        
        try {
            $Item = Get-Item -LiteralPath $Path -Force -ErrorAction Stop

            if (-not $Item.PSIsContainer) 
            {
                $ConfirmMessage = "You are about to change the owner of $($Item.FullName) to $Account."
                $WhatIfDescription = "Set Owner of $($Item.FullName)."
                $ConfirmCaption = "Set File Owner"

                if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
                {
                    $Item.SetAccessControl($FileOwner)
                    Write-Log -Message "Set ownership to $Account on $($Item.FullName)" -Level VERBOSE
                }
            }
            else
            {
                $ConfirmMessage = "You are about to change the owner of $($Item.FullName) to $Account."
                $WhatIfDescription = "Set Owner of $($Item.FullName)."
                $ConfirmCaption = "Set Directory Owner"

                if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
                {
                    $Item.SetAccessControl($DirOwner)
                    Write-Log -Message "Set ownership to $Account on $($Item.FullName)" -Level VERBOSE
                }

                if ($Recurse) 
                {
                    Get-ChildItem $Item -Force -Recurse | ForEach-Object {
                        Set-Owner -Path $_.FullName -Account $Account -Force
                    }
                }
            }
        }
        catch [Exception] {
            Write-Log -Message "Failed to set owner on $($Item.FullName)" -ErrorRecord $_ -Level WARNING
        }
    }

    End {
        Set-TokenPrivilege -Privileges @("SeRestorePrivilege","SeBackupPrivilege","SeTakeOwnershipPrivilege") -Disable
    }
}

Function Invoke-ForceDelete {
    <#
        .SYNOPSIS
            The cmdlet forces the deletion of a file or folder and all of its content.
 
        .DESCRIPTION
            The cmdlet takes ownership of the file or content in a directory and grants the current user
            full control permissions to the item. Then it deletes the item and performs this recursively
            through the directory structure specified.
         
        .PARAMETER Path
            The path to the file or folder to forcefully delete.
 
        .PARAMETER Force
            Ignores the confirmation to delete each item.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Invoke-ForceDelete -Path c:\windows.old
 
            Forcefully deletes the c:\windows.old directory and all of its content.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 4/24/2017
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = "HIGH")]
    [OutputType()]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ 
            try {
               Test-Path -Path $_ -ErrorAction Stop
            }
            catch [System.UnauthorizedAccessException] {
                $true
            } 
        })]
        [System.String]$Path,

        [Parameter()]
        [Switch]$Force
    )

    Begin {
    }

    Process {    
        # Fix any paths that were fed in dot sourced
        $Path = Resolve-Path -Path $Path
        $IsDir = [System.IO.Directory]::Exists($Path)

        Write-Log -Message "Invoke-ForceDelete cmdlet called with path $Path." -Level VERBOSE

        [System.String]$UserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
        $UserName = Get-AccountTranslatedNTName -UserName $UserName

        # Take ownership of the provided path
        Set-Owner -Path $Path -Account $UserName -Recurse -Force

        # Full Control to "This folder, subfolders, and files"
        [System.Security.Principal.NTAccount]$NTAccount = New-Object -TypeName System.Security.Principal.NTAccount($UserName)
        [System.Security.Principal.SecurityIdentifier]$Sid = $NTAccount.Translate([System.Security.Principal.SecurityIdentifier])
        
        if ($IsDir)
        {
            $Ace = New-Object System.Security.AccessControl.FileSystemAccessRule($Sid,
                [System.Security.AccessControl.FileSystemRights]::FullControl,
                ([System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit),
                [System.Security.AccessControl.PropagationFlags]::None,
                [System.Security.AccessControl.AccessControlType]::Allow       
            )
        }
        else
        {
            $Ace = New-Object System.Security.AccessControl.FileSystemAccessRule($Sid,
                [System.Security.AccessControl.FileSystemRights]::FullControl,
                [System.Security.AccessControl.InheritanceFlags]::None,
                [System.Security.AccessControl.PropagationFlags]::None,
                [System.Security.AccessControl.AccessControlType]::Allow       
            )
        }

        Set-FileSecurity -Path $Path -AccessRules $Ace -ForceChildInheritance

        # If it's a directory, remove all of the child content
        if ($IsDir)
        {
            Write-Log -Message "The current path $Path is a directory." -Level VERBOSE

            Get-ChildItem -Path $Path -Force | ForEach-Object {         
                Invoke-ForceDelete -Path $_.FullName -Force
            }
        }
        
        # Remove the specified path whether it is a folder or file
        try
        {    
            $ConfirmMessage = "You are about to force delete $Path."
            $WhatIfDescription = "Deleted $Path."
            $ConfirmCaption = "Force Delete"

            if ($Force -or $PSCmdlet.ShouldProcess($WhatIfDescription, $ConfirmMessage, $ConfirmCaption))
            {
                Write-Log -Message "Deleting $Path" -Level VERBOSE
                Remove-Item -Path $Path -Confirm:$false -Force -Recurse

                $Counter = 0

                do 
                {
                    try {
                        $Found = Test-Path -Path $Path -ErrorAction Stop
                    }
                    catch [System.UnauthorizedAccessException] {
                        $Found = $true
                    }

                    Start-Sleep -Milliseconds 100
                
                } while (($Found -eq $true) -and $Counter++ -lt 50)

                if ($Counter -eq 50)
                {
                    Write-Log -Message "Timeout waiting for $Path to delete" -Level WARNING
                }
            }
        }
        catch [Exception]
        {
            Write-Log -ErrorRecord $_ -Level WARNING
        }      
    }

    End {
    }
}

Function Rename-FileOrDirectory {
    <#
        .SYNOPSIS
            The cmdlet renames a file or directory and uses an incrementing counter appended to the desired filename if it already exists.
 
        .DESCRIPTION
            The cmdlet attempts to rename a file with the specified new name. If the new name already exists, the postfix "(#)" is added before the file extension,
            or the end of the directory name, where "#" is a number starting from 1 and incrementing by 1 until the new name is unique in the directory.
 
        .PARAMETER Path
            The file or directory to rename.
 
        .PARAMETER NewName
            The new name of the file or directory. Use a literal, not relative, path.
 
        .PARAMETER Credential
            The credential to use to perform the operation.
 
        .EXAMPLE
            Rename-FileOrDirectory -Path c:\temp\file1.txt -NewName c:\temp\file2.txt -PassThru
 
            In this example the file c:\temp\file2.txt and c:\temp\file2(1).txt already exists. The file c:\temp\file1.txt is renamed to
            c:\temp\file2(2).txt and the file info about its resulting name is returned to the pipeline.
 
        .INPUTS
            System.String
 
        .OUTPUTS
            None or System.IO.DirectoryInfo or System.IO.FileInfo
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/23/2017
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo], [System.IO.DirectoryInfo])]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [ValidateNotNullOrEmpty()]
        [System.String]$Path,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.String]$NewName,

        [Parameter()]
        [Switch]$Force,

        [Parameter()]
        [ValidateNotNull()]
        [System.Management.Automation.Credential()]
        [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty,

        [Parameter()]
        [Switch]$PassThru
    )

    Begin{
    }

    Process {
        [System.IO.FileInfo]$Info = New-Object -TypeName System.IO.FileInfo($Path)
        $Base = $Info.DirectoryName
        $Name = $Info.BaseName
        $Ext = $Info.Extension

        $Counter = 1

        while (Test-Path -Path $NewName)
        {
            $NewName = "$Base\$Name($Counter)$Ext"
            $Counter++
        }

        [System.Collections.Hashtable]$Splat = @{}

        if ($Force)
        {
            $Splat.Add("Force", $true)
        }

        if ($Credential -ne [System.Management.Automation.PSCredential]::Empty)
        {
            $Splat.Add("Credential", $Credential)
        }

        if ($PassThru)
        {
            $Splat.Add("PassThru", $true)
        }

        Rename-Item -Path $Path -NewName $NewName @Splat
    }

    End {
    }
}

Function Get-FileVersion {
    <#
        .SYNOPSIS
            Gets the version of a specific file or file running a Windows service from its metadata.
 
        .DESCRIPTION
            This cmdlet gets the FileVersion data from a specified file or file running a service. If no version is included in the FileInfo, the cmdlet returns "0".
 
        .PARAMETER Path
            The path to the file.
 
        .PARAMETER Service
            The name of the service.
 
        .INPUTS
            None
 
        .OUTPUTS
            System.String
 
        .EXAMPLE
            Get-FileVersion -Path "c:\installer.exe"
 
            Gets the file version of installer.exe.
 
        .EXAMPLE
            Get-FileVersion -Service lmhosts
 
            Gets the file version of the svchost.exe running the lmhosts service.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 8/24/2016
    #>

    [CmdletBinding()]
    [OutputType([System.String])]
    Param(
        [Parameter(Mandatory = $true, ParameterSetName = "File", ValueFromPipeline = $true, Position = 0)]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Path
    )

    DynamicParam
    {
        [System.Management.Automation.RuntimeDefinedParameterDictionary]$ParamDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        $Services = Get-Service | Select-Object -ExpandProperty Name
        New-DynamicParameter -Name "ServiceName" -ParameterSets "Service" -Type ([System.String]) -Mandatory -ValueFromPipeline -Position 0 -ValidateSet $Services -RuntimeParameterDictionary $ParamDictionary | Out-Null

        return $ParamDictionary
    }

    Begin {
    }

    Process {
        switch ($PSCmdlet.ParameterSetName) {
            "File" {
                break
            }
            "Service" {
                $Path = (Get-WmiObject -Class Win32_Service -Filter "Name = `"$($PSBoundParameters.ServiceName)`"" | Select-Object -ExpandProperty PathName).Trim("`"")
                break
            }
            default {
                Write-Log -Message "Could not determine parameter set name from given parameters." -Level FATAL
            }
        }

        $Version = New-Object -TypeName System.IO.FileInfo($Path) | Select-Object -ExpandProperty VersionInfo | Select-Object -ExpandProperty FileVersion

        if ([System.String]::IsNullOrEmpty($Version))
        {
            $Version = "0"
        }

        Write-Output -InputObject $Version
    }

    End {    
    }
}

Function Invoke-ExtractZip {
    <#
        .SYNOPSIS
            The cmdlet extracts the contents of a zip file to a specified destination.
 
        .DESCRIPTION
            The cmdlet extracts the contents of a zip file to a specified destination and optionally preserves the contents in the destination if they already exist.
 
        .PARAMETER Source
            The path to the zip file.
 
        .PARAMETER Destination
            The folder where the zip file should be extracted. The destination is created if it does not already exist.
 
        .PARAMETER NoOverwrite
            Specify if the contents in the destination should be preserved if they already exist.
 
        .PARAMETER OverwriteIfNewer
            Only overwrite existing files if the file in the zip is newer (by last modification date) than the existing file.
 
        .INPUTS
            None
         
        .OUTPUTS
            None
 
        .EXAMPLE
            Invoke-ExtractZip -Source "c:\test.zip" -Destination "c:\test"
 
            Extracts the contents of test.zip to c:\test.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/27/2017
    #>

    [CmdletBinding(DefaultParameterSetName = "Overwrite")]
    [OutputType()]
    Param(
        [Parameter(Position = 0, Mandatory = $true)]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Source,

        [Parameter(Position = 1, Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Destination,

        [Parameter(ParameterSetName = "No")]
        [Switch]$NoOverwrite,

        [Parameter(ParameterSetName = "Newer")]
        [Switch]$OverwriteIfNewer
    )

    Begin {
        Add-Type -AssemblyName System.IO.Compression.FileSystem
    }

    Process {
        if (!(Test-Path -Path $Source)) {
            throw (New-Object -TypeName System.IO.FileNotFoundException("Source zip file not found."))
        }

        if (-not (Test-Path -Path $Destination)) {

            Write-Log -Message "Zip extract destination $Destination does not exist, creating it."

            try {
                New-Item -Path $Destination -ItemType Directory | Out-Null

                $Counter = 0

                while (!(Test-Path -Path $Destination)) {
                    Start-Sleep -Seconds 1
                    $Counter++

                    if ($Counter -gt 60) {
                        throw "Timeout error waiting for the zip extraction destination $Destination to be created."
                    }
                }
            }
            catch [Exception] {
                Write-Log -ErrorRecord $_ -Level FATAL
            }
        }
        else {
            if ([System.IO.File]::Exists($Destination)) {
                throw (New-Object -TypeName System.IO.DirectoryNotFoundException("The destination is a file, not a directory."))
            }
        }

        [System.IO.Compression.ZipArchive]$ZipArchive = [System.IO.Compression.ZipFile]::OpenRead($Source)
            
        try
        {
            foreach ($ZipArchiveEntry in $ZipArchive.Entries) 
            {
                $FullPath = [System.IO.Path]::Combine($Destination, $ZipArchiveEntry.FullName)
                [System.String]$Directory = [System.IO.Path]::GetDirectoryName($FullPath)
                
                if (-not [System.IO.Directory]::Exists($Directory))
                {
                    [System.IO.Directory]::CreateDirectory($Directory)
                }

                Write-Log -Message "Evaluating $($ZipArchiveEntry.FullName) to unzip to $FullPath." -Level VERBOSE

                # If we don't want to overwrite existing files, check to see if the path exists
                if ($NoOverwrite)
                {
                    # If it doesn't exist, we'll extract
                    if (-not (Test-Path -Path $FullPath))
                    {
                        # To handle the race condition of extracting it and the file could have been created before the extraction occurs
                        # catch the IOException and check to see if it's because the file already exists, if it is, ignore the exception, otherwise,
                        # throw the exception
                        try
                        {
                            [System.IO.Compression.ZipFileExtensions]::ExtractToFile($ZipArchiveEntry, $FullPath)
                        }
                        catch [System.IO.IOException]
                        {
                            if ($_.Exception.Message -inotlike "*already exists.")
                            {
                                throw $_.Exception
                            }
                        }
                    }
                }
                else
                {
                    # If we are overwriting files, but only want to overwrite if the zip file is newer, check to see if it exists
                    if ($OverwriteIfNewer)
                    {
                        # If the file exists, check the last modified times
                        if (Test-Path -Path $FullPath)
                        {
                            if ((New-Object -TypeName System.IO.FileInfo($FullPath)).LastWriteTimeUtc -lt $ZipArchiveEntry.LastWriteTime.ToUniversalTime().Date)
                            {
                                Write-Log -Message "Overwriting zip output $FullPath with newer file." -Level VERBOSE
                                [System.IO.Compression.ZipFileExtensions]::ExtractToFile($ZipArchiveEntry, $FullPath, $true)
                            }
                        }
                        else
                        {
                            [System.IO.Compression.ZipFileExtensions]::ExtractToFile($ZipArchiveEntry, $FullPath, $true)
                        }
                    }
                    else
                    {
                        [System.IO.Compression.ZipFileExtensions]::ExtractToFile($ZipArchiveEntry, $FullPath, $true)
                    }
                }
            }
        }
        catch [Exception]
        {
            Write-Log -ErrorRecord $_ -Level FATAL
        }
        finally
        {
            $ZipArchive.Dispose()
        }
    }

    End {        
    }
}

Function Invoke-ExtractGZip {
    <#
        .SYNOPSIS
            The cmdlet extracts the contents of a gzip file to a specified destination.
 
        .DESCRIPTION
            The cmdlet first examines the gzip file to see if it contains concatenated files. If it does, it separates those chunks and decompresses each chunk to its
            own file, or if specified, to the same file. If the contents are each written to their own file, each file is written to the destination directory using
            a "(#)" post fix for each file after the first. The decompressed files will not use an extension, or can optionally use an extension you specify
 
        .PARAMETER Source
            The path to the gzip file.
 
        .PARAMETER Destination
            The folder where the gzip file should be extracted. The destination is created if it does not already exist.
 
        .PARAMETER CreateSingleFile
            Creates a single file in case the gzip contains concatenated gzip files. If this is not specified and the gzip contains concatenated files, each
            concatenated file it written to its own output file.
 
        .PARAMETER NoOverwrite
            Specify this to not overwrite an existing file in the destination directory.
 
        .PARAMETER Extension
            The extension to use on the decompressed files. If this is not specified, any extension contained in the filename is used. For example, if the
            input source is file.json.gz, the extension on the output is .json since the .gz is stripped off. If you provided "txt" as the extension, the output
            file would be file.json.txt.
 
        .INPUTS
            System.String
         
        .OUTPUTS
            None or System.IO.FileInfo[]
 
        .EXAMPLE
            Invoke-ExtractGZip -Source "c:\test.gz" -Destination "c:\test" -Extension "txt"
 
            Extracts the contents of test.gzip to c:\test\test.txt
 
        .EXAMPLE
            Invoke-ExtractGZip -Source "c:\test.json.gz" -Destination "c:\test"
 
            In this example, the gzip contains 2 concatenated files. The output is:
 
            c:\test\test.json
            c:\test\test(1).json
 
        .EXAMPLE
            Invoke-ExtractGZip -Source "c:\test.json.gz" -Destination "c:\test" -CreateSingleFile
 
            In this example, the gzip contains 2 concatenated files, but the contents of both files are output to a single file, c:\test\test.json.
         
            As a note, the resulting json file would not be well-formatted since it contains to JSON objects not inside an array.
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 10/27/2017
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo[]])]
    Param(
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$Source,

        [Parameter(Position = 1, Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Destination,

        [Parameter()]
        [Switch]$NoOverwrite,

        [Parameter()]
        [Switch]$CreateSingleFile,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$Extension,

        [Parameter()]
        [Switch]$PassThru
    )

    Begin {
        Add-Type -AssemblyName System.IO.Compression.FileSystem
    }

    Process {
        [System.Byte[]]$FileBytes = [System.IO.File]::ReadAllBytes($Source)
        [System.Int32[]]$StartIndexes = @()
        
        # This pattern indicates the start of a GZip file as found from looking at the files
        # The file header is 10 bytes in size
        # 0-1 Signature 0x1F, 0x8B
        # 2 Compression Method - 0x08 is for DEFLATE, 0-7 are reserved
        # 3 Flags
        # 4-7 Last Modification Time
        # 8 Compression Flags
        # 9 Operating System
        
        [System.Byte[]]$StartOfFilePattern = [System.Byte[]](0x1F, 0x8B, 0x08)

        # This will limit the last byte we check to make sure it doesn't exceed the end of the file
        # If the file is 100 bytes and the file pattern is 10 bytes, the last byte we want to check is
        # 90 -> i.e. we will check index 90, 91, 92, 93, 94, 95, 96, 97, 98, 99 and index 99 is the last
        # index in the file bytes
        [System.Int64]$TraversableLength = $FileBytes.LongLength - $StartOfFilePattern.LongLength

        for ($i = 0; $i -le $TraversableLength; $i++)
        {
            [System.Boolean]$Match = $true

            # Test the next run of characters to see if they match
            for ($j = 0; $j -lt $StartOfFilePattern.Length; $j++)
            {
                # If the character doesn't match, break out
                # We're making sure that i + j doesn't exceed the length as part
                # of the loop bounds
                if ($FileBytes[$i + $j] -ne $StartOfFilePattern[$j])
                {
                    $Match = $false
                    break
                }
            }

            # If we did find a run of matching bytes
            if ($Match -eq $true)
            {
                $StartIndexes += $i
                # Since we had a match, move the index ahead the length of the start of file pattern
                # Then the for loop will add 1 to move to the next index
                $i += $StartOfFilePattern.Length
            }
        }

        # In case the pattern doesn't match, just start from the beginning of the file
        if ($StartIndexes.Count -eq 0)
        {
            $StartIndexes += 0
        }

        [System.Collections.Generic.List[System.Byte[]]]$Chunks = New-Object -TypeName System.Collections.Generic.List[System.Byte[]]

        for ($i = 0; $i -lt $StartIndexes.Count; $i++)
        {
            [System.Int32]$Start = $StartIndexes[$i]

            [System.Int32]$Length = 0

            # If this index is the last index, take the rest of the file bytes
            if ($i + 1 -eq $StartIndexes.Count)
            {
                $Length = $FileBytes.Length - $Start
            }
            # Otherwise take the chunk from this start to the next one
            else
            {
                # The length to read is the next start index minus the current start index
                $Length = $StartIndexes[$i + 1] - $StartIndexes[$i]
            }

            if ($Length -gt 0)
            {
                $Chunks.Add(($FileBytes | Select-Object -Skip $Start -First $Length))
            }
        }

        [System.IO.MemoryStream]$MStreamOut = New-Object -TypeName System.IO.MemoryStream
        [System.IO.FileInfo]$Info = New-Object -TypeName System.IO.FileInfo($Source)
        [System.Int32]$Counter = 0

        if (-not [System.String]::IsNullOrEmpty($Extension))
        {
            if ($Extension.StartsWith("."))
            {
                $Extension = $Extension.Substring(1)
            }
        }

        if (-not $CreateSingleFile)
        {
            [System.String]$BaseName = $Info.BaseName

            if ([System.String]::IsNullOrEmpty($Extension))
            {
                if ($BaseName.Contains("."))
                {
                    $Extension = $BaseName.Substring($BaseName.IndexOf(".") + 1)
                    $BaseName = $BaseName.Substring(0, $BaseName.IndexOf("."))
                }
            }
        }
        
        [System.IO.FileInfo[]]$Results = @()

        try
        {
            foreach ($Chunk in $Chunks)
            {
                [System.IO.MemoryStream]$MStream = New-Object -TypeName System.IO.MemoryStream(,$Chunk)

                try
                {
                    [System.IO.Compression.GZipStream]$GZStream = New-Object -TypeName System.IO.Compression.GZipStream($MStream, [System.IO.Compression.CompressionMode]::Decompress)
                
                    try
                    {
                        $GZStream.CopyTo($MStreamOut)

                        if (-not $CreateSingleFile)
                        {        
                            [System.String]$FileName = [System.IO.Path]::Combine($Destination, "$BaseName$(if ($Counter -gt 0) {"($Counter)"})$(if (-not [System.String]::IsNullOrEmpty($Extension)) { ".$Extension" })")                            
                            $Counter++

                            [System.IO.File]::WriteAllBytes($FileName, $MStreamOut.ToArray())
                            $Results += (Get-Item -Path $FileName)

                            # Reset the output memory stream
                            $MStreamOut.SetLength(0)
                        }
                    }
                    finally
                    {
                        $GZStream.Dispose()
                    }
                }
                finally
                {
                    $MStream.Dispose()
                }
            }
                
            if ($CreateSingleFile)
            {
                $FileName = [System.IO.Path]::Combine($Destination, "$($Info.BaseName)$(if (-not [System.String]::IsNullOrEmpty($Extension)) {".$Extension"})")
                [System.IO.File]::WriteAllBytes($FileName, $MStreamOut.ToArray())
                $Results += (Get-Item -Path $FileName)
            }
        }
        finally
        {
            $MStreamOut.Dispose()
        }

        if ($PassThru)
        {
            Write-Output -InputObject $Results
        }
    }

    End {
    }
}

Function New-ISO {
    <#
        .SYNOPSIS
            Creates a new ISO image from the specified content.
 
        .DESCRIPTION
            This cmdlet creates in ISO image file containing specified content. It can also be used to make a bootable ISO with something like WinPE.
 
        .PARAMETER Content
            The FileInfo, DirectoryInfo, or string Path names of the files and folders to include in the ISO. Each item will be added to the root of the ISO file system tree.
 
        .PARAMETER Destination
            The location the ISO file will be created and written to. Use -Force to overwrite an existing file.
 
        .PARAMETER BootFile
            The path to a boot file. For example c:\Program Files (X86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\efisys.bin".
 
        .PARAMETER Media
            The media type to emulate with the ISO. This defaults to DVDPLUSRW_DUALLAYER.
 
        .PARAMETER Title
            The title of the ISO image file.
 
        .EXAMPLE
            New-ISO -Content c:\users\john.smith\Desktop -Destination c:\users\john.smith\desktop\backup.iso
 
            This creates a new ISO image file on the user's desktop containing the desktop folder at the root of the ISO with all of its contents inside.
 
        .EXAMPLE
            Get-ChildItem -Path c:\users\john.smith\Desktop | New-ISO -Destination c:\users\john.smith\desktop\backup.iso
 
            This creates a new ISO image file on the user's desktop containing the contents of the desktop folder at the root of the ISO.
 
        .EXAMPLE
            Get-ChildItem -Path c:\WinPE | New-ISO -Destination c:\temp\WinPE.iso -BootFile "$env:ProgramFiles(x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\efisys.bin" -Media DVDPLUSR -Title "WinPE"
 
            This creates a bootable ISO from WinPE and includes the contents of the c:\WinPE folder in the image.
 
        .INPUT
            System.Object[]
 
        .OUTPUT
            System.IO.FileInfo
 
        .NOTES
            AUTHOR: Michael Haken
            LAST UPDATE: 2/23/2019
    #>


    [CmdletBinding(DefaultParameterSetName = "Content")]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ParameterSetName = "Content", ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [System.Object[]]$Content,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [System.String]$Destination,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({
            Test-Path -Path $_
        })]
        [System.String]$BootFile = [System.String]::Empty,

        [Parameter()]
        [ValidateSet("CDR","CDRW","DVDRAM","DVDPLUSR","DVDPLUSRW","DVDPLUSR_DUALLAYER","DVDDASHR","DVDDASHRW","DVDDASHR_DUALLAYER","DISK","DVDPLUSRW_DUALLAYER","BDR","BDRE")]
        [System.String]$Media = "DVDPLUSRW_DUALLAYER",

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]$Title = ([System.DateTime]::UtcNow.ToString("yyyy-mm-ddThh:mm:ss.fff")),

        [Parameter()]
        [Switch]$Force
    )

    Begin {
        if (-not [System.String]::IsNullOrEmpty($BootFile)) 
        {       
            if (@("BDR","BDRE") -contains $Media) 
            { 
                throw "Bootable image doesn't work with media type $Media." 
            } 

            $BootFileName = Get-Item -LiteralPath $BootFile | Select-Object -ExpandProperty FullName
            $Stream = New-Object -ComObject ADODB.Stream -Property @{Type = 1}
            $Stream.Open()
            $Stream.LoadFromFile($BootFileName)
            $BootOptions = New-Object -ComObject IMAPI2FS.BootOptions
            $BootOptions.AssignBootImage($Stream)
        }
        
        if (([System.AppDomain]::CurrentDomain.GetAssemblies() | 
            Where-Object { -not [System.String]::IsNullOrEmpty($_.Location) } | 
            Select-Object -Property @{Name = "Type"; Expression = {$_.GetTypes()}} |
            Select-Object -ExpandProperty Type |
            Where-Object { $_.FullName -eq "BAMCIS.FileIO.ISO.IMAPI_MEDIA_PHYSICAL_TYPE" }).Count -eq 0)
        {
            # The members are prefaced with IMAPI_MEDIA_TYPE_ in the real enum
            Add-Type -TypeDefinition @"
namespace BAMCIS.FileIO.ISO
{
    public enum IMAPI_MEDIA_PHYSICAL_TYPE
    {
        UNKNOWN,
        CDROM,
        CDR,
        CDRW,
        DVDROM,
        DVDRAM,
        DVDPLUSR,
        DVDPLUSRW,
        DVDPLUSR_DUALLAYER,
        DVDDASHR,
        DVDDASHRW,
        DVDDASHR_DUALLAYER,
        DISK,
        DVDPLUSRW_DUALLAYER,
        HDDVDROM,
        HDDVDR,
        HDDVDRAM,
        BDROM,
        BDR,
        BDRE,
        MAX
    }
}
"@

        }

        
        if (([System.AppDomain]::CurrentDomain.GetAssemblies() | 
            Where-Object { -not [System.String]::IsNullOrEmpty($_.Location) } | 
            Select-Object -Property @{Name = "Type"; Expression = {$_.GetTypes()}} |
            Select-Object -ExpandProperty Type |
            Where-Object { $_.FullName -eq "BAMCIS.FileIO.ISO.ISOFile" }).Count -eq 0)
        {
            [System.CodeDom.Compiler.CompilerParameters]$CompilerParameters = New-Object -TypeName System.CodeDom.Compiler.CompilerParameters
            $CompilerParameters.CompilerOptions = "/unsafe"

            # Needs to be unsafe so that we can reference the address for the Bytes variable
            # and create and IntPtr from it that is used by the stream to track where in the
            # input stream the reader currently is
            Add-Type -CompilerParameters $CompilerParameters -TypeDefinition @"
using System.Runtime.InteropServices.ComTypes;
using System.IO;
using System;
 
namespace BAMCIS.FileIO.ISO
{
    public class ISOFile
    {
        private ISOFile()
        {
        }
 
        public unsafe static void Create(string path, object stream, int blockSize, int totalBlocks)
        {
            int Bytes = 0;
            IntPtr SeekPointer = (IntPtr)(&Bytes);
            byte[] Buffer = new byte[blockSize];
            IStream InputStream = stream as IStream;
 
            using (FileStream FStream = System.IO.File.OpenWrite(path))
            {
                if (FStream != null)
                {
                    while (totalBlocks > 0)
                    {
                        InputStream.Read(Buffer, blockSize, SeekPointer);
                        FStream.Write(Buffer, 0, Bytes);
                        totalBlocks += -1;
                    }
                }
                else
                {
                    throw new InvalidOperationException(String.Format("The file stream at {0} was null.", path));
                }
            }
        }
    }
}
"@

        }

        [BAMCIS.FileIO.ISO.IMAPI_MEDIA_PHYSICAL_TYPE]$MediaType = [System.Enum]::Parse([BAMCIS.FileIO.ISO.IMAPI_MEDIA_PHYSICAL_TYPE], $Media)
       
        Write-Verbose -Message "Selected media type is $($MediaType.ToString()) with value $([System.Int32]$MediaType)."

        $Image = New-Object -ComObject IMAPI2FS.MsftFileSystemImage -Property @{VolumeName = $Title}
        $Image.ChooseImageDefaultsForMediaType(([Int32]$MediaType)) 
    }

    Process {
        foreach ($Item in $Content)
        {
            if ($Item -isnot [System.IO.FileInfo] -and $Item -isnot [System.IO.DirectoryInfo])
            {
                $Item = Get-Item -LiteralPath $Item
            }

            Write-Verbose -Message "Adding $($Item.FullName) to image."

            try 
            { 
                $Image.Root.AddTree($Item.FullName, $true) 
            } 
            catch [Exception] 
            { 
                throw "$($_.Exception.Message) : Try a different media type." 
            } 
        }
    }

    End {
        if ($BootOptions -ne $null) 
        { 
            $Image.BootImageOptions = $BootOptions 
        }  
    
        $Result = $Image.CreateResultImage()  

        if (-not ($Target = New-Item -Path $Destination -ItemType File -Force:$Force -ErrorAction SilentlyContinue))
        {
            throw "Could not create file at $Destination. Use -Force parameter to overwrite an existing file."
        }

        [BAMCIS.FileIO.ISO.ISOFile]::Create(
            $Target.FullName,
            $Result.ImageStream,
            $Result.BlockSize,
            $Result.TotalBlocks
        ) 
    
        Write-Verbose -Message "Target image $($Target.FullName) has been created"
    
        Write-Output -InputObject $Target
    }
}