Private/Assert-HDTWorkspaceDocument.ps1
|
function Assert-HDTWorkspaceDocument { <# .SYNOPSIS Validates a parsed workspace.yaml against the authoring rules. .DESCRIPTION The engine's own validator, and the one that actually runs in WinPE: Test-Json does not exist under Windows PowerShell 5.1, so schemas/workspace.schema.json is a gate for the console, editors and CI while this is the gate for a boot image build and a deployment. It throws on the first violation and returns nothing otherwise. Every failure is a terminating HDTConfigurationError built by New-HDTErrorRecord, so it names the file, carries the file as its TargetObject, and says which key is wrong - "fail fast and point at the file". The authoring rules, in the order they are checked: document not empty; a mapping; only the seven known keys; schemaVersion present, an integer, not newer than this engine (delegated to Test-HDTSchemaVersion) identity id present, [A-Za-z0-9-_]{1,64}; name present deployRoot present, not whitespace, no '..' logLevel one of Error, Warning, Info, Debug credential a mapping; username only; NO password bootImage a mapping; only the seven known keys; architecture amd64 or arm64; scratchSpaceMB 32..1024; optionalComponents a list of ^WinPE-[A-Za-z0-9-]+$ with no case-insensitive duplicate; extraContent rows carrying source and destination, destination rooted and free of '..' A PASSWORD KEY HERE IS AN ERROR, NOT AN OVERSIGHT. workspace.yaml is the file an administrator hand-edits and commits, so a share password in it is a share password in git. The refusal names Set-HDTShareCredential and says where the secret does live - Control\share-credential.json, written by that command. The message deliberately does not echo the value back: a validator that quotes the secret it is refusing has just written it to the log. deployRoot HAS THREE LEGAL FORMS and the third is not decoration: \\server\HdtShare UNC, reached with the Smb provider C:\HDTLab\Share rooted local; correct on a build host, wrong inside a boot image \Share VOLUME-RELATIVE - the volume is discovered at boot, and it is the only form a Local boot image may carry, because a lab test recorded WinPE handing the content disk C: while the RAM disk was X: Nothing here resolves the third form. Resolve-HDTDeployRoot (05-03) does that inside WinPE, by probing every ready drive for the workspace marker. This function only has to ACCEPT it. .PARAMETER Document The parsed document, as returned by ConvertFrom-HDTYaml. $null is accepted and reported as an empty file rather than crashing. .PARAMETER Path The file the document came from. Used for the message and the TargetObject; this function reads nothing. .OUTPUTS None. It throws or it returns nothing. .EXAMPLE Assert-HDTWorkspaceDocument -Document (ConvertFrom-HDTYaml -Yaml $text -Path $path) -Path $path .NOTES The locator in a message is the KEY or the extraContent INDEX, not a line number: the YAML parser does not carry line information onto the object graph it returns. Only ConvertFrom-HDTYaml, which still holds the parser's own exception, can name a line. #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory = $true, Position = 0)] [AllowNull()] [object] $Document, [Parameter(Mandatory = $true, Position = 1)] [ValidateNotNullOrEmpty()] [string] $Path ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $supportedSchemaVersion = 1 # THE REQUIRED KEYS LIVE HERE, IN ONE PLACE, AND THEY ARE COMPARED AGAINST # schemas/workspace.schema.json's own "required" array by # tests/contract/WorkspaceSchema.Contract.Tests.ps1, which reads this very # variable out of this file. A key added to one and forgotten in the other # turns that contract red. Do not inline this list into the loop below. $requiredRootKey = @('schemaVersion', 'id', 'name') $allowedRootKey = @('schemaVersion', 'id', 'name', 'deployRoot', 'logLevel', 'credential', 'bootImage', 'folders') $allowedCredentialKey = @('username') $allowedFolderKey = @('taskSequences', 'operatingSystems', 'applications') $allowedBootImageKey = @('name', 'architecture', 'language', 'scratchSpaceMB', 'optionalComponents', 'extraContent', 'drivers', 'unattend', 'background', 'timeZone', 'promptForKey', 'rootCertificates', 'clientCertificate', 'entryCommand', 'startCommand', 'skip') $allowedSkipKey = @('welcome', 'staticIp', 'deployRoot', 'credential') $allowedExtraContentKey = @('source', 'destination') $allowedArchitecture = @('amd64', 'arm64') # ASKED FOR RATHER THAN REPEATED. The console offers these four in a list, # and a second hand-written copy here is how the validator and the list come # to disagree about what a share may log at. $allowedLogLevel = @(Get-HDTWorkspaceLogLevel) $minimumScratchSpaceMB = 32 $maximumScratchSpaceMB = 1024 # -- the document --------------------------------------------------------- if ($null -eq $Document) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'the file is empty. A workspace document must declare schemaVersion, id, name and deployRoot.')) } if (-not ($Document -is [System.Collections.IDictionary])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("the document must be a mapping with schemaVersion, id, name and deployRoot keys, but it is a {0}." -f $Document.GetType().Name))) } foreach ($key in @($Document.Keys)) { if ($allowedRootKey -notcontains [string] $key) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("'{0}' is not a key a workspace document may declare. The allowed keys are {1}." -f $key, ($allowedRootKey -join ', ')))) } } foreach ($key in $requiredRootKey) { if (-not $Document.Contains($key)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("{0} is missing. A workspace document declares {1}." -f $key, ($requiredRootKey -join ', ')))) } } $schemaVersion = $Document['schemaVersion'] if (-not (($schemaVersion -is [int]) -or ($schemaVersion -is [long]))) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("schemaVersion must be an integer, but it is '{0}'." -f $schemaVersion))) } $supported = $false try { $supported = Test-HDTSchemaVersion -SchemaVersion ([int] $schemaVersion) -Supported $supportedSchemaVersion } catch { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("schemaVersion {0} is not a valid schema version. It must be 1 or greater." -f $schemaVersion))) } if (-not $supported) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("schemaVersion {0} is newer than this engine understands (schemaVersion {1}). Upgrade the engine rather than the workspace." -f $schemaVersion, $supportedSchemaVersion))) } # -- the identity --------------------------------------------------------- $id = [string] $Document['id'] if ([string]::IsNullOrWhiteSpace($id)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'id is missing. The id names this share in a log line, in a boot image manifest and on a client that connected to it.')) } if ($id.Length -gt 64) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("id '{0}' is {1} characters. A workspace id is at most 64." -f $id, $id.Length))) } if ($id -notmatch '^[A-Za-z0-9_-]+$') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("id '{0}' is not a legal workspace id. It is carried into the boot image and written into log and artifact names, so it may hold only letters, digits, hyphen and underscore." -f $id))) } $name = [string] $Document['name'] if ([string]::IsNullOrWhiteSpace($name)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'name is missing. The name is what an administrator reads in the console and in a log line.')) } # -- deployRoot ----------------------------------------------------------- # NO SHARE IS NO SHARE, however it was spelled. An omitted key, an empty # value and a whitespace value all mean the same thing here: the image is # built without a deployment root, reaches the Welcome screen with an empty # box, and asks the technician for one. # # An earlier version refused the empty spelling on the grounds that a # written-but-blank key looks like a failed template substitution rather # than a decision. That is true, and it is still not worth two documents # that mean the same thing behaving differently - the surprise costs more # than the typo it caught, and an image that asks for its share is not a # broken image. $deployRoot = '' if ($Document.Contains('deployRoot')) { $deployRoot = [string] $Document['deployRoot'] } if ([string]::IsNullOrWhiteSpace($deployRoot)) { $deployRoot = '' } if ($deployRoot -like '*..*') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("deployRoot '{0}' contains '..'. A deployment root is named outright, not walked up to." -f $deployRoot))) } # -- logLevel ------------------------------------------------------------- if ($Document.Contains('logLevel')) { $logLevel = [string] $Document['logLevel'] if ($allowedLogLevel -notcontains $logLevel) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("logLevel '{0}' is not a level HDT logs at. The levels are {1}." -f $logLevel, ($allowedLogLevel -join ', ')))) } } # -- the credential ------------------------------------------------------- if ($Document.Contains('credential')) { $credential = $Document['credential'] if (-not ($credential -is [System.Collections.IDictionary])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'credential must be a mapping carrying a username.')) } foreach ($key in @($credential.Keys)) { $keyName = [string] $key # Checked before the generic unknown-key message, because THIS one is # the sentence that has to be read: it says where the secret goes. if ($keyName -eq 'password') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message "credential declares a password. The share password does not belong in this file: this is the document an administrator hand-edits and commits. Remove the key and run Set-HDTShareCredential, which writes the secret to Control\share-credential.json instead.")) } if ($allowedCredentialKey -notcontains $keyName) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("'{0}' is not a key credential may declare. The allowed keys are {1}; the secret is written by Set-HDTShareCredential." -f $keyName, ($allowedCredentialKey -join ', ')))) } } $username = '' if ($credential.Contains('username')) { $username = [string] $credential['username'] } if ([string]::IsNullOrWhiteSpace($username)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'credential is declared with no username. Name the deployment account, or remove the credential block entirely.')) } } # -- the console's folders ------------------------------------------------ # # NOTHING THE ENGINE READS. A deployment does not care which folder the # console draws a sequence under, and this block exists only so a folder # with nothing in it yet survives the tree being rebuilt from the documents. # It is still checked here, because a document that half-parses is a window # that half-draws. if ($Document.Contains('folders')) { $folders = $Document['folders'] if (-not ($folders -is [System.Collections.IDictionary])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ('folders must be a mapping carrying a list per category. The categories are {0}.' -f ($allowedFolderKey -join ', ')))) } foreach ($key in @($folders.Keys)) { $keyName = [string] $key if ($allowedFolderKey -notcontains $keyName) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("'{0}' is not a category folders may declare. The categories are {1}." -f $keyName, ($allowedFolderKey -join ', ')))) } foreach ($current in @($folders[$keyName])) { $folder = [string] $current # A FOLDER IS DRAWN FROM ITS OWN TEXT, so a leading or trailing # separator produces a nameless level in the tree and a doubled # one produces two - the same rule Set-HDTTaskSequenceProperty # applies to the key on a document. if ([string]::IsNullOrWhiteSpace($folder) -or $folder -match '^\\|\\$|\\\\' -or $folder -match '/') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("'{0}' is not a folder path the console can draw. Separate levels with a single backslash - 'Clients\Laptops' - with nothing before the first or after the last." -f $folder))) } } } } # -- the boot image ------------------------------------------------------- if (-not $Document.Contains('bootImage')) { return } $bootImage = $Document['bootImage'] if (-not ($bootImage -is [System.Collections.IDictionary])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage must be a mapping. Its keys are the boot image build settings.')) } foreach ($key in @($bootImage.Keys)) { if ($allowedBootImageKey -notcontains [string] $key) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: '{0}' is not a key it may declare. The allowed keys are {1}." -f $key, ($allowedBootImageKey -join ', ')))) } } if ($bootImage.Contains('name')) { $imageName = [string] $bootImage['name'] if ($imageName -notmatch '^[A-Za-z0-9][A-Za-z0-9_.-]*$') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: name '{0}' is not a legal artifact name. It becomes the base name of the .wim and the .iso under Boot\, so it must start with a letter or a digit and hold only letters, digits, underscore, dot and hyphen." -f $imageName))) } } if ($bootImage.Contains('architecture')) { $architecture = [string] $bootImage['architecture'] if ($allowedArchitecture -notcontains $architecture) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: architecture '{0}' is not an architecture the ADK ships a WinPE for. The architectures are {1}." -f $architecture, ($allowedArchitecture -join ', ')))) } } if ($bootImage.Contains('language')) { $language = [string] $bootImage['language'] if ([string]::IsNullOrWhiteSpace($language)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: language is empty. It names the ADK language folder, for example en-us.')) } } if ($bootImage.Contains('scratchSpaceMB')) { $scratchSpace = $bootImage['scratchSpaceMB'] if (-not (($scratchSpace -is [int]) -or ($scratchSpace -is [long]))) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: scratchSpaceMB must be an integer, but it is '{0}'." -f $scratchSpace))) } if (([int] $scratchSpace -lt $minimumScratchSpaceMB) -or ([int] $scratchSpace -gt $maximumScratchSpaceMB)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: scratchSpaceMB {0} is outside the range DISM accepts. It must be between {1} and {2}." -f $scratchSpace, $minimumScratchSpaceMB, $maximumScratchSpaceMB))) } } # -- the WinPE answer file ------------------------------------------------ # # A PATH IS A PATH HERE, exactly as it is for extraContent's SOURCE: # relative to the share, or rooted on the build host. An earlier version # refused a rooted one on the grounds that the answer file was share # content - which was a rule this file invented and nothing else in the # workspace holds. The file an administrator browses to is wherever they # keep it. # # WHAT IS INSIDE THE FILE IS NOT CHECKED HERE. wpeinit accepts a fixed set # of settings and ignores the rest; deciding which of them an administrator # meant is not a YAML validator's job. Whether the file EXISTS is # Update-HDTBootImage's business, and it refuses before it mounts. if ($bootImage.Contains('unattend')) { $unattend = $bootImage['unattend'] if ($null -ne $unattend -and -not ($unattend -is [string])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: unattend must be a path to a WinPE answer file, but it is '{0}'." -f $unattend))) } if ([string]::IsNullOrWhiteSpace([string] $unattend)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: unattend is empty. Remove the key to build the image with no answer file.')) } } # -- the WinPE background -------------------------------------------------- # # JUDGED THE WAY THE ANSWER FILE IS - a path is a path - plus the one rule # that is not about paths: WinPE reads \Windows\System32\winpe.jpg and # nothing else, so a .png named here is a file the image carries and never # shows. A build that succeeds and a background that does not appear is the # worst way to learn that. if ($bootImage.Contains('background')) { $background = $bootImage['background'] if ($null -ne $background -and -not ($background -is [string])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: background must be a path to a .jpg, but it is '{0}'." -f $background))) } if ([string]::IsNullOrWhiteSpace([string] $background)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: background is empty. Remove the key to use the background WinPE ships.')) } if (([string] $background) -notmatch '\.(jpg|jpeg)$') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: background '{0}' is not a .jpg. WinPE's background is \Windows\System32\winpe.jpg and it must be a JPEG." -f $background))) } } # -- the certificates ------------------------------------------------------ # # THE ONE RULE THAT IS NOT ABOUT PATHS IS WHICH STORE THE FILE IS FOR. A # .cer is a public certificate and belongs in Root; a .pfx carries a private # key and belongs in My. Swapping them produces a build that succeeds and a # machine that either trusts a private key as a certificate authority or # tries to authenticate with a certificate it cannot prove it owns - and the # second one looks exactly like a broken share. # # WHETHER THE FILE EXISTS IS Update-HDTBootImage'S BUSINESS, as it is for # the answer file and the background: it refuses before it mounts. if ($bootImage.Contains('rootCertificates')) { $rootCertificate = @($bootImage['rootCertificates']) if (@($rootCertificate).Count -eq 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: rootCertificates is empty. Remove the key to build the image with no certificate authorities of your own.')) } foreach ($current in $rootCertificate) { if ([string]::IsNullOrWhiteSpace([string] $current)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: rootCertificates has an empty entry. Each one is a path to a certificate file.')) } if (([string] $current) -match '\.pfx$') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: rootCertificates names '{0}', which is a .pfx. A .pfx carries a private key and this list is imported into the trusted root store of every machine that boots the image. Name the .cer here and the .pfx in clientCertificate." -f $current))) } } } if ($bootImage.Contains('clientCertificate')) { $clientCertificate = $bootImage['clientCertificate'] if ($null -ne $clientCertificate -and -not ($clientCertificate -is [string])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: clientCertificate must be a path to a .pfx, but it is '{0}'." -f $clientCertificate))) } if ([string]::IsNullOrWhiteSpace([string] $clientCertificate)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: clientCertificate is empty. Remove the key to build the image with no machine certificate.')) } if (([string] $clientCertificate) -notmatch '\.pfx$') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: clientCertificate '{0}' is not a .pfx. This certificate is what the machine authenticates WITH, so it needs its private key, and a .cer has none - an 802.1X port stays shut for a machine holding one." -f $clientCertificate))) } } # -- the entry command ---------------------------------------------------- # # It is written into startnet.cmd verbatim (DESIGN 5.1), so the two things # checked here are the two that make the written file mean something other # than what the document says: nothing to run, and more than one thing to # run. What the command DOES is not this validator's business - a diagnostic # image exists to run something other than the deployment payload. if ($bootImage.Contains('entryCommand')) { $entryCommand = $bootImage['entryCommand'] if ($null -ne $entryCommand -and -not ($entryCommand -is [string])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: entryCommand must be a command to run, but it is '{0}'." -f $entryCommand))) } if ([string]::IsNullOrWhiteSpace([string] $entryCommand)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: entryCommand must be a command to run. Remove the key to launch the deployment payload.')) } if (([string] $entryCommand).IndexOfAny([char[]] @("`r", "`n")) -ge 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: entryCommand must be one command on one line. A line break here becomes a second command inside startnet.cmd that nobody reading this document would see.')) } } # -- the start commands --------------------------------------------------- # # WHAT MAKES A COPIED TOOL A RUNNING TOOL. extraContent puts BGInfo or a VNC # server inside the image and nothing starts it; entryCommand is one slot, # already holding the deployment payload, and that payload does not return. # So these run between the two, after wpeinit has brought the network up. # # Each one is written into startnet.cmd verbatim, which is why the two things # checked are the two that make the written file mean something other than # what this document says: nothing to run, and more than one thing to run on # a line that claims to be one. if ($bootImage.Contains('startCommand')) { $startCommand = $bootImage['startCommand'] # An explicit empty list is legal and means "run nothing extra", which is # the same build as omitting the key. if ($null -ne $startCommand -and -not ($startCommand -is [System.Collections.IList])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: startCommand must be a list of commands, run in the order they are written. One command on its own is still a list of one.')) } $position = 0 foreach ($current in @($startCommand)) { $position++ if ($null -ne $current -and -not ($current -is [string])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: startCommand {0} must be a command to run, but it is '{1}'." -f $position, $current))) } if ([string]::IsNullOrWhiteSpace([string] $current)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: startCommand {0} is empty. Name the command to run, or remove the entry." -f $position))) } if (([string] $current).IndexOfAny([char[]] @("`r", "`n")) -ge 0) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: startCommand {0} must be one command on one line. A line break here becomes a second command inside startnet.cmd that nobody reading this document would see." -f $position))) } } } # -- the skip block ------------------------------------------------------- # # MDT's Skip* properties for the Welcome screen, which live here rather than # in rules.yaml because that screen runs BEFORE the share is reachable # (.planning/WPF-FIRST.md, W2). # # AN OMITTED KEY IS NOT false, and this validator must not turn it into one: # it checks shape and says nothing about defaults. Get-HDTWizardSkip is the # only place "the workspace said nothing" becomes a decision. # # A NON-BOOLEAN IS REFUSED RATHER THAN COERCED. 'yes', 'no' and '' are all # truthy or falsy in PowerShell in ways nobody reading the YAML would # predict, and the wrong guess here is a wizard that does not appear on a # machine somebody is standing in front of. if ($bootImage.Contains('skip')) { $skip = $bootImage['skip'] if ($null -ne $skip -and -not ($skip -is [System.Collections.IDictionary])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: skip must be a mapping of {0}, but it is a {1}." -f ($allowedSkipKey -join ', '), $skip.GetType().Name))) } if ($null -ne $skip) { foreach ($key in @($skip.Keys)) { if ($allowedSkipKey -notcontains [string] $key) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: skip: '{0}' is not a rule it may declare. The allowed rules are {1}." -f $key, ($allowedSkipKey -join ', ')))) } $value = $skip[$key] if ($null -ne $value -and -not ($value -is [bool])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: skip: {0} must be true or false, but it is '{1}'. Remove the rule to leave it unstated." -f $key, $value))) } } } } if ($bootImage.Contains('drivers')) { $driverGroup = [string] $bootImage['drivers'] if ([string]::IsNullOrWhiteSpace($driverGroup)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: drivers is empty. Name a driver group under Drivers\, or remove the key.')) } } # -- the optional components ---------------------------------------------- if ($bootImage.Contains('optionalComponents')) { $component = $bootImage['optionalComponents'] # An explicit empty list is LEGAL and means "the required set and nothing # else" - a different instruction from omitting the key, which takes the # DESIGN 5.1 defaults. if ($null -ne $component -and -not ($component -is [System.Collections.IList])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: optionalComponents must be a list of WinPE optional component names.')) } $seen = New-Object -TypeName System.Collections.ArrayList foreach ($current in @($component)) { $componentName = [string] $current if ($componentName -notmatch '^WinPE-[A-Za-z0-9-]+$') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: '{0}' is not a WinPE optional component name. A component is named as the ADK names its cab, for example WinPE-WMI or WinPE-NetFx - note the lowercase x." -f $componentName))) } $comparable = $componentName.ToLowerInvariant() if ($seen -contains $comparable) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("bootImage: optionalComponents declares '{0}' twice. Component names are compared without regard to case, and applying a cab twice is not a thing to ask for by accident." -f $componentName))) } [void] $seen.Add($comparable) } } # -- the extra content ---------------------------------------------------- if (-not $bootImage.Contains('extraContent')) { return } $extraContent = $bootImage['extraContent'] if ($null -ne $extraContent -and -not ($extraContent -is [System.Collections.IList])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message 'bootImage: extraContent must be a list of source and destination pairs.')) } $position = 0 foreach ($current in @($extraContent)) { $position++ $locator = 'bootImage: extraContent {0}' -f $position if (-not ($current -is [System.Collections.IDictionary])) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("{0}: an entry must be a mapping with a source and a destination." -f $locator))) } foreach ($key in @($current.Keys)) { if ($allowedExtraContentKey -notcontains [string] $key) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("{0}: '{1}' is not a key an entry may declare. The allowed keys are {2}." -f $locator, $key, ($allowedExtraContentKey -join ', ')))) } } $source = '' if ($current.Contains('source')) { $source = [string] $current['source'] } if ([string]::IsNullOrWhiteSpace($source)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("{0}: source is missing. It names what to copy, relative to the workspace root unless it is rooted." -f $locator))) } $destination = '' if ($current.Contains('destination')) { $destination = [string] $current['destination'] } if ([string]::IsNullOrWhiteSpace($destination)) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("{0}: destination is missing. It names where inside the boot image the content lands, for example \HDT\Modules\MyVendorTools." -f $locator))) } if (-not $destination.StartsWith('\')) { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("{0}: destination '{1}' does not start with a separator, and a destination is a path inside the image - it is rooted at the image, not at the machine building it." -f $locator, $destination))) } if ($destination -like '*..*') { $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -Path $Path ` -Message ("{0}: destination '{1}' contains '..', which escapes the image. Name a path under the image root." -f $locator, $destination))) } } } |