Public/New-HDTSmbService.ps1

function New-HDTSmbService {
    <#
        .SYNOPSIS
            Creates the real ISmbService adapter over the SmbShare module.
 
        .DESCRIPTION
            A THIN ADAPTER, AND DELIBERATELY DUMB. It constructs arguments for
            five cmdlets and
            projects what they return; every decision that could be got wrong -
            whether the identity that came back is a guest, whether the dialect
            is acceptable, whether a credential was supplied at all - lives in
            New-HDTSmbContentProvider, where it is unit tested against
            New-HDTFakeSmbService.
 
              NewMapping(path, user, password, drive) New-SmbMapping
              RemoveMapping(remotePath) Remove-SmbMapping -Force
              GetUsedDriveLetter() [IO.DriveInfo]::GetDrives
              GetConnection(serverName) Get-SmbConnection
              GetClientConfiguration() Get-SmbClientConfiguration
 
            THE MAPPING TAKES A DRIVE LETTER, WHICH IS WHAT MDT DID. A share
            connected without one can only be reached by its UNC path, and
            cmd.exe REFUSES A UNC WORKING DIRECTORY - it prints "UNC paths are
            not supported", moves itself to %SystemRoot%, and every application
            whose install command names its own installer relatively then runs
            in the wrong folder. The letter is chosen by the provider, which is
            where "the first free one from Z downward" is unit tested; this
            passes it to New-SmbMapping and nothing more.
 
            THE LETTERS IN USE COME FROM [IO.DriveInfo], NOT Get-SmbMapping. The
            question is which letters are free, and a local disk, the WinPE RAM
            disk on X: and somebody else's mapping all answer it; only one of
            the three is an SMB mapping.
 
            THE SmbShare MODULE IS THE MECHANISM BECAUSE IT IS PRESENT IN WinPE.
            The boot image contents table records SmbShare as present and NetTCPIP,
            NetAdapter and DnsClient as absent, so nothing here may reach for
            those - and 'net use' is not needed as a fallback.
 
            IT CARRIES EXACTLY ONE BRANCH, and it is named rather than hidden:
            New-SmbMapping refuses an empty -UserName, so a connection made as
            the caller's own identity - which is what the loopback integration
            test does, and what -AllowAnonymous means - has to omit the two
            parameters rather than pass them empty. The rule that adapters stay
            branch-free exists because they are not unit tested; this one is
            exercised for real by
            tests/integration/SmbContentProvider.Integration.Tests.ps1 against a
            throwaway share, which is the same bargain README section 11 records
            for New-HDTScriptInvoker.
 
            THE PASSWORD IS RECORDED AS '<redacted>'. $Operations is printed
            verbatim in a failure dump, and the deployment account's password
            does not belong in one (tests/helpers/README.md section 4).
 
        .PARAMETER Journal
            The shared cross-service operation journal. When supplied, every
            recorded call is appended to it in addition to $Operations, numbered
            globally across services.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject with the five ISmbService
            ScriptMethods. Note that Get-Member -MemberType Method does NOT list
            a ScriptMethod - use -MemberType Method, ScriptMethod.
 
        .EXAMPLE
            $smb = New-HDTSmbService
            $content = New-HDTSmbContentProvider -Root '\\server\HdtShare' -Credential $credential -SmbService $smb
            $content.Connect()
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Builds a stateless service adapter object; it changes no state.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '',
        Justification = 'The adapter boundary: New-SmbMapping -Password takes a plain string, and the provider above it holds the PSCredential.')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPasswordParams', '',
        Justification = 'The adapter boundary: these are New-SmbMapping parameters, not a credential prompt.')]
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter()]
        [AllowNull()]
        [System.Collections.ArrayList] $Journal
    )

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

    $service = [pscustomobject] @{
        Operations  = [System.Collections.ArrayList]::new()
        Journal     = $Journal
        ServiceName = 'SmbService'
    }

    $service | Add-Member -MemberType ScriptMethod -Name Record -Value {
        param([string] $Operation, [object[]] $Argument)

        [void] $this.Operations.Add([pscustomobject] @{
                Sequence  = $this.Operations.Count + 1
                Operation = $Operation
                Arguments = $Argument
            })

        if ($null -ne $this.Journal) {
            [void] $this.Journal.Add([pscustomobject] @{
                    Sequence  = $this.Journal.Count + 1
                    Service   = $this.ServiceName
                    Operation = $Operation
                    Arguments = $Argument
                })
        }
    }

    $service | Add-Member -MemberType ScriptMethod -Name GetOperationName -Value {
        return , ([string[]] @($this.Operations | ForEach-Object { $_.Operation }))
    }

    $service | Add-Member -MemberType ScriptMethod -Name NewMapping -Value {
        param([string] $RemotePath, [string] $UserName, [string] $Password, [string] $LocalPath)

        $this.Record('NewMapping', @($RemotePath, $UserName, '<redacted>', $LocalPath))

        $argument = @{
            RemotePath = $RemotePath
            LocalPath  = $LocalPath
            Persistent = $false
        }

        # The one branch, and the reason for it is in the help above.
        if (-not [string]::IsNullOrEmpty($UserName)) {
            $argument['UserName'] = $UserName
            $argument['Password'] = $Password
        }

        New-SmbMapping @argument | Out-Null
    }

    # -- the server side ---------------------------------------------------
    #
    # THE OTHER HALF OF SMB, and the half MDT's New Deployment Share wizard
    # uses: a deployment share is a folder that has been PUBLISHED, and
    # DeployRoot is \<server>\<share> derived from that. These three are as
    # dumb as the mapping methods above and for the same reason - they are the
    # part no unit test can reach.

    $service | Add-Member -MemberType ScriptMethod -Name NewShare -Value {
        param([string] $Path, [string] $Name, [string] $Description)

        $this.Record('NewShare', @($Path, $Name, $Description))

        New-SmbShare -Name $Name -Path $Path -Description $Description | Out-Null
    }

    $service | Add-Member -MemberType ScriptMethod -Name GetShare -Value {
        param([string] $Name)

        $this.Record('GetShare', @($Name))

        # A NAME THAT IS NOT THERE IS AN ERROR FROM Get-SmbShare, not an empty
        # answer, and "is this name taken" is a question with a false answer.
        $found = Get-SmbShare -Name $Name -ErrorAction SilentlyContinue

        return [bool] ($null -ne $found)
    }

    $service | Add-Member -MemberType ScriptMethod -Name GrantShareAccess -Value {
        param([string] $Name, [string] $Account, [string] $Right)

        $this.Record('GrantShareAccess', @($Name, $Account, $Right))

        Grant-SmbShareAccess -Name $Name -AccountName $Account -AccessRight $Right -Force | Out-Null
    }

    $service | Add-Member -MemberType ScriptMethod -Name RemoveMapping -Value {
        param([string] $RemotePath)

        $this.Record('RemoveMapping', @($RemotePath))

        Remove-SmbMapping -RemotePath $RemotePath -Force | Out-Null
    }

    $service | Add-Member -MemberType ScriptMethod -Name GetUsedDriveLetter -Value {
        $this.Record('GetUsedDriveLetter', @())

        $letter = @([System.IO.DriveInfo]::GetDrives() |
                ForEach-Object { [string] $_.Name.Substring(0, 1).ToUpperInvariant() })

        return , ([string[]] $letter)
    }

    $service | Add-Member -MemberType ScriptMethod -Name GetConnection -Value {
        param([string] $ServerName)

        $this.Record('GetConnection', @($ServerName))

        $row = @(Get-SmbConnection -ServerName $ServerName -ErrorAction SilentlyContinue |
                ForEach-Object {
                    [pscustomobject] @{
                        ServerName = [string] $_.ServerName
                        ShareName  = [string] $_.ShareName
                        UserName   = [string] $_.UserName
                        Dialect    = [string] $_.Dialect
                        Encrypted  = [bool] $_.Encrypted
                        Signed     = [bool] $_.Signed
                    }
                })

        # The unary comma is mandatory: a ScriptMethod collapses a single-element
        # array to a scalar without it (tests/helpers/README.md F3), and one
        # connection is the normal case.
        return , ([object[]] $row)
    }

    $service | Add-Member -MemberType ScriptMethod -Name GetClientConfiguration -Value {
        $this.Record('GetClientConfiguration', @())

        $configuration = Get-SmbClientConfiguration

        return [pscustomobject] @{
            EnableInsecureGuestLogons = [bool] $configuration.EnableInsecureGuestLogons
            RequireSecuritySignature  = [bool] $configuration.RequireSecuritySignature
        }
    }

    return $service
}