DSCResources/MSFT_xArchive/MSFT_xArchive.psm1

data LocalizedData
{
    # culture="en-US"
    # TODO: Support WhatIf
    ConvertFrom-StringData @'
InvalidChecksumArgsMessage = Specifying a Checksum without requesting content validation (the Validate parameter) is not meaningful
InvalidDestinationDirectory = The specified destination directory {0} does not exist or is not a directory
InvalidSourcePath = The specified source file {0} does not exist or is not a file
InvalidNetSourcePath = The specified source file {0} is not a valid net source path
ErrorOpeningExistingFile = An error occurred while opening the file {0} on disk. Please examine the inner exception for details
ErrorOpeningArchiveFile = An error occurred while opening the archive file {0}. Please examine the inner exception for details
ItemExistsButIsWrongType = The named item ({0}) exists but is not the expected type, and Force was not specified
ItemExistsButIsIncorrect = The destination file {0} has been determined not to match the source, but Force has not been specified. Cannot continue
ErrorCopyingToOutstream = An error was encountered while copying the archived file to {0}
PackageUninstalled = The archive at {0} was removed from destination {1}
PackageInstalled = The archive at {0} was unpacked to destination {1}
ConfigurationStarted = The configuration of MSFT_xArchive is starting
ConfigurationFinished = The configuration of MSFT_xArchive has completed
MakeDirectory = Make directory {0}
RemoveFileAndRecreateAsDirectory = Remove existing file {0} and replace it with a directory of the same name
RemoveFile = Remove file {0}
RemoveDirectory = Remove directory {0}
UnzipFile = Unzip archived file to {0}
DestMissingOrIncorrectTypeReason = The destination file {0} was missing or was not a file
DestHasIncorrectHashvalue = The destination file {0} exists but its checksum did not match the origin file
DestShouldNotBeThereReason = The destination file {0} exists but should not
UsingKeyToRetrieveHashValue = Using {0} to retrieve hash value
NoCacheValueFound = No cache value found
CacheValueFoundReturning = Cache value found, returning {0}
CacheCorrupt = Cache found, but failed to loaded. Ignoring Cache.
Usingtmpkeytosavehashvalue = Using {0} {1} to save hash value
AboutToCacheValueInputObject = About to cache value {0}
InUpdateCache = In Update-Cache
AddingEntryFullNameAsACacheEntry = Adding {0} as a cache entry
UpdatingCacheObject = Updating CacheObject
PlacedNewCacheEntry = Placed new cache entry
NormalizeChecksumReturningChecksum = Normalize-Checksum returning {0}
PathPathIsAlreadyAccessiableNoMountNeeded. = Path {0} is already accessible. No mount needed.
PathPathIsNotAValidateNetPath = Path {0} is not a validate net path.
CreatePsDriveWithPathPath = create psdrive with Path {0}...
CannotAccessPathPathWithGivenCredential = Cannot access Path {0} with given Credential
AboutToValidateStandardArguments = About to validate standard arguments
GoingForCacheEntries = Going for cache entries
TheCacheWasUpToDateUsingCacheToSatisfyRequests = The cache was up to date, using cache to satisfy requests
AboutToOpenTheZipFile = About to open the zip file
CacheUpdatedWithEntries = Cache updated with {0} entries
Processing = Processing {0}
InTestTargetResourceDestExistsNotUsingChecksumsContinuing = In Test-TargetResource: {0} exists, not using checksums, continuing
NotPerformingChecksumTheFileOnDiskHasTheSameWritetTimeAsTheLastTimeWeVerifiedItsContents = Not performing checksum, the file on disk has the same write time as the last time we verified its contents
DestExistsAndTheHashMatchesEven = {0} exists and the hash matches even though the LastModifiedTime did not. Updating cache
InTestTargetResourceDestExistsAndTheSelectedTimestampChecksumMatched = In Test-TargetResource: {0} exists and the selected timestamp {1} matched
RemovePSDriveonRootPsDriveRoot = Remove PSDrive on Root {0}
RemovingDir = Removing {0}
HashesOfExistingAndZipFilesMatchRemoving = Hashes of existing and zip files match, removing
HashDidNotMatchFileHasBeenModifiedSinceItWasExtractedLeaving = Hash did not match, file has been modified since it was extracted. Leaving
InSetTargetResourceExistsSelectedTimestampMatched = In Set-TargetResource: {0} exists and the selected timestamp {1} matched, removing
InSetTargetResourceExistsdTheSelectedTimestampNotMatchG = In Set-TargetResource: {0} exists and the selected timestamp {1} did not match, leaving
ExistingAppearsToBeAnEmptyDirectoryRemovingIt = {0} appears to be an empty directory. Removing it
LastWriteTimeMtchesWhatWeHaveRecordNotReexaminingChecksum = LastWriteTime of {0} matches what we have on record, not re-examining {1}
FoundFAtDestWhereGoingToPlaceOneAndHashMatchedContinuing = Found a file at {0} where we were going to place one and hash matched. Continuing
FoundFileAtDestWhereWeWereGoingToPlaceOneAndHashDidntMatchItWillBeOverwritten = Found a file at $dest where we were going to place one and hash did not match. It will be overwritten
FoundFileAtDestWhereWeWereGoingToPlaceOneAndDoesNotMatchtTheSourceButForceWasNotSpecifiedErroring = Found a file at {0} where we were going to place one and does not match the source, but Force was not specified. Erroring
InSetTargetResourceDestExistsAndTheSelectedTimestamp$ChecksumDidNotMatchForceWasSpecifiedWeWillOverwrite = In Set-TargetResource: {0} exists and the selected timestamp {1} did not match. Force was specified, we will overwrite
FoundAFileAtDestAndTimestampChecksumDoesNotMatchTheSourceButForceWasNotSpecifiedErroring = Found a file at {0} and timestamp {1} does not match the source, but Force was not specified. Erroring
FoundADirectoryAtDestWhereAFileShouldBeRemoving = Found a directory at {0} where a file should be. Removing
FoundDirectoryAtDestWhereAFileShouldBeAndForceWasNotSpecifiedErroring = Found a directory at {0} where a file should be and Force was not specified. Erroring.
WritingToFileDest = Writing to file {0}
RemovePSDriveonRootDriveRoot = Remove PSDrive on Root {0}
UpdatingCache = Updating cache
FolderDirDoesNotExist = Folder {0} does not exist
ExaminingDirectoryToSeeIfItShouldBeRemoved = Examining {0} to see if it should be removed
InSetTargetResourceDestExistsAndTheSelectedTimestampChecksumMatchedWillLeaveIt = In Set-TargetResource: {0} exists and the selected timestamp {1} matched, will leave it
'@

}

