Private/Copy-HDTContentTree.ps1

function Copy-HDTContentTree {
    <#
        .SYNOPSIS
            Copies a directory tree through the injected filesystem service.
 
        .DESCRIPTION
            What -Copy on Import-HDTOperatingSystem does: bring a media tree into
            the workspace. It goes through IFileSystem and nothing else, so
            importing 4 GB of media is provable under Pester with nothing on disk.
 
            IT REFUSES TO COPY A TREE INTO ITSELF. A destination underneath the
            source is the loop that fills a disk: every pass copies what the
            previous pass wrote. The check compares normalised paths and requires
            a separator, so C:\media\Win11-copy is correctly NOT inside
            C:\media\Win11 - a bare StartsWith would say it was.
 
            A DIRECTORY IS TOLD FROM A FILE BY GetLength. IFileSystem has nine
            methods and none of them is TestDirectory; TestPath answers for both.
            Both the real adapter and the fake throw
            System.IO.FileNotFoundException for a path that is not a file, and
            that error parity is a contract assertion (tests/helpers/README.md
            section 5), so the classification behaves identically against either
            implementation. Widening a service interface 04-01 fixed, in order to
            copy a directory, would have been the larger change.
 
        .PARAMETER Source
            The directory to copy. Must exist.
 
        .PARAMETER Destination
            Where to copy it. Created if absent, along with every subdirectory.
 
        .PARAMETER FileSystem
            An IFileSystem - the real adapter in production,
            New-HDTFakeFileSystem in a test.
 
        .OUTPUTS
            System.Int32 - the number of files copied.
 
        .EXAMPLE
            Copy-HDTContentTree -Source 'C:\media\Win11' -Destination $osFolder -FileSystem $fs
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'The caller owns the ShouldProcess decision; this runs only after it was made.')]
    [CmdletBinding()]
    [OutputType([int])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $Source,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $Destination,

        [Parameter(Mandatory = $true)]
        [ValidateNotNull()]
        [object] $FileSystem
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    if (-not $FileSystem.TestPath($Source)) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Source `
                    -Message 'the source tree does not exist, so there is nothing to copy into the workspace.' `
                    -Category ObjectNotFound))
    }

    $sourceFull = [System.IO.Path]::GetFullPath($Source).TrimEnd('\', '/')
    $destinationFull = [System.IO.Path]::GetFullPath($Destination).TrimEnd('\', '/')

    if ($destinationFull -eq $sourceFull -or
        $destinationFull.StartsWith(($sourceFull + [System.IO.Path]::DirectorySeparatorChar), [System.StringComparison]::OrdinalIgnoreCase)) {

        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Source `
                    -Message ("the destination '{0}' is inside the source tree, so copying it would copy what the copy is writing. Choose a destination outside the source." -f $Destination)))
    }

    $FileSystem.CreateDirectory($Destination)

    $copied = 0

    foreach ($child in @($FileSystem.GetChildItem($Source))) {
        $leaf = [System.IO.Path]::GetFileName(([string] $child).TrimEnd('\', '/'))
        $target = [System.IO.Path]::Combine($Destination, $leaf)

        $isFile = $true
        try {
            [void] $FileSystem.GetLength($child)
        } catch {
            # Not a file. Both implementations throw FileNotFoundException here,
            # and the child came from GetChildItem so it certainly exists.
            $isFile = $false
        }

        if ($isFile) {
            $FileSystem.CopyItem($child, $target)
            $copied++
            continue
        }

        $copied += Copy-HDTContentTree -Source $child -Destination $target -FileSystem $FileSystem
    }

    return $copied
}