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
'@

}

# Commented-out until more languages are supported
# Import-LocalizedData LocalizedData -FileName 'MSFT_xArchive.strings.psd1'

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

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

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')
    }

    $ensureValue = 'Absent'
    $testTargetResourceResult = Test-TargetResource @PSBoundParameters

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

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

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 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
        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
}

<#
        .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
}





Export-ModuleMember -Function *-TargetResource