Import-LocalizedData LocalizedData -FileName 'MSFT_xArchive.strings.psd1'

Import-Module "$PSScriptRoot\..\CommonResourceHelper.psm1"

$script:cacheLocation = "$env:systemRoot\system32\Configuration\BuiltinProvCache\MSFT_ArchiveResource"

<#
    .SYNOPSIS
        Converts a DSC hash name (with a hyphen) to a PowerShell hash name (without a hyphen).
        The in-box PowerShell Get-FileHash cmdlet takes only hash names without hypens.
 
    .PARAMETER DscHashName
        The DSC hash name to convert.
#>

function ConvertTo-PowerShellHashAlgorithmName
{
    [OutputType([String])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $DscHashAlgorithmName
    )

    return $DscHashAlgorithmName.Replace('-', '')
}

<#
    .SYNOPSIS
        Tests if the given Checksum string specifies a SHA hash algorithm.
 
    .PARAMETER Checksum
        The Checksum string to test.
#>

function Test-ChecksumIsSha
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [String] $Checksum
    )

    return $null -ne $Checksum -and $Checksum.Length -ge 3 -and $Checksum.Substring(0, 3) -ieq 'sha'
}

<#
    .SYNOPSIS
        Retrieves the entry with the given path and destination from the cache
 
    .PARAMETER Path
        The path property of the cache entry to retrieve
 
    .PARAMETER Destination
        The destination property of the cache entry to retrieve
#>

function Get-CacheEntry
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Destination
    )

    $cacheEntry = @{}

    $cacheEntryKey = ($Path + $Destination).GetHashCode()
    Write-Verbose -Message ($LocalizedData.UsingKeyToRetrieveHashValue -f $cacheEntryKey)

    $cacheEntryPath = Join-Path -Path $script:cacheLocation -ChildPath $cacheEntryKey
    if (-not (Test-Path -Path $cacheEntryPath))
    {
        Write-Verbose -Message ($LocalizedData.NoCacheValueFound)
    }
    else
    {
        # ErrorAction seems to have no affect on this exception, (see: https://microsoft.visualstudio.com/web/wi.aspx?pcguid=cb55739e-4afe-46a3-970f-1b49d8ee7564&id=1185735)
        try
        {
            $cacheEntry = Import-CliXml -Path $cacheEntryPath
            Write-Verbose -Message ($LocalizedData.CacheValueFoundReturning -f $cacheEntry)
        }
        catch [System.Xml.XmlException]
        {
            Write-Verbose -Message ($LocalizedData.CacheCorrupt)
        }
    }

    return $cacheEntry
}

<#
    .SYNOPSIS
        Sets an entry in the cache.
 
    .PARAMETER Path
        The path property to use as part of a key for the cache entry.
 
    .PARAMETER Destination
        The destination property to use as part of a key for the cache entry.
 
    .PARAMETER InputObject
        The object to store in the cache.
#>

function Set-CacheEntry
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Destination,

        [Object] $InputObject
    )

    $cacheEntryKey = ($Path + $Destination).GetHashCode()

    Write-Verbose -Message ($LocalizedData.UsingTmpKeyToSaveHashValue -f $tmp, $cacheEntryKey)
    $cacheEntryPath = Join-Path -Path $script:cacheLocation -ChildPath $cacheEntryKey

    Write-Verbose -Message ($LocalizedData.AboutToCacheValueInputObject -f $InputObject)
    if (-not (Test-Path -Path $script:cacheLocation))
    {
        New-Item -Path $script:cacheLocation -ItemType Directory | Out-Null
    }

    Export-CliXml -Path $cacheEntryPath -InputObject $InputObject
}

<#
    .SYNOPSIS
        Tests if the Path argument to the Archive resource is valid.
        Throws an error if Path is not valid.
 
    .PARAMETER Path
        The path to test
#>

function Assert-PathArgumentValid
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path
    )

    $ErrorActionPreference = 'Stop'

    if (-not (Test-Path -Path $Path -PathType Leaf))
    {
        New-InvalidArgumentException -Message ($LocalizedData.InvalidSourcePath -f $Path) -ArgumentName 'Path'
    }
}

<#
    .SYNOPSIS
        Tests if the Destination argument to the Archive resource is valid.
        Throws an error if Destination is not valid.
 
    .PARAMETER Path
        The destination path to test
#>

