Public/ConvertTo-HDTDiskLayout.ps1

function ConvertTo-HDTDiskLayout {
    <#
        .SYNOPSIS
            Turns an authored partition table into the layout the planner reads.

        .DESCRIPTION
            TWO NAMED LAYOUTS WAS THE WHOLE VOCABULARY. DESIGN 9.1 offers
            uefi-standard and bios-standard and a DiskPartition step names one -
            so a share that wants a data volume beside Windows, which is the
            commonest thing an administrator does with MDT's Format and
            Partition Disk, could not say it at all.

            THIS IS THE HALF THAT DOES NO ARITHMETIC. It maps what somebody
            wrote - a name, a type, '260MB' or '60%', a filesystem - onto the
            row shape Get-HDTDiskLayout produces, so New-HDTDiskLayoutPlan works
            unchanged and every byte-count decision stays in the one place that
            is already tested against real disk sizes.

            A PERCENTAGE STAYS A PERCENTAGE. 60% of what depends on the machine
            this runs on; turning it into bytes here would bake the authoring
            host's idea of a disk into the document. The row carries
            PercentOfRemainder and the planner resolves it against the disk in
            front of it.

            THE GUIDs ARE NOT THE AUTHOR'S PROBLEM. Nobody should type
            c12a7328-f81f-11d2-ba4b-00a0c93ec93b into a task sequence to get an
            EFI partition - they type EFI, and this knows what that means,
            including that an ESP is CREATED as basic data so it can be given a
            letter and formatted, and retyped afterwards (DESIGN 9.1).

            IT REFUSES RATHER THAN GUESSES. Two rows claiming the remainder,
            percentages adding past 100, an EFI partition on an MBR disk, a size
            it cannot read: each is a disk laid out by accident, and this
            subsystem exists to prevent exactly that.

        .PARAMETER Partition
            The authored rows, in on-disk order. Each carries name, type and
            size; filesystem, label and variable are optional.

        .PARAMETER Style
            GPT or MBR.

        .PARAMETER Name
            What to call the layout in messages. Defaults to 'authored'.

        .INPUTS
            None. This command does not accept pipeline input.

        .OUTPUTS
            System.Management.Automation.PSCustomObject - Name, PartitionStyle,
            ReservedSizeByte, AlignmentSizeByte and Partition, the same shape
            Get-HDTDiskLayout returns.

        .EXAMPLE
            ConvertTo-HDTDiskLayout -Style GPT -Partition @(
                @{ name = 'System'; type = 'EFI'; size = '260MB' }
                @{ name = 'Windows'; type = 'Primary'; size = '60%' }
                @{ name = 'Data'; type = 'Primary'; size = 'remainder' })

        .EXAMPLE
            $layout = ConvertTo-HDTDiskLayout -Style GPT -Partition $step.partition
            New-HDTDiskLayoutPlan -Layout $layout -DiskSizeByte $disk.SizeByte
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyCollection()]
        [object[]] $Partition,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateSet('GPT', 'MBR')]
        [string] $Style,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $Name = 'authored'
    )

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

    # THE SAME GUIDs Get-HDTDiskLayout USES. They are repeated here rather than
    # shared because the named layouts are DATA in that command and these are
    # the mapping from a word an author types; a shared constant would couple
    # two lists that answer different questions.
    $basicDataType = '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}'
    $espType = '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}'
    $recoveryType = '{de94bba4-06d1-4d40-a16a-bfd50179d6ac}'

    # THE SAME ALLOWANCES, and DESIGN 9.1's reason: Initialize-Disk creates the
    # 16 MB MSR itself, so it is subtracted and never planned. An authored
    # layout gets that for free rather than asking an author to know it.
    $reservedSizeByte = 16777216
    $alignmentSizeByte = 1048576

    if (@($Partition).Count -eq 0) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Name `
                    -Message 'a partition table with no rows in it is not a layout. Name at least one partition, or use a named layout.'))
    }

    $row = New-Object -TypeName System.Collections.ArrayList
    $remainderCount = 0
    $percentTotal = 0
    $bootableCount = 0
    $index = 0

    foreach ($current in @($Partition)) {
        $index++

        # A hashtable and a PSCustomObject both authored the same way, because
        # YAML gives one and a caller building rows by hand gives the other.
        $read = {
            param([string] $Key)

            if ($current -is [System.Collections.IDictionary]) {
                if ($current.Contains($Key)) { return [string] $current[$Key] }
                return ''
            }

            if ($null -eq $current.PSObject.Properties[$Key]) { return '' }
            return [string] $current.$Key
        }

        $label = & $read 'name'
        $type = & $read 'type'
        $size = & $read 'size'
        $fileSystem = & $read 'filesystem'
        $variable = & $read 'variable'
        $quickFormatText = & $read 'quickFormat'
        $bootableText = & $read 'bootable'

        $locator = 'partition {0}' -f $index
        if (-not [string]::IsNullOrWhiteSpace($label)) { $locator = "partition '{0}'" -f $label }

        # -- the type ---------------------------------------------------------

        $gptType = ''
        $createGptType = ''
        $defaultFileSystem = 'NTFS'

        switch ($type) {
            'EFI' {
                if ($Style -eq 'MBR') {
                    $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $label `
                                -Message ("{0} is an EFI partition, and this layout is MBR. An EFI System Partition only exists on a GPT disk - change the style to GPT, or make it a Primary partition." -f $locator)))
                }

                $gptType = $espType

                # CREATED AS BASIC DATA, THEN RETYPED. An ESP cannot be given a
                # drive letter and formatted while it carries the ESP type, and
                # a partition that cannot be formatted cannot be a boot volume.
                $createGptType = $basicDataType
                $defaultFileSystem = 'FAT32'
            }
            'Recovery' {
                if ($Style -eq 'GPT') { $gptType = $recoveryType }
            }
            'Primary' { }
            'Logical' { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $type `
                        -Message ("{0} is type 'Logical'. MDT's dialog offers it, and this engine has never created an extended container to put one in - so accepting the word and quietly making a Primary would produce a disk that does not match the document." -f $locator))) }
            'Extended' { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $type `
                        -Message ("{0} is type 'Extended'. MDT's dialog offers it; this engine creates basic partitions on GPT or MBR and makes no extended container." -f $locator))) }
            default {
                $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $type `
                            -Message ("{0} has type '{1}', which is not one this engine creates. Use EFI, Primary or Recovery." -f $locator, $type)))
            }
        }

        if ([string]::IsNullOrWhiteSpace($fileSystem)) { $fileSystem = $defaultFileSystem }

        # -- the size ---------------------------------------------------------

        $sizeByte = [long] 0
        $useMaximum = $false
        $percent = 0

        $sizeText = ([string] $size).Trim()

        if ($sizeText -match '^(remainder|\*)$') {
            $useMaximum = $true
            $remainderCount++
        } elseif ($sizeText -match '^([0-9]+)\s*%$') {
            $percent = [int] $Matches[1]
            $percentTotal += $percent
        } elseif ($sizeText -match '^([0-9]+)\s*(KB|MB|GB|TB)$') {
            $unit = @{ KB = 1KB; MB = 1MB; GB = 1GB; TB = 1TB }[$Matches[2].ToUpperInvariant()]
            $sizeByte = [long] $Matches[1] * [long] $unit
        } elseif ($sizeText -match '^[0-9]+$') {
            $sizeByte = [long] $sizeText
        } else {
            $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $size `
                        -Message ("{0} has size '{1}', which is not a size this engine reads. Use bytes, 260MB, 1GB, a percentage like 60%, or remainder." -f $locator, $size)))
        }

        $quickFormat = $true
        if ($quickFormatText -match '^(false|0|no)$') { $quickFormat = $false }

        # UNSET IS NOT False. A row that says nothing about booting takes the
        # positional default; a row that says bootable: false is refusing it,
        # and the two have to be told apart or an explicit bootable on the
        # second row could never win.
        $isActive = ($Style -eq 'MBR' -and $index -eq 1)

        if ($bootableText -match '^(true|1|yes)$') {
            $isActive = $true
            $bootableCount++
        } elseif ($bootableText -match '^(false|0|no)$') {
            $isActive = $false
        }

        [void] $row.Add([pscustomobject] @{
                Role              = $label
                SizeByte          = $sizeByte

                # TWO FLAGS, BECAUSE THEY ARE TWO QUESTIONS. UseMaximumSize is
                # where leftover slack LANDS when the partition is created -
                # uefi-standard puts it on Recovery. TakesRemainder is which row
                # gets the space nothing else claimed, which in the named
                # layouts is Windows and here is whichever row said 'remainder'.
                # Collapsing them would give an authored layout Recovery's flag
                # and Windows' meaning.
                UseMaximumSize    = $useMaximum
                TakesRemainder    = $useMaximum
                PercentOfRemainder = $percent
                FileSystem        = $fileSystem
                Label             = $label
                DriveLetter       = ''
                GptType           = $gptType
                CreateGptType     = $createGptType

                # MBR BOOTS FROM AN ACTIVE PARTITION and GPT has no such
                # concept. The default is the first row - which is MDT's default
                # too, "True if 1st partition" - and an explicit bootable on any
                # row overrides it, because a System partition is not always
                # written first.
                IsActive          = $isActive
                Variable          = $variable

                # MDT's QUICKFORMAT, default True. A full format is hours on a
                # large disk and the only way to know a suspect one can hold
                # what is written to it; refusing to carry the choice would mean
                # HDT could not express a sequence somebody already runs.
                QuickFormat       = $quickFormat
            })
    }

    if ($remainderCount -gt 1) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Name `
                    -Message ("{0} partitions claim the remainder of the disk. Only one can have it, and which one would otherwise be an accident of order." -f $remainderCount)))
    }

    if ($bootableCount -gt 1) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Name `
                    -Message ("{0} partitions are declared bootable. The firmware picks one, and which one is then not the author's decision." -f $bootableCount)))
    }

    if ($percentTotal -gt 100) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -TargetObject $Name `
                    -Message ("the percentages in this layout add up to {0}%, which does not fit on a disk. Reduce them, or give the last partition 'remainder' instead." -f $percentTotal)))
    }

    return [pscustomobject] @{
        Name              = $Name
        PartitionStyle    = $Style
        ReservedSizeByte  = $reservedSizeByte
        AlignmentSizeByte = $alignmentSizeByte
        Partition         = [pscustomobject[]] @($row)
    }
}