function Assert-DestinationArgumentValid
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Destination
    )

    $ErrorActionPreference = 'Stop'

    $destinationFileInfo = Get-Item -LiteralPath $Destination -ErrorAction Ignore
    if ($null -ne $destinationFileInfo -and $destinationFileInfo.GetType() -eq [System.IO.FileInfo])
    {
        New-InvalidArgumentException -Message ($LocalizedData.InvalidDestinationDirectory -f $Destination) -ArgumentName 'Destination'
    }
}

<#
    .SYNOPSIS
        Tests if the Validate and Checksum arguments to the Archive resource are valid.
        Throws an error if they are not valid.
 
    .PARAMETER Validate
        The Validate value to test
 
    .PARAMETER Checksum
        The Checksum value to test
#>

function Assert-ValidateAndChecksumArgumentsValid
{
    [CmdletBinding()]
    param
    (
        [Boolean] $Validate,

        [String] $Checksum
    )

    $ErrorActionPreference = 'Stop'

    if ($PSBoundParameters.ContainsKey('Checksum') -and -not $Validate)
    {
        New-InvalidArgumentException -Message ($LocalizedData.InvalidChecksumArgsMessage -f $Checksum) -ArgumentName 'Checksum'
    }
}

<#
    .SYNOPSIS
        Tests if the hash for the given file matches the hash for the given cache entry.
 
    .PARAMETER FilePath
        The path to the file to test the hash for
 
    .PARAMETER CacheEntry
        The cache entry to test the hash for
 
    .PARAMETER HashAlgorithmName
        The name of the hash algorithm to use to retrieve the file's hash
#>

function Test-FileHashMatchesArchiveEntryHash
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $FilePath,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [Object] $ArchiveEntry,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $HashAlgorithmName
    )

    $existingFileStream = $null
    $fileHash = $null

    try
    {
        $existingFileStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $FilePath, 'Open')
        $powerShellHashAlgorithmName = ConvertTo-PowerShellHashAlgorithmName -DscHashAlgorithmName $HashAlgorithmName
        $fileHash = Get-FileHash -InputStream $existingFileStream -Algorithm $powerShellHashAlgorithmName
    }
    catch
    {
        New-InvalidOperationException -Message ($LocalizedData.ErrorOpeningExistingFile -f $FilePath) -ErrorRecord $_
    }
    finally
    {
        if ($null -ne $existingFileStream)
        {
            $existingFileStream.Dispose()
        }
    }

    $archiveEntryHash = $ArchiveEntry.Checksum

    return ($fileHash.Algorithm -eq $archiveEntryHash.Algorithm) -and ($fileHash.Hash -eq $archiveEntryHash.Hash)
}

<#
    .SYNOPSIS
        Retrieves the appropriate timestamp from the given file system info object based on the given Checksum
 
    .PARAMETER FileSystemObject
        The file system info object to retrieve the timestamp for
 
    .PARAMETER Checksum
        The Checksum to retrieve the appropriate timestamp for
#>

function Get-RelevantChecksumTimestamp
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.IO.FileSystemInfo] $FileSystemObject,

        [String] $Checksum
    )

    if ($Checksum -ieq 'CreatedDate')
    {
        return $FileSystemObject.CreationTime
    }
    else
    {
        return $FileSystemObject.LastWriteTime
    }
}

<#
    .SYNOPSIS
        Updates the given cache entry
 
    .PARAMETER CacheEntryToUpdate
        The cache entry to update
 
    .PARAMETER ArchiveEntries
        The archive entries to update the given cache entry with
 
    .PARAMETER Checksum
        The Checksum to update the given cache entry with
 
    .PARAMETER SourceLastWriteTime
        The source last write time to update the given cache entry with
#>

function Update-Cache
{
    [CmdletBinding()]
    param
    (
        [Hashtable] $CacheEntryToUpdate,

        [System.IO.Compression.ZipArchiveEntry[]] $ArchiveEntries,

        [String] $Checksum,

        [String] $SourceLastWriteTime
    )

    Write-Verbose -Message ($LocalizedData.InUpdateCache)

    $cacheEntries = New-Object -TypeName 'System.Collections.ArrayList'

    foreach ($archiveEntry in $ArchiveEntries)
    {
        $archiveEntryHash = $null

        if (Test-ChecksumIsSha -Checksum $Checksum)
        {
            $archiveEntryStream = $null
            try
            {
                $archiveEntryStream = $archiveEntry.Open()
                $powerShellHashAlgorithmName = ConvertTo-PowerShellHashAlgorithmName -DscHashAlgorithmName $Checksum
                $archiveEntryHash = Get-FileHash -InputStream $archiveEntryStream -Algorithm $powerShellHashAlgorithmName
            }
            finally
            {
                if ($null -ne $archiveEntryStream)
                {
                    $archiveEntryStream.Dispose()
                }
            }
        }

        $cacheEntry = @{
            FullName = $archiveEntry.FullName
            LastWriteTime = $archiveEntry.LastWriteTime
            Checksum = $archiveEntryHash
        }

        Write-Verbose -Message  ($LoalizedData.AddingEntryFullNameAsACacheEntry -f $archiveEntry.FullName)
        $cacheEntries.Add($cacheEntry) | Out-Null
    }

    Write-Verbose -Message ($LocalizedData.UpdatingCacheObject)

    if ($null -eq $CacheEntryToUpdate)
    {
        $CacheEntryToUpdate = @{}
    }

    $CacheEntryToUpdate['SourceLastWriteTime'] = $SourceLastWriteTime
    $CacheEntryToUpdate['Entries'] = $cacheEntries.ToArray()
    Set-CacheEntry -InputObject $CacheEntryToUpdate -Path $Path -Destination $Destination

    Write-Verbose -Message ($LocalizedData.PlacedNewCacheEntry)
}

<#
    .SYNOPSIS
        Creates a PSDrive to a net share with the given credential.
 
    .PARAMETER Path
        The file path mount the PSDrive for
 
    .PARAMETER Credential
        The credential to access the given file path
#>

function Mount-NetworkPath
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path,

        [PSCredential] $Credential
    )

    $psDrive = $null

    # Mount the drive only if not accessible
    if (Test-Path -Path $Path -ErrorAction Ignore)
    {
        Write-Verbose -Message  ($LocalizedData.PathPathIsAlreadyAccessiableNoMountNeeded -f $Path)
    }
    else
    {
        if (-not $Path.EndsWith('\'))
        {
            $lastBackslashIndex = $Path.LastIndexOf('\')
            if ($lastBackslashIndex -eq -1)
            {
                Write-Verbose -Message ($LocalizedData.PathPathIsNotAValidateNetPath -f $Path)
                New-InvalidOperationException ($LocalizedData.InvalidNetSourcePath -f $Path)
            }
            else
            {
                $Path = $Path.Substring(0, $lastBackslashIndex)
            }
        }

        $newPSDriveArgs = @{
            Name = [Guid]::NewGuid()
            PSProvider = 'FileSystem'
            Root = $Path
            Scope = 'Script'
            Credential = $Credential
        }

        try
        {
            Write-Verbose -Message ($LocalizedData.CreatePSDriveWithPathPath -f $Path)
            $psDrive = New-PSDrive @newPSDriveArgs
        }
        catch
        {
            Write-Verbose -Message ($LocalizedData.CannotAccessPathPathWithGivenCredential -f $Path)
            New-InvalidOperationException -Message ($LocalizedData.ErrorOpeningArchiveFile -f $Path) -ErrorRecord $_
        }
    }

    return $psDrive
}

function Test-TargetResource
{
    [OutputType([Boolean])]
    [CmdletBinding()]
    param
    (
        [ValidateSet('Present', 'Absent')]
        [String] $Ensure = 'Present',

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Destination,

        [Boolean] $Validate = $false,

        [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')]
        [String] $Checksum = 'SHA-256',

        [Boolean] $Force = $false,

        [PSCredential] $Credential
    )

    if ($null -ne $Credential)
    {
        $psDrive = Mount-NetworkPath -Path $Path -Credential $Credential
    }

    try
    {
        $ErrorActionPreference = 'Stop'

        Write-Verbose -Message ($LocalizedData.AboutToValidateStandardArguments)

        Assert-PathArgumentValid -Path $Path
        Assert-DestinationArgumentValid -Destination $Destination

        if ($PSBoundParameters.ContainsKey('Checksum'))
        {
            Assert-ValidateAndChecksumArgumentsValid -Validate $Validate -Checksum $Checksum
        }
        else
        {
            Assert-ValidateAndChecksumArgumentsValid -Validate $Validate
        }

        Write-Verbose -Message ($LocalizedData.GoingForCacheEntries)

        $result = $true

        $cacheEntry = Get-CacheEntry -Path $Path -Destination $Destination
        $sourceLastWriteTime = (Get-Item -LiteralPath $Path).LastWriteTime

        $cacheUpToDate = $null -ne $cacheEntry -and $null -ne $cacheEntry.SourceLastWriteTime -and $cacheEntry.SourceLastWriteTime -eq $sourceLastWriteTime

        $fileHandle = $null

        try
        {
            $archiveEntries = $null

            if ($cacheUpToDate)
            {
                Write-Verbose -Message ($LocalizedData.TheCacheWasUpToDateUsingCacheToSatisfyRequests)
            }
            else
            {
                Write-Verbose -Message ($LocalizedData.AboutToOpenTheZipFile)
                $archiveEntries, $null, $fileHandle = Open-ZipFile -Path $Path

                Write-Verbose -Message ($LocalizedData.UpdatingCache)
                Update-Cache -CacheEntryToUpdate $cacheEntry -ArchiveEntries $archiveEntries -Checksum $Checksum -SourceLastWriteTime $sourceLastWriteTime
                $cacheEntry = Get-CacheEntry -Path $Path -Destination $Destination

                Write-Verbose -Message ($LocalizedData.CacheUpdatedWithEntries -f $cacheEntry.Entries.Length)
            }

            $archiveEntries = $cacheEntry.Entries

            foreach ($archiveEntry in $archiveEntries)
            {
                $individualResult = $true
                Write-Verbose -Message ($LocalizedData.Processing -f $archiveEntry.FullName)

                $archiveEntryDestinationPath = Join-Path -Path $Destination -ChildPath $archiveEntry.FullName
                if ($archiveEntryDestinationPath.EndsWith('\'))
                {
                    $archiveEntryDestinationPath = $archiveEntryDestinationPath.TrimEnd('\')
                    if (-not (Test-Path -Path $archiveEntryDestinationPath -PathType Container))
                    {
                        Write-Verbose ($LocalizedData.DestMissingOrIncorrectTypeReason -f $archiveEntryDestinationPath)
                        $individualResult = $result = $false
                    }
                }
                else
                {
                    $archiveEntryDestinationFileInfo = Get-Item -LiteralPath $archiveEntryDestinationPath -ErrorAction Ignore
                    if ($null -eq $archiveEntryDestinationFileInfo)
                    {
                        $individualResult = $result = $false
                    }
                    elseif ($archiveEntryDestinationFileInfo.GetType() -ne [System.IO.FileInfo])
                    {
                        $individualResult = $result = $false
                    }

                    if (-not $Validate)
                    {
                        Write-Verbose -Message ($LocalizedData.InTestTargetResourceDestExistsNotUsingChecksumsContinuing -f $archiveEntryDestinationPath)
                        if (-not $individualResult -and $Ensure -eq 'Present')
                        {
                            Write-Verbose ($LocalizedData.DestMissingOrIncorrectTypeReason -f $archiveEntryDestinationPath)
                        }
                        elseif ($individualResult -and $Ensure -eq 'Absent')
                        {
                            Write-Verbose ($LocalizedData.DestShouldNotBeThereReason -f $archiveEntryDestinationPath)
                        }
                    }
                    else
                    {
                        # If the file is there we need to check if it could possibly fail in a different way
                        # Otherwise we skip all these checks - there's nothing to work with
                        if ($individualResult)
                        {
                            if (Test-ChecksumIsSha -Checksum $Checksum)
                            {
                                if ($archiveEntryDestinationFileInfo.LastWriteTime.Equals($archiveEntry.ExistingItemTimestamp))
                                {
                                    Write-Verbose -Message ($LocalizedData.NotPerformingChecksumTheFileOnDiskHasTheSameWriteTimeAsTheLastTimeWeVerifiedItsContents)
                                }
                                else
                                {
                                    if (-not (Test-FileHashMatchesArchiveEntryHash -FilePath $archiveEntryDestinationPath -ArchiveEntry $archiveEntry -HashAlgorithmName $Checksum))
                                    {
                                        $individualResult = $result = $false
                                    }
                                    else
                                    {
                                        $archiveEntry.ExistingItemTimestamp = $archiveEntryDestinationFileInfo.LastWriteTime
                                        Write-Verbose -Message ($LocalizedData.DestExistsAndTheHashMatchesEven -f $archiveEntryDestinationPath)
                                    }
                                }
                            }
                            else
                            {
                                $archiveEntryTimestamp = Get-RelevantChecksumTimestamp -FileSystemObject $archiveEntryDestinationFileInfo -Checksum $Checksum

                                if (-not $archiveEntryTimestamp.Equals($archiveEntryTimestamp.LastWriteTime.DateTime))
                                {
                                    $individualResult = $result = $false
                                }
                                else
                                {
                                    Write-Verbose -Message ($LocalizedData.InTestTargetResourceDestExistsAndTheSelectedTimestampChecksumMatched -f $archiveEntryDestinationPath, $Checksum)
                                }
                            }
                        }

                        if (-not $individualResult -and $Ensure -eq 'Present')
                        {
                            Write-Verbose ($LocalizedData.DestHasIncorrectHashvalue -f $archiveEntryDestinationPath)
                        }
                        elseif ($individualResult -and $Ensure -eq 'Absent')
                        {
                            Write-Verbose ($LocalizedData.DestShouldNotBeThereReason -f $archiveEntryDestinationPath)
                        }
                    }
                }
            }
        }
        finally
        {
            if ($null -ne $fileHandle)
            {
                $fileHandle.Dispose()
            }
        }

        Set-CacheEntry -InputObject $cacheObj -path $Path -destination $Destination
        $result = $result -eq ('Present' -eq $Ensure)
    }
    finally
    {
        if ($null -ne $psDrive)
        {
            Write-Verbose -Message ($LoalizedData.RemovePSDriveOnRootPSDrive -f $($psDrive.Root))
            Remove-PSDrive -Name $psDrive -Force -ErrorAction SilentlyContinue
        }
    }

    return $result
}

<#
    .SYNOPSIS
        Creates a new directory at the specified path if it does not already exist.
        If the Force parameter is specified, a file with the same path will be overwritten with a new directory.
 
    .PARAMETER Path
        The path at which to create the new directory
#>

function New-Directory
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path
    )

    $fileInfo = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue

    if ($null -eq $fileInfo)
    {
        Write-Verbose -Message ($LocalizedData.FolderDirDoesNotExist -f $Path)

        if ($PSCmdlet.ShouldProcess(($LocalizedData.MakeDirectory -f $Path), $null, $null))
        {
            New-Item -Path $Path -ItemType Directory | Out-Null
        }
    }
    else
    {
        if ($fileInfo.GetType() -ne [System.IO.DirectoryInfo])
        {
            if ($Force -and $PSCmdlet.ShouldProcess(($LocalizedData.RemoveFileAndRecreateAsDirectory -f $Path), $null, $null))
            {
                Write-Verbose -Message ($LocalizedData.RemovingDir -f $Path)
                Remove-Item -Path $Path | Out-Null
                New-Item -Path $Path -ItemType Directory | Out-Null
            }
            else
            {
                New-InvalidOperationException ($LocalizedData.ItemExistsButIsWrongType -f $Path)
            }
        }
    }
}

<#
    .SYNOPSIS
        Opens the given zip file.
 
    .PARAMETER Path
        The path to the zip file to open
#>

function Open-ZipFile
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path
    )

    if (Test-IsNanoServer)
    {
        Add-Type -AssemblyName System.IO.Compression
    }
    else
    {
        Add-Type -AssemblyName System.IO.Compression.FileSystem
    }

    try
    {
        $zipFileHandle = [System.IO.Compression.ZipFile]::OpenRead($Path)
        $archiveEntries = $zipFileHandle.Entries
    }
    catch
    {
        New-InvalidOperationException ($LocalizedData.ErrorOpeningArchiveFile -f $Path) $_
    }

    $archiveEntryNameHashtable = @{}

    foreach ($archiveEntry in $archiveEntries)
    {
        $archiveEntryNameHashtable[$archiveEntry.FullName] = $archiveEntry
    }

    return $archiveEntries, $archiveEntryNameHashtable, $zipFileHandle
}

function Set-TargetResource
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Destination,

        [ValidateSet('Present', 'Absent')]
        [String] $Ensure = 'Present',

        [Boolean] $Validate = $false,

        [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')]
        [String] $Checksum,

        [Boolean] $Force = $false,

        [PSCredential] $Credential
    )

    if ($Credential)
    {
        $psDrive = Mount-NetworkPath -Path $Path -Credential $Credential
    }

    try
    {
        $ErrorActionPreference = 'Stop'

        Write-Verbose -Message ($LocalizedData.AboutToValidateStandardArguments)

        Assert-PathArgumentValid -Path $Path
        Assert-DestinationArgumentValid -Destination $Destination

        if ($PSBoundParameters.ContainsKey('Checksum'))
        {
            Assert-ValidateAndChecksumArgumentsValid -Validate $Validate -Checksum $Checksum
        }
        else
        {
            Assert-ValidateAndChecksumArgumentsValid -Validate $Validate
        }

        Write-Verbose -Message $LocalizedData.ConfigurationStarted

        if (-not (Test-Path -Path $Destination))
        {
            New-Item -Path $Destination -ItemType Directory | Out-Null
        }

        $cacheEntry = Get-CacheEntry -Path $Path -Destination $Destination
        $sourceLastWriteTime = (Get-Item -LiteralPath $Path).LastWriteTime

        $cacheUpToDate = $null -ne $cacheEntry -and $null -ne $cacheEntry.SourceLastWriteTime -and $cacheEntry.SourceLastWriteTime -eq $sourceLastWriteTime

        $zipFileHandle = $null
        $archiveEntryNameHashtable = @{}

        try
        {
            if(-not $cacheUpToDate)
            {
                $archiveEntries, $archiveEntryNameHashtable, $zipFileHandle = Open-ZipFile -Path $Path
                Update-Cache -CacheEntryToUpdate $cacheEntry -ArchiveEntries $archiveEntries -Checksum $Checksum -SourceLastWriteTime $sourceLastWriteTime
                $cacheEntry = Get-CacheEntry -Path $Path -Destination $Destination
            }
        }
        finally
        {
            if ($null -ne $zipFileHandle)
            {
                $zipFileHandle.Dispose()
                $zipFileHandle = $null
            }

        }

        $archiveEntries = $cacheEntry.Entries

        if ($Ensure -eq 'Absent')
        {
            $directories = New-Object -TypeName 'System.Collections.Generic.Hashset[String]'

            foreach ($archiveEntry in $archiveEntries)
            {
                $parentDirectory = Split-Path -Path $archiveEntry.FullName

                while (-not [String]::IsNullOrEmpty($parentDirectory))
                {
                    $directories.Add($parentDirectory) | Out-Null
                    $parentDirectory = Split-Path -Path $parentDirectory
                }

                if ($archiveEntry.FullName.EndsWith('\'))
                {
                    $directories.Add($archiveEntry.FullName) | Out-Null
                    continue
                }

                $archiveEntryDestinationPath = Join-Path -Path $Destination -ChildPath $archiveEntry.FullName

                $fileInfoAtDestinationPath = Get-Item -LiteralPath $archiveEntryDestinationPath -ErrorAction SilentlyContinue
                if ($null -eq $fileInfoAtDestinationPath)
                {
                    continue
                }

                # Possible for a folder to have been replaced by a directory of the same name, in which case we must leave it alone
                $fileTypeAtDestinationPath = $fileInfoAtDestinationPath.GetType()
                if ($fileTypeAtDestinationPath -ne [System.IO.FileInfo])
                {
                    continue
                }

                if (-not $Checksum -and $PSCmdlet.ShouldProcess(($LocalizedData.RemoveFile -f $archiveEntryDestinationPath), $null, $null))
                {
                    Write-Verbose -Message ($LocalizedData.RemovingDir -f $archiveEntryDestinationPath)
                    Remove-Item -Path $archiveEntryDestinationPath
                    continue
                }

                if (Test-ChecksumIsSha -Checksum $Checksum)
                {
                    if ((Test-FileHashMatchesArchiveEntryHash -FilePath $archiveEntryDestinationPath -ArchiveEntry $archiveEntry -HashAlgorithmName $Checksum) -and $PSCmdlet.ShouldProcess(($LocalizedData.RemoveFile -f $archiveEntryDestinationPath), $null, $null))
                    {
                        Write-Verbose -Message ($LocalizedData.HashesOfExistingAndZipFilesMatchRemoving)
                        Remove-Item -Path $archiveEntryDestinationPath
                    }
                    else
                    {
                        Write-Verbose -Message ($LocalizedData.HashDidNotMatchFileHasBeenModifiedSinceItWasExtractedLeaving)
                    }
                }
                else
                {
                    $relevantTimestamp = Get-RelevantChecksumTimestamp -FileSystemObject $fileInfoAtDestinationPath -Checksum $Checksum
                    if ($relevantTimestamp.Equals($archiveEntry.LastWriteTime.DateTime) -and $PSCmdlet.ShouldProcess(($LocalizedData.RemoveFile -f $archiveEntryDestinationPath), $null, $null))
                    {
                        Write-Verbose -Message ($LocalizedData.InSetTargetResourceexistsselectedtimestampmatched -f $archiveEntryDestinationPath, $Checksum)
                        Remove-Item -Path $archiveEntryDestinationPath
                    }
                    else
                    {
                        Write-Verbose -Message ($LocalizedData.InSetTargetResourceexistsdtheselectedtimestampnotmatchg -f $archiveEntryDestinationPathg, $Checksum)
                    }
                }
            }

            <#
                Hashset was useful for dropping dupes in an efficient manner, but it can mess with ordering.
                Sort according to current culture (directory names can be localized, obviously).
                Reverse so we hit children before parents.
            #>

            $directories = [System.Linq.Enumerable]::ToList($directories)
            $directories.Sort([System.StringComparer]::InvariantCultureIgnoreCase)
            $directories.Reverse()

            foreach ($directory in $directories)
            {
                Write-Verbose -Message ($LocalizedData.ExaminingDirectoryToSeeIfiItShouldBeRemoved -f $directory)

                $directoryDestinationPath = Join-Path -Path $Destination -ChildPath $directory

                $fileInfoAtDestinationPath = Get-Item -LiteralPath $directoryDestinationPath -ErrorAction SilentlyContinue
                if ($null -ne $fileInfoAtDestinationPath -and $null -ne $fileInfoAtDestinationPath.GetType() -and $fileInfoAtDestinationPath.GetType() -eq [System.IO.DirectoryInfo] -and $fileInfoAtDestinationPath.GetFiles().Count -eq 0 -and $fileInfoAtDestinationPath.GetDirectories().Count -eq 0 `
                        -and $PSCmdlet.ShouldProcess(($LocalizedData.RemoveDirectory -f $fileInfoAtDestinationPath), $null, $null))
                {
                    Write-Verbose -Message ($LocalizedData.ExistingaAppearsToBeAneEmptyDirectoryRemovingit -f $fileInfoAtDestinationPath)
                    Remove-Item -Path $fileInfoAtDestinationPath
                }
            }

            Write-Verbose ($LocalizedData.PackageUninstalled -f $Path, $Destination)
            Write-Verbose $LocalizedData.ConfigurationFinished
            return
        }

        New-Directory -Path $Destination

        foreach ($archiveEntry in $archiveEntries)
        {
            $archiveEntryDestinationPath = Join-Path -Path $Destination -ChildPath $archiveEntry.FullName

            if ($archiveEntryDestinationPath.EndsWith('\'))
            {
                New-Directory -Path $archiveEntryDestinationPath.TrimEnd("\")
                continue
            }

            $fileInfoAtDestinationPath = Get-Item -LiteralPath $archiveEntryDestinationPath -ErrorAction SilentlyContinue
            if ($null -ne $fileInfoAtDestinationPath)
            {
                if ($fileInfoAtDestinationPath.GetType() -eq [System.IO.FileInfo])
                {
                    if (-not $Validate)
                    {
                        continue
                    }

                    if (Test-ChecksumIsSha -Checksum $Checksum)
                    {
                        if ($fileInfoAtDestinationPath.LastWriteTime.Equals($archiveEntry.ExistingTimestamp))
                        {
                            Write-Verbose -Message ($LocalizedData.LastWriteTimeMtchesWhatWeHaveRecordNotReexaminingChecksum -f $archiveEntryDestinationPath, $Checksum)
                        }
                        else
                        {
                            $fileHashMatchesArchiveEntryHash = Test-FileHashMatchesArchiveEntryHash -FilePath $archiveEntryDestinationPath -ArchiveEntry $archiveEntry -HashAlgorithmName $Checksum

                            if ($fileHashMatchesArchiveEntryHash)
                            {
                                Write-Verbose -Message ($LocalizedData.FoundfatdestwheregoingtoplaceoneandhashmatchedContinuing -f $archiveEntryDestinationPath)

                                $archiveEntry.ExistingItemTimestamp = $fileInfoAtDestinationPath.LastWriteTime
                                continue
                            }
                            else
                            {
                                if ($Force)
                                {
                                    Write-Verbose -Message ($LocalizedData.FoundFileAtDestWhereWeWereGoingToPlaceOneAndHashDidntMatchItWillBeOverwritten -f $archiveEntryDestinationPath)
                                }
                                else
                                {
                                    Write-Verbose -Message ($LocalizedData.FoundFileAtdDestWhereWeWereGoingToPlaceOneAndDoesNotMatchTheSourceButForceWasNotSpecifiedErroring -f $archiveEntryDestinationPath)
                                    New-InvalidOperationException ($LocalizedData.ItemExistsButIsIncorrect -f $archiveEntryDestinationPath)
                                }
                            }
                        }
                    }
                    else
                    {
                        $relevantTimestamp = Get-RelevantChecksumTimestamp -FileSystemObject $fileInfoAtDestinationPath -Checksum $Checksum
                        if ($relevantTimestamp.Equals($archiveEntry.LastWriteTime.DateTime))
                        {
                            Write-Verbose -Message ($LocalizedData.InSetTargetResourceDestExistsAndtTheSelectedTimestampChecksumMatchedWilllLeaveIt -f $archiveEntryDestinationPath, $Checksum)
                            continue
                        }
                        else
                        {
                            if ($Force)
                            {
                                Write-Verbose -Message ($LocalizedData.InSetTargetResourceDestExistsAndTheSelectedTimestamp -f $archiveEntryDestinationPath, $Checksum)
                            }
                            else
                            {
                                Write-Verbose -Message ($LocalizedData.FoundaAFileAtDestAndTimestampChecksumDoesNotMatchTheSourceButForceWasNotSpecifiedErroring -f $archiveEntryDestinationPath, $Checksum)
                                New-InvalidOperationException ($LocalizedData.ItemExistsButIsIncorrect -f $archiveEntryDestinationPath)
                            }
                        }
                    }
                }
                else
                {
                    if ($Force)
                    {
                        Write-Verbose -Message ($LocalizedData.FoundADirectoryAtDestWhereAFileShouldBeRemoving -f $archiveEntryDestinationPath)

                        if ($PSCmdlet.ShouldProcess(($LocalizedData.RemoveDirectory -f $archiveEntryDestinationPath), $null, $null))
                        {
                            Remove-Item -Path $archiveEntryDestinationPath -Recurse -Force | Out-Null
                        }
                    }
                    else
                    {
                        Write-Verbose -Message ($LocalizedData.FoundDirectoryAtDestWhereAFileShouldBeAndForceWasNotSpecifiedErroring -f $archiveEntryDestinationPath)
                        New-InvalidOperationException ($LocalizedData.ItemExistsButIsWrongType -f $archiveEntryDestinationPath)
                    }
                }
            }

            $archiveEntryDestinationParentPath = Split-Path -Path $archiveEntryDestinationPath
            if (-not (Test-Path $archiveEntryDestinationParentPath) -and $PSCmdlet.ShouldProcess(($LocalizedData.MakeDirectory -f $archiveEntryDestinationParentPath), $null, $null))
            {
                <#
                    TODO: This is an edge case we need to revisit. We should be correctly handling wrong file types along
                    the directory path if they occur within the archive, but they don't have to. Simple tests demonstrate that
                    the Zip format allows you to have the file within a folder without explicitly having an entry for the folder
                    This solution will fail in such a case IF anything along the path is of the wrong type (e.g. file in a place
                    we expect a directory to be)
                #>

                New-Item -Path $archiveEntryDestinationParentPath -ItemType Directory | Out-Null
            }

            try
            {
                if ($PSCmdlet.ShouldProcess(($LocalizedData.UnzipFile -f $archiveEntryDestinationPath), $null, $null))
                {
                    # If we get here we can safely blow away anything we find.

                    $null, $archiveEntryNameHashtable, $zipFileHandle = Open-ZipFile -Path $Path
                    $archiveFileSourceStream = $null
                    $archiveFileDestinationStream = $null

                    try
                    {
                        Write-Verbose -Message ($LocalizedData.WritingToFileDest -f $archiveEntryDestinationPath)
                        $archiveFileSourceStream = $archiveEntryNameHashtable[$archiveEntry.FullName].Open()
                        $archiveFileDestinationStream = New-Object -TypeName 'System.IO.FileStream' -ArgumentList @( $archiveEntryDestinationPath, 'Create' )
                        $archiveFileSourceStream.CopyTo($archiveFileDestinationStream)
                    }
                    catch
                    {
                        New-InvalidOperationException ($LocalizedData.ErrorCopyingToOutstream -f $archiveEntryDestinationPath) $_
                    }
                    finally
                    {
                        if ($null -ne $archiveFileSourceStream)
                        {
                            $archiveFileSourceStream.Dispose()
                        }

                        if ($null -ne $archiveFileDestinationStream)
                        {
                            $archiveFileDestinationStream.Dispose()
                        }
                    }

                    $newArchiveFileInfo = New-Object -TypeName 'System.IO.FileInfo' -ArgumentList @( $archiveEntryDestinationPath )

                    $updatedTimestamp = $archiveEntry.LastWriteTime.DateTime
                    $archiveEntry.ExistingItemTimestamp = $updatedTimestamp

                    Set-ItemProperty -Path $archiveEntryDestinationPath -Name 'LastWriteTime' -Value $updatedTimestamp
                    Set-ItemProperty -Path $archiveEntryDestinationPath -Name 'LastAccessTime' -Value $updatedTimestamp
                    Set-ItemProperty -Path $archiveEntryDestinationPath -Name 'CreationTime' -Value $updatedTimestamp
                }
            }
            finally
            {
                if ($null -ne $zipFileHandle)
                {
                    $zipFileHandle.Dispose()
                }
            }

            Set-CacheEntry -InputObject $archiveEntry -Path $Path -Destination $Destination
            Write-Verbose -Message ($LocalizedData.PackageInstalled -f $Path, $Destination)
            Write-Verbose -Message $LocalizedData.ConfigurationFinished
        }
    }
    finally
    {
        if ($null -ne $psDrive)
        {
            Write-Verbose -Message ($LocalizedData.RemovePSDriveonRootdriveRoot -f $psDrive.Root)
            Remove-PSDrive $psDrive -Force -ErrorAction SilentlyContinue
        }
    }
}

function Get-TargetResource
{
    [OutputType([Hashtable])]
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Path,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $Destination,

        [Boolean] $Validate = $false,

        [ValidateSet('SHA-1', 'SHA-256', 'SHA-512', 'CreatedDate', 'ModifiedDate')]
        [String] $Checksum,

        [PSCredential] $Credential
    )

    if ($null -eq $Credential)
    {
        $PSBoundParameters.Remove('Credential')
    }

    $testTargetResourceResult = Test-TargetResource @PSBoundParameters

    $ensureValue = 'Absent'

    if ($testTargetResourceResult)
    {
        $ensureValue = 'Present'
    }

    @{
        Ensure = $ensureValue
        Path = $Path
        Destination = $Destination
    }
}

Export-ModuleMember -Function *-TargetResource